abstracttui_graph/lib.rs
1//! # abstracttui-graph
2//!
3//! Graph auto-layout for [AbstractTUI](https://docs.rs/abstracttui):
4//! the layout half of the diagram lane (backlog 0440), an ADR-0004
5//! sibling crate built on core's public API only.
6//!
7//! ## The one contract
8//!
9//! Every layout pass is **`GraphDesc -> Layout`**: nodes with cell
10//! sizes and edges in, per-node positions/ranks, per-edge waypoint
11//! polylines, a bounding box and honesty markers out. Consumers select
12//! the ALGORITHM, never a different data contract:
13//!
14//! - [`layered`] — sugiyama-lite, the workflow/DAG path (v1).
15//! - [`force`] — bounded seeded force placement, the knowledge-graph
16//! path (v1.5).
17//! - [`grid`] — labeled near-square placement, the honest fallback.
18//!
19//! ## Honesty markers
20//!
21//! A layout never lies about degradation: cycle-broken edges are
22//! marked ([`EdgeLayout::broken`], [`Layout::broken_edges`]), and
23//! [`Layout::fallback`] names every degradation that occurred (node
24//! cap exceeded, duplicate node ids dropped, unresolvable edges
25//! skipped, grid placement). `None` means the requested algorithm ran
26//! cleanly.
27//!
28//! ## Determinism
29//!
30//! Same graph + same options = identical `Layout`, golden-test-pinned.
31//! No map-iteration order leaks into results, every tiebreak is input
32//! order, and float arithmetic sticks to IEEE-exact operations
33//! (`+ - * / sqrt`, no transcendentals), so goldens hold across
34//! platforms.
35//!
36//! ## Bounds
37//!
38//! Everything is bounded: crossing-reduction sweeps
39//! ([`LayeredOpts::sweeps`], default 4), the layered node cap
40//! ([`LayeredOpts::node_cap`], default 512, past which the grid
41//! fallback engages with a label), and the force iteration budget
42//! ([`ForceOpts::budget`], default 256, freezing earlier on settle).
43//! The force pass is an *act*, not an animation: run it on demand,
44//! cache the `Layout`, re-render from the cache (zero idle cost is the
45//! caller's story and the engine's rule).
46//!
47//! ```
48//! use abstracttui_graph::{layered, GraphDesc, LayeredOpts};
49//!
50//! let desc = GraphDesc::new()
51//! .node("fetch", 9, 3)
52//! .node("build", 9, 3)
53//! .node("test", 8, 3)
54//! .edge("fetch", "build")
55//! .edge("build", "test");
56//! let layout = layered(&desc, &LayeredOpts::default());
57//! assert_eq!(layout.node("fetch").unwrap().rank, 0);
58//! assert_eq!(layout.node("test").unwrap().rank, 2);
59//! assert!(layout.fallback.is_none(), "clean run, no degradation");
60//! ```
61
62#![forbid(unsafe_code)]
63#![warn(missing_docs)]
64
65pub mod desc;
66pub mod dump;
67pub mod layout;
68pub mod view;
69
70pub use desc::{Direction, EdgeDesc, GraphDesc, NodeDesc};
71pub use layout::{
72 force, grid, layered, EdgeLayout, ForceOpts, IterationBudget, LayeredOpts, Layout, NodeLayout,
73};
74// The view half (cycle 2): the rendering widget over the core canvas
75// layer — cards, canvas-stroke edges, selection/pan/tooltips.
76pub use view::{GraphAlgo, GraphStyle, GraphView};
77
78// Core geometry types used by the contract, re-exported so consumers
79// (and tests) need not name the engine crate for a Point.
80pub use abstracttui::base::{Point, Rect, Size};