1use std::collections::HashMap;
12
13use super::graph::{Graph, Position};
14
15#[inline]
18fn f(n: usize) -> f32 {
19 debug_assert!(n <= (1 << 24), "layout value {n} exceeds f32 mantissa precision");
20 n as f32
21}
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
25pub enum LayoutAlgorithm {
26 #[default]
28 Grid,
29 ForceDirected,
31 Hierarchical,
33 Radial,
35 Circular,
37 Concentric,
39}
40
41#[derive(Debug, Clone)]
43pub struct LayoutConfig {
44 pub algorithm: LayoutAlgorithm,
46 pub width: f32,
48 pub height: f32,
50 pub iterations: usize,
52 pub optimal_distance: f32,
54 pub cooling: f32,
56}
57
58impl Default for LayoutConfig {
59 fn default() -> Self {
60 Self {
61 algorithm: LayoutAlgorithm::Grid,
62 width: 80.0,
63 height: 24.0,
64 iterations: 50,
65 optimal_distance: 3.0,
66 cooling: 0.95,
67 }
68 }
69}
70
71pub struct LayoutEngine;
73
74impl LayoutEngine {
75 pub fn compute<N, E>(graph: &mut Graph<N, E>, config: &LayoutConfig) {
77 match config.algorithm {
78 LayoutAlgorithm::Grid => Self::grid_layout(graph, config),
79 LayoutAlgorithm::ForceDirected => Self::force_directed_layout(graph, config),
80 LayoutAlgorithm::Hierarchical => Self::hierarchical_layout(graph, config),
81 LayoutAlgorithm::Circular => Self::circular_layout(graph, config),
82 LayoutAlgorithm::Concentric => Self::concentric_layout(graph, config),
83 LayoutAlgorithm::Radial => Self::radial_layout(graph, config),
84 }
85 }
86
87 fn grid_layout<N, E>(graph: &mut Graph<N, E>, config: &LayoutConfig) {
89 let n = graph.node_count();
90 if n == 0 {
91 return;
92 }
93
94 let cols = f(n).sqrt().ceil() as usize;
95 let rows = n.div_ceil(cols);
96
97 let cell_width = config.width / f(cols);
98 let cell_height = config.height / f(rows);
99
100 for (i, node) in graph.nodes_mut().enumerate() {
101 let col = i % cols;
102 let row = i / cols;
103 node.position =
104 Position::new((f(col) + 0.5) * cell_width, (f(row) + 0.5) * cell_height);
105 }
106 }
107
108 fn force_directed_layout<N, E>(graph: &mut Graph<N, E>, config: &LayoutConfig) {
111 let n = graph.node_count();
112 if n == 0 {
113 return;
114 }
115
116 Self::grid_layout(graph, config);
118
119 let k = config.optimal_distance;
120 let k_squared = k * k;
121 let mut temperature = config.width.min(config.height) / 10.0;
122
123 let node_ids: Vec<String> = graph.nodes.keys().cloned().collect();
125
126 for _ in 0..config.iterations {
127 let mut forces: HashMap<String, (f32, f32)> = HashMap::new();
129 for id in &node_ids {
130 forces.insert(id.clone(), (0.0, 0.0));
131 }
132
133 for i in 0..node_ids.len() {
135 for j in (i + 1)..node_ids.len() {
136 let id_i = &node_ids[i];
137 let id_j = &node_ids[j];
138
139 let pos_i = graph.nodes.get(id_i).map(|n| n.position).unwrap_or_default();
140 let pos_j = graph.nodes.get(id_j).map(|n| n.position).unwrap_or_default();
141
142 let dx = pos_i.x - pos_j.x;
143 let dy = pos_i.y - pos_j.y;
144 let dist = (dx * dx + dy * dy).sqrt().max(0.01);
145
146 let force = k_squared / dist;
148 let fx = (dx / dist) * force;
149 let fy = (dy / dist) * force;
150
151 if let Some(f) = forces.get_mut(id_i) {
152 f.0 += fx;
153 f.1 += fy;
154 }
155 if let Some(f) = forces.get_mut(id_j) {
156 f.0 -= fx;
157 f.1 -= fy;
158 }
159 }
160 }
161
162 for edge in &graph.edges {
164 let pos_from = graph.nodes.get(&edge.from).map(|n| n.position).unwrap_or_default();
165 let pos_to = graph.nodes.get(&edge.to).map(|n| n.position).unwrap_or_default();
166
167 let dx = pos_to.x - pos_from.x;
168 let dy = pos_to.y - pos_from.y;
169 let dist = (dx * dx + dy * dy).sqrt().max(0.01);
170
171 let force = (dist * dist) / k;
173 let fx = (dx / dist) * force;
174 let fy = (dy / dist) * force;
175
176 if let Some(f) = forces.get_mut(&edge.from) {
177 f.0 += fx;
178 f.1 += fy;
179 }
180 if let Some(f) = forces.get_mut(&edge.to) {
181 f.0 -= fx;
182 f.1 -= fy;
183 }
184 }
185
186 for id in &node_ids {
188 if let (Some(node), Some(&(fx, fy))) = (graph.nodes.get_mut(id), forces.get(id)) {
189 let mag = (fx * fx + fy * fy).sqrt().max(0.01);
190 let capped_mag = mag.min(temperature);
191 node.position.x += (fx / mag) * capped_mag;
192 node.position.y += (fy / mag) * capped_mag;
193
194 node.position.x = node.position.x.clamp(1.0, config.width - 1.0);
196 node.position.y = node.position.y.clamp(1.0, config.height - 1.0);
197 }
198 }
199
200 temperature *= config.cooling;
202 }
203 }
204
205 fn hierarchical_layout<N, E>(graph: &mut Graph<N, E>, config: &LayoutConfig) {
207 let n = graph.node_count();
208 if n == 0 {
209 return;
210 }
211
212 let mut in_degree: HashMap<String, usize> = HashMap::new();
214 for id in graph.nodes.keys() {
215 in_degree.insert(id.clone(), 0);
216 }
217 for edge in &graph.edges {
218 *in_degree.entry(edge.to.clone()).or_default() += 1;
219 }
220
221 let mut layers: HashMap<String, usize> = HashMap::new();
223 let mut queue: Vec<String> =
224 in_degree.iter().filter(|(_, &d)| d == 0).map(|(id, _)| id.clone()).collect();
225
226 for id in &queue {
227 layers.insert(id.clone(), 0);
228 }
229
230 let mut max_layer = 0;
231 while let Some(id) = queue.pop() {
232 let current_layer = *layers.get(&id).unwrap_or(&0);
233 for neighbor in graph.neighbors(&id) {
234 let new_layer = current_layer + 1;
235 let entry = layers.entry(neighbor.to_string()).or_insert(0);
236 if new_layer > *entry {
237 *entry = new_layer;
238 max_layer = max_layer.max(new_layer);
239 }
240 queue.push(neighbor.to_string());
241 }
242 }
243
244 let layer_height = config.height / f(max_layer + 1);
246 let mut layer_counts: HashMap<usize, usize> = HashMap::new();
247 let mut layer_positions: HashMap<usize, usize> = HashMap::new();
248
249 for &layer in layers.values() {
251 *layer_counts.entry(layer).or_default() += 1;
252 }
253
254 for node in graph.nodes_mut() {
256 let layer = *layers.get(&node.id).unwrap_or(&0);
257 let count = *layer_counts.get(&layer).unwrap_or(&1);
258 let pos_in_layer = *layer_positions.entry(layer).or_default();
259 layer_positions.insert(layer, pos_in_layer + 1);
260
261 let layer_width = config.width / f(count);
262 node.position = Position::new(
263 (f(pos_in_layer) + 0.5) * layer_width,
264 (f(layer) + 0.5) * layer_height,
265 );
266 }
267 }
268
269 fn radial_layout<N, E>(graph: &mut Graph<N, E>, config: &LayoutConfig) {
271 let n = graph.node_count();
272 if n == 0 {
273 return;
274 }
275
276 let center_x = config.width / 2.0;
277 let center_y = config.height / 2.0;
278 let radius = config.width.min(config.height) / 2.5;
279
280 let mut in_degree: HashMap<String, usize> = HashMap::new();
282 for id in graph.nodes.keys() {
283 in_degree.insert(id.clone(), 0);
284 }
285 for edge in &graph.edges {
286 *in_degree.entry(edge.to.clone()).or_default() += 1;
287 }
288
289 let root = in_degree.iter().find(|(_, &d)| d == 0).map(|(id, _)| id.clone());
290
291 if let Some(root_id) = root {
292 if let Some(node) = graph.nodes.get_mut(&root_id) {
294 node.position = Position::new(center_x, center_y);
295 }
296
297 let mut visited: HashMap<String, usize> = HashMap::new();
299 visited.insert(root_id.clone(), 0);
300 let root_id_clone = root_id.clone();
301 let mut queue = vec![root_id];
302 let mut depth_counts: HashMap<usize, usize> = HashMap::new();
303 let mut depth_positions: HashMap<usize, usize> = HashMap::new();
304
305 while let Some(id) = queue.pop() {
306 let depth = *visited.get(&id).unwrap_or(&0);
307 for neighbor in graph.neighbors(&id) {
308 if !visited.contains_key(neighbor) {
309 visited.insert(neighbor.to_string(), depth + 1);
310 *depth_counts.entry(depth + 1).or_default() += 1;
311 queue.push(neighbor.to_string());
312 }
313 }
314 }
315
316 let max_depth = visited.values().max().copied().unwrap_or(1);
318 let mut unvisited_idx = 0;
319 let unvisited_count = graph.node_count() - visited.len();
320
321 for node in graph.nodes_mut() {
322 if node.id == root_id_clone {
323 continue;
324 }
325
326 if let Some(&depth) = visited.get(&node.id) {
327 let count = *depth_counts.get(&depth).unwrap_or(&1);
329 let pos = *depth_positions.entry(depth).or_default();
330 depth_positions.insert(depth, pos + 1);
331
332 let angle = 2.0 * std::f32::consts::PI * (f(pos) / f(count));
333 let r = radius * (f(depth) / f(max_depth));
334
335 node.position = Position::new(
336 (center_x + r * angle.cos()).clamp(1.0, config.width - 1.0),
337 (center_y + r * angle.sin()).clamp(1.0, config.height - 1.0),
338 );
339 } else {
340 let angle =
342 2.0 * std::f32::consts::PI * (f(unvisited_idx) / f(unvisited_count.max(1)));
343 let r = radius * 1.2; unvisited_idx += 1;
345
346 node.position = Position::new(
347 (center_x + r * angle.cos()).clamp(1.0, config.width - 1.0),
348 (center_y + r * angle.sin()).clamp(1.0, config.height - 1.0),
349 );
350 }
351 }
352 } else {
353 Self::grid_layout(graph, config);
355 }
356 }
357
358 fn circular_layout<N, E>(graph: &mut Graph<N, E>, config: &LayoutConfig) {
360 let n = graph.node_count();
361 if n == 0 {
362 return;
363 }
364
365 let center_x = config.width / 2.0;
366 let center_y = config.height / 2.0;
367 let radius = config.width.min(config.height) / 2.5;
368
369 for (i, node) in graph.nodes_mut().enumerate() {
370 let angle = 2.0 * std::f32::consts::PI * (f(i) / f(n));
371 node.position = Position::new(
372 (center_x + radius * angle.cos()).clamp(1.0, config.width - 1.0),
373 (center_y + radius * angle.sin()).clamp(1.0, config.height - 1.0),
374 );
375 }
376 }
377
378 fn concentric_layout<N, E>(graph: &mut Graph<N, E>, config: &LayoutConfig) {
380 let n = graph.node_count();
381 if n == 0 {
382 return;
383 }
384
385 let center_x = config.width / 2.0;
386 let center_y = config.height / 2.0;
387 let max_radius = config.width.min(config.height) / 2.5;
388
389 let mut node_order: Vec<(String, f32)> =
391 graph.nodes().map(|n| (n.id.clone(), n.importance)).collect();
392 node_order.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
393
394 let num_rings = 4.min(n);
396 let nodes_per_ring = n.div_ceil(num_rings);
397
398 for (idx, (node_id, _)) in node_order.iter().enumerate() {
399 let ring = idx / nodes_per_ring;
400 let pos_in_ring = idx % nodes_per_ring;
401 let nodes_in_this_ring =
402 if ring == num_rings - 1 { n - ring * nodes_per_ring } else { nodes_per_ring };
403
404 let radius = if ring == 0 && nodes_in_this_ring == 1 {
406 0.0 } else {
408 max_radius * (f(ring + 1) / f(num_rings))
409 };
410
411 let angle = 2.0 * std::f32::consts::PI * (f(pos_in_ring) / f(nodes_in_this_ring));
412
413 if let Some(node) = graph.nodes.get_mut(node_id) {
414 node.position = Position::new(
415 (center_x + radius * angle.cos()).clamp(1.0, config.width - 1.0),
416 (center_y + radius * angle.sin()).clamp(1.0, config.height - 1.0),
417 );
418 }
419 }
420 }
421}
422
423#[cfg(test)]
424mod tests {
425 use super::*;
426 use crate::tui::graph::Node;
427
428 #[test]
429 fn test_layout_algorithm_default() {
430 assert_eq!(LayoutAlgorithm::default(), LayoutAlgorithm::Grid);
431 }
432
433 #[test]
434 fn test_layout_config_default() {
435 let config = LayoutConfig::default();
436 assert_eq!(config.algorithm, LayoutAlgorithm::Grid);
437 assert_eq!(config.width, 80.0);
438 assert_eq!(config.height, 24.0);
439 assert_eq!(config.iterations, 50);
440 }
441
442 #[test]
443 fn test_grid_layout_empty() {
444 let mut graph: Graph<(), ()> = Graph::new();
445 let config = LayoutConfig::default();
446 LayoutEngine::compute(&mut graph, &config);
447 assert_eq!(graph.node_count(), 0);
448 }
449
450 #[test]
451 fn test_grid_layout_single() {
452 let mut graph: Graph<(), ()> = Graph::new();
453 graph.add_node(Node::new("a", ()));
454 let config = LayoutConfig::default();
455 LayoutEngine::compute(&mut graph, &config);
456
457 let node = graph.nodes.get("a").expect("key not found");
458 assert!(node.position.x > 0.0);
459 assert!(node.position.y > 0.0);
460 }
461
462 #[test]
463 fn test_circular_layout() {
464 let mut graph: Graph<(), ()> = Graph::new();
465 graph.add_node(Node::new("a", ()));
466 graph.add_node(Node::new("b", ()));
467 graph.add_node(Node::new("c", ()));
468
469 let config = LayoutConfig { algorithm: LayoutAlgorithm::Circular, ..Default::default() };
470 LayoutEngine::compute(&mut graph, &config);
471
472 for node in graph.nodes() {
474 assert!(node.position.x > 0.0);
475 assert!(node.position.y > 0.0);
476 }
477 }
478
479 #[test]
480 fn test_force_directed_layout() {
481 use crate::tui::graph::Edge;
482 let mut graph: Graph<(), ()> = Graph::new();
483 graph.add_node(Node::new("a", ()));
484 graph.add_node(Node::new("b", ()));
485 graph.add_edge(Edge::new("a", "b", ()));
486
487 let config = LayoutConfig {
488 algorithm: LayoutAlgorithm::ForceDirected,
489 iterations: 5,
490 ..Default::default()
491 };
492 LayoutEngine::compute(&mut graph, &config);
493
494 for node in graph.nodes() {
496 assert!(node.position.x >= 1.0);
497 assert!(node.position.y >= 1.0);
498 assert!(node.position.x <= config.width - 1.0);
499 assert!(node.position.y <= config.height - 1.0);
500 }
501 }
502
503 #[test]
504 fn test_hierarchical_layout() {
505 use crate::tui::graph::Edge;
506 let mut graph: Graph<(), ()> = Graph::new();
507 graph.add_node(Node::new("root", ()));
508 graph.add_node(Node::new("child1", ()));
509 graph.add_node(Node::new("child2", ()));
510 graph.add_edge(Edge::new("root", "child1", ()));
511 graph.add_edge(Edge::new("root", "child2", ()));
512
513 let config =
514 LayoutConfig { algorithm: LayoutAlgorithm::Hierarchical, ..Default::default() };
515 LayoutEngine::compute(&mut graph, &config);
516
517 for node in graph.nodes() {
519 assert!(node.position.x > 0.0);
520 assert!(node.position.y > 0.0);
521 }
522 }
523
524 #[test]
525 fn test_radial_layout() {
526 use crate::tui::graph::Edge;
527 let mut graph: Graph<(), ()> = Graph::new();
528 graph.add_node(Node::new("center", ()));
529 graph.add_node(Node::new("leaf1", ()));
530 graph.add_node(Node::new("leaf2", ()));
531 graph.add_edge(Edge::new("center", "leaf1", ()));
532 graph.add_edge(Edge::new("center", "leaf2", ()));
533
534 let config = LayoutConfig { algorithm: LayoutAlgorithm::Radial, ..Default::default() };
535 LayoutEngine::compute(&mut graph, &config);
536
537 for node in graph.nodes() {
539 assert!(node.position.x > 0.0);
540 assert!(node.position.y > 0.0);
541 }
542 }
543
544 #[test]
545 fn test_concentric_layout() {
546 let mut graph: Graph<(), ()> = Graph::new();
547 let mut node1 = Node::new("important", ());
548 node1.importance = 1.0;
549 let mut node2 = Node::new("less", ());
550 node2.importance = 0.5;
551 let mut node3 = Node::new("least", ());
552 node3.importance = 0.1;
553 graph.add_node(node1);
554 graph.add_node(node2);
555 graph.add_node(node3);
556
557 let config = LayoutConfig { algorithm: LayoutAlgorithm::Concentric, ..Default::default() };
558 LayoutEngine::compute(&mut graph, &config);
559
560 for node in graph.nodes() {
562 assert!(node.position.x > 0.0);
563 assert!(node.position.y > 0.0);
564 }
565 }
566
567 #[test]
568 fn test_radial_layout_no_root() {
569 use crate::tui::graph::Edge;
570 let mut graph: Graph<(), ()> = Graph::new();
571 graph.add_node(Node::new("a", ()));
572 graph.add_node(Node::new("b", ()));
573 graph.add_edge(Edge::new("a", "b", ()));
575 graph.add_edge(Edge::new("b", "a", ()));
576
577 let config = LayoutConfig { algorithm: LayoutAlgorithm::Radial, ..Default::default() };
578 LayoutEngine::compute(&mut graph, &config);
579
580 for node in graph.nodes() {
582 assert!(node.position.x > 0.0);
583 assert!(node.position.y > 0.0);
584 }
585 }
586
587 #[test]
588 fn test_layout_algorithm_variants() {
589 assert_ne!(LayoutAlgorithm::Grid, LayoutAlgorithm::ForceDirected);
590 assert_ne!(LayoutAlgorithm::Hierarchical, LayoutAlgorithm::Radial);
591 assert_ne!(LayoutAlgorithm::Circular, LayoutAlgorithm::Concentric);
592 }
593
594 #[test]
595 fn test_layout_config_custom() {
596 let config = LayoutConfig {
597 algorithm: LayoutAlgorithm::ForceDirected,
598 width: 100.0,
599 height: 50.0,
600 iterations: 100,
601 optimal_distance: 5.0,
602 cooling: 0.9,
603 };
604 assert_eq!(config.iterations, 100);
605 assert_eq!(config.optimal_distance, 5.0);
606 assert_eq!(config.cooling, 0.9);
607 }
608
609 #[test]
610 fn test_grid_layout_multiple_rows() {
611 let mut graph: Graph<(), ()> = Graph::new();
612 for i in 0..6 {
613 graph.add_node(Node::new(&format!("n{}", i), ()));
614 }
615
616 let config = LayoutConfig::default();
617 LayoutEngine::compute(&mut graph, &config);
618
619 let positions: Vec<_> = graph.nodes().map(|n| (n.position.x, n.position.y)).collect();
621 for i in 0..positions.len() {
622 for j in (i + 1)..positions.len() {
623 assert!(positions[i] != positions[j] || i == j);
624 }
625 }
626 }
627
628 #[test]
629 fn test_hierarchical_layout_empty() {
630 let mut graph: Graph<(), ()> = Graph::new();
631 let config =
632 LayoutConfig { algorithm: LayoutAlgorithm::Hierarchical, ..Default::default() };
633 LayoutEngine::compute(&mut graph, &config);
634 assert_eq!(graph.node_count(), 0);
635 }
636
637 #[test]
638 fn test_radial_layout_empty() {
639 let mut graph: Graph<(), ()> = Graph::new();
640 let config = LayoutConfig { algorithm: LayoutAlgorithm::Radial, ..Default::default() };
641 LayoutEngine::compute(&mut graph, &config);
642 assert_eq!(graph.node_count(), 0);
643 }
644
645 #[test]
646 fn test_force_directed_layout_empty() {
647 let mut graph: Graph<(), ()> = Graph::new();
648 let config =
649 LayoutConfig { algorithm: LayoutAlgorithm::ForceDirected, ..Default::default() };
650 LayoutEngine::compute(&mut graph, &config);
651 assert_eq!(graph.node_count(), 0);
652 }
653
654 #[test]
655 fn test_circular_layout_empty() {
656 let mut graph: Graph<(), ()> = Graph::new();
657 let config = LayoutConfig { algorithm: LayoutAlgorithm::Circular, ..Default::default() };
658 LayoutEngine::compute(&mut graph, &config);
659 assert_eq!(graph.node_count(), 0);
660 }
661
662 #[test]
663 fn test_concentric_layout_empty() {
664 let mut graph: Graph<(), ()> = Graph::new();
665 let config = LayoutConfig { algorithm: LayoutAlgorithm::Concentric, ..Default::default() };
666 LayoutEngine::compute(&mut graph, &config);
667 assert_eq!(graph.node_count(), 0);
668 }
669}