graphic design tool
git clone https://git.lucas.co/cce-designer.git
src/layout.rs (5.4K)
1 //! Auto-layout: arrange a level's nodes from their wiring.
2 //!
3 //! The network is already a GRID — every node's position is an integer cell,
4 //! and the keyboard cursor moves cell by cell — so this is not the usual
5 //! force-directed sprawl. It is a layered assignment on cells: a node's ROW is
6 //! how far it is downstream, and its COLUMN is chosen to sit under the node it
7 //! reads from.
8 //!
9 //! **Edges come from the same rule the wires do**: a node's `Input` parameter
10 //! naming another node. That is the widget's `wire_pairs` derivation, and
11 //! matching it is the point — a layout computed from relationships you cannot
12 //! see would move nodes for reasons that are not on screen. It also means a
13 //! second operand (a Boolean's `With`, a Copy's target) does not pull on the
14 //! layout, because it does not draw a wire either. When those become wires,
15 //! they should become edges here in the same change.
16 //!
17 //! **Flow is downward**, matching every project in the repo: a Sphere at
18 //! (4, 2) feeds an output at (4, 3). Row is the LONGEST path from a root, not
19 //! the shortest, so a node always sits below every one of its inputs rather
20 //! than beside one of them.
21 //!
22 //! Nothing is pinned any more. The settings tree — the root meta node and
23 //! its four utility subnets — was, because it lived where the user put it and
24 //! relocating it would have been a surprise every time; it is retired, and
25 //! `Node::pinned` outlives it for whatever wants it next.
26
27 /// One node's layout input: what it is called, what it reads, where it is now,
28 /// and whether it may be moved.
29 pub struct LayoutNode {
30 pub name: String,
31 /// The value of its `Input` parameter, if it has one.
32 pub input: Option<String>,
33 pub position: (f32, f32),
34 pub pinned: bool,
35 }
36
37 /// New positions for the nodes that moved, as (index, (column, row)).
38 ///
39 /// Only movers are returned, so a caller can tell whether the layout changed
40 /// anything and report it — an arrange that silently did nothing looks broken.
41 pub fn arrange(nodes: &[LayoutNode]) -> Vec<(usize, (f32, f32))> {
42 let n = nodes.len();
43 if n == 0 {
44 return Vec::new();
45 }
46
47 // Parent index per node, by the wires' own rule.
48 let parent: Vec<Option<usize>> = nodes
49 .iter()
50 .map(|node| {
51 let want = node.input.as_deref()?.trim();
52 if want.is_empty() {
53 return None;
54 }
55 nodes.iter().position(|other| other.name == want)
56 })
57 .collect();
58
59 // Depth by longest path, iteratively. A name-wired graph can contain a
60 // cycle (A reads B reads A), and the fixed point below simply stops
61 // improving instead of recursing forever — the cycle's members end up at
62 // the deepest row any of them could justify, which is as meaningful an
63 // answer as a cyclic graph has.
64 let mut depth = vec![0usize; n];
65 for _ in 0..n {
66 let mut changed = false;
67 for i in 0..n {
68 if let Some(p) = parent[i] {
69 if p != i && depth[p] + 1 > depth[i] {
70 depth[i] = depth[p] + 1;
71 changed = true;
72 }
73 }
74 }
75 if !changed {
76 break;
77 }
78 }
79
80 // Cells a pinned node holds; the assignment steps around them.
81 let mut taken: Vec<(i32, i32)> = nodes
82 .iter()
83 .filter(|node| node.pinned)
84 .map(|node| (node.position.0 as i32, node.position.1 as i32))
85 .collect();
86
87 let max_depth = (0..n).filter(|&i| !nodes[i].pinned).map(|i| depth[i]).max().unwrap_or(0);
88 let mut column = vec![0i32; n];
89 let mut placed = vec![false; n];
90 let mut out = Vec::new();
91
92 for row in 0..=max_depth {
93 let mut in_row: Vec<usize> =
94 (0..n).filter(|&i| !nodes[i].pinned && depth[i] == row).collect();
95
96 // Order within the row by where the node WANTS to be, so the ordering
97 // and the placement agree and the pass does not fight itself. A root's
98 // wish is its current column, which preserves the left-to-right
99 // arrangement the user already made among independent chains.
100 let wish = |i: usize, column: &Vec<i32>, placed: &Vec<bool>| -> i32 {
101 match parent[i] {
102 Some(p) if placed[p] => column[p],
103 _ => nodes[i].position.0.round() as i32,
104 }
105 };
106 in_row.sort_by_key(|&i| (wish(i, &column, &placed), nodes[i].name.clone()));
107
108 for i in in_row {
109 let want = wish(i, &column, &placed);
110 // Nearest free column to the one it wants, searching outward so a
111 // collision nudges a node aside rather than pushing the whole row
112 // to the right. A chain whose parent's column is free stays
113 // perfectly vertical, which is what a chain should look like.
114 // Terminates because `taken` is finite: some column is always free.
115 let col = (0i32..)
116 .flat_map(|step| {
117 if step == 0 { vec![want] } else { vec![want + step, want - step] }
118 })
119 .find(|c| !taken.contains(&(*c, row as i32)))
120 .expect("an unbounded column scan always finds a free cell");
121 taken.push((col, row as i32));
122 column[i] = col;
123 placed[i] = true;
124 let new_pos = (col as f32, row as f32);
125 if new_pos != nodes[i].position {
126 out.push((i, new_pos));
127 }
128 }
129 }
130 out
131 }