use std::collections::{HashMap, HashSet, VecDeque};
use bevy_egui::egui;
use crate::editor::layout::{LayoutConfig, NodeLayout};
use crate::editor::view_model::ViewScene;
use crate::model::StateMachineGraph;
use crate::types::EntityId;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LayoutDirection {
LR,
RL,
TB,
BT,
}
#[derive(Debug, Clone)]
pub struct AutoLayoutConfig {
pub direction: LayoutDirection,
pub layer_spacing: f32,
pub node_spacing: f32,
pub barycenter_sweeps: u32,
pub reserve_pill_gap: f32,
}
impl Default for AutoLayoutConfig {
fn default() -> Self {
Self {
direction: LayoutDirection::LR,
layer_spacing: 80.0,
node_spacing: 32.0,
barycenter_sweeps: 24,
reserve_pill_gap: 40.0,
}
}
}
#[derive(Debug, Default, Clone)]
pub struct AutoLayoutReport {
pub nodes_moved: usize,
pub compounds_processed: usize,
pub reversed_edges: usize,
pub long_edges: usize,
}
pub(crate) fn auto_layout_subtree(
scene: &mut ViewScene,
graph: &StateMachineGraph,
target: EntityId,
cfg: &AutoLayoutConfig,
layout_cfg: &LayoutConfig,
) -> AutoLayoutReport {
let root = if graph.get_children(&target).is_empty() {
graph.get_parent(&target).unwrap_or(target)
} else {
target
};
let root_min = scene
.node_rects
.get(&root)
.map(|r| r.min)
.unwrap_or(egui::pos2(0.0, 0.0));
let subtree = collect_subtree(graph, root);
let initial_sizes = snapshot_sizes(scene, &subtree, layout_cfg);
let pill_sizes = snapshot_pill_sizes(scene);
let mut local_positions: HashMap<EntityId, HashMap<EntityId, egui::Pos2>> = HashMap::new();
let mut new_sizes: HashMap<EntityId, egui::Vec2> = HashMap::new();
let mut report = AutoLayoutReport::default();
compute_layout_recursive(
graph,
root,
&initial_sizes,
&pill_sizes,
cfg,
layout_cfg,
&mut local_positions,
&mut new_sizes,
&mut report,
);
let mut new_rects: HashMap<EntityId, egui::Rect> = HashMap::new();
apply_layout_recursive(
graph,
root,
root_min,
&new_sizes,
&local_positions,
&mut new_rects,
layout_cfg,
);
for (id, new_rect) in new_rects.iter() {
let old = scene.node_rects.get(id).copied();
if old.map(|r| r != *new_rect).unwrap_or(true) {
report.nodes_moved += 1;
}
}
write_back_node_rects(scene, &new_rects);
reposition_pills_for_subtree(scene, graph, &subtree, cfg);
finalize_with_node_layout(scene, layout_cfg);
report
}
fn collect_subtree(graph: &StateMachineGraph, root: EntityId) -> HashSet<EntityId> {
let mut out: HashSet<EntityId> = HashSet::new();
let mut stack: Vec<EntityId> = vec![root];
while let Some(id) = stack.pop() {
if !out.insert(id) {
continue;
}
for child in graph.get_children(&id) {
stack.push(child);
}
}
out
}
fn snapshot_sizes(
scene: &ViewScene,
subtree: &HashSet<EntityId>,
layout_cfg: &LayoutConfig,
) -> HashMap<EntityId, egui::Vec2> {
let mut sizes: HashMap<EntityId, egui::Vec2> = HashMap::new();
for id in subtree.iter() {
let sz = scene
.node_rects
.get(id)
.map(|r| r.size())
.unwrap_or(layout_cfg.min_node_size);
sizes.insert(*id, sz);
}
sizes
}
fn snapshot_pill_sizes(scene: &ViewScene) -> HashMap<EntityId, egui::Vec2> {
scene
.edges
.iter()
.map(|(id, ev)| (*id, ev.rect.size()))
.collect()
}
fn compute_layout_recursive(
graph: &StateMachineGraph,
node: EntityId,
initial_sizes: &HashMap<EntityId, egui::Vec2>,
pill_sizes: &HashMap<EntityId, egui::Vec2>,
cfg: &AutoLayoutConfig,
layout_cfg: &LayoutConfig,
local_positions: &mut HashMap<EntityId, HashMap<EntityId, egui::Pos2>>,
new_sizes: &mut HashMap<EntityId, egui::Vec2>,
report: &mut AutoLayoutReport,
) -> egui::Vec2 {
let children = graph.get_children(&node);
if children.is_empty() {
let raw = initial_sizes
.get(&node)
.copied()
.unwrap_or(layout_cfg.min_node_size);
let size = egui::vec2(
raw.x.max(layout_cfg.min_node_size.x),
raw.y.max(layout_cfg.min_node_size.y),
);
new_sizes.insert(node, size);
return size;
}
let mut child_sizes: HashMap<EntityId, egui::Vec2> = HashMap::new();
for child in children.iter() {
let s = compute_layout_recursive(
graph,
*child,
initial_sizes,
pill_sizes,
cfg,
layout_cfg,
local_positions,
new_sizes,
report,
);
child_sizes.insert(*child, s);
}
let positions = run_sugiyama_for_compound(
graph,
node,
&children,
&child_sizes,
pill_sizes,
cfg,
layout_cfg,
report,
);
let pad = layout_cfg.content_padding;
let header_h = layout_cfg.header_height_world;
let mut max_extent = egui::vec2(0.0, 0.0);
for child in children.iter() {
let pos = positions.get(child).copied().unwrap_or(egui::pos2(0.0, 0.0));
let sz = child_sizes
.get(child)
.copied()
.unwrap_or(layout_cfg.min_node_size);
max_extent.x = max_extent.x.max(pos.x + sz.x);
max_extent.y = max_extent.y.max(pos.y + sz.y);
}
let size = egui::vec2(
(2.0 * pad.x + max_extent.x).max(layout_cfg.min_node_size.x),
(2.0 * pad.y + header_h + max_extent.y).max(layout_cfg.min_node_size.y),
);
local_positions.insert(node, positions);
new_sizes.insert(node, size);
report.compounds_processed += 1;
size
}
fn apply_layout_recursive(
graph: &StateMachineGraph,
node: EntityId,
node_min: egui::Pos2,
new_sizes: &HashMap<EntityId, egui::Vec2>,
local_positions: &HashMap<EntityId, HashMap<EntityId, egui::Pos2>>,
out_rects: &mut HashMap<EntityId, egui::Rect>,
layout_cfg: &LayoutConfig,
) {
let size = new_sizes
.get(&node)
.copied()
.unwrap_or(layout_cfg.min_node_size);
out_rects.insert(node, egui::Rect::from_min_size(node_min, size));
let children = graph.get_children(&node);
if children.is_empty() {
return;
}
let pad = layout_cfg.content_padding;
let header_h = layout_cfg.header_height_world;
let content_origin = egui::pos2(node_min.x + pad.x, node_min.y + pad.y + header_h);
if let Some(positions) = local_positions.get(&node) {
for child in children.iter() {
let local = positions
.get(child)
.copied()
.unwrap_or(egui::pos2(0.0, 0.0));
let child_min = egui::pos2(content_origin.x + local.x, content_origin.y + local.y);
apply_layout_recursive(
graph,
*child,
child_min,
new_sizes,
local_positions,
out_rects,
layout_cfg,
);
}
}
}
fn write_back_node_rects(scene: &mut ViewScene, new_rects: &HashMap<EntityId, egui::Rect>) {
for (id, rect) in new_rects.iter() {
if let Some(r) = scene.node_rects.get_mut(id) {
*r = *rect;
}
if let Some(sv) = scene.states.get_mut(id) {
sv.rect = *rect;
}
}
}
fn reposition_pills_for_subtree(
scene: &mut ViewScene,
graph: &StateMachineGraph,
subtree: &HashSet<EntityId>,
cfg: &AutoLayoutConfig,
) {
let edge_ids: Vec<EntityId> = scene
.edges
.iter()
.filter(|(_, ev)| subtree.contains(&ev.source) || subtree.contains(&ev.target))
.map(|(id, _)| *id)
.collect();
for eid in edge_ids {
let (src, dst) = match scene.edges.get(&eid) {
Some(ev) => (ev.source, ev.target),
None => continue,
};
let src_rect = match scene.node_rects.get(&src).copied() {
Some(r) => r,
None => continue,
};
let dst_rect = match scene.node_rects.get(&dst).copied() {
Some(r) => r,
None => continue,
};
let pill_size = scene
.edges
.get(&eid)
.map(|ev| ev.rect.size())
.unwrap_or(egui::vec2(80.0, 24.0));
let new_center = if src == dst {
egui::pos2(src_rect.center().x, src_rect.max.y + 32.0)
} else if graph.get_parent(&dst) == Some(src) {
place_pill_aligned_far_side(dst_rect, src_rect, pill_size, cfg.direction)
} else if graph.get_parent(&src) == Some(dst) {
place_pill_aligned_far_side(src_rect, dst_rect, pill_size, cfg.direction)
} else {
let from_src = rect_border_toward(src_rect, dst_rect.center());
let from_dst = rect_border_toward(dst_rect, src_rect.center());
egui::pos2(
(from_src.x + from_dst.x) * 0.5,
(from_src.y + from_dst.y) * 0.5,
)
};
let new_rect = egui::Rect::from_center_size(new_center, pill_size);
if let Some(r) = scene.node_rects.get_mut(&eid) {
*r = new_rect;
}
if let Some(ev) = scene.edges.get_mut(&eid) {
ev.rect = new_rect;
}
}
}
fn rect_border_toward(rect: egui::Rect, target: egui::Pos2) -> egui::Pos2 {
let c = rect.center();
let dir = target - c;
if dir.length_sq() < 1e-6 {
return c;
}
let half = rect.size() * 0.5;
let tx = if dir.x.abs() > 1e-6 {
half.x / dir.x.abs()
} else {
f32::INFINITY
};
let ty = if dir.y.abs() > 1e-6 {
half.y / dir.y.abs()
} else {
f32::INFINITY
};
let t = tx.min(ty);
c + dir * t
}
fn place_pill_aligned_far_side(
inner: egui::Rect,
outer: egui::Rect,
pill_size: egui::Vec2,
direction: LayoutDirection,
) -> egui::Pos2 {
const EXTRA_GAP: f32 = 16.0;
let inner_c = inner.center();
let outer_c = outer.center();
let is_horizontal = matches!(direction, LayoutDirection::LR | LayoutDirection::RL);
if is_horizontal {
let pad = pill_size.x * 0.5 + EXTRA_GAP;
let right_x = inner.max.x + pad;
let left_x = inner.min.x - pad;
let right_dist = (right_x - outer_c.x).abs();
let left_dist = (left_x - outer_c.x).abs();
let x = if right_dist >= left_dist { right_x } else { left_x };
egui::pos2(x, inner_c.y)
} else {
let pad = pill_size.y * 0.5 + EXTRA_GAP;
let bottom_y = inner.max.y + pad;
let top_y = inner.min.y - pad;
let bottom_dist = (bottom_y - outer_c.y).abs();
let top_dist = (top_y - outer_c.y).abs();
let y = if bottom_dist >= top_dist { bottom_y } else { top_y };
egui::pos2(inner_c.x, y)
}
}
fn finalize_with_node_layout(scene: &mut ViewScene, layout_cfg: &LayoutConfig) {
let mut nl = NodeLayout {
node_rects: scene.node_rects.clone(),
parent_of: scene.tree.parent_of.clone(),
children_of: scene.tree.children_of.clone(),
container_nodes: scene.tree.containers.clone(),
root: None,
draw_order_nodes: Vec::new(),
};
let mut attachments: HashMap<EntityId, Vec<egui::Rect>> = HashMap::new();
for (eid, ev) in scene.edges.iter() {
if let Some(pid) = ev.pill_parent {
if let Some(rect) = scene.node_rects.get(eid).copied() {
attachments.entry(pid).or_default().push(rect);
}
}
}
nl.clamp_children_left_top(layout_cfg);
nl.fit_parents_to_children(layout_cfg, Some(&attachments));
for (id, rect) in nl.node_rects.iter() {
let changed = scene
.node_rects
.get(id)
.map(|cur| *cur != *rect)
.unwrap_or(true);
if changed {
if let Some(r) = scene.node_rects.get_mut(id) {
*r = *rect;
}
if let Some(sv) = scene.states.get_mut(id) {
sv.rect = *rect;
}
if let Some(ev) = scene.edges.get_mut(id) {
ev.rect = *rect;
}
}
}
}
fn run_sugiyama_for_compound(
graph: &StateMachineGraph,
compound: EntityId,
children: &[EntityId],
child_sizes: &HashMap<EntityId, egui::Vec2>,
pill_sizes: &HashMap<EntityId, egui::Vec2>,
cfg: &AutoLayoutConfig,
layout_cfg: &LayoutConfig,
report: &mut AutoLayoutReport,
) -> HashMap<EntityId, egui::Pos2> {
let n = children.len();
if n == 0 {
return HashMap::new();
}
if n == 1 {
let mut out = HashMap::new();
out.insert(children[0], egui::pos2(0.0, 0.0));
return out;
}
let mut node_idx: HashMap<EntityId, usize> = HashMap::new();
for (i, c) in children.iter().enumerate() {
node_idx.insert(*c, i);
}
let direct_children: HashSet<EntityId> = children.iter().copied().collect();
let mut edge_thickness: HashMap<(usize, usize), f32> = HashMap::new();
for (eid, edge) in graph.edges.iter() {
let pair = bubble_edge_to_level(graph, compound, &direct_children, edge.source, edge.target);
if let Some((src_rep, dst_rep)) = pair {
if src_rep == dst_rep {
continue;
}
let si = node_idx[&src_rep];
let di = node_idx[&dst_rep];
let pill_sz = pill_sizes
.get(eid)
.copied()
.unwrap_or(egui::vec2(80.0, 24.0));
let thickness = match cfg.direction {
LayoutDirection::LR | LayoutDirection::RL => pill_sz.x,
LayoutDirection::TB | LayoutDirection::BT => pill_sz.y,
};
let entry = edge_thickness.entry((si, di)).or_insert(0.0);
if thickness > *entry {
*entry = thickness;
}
}
}
let mut raw_edges: Vec<(usize, usize, f32)> = edge_thickness
.into_iter()
.map(|((s, d), t)| (s, d, t))
.collect();
raw_edges.sort_by_key(|(s, d, _)| (*s, *d));
let pair_list: Vec<(usize, usize)> = raw_edges.iter().map(|(s, d, _)| (*s, *d)).collect();
let reversed = greedy_fas(n, &pair_list);
let oriented: Vec<(usize, usize, f32)> = raw_edges
.iter()
.enumerate()
.map(|(i, &(s, d, t))| if reversed[i] { (d, s, t) } else { (s, d, t) })
.collect();
report.reversed_edges += reversed.iter().filter(|x| **x).count();
let oriented_pairs: Vec<(usize, usize)> =
oriented.iter().map(|(s, d, _)| (*s, *d)).collect();
let layers = assign_layers(n, &oriented_pairs);
let mut lg = build_layered_graph(n, &layers, &oriented, report);
barycenter_sweeps(&mut lg, cfg.barycenter_sweeps);
assign_coordinates(&lg, children, child_sizes, cfg, layout_cfg)
}
fn bubble_edge_to_level(
graph: &StateMachineGraph,
_level: EntityId,
direct_children: &HashSet<EntityId>,
src: EntityId,
dst: EntityId,
) -> Option<(EntityId, EntityId)> {
let s = bubble_up(graph, direct_children, src)?;
let d = bubble_up(graph, direct_children, dst)?;
Some((s, d))
}
fn bubble_up(
graph: &StateMachineGraph,
direct_children: &HashSet<EntityId>,
start: EntityId,
) -> Option<EntityId> {
let mut node = start;
loop {
if direct_children.contains(&node) {
return Some(node);
}
match graph.get_parent(&node) {
Some(p) => node = p,
None => return None,
}
}
}
fn greedy_fas(n: usize, edges: &[(usize, usize)]) -> Vec<bool> {
let m = edges.len();
let mut reversed = vec![false; m];
if n == 0 || m == 0 {
return reversed;
}
let mut out_eidx: Vec<Vec<usize>> = vec![Vec::new(); n];
let mut in_eidx: Vec<Vec<usize>> = vec![Vec::new(); n];
for (ei, &(s, d)) in edges.iter().enumerate() {
if s == d {
continue;
}
out_eidx[s].push(ei);
in_eidx[d].push(ei);
}
let mut alive = vec![true; n];
let mut in_deg: Vec<i32> = (0..n).map(|v| in_eidx[v].len() as i32).collect();
let mut out_deg: Vec<i32> = (0..n).map(|v| out_eidx[v].len() as i32).collect();
let mut s1: Vec<usize> = Vec::new();
let mut s2: Vec<usize> = Vec::new();
let mut remaining = n;
while remaining > 0 {
let mut changed = true;
while changed {
changed = false;
for v in 0..n {
if alive[v] && out_deg[v] == 0 {
s2.push(v);
remove_vertex(v, &mut alive, &mut in_deg, &mut out_deg, &out_eidx, &in_eidx, edges);
remaining -= 1;
changed = true;
}
}
}
changed = true;
while changed {
changed = false;
for v in 0..n {
if alive[v] && in_deg[v] == 0 {
s1.push(v);
remove_vertex(v, &mut alive, &mut in_deg, &mut out_deg, &out_eidx, &in_eidx, edges);
remaining -= 1;
changed = true;
}
}
}
if remaining > 0 {
let mut best: Option<(usize, i32)> = None;
for v in 0..n {
if alive[v] {
let diff = out_deg[v] - in_deg[v];
match best {
None => best = Some((v, diff)),
Some((_, b)) if diff > b => best = Some((v, diff)),
_ => {}
}
}
}
if let Some((v, _)) = best {
s1.push(v);
remove_vertex(v, &mut alive, &mut in_deg, &mut out_deg, &out_eidx, &in_eidx, edges);
remaining -= 1;
}
}
}
let mut order: Vec<usize> = s1;
s2.reverse();
order.extend(s2);
let mut pos: Vec<usize> = vec![0; n];
for (i, &v) in order.iter().enumerate() {
pos[v] = i;
}
for (i, &(s, d)) in edges.iter().enumerate() {
if s == d {
continue;
}
if pos[s] > pos[d] {
reversed[i] = true;
}
}
reversed
}
fn remove_vertex(
v: usize,
alive: &mut [bool],
in_deg: &mut [i32],
out_deg: &mut [i32],
out_eidx: &[Vec<usize>],
in_eidx: &[Vec<usize>],
edges: &[(usize, usize)],
) {
alive[v] = false;
for &ei in &out_eidx[v] {
let d = edges[ei].1;
if alive[d] {
in_deg[d] -= 1;
}
}
for &ei in &in_eidx[v] {
let s = edges[ei].0;
if alive[s] {
out_deg[s] -= 1;
}
}
}
fn assign_layers(n: usize, oriented_edges: &[(usize, usize)]) -> Vec<usize> {
let mut out: Vec<Vec<usize>> = vec![Vec::new(); n];
let mut in_count: Vec<usize> = vec![0; n];
for &(s, d) in oriented_edges {
if s == d {
continue;
}
out[s].push(d);
in_count[d] += 1;
}
let mut deg = in_count.clone();
let mut queue: VecDeque<usize> = VecDeque::new();
for v in 0..n {
if deg[v] == 0 {
queue.push_back(v);
}
}
let mut topo: Vec<usize> = Vec::with_capacity(n);
while let Some(v) = queue.pop_front() {
topo.push(v);
for &d in &out[v] {
deg[d] -= 1;
if deg[d] == 0 {
queue.push_back(d);
}
}
}
if topo.len() < n {
for v in 0..n {
if !topo.contains(&v) {
topo.push(v);
}
}
}
let mut layers = vec![0usize; n];
for &v in &topo {
for &d in &out[v] {
if layers[d] < layers[v] + 1 {
layers[d] = layers[v] + 1;
}
}
}
layers
}
struct LayeredGraph {
n_real: usize,
layers: Vec<Vec<usize>>,
pred: Vec<Vec<usize>>,
succ: Vec<Vec<usize>>,
gap_pill_thickness: Vec<f32>,
}
fn build_layered_graph(
n_real: usize,
real_layers: &[usize],
oriented_edges: &[(usize, usize, f32)],
report: &mut AutoLayoutReport,
) -> LayeredGraph {
let max_layer = *real_layers.iter().max().unwrap_or(&0);
let n_layers = max_layer + 1;
let mut layers: Vec<Vec<usize>> = vec![Vec::new(); n_layers];
let mut layer_of: Vec<usize> = real_layers.to_vec();
for v in 0..n_real {
layers[real_layers[v]].push(v);
}
let mut pred: Vec<Vec<usize>> = vec![Vec::new(); n_real];
let mut succ: Vec<Vec<usize>> = vec![Vec::new(); n_real];
let mut gap_pill_thickness: Vec<f32> = vec![0.0; n_layers.saturating_sub(1)];
for &(s, d, thickness) in oriented_edges {
if s == d {
continue;
}
let ls = real_layers[s];
let ld = real_layers[d];
if ld <= ls {
continue;
}
for g in ls..ld {
if g < gap_pill_thickness.len() && thickness > gap_pill_thickness[g] {
gap_pill_thickness[g] = thickness;
}
}
if ld - ls == 1 {
succ[s].push(d);
pred[d].push(s);
} else {
report.long_edges += 1;
let mut prev_v = s;
for layer in (ls + 1)..ld {
let dummy_idx = layer_of.len();
layer_of.push(layer);
layers[layer].push(dummy_idx);
succ.push(Vec::new());
pred.push(Vec::new());
succ[prev_v].push(dummy_idx);
pred[dummy_idx].push(prev_v);
prev_v = dummy_idx;
}
succ[prev_v].push(d);
pred[d].push(prev_v);
}
}
LayeredGraph {
n_real,
layers,
pred,
succ,
gap_pill_thickness,
}
}
fn barycenter_sweeps(lg: &mut LayeredGraph, sweeps: u32) {
let n_layers = lg.layers.len();
if n_layers <= 1 {
return;
}
for _ in 0..sweeps {
for li in 1..n_layers {
reorder_layer_by_neighbors(lg, li, true);
}
for li in (0..n_layers - 1).rev() {
reorder_layer_by_neighbors(lg, li, false);
}
}
}
fn reorder_layer_by_neighbors(lg: &mut LayeredGraph, li: usize, use_pred: bool) {
let neighbor_layer_idx = if use_pred {
if li == 0 {
return;
}
li - 1
} else {
if li + 1 >= lg.layers.len() {
return;
}
li + 1
};
let neighbor_layer = &lg.layers[neighbor_layer_idx];
let mut pos_of: HashMap<usize, usize> = HashMap::with_capacity(neighbor_layer.len());
for (i, &v) in neighbor_layer.iter().enumerate() {
pos_of.insert(v, i);
}
let mut keyed: Vec<(usize, f64, usize)> = lg.layers[li]
.iter()
.enumerate()
.map(|(cur_pos, &v)| {
let neighbors: &[usize] = if use_pred { &lg.pred[v] } else { &lg.succ[v] };
let mut sum = 0.0f64;
let mut cnt = 0usize;
for nb in neighbors {
if let Some(p) = pos_of.get(nb) {
sum += *p as f64;
cnt += 1;
}
}
let bary = if cnt > 0 {
sum / cnt as f64
} else {
cur_pos as f64
};
(v, bary, cur_pos)
})
.collect();
keyed.sort_by(|a, b| {
a.1.partial_cmp(&b.1)
.unwrap_or(std::cmp::Ordering::Equal)
.then(a.2.cmp(&b.2))
});
lg.layers[li] = keyed.into_iter().map(|(v, _, _)| v).collect();
}
fn assign_coordinates(
lg: &LayeredGraph,
children: &[EntityId],
child_sizes: &HashMap<EntityId, egui::Vec2>,
cfg: &AutoLayoutConfig,
layout_cfg: &LayoutConfig,
) -> HashMap<EntityId, egui::Pos2> {
let n_layers = lg.layers.len();
if n_layers == 0 {
return HashMap::new();
}
let is_horizontal = matches!(cfg.direction, LayoutDirection::LR | LayoutDirection::RL);
let mut layer_thickness: Vec<f32> = vec![0.0; n_layers];
for (li, layer) in lg.layers.iter().enumerate() {
let mut max_t = 0.0f32;
for &v in layer {
if v < lg.n_real {
let id = children[v];
let sz = child_sizes
.get(&id)
.copied()
.unwrap_or(layout_cfg.min_node_size);
let t = if is_horizontal { sz.x } else { sz.y };
if t > max_t {
max_t = t;
}
}
}
layer_thickness[li] = max_t.max(0.0);
}
let mut layer_t_center = vec![0.0f32; n_layers];
let mut t_cursor = 0.0f32;
for li in 0..n_layers {
t_cursor += layer_thickness[li] * 0.5;
layer_t_center[li] = t_cursor;
t_cursor += layer_thickness[li] * 0.5;
if li + 1 < n_layers {
let pill_t = lg.gap_pill_thickness.get(li).copied().unwrap_or(0.0);
let extra = pill_t.max(cfg.reserve_pill_gap);
t_cursor += cfg.layer_spacing + extra;
}
}
let total_t_extent = if n_layers == 0 {
0.0
} else {
layer_t_center[n_layers - 1] + layer_thickness[n_layers - 1] * 0.5
};
let mut s_min_of: HashMap<usize, f32> = HashMap::new();
let mut layer_s_extent: Vec<f32> = vec![0.0; n_layers];
for (li, layer) in lg.layers.iter().enumerate() {
let mut s = 0.0f32;
let mut placed_real = false;
for &v in layer {
if v < lg.n_real {
let id = children[v];
let sz = child_sizes
.get(&id)
.copied()
.unwrap_or(layout_cfg.min_node_size);
let s_dim = if is_horizontal { sz.y } else { sz.x };
if placed_real {
s += cfg.node_spacing;
}
s_min_of.insert(v, s);
s += s_dim;
placed_real = true;
} else {
s_min_of.insert(v, s);
}
}
layer_s_extent[li] = s;
}
let mut out: HashMap<EntityId, egui::Pos2> = HashMap::new();
for (li, layer) in lg.layers.iter().enumerate() {
for &v in layer {
if v >= lg.n_real {
continue;
}
let id = children[v];
let sz = child_sizes
.get(&id)
.copied()
.unwrap_or(layout_cfg.min_node_size);
let (t_dim, _s_dim) = if is_horizontal {
(sz.x, sz.y)
} else {
(sz.y, sz.x)
};
let t_min = layer_t_center[li] - t_dim * 0.5;
let s_min = *s_min_of.get(&v).unwrap_or(&0.0);
let pos = match cfg.direction {
LayoutDirection::LR => egui::pos2(t_min, s_min),
LayoutDirection::RL => egui::pos2(total_t_extent - t_min - t_dim, s_min),
LayoutDirection::TB => egui::pos2(s_min, t_min),
LayoutDirection::BT => egui::pos2(s_min, total_t_extent - t_min - t_dim),
};
out.insert(id, pos);
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
fn make_pair(s: usize, d: usize) -> (usize, usize) {
(s, d)
}
fn edge3(s: usize, d: usize) -> (usize, usize, f32) {
(s, d, 0.0)
}
#[test]
fn greedy_fas_breaks_simple_cycle() {
let edges = vec![make_pair(0, 1), make_pair(1, 2), make_pair(2, 0)];
let reversed = greedy_fas(3, &edges);
let count = reversed.iter().filter(|x| **x).count();
assert_eq!(count, 1, "exactly one edge should be reversed for a 3-cycle");
}
#[test]
fn greedy_fas_acyclic_reverses_nothing() {
let edges = vec![make_pair(0, 1), make_pair(1, 2), make_pair(0, 2)];
let reversed = greedy_fas(3, &edges);
assert!(reversed.iter().all(|x| !*x));
}
#[test]
fn longest_path_diamond() {
let edges = vec![
make_pair(0, 1),
make_pair(0, 2),
make_pair(1, 3),
make_pair(2, 3),
];
let layers = assign_layers(4, &edges);
assert_eq!(layers[0], 0);
assert_eq!(layers[1], 1);
assert_eq!(layers[2], 1);
assert_eq!(layers[3], 2);
}
#[test]
fn longest_path_chain_with_shortcut() {
let edges = vec![
make_pair(0, 1),
make_pair(1, 2),
make_pair(2, 3),
make_pair(0, 3),
];
let layers = assign_layers(4, &edges);
assert_eq!(layers[0], 0);
assert_eq!(layers[1], 1);
assert_eq!(layers[2], 2);
assert_eq!(layers[3], 3);
}
#[test]
fn isolated_nodes_get_layer_zero() {
let edges: Vec<(usize, usize)> = vec![];
let layers = assign_layers(4, &edges);
for l in layers {
assert_eq!(l, 0);
}
}
#[test]
fn dummy_insertion_for_long_edges() {
let real_layers = vec![0, 1, 2, 3];
let edges = vec![edge3(0, 1), edge3(1, 2), edge3(2, 3), edge3(0, 3)];
let mut report = AutoLayoutReport::default();
let lg = build_layered_graph(4, &real_layers, &edges, &mut report);
assert_eq!(lg.layers.len(), 4);
assert_eq!(report.long_edges, 1, "the 0->3 edge should be marked long");
assert_eq!(lg.layers[1].len(), 2, "layer 1 has node 1 + dummy");
assert_eq!(lg.layers[2].len(), 2, "layer 2 has node 2 + dummy");
}
#[test]
fn gap_pill_thickness_tracks_max_per_gap() {
let real_layers = vec![0, 1, 2];
let edges = vec![(0, 1, 200.0), (1, 2, 50.0), (0, 2, 30.0)];
let mut report = AutoLayoutReport::default();
let lg = build_layered_graph(3, &real_layers, &edges, &mut report);
assert_eq!(lg.gap_pill_thickness.len(), 2);
assert_eq!(lg.gap_pill_thickness[0], 200.0);
assert_eq!(lg.gap_pill_thickness[1], 50.0);
}
#[test]
fn coords_layer_gap_grows_with_pill_thickness() {
let real_layers = vec![0, 1];
let cfg = AutoLayoutConfig::default();
let layout_cfg = LayoutConfig::default();
let children = vec![EntityId(1), EntityId(2)];
let mut sizes = HashMap::new();
sizes.insert(EntityId(1), egui::vec2(140.0, 60.0));
sizes.insert(EntityId(2), egui::vec2(140.0, 60.0));
let mut report = AutoLayoutReport::default();
let lg_thin = build_layered_graph(2, &real_layers, &[edge3(0, 1)], &mut report);
let pos_thin = assign_coordinates(&lg_thin, &children, &sizes, &cfg, &layout_cfg);
let mut report = AutoLayoutReport::default();
let fat = vec![(0usize, 1usize, 300.0f32)];
let lg_fat = build_layered_graph(2, &real_layers, &fat, &mut report);
let pos_fat = assign_coordinates(&lg_fat, &children, &sizes, &cfg, &layout_cfg);
let dx_thin = pos_thin[&EntityId(2)].x - pos_thin[&EntityId(1)].x;
let dx_fat = pos_fat[&EntityId(2)].x - pos_fat[&EntityId(1)].x;
let expected_growth = 300.0 - cfg.reserve_pill_gap;
assert!(
(dx_fat - dx_thin - expected_growth).abs() < 0.01,
"gap should grow by {} (was {}, became {})",
expected_growth,
dx_thin,
dx_fat
);
}
#[test]
fn barycenter_reduces_or_keeps_crossings() {
let real_layers = vec![0, 0, 1, 1];
let edges = vec![edge3(0, 3), edge3(1, 2)];
let mut report = AutoLayoutReport::default();
let mut lg = build_layered_graph(4, &real_layers, &edges, &mut report);
lg.layers[0] = vec![0, 1];
lg.layers[1] = vec![2, 3];
let crossings_before = count_crossings_for_test(&lg);
barycenter_sweeps(&mut lg, 8);
let crossings_after = count_crossings_for_test(&lg);
assert!(
crossings_after <= crossings_before,
"barycenter should not increase crossings ({} -> {})",
crossings_before,
crossings_after
);
assert_eq!(crossings_after, 0, "K2,2 should resolve to zero crossings");
}
fn count_crossings_for_test(lg: &LayeredGraph) -> u32 {
let mut total = 0u32;
for li in 0..lg.layers.len().saturating_sub(1) {
let upper = &lg.layers[li];
let lower = &lg.layers[li + 1];
let mut upper_pos: HashMap<usize, usize> = HashMap::new();
let mut lower_pos: HashMap<usize, usize> = HashMap::new();
for (i, &v) in upper.iter().enumerate() {
upper_pos.insert(v, i);
}
for (i, &v) in lower.iter().enumerate() {
lower_pos.insert(v, i);
}
let mut pairs: Vec<(usize, usize)> = Vec::new();
for (vi, _) in upper.iter().enumerate() {
let v = upper[vi];
for s in &lg.succ[v] {
if let Some(lp) = lower_pos.get(s) {
pairs.push((vi, *lp));
}
}
}
for i in 0..pairs.len() {
for j in (i + 1)..pairs.len() {
let (a1, b1) = pairs[i];
let (a2, b2) = pairs[j];
if (a1 < a2 && b1 > b2) || (a1 > a2 && b1 < b2) {
total += 1;
}
}
}
}
total
}
#[test]
fn coords_no_overlap_within_layer_lr() {
let real_layers = vec![0, 0, 1];
let edges: Vec<(usize, usize, f32)> = vec![];
let mut report = AutoLayoutReport::default();
let lg = build_layered_graph(3, &real_layers, &edges, &mut report);
let children = vec![EntityId(10), EntityId(20), EntityId(30)];
let mut sizes = HashMap::new();
sizes.insert(EntityId(10), egui::vec2(140.0, 60.0));
sizes.insert(EntityId(20), egui::vec2(140.0, 60.0));
sizes.insert(EntityId(30), egui::vec2(140.0, 60.0));
let cfg = AutoLayoutConfig::default();
let layout_cfg = LayoutConfig::default();
let positions = assign_coordinates(&lg, &children, &sizes, &cfg, &layout_cfg);
let p10 = positions[&EntityId(10)];
let p20 = positions[&EntityId(20)];
let dy = (p10.y - p20.y).abs();
assert!(
dy >= 60.0,
"layer-0 nodes should not overlap vertically (dy = {})",
dy
);
let p30 = positions[&EntityId(30)];
let dx_layer = p30.x - p10.x;
assert!(
dx_layer > 0.0,
"layer 1 should be to the right of layer 0 (dx = {})",
dx_layer
);
}
}