OXYGENHUB
# bd1d3ad70c70 oxygenhub-android / rust-core / src / backdrop / swarm.rs
7.4 KiB · Rust · last changed The hub's living backgrounds in the app: the mark at sign-in, Your sky on the dashboard, live runs pulse (refs #23) 8 days ago source blame history raw
1
// Copied from OxygenHub crates/backdrop/src/swarm.rs (6dc080f6) by
2
// scripts/sync-backdrop.sh: change it there, never here.
3
//! The swarm (docs/backgrounds.md §2): Admin › Runners › Images. The hub
4
//! and the runners as nodes, each ringed by a dot for every image it
5
//! holds; while a runner fetches an image, chunks (small squares) stream
6
//! to it from every holder, the hub and its peers alike, as Jetstream's
7
//! swarm fetches them. The data is the page's own snapshot.
8
9
use crate::backdrop::field::{knot, margins, Dot, Field, Named, Orbit, ZOOM};
10
use crate::backdrop::scene::Scene;
11
use crate::backdrop::sky::Colours;
12
use std::f32::consts::PI;
13
14
/// Dots for each image a node holds (its chunks, drawn as a sky's knot).
15
const PER_IMAGE: usize = 6;
16
/// Chunks streaming along each holder-to-fetcher path, their speed, and
17
/// the most streams drawn.
18
const CHUNKS: usize = 20;
19
const CHUNK_SPEED: f32 = 40.0;
20
const MAX_STREAMS: usize = 40;
21
22
/// One image: the nodes that hold it and those fetching it now.
23
pub struct Image {
24
    pub held: Vec<usize>,
25
    pub fetching: Vec<usize>,
26
}
27
28
struct Held {
29
    node: usize,
30
    orbit: Orbit,
31
}
32
33
pub struct Swarm {
34
    field: Field,
35
    /// The hub first, then the runners.
36
    names: Vec<String>,
37
    pos: Vec<(f32, f32)>,
38
    held: Vec<Held>,
39
    /// Streams: from, to, and their chunks' first index.
40
    streams: Vec<(usize, usize, usize)>,
41
    w: f32,
42
    h: f32,
43
    t: f32,
44
}
45
46
impl Swarm {
47
    pub fn new(names: Vec<String>, images: Vec<Image>, still: bool) -> Swarm {
48
        let mut field = Field::new(still);
49
        for i in 0..names.len() {
50
            field.dots.push(Dot::new(0.0, 0.0, if i == 0 { 10.0 } else { 8.0 }, 4.0));
51
        }
52
        let mut held = Vec::new();
53
        let mut count = vec![0usize; names.len()];
54
        for img in &images {
55
            for &node in img.held.iter().filter(|&&n| n < names.len()) {
56
                count[node] += 1;
57
            }
58
        }
59
        for (node, name) in names.iter().enumerate() {
60
            for (k, orbit) in knot(name, count[node] * PER_IMAGE).into_iter().enumerate() {
61
                let mut d = Dot::new(0.0, 0.0, 2.0 * ZOOM, if k % PER_IMAGE == 0 { 5.0 } else { 0.0 });
62
                d.group = node as u32;
63
                d.order = k as u32;
64
                d.bright = 0.75;
65
                field.dots.push(d);
66
                held.push(Held { node, orbit });
67
            }
68
        }
69
        let mut streams = Vec::new();
70
        'images: for img in &images {
71
            for &to in img.fetching.iter().filter(|&&n| n < names.len()) {
72
                for &from in img.held.iter().filter(|&&n| n < names.len() && n != to) {
73
                    if streams.len() == MAX_STREAMS {
74
                        break 'images;
75
                    }
76
                    streams.push((from, to, field.dots.len()));
77
                    for _ in 0..CHUNKS {
78
                        let mut d = Dot::new(0.0, 0.0, 1.8 * ZOOM, 5.0);
79
                        d.bright = 0.0;
80
                        field.dots.push(d);
81
                    }
82
                }
83
            }
84
        }
85
        let mut s = Swarm { field, names, pos: Vec::new(), held, streams, w: 0.0, h: 0.0, t: 0.0 };
86
        s.layout(None);
87
        s
88
    }
89
90
    fn layout(&mut self, avoid: Option<(f32, f32)>) {
91
        let (w, h) = (self.w, self.h);
92
        let runners = self.names.len().saturating_sub(1);
93
        let (hub_x, run_x) = match margins(w, avoid) {
94
            Some((l, x1)) => (l * 0.5, x1 + (w - x1) * 0.5),
95
            None => (w * 0.1, w * 0.9),
96
        };
97
        self.pos = (0..self.names.len())
98
            .map(|i| match i {
99
                0 => (hub_x, h * 0.5),
100
                _ if runners <= 1 => (run_x, h * 0.5),
101
                i => (run_x, h * 0.2 + h * 0.6 * (i - 1) as f32 / (runners - 1) as f32),
102
            })
103
            .collect();
104
        self.field.named = self
105
            .names
106
            .iter()
107
            .zip(&self.pos)
108
            .enumerate()
109
            .map(|(i, (name, &(x, y)))| {
110
                let reach = self.held.iter().filter(|h| h.node == i).map(|h| h.orbit.radius).fold(12.0, f32::max);
111
                Named { name: name.clone(), x, y, reach }
112
            })
113
            .collect();
114
    }
115
116
    fn place(&mut self) {
117
        let n = self.names.len();
118
        for (i, &(x, y)) in self.pos.iter().enumerate() {
119
            self.field.dots[i].x = x;
120
            self.field.dots[i].y = y;
121
        }
122
        for (k, hd) in self.held.iter().enumerate() {
123
            let (x, y) = hd.orbit.at(self.pos[hd.node], self.t);
124
            self.field.dots[n + k].x = x;
125
            self.field.dots[n + k].y = y;
126
        }
127
        for &(from, to, first) in &self.streams {
128
            let (a, b) = (self.pos[from], self.pos[to]);
129
            let len = (b.0 - a.0).hypot(b.1 - a.1).max(1.0);
130
            // Peers' streams bow one way, the hub's the other, so they
131
            // read as separate sources.
132
            let bow = if from == 0 { -0.12 } else { 0.12 };
133
            let ctrl = ((a.0 + b.0) / 2.0, (a.1 + b.1) / 2.0 + len * bow);
134
            for k in 0..CHUNKS {
135
                let s = (k as f32 / CHUNKS as f32 + self.t * CHUNK_SPEED / len).fract();
136
                let u = 1.0 - s;
137
                let d = &mut self.field.dots[first + k];
138
                d.x = u * u * a.0 + 2.0 * u * s * ctrl.0 + s * s * b.0;
139
                d.y = u * u * a.1 + 2.0 * u * s * ctrl.1 + s * s * b.1;
140
                d.bright = (s * PI).sin();
141
            }
142
        }
143
    }
144
}
145
146
impl Scene for Swarm {
147
    fn resize(&mut self, w: f32, h: f32, avoid: Option<(f32, f32)>) {
148
        self.w = w;
149
        self.h = h;
150
        self.layout(avoid);
151
        self.place();
152
    }
153
    fn point(&mut self, at: Option<(f32, f32)>) {
154
        self.field.point(at);
155
    }
156
    fn step(&mut self, dt: f32) {
157
        if !self.field.still {
158
            self.t += dt.clamp(0.0, 0.1);
159
        }
160
        self.place();
161
        self.field.light(dt);
162
    }
163
    fn instances(&self, c: &Colours, out: &mut Vec<f32>) {
164
        self.field.instances(c, out)
165
    }
166
    fn links(&self, c: &Colours, out: &mut Vec<f32>) {
167
        self.field.links(c, out)
168
    }
169
    fn label(&self) -> Option<(&str, f32, f32)> {
170
        self.field.label()
171
    }
172
    fn busy(&self) -> bool {
173
        self.field.easing()
174
    }
175
    fn still(&self) -> bool {
176
        self.field.still
177
    }
178
    fn size(&self) -> (f32, f32) {
179
        (self.w, self.h)
180
    }
181
}
182
183
#[cfg(test)]
184
mod tests {
185
    use super::*;
186
187
    #[test]
188
    fn chunks_stream_to_a_fetching_runner_from_every_holder() {
189
        let names = vec!["hub".to_string(), "earth-1".to_string(), "plex-1".to_string()];
190
        let images = vec![Image { held: vec![0, 1], fetching: vec![2] }, Image { held: vec![0, 1, 2], fetching: vec![] }];
191
        let mut s = Swarm::new(names, images, false);
192
        s.resize(2560.0, 1270.0, Some((707.0, 1838.0)));
193
        assert!(s.pos[0].0 < 707.0 && s.pos[1].0 > 1838.0 && s.pos[2].0 > 1838.0, "the hub on one side, runners on the other");
194
        assert_eq!(s.held.len(), 5 * PER_IMAGE, "dots for every image each node holds");
195
        assert_eq!(s.streams.iter().map(|&(a, b, _)| (a, b)).collect::<Vec<_>>(), vec![(0, 2), (1, 2)], "from the hub and the peer");
196
        let first = s.streams[0].2;
197
        let before = s.field.dots[first].x;
198
        s.step(0.1);
199
        assert_ne!(before, s.field.dots[first].x);
200
        s.point(Some(s.pos[1]));
201
        for _ in 0..30 {
202
            s.step(0.02);
203
        }
204
        assert_eq!(s.label().map(|l| l.0), Some("earth-1"));
205
    }
206
}