Add structural graph analysis and fold groups
This commit is contained in:
parent
4d1f2fc94f
commit
67924e5b9e
2 changed files with 728 additions and 7 deletions
54
README.md
54
README.md
|
|
@ -15,11 +15,15 @@ The result is a spectrum:
|
||||||
|
|
||||||
## Current Pipeline
|
## Current Pipeline
|
||||||
|
|
||||||
1. Assign ranks from directed edges using a longest-path DAG pass.
|
1. Analyze graph anatomy: weak components, SCCs, bridges, articulation points,
|
||||||
2. Preserve partial hierarchy for cyclic leftovers with local edge pressure.
|
edge roles, degree metrics, and cyclic nodes.
|
||||||
3. Order nodes inside each rank with repeated barycenter sweeps.
|
2. Assign ranks from directed edges using a longest-path DAG pass.
|
||||||
4. Generate rank, order, and edge-direction constraints.
|
3. Preserve partial hierarchy for cyclic leftovers with local edge pressure.
|
||||||
5. Relax the graph with deterministic force iterations.
|
4. Order nodes inside each rank with repeated barycenter sweeps.
|
||||||
|
5. Generate rank, order, and edge-direction constraints.
|
||||||
|
6. Relax the graph with deterministic force iterations.
|
||||||
|
7. Emit fold groups for renderer-side collapse, expansion, highlighting, or
|
||||||
|
protein-ish folding.
|
||||||
|
|
||||||
## Example
|
## Example
|
||||||
|
|
||||||
|
|
@ -68,6 +72,46 @@ For Bevy, map `NodeLayout3d::as_xyz()` directly into `Vec3::new(x, y, z)`.
|
||||||
The crate does not depend on Bevy just to borrow its vector type; the layout
|
The crate does not depend on Bevy just to borrow its vector type; the layout
|
||||||
core stays small and renderer-agnostic.
|
core stays small and renderer-agnostic.
|
||||||
|
|
||||||
|
## Graph Analysis And Folding
|
||||||
|
|
||||||
|
The crate exposes `analyze(&graph)` for layout-independent structure reads:
|
||||||
|
|
||||||
|
- weak components
|
||||||
|
- strongly connected components
|
||||||
|
- articulation points
|
||||||
|
- bridge edges
|
||||||
|
- per-node degree metrics
|
||||||
|
- edge roles
|
||||||
|
- cyclic node flags
|
||||||
|
|
||||||
|
`layout_3d` uses that same analysis to add structural pressure:
|
||||||
|
|
||||||
|
- SCC cohesion folds cycles and tight feedback regions into local bodies.
|
||||||
|
- weak component cohesion keeps broad regions visually legible.
|
||||||
|
- bridge hinges keep connector edges readable between bodies.
|
||||||
|
- centrality anchoring keeps important nodes from drifting into decorative exile.
|
||||||
|
- cycle folding uses depth so feedback does not have to lose every argument
|
||||||
|
against hierarchy.
|
||||||
|
|
||||||
|
The result includes `fold_groups`, which are renderer-facing bodies:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
for group in result.fold_groups {
|
||||||
|
println!(
|
||||||
|
"{:?}: center={:?} radius={} nodes={}",
|
||||||
|
group.kind,
|
||||||
|
group.center,
|
||||||
|
group.radius,
|
||||||
|
group.nodes.len()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Fold groups currently cover weak components, strongly connected components, and
|
||||||
|
cycle bodies. They are meant to be useful handles for Bevy interactions:
|
||||||
|
collapse a component, pulse a cycle, dim a subtree, or unfold a dense region
|
||||||
|
without asking the raw node soup for permission.
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
The first release favors a small deterministic core over a sprawling layout
|
The first release favors a small deterministic core over a sprawling layout
|
||||||
|
|
|
||||||
681
src/lib.rs
681
src/lib.rs
|
|
@ -130,6 +130,16 @@ pub struct Layout3dConfig {
|
||||||
pub horizontal_repulsion: f32,
|
pub horizontal_repulsion: f32,
|
||||||
/// Extra multiplier for vertical repulsion.
|
/// Extra multiplier for vertical repulsion.
|
||||||
pub vertical_repulsion: f32,
|
pub vertical_repulsion: f32,
|
||||||
|
/// Force pulling strongly connected components into foldable bodies.
|
||||||
|
pub component_cohesion_strength: f32,
|
||||||
|
/// Force pulling weakly connected components into broad local regions.
|
||||||
|
pub weak_component_cohesion_strength: f32,
|
||||||
|
/// Force letting bridge edges behave like hinges between folded bodies.
|
||||||
|
pub bridge_hinge_strength: f32,
|
||||||
|
/// Force making cyclic nodes fold into depth instead of fighting hierarchy.
|
||||||
|
pub cycle_fold_strength: f32,
|
||||||
|
/// Force anchoring high-centrality nodes closer to their component center.
|
||||||
|
pub centrality_anchor_strength: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Layout3dConfig {
|
impl Default for Layout3dConfig {
|
||||||
|
|
@ -141,6 +151,11 @@ impl Default for Layout3dConfig {
|
||||||
depth_strength: 0.025,
|
depth_strength: 0.025,
|
||||||
horizontal_repulsion: 1.0,
|
horizontal_repulsion: 1.0,
|
||||||
vertical_repulsion: 0.35,
|
vertical_repulsion: 0.35,
|
||||||
|
component_cohesion_strength: 0.045,
|
||||||
|
weak_component_cohesion_strength: 0.012,
|
||||||
|
bridge_hinge_strength: 0.025,
|
||||||
|
cycle_fold_strength: 0.035,
|
||||||
|
centrality_anchor_strength: 0.02,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -186,6 +201,65 @@ impl NodeLayout3d {
|
||||||
pub struct Layout3d {
|
pub struct Layout3d {
|
||||||
pub nodes: Vec<NodeLayout3d>,
|
pub nodes: Vec<NodeLayout3d>,
|
||||||
pub constraints: ConstraintSet,
|
pub constraints: ConstraintSet,
|
||||||
|
pub analysis: GraphAnalysis,
|
||||||
|
pub fold_groups: Vec<FoldGroup>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Structural analysis used to turn graph anatomy into layout pressure.
|
||||||
|
#[derive(Clone, Debug, Default, PartialEq)]
|
||||||
|
pub struct GraphAnalysis {
|
||||||
|
pub weak_components: Vec<Component>,
|
||||||
|
pub strongly_connected_components: Vec<Component>,
|
||||||
|
pub articulation_points: Vec<NodeId>,
|
||||||
|
pub bridges: Vec<EdgeId>,
|
||||||
|
pub node_metrics: Vec<NodeMetrics>,
|
||||||
|
pub edge_roles: Vec<EdgeRole>,
|
||||||
|
pub node_to_weak_component: Vec<usize>,
|
||||||
|
pub node_to_strong_component: Vec<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub struct Component {
|
||||||
|
pub id: usize,
|
||||||
|
pub nodes: Vec<NodeId>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||||
|
pub struct NodeMetrics {
|
||||||
|
pub in_degree: usize,
|
||||||
|
pub out_degree: usize,
|
||||||
|
pub undirected_degree: usize,
|
||||||
|
pub degree_centrality: f32,
|
||||||
|
pub is_articulation: bool,
|
||||||
|
pub is_cyclic: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
||||||
|
pub enum EdgeRole {
|
||||||
|
Tree,
|
||||||
|
Back,
|
||||||
|
#[default]
|
||||||
|
Cross,
|
||||||
|
Bridge,
|
||||||
|
IntraComponent,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A renderer-facing body that can be folded, collapsed, or highlighted.
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub struct FoldGroup {
|
||||||
|
pub id: usize,
|
||||||
|
pub kind: FoldGroupKind,
|
||||||
|
pub nodes: Vec<NodeId>,
|
||||||
|
pub center: [f32; 3],
|
||||||
|
pub radius: f32,
|
||||||
|
pub parent: Option<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
pub enum FoldGroupKind {
|
||||||
|
WeakComponent,
|
||||||
|
StrongComponent,
|
||||||
|
Cycle,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Constraints generated by the Sugiyama pass.
|
/// Constraints generated by the Sugiyama pass.
|
||||||
|
|
@ -259,15 +333,18 @@ pub fn layout_3d(graph: &Graph, config: &Layout3dConfig) -> Layout3d {
|
||||||
return Layout3d {
|
return Layout3d {
|
||||||
nodes: Vec::new(),
|
nodes: Vec::new(),
|
||||||
constraints: ConstraintSet::default(),
|
constraints: ConstraintSet::default(),
|
||||||
|
analysis: GraphAnalysis::default(),
|
||||||
|
fold_groups: Vec::new(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let analysis = analyze(graph);
|
||||||
let ranks = assign_ranks(graph);
|
let ranks = assign_ranks(graph);
|
||||||
let orders = order_ranks(graph, &ranks);
|
let orders = order_ranks(graph, &ranks);
|
||||||
let constraints = build_constraints(graph, &ranks, &orders, &config.base);
|
let constraints = build_constraints(graph, &ranks, &orders, &config.base);
|
||||||
let mut positions = initial_positions_3d(&ranks, &orders, config);
|
let mut positions = initial_positions_3d(&ranks, &orders, config);
|
||||||
|
|
||||||
relax_3d(graph, &constraints, config, &mut positions);
|
relax_3d(graph, &constraints, &analysis, config, &mut positions);
|
||||||
|
|
||||||
let mut nodes = (0..n)
|
let mut nodes = (0..n)
|
||||||
.map(|idx| NodeLayout3d {
|
.map(|idx| NodeLayout3d {
|
||||||
|
|
@ -281,7 +358,53 @@ pub fn layout_3d(graph: &Graph, config: &Layout3dConfig) -> Layout3d {
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
nodes.sort_by_key(|node| node.id.0);
|
nodes.sort_by_key(|node| node.id.0);
|
||||||
|
|
||||||
Layout3d { nodes, constraints }
|
let fold_groups = build_fold_groups(&analysis, &positions);
|
||||||
|
|
||||||
|
Layout3d {
|
||||||
|
nodes,
|
||||||
|
constraints,
|
||||||
|
analysis,
|
||||||
|
fold_groups,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Analyze graph structure without running layout.
|
||||||
|
pub fn analyze(graph: &Graph) -> GraphAnalysis {
|
||||||
|
let n = graph.node_count();
|
||||||
|
if n == 0 {
|
||||||
|
return GraphAnalysis::default();
|
||||||
|
}
|
||||||
|
|
||||||
|
let directed = directed_adjacency(graph);
|
||||||
|
let undirected = undirected_adjacency(graph);
|
||||||
|
let weak_components = weak_components(&undirected);
|
||||||
|
let (sccs, node_to_scc) = strongly_connected_components(&directed);
|
||||||
|
let (articulation_points, bridges) = articulation_points_and_bridges(&undirected);
|
||||||
|
let edge_roles = classify_edges(graph, &node_to_scc, &bridges);
|
||||||
|
let node_metrics = node_metrics(
|
||||||
|
graph,
|
||||||
|
&undirected,
|
||||||
|
&articulation_points,
|
||||||
|
&sccs,
|
||||||
|
&node_to_scc,
|
||||||
|
);
|
||||||
|
let mut node_to_weak_component = vec![0usize; n];
|
||||||
|
for component in &weak_components {
|
||||||
|
for node in &component.nodes {
|
||||||
|
node_to_weak_component[node.0] = component.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
GraphAnalysis {
|
||||||
|
weak_components,
|
||||||
|
strongly_connected_components: sccs,
|
||||||
|
articulation_points,
|
||||||
|
bridges,
|
||||||
|
node_metrics,
|
||||||
|
edge_roles,
|
||||||
|
node_to_weak_component,
|
||||||
|
node_to_strong_component: node_to_scc,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn assign_ranks(graph: &Graph) -> Vec<usize> {
|
fn assign_ranks(graph: &Graph) -> Vec<usize> {
|
||||||
|
|
@ -339,6 +462,265 @@ fn assign_ranks(graph: &Graph) -> Vec<usize> {
|
||||||
ranks
|
ranks
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn directed_adjacency(graph: &Graph) -> Vec<Vec<usize>> {
|
||||||
|
let mut adjacency = vec![Vec::new(); graph.node_count()];
|
||||||
|
for &(source, target) in graph.edges() {
|
||||||
|
adjacency[source.0].push(target.0);
|
||||||
|
}
|
||||||
|
adjacency
|
||||||
|
}
|
||||||
|
|
||||||
|
fn undirected_adjacency(graph: &Graph) -> Vec<Vec<(usize, EdgeId)>> {
|
||||||
|
let mut adjacency = vec![Vec::new(); graph.node_count()];
|
||||||
|
for (idx, &(source, target)) in graph.edges().iter().enumerate() {
|
||||||
|
let edge = EdgeId(idx);
|
||||||
|
adjacency[source.0].push((target.0, edge));
|
||||||
|
adjacency[target.0].push((source.0, edge));
|
||||||
|
}
|
||||||
|
adjacency
|
||||||
|
}
|
||||||
|
|
||||||
|
fn weak_components(adjacency: &[Vec<(usize, EdgeId)>]) -> Vec<Component> {
|
||||||
|
let mut components = Vec::new();
|
||||||
|
let mut seen = vec![false; adjacency.len()];
|
||||||
|
|
||||||
|
for start in 0..adjacency.len() {
|
||||||
|
if seen[start] {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut queue = VecDeque::from([start]);
|
||||||
|
let mut nodes = Vec::new();
|
||||||
|
seen[start] = true;
|
||||||
|
|
||||||
|
while let Some(node) = queue.pop_front() {
|
||||||
|
nodes.push(NodeId(node));
|
||||||
|
for &(next, _) in &adjacency[node] {
|
||||||
|
if !seen[next] {
|
||||||
|
seen[next] = true;
|
||||||
|
queue.push_back(next);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
components.push(Component {
|
||||||
|
id: components.len(),
|
||||||
|
nodes,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
components
|
||||||
|
}
|
||||||
|
|
||||||
|
fn strongly_connected_components(adjacency: &[Vec<usize>]) -> (Vec<Component>, Vec<usize>) {
|
||||||
|
struct Tarjan<'a> {
|
||||||
|
adjacency: &'a [Vec<usize>],
|
||||||
|
index: usize,
|
||||||
|
stack: Vec<usize>,
|
||||||
|
on_stack: Vec<bool>,
|
||||||
|
indices: Vec<Option<usize>>,
|
||||||
|
lowlink: Vec<usize>,
|
||||||
|
components: Vec<Component>,
|
||||||
|
node_to_component: Vec<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Tarjan<'_> {
|
||||||
|
fn visit(&mut self, node: usize) {
|
||||||
|
self.indices[node] = Some(self.index);
|
||||||
|
self.lowlink[node] = self.index;
|
||||||
|
self.index += 1;
|
||||||
|
self.stack.push(node);
|
||||||
|
self.on_stack[node] = true;
|
||||||
|
|
||||||
|
for &next in &self.adjacency[node] {
|
||||||
|
if self.indices[next].is_none() {
|
||||||
|
self.visit(next);
|
||||||
|
self.lowlink[node] = self.lowlink[node].min(self.lowlink[next]);
|
||||||
|
} else if self.on_stack[next] {
|
||||||
|
self.lowlink[node] = self.lowlink[node].min(self.indices[next].unwrap_or(0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.lowlink[node] == self.indices[node].unwrap_or(0) {
|
||||||
|
let id = self.components.len();
|
||||||
|
let mut nodes = Vec::new();
|
||||||
|
while let Some(member) = self.stack.pop() {
|
||||||
|
self.on_stack[member] = false;
|
||||||
|
self.node_to_component[member] = id;
|
||||||
|
nodes.push(NodeId(member));
|
||||||
|
if member == node {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
nodes.sort_by_key(|node| node.0);
|
||||||
|
self.components.push(Component { id, nodes });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let n = adjacency.len();
|
||||||
|
let mut tarjan = Tarjan {
|
||||||
|
adjacency,
|
||||||
|
index: 0,
|
||||||
|
stack: Vec::new(),
|
||||||
|
on_stack: vec![false; n],
|
||||||
|
indices: vec![None; n],
|
||||||
|
lowlink: vec![0; n],
|
||||||
|
components: Vec::new(),
|
||||||
|
node_to_component: vec![0; n],
|
||||||
|
};
|
||||||
|
|
||||||
|
for node in 0..n {
|
||||||
|
if tarjan.indices[node].is_none() {
|
||||||
|
tarjan.visit(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
(tarjan.components, tarjan.node_to_component)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn articulation_points_and_bridges(
|
||||||
|
adjacency: &[Vec<(usize, EdgeId)>],
|
||||||
|
) -> (Vec<NodeId>, Vec<EdgeId>) {
|
||||||
|
struct Dfs<'a> {
|
||||||
|
adjacency: &'a [Vec<(usize, EdgeId)>],
|
||||||
|
time: usize,
|
||||||
|
visited: Vec<bool>,
|
||||||
|
discovery: Vec<usize>,
|
||||||
|
low: Vec<usize>,
|
||||||
|
articulation: Vec<bool>,
|
||||||
|
bridges: Vec<EdgeId>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Dfs<'_> {
|
||||||
|
fn visit(&mut self, node: usize, parent_edge: Option<EdgeId>) {
|
||||||
|
self.visited[node] = true;
|
||||||
|
self.discovery[node] = self.time;
|
||||||
|
self.low[node] = self.time;
|
||||||
|
self.time += 1;
|
||||||
|
|
||||||
|
let mut child_count = 0usize;
|
||||||
|
for &(next, edge) in &self.adjacency[node] {
|
||||||
|
if Some(edge) == parent_edge {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if self.visited[next] {
|
||||||
|
self.low[node] = self.low[node].min(self.discovery[next]);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
child_count += 1;
|
||||||
|
self.visit(next, Some(edge));
|
||||||
|
self.low[node] = self.low[node].min(self.low[next]);
|
||||||
|
|
||||||
|
if parent_edge.is_some() && self.low[next] >= self.discovery[node] {
|
||||||
|
self.articulation[node] = true;
|
||||||
|
}
|
||||||
|
if self.low[next] > self.discovery[node] {
|
||||||
|
self.bridges.push(edge);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if parent_edge.is_none() && child_count > 1 {
|
||||||
|
self.articulation[node] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let n = adjacency.len();
|
||||||
|
let mut dfs = Dfs {
|
||||||
|
adjacency,
|
||||||
|
time: 0,
|
||||||
|
visited: vec![false; n],
|
||||||
|
discovery: vec![0; n],
|
||||||
|
low: vec![0; n],
|
||||||
|
articulation: vec![false; n],
|
||||||
|
bridges: Vec::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
for node in 0..n {
|
||||||
|
if !dfs.visited[node] {
|
||||||
|
dfs.visit(node, None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let articulation_points = dfs
|
||||||
|
.articulation
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.filter_map(|(idx, &is_articulation)| is_articulation.then_some(NodeId(idx)))
|
||||||
|
.collect();
|
||||||
|
dfs.bridges.sort_by_key(|edge| edge.0);
|
||||||
|
(articulation_points, dfs.bridges)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn classify_edges(graph: &Graph, node_to_scc: &[usize], bridges: &[EdgeId]) -> Vec<EdgeRole> {
|
||||||
|
let mut roles = vec![EdgeRole::Cross; graph.edge_count()];
|
||||||
|
let mut bridge_marks = vec![false; graph.edge_count()];
|
||||||
|
for edge in bridges {
|
||||||
|
bridge_marks[edge.0] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (idx, &(source, target)) in graph.edges().iter().enumerate() {
|
||||||
|
roles[idx] = if bridge_marks[idx] {
|
||||||
|
EdgeRole::Bridge
|
||||||
|
} else if node_to_scc[source.0] == node_to_scc[target.0] {
|
||||||
|
EdgeRole::IntraComponent
|
||||||
|
} else if source.0 < target.0 {
|
||||||
|
EdgeRole::Tree
|
||||||
|
} else {
|
||||||
|
EdgeRole::Back
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
roles
|
||||||
|
}
|
||||||
|
|
||||||
|
fn node_metrics(
|
||||||
|
graph: &Graph,
|
||||||
|
undirected: &[Vec<(usize, EdgeId)>],
|
||||||
|
articulation_points: &[NodeId],
|
||||||
|
sccs: &[Component],
|
||||||
|
node_to_scc: &[usize],
|
||||||
|
) -> Vec<NodeMetrics> {
|
||||||
|
let n = graph.node_count();
|
||||||
|
let mut in_degree = vec![0usize; n];
|
||||||
|
let mut out_degree = vec![0usize; n];
|
||||||
|
for &(source, target) in graph.edges() {
|
||||||
|
out_degree[source.0] += 1;
|
||||||
|
in_degree[target.0] += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut articulation = vec![false; n];
|
||||||
|
for node in articulation_points {
|
||||||
|
articulation[node.0] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut cyclic_scc = vec![false; sccs.len()];
|
||||||
|
for component in sccs {
|
||||||
|
if component.nodes.len() > 1 {
|
||||||
|
cyclic_scc[component.id] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for &(source, target) in graph.edges() {
|
||||||
|
if source == target {
|
||||||
|
cyclic_scc[node_to_scc[source.0]] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let centrality_denominator = n.saturating_sub(1).max(1) as f32;
|
||||||
|
(0..n)
|
||||||
|
.map(|idx| NodeMetrics {
|
||||||
|
in_degree: in_degree[idx],
|
||||||
|
out_degree: out_degree[idx],
|
||||||
|
undirected_degree: undirected[idx].len(),
|
||||||
|
degree_centrality: undirected[idx].len() as f32 / centrality_denominator,
|
||||||
|
is_articulation: articulation[idx],
|
||||||
|
is_cyclic: cyclic_scc[node_to_scc[idx]],
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
fn normalize_ranks(ranks: &mut [usize]) {
|
fn normalize_ranks(ranks: &mut [usize]) {
|
||||||
let mut used = ranks.to_vec();
|
let mut used = ranks.to_vec();
|
||||||
used.sort_unstable();
|
used.sort_unstable();
|
||||||
|
|
@ -515,6 +897,7 @@ fn relax(
|
||||||
fn relax_3d(
|
fn relax_3d(
|
||||||
graph: &Graph,
|
graph: &Graph,
|
||||||
constraints: &ConstraintSet,
|
constraints: &ConstraintSet,
|
||||||
|
analysis: &GraphAnalysis,
|
||||||
config: &Layout3dConfig,
|
config: &Layout3dConfig,
|
||||||
positions: &mut [(f32, f32, f32)],
|
positions: &mut [(f32, f32, f32)],
|
||||||
) {
|
) {
|
||||||
|
|
@ -527,6 +910,7 @@ fn relax_3d(
|
||||||
apply_repulsion_3d(positions, &mut forces, config);
|
apply_repulsion_3d(positions, &mut forces, config);
|
||||||
apply_springs_3d(graph, positions, &mut forces, config);
|
apply_springs_3d(graph, positions, &mut forces, config);
|
||||||
apply_constraints_3d(constraints, positions, &mut forces, config);
|
apply_constraints_3d(constraints, positions, &mut forces, config);
|
||||||
|
apply_structural_forces_3d(graph, analysis, positions, &mut forces, config);
|
||||||
apply_grounding_3d(positions, &mut forces, config);
|
apply_grounding_3d(positions, &mut forces, config);
|
||||||
|
|
||||||
let cooling = 1.0 - (iter as f32 / config.base.iterations.max(1) as f32);
|
let cooling = 1.0 - (iter as f32 / config.base.iterations.max(1) as f32);
|
||||||
|
|
@ -539,6 +923,242 @@ fn relax_3d(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn apply_structural_forces_3d(
|
||||||
|
graph: &Graph,
|
||||||
|
analysis: &GraphAnalysis,
|
||||||
|
positions: &[(f32, f32, f32)],
|
||||||
|
forces: &mut [(f32, f32, f32)],
|
||||||
|
config: &Layout3dConfig,
|
||||||
|
) {
|
||||||
|
apply_component_cohesion(
|
||||||
|
&analysis.strongly_connected_components,
|
||||||
|
positions,
|
||||||
|
forces,
|
||||||
|
config.component_cohesion_strength,
|
||||||
|
);
|
||||||
|
apply_component_cohesion(
|
||||||
|
&analysis.weak_components,
|
||||||
|
positions,
|
||||||
|
forces,
|
||||||
|
config.weak_component_cohesion_strength,
|
||||||
|
);
|
||||||
|
apply_centrality_anchors(analysis, positions, forces, config);
|
||||||
|
apply_cycle_folds(analysis, positions, forces, config);
|
||||||
|
apply_bridge_hinges(graph, analysis, positions, forces, config);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_component_cohesion(
|
||||||
|
components: &[Component],
|
||||||
|
positions: &[(f32, f32, f32)],
|
||||||
|
forces: &mut [(f32, f32, f32)],
|
||||||
|
strength: f32,
|
||||||
|
) {
|
||||||
|
if strength <= 0.0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for component in components {
|
||||||
|
if component.nodes.len() < 2 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let center = component_center(&component.nodes, positions);
|
||||||
|
for node in &component.nodes {
|
||||||
|
let idx = node.0;
|
||||||
|
forces[idx].0 += (center[0] - positions[idx].0) * strength;
|
||||||
|
forces[idx].1 += (center[1] - positions[idx].1) * strength * 0.25;
|
||||||
|
forces[idx].2 += (center[2] - positions[idx].2) * strength;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_centrality_anchors(
|
||||||
|
analysis: &GraphAnalysis,
|
||||||
|
positions: &[(f32, f32, f32)],
|
||||||
|
forces: &mut [(f32, f32, f32)],
|
||||||
|
config: &Layout3dConfig,
|
||||||
|
) {
|
||||||
|
if config.centrality_anchor_strength <= 0.0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for component in &analysis.weak_components {
|
||||||
|
if component.nodes.len() < 3 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let center = component_center(&component.nodes, positions);
|
||||||
|
for node in &component.nodes {
|
||||||
|
let idx = node.0;
|
||||||
|
let metric = analysis.node_metrics[idx];
|
||||||
|
let strength = config.centrality_anchor_strength * metric.degree_centrality;
|
||||||
|
forces[idx].0 += (center[0] - positions[idx].0) * strength;
|
||||||
|
forces[idx].2 += (center[2] - positions[idx].2) * strength;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_cycle_folds(
|
||||||
|
analysis: &GraphAnalysis,
|
||||||
|
positions: &[(f32, f32, f32)],
|
||||||
|
forces: &mut [(f32, f32, f32)],
|
||||||
|
config: &Layout3dConfig,
|
||||||
|
) {
|
||||||
|
if config.cycle_fold_strength <= 0.0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for component in &analysis.strongly_connected_components {
|
||||||
|
if component.nodes.len() < 2 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let center = component_center(&component.nodes, positions);
|
||||||
|
for (offset, node) in component.nodes.iter().enumerate() {
|
||||||
|
let idx = node.0;
|
||||||
|
let side = if offset % 2 == 0 { 1.0 } else { -1.0 };
|
||||||
|
let target_z =
|
||||||
|
center[2] + side * config.depth_gap * component.nodes.len() as f32 * 0.35;
|
||||||
|
forces[idx].2 += (target_z - positions[idx].2) * config.cycle_fold_strength;
|
||||||
|
forces[idx].1 -= positions[idx].1 * config.cycle_fold_strength * 0.08;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_bridge_hinges(
|
||||||
|
graph: &Graph,
|
||||||
|
analysis: &GraphAnalysis,
|
||||||
|
positions: &[(f32, f32, f32)],
|
||||||
|
forces: &mut [(f32, f32, f32)],
|
||||||
|
config: &Layout3dConfig,
|
||||||
|
) {
|
||||||
|
if config.bridge_hinge_strength <= 0.0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for bridge in &analysis.bridges {
|
||||||
|
let (source, target) = graph.edges()[bridge.0];
|
||||||
|
let source_component = analysis.node_to_weak_component[source.0];
|
||||||
|
let target_component = analysis.node_to_weak_component[target.0];
|
||||||
|
if source_component == target_component {
|
||||||
|
let midpoint = midpoint_3d(positions[source.0], positions[target.0]);
|
||||||
|
for node in [source, target] {
|
||||||
|
let idx = node.0;
|
||||||
|
let multiplier = if analysis.node_metrics[idx].is_articulation {
|
||||||
|
1.5
|
||||||
|
} else {
|
||||||
|
1.0
|
||||||
|
};
|
||||||
|
forces[idx].0 +=
|
||||||
|
(midpoint.0 - positions[idx].0) * config.bridge_hinge_strength * multiplier;
|
||||||
|
forces[idx].2 +=
|
||||||
|
(midpoint.2 - positions[idx].2) * config.bridge_hinge_strength * multiplier;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn component_center(nodes: &[NodeId], positions: &[(f32, f32, f32)]) -> [f32; 3] {
|
||||||
|
let mut center = [0.0f32; 3];
|
||||||
|
for node in nodes {
|
||||||
|
center[0] += positions[node.0].0;
|
||||||
|
center[1] += positions[node.0].1;
|
||||||
|
center[2] += positions[node.0].2;
|
||||||
|
}
|
||||||
|
let len = nodes.len().max(1) as f32;
|
||||||
|
center[0] /= len;
|
||||||
|
center[1] /= len;
|
||||||
|
center[2] /= len;
|
||||||
|
center
|
||||||
|
}
|
||||||
|
|
||||||
|
fn midpoint_3d(a: (f32, f32, f32), b: (f32, f32, f32)) -> (f32, f32, f32) {
|
||||||
|
((a.0 + b.0) * 0.5, (a.1 + b.1) * 0.5, (a.2 + b.2) * 0.5)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_fold_groups(analysis: &GraphAnalysis, positions: &[(f32, f32, f32)]) -> Vec<FoldGroup> {
|
||||||
|
let mut groups = Vec::new();
|
||||||
|
|
||||||
|
for component in &analysis.weak_components {
|
||||||
|
if component.nodes.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
groups.push(fold_group(
|
||||||
|
groups.len(),
|
||||||
|
FoldGroupKind::WeakComponent,
|
||||||
|
component.nodes.clone(),
|
||||||
|
positions,
|
||||||
|
None,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
for component in &analysis.strongly_connected_components {
|
||||||
|
if component.nodes.len() < 2 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let parent = component
|
||||||
|
.nodes
|
||||||
|
.first()
|
||||||
|
.map(|node| analysis.node_to_weak_component[node.0]);
|
||||||
|
groups.push(fold_group(
|
||||||
|
groups.len(),
|
||||||
|
FoldGroupKind::StrongComponent,
|
||||||
|
component.nodes.clone(),
|
||||||
|
positions,
|
||||||
|
parent,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
for component in &analysis.strongly_connected_components {
|
||||||
|
let is_cycle = component
|
||||||
|
.nodes
|
||||||
|
.iter()
|
||||||
|
.any(|node| analysis.node_metrics[node.0].is_cyclic);
|
||||||
|
if !is_cycle || component.nodes.len() < 2 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let parent = component
|
||||||
|
.nodes
|
||||||
|
.first()
|
||||||
|
.map(|node| analysis.node_to_weak_component[node.0]);
|
||||||
|
groups.push(fold_group(
|
||||||
|
groups.len(),
|
||||||
|
FoldGroupKind::Cycle,
|
||||||
|
component.nodes.clone(),
|
||||||
|
positions,
|
||||||
|
parent,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
groups
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fold_group(
|
||||||
|
id: usize,
|
||||||
|
kind: FoldGroupKind,
|
||||||
|
nodes: Vec<NodeId>,
|
||||||
|
positions: &[(f32, f32, f32)],
|
||||||
|
parent: Option<usize>,
|
||||||
|
) -> FoldGroup {
|
||||||
|
let center = component_center(&nodes, positions);
|
||||||
|
let mut radius = 0.0f32;
|
||||||
|
for node in &nodes {
|
||||||
|
let dx = positions[node.0].0 - center[0];
|
||||||
|
let dy = positions[node.0].1 - center[1];
|
||||||
|
let dz = positions[node.0].2 - center[2];
|
||||||
|
radius = radius.max((dx * dx + dy * dy + dz * dz).sqrt());
|
||||||
|
}
|
||||||
|
|
||||||
|
FoldGroup {
|
||||||
|
id,
|
||||||
|
kind,
|
||||||
|
nodes,
|
||||||
|
center,
|
||||||
|
radius,
|
||||||
|
parent,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn apply_repulsion(positions: &[(f32, f32)], forces: &mut [(f32, f32)], strength: f32) {
|
fn apply_repulsion(positions: &[(f32, f32)], forces: &mut [(f32, f32)], strength: f32) {
|
||||||
for a in 0..positions.len() {
|
for a in 0..positions.len() {
|
||||||
for b in (a + 1)..positions.len() {
|
for b in (a + 1)..positions.len() {
|
||||||
|
|
@ -827,4 +1447,61 @@ mod tests {
|
||||||
assert!(result.nodes.is_empty());
|
assert!(result.nodes.is_empty());
|
||||||
assert!(result.constraints.ranks.is_empty());
|
assert!(result.constraints.ranks.is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn analysis_finds_cycles_bridges_and_articulation_points() {
|
||||||
|
let mut graph = Graph::new();
|
||||||
|
let a = graph.add_node(1.0);
|
||||||
|
let b = graph.add_node(1.0);
|
||||||
|
let c = graph.add_node(1.0);
|
||||||
|
let d = graph.add_node(1.0);
|
||||||
|
graph.add_edge(a, b);
|
||||||
|
graph.add_edge(b, c);
|
||||||
|
graph.add_edge(c, a);
|
||||||
|
let bridge = graph.add_edge(c, d);
|
||||||
|
|
||||||
|
let analysis = analyze(&graph);
|
||||||
|
|
||||||
|
assert_eq!(analysis.weak_components.len(), 1);
|
||||||
|
assert_eq!(analysis.strongly_connected_components.len(), 2);
|
||||||
|
assert!(analysis.node_metrics[a.0].is_cyclic);
|
||||||
|
assert!(analysis.node_metrics[b.0].is_cyclic);
|
||||||
|
assert!(analysis.node_metrics[c.0].is_cyclic);
|
||||||
|
assert!(!analysis.node_metrics[d.0].is_cyclic);
|
||||||
|
assert!(analysis.articulation_points.contains(&c));
|
||||||
|
assert!(analysis.bridges.contains(&bridge));
|
||||||
|
assert_eq!(analysis.edge_roles[bridge.0], EdgeRole::Bridge);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn layout_3d_exposes_fold_groups() {
|
||||||
|
let mut graph = Graph::new();
|
||||||
|
let a = graph.add_node(1.0);
|
||||||
|
let b = graph.add_node(1.0);
|
||||||
|
let c = graph.add_node(1.0);
|
||||||
|
graph.add_edge(a, b);
|
||||||
|
graph.add_edge(b, c);
|
||||||
|
graph.add_edge(c, a);
|
||||||
|
|
||||||
|
let result = layout_3d(&graph, &Layout3dConfig::default());
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
result
|
||||||
|
.fold_groups
|
||||||
|
.iter()
|
||||||
|
.any(|group| group.kind == FoldGroupKind::WeakComponent)
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
result
|
||||||
|
.fold_groups
|
||||||
|
.iter()
|
||||||
|
.any(|group| group.kind == FoldGroupKind::StrongComponent)
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
result
|
||||||
|
.fold_groups
|
||||||
|
.iter()
|
||||||
|
.any(|group| group.kind == FoldGroupKind::Cycle)
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Reference in a new issue