abstracttui_graph/layout/
force.rs1use crate::desc::{Direction, GraphDesc};
17
18use super::geom::{clip_border, self_loop};
19use super::resolve::Resolved;
20use super::{assemble, EdgeLayout, Layout, NodeLayout};
21
22use abstracttui::base::Rect;
23
24pub type IterationBudget = u32;
28
29#[derive(Clone, Debug, PartialEq)]
42pub struct ForceOpts {
43 pub seed: u64,
46 pub budget: IterationBudget,
49 pub rank_bias: Option<Direction>,
53 pub spacing: f64,
57}
58
59impl Default for ForceOpts {
60 fn default() -> Self {
61 ForceOpts {
62 seed: 0xAB57_AC77,
63 budget: 256,
64 rank_bias: None,
65 spacing: 14.0,
66 }
67 }
68}
69
70const SETTLE_EPSILON: f64 = 0.05;
72const SPRING: f64 = 0.08;
74const BIAS: f64 = 0.05;
76
77pub fn force(desc: &GraphDesc, opts: &ForceOpts) -> Layout {
82 run(desc, opts).0
83}
84
85pub(crate) fn run(desc: &GraphDesc, opts: &ForceOpts) -> (Layout, u32) {
88 let resolved = Resolved::new(desc);
89 let notes = resolved.notes();
90 let n = resolved.len();
91 if n == 0 {
92 return (assemble(Vec::new(), Vec::new(), notes), 0);
93 }
94
95 let spacing = if opts.spacing > 0.0 {
96 opts.spacing
97 } else {
98 14.0
99 };
100 let radius: Vec<f64> = resolved
101 .sizes
102 .iter()
103 .map(|s| (f64::from(s.w) + f64::from(s.h)) / 4.0)
104 .collect();
105
106 let mut rng = SplitMix64::new(opts.seed);
109 let area: f64 = resolved
110 .sizes
111 .iter()
112 .map(|s| f64::from(s.w) * f64::from(s.h))
113 .sum();
114 let side = area.sqrt() * 2.0 + spacing;
115 let mut px: Vec<f64> = Vec::with_capacity(n);
116 let mut py: Vec<f64> = Vec::with_capacity(n);
117 for _ in 0..n {
118 px.push(rng.next_f64() * side);
119 py.push(rng.next_f64() * side);
120 }
121
122 let (bias_vertical, bias_sign) = match opts.rank_bias {
123 Some(d) => (d.is_vertical(), if d.is_reversed() { -1.0 } else { 1.0 }),
124 None => (true, 0.0),
125 };
126
127 let mut ran = 0u32;
128 let mut dx = vec![0.0f64; n];
129 let mut dy = vec![0.0f64; n];
130 for it in 0..opts.budget {
131 ran = it + 1;
132 let t = 1.0 - f64::from(it) / f64::from(opts.budget.max(1));
133 let step_cap = spacing * (0.1 + 0.9 * t);
134 dx.iter_mut().for_each(|v| *v = 0.0);
135 dy.iter_mut().for_each(|v| *v = 0.0);
136
137 for i in 0..n {
141 for j in (i + 1)..n {
142 let (mut ex, mut ey) = (px[i] - px[j], py[i] - py[j]);
143 let mut d2 = ex * ex + ey * ey;
144 if d2 < 1e-4 {
145 ex = 0.01 * ((i + 1) as f64);
146 ey = 0.013 * ((j + 1) as f64);
147 d2 = ex * ex + ey * ey;
148 }
149 let k = spacing + radius[i] + radius[j];
150 let f = (k * k) / d2;
151 dx[i] += ex * f / 8.0;
152 dy[i] += ey * f / 8.0;
153 dx[j] -= ex * f / 8.0;
154 dy[j] -= ey * f / 8.0;
155 }
156 }
157
158 for e in &resolved.edges {
160 let (ex, ey) = (px[e.to] - px[e.from], py[e.to] - py[e.from]);
161 let d = (ex * ex + ey * ey).sqrt().max(1e-3);
162 let rest = spacing + radius[e.from] + radius[e.to];
163 let f = (d - rest) * SPRING;
164 let (ux, uy) = (ex / d, ey / d);
165 dx[e.from] += ux * f;
166 dy[e.from] += uy * f;
167 dx[e.to] -= ux * f;
168 dy[e.to] -= uy * f;
169 }
170
171 if bias_sign != 0.0 {
176 for e in &resolved.edges {
177 let want = (spacing + radius[e.from] + radius[e.to]) * 0.6;
178 let actual = if bias_vertical {
179 (py[e.to] - py[e.from]) * bias_sign
180 } else {
181 (px[e.to] - px[e.from]) * bias_sign
182 };
183 let short = want - actual;
184 if short > 0.0 {
185 let adj = short * BIAS * bias_sign;
186 if bias_vertical {
187 dy[e.to] += adj;
188 dy[e.from] -= adj;
189 } else {
190 dx[e.to] += adj;
191 dx[e.from] -= adj;
192 }
193 }
194 }
195 }
196
197 let mut max_step = 0.0f64;
199 for i in 0..n {
200 let len = (dx[i] * dx[i] + dy[i] * dy[i]).sqrt();
201 if len > 1e-12 {
202 let step = len.min(step_cap);
203 px[i] += dx[i] / len * step;
204 py[i] += dy[i] / len * step;
205 max_step = max_step.max(step);
206 }
207 }
208 if max_step < SETTLE_EPSILON {
209 break;
210 }
211 }
212
213 let mut nodes = Vec::with_capacity(n);
215 for g in 0..n {
216 let size = resolved.sizes[g];
217 let x = (px[g] - f64::from(size.w) / 2.0).round() as i32;
218 let y = (py[g] - f64::from(size.h) / 2.0).round() as i32;
219 nodes.push(NodeLayout::new(
220 resolved.id(desc, g),
221 Rect::new(x, y, size.w, size.h),
222 0,
223 ));
224 }
225
226 let center = |r: Rect| {
227 (
228 f64::from(r.x) + f64::from(r.w) / 2.0,
229 f64::from(r.y) + f64::from(r.h) / 2.0,
230 )
231 };
232 let mut edge_out: Vec<(usize, EdgeLayout)> = Vec::new();
233 for e in &resolved.edges {
234 let (ra, rb) = (nodes[e.from].rect, nodes[e.to].rect);
235 let waypoints = vec![clip_border(ra, center(rb)), clip_border(rb, center(ra))];
236 let d = &desc.edges[e.desc_index];
237 edge_out.push((
238 e.desc_index,
239 EdgeLayout::new(d.from.clone(), d.to.clone(), e.desc_index, waypoints),
240 ));
241 }
242 for &(g, desc_index) in &resolved.self_edges {
243 let d = &desc.edges[desc_index];
244 edge_out.push((
245 desc_index,
246 EdgeLayout::new(
247 d.from.clone(),
248 d.to.clone(),
249 desc_index,
250 self_loop(nodes[g].rect),
251 ),
252 ));
253 }
254 edge_out.sort_by_key(|(i, _)| *i);
255 let edges = edge_out.into_iter().map(|(_, e)| e).collect();
256 (assemble(nodes, edges, notes), ran)
257}
258
259struct SplitMix64 {
261 state: u64,
262}
263
264impl SplitMix64 {
265 fn new(seed: u64) -> Self {
266 SplitMix64 { state: seed }
267 }
268
269 fn next_u64(&mut self) -> u64 {
270 self.state = self.state.wrapping_add(0x9E37_79B9_7F4A_7C15);
271 let mut z = self.state;
272 z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
273 z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
274 z ^ (z >> 31)
275 }
276
277 fn next_f64(&mut self) -> f64 {
279 (self.next_u64() >> 11) as f64 / (1u64 << 53) as f64
280 }
281}
282
283#[cfg(test)]
284mod tests {
285 use super::*;
286 use crate::desc::GraphDesc;
287
288 #[test]
289 fn single_node_settles_immediately() {
290 let desc = GraphDesc::new().node("only", 6, 3);
291 let (_, ran) = run(&desc, &ForceOpts::default());
292 assert_eq!(ran, 1, "no forces, first iteration settles");
293 }
294
295 #[test]
296 fn pair_freezes_well_under_a_large_budget() {
297 let desc = GraphDesc::new()
298 .node("a", 6, 3)
299 .node("b", 6, 3)
300 .edge("a", "b");
301 let opts = ForceOpts {
302 budget: 10_000,
303 ..Default::default()
304 };
305 let (_, ran) = run(&desc, &opts);
306 assert!(
307 ran < 2_000,
308 "spring/repulsion equilibrium settles early, ran {ran}"
309 );
310 }
311
312 #[test]
313 fn prng_streams_are_seed_stable_and_in_range() {
314 let mut rng = SplitMix64::new(42);
315 let mut rng2 = SplitMix64::new(42);
316 let a: Vec<u64> = (0..8).map(|_| rng.next_u64()).collect();
317 let b: Vec<u64> = (0..8).map(|_| rng2.next_u64()).collect();
318 assert_eq!(a, b, "same seed, same stream");
319 let mut rng3 = SplitMix64::new(43);
320 assert_ne!(a[0], rng3.next_u64(), "different seed, different stream");
321 for _ in 0..64 {
322 let f = rng3.next_f64();
323 assert!((0.0..1.0).contains(&f));
324 }
325 }
326}