Skip to main content

gpui_graph/
layout.rs

1//! A d3-force style simulation: link, many-body (Barnes–Hut), collide and
2//! centering forces, integrated with velocity Verlet and an annealing `alpha`.
3
4/// Tuning knobs, named after their d3-force counterparts.
5///
6/// ```ignore
7/// ForceConfig::default().with_link_distance(90.).with_charge(-400.)
8/// ```
9#[derive(Clone, Debug)]
10#[non_exhaustive]
11pub struct ForceConfig {
12    /// `forceLink().distance()`: rest length of an edge, in world units.
13    pub link_distance: f32,
14    /// `forceManyBody().strength()`: negative repels.
15    pub charge: f32,
16    /// `forceManyBody().theta()`: Barnes–Hut accuracy, lower is more exact.
17    pub theta: f32,
18    /// Ignore charge beyond this distance; keeps far components from drifting apart.
19    pub charge_distance_max: f32,
20    /// `forceCollide()` padding added to each node's radius.
21    pub collide_padding: f32,
22    pub collide_strength: f32,
23    /// `forceX/forceY` pull toward the origin; holds disconnected components together.
24    pub gravity: f32,
25    /// Pull of each group (community) member toward its group's centroid, so
26    /// communities form visible clusters. 0 disables.
27    pub group_strength: f32,
28    pub velocity_decay: f32,
29    pub alpha_min: f32,
30    pub alpha_decay: f32,
31}
32
33impl Default for ForceConfig {
34    fn default() -> Self {
35        let alpha_min: f32 = 0.001;
36        Self {
37            link_distance: 60.,
38            charge: -220.,
39            theta: 0.9,
40            charge_distance_max: 2000.,
41            collide_padding: 4.,
42            collide_strength: 0.7,
43            gravity: 0.04,
44            group_strength: 0.12,
45            velocity_decay: 0.4,
46            alpha_min,
47            alpha_decay: 1. - alpha_min.powf(1. / 300.),
48        }
49    }
50}
51
52macro_rules! with_fields {
53    ($ty:ty { $($field:ident: $t:ty => $with:ident),* $(,)? }) => {
54        impl $ty {
55            $(
56                pub fn $with(mut self, value: $t) -> Self {
57                    self.$field = value;
58                    self
59                }
60            )*
61        }
62    };
63}
64pub(crate) use with_fields;
65
66with_fields!(ForceConfig {
67    link_distance: f32 => with_link_distance,
68    charge: f32 => with_charge,
69    theta: f32 => with_theta,
70    charge_distance_max: f32 => with_charge_distance_max,
71    collide_padding: f32 => with_collide_padding,
72    collide_strength: f32 => with_collide_strength,
73    gravity: f32 => with_gravity,
74    group_strength: f32 => with_group_strength,
75    velocity_decay: f32 => with_velocity_decay,
76});
77
78impl ForceConfig {
79    /// Sets `alpha_min` and derives `alpha_decay` so the layout settles in about `ticks` steps.
80    pub fn with_settle_ticks(mut self, ticks: u32, alpha_min: f32) -> Self {
81        self.alpha_min = alpha_min;
82        self.alpha_decay = 1. - alpha_min.powf(1. / ticks.max(1) as f32);
83        self
84    }
85}
86
87#[derive(Clone, Copy, Debug, Default, PartialEq)]
88pub struct Vec2 {
89    pub x: f32,
90    pub y: f32,
91}
92
93impl Vec2 {
94    pub const fn new(x: f32, y: f32) -> Self {
95        Self { x, y }
96    }
97}
98
99#[derive(Clone, Debug, Default)]
100pub struct Body {
101    pub pos: Vec2,
102    pub vel: Vec2,
103    /// Pinned position (while dragged, or pinned by the user).
104    pub fixed: Option<Vec2>,
105    pub radius: f32,
106}
107
108pub struct Simulation {
109    pub config: ForceConfig,
110    pub alpha: f32,
111    pub alpha_target: f32,
112    quadtree: QuadTree,
113    rng: u32,
114}
115
116impl Simulation {
117    pub fn new(config: ForceConfig) -> Self {
118        Self {
119            config,
120            alpha: 1.,
121            alpha_target: 0.,
122            quadtree: QuadTree::default(),
123            rng: 0x9e37_79b9,
124        }
125    }
126
127    pub fn is_active(&self) -> bool {
128        self.alpha >= self.config.alpha_min || self.alpha_target > self.config.alpha_min
129    }
130
131    /// Restart the annealing from `alpha` (d3 `simulation.alpha(a).restart()`).
132    pub fn reheat(&mut self, alpha: f32) {
133        self.alpha = self.alpha.max(alpha);
134    }
135
136    /// Place bodies without a position on a phyllotaxis spiral, like d3 does.
137    pub fn initialize_positions(bodies: &mut [Body], start: usize) {
138        const INITIAL_RADIUS: f32 = 10.;
139        let angle = std::f32::consts::PI * (3. - 5f32.sqrt());
140        for (i, body) in bodies.iter_mut().enumerate().skip(start) {
141            let r = INITIAL_RADIUS * (0.5 + i as f32).sqrt();
142            let a = i as f32 * angle;
143            body.pos = Vec2::new(r * a.cos(), r * a.sin());
144            body.vel = Vec2::default();
145        }
146    }
147
148    /// Advance one step. `groups` lists body indices per cluster for the group force.
149    pub fn tick(
150        &mut self,
151        bodies: &mut [Body],
152        links: &[(usize, usize)],
153        degrees: &[usize],
154        groups: &[Vec<usize>],
155    ) {
156        if bodies.is_empty() {
157            return;
158        }
159        self.alpha += (self.alpha_target - self.alpha) * self.config.alpha_decay;
160        let alpha = self.alpha;
161
162        self.apply_links(bodies, links, degrees, alpha);
163        self.apply_many_body(bodies, alpha);
164        self.apply_collide(bodies);
165        self.apply_groups(bodies, groups, alpha);
166        let gravity = self.config.gravity * alpha;
167        for b in bodies.iter_mut() {
168            b.vel.x -= b.pos.x * gravity;
169            b.vel.y -= b.pos.y * gravity;
170        }
171
172        let keep = 1. - self.config.velocity_decay;
173        for b in bodies.iter_mut() {
174            if let Some(fixed) = b.fixed {
175                b.pos = fixed;
176                b.vel = Vec2::default();
177            } else {
178                b.vel.x *= keep;
179                b.vel.y *= keep;
180                b.pos.x += b.vel.x;
181                b.pos.y += b.vel.y;
182            }
183        }
184    }
185
186    fn jiggle(&mut self) -> f32 {
187        // xorshift; only used to separate coincident points.
188        self.rng ^= self.rng << 13;
189        self.rng ^= self.rng >> 17;
190        self.rng ^= self.rng << 5;
191        (self.rng as f32 / u32::MAX as f32 - 0.5) * 1e-6
192    }
193
194    fn apply_links(&mut self, bodies: &mut [Body], links: &[(usize, usize)], degrees: &[usize], alpha: f32) {
195        for &(s, t) in links {
196            if s == t {
197                continue;
198            }
199            let (ds, dt) = (degrees[s].max(1) as f32, degrees[t].max(1) as f32);
200            let strength = 1. / ds.min(dt);
201            let bias = ds / (ds + dt);
202            let (bs, bt) = (&bodies[s], &bodies[t]);
203            let mut x = bt.pos.x + bt.vel.x - bs.pos.x - bs.vel.x;
204            let mut y = bt.pos.y + bt.vel.y - bs.pos.y - bs.vel.y;
205            if x == 0. {
206                x = self.jiggle();
207            }
208            if y == 0. {
209                y = self.jiggle();
210            }
211            let len = (x * x + y * y).sqrt();
212            let l = (len - self.config.link_distance) / len * alpha * strength;
213            x *= l;
214            y *= l;
215            bodies[t].vel.x -= x * bias;
216            bodies[t].vel.y -= y * bias;
217            bodies[s].vel.x += x * (1. - bias);
218            bodies[s].vel.y += y * (1. - bias);
219        }
220    }
221
222    fn apply_groups(&self, bodies: &mut [Body], groups: &[Vec<usize>], alpha: f32) {
223        let k = self.config.group_strength * alpha;
224        if k == 0. {
225            return;
226        }
227        for members in groups.iter().filter(|m| m.len() > 1) {
228            let n = members.len() as f32;
229            let (cx, cy) = members.iter().fold((0., 0.), |(x, y), &i| (x + bodies[i].pos.x, y + bodies[i].pos.y));
230            let (cx, cy) = (cx / n, cy / n);
231            for &i in members {
232                bodies[i].vel.x += (cx - bodies[i].pos.x) * k;
233                bodies[i].vel.y += (cy - bodies[i].pos.y) * k;
234            }
235        }
236    }
237
238    fn apply_many_body(&mut self, bodies: &mut [Body], alpha: f32) {
239        self.quadtree.build(bodies);
240        let theta2 = self.config.theta * self.config.theta;
241        let max2 = self.config.charge_distance_max * self.config.charge_distance_max;
242        let strength = self.config.charge;
243        for i in 0..bodies.len() {
244            let (fx, fy) = self.quadtree.force(i, strength * alpha, theta2, max2);
245            bodies[i].vel.x += fx;
246            bodies[i].vel.y += fy;
247        }
248    }
249
250    /// Spatial-hash collision, so large result sets stay O(n).
251    fn apply_collide(&mut self, bodies: &mut [Body]) {
252        let pad = self.config.collide_padding;
253        let max_r = bodies.iter().map(|b| b.radius).fold(0., f32::max) + pad;
254        let cell = (max_r * 2.).max(1.);
255        let mut grid: std::collections::HashMap<(i32, i32), Vec<usize>> = Default::default();
256        for (i, b) in bodies.iter().enumerate() {
257            let key = ((b.pos.x / cell).floor() as i32, (b.pos.y / cell).floor() as i32);
258            grid.entry(key).or_default().push(i);
259        }
260        let strength = self.config.collide_strength;
261        for i in 0..bodies.len() {
262            let (cx, cy) = (
263                (bodies[i].pos.x / cell).floor() as i32,
264                (bodies[i].pos.y / cell).floor() as i32,
265            );
266            for gx in cx - 1..=cx + 1 {
267                for gy in cy - 1..=cy + 1 {
268                    let Some(cell_items) = grid.get(&(gx, gy)) else { continue };
269                    for &j in cell_items {
270                        if j <= i {
271                            continue;
272                        }
273                        let (a, b) = (&bodies[i], &bodies[j]);
274                        let r = a.radius + b.radius + pad * 2.;
275                        let mut x = a.pos.x + a.vel.x - b.pos.x - b.vel.x;
276                        let mut y = a.pos.y + a.vel.y - b.pos.y - b.vel.y;
277                        let mut l = x * x + y * y;
278                        if l >= r * r {
279                            continue;
280                        }
281                        if x == 0. {
282                            x = self.jiggle();
283                            l += x * x;
284                        }
285                        if y == 0. {
286                            y = self.jiggle();
287                            l += y * y;
288                        }
289                        let len = l.sqrt();
290                        let k = (r - len) / len * strength;
291                        let (ra, rb) = (a.radius * a.radius, b.radius * b.radius);
292                        let w = rb / (ra + rb).max(f32::EPSILON);
293                        x *= k;
294                        y *= k;
295                        bodies[i].vel.x += x * w;
296                        bodies[i].vel.y += y * w;
297                        bodies[j].vel.x -= x * (1. - w);
298                        bodies[j].vel.y -= y * (1. - w);
299                    }
300                }
301            }
302        }
303    }
304}
305
306const EMPTY: u32 = u32::MAX;
307
308#[derive(Clone, Copy)]
309struct Cell {
310    x0: f32,
311    y0: f32,
312    size: f32,
313    /// Number of bodies below this cell (each has unit charge).
314    count: f32,
315    cx: f32,
316    cy: f32,
317    /// Leaf payload: first body, chained through `QuadTree::next`.
318    body: u32,
319    /// Index of the first of four consecutive children, or `EMPTY` for a leaf.
320    children: u32,
321}
322
323impl Cell {
324    fn new(x0: f32, y0: f32, size: f32) -> Self {
325        Self { x0, y0, size, count: 0., cx: 0., cy: 0., body: EMPTY, children: EMPTY }
326    }
327
328    fn is_leaf(&self) -> bool {
329        self.children == EMPTY
330    }
331
332    fn quadrant(&self, p: Vec2) -> u32 {
333        let half = self.size / 2.;
334        let right = (p.x >= self.x0 + half) as u32;
335        let bottom = (p.y >= self.y0 + half) as u32;
336        right | (bottom << 1)
337    }
338}
339
340/// Barnes–Hut quadtree over unit charges. Rebuilt every tick; buffers are reused.
341#[derive(Default)]
342struct QuadTree {
343    cells: Vec<Cell>,
344    /// Intrusive linked list of bodies sharing a leaf.
345    next: Vec<u32>,
346    pos: Vec<Vec2>,
347    stack: Vec<u32>,
348}
349
350impl QuadTree {
351    const MAX_DEPTH: u32 = 24;
352
353    fn build(&mut self, bodies: &[Body]) {
354        self.cells.clear();
355        self.next.clear();
356        self.next.resize(bodies.len(), EMPTY);
357        self.pos.clear();
358        self.pos.extend(bodies.iter().map(|b| b.pos));
359        let (mut x0, mut y0, mut x1, mut y1) = (f32::MAX, f32::MAX, f32::MIN, f32::MIN);
360        for p in &self.pos {
361            x0 = x0.min(p.x);
362            y0 = y0.min(p.y);
363            x1 = x1.max(p.x);
364            y1 = y1.max(p.y);
365        }
366        let size = (x1 - x0).max(y1 - y0).max(1.) * 1.0001;
367        self.cells.push(Cell::new(x0, y0, size));
368        for i in 0..self.pos.len() as u32 {
369            self.insert(i);
370        }
371        self.accumulate(0);
372    }
373
374    fn insert(&mut self, body: u32) {
375        let p = self.pos[body as usize];
376        let mut cell = 0usize;
377        let mut depth = 0;
378        loop {
379            let c = self.cells[cell];
380            if c.is_leaf() {
381                if c.body == EMPTY || depth >= Self::MAX_DEPTH {
382                    // Empty leaf, or (nearly) coincident points: chain here.
383                    self.next[body as usize] = c.body;
384                    self.cells[cell].body = body;
385                    return;
386                }
387                self.split(cell);
388            }
389            let c = self.cells[cell];
390            cell = (c.children + c.quadrant(p)) as usize;
391            depth += 1;
392        }
393    }
394
395    /// Turn a leaf into an inner cell, moving its bodies into the new children.
396    fn split(&mut self, cell: usize) {
397        let c = self.cells[cell];
398        let half = c.size / 2.;
399        let first = self.cells.len() as u32;
400        for q in 0..4 {
401            let x = c.x0 + if q & 1 == 1 { half } else { 0. };
402            let y = c.y0 + if q & 2 == 2 { half } else { 0. };
403            self.cells.push(Cell::new(x, y, half));
404        }
405        self.cells[cell].children = first;
406        self.cells[cell].body = EMPTY;
407        let mut b = c.body;
408        while b != EMPTY {
409            let following = self.next[b as usize];
410            let child = (first + c.quadrant(self.pos[b as usize])) as usize;
411            self.next[b as usize] = self.cells[child].body;
412            self.cells[child].body = b;
413            b = following;
414        }
415    }
416
417    fn accumulate(&mut self, cell: usize) -> (f32, f32, f32) {
418        let c = self.cells[cell];
419        let (mut n, mut sx, mut sy) = (0., 0., 0.);
420        if c.is_leaf() {
421            let mut b = c.body;
422            while b != EMPTY {
423                let p = self.pos[b as usize];
424                n += 1.;
425                sx += p.x;
426                sy += p.y;
427                b = self.next[b as usize];
428            }
429        } else {
430            for q in 0..4 {
431                let (cn, cx, cy) = self.accumulate((c.children + q) as usize);
432                n += cn;
433                sx += cx * cn;
434                sy += cy * cn;
435            }
436        }
437        let (cx, cy) = if n > 0. { (sx / n, sy / n) } else { (0., 0.) };
438        let cell = &mut self.cells[cell];
439        cell.count = n;
440        cell.cx = cx;
441        cell.cy = cy;
442        (n, cx, cy)
443    }
444
445    /// Velocity change on body `i` from all other charges (d3 `forceManyBody`).
446    fn force(&mut self, i: usize, strength: f32, theta2: f32, max2: f32) -> (f32, f32) {
447        const DISTANCE_MIN2: f32 = 1.;
448        let p = self.pos[i];
449        let (mut fx, mut fy) = (0., 0.);
450        self.stack.clear();
451        self.stack.push(0);
452        while let Some(ix) = self.stack.pop() {
453            let c = self.cells[ix as usize];
454            if c.count == 0. {
455                continue;
456            }
457            if !c.is_leaf() {
458                let (x, y) = (c.cx - p.x, c.cy - p.y);
459                let l = x * x + y * y;
460                if c.size * c.size / theta2 < l {
461                    // Far enough: treat the whole cell as one aggregate charge.
462                    if l < max2 {
463                        let k = strength * c.count / l.max(DISTANCE_MIN2);
464                        fx += x * k;
465                        fy += y * k;
466                    }
467                } else {
468                    self.stack.extend((0..4).map(|q| c.children + q));
469                }
470                continue;
471            }
472            let mut b = c.body;
473            while b != EMPTY {
474                if b as usize != i {
475                    let q = self.pos[b as usize];
476                    let (x, y) = (q.x - p.x, q.y - p.y);
477                    let l = x * x + y * y;
478                    if l < max2 {
479                        let k = strength / l.max(DISTANCE_MIN2);
480                        fx += x * k;
481                        fy += y * k;
482                    }
483                }
484                b = self.next[b as usize];
485            }
486        }
487        (fx, fy)
488    }
489}
490
491#[cfg(test)]
492mod tests {
493    use super::*;
494
495    fn bodies(n: usize) -> Vec<Body> {
496        let mut b = vec![Body { radius: 8., ..Default::default() }; n];
497        Simulation::initialize_positions(&mut b, 0);
498        b
499    }
500
501    #[test]
502    fn barnes_hut_matches_brute_force() {
503        let b = bodies(300);
504        let mut tree = QuadTree::default();
505        tree.build(&b);
506        for i in [0, 17, 150, 299] {
507            // theta = 0 forces exact summation through the tree.
508            let exact = tree.force(i, -1., 0., f32::MAX);
509            let (mut fx, mut fy, mut scale) = (0., 0., 0.);
510            for (j, o) in b.iter().enumerate() {
511                if j != i {
512                    let (x, y) = (o.pos.x - b[i].pos.x, o.pos.y - b[i].pos.y);
513                    let l = (x * x + y * y).max(1.);
514                    fx -= x / l;
515                    fy -= y / l;
516                    scale += x.hypot(y) / l;
517                }
518            }
519            assert!((exact.0 - fx).abs() < 1e-3 && (exact.1 - fy).abs() < 1e-3);
520            let approx = tree.force(i, -1., 0.81, f32::MAX);
521            // Relative to the summed pair magnitudes: the net force can cancel to ~0.
522            let err = (approx.0 - fx).hypot(approx.1 - fy) / scale;
523            assert!(err < 0.02, "relative error {err}");
524        }
525    }
526
527    #[test]
528    fn coincident_points_do_not_overflow() {
529        let mut b = vec![Body { radius: 5., ..Default::default() }; 50];
530        let mut sim = Simulation::new(ForceConfig::default());
531        let degrees = vec![0; 50];
532        for _ in 0..20 {
533            sim.tick(&mut b, &[], &degrees, &[]);
534        }
535        assert!(b.iter().all(|b| b.pos.x.is_finite() && b.pos.y.is_finite()));
536    }
537
538    #[test]
539    fn simulation_settles_with_links_near_rest_length() {
540        let n = 60;
541        let mut b = bodies(n);
542        let links: Vec<_> = (1..n).map(|i| (i, (i - 1) / 2)).collect();
543        let mut degrees = vec![0; n];
544        for &(s, t) in &links {
545            degrees[s] += 1;
546            degrees[t] += 1;
547        }
548        let mut sim = Simulation::new(ForceConfig::default());
549        let mut ticks = 0;
550        while sim.is_active() {
551            sim.tick(&mut b, &links, &degrees, &[]);
552            ticks += 1;
553        }
554        assert!(ticks < 400);
555        let mean: f32 = links
556            .iter()
557            .map(|&(s, t)| (b[s].pos.x - b[t].pos.x).hypot(b[s].pos.y - b[t].pos.y))
558            .sum::<f32>()
559            / links.len() as f32;
560        assert!(mean > 30. && mean < 150., "mean link length {mean}");
561    }
562}