1use std::{
10 collections::BTreeMap,
11 error::Error,
12 fmt::{self, Debug},
13};
14
15use rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet};
16
17use crate::{
18 graph::{
19 Graph, GraphMut,
20 analysis::{SeseRegion, SeseTree, compute_sese, compute_sese_candidates},
21 edge::Edge,
22 node::Node,
23 owning::OwningGraph,
24 },
25 registry::Identifier,
26 triskel::{
27 coordinate, cycle,
28 hammock::compute_hammocks,
29 order, rank,
30 render::{render_html, render_html_with_labels, render_svg, render_svg_with_labels},
31 router::{EdgeRouter, EdgeStyle, OrthogonalRouter, StraightRouter},
32 segment,
33 },
34};
35
36const DEFAULT_NODE_SIZE: f64 = 24.0;
37const DEFAULT_LAYER_GAP: f64 = 50.0;
38const DEFAULT_NODE_GAP: f64 = 40.0;
39const DEFAULT_MAX_SWEEPS: usize = 8;
40
41const SELF_LOOP_GAP: f64 = 16.0;
43const SELF_LOOP_STEP: f64 = 8.0;
45
46pub(crate) fn loop_reserve(count: u32) -> f64 {
48 if count == 0 {
49 0.0
50 } else {
51 SELF_LOOP_GAP + (count - 1) as f64 * SELF_LOOP_STEP
52 }
53}
54
55fn self_loop_waypoints(
60 x: f64,
61 y: f64,
62 width: f64,
63 height: f64,
64 index: u32,
65 total: u32,
66) -> Vec<Point> {
67 let right = x + width / 2.0;
68 let span = height / 2.0 * (index + 1) as f64 / (total + 1) as f64;
69 let out = right + SELF_LOOP_GAP + index as f64 * SELF_LOOP_STEP;
70 vec![
71 Point {
72 x: right,
73 y: y - span,
74 },
75 Point {
76 x: out,
77 y: y - span,
78 },
79 Point {
80 x: out,
81 y: y + span,
82 },
83 Point {
84 x: right,
85 y: y + span,
86 },
87 ]
88}
89
90#[derive(Debug, Clone, Copy, Default, PartialEq)]
93pub struct Point {
94 pub x: f64,
95 pub y: f64,
96}
97
98#[derive(Debug, Clone, Copy, PartialEq)]
99pub struct NodeGeometry {
100 pub width: f64,
101 pub height: f64,
102}
103
104impl Default for NodeGeometry {
105 fn default() -> Self {
106 Self {
107 width: DEFAULT_NODE_SIZE,
108 height: DEFAULT_NODE_SIZE,
109 }
110 }
111}
112
113#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
114pub enum LayoutMode {
115 #[default]
117 Flat,
118 Sese,
120}
121
122#[derive(Debug, Clone, Copy, PartialEq)]
123pub struct LayoutSettings {
124 pub layer_gap: f64,
126 pub node_gap: f64,
128 pub edge_style: EdgeStyle,
130 pub max_sweeps: usize,
132 pub mode: LayoutMode,
134}
135
136impl Default for LayoutSettings {
137 fn default() -> Self {
138 Self {
139 layer_gap: DEFAULT_LAYER_GAP,
140 node_gap: DEFAULT_NODE_GAP,
141 edge_style: EdgeStyle::default(),
142 max_sweeps: DEFAULT_MAX_SWEEPS,
143 mode: LayoutMode::Flat,
144 }
145 }
146}
147
148#[derive(Debug, Clone, Copy, PartialEq, Eq)]
149pub enum LayoutError {
150 EmptyGraph,
151}
152
153impl fmt::Display for LayoutError {
154 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
155 match self {
156 LayoutError::EmptyGraph => f.write_str("cannot lay out an empty graph"),
157 }
158 }
159}
160
161impl Error for LayoutError {}
162
163#[derive(Debug, Clone, Copy, PartialEq)]
164pub struct LayoutNode<NodeId: Identifier> {
165 pub id: NodeId,
166 pub x: f64,
167 pub y: f64,
168 pub width: f64,
169 pub height: f64,
170}
171
172#[derive(Debug, Clone)]
173pub struct LayoutResult<NodeId: Identifier, EdgeId: Identifier> {
174 pub nodes: HashMap<NodeId, LayoutNode<NodeId>>,
175 pub edges: HashMap<EdgeId, Vec<Point>>,
176 pub regions: Vec<LayoutRegion>,
179}
180
181#[derive(Debug, Clone, Copy, PartialEq)]
182pub struct LayoutRegion {
183 pub id: usize,
184 pub parent: Option<usize>,
185 pub x: f64,
186 pub y: f64,
187 pub width: f64,
188 pub height: f64,
189}
190
191impl<NodeId: Identifier, EdgeId: Identifier> LayoutResult<NodeId, EdgeId> {
192 pub fn get_node(&self, node: NodeId) -> Option<&LayoutNode<NodeId>> {
193 self.nodes.get(&node)
194 }
195
196 pub fn get_waypoints(&self, edge: EdgeId) -> Option<&[Point]> {
197 self.edges.get(&edge).map(Vec::as_slice)
198 }
199
200 pub fn render_svg(&self) -> String {
201 render_svg::<NodeId, EdgeId>(self)
202 }
203
204 pub fn render_svg_with_labels<F>(&self, label_for: F) -> String
205 where
206 F: FnMut(NodeId) -> String,
207 {
208 render_svg_with_labels::<NodeId, EdgeId, F>(self, label_for)
209 }
210
211 pub fn render_html(&self, title: &str) -> String {
212 render_html::<NodeId, EdgeId>(self, title)
213 }
214
215 pub fn render_html_with_labels<F>(&self, title: &str, label_for: F) -> String
216 where
217 F: FnMut(NodeId) -> String,
218 {
219 render_html_with_labels::<NodeId, EdgeId, F>(self, title, label_for)
220 }
221}
222
223#[derive(Clone, Copy)]
226pub(crate) struct NodeLayoutData {
227 pub width: f64,
228 pub height: f64,
229 pub rank: i64,
230 pub order: usize,
231 pub x: f64,
232 pub y: f64,
233 pub is_dummy: bool,
234 pub self_loops: u32,
238}
239
240impl Default for NodeLayoutData {
241 fn default() -> Self {
242 Self {
243 width: DEFAULT_NODE_SIZE,
244 height: DEFAULT_NODE_SIZE,
245 rank: 0,
246 order: 0,
247 x: 0.0,
248 y: 0.0,
249 is_dummy: false,
250 self_loops: 0,
251 }
252 }
253}
254
255#[derive(Clone, Copy)]
256pub(crate) struct EdgeLayoutData {
257 pub minlen: i64,
258 pub weight: i64,
259 pub reversed: bool,
261 pub orig: usize,
263 pub port_start: Option<f64>,
266 pub port_end: Option<f64>,
267}
268
269impl Default for EdgeLayoutData {
270 fn default() -> Self {
271 Self {
272 minlen: 1,
273 weight: 1,
274 reversed: false,
275 orig: 0,
276 port_start: None,
277 port_end: None,
278 }
279 }
280}
281
282pub(crate) type LayoutGraph = OwningGraph<usize, usize, NodeLayoutData, EdgeLayoutData>;
283
284#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
285enum RegionPortId<NodeId> {
286 Entry,
287 Exit { source: NodeId },
288}
289
290#[derive(Clone, Copy, Debug)]
291struct ProxyPort<NodeId> {
292 id: RegionPortId<NodeId>,
293 x_offset: f64,
294 is_entry: bool,
295}
296
297#[derive(Clone, Copy, Debug, Default)]
298struct PortHint {
299 source_bottom: Option<f64>,
300 target_top: Option<f64>,
301}
302
303pub(crate) fn layer_of(graph: &LayoutGraph, node: usize) -> usize {
305 graph.get_node(node).unwrap().rank.max(0) as usize
306}
307
308pub struct LayoutBuilder<'g, NodeId, EdgeId, NodeData, EdgeData>
311where
312 NodeId: Identifier,
313 EdgeId: Identifier,
314{
315 graph: &'g OwningGraph<NodeId, EdgeId, NodeData, EdgeData>,
316 root: Option<NodeId>,
317 settings: LayoutSettings,
318 geometry: Box<dyn FnMut(NodeId) -> NodeGeometry + 'g>,
319}
320
321impl<'g, NodeId, EdgeId, NodeData, EdgeData> LayoutBuilder<'g, NodeId, EdgeId, NodeData, EdgeData>
322where
323 NodeId: Identifier + Debug,
324 EdgeId: Identifier + Debug,
325{
326 pub fn new(graph: &'g OwningGraph<NodeId, EdgeId, NodeData, EdgeData>) -> Self {
327 Self {
328 graph,
329 root: None,
330 settings: LayoutSettings::default(),
331 geometry: Box::new(|_| NodeGeometry::default()),
332 }
333 }
334
335 pub fn root(mut self, root: NodeId) -> Self {
338 self.root = Some(root);
339 self
340 }
341
342 pub fn node_gap(mut self, gap: f64) -> Self {
343 self.settings.node_gap = gap;
344 self
345 }
346
347 pub fn layer_gap(mut self, gap: f64) -> Self {
348 self.settings.layer_gap = gap;
349 self
350 }
351
352 pub fn edge_style(mut self, style: EdgeStyle) -> Self {
353 self.settings.edge_style = style;
354 self
355 }
356
357 pub fn max_sweeps(mut self, sweeps: usize) -> Self {
358 self.settings.max_sweeps = sweeps;
359 self
360 }
361
362 pub fn mode(mut self, mode: LayoutMode) -> Self {
363 self.settings.mode = mode;
364 self
365 }
366
367 pub fn settings(mut self, settings: LayoutSettings) -> Self {
368 self.settings = settings;
369 self
370 }
371
372 pub fn geometry<F>(mut self, geometry: F) -> Self
373 where
374 F: FnMut(NodeId) -> NodeGeometry + 'g,
375 {
376 self.geometry = Box::new(geometry);
377 self
378 }
379
380 pub fn build(mut self) -> Result<LayoutResult<NodeId, EdgeId>, LayoutError> {
381 let mut node_ids: Vec<NodeId> = self.graph.nodes().map(|n| n.id()).collect();
382 node_ids.sort_by_key(|id| Into::<usize>::into(*id));
383 if node_ids.is_empty() {
384 return Err(LayoutError::EmptyGraph);
385 }
386
387 let mut geometry = HashMap::default();
388 for id in &node_ids {
389 let g = (self.geometry)(*id);
390 geometry.insert(
391 *id,
392 NodeGeometry {
393 width: g.width.max(1.0),
394 height: g.height.max(1.0),
395 },
396 );
397 }
398
399 let components = weakly_connected_components(self.graph, &node_ids);
400
401 let mut nodes = HashMap::default();
402 let mut edges = HashMap::default();
403 let mut regions = Vec::new();
404 let mut x_offset = 0.0f64;
405
406 for component in components {
407 let local = match self.settings.mode {
408 LayoutMode::Flat => {
409 layout_component(self.graph, &component, &geometry, self.root, &self.settings)
410 }
411 LayoutMode::Sese => layout_component_sese(
412 self.graph,
413 &component,
414 &geometry,
415 self.root,
416 &self.settings,
417 ),
418 };
419
420 let (min_x, max_x, min_y) = local_bounds(&local);
421 let shift_x = x_offset - min_x;
422 let shift_y = -min_y;
423
424 for (id, mut node) in local.nodes {
425 node.x += shift_x;
426 node.y += shift_y;
427 nodes.insert(id, node);
428 }
429 let region_base = regions.len();
430 for region in local.regions {
431 regions.push(LayoutRegion {
432 id: region_base + region.id,
433 parent: region.parent.map(|parent| region_base + parent),
434 x: region.x + shift_x,
435 y: region.y + shift_y,
436 ..region
437 });
438 }
439 for (id, points) in local.edges {
440 edges.insert(
441 id,
442 points
443 .into_iter()
444 .map(|p| Point {
445 x: p.x + shift_x,
446 y: p.y + shift_y,
447 })
448 .collect(),
449 );
450 }
451
452 x_offset += (max_x - min_x) + self.settings.node_gap;
453 }
454
455 Ok(LayoutResult {
456 nodes,
457 edges,
458 regions,
459 })
460 }
461}
462
463struct ComponentLayout<NodeId: Identifier, EdgeId: Identifier> {
466 nodes: HashMap<NodeId, LayoutNode<NodeId>>,
467 edges: HashMap<EdgeId, Vec<Point>>,
468 regions: Vec<LayoutRegion>,
469}
470
471fn compute_sese_candidates_normalized<NodeId, EdgeId, NodeData, EdgeData>(
475 graph: &OwningGraph<NodeId, EdgeId, NodeData, EdgeData>,
476 component: &[NodeId],
477 root: NodeId,
478) -> Vec<(Vec<NodeId>, EdgeId, EdgeId)>
479where
480 NodeId: Identifier + Debug + Ord,
481 EdgeId: Identifier + Debug + Ord,
482{
483 let mut augmented = OwningGraph::<usize, usize, (), ()>::default();
484 let super_entry = augmented.make_node(());
485 let super_exit = augmented.make_node(());
486 let mut augmented_node = HashMap::default();
487 let mut original_node = HashMap::default();
488 for &node in component {
489 let augmented_id = augmented.make_node(());
490 augmented_node.insert(node, augmented_id);
491 original_node.insert(augmented_id, node);
492 }
493 augmented.make_edge(super_entry, augmented_node[&root], ());
494 let component_set: HashSet<_> = component.iter().copied().collect();
495 let mut has_real_out = HashMap::<NodeId, bool>::default();
496 for &node in component {
497 has_real_out.insert(node, false);
498 }
499 let mut original_edge = HashMap::default();
500 let mut edge_ids: Vec<_> = graph.edges().map(|edge| edge.id()).collect();
501 edge_ids.sort();
502 for edge_id in edge_ids {
503 let edge = graph.get_edge(edge_id).unwrap();
504 let from = edge.from_id();
505 let to = edge.to_id();
506 if !component_set.contains(&from) || !component_set.contains(&to) {
507 continue;
508 }
509 let augmented_id = augmented.make_edge(augmented_node[&from], augmented_node[&to], ());
510 original_edge.insert(augmented_id, edge_id);
511 if from != to {
512 has_real_out.insert(from, true);
513 }
514 }
515 let exits: Vec<_> = component
516 .iter()
517 .copied()
518 .filter(|node| !has_real_out[node])
519 .collect();
520 if exits.is_empty() {
521 augmented.make_edge(augmented_node[component.last().unwrap()], super_exit, ());
522 } else {
523 for node in exits {
524 augmented.make_edge(augmented_node[&node], super_exit, ());
525 }
526 }
527
528 compute_sese_candidates(&augmented, super_entry)
529 .into_iter()
530 .filter_map(|candidate| {
531 Some((
532 candidate
533 .contained_nodes
534 .into_iter()
535 .map(|node| original_node.get(&node).copied())
536 .collect::<Option<Vec<_>>>()?,
537 original_edge.get(&candidate.entry_edge).copied()?,
538 original_edge.get(&candidate.exit_edge).copied()?,
539 ))
540 })
541 .collect()
542}
543
544fn compute_sese_normalized<NodeId, EdgeId, NodeData, EdgeData>(
549 graph: &OwningGraph<NodeId, EdgeId, NodeData, EdgeData>,
550 component: &[NodeId],
551 root: NodeId,
552) -> SeseTree<NodeId, EdgeId>
553where
554 NodeId: Identifier + Debug + Ord,
555 EdgeId: Identifier + Debug + Ord,
556{
557 let mut augmented = OwningGraph::<usize, usize, (), ()>::default();
558 let super_entry = augmented.make_node(());
559 let super_exit = augmented.make_node(());
560 let mut augmented_node = HashMap::default();
561 let mut original_node = HashMap::default();
562 for &node in component {
563 let augmented_id = augmented.make_node(());
564 augmented_node.insert(node, augmented_id);
565 original_node.insert(augmented_id, node);
566 }
567 augmented.make_edge(super_entry, augmented_node[&root], ());
568
569 let component_set: HashSet<_> = component.iter().copied().collect();
570 let mut has_real_out = HashMap::<NodeId, bool>::default();
571 for &node in component {
572 has_real_out.insert(node, false);
573 }
574 let mut original_edge = HashMap::default();
575 let mut edge_ids: Vec<_> = graph.edges().map(|edge| edge.id()).collect();
576 edge_ids.sort();
577 for edge_id in edge_ids {
578 let edge = graph.get_edge(edge_id).unwrap();
579 let from = edge.from_id();
580 let to = edge.to_id();
581 if !component_set.contains(&from) || !component_set.contains(&to) {
582 continue;
583 }
584 let augmented_id = augmented.make_edge(augmented_node[&from], augmented_node[&to], ());
585 original_edge.insert(augmented_id, edge_id);
586 if from != to {
587 has_real_out.insert(from, true);
588 }
589 }
590 let exits: Vec<_> = component
591 .iter()
592 .copied()
593 .filter(|node| !has_real_out[node])
594 .collect();
595 if exits.is_empty() {
596 augmented.make_edge(augmented_node[component.last().unwrap()], super_exit, ());
598 } else {
599 for node in exits {
600 augmented.make_edge(augmented_node[&node], super_exit, ());
601 }
602 }
603
604 let raw = compute_sese(&augmented, super_entry);
605 let mut old_to_new = vec![None; raw.regions.len()];
606 old_to_new[0] = Some(0);
607 let mut regions = vec![SeseRegion {
608 parent: None,
609 children: Vec::new(),
610 entry_edge: None,
611 exit_edge: None,
612 nodes: Vec::new(),
613 contained_nodes: component.to_vec(),
614 }];
615 for (old, region) in raw.regions.iter().enumerate().skip(1) {
616 let Some(entry) = region
617 .entry_edge
618 .and_then(|edge| original_edge.get(&edge))
619 .copied()
620 else {
621 continue;
622 };
623 let Some(exit) = region
624 .exit_edge
625 .and_then(|edge| original_edge.get(&edge))
626 .copied()
627 else {
628 continue;
629 };
630 let Some(contained_nodes) = region
631 .contained_nodes
632 .iter()
633 .map(|node| original_node.get(node).copied())
634 .collect::<Option<Vec<_>>>()
635 else {
636 continue;
637 };
638 if contained_nodes.is_empty() {
639 continue;
640 }
641 let new = regions.len();
642 old_to_new[old] = Some(new);
643 regions.push(SeseRegion {
644 parent: None,
645 children: Vec::new(),
646 entry_edge: Some(entry),
647 exit_edge: Some(exit),
648 nodes: Vec::new(),
649 contained_nodes,
650 });
651 }
652 for old in 1..raw.regions.len() {
653 let Some(new) = old_to_new[old] else {
654 continue;
655 };
656 let mut parent = raw.regions[old].parent;
657 while let Some(old_parent) = parent {
658 if let Some(new_parent) = old_to_new[old_parent] {
659 regions[new].parent = Some(new_parent);
660 regions[new_parent].children.push(new);
661 break;
662 }
663 parent = raw.regions[old_parent].parent;
664 }
665 }
666 for &node in component {
667 let owner = regions
668 .iter()
669 .enumerate()
670 .skip(1)
671 .filter(|(_, region)| region.contained_nodes.contains(&node))
672 .min_by_key(|(_, region)| region.contained_nodes.len())
673 .map(|(id, _)| id)
674 .unwrap_or(0);
675 regions[owner].nodes.push(node);
676 }
677 SeseTree { regions }
678}
679
680#[cfg(test)]
683fn compute_hammock_candidates<NodeId, EdgeId, NodeData, EdgeData>(
684 graph: &OwningGraph<NodeId, EdgeId, NodeData, EdgeData>,
685 component: &[NodeId],
686) -> Vec<(Vec<NodeId>, EdgeId, EdgeId)>
687where
688 NodeId: Identifier + Debug + Ord,
689 EdgeId: Identifier + Debug + Ord,
690{
691 let component_set: HashSet<_> = component.iter().copied().collect();
692 let mut edges: Vec<_> = graph
693 .edges()
694 .filter_map(|edge| {
695 (component_set.contains(&edge.from_id()) && component_set.contains(&edge.to_id()))
696 .then_some((edge.id(), edge.from_id(), edge.to_id()))
697 })
698 .collect();
699 edges.sort_by_key(|(id, _, _)| *id);
700 let mut candidates = Vec::<(Vec<NodeId>, EdgeId, EdgeId)>::new();
701 for &entry in component {
702 for &exit in component {
703 if entry == exit {
704 continue;
705 }
706 let mut forward = HashSet::default();
707 let mut stack = vec![entry];
708 while let Some(node) = stack.pop() {
709 if node == exit || !forward.insert(node) {
710 continue;
711 }
712 stack.extend(
713 edges
714 .iter()
715 .filter_map(|(_, from, to)| (*from == node).then_some(*to)),
716 );
717 }
718 let mut reverse = HashSet::default();
719 let mut stack = vec![exit];
720 while let Some(node) = stack.pop() {
721 if !reverse.insert(node) {
722 continue;
723 }
724 stack.extend(
725 edges
726 .iter()
727 .filter_map(|(_, from, to)| (*to == node).then_some(*from)),
728 );
729 }
730 let contained: HashSet<_> = forward.intersection(&reverse).copied().collect();
731 if contained.len() <= 1 || !contained.contains(&entry) {
732 continue;
733 }
734 let incoming: Vec<_> = edges
735 .iter()
736 .filter(|(_, from, to)| !contained.contains(from) && contained.contains(to))
737 .collect();
738 let outgoing: Vec<_> = edges
739 .iter()
740 .filter(|(_, from, to)| contained.contains(from) && !contained.contains(to))
741 .collect();
742 if incoming.is_empty()
743 || outgoing.is_empty()
744 || incoming.iter().any(|(_, _, to)| *to != entry)
745 || outgoing.iter().any(|(_, _, to)| *to != exit)
746 {
747 continue;
748 }
749 let mut nodes: Vec<_> = contained.into_iter().collect();
750 nodes.sort();
751 candidates.push((nodes, incoming[0].0, outgoing[0].0));
752 }
753 }
754 candidates.sort_by_key(|(nodes, entry, exit)| (std::cmp::Reverse(nodes.len()), *entry, *exit));
755 candidates
756}
757
758#[cfg(test)]
760fn compute_hammock_fallback<NodeId, EdgeId, NodeData, EdgeData>(
761 graph: &OwningGraph<NodeId, EdgeId, NodeData, EdgeData>,
762 component: &[NodeId],
763) -> SeseTree<NodeId, EdgeId>
764where
765 NodeId: Identifier + Debug + Ord,
766 EdgeId: Identifier + Debug + Ord,
767{
768 let candidates = compute_hammock_candidates(graph, component);
769 let mut regions = vec![SeseRegion {
770 parent: None,
771 children: Vec::new(),
772 entry_edge: None,
773 exit_edge: None,
774 nodes: Vec::new(),
775 contained_nodes: component.to_vec(),
776 }];
777 for (nodes, entry, exit) in candidates {
778 if regions.iter().skip(1).any(|region| {
779 region
780 .contained_nodes
781 .iter()
782 .any(|node| nodes.contains(node))
783 }) {
784 continue;
785 }
786 let id = regions.len();
787 regions.push(SeseRegion {
788 parent: Some(0),
789 children: Vec::new(),
790 entry_edge: Some(entry),
791 exit_edge: Some(exit),
792 nodes: Vec::new(),
793 contained_nodes: nodes,
794 });
795 regions[0].children.push(id);
796 }
797 for &node in component {
798 let owner = regions
799 .iter()
800 .enumerate()
801 .skip(1)
802 .find_map(|(id, region)| region.contained_nodes.contains(&node).then_some(id))
803 .unwrap_or(0);
804 regions[owner].nodes.push(node);
805 }
806 SeseTree { regions }
807}
808
809fn compute_layout_sese_tree<NodeId, EdgeId, NodeData, EdgeData>(
813 graph: &OwningGraph<NodeId, EdgeId, NodeData, EdgeData>,
814 component: &[NodeId],
815 root: NodeId,
816) -> SeseTree<NodeId, EdgeId>
817where
818 NodeId: Identifier + Debug + Ord,
819 EdgeId: Identifier + Debug + Ord,
820{
821 let hammocks: Vec<_> = compute_hammocks(graph, component, root)
822 .into_iter()
823 .map(|hammock| (hammock.nodes, hammock.entry_edge, hammock.exit_edge))
824 .collect();
825 if hammocks.is_empty() {
828 return compute_sese_normalized(graph, component, root);
829 }
830
831 let mut selected = Vec::new();
832 for candidate in compute_sese_candidates_normalized(graph, component, root) {
833 if candidate.0.len() <= 1
834 || !hammocks.iter().any(|(nodes, _, _)| {
835 candidate.0.len() < nodes.len()
836 && candidate.0.iter().all(|node| nodes.contains(node))
837 })
838 {
839 continue;
840 }
841 let laminar = selected
842 .iter()
843 .all(|(other, _, _): &(Vec<NodeId>, EdgeId, EdgeId)| {
844 let intersects = candidate.0.iter().any(|node| other.contains(node));
845 !intersects
846 || candidate.0.iter().all(|node| other.contains(node))
847 || other.iter().all(|node| candidate.0.contains(node))
848 });
849 if laminar
850 && !selected
851 .iter()
852 .any(|(nodes, _, _): &(Vec<NodeId>, EdgeId, EdgeId)| *nodes == candidate.0)
853 {
854 selected.push(candidate);
855 }
856 }
857 selected.sort_by_key(|(nodes, entry, exit)| (std::cmp::Reverse(nodes.len()), *entry, *exit));
858
859 let mut regions = vec![SeseRegion {
860 parent: None,
861 children: Vec::new(),
862 entry_edge: None,
863 exit_edge: None,
864 nodes: Vec::new(),
865 contained_nodes: component.to_vec(),
866 }];
867 for (nodes, entry_edge, exit_edge) in hammocks {
868 let id = regions.len();
869 regions.push(SeseRegion {
870 parent: Some(0),
871 children: Vec::new(),
872 entry_edge: Some(entry_edge),
873 exit_edge: Some(exit_edge),
874 nodes: Vec::new(),
875 contained_nodes: nodes,
876 });
877 regions[0].children.push(id);
878 }
879 for (nodes, entry_edge, exit_edge) in selected {
880 let parent = regions
881 .iter()
882 .enumerate()
883 .filter(|(_, region)| {
884 nodes
885 .iter()
886 .all(|node| region.contained_nodes.contains(node))
887 })
888 .min_by_key(|(_, region)| region.contained_nodes.len())
889 .map(|(id, _)| id)
890 .unwrap_or(0);
891 let id = regions.len();
892 regions.push(SeseRegion {
893 parent: Some(parent),
894 children: Vec::new(),
895 entry_edge: Some(entry_edge),
896 exit_edge: Some(exit_edge),
897 nodes: Vec::new(),
898 contained_nodes: nodes,
899 });
900 regions[parent].children.push(id);
901 }
902 for &node in component {
903 let owner = regions
904 .iter()
905 .enumerate()
906 .filter(|(_, region)| region.contained_nodes.contains(&node))
907 .min_by_key(|(_, region)| region.contained_nodes.len())
908 .map(|(id, _)| id)
909 .unwrap_or(0);
910 regions[owner].nodes.push(node);
911 }
912 SeseTree { regions }
913}
914
915fn layout_component_sese<NodeId, EdgeId, NodeData, EdgeData>(
916 graph: &OwningGraph<NodeId, EdgeId, NodeData, EdgeData>,
917 component: &[NodeId],
918 geometry: &HashMap<NodeId, NodeGeometry>,
919 preferred_root: Option<NodeId>,
920 settings: &LayoutSettings,
921) -> ComponentLayout<NodeId, EdgeId>
922where
923 NodeId: Identifier + Debug + Ord,
924 EdgeId: Identifier + Debug + Ord,
925{
926 let component_set: HashSet<_> = component.iter().copied().collect();
927 let root = preferred_root
928 .filter(|root| component_set.contains(root))
929 .unwrap_or_else(|| *component.iter().min().unwrap());
930 let tree = compute_layout_sese_tree(graph, component, root);
931 let useful_region = tree
932 .regions
933 .iter()
934 .skip(1)
935 .any(|region| region.contained_nodes.len() > 1);
936 if !useful_region || tree.root().contained_nodes.len() != component.len() {
937 return layout_component(graph, component, geometry, preferred_root, settings);
938 }
939
940 let composition = compose_sese_region(graph, geometry, settings, &tree, 0, root);
941 if !composition.valid
945 || !routes_clear_of_nodes(&composition.nodes, &composition.edges)
946 || !routes_are_deconflicted(&composition.edges)
947 || (settings.edge_style == EdgeStyle::Orthogonal
948 && !routes_are_orthogonal(&composition.edges))
949 {
950 return layout_component(graph, component, geometry, preferred_root, settings);
951 }
952 let RegionComposition {
953 mut nodes,
954 edges,
955 mut region_boxes,
956 ..
957 } = composition;
958 let (min_x, max_x, min_y, max_y) = node_bounds(&nodes);
961 let dx = -(min_x + max_x) / 2.0;
962 let dy = -(min_y + max_y) / 2.0;
963 for node in nodes.values_mut() {
964 node.x += dx;
965 node.y += dy;
966 }
967 let regions = region_boxes
968 .drain(..)
969 .map(|region| LayoutRegion {
970 id: region.id,
971 parent: region.parent,
972 x: (region.min_x + region.max_x) / 2.0 + dx,
973 y: (region.min_y + region.max_y) / 2.0 + dy,
974 width: region.max_x - region.min_x,
975 height: region.max_y - region.min_y,
976 })
977 .collect();
978 let edges = edges
979 .into_iter()
980 .map(|(id, points)| {
981 (
982 id,
983 points
984 .into_iter()
985 .map(|point| Point {
986 x: point.x + dx,
987 y: point.y + dy,
988 })
989 .collect(),
990 )
991 })
992 .collect();
993 ComponentLayout {
994 nodes,
995 edges,
996 regions,
997 }
998}
999
1000#[derive(Clone, Copy)]
1001struct RegionBox {
1002 id: usize,
1003 parent: Option<usize>,
1004 min_x: f64,
1005 max_x: f64,
1006 min_y: f64,
1007 max_y: f64,
1008}
1009
1010struct RegionInterface<NodeId: Identifier> {
1011 width: f64,
1012 height: f64,
1013 ports: BTreeMap<RegionPortId<NodeId>, ProxyPort<NodeId>>,
1014 entry_route: Option<Vec<Point>>,
1016 exit_routes: HashMap<NodeId, Vec<Point>>,
1017}
1018
1019struct RegionComposition<NodeId: Identifier, EdgeId: Identifier> {
1020 nodes: HashMap<NodeId, LayoutNode<NodeId>>,
1021 edges: HashMap<EdgeId, Vec<Point>>,
1024 interface: RegionInterface<NodeId>,
1025 valid: bool,
1026 region_boxes: Vec<RegionBox>,
1027}
1028
1029fn compose_sese_region<NodeId, EdgeId, NodeData, EdgeData>(
1030 graph: &OwningGraph<NodeId, EdgeId, NodeData, EdgeData>,
1031 geometry: &HashMap<NodeId, NodeGeometry>,
1032 settings: &LayoutSettings,
1033 tree: &SeseTree<NodeId, EdgeId>,
1034 region_id: usize,
1035 component_root: NodeId,
1036) -> RegionComposition<NodeId, EdgeId>
1037where
1038 NodeId: Identifier + Debug + Ord,
1039 EdgeId: Identifier + Debug + Ord,
1040{
1041 let region = &tree.regions[region_id];
1042 let children: Vec<_> = region
1043 .children
1044 .iter()
1045 .map(|child| {
1046 (
1047 *child,
1048 compose_sese_region(graph, geometry, settings, tree, *child, component_root),
1049 )
1050 })
1051 .collect();
1052
1053 let mut valid = children.iter().all(|(_, child)| child.valid);
1054 let mut quotient = OwningGraph::<usize, usize, (), ()>::default();
1055 let mut entity_for_node = HashMap::default();
1056 let mut direct_entity = HashMap::default();
1057 let mut proxy_entity = HashMap::default();
1058 let mut child_for_proxy = HashMap::default();
1059 let mut quotient_geometry = HashMap::default();
1060 for &node in ®ion.nodes {
1061 let entity = quotient.make_node(());
1062 direct_entity.insert(entity, node);
1063 entity_for_node.insert(node, entity);
1064 quotient_geometry.insert(entity, geometry[&node]);
1065 }
1066 for (child_id, child) in &children {
1067 let entity = quotient.make_node(());
1068 proxy_entity.insert(*child_id, entity);
1069 child_for_proxy.insert(entity, *child_id);
1070 quotient_geometry.insert(
1071 entity,
1072 NodeGeometry {
1075 width: child.interface.width,
1076 height: child.interface.height,
1077 },
1078 );
1079 for &node in &tree.regions[*child_id].contained_nodes {
1080 entity_for_node.insert(node, entity);
1081 }
1082 }
1083
1084 let contained: HashSet<_> = region.contained_nodes.iter().copied().collect();
1085 let mut original_for_quotient = HashMap::default();
1086 let exit_sources: Vec<_> = graph
1090 .edges()
1091 .filter_map(|edge| {
1092 (contained.contains(&edge.from_id())
1093 && !contained.contains(&edge.to_id())
1094 && edge.from_id() != edge.to_id())
1095 .then_some(edge.from_id())
1096 })
1097 .collect::<std::collections::BTreeSet<_>>()
1098 .into_iter()
1099 .collect();
1100 let interface = region.entry_edge.map(|entry| {
1101 let entry_terminal = quotient.make_node(());
1102 quotient_geometry.insert(
1103 entry_terminal,
1104 NodeGeometry {
1105 width: 1.0,
1106 height: 1.0,
1107 },
1108 );
1109 let entry_target = graph.get_edge(entry).unwrap().to_id();
1110 let entry_link = quotient.make_edge(entry_terminal, entity_for_node[&entry_target], ());
1111 let mut exits = Vec::new();
1112 for source in &exit_sources {
1113 let terminal = quotient.make_node(());
1114 quotient_geometry.insert(
1115 terminal,
1116 NodeGeometry {
1117 width: 1.0,
1118 height: 1.0,
1119 },
1120 );
1121 let link = quotient.make_edge(entity_for_node[source], terminal, ());
1122 exits.push((*source, terminal, link));
1123 }
1124 (entry_terminal, entry_link, exits)
1125 });
1126 let mut edge_ids: Vec<_> = graph.edges().map(|edge| edge.id()).collect();
1127 edge_ids.sort();
1128 for edge_id in edge_ids {
1129 let edge = graph.get_edge(edge_id).unwrap();
1130 let from = edge.from_id();
1131 let to = edge.to_id();
1132 if !contained.contains(&from) || !contained.contains(&to) || from == to {
1133 continue;
1134 }
1135 let from_entity = entity_for_node[&from];
1136 let to_entity = entity_for_node[&to];
1137 if from_entity != to_entity {
1138 let quotient_edge = quotient.make_edge(from_entity, to_entity, ());
1139 original_for_quotient.insert(quotient_edge, edge_id);
1140 }
1141 }
1142
1143 let mut entities: Vec<_> = quotient.nodes().map(|node| node.id()).collect();
1144 entities.sort_unstable();
1145 let entry_node = region
1146 .entry_edge
1147 .and_then(|edge| graph.get_edge(edge).map(|edge| edge.to_id()))
1148 .unwrap_or(component_root);
1149 let quotient_root = entity_for_node
1150 .get(&entry_node)
1151 .copied()
1152 .unwrap_or_else(|| entities[0]);
1153 let child_ports: HashMap<usize, BTreeMap<RegionPortId<NodeId>, ProxyPort<NodeId>>> = children
1157 .iter()
1158 .map(|(child_id, child)| (proxy_entity[child_id], child.interface.ports.clone()))
1159 .collect();
1160 let mut port_hints = HashMap::default();
1161 for ("ient_edge, &original) in &original_for_quotient {
1162 let edge = graph.get_edge(original).unwrap();
1163 let mut hint = PortHint::default();
1164 if let Some(ports) = child_ports.get(&entity_for_node[&edge.from_id()]) {
1165 hint.source_bottom = ports
1166 .get(&RegionPortId::Exit {
1167 source: edge.from_id(),
1168 })
1169 .filter(|port| {
1170 !port.is_entry
1171 && port.id
1172 == RegionPortId::Exit {
1173 source: edge.from_id(),
1174 }
1175 })
1176 .map(|port| port.x_offset);
1177 }
1178 if let Some(ports) = child_ports.get(&entity_for_node[&edge.to_id()]) {
1179 hint.target_top = ports
1180 .get(&RegionPortId::Entry)
1181 .filter(|port| port.is_entry && port.id == RegionPortId::Entry)
1182 .map(|port| port.x_offset);
1183 }
1184 if hint.source_bottom.is_some() || hint.target_top.is_some() {
1185 port_hints.insert(quotient_edge, hint);
1186 }
1187 }
1188 if let Some((_, entry_link, exits)) = &interface {
1189 let target = graph.get_edge(region.entry_edge.unwrap()).unwrap().to_id();
1190 if let Some(port) = child_ports
1191 .get(&entity_for_node[&target])
1192 .and_then(|ports| ports.get(&RegionPortId::Entry))
1193 .filter(|port| port.is_entry && port.id == RegionPortId::Entry)
1194 {
1195 port_hints.insert(
1196 *entry_link,
1197 PortHint {
1198 target_top: Some(port.x_offset),
1199 ..Default::default()
1200 },
1201 );
1202 }
1203 for (source, _, link) in exits {
1204 if let Some(port) = child_ports
1205 .get(&entity_for_node[source])
1206 .and_then(|ports| ports.get(&RegionPortId::Exit { source: *source }))
1207 .filter(|port| !port.is_entry && port.id == RegionPortId::Exit { source: *source })
1208 {
1209 port_hints.insert(
1210 *link,
1211 PortHint {
1212 source_bottom: Some(port.x_offset),
1213 ..Default::default()
1214 },
1215 );
1216 }
1217 }
1218 }
1219 let mut flat_settings = *settings;
1220 flat_settings.mode = LayoutMode::Flat;
1221 let quotient_layout = layout_component_with_port_hints(
1222 "ient,
1223 &entities,
1224 "ient_geometry,
1225 Some(quotient_root),
1226 &flat_settings,
1227 &port_hints,
1228 );
1229
1230 let (mut entry_interface, mut exit_interfaces) =
1231 if let Some((entry, entry_link, exits)) = interface {
1232 let entry_node = quotient_layout.nodes[&entry];
1233 let mut entry_points = vec![
1234 Point {
1235 x: entry_node.x,
1236 y: entry_node.y - entry_node.height / 2.0,
1237 },
1238 Point {
1239 x: entry_node.x,
1240 y: entry_node.y + entry_node.height / 2.0,
1241 },
1242 ];
1243 entry_points.extend(quotient_layout.edges[&entry_link].iter().copied().skip(1));
1244 let mut paths = HashMap::default();
1245 for (source, terminal, link) in exits {
1246 let terminal_node = quotient_layout.nodes[&terminal];
1247 let mut points = quotient_layout.edges[&link].clone();
1248 points.push(Point {
1249 x: terminal_node.x,
1250 y: terminal_node.y + terminal_node.height / 2.0,
1251 });
1252 paths.insert(source, points);
1253 }
1254 (Some(entry_points), paths)
1255 } else {
1256 (None, HashMap::default())
1257 };
1258
1259 let mut nodes = HashMap::default();
1260 for (entity, original) in direct_entity {
1261 let positioned = quotient_layout.nodes[&entity];
1262 let geom = geometry[&original];
1263 nodes.insert(
1264 original,
1265 LayoutNode {
1266 id: original,
1267 x: positioned.x,
1268 y: positioned.y,
1269 width: geom.width,
1270 height: geom.height,
1271 },
1272 );
1273 }
1274 let mut edges = HashMap::default();
1275 let mut region_boxes = Vec::new();
1276 let mut child_interfaces = HashMap::default();
1277 for (child_id, mut child) in children {
1278 let proxy = quotient_layout.nodes[&proxy_entity[&child_id]];
1279 for (_, mut node) in child.nodes.drain() {
1280 node.x += proxy.x;
1281 node.y += proxy.y;
1282 nodes.insert(node.id, node);
1283 }
1284 for (edge_id, points) in child.edges.drain() {
1285 edges.insert(edge_id, translate_points(points, proxy.x, proxy.y));
1286 }
1287 for mut region_box in child.region_boxes.drain(..) {
1288 region_box.min_x += proxy.x;
1289 region_box.max_x += proxy.x;
1290 region_box.min_y += proxy.y;
1291 region_box.max_y += proxy.y;
1292 region_boxes.push(region_box);
1293 }
1294 let child_interface = RegionInterface {
1295 width: child.interface.width,
1296 height: child.interface.height,
1297 ports: child.interface.ports,
1298 entry_route: child
1299 .interface
1300 .entry_route
1301 .take()
1302 .map(|points| translate_points(points, proxy.x, proxy.y)),
1303 exit_routes: child
1304 .interface
1305 .exit_routes
1306 .drain()
1307 .map(|(source, points)| (source, translate_points(points, proxy.x, proxy.y)))
1308 .collect(),
1309 };
1310 child_interfaces.insert(proxy_entity[&child_id], child_interface);
1311 }
1312
1313 if let Some(entry) = region.entry_edge {
1317 let target = graph.get_edge(entry).unwrap().to_id();
1318 let entity = entity_for_node[&target];
1319 if child_for_proxy.contains_key(&entity) {
1320 let nested_entry = child_interfaces[&entity].entry_route.as_ref().unwrap();
1321 let points = entry_interface.as_mut().unwrap();
1322 valid &= join_equal_points(points, nested_entry);
1323 }
1324 }
1325 for (&source, points) in &mut exit_interfaces {
1326 let entity = entity_for_node[&source];
1327 if child_for_proxy.contains_key(&entity) {
1328 let nested_exits = &child_interfaces[&entity].exit_routes;
1329 let mut expanded = nested_exits[&source].clone();
1330 valid &= join_equal_points(&mut expanded, points);
1331 *points = expanded;
1332 }
1333 }
1334
1335 for (quotient_edge, mut points) in quotient_layout.edges {
1340 let Some(&original) = original_for_quotient.get("ient_edge) else {
1341 continue; };
1343 let edge = graph.get_edge(original).unwrap();
1344 let from = edge.from_id();
1345 let to = edge.to_id();
1346 if child_for_proxy.contains_key(&entity_for_node[&from]) {
1347 let exits = &child_interfaces[&entity_for_node[&from]].exit_routes;
1348 let mut expanded = exits[&from].clone();
1349 valid &= join_equal_points(&mut expanded, &points);
1350 points = expanded;
1351 }
1352 if child_for_proxy.contains_key(&entity_for_node[&to]) {
1353 let entry = child_interfaces[&entity_for_node[&to]]
1354 .entry_route
1355 .as_ref()
1356 .unwrap();
1357 valid &= join_equal_points(&mut points, entry);
1358 }
1359 simplify_points(&mut points);
1360 edges.insert(original, points);
1361 }
1362
1363 if region_id == 0 {
1367 let mut loop_index = HashMap::<NodeId, u32>::default();
1368 let mut loop_count = HashMap::<NodeId, u32>::default();
1369 for edge in graph.edges() {
1370 if edge.from_id() == edge.to_id() && contained.contains(&edge.from_id()) {
1371 *loop_count.entry(edge.from_id()).or_default() += 1;
1372 }
1373 }
1374 for edge in graph.edges() {
1375 if edge.from_id() != edge.to_id() || !contained.contains(&edge.from_id()) {
1376 continue;
1377 }
1378 let node = nodes[&edge.from_id()];
1379 let index = loop_index.entry(node.id).or_default();
1380 edges.insert(
1381 edge.id(),
1382 self_loop_waypoints(
1383 node.x,
1384 node.y,
1385 node.width,
1386 node.height,
1387 *index,
1388 loop_count[&node.id],
1389 ),
1390 );
1391 *index += 1;
1392 }
1393 }
1394
1395 let (content_min_x, content_max_x, content_min_y, content_max_y) = node_bounds(&nodes);
1399 let width = content_max_x - content_min_x + settings.node_gap;
1400 let height = content_max_y - content_min_y + settings.layer_gap;
1401 let center_x = (content_min_x + content_max_x) / 2.0;
1402 let center_y = (content_min_y + content_max_y) / 2.0;
1403 let top = center_y - height / 2.0;
1404 let bottom = center_y + height / 2.0;
1405 if let Some(points) = &mut entry_interface {
1406 let terminal_x = points[0].x;
1409 let port_x = terminal_x.clamp(center_x - width / 2.0, center_x + width / 2.0);
1410 points.drain(..2);
1411 points.insert(0, Point { x: port_x, y: top });
1412 }
1413 let mut exit_sources: Vec<_> = exit_interfaces.keys().copied().collect();
1414 exit_sources.sort();
1415 let exit_count = exit_sources.len();
1416 let mut used_exit_x = Vec::new();
1417 for source in exit_sources {
1418 let points = exit_interfaces.get_mut(&source).unwrap();
1419 let terminal_x = points[points.len() - 2].x;
1421 let mut port_x = terminal_x.clamp(center_x - width / 2.0, center_x + width / 2.0);
1422 if used_exit_x.iter().any(|x: &f64| (*x - port_x).abs() < 1e-6) {
1423 let step = width / (exit_count + 1) as f64;
1424 port_x = (center_x - width / 2.0 + step * (used_exit_x.len() + 1) as f64)
1425 .clamp(center_x - width / 2.0, center_x + width / 2.0);
1426 }
1427 used_exit_x.push(port_x);
1428 points.truncate(points.len() - 2);
1429 points.push(Point {
1430 x: port_x,
1431 y: bottom,
1432 });
1433 }
1434 for node in nodes.values_mut() {
1435 node.x -= center_x;
1436 node.y -= center_y;
1437 }
1438 for points in edges.values_mut() {
1439 translate_points_in_place(points, -center_x, -center_y);
1440 }
1441 for region_box in &mut region_boxes {
1442 region_box.min_x -= center_x;
1443 region_box.max_x -= center_x;
1444 region_box.min_y -= center_y;
1445 region_box.max_y -= center_y;
1446 }
1447 region_boxes.push(RegionBox {
1448 id: region_id,
1449 parent: region.parent,
1450 min_x: content_min_x - center_x - settings.node_gap / 2.0,
1451 max_x: content_max_x - center_x + settings.node_gap / 2.0,
1452 min_y: content_min_y - center_y - settings.layer_gap / 2.0,
1453 max_y: content_max_y - center_y + settings.layer_gap / 2.0,
1454 });
1455 if let Some(points) = &mut entry_interface {
1456 translate_points_in_place(points, -center_x, -center_y);
1457 }
1458 for points in exit_interfaces.values_mut() {
1459 translate_points_in_place(points, -center_x, -center_y);
1460 }
1461 let mut ports = BTreeMap::new();
1462 if let Some(route) = &entry_interface {
1463 let point = route[0];
1464 valid &= (point.y + height / 2.0).abs() < 1e-6;
1465 ports.insert(
1466 RegionPortId::Entry,
1467 ProxyPort {
1468 id: RegionPortId::Entry,
1469 x_offset: point.x,
1470 is_entry: true,
1471 },
1472 );
1473 }
1474 for (&source, route) in &exit_interfaces {
1475 let point = *route.last().unwrap();
1476 valid &= (point.y - height / 2.0).abs() < 1e-6;
1477 ports.insert(
1478 RegionPortId::Exit { source },
1479 ProxyPort {
1480 id: RegionPortId::Exit { source },
1481 x_offset: point.x,
1482 is_entry: false,
1483 },
1484 );
1485 }
1486 RegionComposition {
1487 nodes,
1488 edges,
1489 interface: RegionInterface {
1490 width,
1491 height,
1492 ports,
1493 entry_route: entry_interface,
1494 exit_routes: exit_interfaces,
1495 },
1496 valid,
1497 region_boxes,
1498 }
1499}
1500
1501fn translate_points(mut points: Vec<Point>, dx: f64, dy: f64) -> Vec<Point> {
1502 translate_points_in_place(&mut points, dx, dy);
1503 points
1504}
1505
1506fn translate_points_in_place(points: &mut [Point], dx: f64, dy: f64) {
1507 for point in points {
1508 point.x += dx;
1509 point.y += dy;
1510 }
1511}
1512
1513fn join_equal_points(points: &mut Vec<Point>, suffix: &[Point]) -> bool {
1517 const EPS: f64 = 1e-6;
1518 let last = *points.last().expect("non-empty route");
1519 let first = *suffix.first().expect("non-empty route");
1520 let equal = (last.x - first.x).abs() <= EPS && (last.y - first.y).abs() <= EPS;
1521 if equal {
1522 points.extend(suffix.iter().copied().skip(1));
1523 }
1524 equal
1525}
1526
1527fn simplify_points(points: &mut Vec<Point>) {
1528 let mut simplified = Vec::with_capacity(points.len());
1529 for point in points.drain(..) {
1530 if simplified.last() != Some(&point) {
1531 simplified.push(point);
1532 }
1533 }
1534 *points = simplified;
1535}
1536
1537fn routes_clear_of_nodes<NodeId: Identifier, EdgeId: Identifier>(
1538 nodes: &HashMap<NodeId, LayoutNode<NodeId>>,
1539 edges: &HashMap<EdgeId, Vec<Point>>,
1540) -> bool {
1541 edges.values().all(|points| {
1542 points.windows(2).all(|segment| {
1543 nodes.values().all(|node| {
1544 !crate::triskel::geometry::segment_enters_rect_strict(
1545 segment[0],
1546 segment[1],
1547 Point {
1548 x: node.x,
1549 y: node.y,
1550 },
1551 node.width,
1552 node.height,
1553 )
1554 })
1555 })
1556 })
1557}
1558
1559fn routes_are_orthogonal<EdgeId: Identifier>(edges: &HashMap<EdgeId, Vec<Point>>) -> bool {
1560 edges.values().all(|points| {
1561 points.windows(2).all(|segment| {
1562 (segment[0].x - segment[1].x).abs() < 1e-6 || (segment[0].y - segment[1].y).abs() < 1e-6
1563 })
1564 })
1565}
1566
1567fn routes_are_deconflicted<EdgeId: Identifier>(edges: &HashMap<EdgeId, Vec<Point>>) -> bool {
1568 let routes: Vec<_> = edges.values().collect();
1569 for (index, route) in routes.iter().enumerate() {
1570 for other in routes.iter().skip(index + 1) {
1571 if routes_have_collinear_overlap(route, other) {
1572 return false;
1573 }
1574 }
1575 }
1576 true
1577}
1578
1579fn routes_have_collinear_overlap(a: &[Point], b: &[Point]) -> bool {
1580 const EPS: f64 = 1e-6;
1581 a.windows(2).any(|lhs| {
1582 b.windows(2).any(|rhs| {
1583 let lhs_vertical = (lhs[0].x - lhs[1].x).abs() < EPS;
1584 let rhs_vertical = (rhs[0].x - rhs[1].x).abs() < EPS;
1585 if lhs_vertical && rhs_vertical && (lhs[0].x - rhs[0].x).abs() < EPS {
1586 lhs[0].y.min(lhs[1].y).max(rhs[0].y.min(rhs[1].y)) + EPS
1587 < lhs[0].y.max(lhs[1].y).min(rhs[0].y.max(rhs[1].y))
1588 } else if !lhs_vertical && !rhs_vertical && (lhs[0].y - rhs[0].y).abs() < EPS {
1589 lhs[0].x.min(lhs[1].x).max(rhs[0].x.min(rhs[1].x)) + EPS
1590 < lhs[0].x.max(lhs[1].x).min(rhs[0].x.max(rhs[1].x))
1591 } else {
1592 false
1593 }
1594 })
1595 })
1596}
1597
1598fn node_bounds<NodeId: Identifier>(
1599 nodes: &HashMap<NodeId, LayoutNode<NodeId>>,
1600) -> (f64, f64, f64, f64) {
1601 let mut bounds = (
1602 f64::INFINITY,
1603 f64::NEG_INFINITY,
1604 f64::INFINITY,
1605 f64::NEG_INFINITY,
1606 );
1607 for node in nodes.values() {
1608 bounds.0 = bounds.0.min(node.x - node.width / 2.0);
1609 bounds.1 = bounds.1.max(node.x + node.width / 2.0);
1610 bounds.2 = bounds.2.min(node.y - node.height / 2.0);
1611 bounds.3 = bounds.3.max(node.y + node.height / 2.0);
1612 }
1613 bounds
1614}
1615
1616fn layout_component<NodeId, EdgeId, NodeData, EdgeData>(
1617 graph: &OwningGraph<NodeId, EdgeId, NodeData, EdgeData>,
1618 component: &[NodeId],
1619 geometry: &HashMap<NodeId, NodeGeometry>,
1620 preferred_root: Option<NodeId>,
1621 settings: &LayoutSettings,
1622) -> ComponentLayout<NodeId, EdgeId>
1623where
1624 NodeId: Identifier + Debug,
1625 EdgeId: Identifier + Debug,
1626{
1627 layout_component_with_port_hints(
1628 graph,
1629 component,
1630 geometry,
1631 preferred_root,
1632 settings,
1633 &HashMap::default(),
1634 )
1635}
1636
1637fn layout_component_with_port_hints<NodeId, EdgeId, NodeData, EdgeData>(
1638 graph: &OwningGraph<NodeId, EdgeId, NodeData, EdgeData>,
1639 component: &[NodeId],
1640 geometry: &HashMap<NodeId, NodeGeometry>,
1641 preferred_root: Option<NodeId>,
1642 settings: &LayoutSettings,
1643 hints: &HashMap<EdgeId, PortHint>,
1644) -> ComponentLayout<NodeId, EdgeId>
1645where
1646 NodeId: Identifier + Debug,
1647 EdgeId: Identifier + Debug,
1648{
1649 let mut local = LayoutGraph::default();
1652 let mut to_local: HashMap<NodeId, usize> = HashMap::default();
1653 let mut to_orig: HashMap<usize, NodeId> = HashMap::default();
1654
1655 let mut sorted: Vec<NodeId> = component.to_vec();
1656 sorted.sort_by_key(|id| Into::<usize>::into(*id));
1657 for id in &sorted {
1658 let geom = geometry[id];
1659 let local_id = local.make_node(NodeLayoutData {
1660 width: geom.width,
1661 height: geom.height,
1662 ..Default::default()
1663 });
1664 to_local.insert(*id, local_id);
1665 to_orig.insert(local_id, *id);
1666 }
1667
1668 let in_component: HashSet<NodeId> = component.iter().copied().collect();
1669 let mut self_loops: Vec<(EdgeId, usize)> = Vec::new();
1673 let mut edge_ids: Vec<EdgeId> = graph.edges().map(|e| e.id()).collect();
1674 edge_ids.sort_by_key(|id| Into::<usize>::into(*id));
1675 for edge_id in edge_ids {
1676 let edge = graph.get_edge(edge_id).unwrap();
1677 let from = edge.from_id();
1678 let to = edge.to_id();
1679 if !in_component.contains(&from) {
1680 continue; }
1682 if from == to {
1683 let node = to_local[&from];
1684 local.get_node_mut(node).unwrap().self_loops += 1;
1685 self_loops.push((edge_id, node));
1686 continue;
1687 }
1688 local.make_edge(
1689 to_local[&from],
1690 to_local[&to],
1691 EdgeLayoutData {
1692 orig: edge_id.into(),
1693 port_start: hints.get(&edge_id).and_then(|hint| hint.source_bottom),
1694 port_end: hints.get(&edge_id).and_then(|hint| hint.target_top),
1695 ..Default::default()
1696 },
1697 );
1698 }
1699
1700 let root_local = preferred_root
1703 .filter(|r| in_component.contains(r))
1704 .map(|r| to_local[&r])
1705 .unwrap_or_else(|| to_local[&sorted[0]]);
1706
1707 cycle::break_cycles(&mut local, root_local);
1708 rank::assign_ranks(&mut local);
1709 let seg = segment::build_segments(&mut local);
1710 let ordering = order::order(&mut local, &seg, root_local, settings.max_sweeps);
1711 #[cfg(test)]
1715 assert!(coordinate::mandatory_constraints_feasible(
1716 &local, &seg, &ordering
1717 ));
1718 coordinate::assign_x(&mut local, &seg, &ordering, settings.node_gap);
1719 assign_y(&mut local, &ordering.layers, settings.layer_gap);
1720
1721 let waypoints = match settings.edge_style {
1722 EdgeStyle::Orthogonal => OrthogonalRouter.route(&local, &ordering.layers),
1723 EdgeStyle::Straight => StraightRouter.route(&local, &ordering.layers),
1724 };
1725
1726 let mut nodes = HashMap::default();
1727 for local_id in to_orig.keys().copied() {
1728 let node = local.get_node(local_id).unwrap();
1729 let orig = to_orig[&local_id];
1730 nodes.insert(
1731 orig,
1732 LayoutNode {
1733 id: orig,
1734 x: node.x,
1735 y: node.y,
1736 width: node.width,
1737 height: node.height,
1738 },
1739 );
1740 }
1741
1742 let mut edges = HashMap::default();
1743 for (orig, points) in waypoints {
1744 edges.insert(EdgeId::from(orig), points);
1745 }
1746
1747 let mut loop_index: HashMap<usize, u32> = HashMap::default();
1750 for (edge_id, node_id) in self_loops {
1751 let node = local.get_node(node_id).unwrap();
1752 let total = node.self_loops;
1753 let index = loop_index.entry(node_id).or_insert(0);
1754 let points = self_loop_waypoints(node.x, node.y, node.width, node.height, *index, total);
1755 *index += 1;
1756 edges.insert(edge_id, points);
1757 }
1758
1759 ComponentLayout {
1760 nodes,
1761 edges,
1762 regions: Vec::new(),
1763 }
1764}
1765
1766fn assign_y(graph: &mut LayoutGraph, layers: &[Vec<usize>], layer_gap: f64) {
1769 let mut max_h = vec![0.0f64; layers.len()];
1770 for (rank, nodes) in layers.iter().enumerate() {
1771 for &id in nodes {
1772 max_h[rank] = max_h[rank].max(graph.get_node(id).unwrap().height);
1773 }
1774 }
1775
1776 let mut centers = vec![0.0f64; layers.len()];
1777 for rank in 1..layers.len() {
1778 centers[rank] = centers[rank - 1] + (max_h[rank - 1] + max_h[rank]) / 2.0 + layer_gap;
1779 }
1780
1781 for (rank, nodes) in layers.iter().enumerate() {
1782 for &id in nodes {
1783 graph.get_node_mut(id).unwrap().y = centers[rank];
1784 }
1785 }
1786}
1787
1788fn weakly_connected_components<NodeId, EdgeId, NodeData, EdgeData>(
1791 graph: &OwningGraph<NodeId, EdgeId, NodeData, EdgeData>,
1792 sorted_ids: &[NodeId],
1793) -> Vec<Vec<NodeId>>
1794where
1795 NodeId: Identifier + Debug,
1796 EdgeId: Identifier + Debug,
1797{
1798 let mut seen: HashSet<NodeId> = HashSet::default();
1799 let mut components = Vec::new();
1800 for &start in sorted_ids {
1801 if seen.contains(&start) {
1802 continue;
1803 }
1804 let mut component: Vec<NodeId> = graph
1805 .undirected_dfs(start)
1806 .map(|(_, node)| node.id())
1807 .collect();
1808 component.sort_by_key(|id| Into::<usize>::into(*id));
1809 for id in &component {
1810 seen.insert(*id);
1811 }
1812 components.push(component);
1813 }
1814 components
1815}
1816
1817fn local_bounds<NodeId: Identifier, EdgeId: Identifier>(
1818 layout: &ComponentLayout<NodeId, EdgeId>,
1819) -> (f64, f64, f64) {
1820 let mut min_x = f64::INFINITY;
1821 let mut max_x = f64::NEG_INFINITY;
1822 let mut min_y = f64::INFINITY;
1823
1824 for node in layout.nodes.values() {
1825 min_x = min_x.min(node.x - node.width / 2.0);
1826 max_x = max_x.max(node.x + node.width / 2.0);
1827 min_y = min_y.min(node.y - node.height / 2.0);
1828 }
1829 for point in layout.edges.values().flatten() {
1830 min_x = min_x.min(point.x);
1831 max_x = max_x.max(point.x);
1832 min_y = min_y.min(point.y);
1833 }
1834
1835 if !min_x.is_finite() {
1836 return (0.0, 0.0, 0.0);
1837 }
1838 (min_x, max_x, min_y)
1839}
1840
1841#[cfg(test)]
1842mod tests {
1843 use super::*;
1844 use jstd_derive::Identifier;
1845 use proptest::prelude::*;
1846
1847 use crate::triskel::geometry::segment_enters_rect_strict;
1848
1849 #[derive(Identifier)]
1850 struct N(usize);
1851 #[derive(Identifier)]
1852 struct E(usize);
1853 type G = OwningGraph<N, E, (), ()>;
1854
1855 fn geom() -> impl FnMut(N) -> NodeGeometry {
1856 |_| NodeGeometry {
1857 width: 60.0,
1858 height: 30.0,
1859 }
1860 }
1861
1862 fn layout(graph: &G, root: N) -> LayoutResult<N, E> {
1863 LayoutBuilder::new(graph)
1864 .root(root)
1865 .geometry(geom())
1866 .build()
1867 .expect("graph should lay out")
1868 }
1869
1870 fn diamond() -> (G, N) {
1871 let mut g = G::default();
1872 let a = g.make_node(());
1873 let b = g.make_node(());
1874 let c = g.make_node(());
1875 let d = g.make_node(());
1876 g.make_edge(a, b, ());
1877 g.make_edge(a, c, ());
1878 g.make_edge(b, d, ());
1879 g.make_edge(c, d, ());
1880 (g, a)
1881 }
1882
1883 fn signature(result: &LayoutResult<N, E>) -> Vec<String> {
1884 let mut nodes: Vec<_> = result.nodes.values().collect();
1885 nodes.sort_by_key(|n| usize::from(n.id));
1886 let mut lines: Vec<String> = nodes
1887 .iter()
1888 .map(|n| format!("n{}:{:.3},{:.3}", usize::from(n.id), n.x, n.y))
1889 .collect();
1890 let mut edges: Vec<_> = result.edges.iter().collect();
1891 edges.sort_by_key(|(e, _)| usize::from(**e));
1892 for (e, pts) in edges {
1893 let s = pts
1894 .iter()
1895 .map(|p| format!("{:.3},{:.3}", p.x, p.y))
1896 .collect::<Vec<_>>()
1897 .join(";");
1898 lines.push(format!("e{}:{s}", usize::from(*e)));
1899 }
1900 lines
1901 }
1902
1903 fn assert_no_node_overlap(result: &LayoutResult<N, E>) {
1904 let nodes: Vec<_> = result.nodes.values().collect();
1905 for (i, a) in nodes.iter().enumerate() {
1906 for b in nodes.iter().skip(i + 1) {
1907 let sep_x = (a.x - b.x).abs() + 1e-6 >= (a.width + b.width) / 2.0;
1908 let sep_y = (a.y - b.y).abs() + 1e-6 >= (a.height + b.height) / 2.0;
1909 assert!(
1910 sep_x || sep_y,
1911 "nodes {} and {} overlap at ({:.2},{:.2}) / ({:.2},{:.2})",
1912 usize::from(a.id),
1913 usize::from(b.id),
1914 a.x,
1915 a.y,
1916 b.x,
1917 b.y
1918 );
1919 }
1920 }
1921 }
1922
1923 fn assert_no_edge_through_node(result: &LayoutResult<N, E>) {
1924 for (edge_id, points) in &result.edges {
1925 for (index, seg) in points.windows(2).enumerate() {
1926 for node in result.nodes.values() {
1927 assert!(
1928 !segment_enters_rect_strict(
1929 seg[0],
1930 seg[1],
1931 Point {
1932 x: node.x,
1933 y: node.y
1934 },
1935 node.width,
1936 node.height,
1937 ),
1938 "edge {} segment {index} passes through node {}",
1939 usize::from(*edge_id),
1940 usize::from(node.id)
1941 );
1942 }
1943 }
1944 }
1945 }
1946
1947 #[test]
1948 #[should_panic(expected = "segment 1 passes through node 0")]
1949 fn edge_checker_rejects_later_reentry_into_endpoint_node() {
1950 let source = N::from(0);
1951 let target = N::from(1);
1952 let edge = E::from(0);
1953 let mut nodes = HashMap::default();
1954 nodes.insert(
1955 source,
1956 LayoutNode {
1957 id: source,
1958 x: 0.0,
1959 y: 0.0,
1960 width: 10.0,
1961 height: 10.0,
1962 },
1963 );
1964 nodes.insert(
1965 target,
1966 LayoutNode {
1967 id: target,
1968 x: 20.0,
1969 y: 0.0,
1970 width: 10.0,
1971 height: 10.0,
1972 },
1973 );
1974 let mut edges = HashMap::default();
1975 edges.insert(
1979 edge,
1980 vec![
1981 Point { x: 0.0, y: 5.0 },
1982 Point { x: 0.0, y: 10.0 },
1983 Point { x: 0.0, y: 0.0 },
1984 Point { x: 20.0, y: 0.0 },
1985 ],
1986 );
1987 assert_no_edge_through_node(&LayoutResult {
1988 nodes,
1989 edges,
1990 regions: Vec::new(),
1991 });
1992 }
1993
1994 fn assert_route_attaches_to_endpoints(
1998 result: &LayoutResult<N, E>,
1999 endpoints: &HashMap<E, (N, N)>,
2000 ) {
2001 let eps = 1e-6;
2002 for (edge, points) in &result.edges {
2003 let &(source, target) = endpoints.get(edge).expect("missing edge endpoint data");
2004 for (point, node) in [
2005 (points.first().unwrap(), result.nodes[&source]),
2006 (points.last().unwrap(), result.nodes[&target]),
2007 ] {
2008 let on_vertical_face = (point.x - (node.x - node.width / 2.0)).abs() < eps
2009 || (point.x - (node.x + node.width / 2.0)).abs() < eps;
2010 let on_horizontal_face = (point.y - (node.y - node.height / 2.0)).abs() < eps
2011 || (point.y - (node.y + node.height / 2.0)).abs() < eps;
2012 assert!(
2013 (on_vertical_face
2014 && point.y >= node.y - node.height / 2.0 - eps
2015 && point.y <= node.y + node.height / 2.0 + eps)
2016 || (on_horizontal_face
2017 && point.x >= node.x - node.width / 2.0 - eps
2018 && point.x <= node.x + node.width / 2.0 + eps),
2019 "edge {} does not attach to node {}: {point:?} vs {node:?}",
2020 usize::from(*edge),
2021 usize::from(node.id),
2022 );
2023 }
2024 }
2025 }
2026
2027 fn assert_no_edge_through_nonincident_node(
2028 result: &LayoutResult<N, E>,
2029 endpoints: &HashMap<E, (N, N)>,
2030 ) {
2031 for (edge_id, points) in &result.edges {
2032 let &(source, target) = endpoints.get(edge_id).expect("missing edge endpoint data");
2033 for seg in points.windows(2) {
2034 for node in result.nodes.values() {
2035 if node.id == source || node.id == target {
2036 continue;
2037 }
2038 assert!(
2039 !segment_enters_rect_strict(
2040 seg[0],
2041 seg[1],
2042 Point {
2043 x: node.x,
2044 y: node.y
2045 },
2046 node.width,
2047 node.height,
2048 ),
2049 "edge {} passes through non-incident node {}",
2050 usize::from(*edge_id),
2051 usize::from(node.id)
2052 );
2053 }
2054 }
2055 }
2056 }
2057
2058 fn assert_no_horizontal_overlap(result: &LayoutResult<N, E>) {
2061 let eps = 1e-6;
2062 let mut hsegs: Vec<(usize, f64, f64, f64)> = Vec::new();
2064 for (edge_id, points) in &result.edges {
2065 for seg in points.windows(2) {
2066 let (p, q) = (seg[0], seg[1]);
2067 if (p.y - q.y).abs() < eps && (p.x - q.x).abs() > eps {
2068 hsegs.push((usize::from(*edge_id), p.y, p.x.min(q.x), p.x.max(q.x)));
2069 }
2070 }
2071 }
2072 for (i, &(ea, ya, ax0, ax1)) in hsegs.iter().enumerate() {
2073 for &(eb, yb, bx0, bx1) in hsegs.iter().skip(i + 1) {
2074 if ea == eb || (ya - yb).abs() > eps {
2075 continue;
2076 }
2077 let overlap = ax0.max(bx0) + eps < ax1.min(bx1);
2078 assert!(
2079 !overlap,
2080 "edges {ea} and {eb} have overlapping horizontal segments at y={ya:.2}: \
2081 [{ax0:.2},{ax1:.2}] vs [{bx0:.2},{bx1:.2}]"
2082 );
2083 }
2084 }
2085 }
2086
2087 fn assert_no_vertical_overlap(result: &LayoutResult<N, E>) {
2091 let eps = 1e-6;
2092 let mut vsegs: Vec<(usize, f64, f64, f64)> = Vec::new();
2093 for (edge_id, points) in &result.edges {
2094 for seg in points.windows(2) {
2095 let (p, q) = (seg[0], seg[1]);
2096 if (p.x - q.x).abs() < eps && (p.y - q.y).abs() > eps {
2097 vsegs.push((usize::from(*edge_id), p.x, p.y.min(q.y), p.y.max(q.y)));
2098 }
2099 }
2100 }
2101 for (i, &(ea, xa, ay0, ay1)) in vsegs.iter().enumerate() {
2102 for &(eb, xb, by0, by1) in vsegs.iter().skip(i + 1) {
2103 if ea == eb || (xa - xb).abs() > eps {
2104 continue;
2105 }
2106 let overlap = ay0.max(by0) + eps < ay1.min(by1);
2107 assert!(
2108 !overlap,
2109 "edges {ea} and {eb} have overlapping vertical segments at x={xa:.2}: \
2110 [{ay0:.2},{ay1:.2}] vs [{by0:.2},{by1:.2}]"
2111 );
2112 }
2113 }
2114 }
2115
2116 proptest! {
2117 #![proptest_config(ProptestConfig::with_cases(64))]
2118 #[test]
2119 fn generated_segmented_layouts_preserve_phase_and_route_invariants(
2120 node_count in 6usize..=10,
2124 edge_pairs in proptest::collection::vec((0usize..20, 0usize..20), 0..24),
2125 dimensions in proptest::collection::vec((10u32..=140, 10u32..=80), 6..=10),
2126 node_gap in 8u32..=40,
2127 layer_gap in 16u32..=80,
2128 sweeps in 1usize..=6,
2129 orthogonal in any::<bool>(),
2130 sese in any::<bool>(),
2131 ) {
2132 let mut graph = G::default();
2133 let nodes: Vec<_> = (0..node_count).map(|_| graph.make_node(())).collect();
2134 let mut endpoints = HashMap::default();
2135 for (from, to) in [(0, 1), (1, 2), (2, 3), (3, 4), (4, 5), (0, 5), (1, 4), (5, 0), (2, 2)]
2136 .into_iter()
2137 .chain(edge_pairs)
2138 {
2139 let (from, to) = (nodes[from % node_count], nodes[to % node_count]);
2140 let edge = graph.make_edge(from, to, ());
2141 endpoints.insert(edge, (from, to));
2142 }
2143 let geometry = dimensions.clone();
2144 let build = || LayoutBuilder::new(&graph)
2145 .root(nodes[0])
2146 .node_gap(node_gap as f64)
2147 .layer_gap(layer_gap as f64)
2148 .max_sweeps(sweeps)
2149 .edge_style(if orthogonal { EdgeStyle::Orthogonal } else { EdgeStyle::Straight })
2150 .mode(if sese { LayoutMode::Sese } else { LayoutMode::Flat })
2151 .geometry(|id| {
2152 let (width, height) = geometry[usize::from(id) % geometry.len()];
2153 NodeGeometry { width: width as f64, height: height as f64 }
2154 })
2155 .build()
2156 .unwrap();
2157 let first = build();
2158 let second = build();
2159 prop_assert_eq!(signature(&first), signature(&second));
2160 prop_assert_eq!(first.nodes.len(), node_count);
2161 prop_assert_eq!(first.edges.len(), graph.edges().count());
2162 for node in first.nodes.values() {
2163 prop_assert!(node.x.is_finite() && node.y.is_finite());
2164 prop_assert!(node.width.is_finite() && node.height.is_finite());
2165 prop_assert!(node.width > 0.0 && node.height > 0.0);
2166 }
2167 for points in first.edges.values() {
2168 prop_assert!(points.len() >= 2);
2169 for pair in points.windows(2) {
2170 prop_assert!(pair[0].x.is_finite() && pair[0].y.is_finite());
2171 prop_assert!(pair[1].x.is_finite() && pair[1].y.is_finite());
2172 prop_assert!((pair[0].x - pair[1].x).abs() > 1e-9 || (pair[0].y - pair[1].y).abs() > 1e-9);
2173 if orthogonal {
2174 prop_assert!((pair[0].x - pair[1].x).abs() < 1e-6 || (pair[0].y - pair[1].y).abs() < 1e-6);
2175 }
2176 }
2177 }
2178 assert_no_node_overlap(&first);
2179 assert_no_edge_through_node(&first);
2180 assert_no_edge_through_nonincident_node(&first, &endpoints);
2181 if !orthogonal {
2185 assert_no_horizontal_overlap(&first);
2186 assert_no_vertical_overlap(&first);
2187 }
2188 }
2189 }
2190
2191 proptest! {
2192 #![proptest_config(ProptestConfig::with_cases(256))]
2193 #[test]
2194 #[ignore = "run explicitly as cargo test triskel_stress_generated_layouts -- --ignored"]
2195 fn triskel_stress_generated_layouts(
2196 node_count in 2usize..=10,
2197 edge_pairs in proptest::collection::vec((0usize..20, 0usize..20), 1..24),
2198 dimensions in proptest::collection::vec((10u32..=140, 10u32..=80), 2..=10),
2199 root_index in 0usize..24,
2200 orthogonal in any::<bool>(),
2201 sese in any::<bool>(),
2202 ) {
2203 let mut graph = G::default();
2206 let nodes: Vec<_> = (0..node_count).map(|_| graph.make_node(())).collect();
2207 let mut endpoints = HashMap::default();
2208 for (from, to) in edge_pairs {
2209 let edge = graph.make_edge(nodes[from % node_count], nodes[to % node_count], ());
2210 endpoints.insert(edge, (nodes[from % node_count], nodes[to % node_count]));
2211 }
2212 let geometry = dimensions.clone();
2213 let result = LayoutBuilder::new(&graph)
2214 .root(nodes[root_index % node_count])
2215 .max_sweeps(8)
2216 .edge_style(if orthogonal { EdgeStyle::Orthogonal } else { EdgeStyle::Straight })
2217 .mode(if sese { LayoutMode::Sese } else { LayoutMode::Flat })
2218 .geometry(|id| {
2219 let (width, height) = geometry[usize::from(id) % geometry.len()];
2220 NodeGeometry { width: width as f64, height: height as f64 }
2221 })
2222 .build()
2223 .unwrap();
2224 prop_assert_eq!(result.edges.len(), graph.edges().count());
2225 assert_no_node_overlap(&result);
2226 assert_no_edge_through_node(&result);
2227 assert_no_edge_through_nonincident_node(&result, &endpoints);
2228 }
2229 }
2230
2231 #[test]
2232 fn crossing_edges_use_separate_horizontal_lanes() {
2233 let mut g = G::default();
2237 let a = g.make_node(());
2238 let b = g.make_node(());
2239 let c = g.make_node(());
2240 let d = g.make_node(());
2241 g.make_edge(a, c, ());
2242 g.make_edge(a, d, ());
2243 g.make_edge(b, c, ());
2244 g.make_edge(b, d, ());
2245 let r = layout(&g, a);
2246 assert_no_horizontal_overlap(&r);
2247 assert_no_edge_through_node(&r);
2248 assert_no_node_overlap(&r);
2249 }
2250
2251 #[test]
2252 fn non_overlapping_jogs_share_the_channel_midpoint() {
2253 let (g, root) = diamond();
2258 let r = layout(&g, root);
2259 let a = r.get_node(root).unwrap();
2260 let any_child = r.nodes.values().find(|n| n.y > a.y).unwrap();
2261 let midpoint = ((a.y + a.height / 2.0) + (any_child.y - any_child.height / 2.0)) / 2.0;
2262 for points in r.edges.values() {
2263 for seg in points.windows(2) {
2264 let (p, q) = (seg[0], seg[1]);
2265 let horizontal = (p.y - q.y).abs() < 1e-9 && (p.x - q.x).abs() > 1e-9;
2266 if horizontal && (p.y - midpoint).abs() < 5.0 {
2268 assert!(
2269 (p.y - midpoint).abs() < 1e-6,
2270 "jog y {:.3} should equal channel midpoint {:.3}",
2271 p.y,
2272 midpoint
2273 );
2274 }
2275 }
2276 }
2277 assert_no_horizontal_overlap(&r);
2278 assert_no_vertical_overlap(&r);
2279 }
2280
2281 #[test]
2282 fn is_deterministic() {
2283 let (g, root) = diamond();
2284 let first = signature(&layout(&g, root));
2285 for _ in 0..10 {
2286 assert_eq!(first, signature(&layout(&g, root)));
2287 }
2288 }
2289
2290 #[test]
2291 fn chain_ranks_increase_downward() {
2292 let mut g = G::default();
2293 let a = g.make_node(());
2294 let b = g.make_node(());
2295 let c = g.make_node(());
2296 g.make_edge(a, b, ());
2297 g.make_edge(b, c, ());
2298 let r = layout(&g, a);
2299 let ay = r.get_node(a).unwrap().y;
2300 let by = r.get_node(b).unwrap().y;
2301 let cy = r.get_node(c).unwrap().y;
2302 assert!(ay < by && by < cy, "{ay} {by} {cy}");
2303 }
2304
2305 #[test]
2306 fn diamond_has_no_overlap_and_clean_edges() {
2307 let (g, root) = diamond();
2308 let r = layout(&g, root);
2309 assert_no_node_overlap(&r);
2310 assert_no_edge_through_node(&r);
2311 for points in r.edges.values() {
2312 assert!(points.len() >= 2);
2313 }
2314 }
2315
2316 #[test]
2317 fn long_edge_spans_with_dummies_and_renders_once() {
2318 let mut g = G::default();
2319 let a = g.make_node(());
2320 let b = g.make_node(());
2321 let c = g.make_node(());
2322 let d = g.make_node(());
2323 g.make_edge(a, b, ());
2324 g.make_edge(b, c, ());
2325 g.make_edge(c, d, ());
2326 let long = g.make_edge(a, d, ());
2327 let r = layout(&g, a);
2328 assert!(r.get_waypoints(long).unwrap().len() >= 2);
2329 let svg = r.render_svg();
2330 let marker = format!("data-edge-id=\"{}\"", usize::from(long));
2331 assert_eq!(svg.matches(&marker).count(), 1);
2332 }
2333
2334 #[test]
2335 fn orthogonal_router_yields_right_angles() {
2336 let (g, root) = diamond();
2337 let r = layout(&g, root);
2338 for points in r.edges.values() {
2339 for seg in points.windows(2) {
2340 let axis_aligned =
2341 (seg[0].x - seg[1].x).abs() < 1e-6 || (seg[0].y - seg[1].y).abs() < 1e-6;
2342 assert!(
2343 axis_aligned,
2344 "non-orthogonal segment {:?}->{:?}",
2345 seg[0], seg[1]
2346 );
2347 }
2348 }
2349 }
2350
2351 #[test]
2352 fn back_edge_runs_upward_to_target() {
2353 let mut g = G::default();
2356 let a = g.make_node(());
2357 let b = g.make_node(());
2358 let c = g.make_node(());
2359 g.make_edge(a, b, ());
2360 g.make_edge(b, c, ());
2361 let back = g.make_edge(c, a, ());
2362 let r = layout(&g, a);
2363 let pts = r.get_waypoints(back).unwrap();
2364 assert!(
2365 pts.first().unwrap().y > pts.last().unwrap().y,
2366 "back edge should travel upward: {:?}",
2367 pts
2368 );
2369
2370 let eps = 1e-6;
2373 let src = r.get_node(c).unwrap();
2374 let tgt = r.get_node(a).unwrap();
2375 let start = pts.first().unwrap();
2376 let end = pts.last().unwrap();
2377 assert!(
2378 (start.y - (src.y + src.height / 2.0)).abs() < eps,
2379 "back edge should start at the source's bottom: {start:?} vs c={src:?}"
2380 );
2381 assert!(
2382 (end.y - (tgt.y - tgt.height / 2.0)).abs() < eps,
2383 "back edge should end at the target's top: {end:?} vs a={tgt:?}"
2384 );
2385 assert_no_edge_through_node(&r);
2386 }
2387
2388 #[test]
2389 fn adjacent_back_edge_wraps_without_dummies() {
2390 let mut g = G::default();
2394 let a = g.make_node(());
2395 let b = g.make_node(());
2396 g.make_edge(a, b, ());
2397 let back = g.make_edge(b, a, ());
2398 let r = layout(&g, a);
2399 let pts = r.get_waypoints(back).unwrap();
2400 let eps = 1e-6;
2401 let src = r.get_node(b).unwrap();
2402 let tgt = r.get_node(a).unwrap();
2403 let start = pts.first().unwrap();
2404 let end = pts.last().unwrap();
2405 assert!(
2406 (start.y - (src.y + src.height / 2.0)).abs() < eps,
2407 "back edge should start at the source's bottom: {start:?} vs b={src:?}"
2408 );
2409 assert!(
2410 (end.y - (tgt.y - tgt.height / 2.0)).abs() < eps,
2411 "back edge should end at the target's top: {end:?} vs a={tgt:?}"
2412 );
2413 assert_no_edge_through_node(&r);
2414 }
2415
2416 #[test]
2417 fn back_edge_keeps_source_port_on_the_bottom_fan() {
2418 let mut g = G::default();
2422 let top = g.make_node(());
2423 let s = g.make_node(());
2424 let a = g.make_node(());
2425 let b = g.make_node(());
2426 g.make_edge(top, s, ());
2427 let e_a = g.make_edge(s, a, ());
2428 let e_b = g.make_edge(s, b, ());
2429 let e_back = g.make_edge(s, top, ()); let r = layout(&g, top);
2431
2432 let sn = r.get_node(s).unwrap();
2433 let bottom_y = sn.y + sn.height / 2.0;
2434 let eps = 1e-6;
2435
2436 let port_x = |edge| {
2440 let pts = r.get_waypoints(edge).unwrap();
2441 let p = pts.first().unwrap();
2442 assert!(
2443 (p.y - bottom_y).abs() < eps,
2444 "edge should leave s's bottom: {p:?} vs y={bottom_y}"
2445 );
2446 p.x
2447 };
2448 let mut xs = [port_x(e_a), port_x(e_b), port_x(e_back)];
2449 xs.sort_by(f64::total_cmp);
2450
2451 let half = sn.width / 2.0;
2456 assert!(
2457 xs[0] < sn.x - half / 4.0
2458 && (xs[1] - sn.x).abs() <= half / 2.0
2459 && xs[2] > sn.x + half / 4.0,
2460 "the three exits should fan across s's bottom in distinct slots: {xs:?} (s.x={})",
2461 sn.x
2462 );
2463 for p in xs {
2464 assert!(
2465 p >= sn.x - half - eps && p <= sn.x + half + eps,
2466 "port {p} outside s's width [{}, {}]",
2467 sn.x - half,
2468 sn.x + half
2469 );
2470 }
2471 assert_no_edge_through_node(&r);
2472 }
2473
2474 #[test]
2475 fn multiple_back_edges_into_one_node_wrap_cleanly() {
2476 let mut g = G::default();
2480 let a = g.make_node(());
2481 let b = g.make_node(());
2482 let c = g.make_node(());
2483 let d = g.make_node(());
2484 g.make_edge(a, b, ());
2485 g.make_edge(b, c, ());
2486 g.make_edge(b, d, ());
2487 let back_c = g.make_edge(c, a, ());
2488 let back_d = g.make_edge(d, a, ());
2489 let r = layout(&g, a);
2490
2491 let eps = 1e-6;
2492 let tgt = r.get_node(a).unwrap();
2493 let mut top_ports = Vec::new();
2494 for (src_id, back) in [(c, back_c), (d, back_d)] {
2495 let pts = r.get_waypoints(back).unwrap();
2496 let src = r.get_node(src_id).unwrap();
2497 let start = pts.first().unwrap();
2498 let end = pts.last().unwrap();
2499 assert!(
2500 (start.y - (src.y + src.height / 2.0)).abs() < eps,
2501 "back edge should leave the source's bottom: {start:?}"
2502 );
2503 assert!(
2504 (end.y - (tgt.y - tgt.height / 2.0)).abs() < eps,
2505 "back edge should enter the target's top: {end:?}"
2506 );
2507 top_ports.push(end.x);
2508 }
2509 assert!(
2510 (top_ports[0] - top_ports[1]).abs() > eps,
2511 "the two back edges must enter a's top at distinct ports: {top_ports:?}"
2512 );
2513 assert_no_edge_through_node(&r);
2514 assert_no_node_overlap(&r);
2515 }
2516
2517 #[test]
2518 fn long_back_edge_has_a_straight_column() {
2519 let mut g = G::default();
2524 let n: Vec<_> = (0..5).map(|_| g.make_node(())).collect();
2525 for w in n.windows(2) {
2526 g.make_edge(w[0], w[1], ());
2527 }
2528 let back = g.make_edge(n[4], n[0], ());
2529 let r = layout(&g, n[0]);
2530 let pts = r.get_waypoints(back).unwrap();
2531 assert_eq!(
2532 pts.len(),
2533 6,
2534 "a single long back edge should wrap with a straight column: {pts:?}"
2535 );
2536 assert!(
2538 (pts[2].x - pts[3].x).abs() < 1e-6,
2539 "the column must be vertical: {pts:?}"
2540 );
2541 assert!(
2542 (pts[2].y - pts[3].y).abs() > r.get_node(n[0]).unwrap().height,
2543 "the column must span the ranks: {pts:?}"
2544 );
2545 assert_no_edge_through_node(&r);
2546 }
2547
2548 #[test]
2549 fn self_loop_is_rendered_off_the_right_face() {
2550 let mut g = G::default();
2551 let a = g.make_node(());
2552 let b = g.make_node(());
2553 let loop_edge = g.make_edge(a, a, ());
2554 g.make_edge(a, b, ());
2555 let r = layout(&g, a);
2556 assert_eq!(r.nodes.len(), 2);
2557
2558 let an = r.get_node(a).unwrap();
2560 let pts = r.get_waypoints(loop_edge).expect("self-loop must render");
2561 assert!(
2562 pts.len() >= 4,
2563 "self-loop should be a loop polyline: {pts:?}"
2564 );
2565 let right = an.x + an.width / 2.0;
2566 let eps = 1e-6;
2567 assert!(
2568 (pts.first().unwrap().x - right).abs() < eps
2569 && (pts.last().unwrap().x - right).abs() < eps,
2570 "loop must attach to the right face at x={right}: {pts:?}"
2571 );
2572 assert!(
2574 pts.iter().any(|p| p.x > right + eps),
2575 "loop must protrude rightward: {pts:?}"
2576 );
2577 for p in pts {
2578 assert!(
2579 p.y >= an.y - an.height / 2.0 - eps && p.y <= an.y + an.height / 2.0 + eps,
2580 "loop must stay within the node's height band: {p:?}"
2581 );
2582 }
2583
2584 assert_no_node_overlap(&r);
2586 }
2587
2588 #[test]
2589 fn self_loop_reserves_room_from_neighbours() {
2590 let mut g = G::default();
2593 let a = g.make_node(());
2594 let b = g.make_node(());
2595 let c = g.make_node(());
2596 let loop_edge = g.make_edge(a, a, ());
2597 g.make_edge(a, b, ());
2598 g.make_edge(a, c, ());
2599 let r = layout(&g, a);
2600 let an = r.get_node(a).unwrap();
2601 let loop_max_x = r
2602 .get_waypoints(loop_edge)
2603 .unwrap()
2604 .iter()
2605 .map(|p| p.x)
2606 .fold(f64::NEG_INFINITY, f64::max);
2607 assert!(loop_max_x > an.x + an.width / 2.0);
2610 assert_no_edge_through_node(&r);
2611 }
2612
2613 #[test]
2614 fn sese_mode_expands_nested_regions_and_preserves_routes() {
2615 let mut graph = G::default();
2616 let nodes: Vec<_> = (0..10).map(|_| graph.make_node(())).collect();
2617 let mut endpoints = HashMap::default();
2618 for (from, to) in [
2619 (0, 1),
2620 (1, 2),
2621 (1, 3),
2622 (2, 4),
2623 (3, 4),
2624 (4, 5),
2625 (5, 6),
2626 (5, 7),
2627 (6, 8),
2628 (7, 8),
2629 (8, 9),
2630 ] {
2631 let edge = graph.make_edge(nodes[from], nodes[to], ());
2632 endpoints.insert(edge, (nodes[from], nodes[to]));
2633 }
2634 let build = || {
2635 LayoutBuilder::new(&graph)
2636 .root(nodes[0])
2637 .mode(LayoutMode::Sese)
2638 .geometry(|id| NodeGeometry {
2639 width: 30.0 + usize::from(id) as f64 * 3.0,
2640 height: 24.0,
2641 })
2642 .build()
2643 .unwrap()
2644 };
2645 let first = build();
2646 let second = build();
2647 assert_eq!(signature(&first), signature(&second));
2648 assert_eq!(first.nodes.len(), nodes.len());
2649 assert_eq!(first.edges.len(), endpoints.len());
2650 assert!(
2651 !first.regions.is_empty(),
2652 "SESE debug regions should be retained"
2653 );
2654 assert_no_node_overlap(&first);
2655 assert_no_edge_through_nonincident_node(&first, &endpoints);
2656 assert_route_attaches_to_endpoints(&first, &endpoints);
2657 for points in first.edges.values() {
2658 assert!(points.len() >= 2);
2659 assert!(points.windows(2).all(|pair| {
2660 (pair[0].x - pair[1].x).abs() < 1e-6 || (pair[0].y - pair[1].y).abs() < 1e-6
2661 }));
2662 }
2663
2664 let straight = LayoutBuilder::new(&graph)
2665 .root(nodes[0])
2666 .mode(LayoutMode::Sese)
2667 .edge_style(EdgeStyle::Straight)
2668 .build()
2669 .unwrap();
2670 assert!(
2671 straight
2672 .edges
2673 .values()
2674 .any(|points| points.windows(2).any(|pair| {
2675 (pair[0].x - pair[1].x).abs() > 1e-6 && (pair[0].y - pair[1].y).abs() > 1e-6
2676 }))
2677 );
2678 assert_no_edge_through_nonincident_node(&straight, &endpoints);
2679 }
2680
2681 #[test]
2682 fn sese_normalization_keeps_virtual_hammock_terminals_internal() {
2683 let mut graph = G::default();
2684 let entry = graph.make_node(());
2685 let left = graph.make_node(());
2686 let right = graph.make_node(());
2687 graph.make_edge(entry, left, ());
2688 graph.make_edge(entry, right, ());
2689 let component = vec![entry, left, right];
2690 let tree = compute_sese_normalized(&graph, &component, entry);
2691 assert_eq!(tree.root().contained_nodes, component);
2692 let mut owned: Vec<_> = tree
2693 .regions
2694 .iter()
2695 .flat_map(|region| region.nodes.iter().copied())
2696 .collect();
2697 owned.sort();
2698 assert_eq!(owned, vec![entry, left, right]);
2699 assert!(tree.regions.iter().all(|region| {
2700 region
2701 .contained_nodes
2702 .iter()
2703 .all(|node| [entry, left, right].contains(node))
2704 }));
2705 }
2706
2707 #[test]
2708 fn hammock_fallback_groups_multiple_exit_edges_to_one_exit_node() {
2709 let mut graph = G::default();
2710 let root = graph.make_node(());
2711 let a = graph.make_node(());
2712 let end = graph.make_node(());
2713 let b = graph.make_node(());
2714 let c = graph.make_node(());
2715 let d = graph.make_node(());
2716 let e = graph.make_node(());
2717 let f = graph.make_node(());
2718 graph.make_edge(root, a, ());
2719 graph.make_edge(a, end, ());
2720 let entry = graph.make_edge(root, b, ());
2721 graph.make_edge(b, c, ());
2722 graph.make_edge(c, d, ());
2723 let first_exit = graph.make_edge(d, end, ());
2724 graph.make_edge(b, e, ());
2725 graph.make_edge(e, f, ());
2726 graph.make_edge(f, end, ());
2727 let component = vec![root, a, end, b, c, d, e, f];
2728 let fallback = compute_hammock_fallback(&graph, &component);
2729 let tree = compute_layout_sese_tree(&graph, &component, root);
2730 assert_eq!(
2731 tree.regions.len(),
2732 4,
2733 "root, hammock, and two edge-SESE children"
2734 );
2735 let hammock = tree
2736 .regions
2737 .iter()
2738 .find(|region| region.contained_nodes == vec![b, c, d, e, f])
2739 .expect("b..f hammock");
2740 assert_eq!(hammock.entry_edge, Some(entry));
2741 assert_eq!(hammock.exit_edge, Some(first_exit));
2742 let hammock_id = tree
2743 .regions
2744 .iter()
2745 .position(|region| region.contained_nodes == vec![b, c, d, e, f])
2746 .unwrap();
2747 for nodes in [[c, d], [e, f]] {
2748 let child = tree
2749 .regions
2750 .iter()
2751 .find(|region| region.contained_nodes == nodes)
2752 .expect("nested edge-SESE");
2753 assert_eq!(child.parent, Some(hammock_id));
2754 }
2755 assert!(
2756 fallback
2757 .regions
2758 .iter()
2759 .any(|region| region.contained_nodes == vec![b, c, d, e, f])
2760 );
2761
2762 let geometry: HashMap<_, _> = graph
2766 .nodes()
2767 .map(|node| (node.id(), NodeGeometry::default()))
2768 .collect();
2769 let hammock_composition = compose_sese_region(
2770 &graph,
2771 &geometry,
2772 &LayoutSettings::default(),
2773 &tree,
2774 hammock_id,
2775 root,
2776 );
2777 assert!(hammock_composition.valid, "all proxy joins are point equal");
2778 for region_box in &hammock_composition.region_boxes {
2781 let owned = &tree.regions[region_box.id].contained_nodes;
2782 let owned_nodes: HashMap<_, _> = hammock_composition
2783 .nodes
2784 .iter()
2785 .filter(|(id, _)| owned.contains(id))
2786 .map(|(&id, &node)| (id, node))
2787 .collect();
2788 let (min_x, max_x, min_y, max_y) = node_bounds(&owned_nodes);
2789 assert!((region_box.max_x - region_box.min_x - (max_x - min_x + 40.0)).abs() < 1e-6);
2790 assert!((region_box.max_y - region_box.min_y - (max_y - min_y + 50.0)).abs() < 1e-6);
2791 }
2792 let first_port = hammock_composition.interface.ports[&RegionPortId::Exit { source: d }];
2793 let second_port = hammock_composition.interface.ports[&RegionPortId::Exit { source: f }];
2794 assert_ne!(first_port.x_offset, second_port.x_offset);
2795 for (source, port) in [(d, first_port), (f, second_port)] {
2796 assert!(!port.is_entry);
2797 assert_eq!(port.id, RegionPortId::Exit { source });
2798 assert!(port.x_offset.abs() <= hammock_composition.interface.width / 2.0);
2799 let route = &hammock_composition.interface.exit_routes[&source];
2800 let endpoint = route.last().unwrap();
2801 assert!((endpoint.x - port.x_offset).abs() < 1e-6);
2802 assert!((endpoint.y - hammock_composition.interface.height / 2.0).abs() < 1e-6);
2803 }
2804 assert!(
2805 compose_sese_region(
2806 &graph,
2807 &geometry,
2808 &LayoutSettings::default(),
2809 &tree,
2810 0,
2811 root,
2812 )
2813 .valid
2814 );
2815
2816 let layout = LayoutBuilder::new(&graph)
2817 .root(root)
2818 .mode(LayoutMode::Sese)
2819 .build()
2820 .unwrap();
2821 assert!(layout.regions.len() >= 2, "root and hammock debug bounds");
2822 let mut endpoints = HashMap::default();
2823 for edge in graph.edges() {
2824 endpoints.insert(edge.id(), (edge.from_id(), edge.to_id()));
2825 }
2826 assert_route_attaches_to_endpoints(&layout, &endpoints);
2827 assert_no_edge_through_nonincident_node(&layout, &endpoints);
2828 }
2829
2830 #[test]
2831 fn sese_parallel_boundary_edges_keep_flat_router_ports() {
2832 let mut graph = G::default();
2836 let s = graph.make_node(());
2837 let a = graph.make_node(());
2838 let b = graph.make_node(());
2839 let c = graph.make_node(());
2840 let d = graph.make_node(());
2841 let t = graph.make_node(());
2842 let mut endpoints = HashMap::default();
2843 let record = |graph: &mut G, from, to, endpoints: &mut HashMap<E, (N, N)>| {
2844 let edge = graph.make_edge(from, to, ());
2845 endpoints.insert(edge, (from, to));
2846 };
2847 record(&mut graph, s, a, &mut endpoints);
2848 for _ in 0..35 {
2849 record(&mut graph, a, b, &mut endpoints);
2850 }
2851 record(&mut graph, a, c, &mut endpoints);
2852 record(&mut graph, b, d, &mut endpoints);
2853 record(&mut graph, c, d, &mut endpoints);
2854 record(&mut graph, d, t, &mut endpoints);
2855
2856 let result = LayoutBuilder::new(&graph)
2857 .root(s)
2858 .mode(LayoutMode::Sese)
2859 .build()
2860 .unwrap();
2861 assert_route_attaches_to_endpoints(&result, &endpoints);
2862 assert_no_edge_through_nonincident_node(&result, &endpoints);
2863 assert_no_horizontal_overlap(&result);
2864 assert_no_vertical_overlap(&result);
2865 }
2866
2867 #[test]
2868 fn sese_mode_falls_back_for_unstructured_component() {
2869 let mut graph = G::default();
2870 let root = graph.make_node(());
2871 let middle = graph.make_node(());
2872 let exit = graph.make_node(());
2873 graph.make_edge(root, middle, ());
2874 graph.make_edge(middle, exit, ());
2875 let flat = LayoutBuilder::new(&graph).root(root).build().unwrap();
2876 let sese = LayoutBuilder::new(&graph)
2877 .root(root)
2878 .mode(LayoutMode::Sese)
2879 .build()
2880 .unwrap();
2881 assert_eq!(signature(&flat), signature(&sese));
2882 }
2883
2884 #[test]
2885 fn disconnected_components_pack_without_overlap() {
2886 let mut g = G::default();
2887 let a = g.make_node(());
2888 let b = g.make_node(());
2889 let c = g.make_node(());
2890 let d = g.make_node(());
2891 g.make_edge(a, b, ());
2892 g.make_edge(c, d, ());
2893 for mode in [LayoutMode::Flat, LayoutMode::Sese] {
2894 let r = LayoutBuilder::new(&g).root(a).mode(mode).build().unwrap();
2895 assert_no_node_overlap(&r);
2896 let comp1_max = r.get_node(a).unwrap().x.max(r.get_node(b).unwrap().x);
2898 let comp2_min = r.get_node(c).unwrap().x.min(r.get_node(d).unwrap().x);
2899 assert!(comp2_min > comp1_max, "components overlap horizontally");
2900 }
2901 }
2902
2903 #[test]
2904 fn variable_width_nodes_do_not_overlap() {
2905 let mut g = G::default();
2906 let a = g.make_node(());
2907 let wide = g.make_node(());
2908 let narrow = g.make_node(());
2909 let d = g.make_node(());
2910 g.make_edge(a, wide, ());
2911 g.make_edge(a, narrow, ());
2912 g.make_edge(wide, d, ());
2913 g.make_edge(narrow, d, ());
2914 let r = LayoutBuilder::new(&g)
2915 .root(a)
2916 .geometry(move |id| {
2917 let w = if id == wide { 200.0 } else { 40.0 };
2918 NodeGeometry {
2919 width: w,
2920 height: 30.0,
2921 }
2922 })
2923 .build()
2924 .unwrap();
2925 assert_no_node_overlap(&r);
2926 assert_no_edge_through_node(&r);
2927 }
2928
2929 #[test]
2930 fn segment_chain_preserves_order_beside_wide_node_regression() {
2931 let mut g = G::default();
2934 let n: Vec<_> = (0..4).map(|_| g.make_node(())).collect();
2935 g.make_edge(n[0], n[2], ());
2936 g.make_edge(n[1], n[0], ());
2937 let edge = g.make_edge(n[1], n[2], ());
2938 g.make_edge(n[2], n[0], ());
2939 g.make_edge(n[2], n[3], ());
2940 g.make_edge(n[3], n[1], ());
2941 let r = LayoutBuilder::new(&g)
2942 .root(n[0])
2943 .geometry(|id| NodeGeometry {
2944 width: if usize::from(id) % 3 == 0 {
2945 120.0
2946 } else {
2947 40.0
2948 },
2949 height: 30.0,
2950 })
2951 .build()
2952 .unwrap();
2953 assert!(r.get_waypoints(edge).unwrap().len() >= 2);
2954 assert_no_edge_through_node(&r);
2955 assert_no_node_overlap(&r);
2956 }
2957
2958 #[test]
2959 fn long_edge_segment_avoids_wide_intermediate_node() {
2960 let mut g = G::default();
2965 let a = g.make_node(());
2966 let b = g.make_node(());
2967 let c = g.make_node(());
2968 let d = g.make_node(());
2969 let e = g.make_node(());
2970 g.make_edge(a, b, ());
2971 g.make_edge(b, c, ());
2972 g.make_edge(c, d, ());
2973 g.make_edge(d, e, ());
2974 g.make_edge(a, e, ());
2975 let r = LayoutBuilder::new(&g)
2976 .root(a)
2977 .geometry(move |id| {
2978 let w = if id == c { 220.0 } else { 40.0 };
2979 NodeGeometry {
2980 width: w,
2981 height: 30.0,
2982 }
2983 })
2984 .build()
2985 .unwrap();
2986 assert_no_node_overlap(&r);
2987 assert_no_edge_through_node(&r);
2988 }
2989
2990 #[test]
2991 fn root_sits_above_its_back_edge_predecessor() {
2992 let mut g = G::default();
2998 let pred = g.make_node(());
2999 let entry = g.make_node(());
3000 g.make_edge(pred, entry, ());
3001 g.make_edge(entry, pred, ());
3002 let r = layout(&g, entry);
3003 let ey = r.get_node(entry).unwrap().y;
3004 let py = r.get_node(pred).unwrap().y;
3005 assert!(
3006 ey < py,
3007 "entry (root) should sit above its back-edge predecessor: entry.y={ey} pred.y={py}"
3008 );
3009 }
3010
3011 #[test]
3012 fn root_tops_loop_when_entry_has_higher_id() {
3013 let mut g = G::default();
3018 let a = g.make_node(());
3019 let b = g.make_node(());
3020 let entry = g.make_node(());
3021 g.make_edge(entry, a, ());
3022 g.make_edge(a, b, ());
3023 g.make_edge(b, entry, ());
3024 let r = layout(&g, entry);
3025 let ey = r.get_node(entry).unwrap().y;
3026 for (id, n) in &r.nodes {
3027 if *id != entry {
3028 assert!(
3029 ey <= n.y,
3030 "entry must be the top real node: entry.y={ey} node{}.y={}",
3031 usize::from(*id),
3032 n.y
3033 );
3034 }
3035 }
3036 }
3037
3038 #[test]
3039 fn empty_graph_errors() {
3040 let g = G::default();
3041 let err = LayoutBuilder::new(&g).geometry(geom()).build().unwrap_err();
3042 assert_eq!(err, LayoutError::EmptyGraph);
3043 }
3044}