git.lucas.co / cce-designer
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 }