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