bbx_draw/visualizers/
graph_topology.rs

1//! DSP graph topology visualizer.
2
3use bbx_dsp::{
4    BlockCategory,
5    graph::{BlockSnapshot, ConnectionSnapshot, GraphTopologySnapshot, ModulationConnectionSnapshot},
6};
7use nannou::{
8    Draw,
9    geom::{Point2, Rect, Vec2},
10};
11
12use crate::{Visualizer, config::GraphTopologyConfig};
13
14/// Visualizes a DSP graph's topology as connected blocks.
15///
16/// Blocks are arranged left-to-right based on their topological depth
17/// (distance from source blocks).
18pub struct GraphTopologyVisualizer {
19    topology: GraphTopologySnapshot,
20    config: GraphTopologyConfig,
21    block_positions: Vec<Point2>,
22    depths: Vec<usize>,
23}
24
25impl GraphTopologyVisualizer {
26    /// Create a new graph topology visualizer with default configuration.
27    pub fn new(topology: GraphTopologySnapshot) -> Self {
28        let mut visualizer = Self {
29            topology,
30            config: GraphTopologyConfig::default(),
31            block_positions: Vec::new(),
32            depths: Vec::new(),
33        };
34        visualizer.compute_layout();
35        visualizer
36    }
37
38    /// Create a new graph topology visualizer with custom configuration.
39    pub fn with_config(topology: GraphTopologySnapshot, config: GraphTopologyConfig) -> Self {
40        let mut visualizer = Self {
41            topology,
42            config,
43            block_positions: Vec::new(),
44            depths: Vec::new(),
45        };
46        visualizer.compute_layout();
47        visualizer
48    }
49
50    /// Get the current topology snapshot.
51    pub fn topology(&self) -> &GraphTopologySnapshot {
52        &self.topology
53    }
54
55    /// Update the topology (recomputes layout).
56    pub fn set_topology(&mut self, topology: GraphTopologySnapshot) {
57        self.topology = topology;
58        self.compute_layout();
59    }
60
61    fn compute_layout(&mut self) {
62        let num_blocks = self.topology.blocks.len();
63        if num_blocks == 0 {
64            self.block_positions.clear();
65            self.depths.clear();
66            return;
67        }
68
69        self.depths = self.compute_depths();
70        let max_depth = *self.depths.iter().max().unwrap_or(&0);
71
72        let mut blocks_at_depth: Vec<Vec<usize>> = vec![Vec::new(); max_depth + 1];
73        for (block_idx, &depth) in self.depths.iter().enumerate() {
74            blocks_at_depth[depth].push(block_idx);
75        }
76
77        self.block_positions = vec![Point2::ZERO; num_blocks];
78
79        for (depth, block_indices) in blocks_at_depth.iter().enumerate() {
80            let num_at_depth = block_indices.len();
81            let x = depth as f32 * (self.config.block_width + self.config.horizontal_spacing);
82
83            for (row, &block_idx) in block_indices.iter().enumerate() {
84                let y_offset = (num_at_depth as f32 - 1.0) / 2.0;
85                let y = (row as f32 - y_offset) * (self.config.block_height + self.config.vertical_spacing);
86                self.block_positions[block_idx] = Point2::new(x, y);
87            }
88        }
89    }
90
91    fn compute_depths(&self) -> Vec<usize> {
92        let num_blocks = self.topology.blocks.len();
93        let mut depths = vec![0usize; num_blocks];
94        let mut changed = true;
95
96        while changed {
97            changed = false;
98            for conn in &self.topology.connections {
99                let new_depth = depths[conn.from_block] + 1;
100                if new_depth > depths[conn.to_block] {
101                    depths[conn.to_block] = new_depth;
102                    changed = true;
103                }
104            }
105        }
106
107        depths
108    }
109
110    fn color_for_category(&self, category: BlockCategory) -> nannou::color::Rgb {
111        match category {
112            BlockCategory::Generator => self.config.generator_color,
113            BlockCategory::Effector => self.config.effector_color,
114            BlockCategory::Modulator => self.config.modulator_color,
115            BlockCategory::IO => self.config.io_color,
116        }
117    }
118
119    fn draw_block(&self, draw: &Draw, block: &BlockSnapshot, position: Point2, bounds: Rect) {
120        let offset_x = bounds.left() + bounds.w() / 2.0;
121        let offset_y = bounds.bottom() + bounds.h() / 2.0;
122        let adjusted_pos = Point2::new(position.x + offset_x, position.y + offset_y);
123
124        let block_rect = Rect::from_xy_wh(adjusted_pos, [self.config.block_width, self.config.block_height].into());
125
126        if bounds.overlap(block_rect).is_none() {
127            return;
128        }
129
130        let color = self.color_for_category(block.category);
131
132        draw.rect()
133            .xy(adjusted_pos)
134            .w_h(self.config.block_width, self.config.block_height)
135            .color(color);
136
137        draw.text(&block.name)
138            .xy(adjusted_pos)
139            .color(self.config.text_color)
140            .font_size(12);
141    }
142
143    fn draw_audio_connection(&self, draw: &Draw, conn: &ConnectionSnapshot, bounds: Rect) {
144        let from_pos = self.block_positions.get(conn.from_block);
145        let to_pos = self.block_positions.get(conn.to_block);
146
147        if let (Some(&from), Some(&to)) = (from_pos, to_pos) {
148            let offset_x = bounds.left() + bounds.w() / 2.0;
149            let offset_y = bounds.bottom() + bounds.h() / 2.0;
150
151            let start = Point2::new(from.x + offset_x + self.config.block_width / 2.0, from.y + offset_y);
152            let end = Point2::new(to.x + offset_x - self.config.block_width / 2.0, to.y + offset_y);
153
154            let control_offset = (end.x - start.x) * 0.4;
155            let control1 = Point2::new(start.x + control_offset, start.y);
156            let control2 = Point2::new(end.x - control_offset, end.y);
157
158            let points = bezier_points(start, control1, control2, end, 20);
159
160            draw.path()
161                .stroke()
162                .weight(self.config.audio_connection_weight)
163                .color(self.config.audio_connection_color)
164                .points(points.clone());
165
166            if self.config.show_arrows && points.len() >= 2 {
167                let arrow_end = points[points.len() - 1];
168                let arrow_prev = points[points.len() - 2];
169                draw_arrow_head(
170                    draw,
171                    arrow_prev,
172                    arrow_end,
173                    self.config.arrow_size,
174                    self.config.audio_connection_color,
175                );
176            }
177        }
178    }
179
180    fn draw_modulation_connection(&self, draw: &Draw, conn: &ModulationConnectionSnapshot, bounds: Rect) {
181        let from_pos = self.block_positions.get(conn.from_block);
182        let to_pos = self.block_positions.get(conn.to_block);
183
184        if let (Some(&from), Some(&to)) = (from_pos, to_pos) {
185            let offset_x = bounds.left() + bounds.w() / 2.0;
186            let offset_y = bounds.bottom() + bounds.h() / 2.0;
187
188            let from_center = Point2::new(from.x + offset_x, from.y + offset_y);
189            let to_center = Point2::new(to.x + offset_x, to.y + offset_y);
190
191            let (start, end, control1, control2, label_pos) = if (from.x - to.x).abs() < 1.0 {
192                let start = Point2::new(
193                    from_center.x - self.config.block_width / 2.0,
194                    from_center.y - self.config.block_height / 2.0,
195                );
196                let end = Point2::new(
197                    to_center.x - self.config.block_width / 2.0,
198                    to_center.y + self.config.block_height / 2.0,
199                );
200                let curve_offset = self.config.horizontal_spacing * 0.5;
201                let control1 = Point2::new(start.x - curve_offset, start.y);
202                let control2 = Point2::new(end.x - curve_offset, end.y);
203                let label_pos = Point2::new(start.x - curve_offset - 5.0, (start.y + end.y) / 2.0);
204                (start, end, control1, control2, label_pos)
205            } else {
206                let start = Point2::new(from_center.x + self.config.block_width / 2.0, from_center.y);
207                let end = Point2::new(
208                    to_center.x - self.config.block_width / 2.0,
209                    to_center.y - self.config.block_height * 0.25,
210                );
211                let control_offset = (end.x - start.x).abs() * 0.4;
212                let control1 = Point2::new(start.x + control_offset, start.y);
213                let control2 = Point2::new(end.x - control_offset, end.y);
214                let label_pos = Point2::new(end.x + 5.0, end.y);
215                (start, end, control1, control2, label_pos)
216            };
217
218            let points = bezier_points(start, control1, control2, end, 40);
219            let dashed = dashed_bezier_points(&points, self.config.dash_length, self.config.dash_gap);
220
221            for segment in &dashed {
222                draw.path()
223                    .stroke()
224                    .weight(self.config.modulation_connection_weight)
225                    .color(self.config.modulation_connection_color)
226                    .points(segment.clone());
227            }
228
229            if self.config.show_arrows && points.len() >= 2 {
230                let arrow_end = points[points.len() - 1];
231                let arrow_prev = points[points.len() - 2];
232                draw_arrow_head(
233                    draw,
234                    arrow_prev,
235                    arrow_end,
236                    self.config.arrow_size * 0.8,
237                    self.config.modulation_connection_color,
238                );
239            }
240
241            draw.text(&conn.parameter_name)
242                .xy(label_pos)
243                .color(self.config.modulation_connection_color)
244                .font_size(10)
245                .right_justify();
246        }
247    }
248}
249
250impl Visualizer for GraphTopologyVisualizer {
251    fn update(&mut self) {}
252
253    fn draw(&self, draw: &Draw, bounds: Rect) {
254        for conn in &self.topology.connections {
255            self.draw_audio_connection(draw, conn, bounds);
256        }
257
258        for conn in &self.topology.modulation_connections {
259            self.draw_modulation_connection(draw, conn, bounds);
260        }
261
262        for (idx, block) in self.topology.blocks.iter().enumerate() {
263            if let Some(&pos) = self.block_positions.get(idx) {
264                self.draw_block(draw, block, pos, bounds);
265            }
266        }
267    }
268}
269
270fn bezier_points(p0: Point2, p1: Point2, p2: Point2, p3: Point2, segments: usize) -> Vec<Point2> {
271    (0..=segments)
272        .map(|i| {
273            let t = i as f32 / segments as f32;
274            let t2 = t * t;
275            let t3 = t2 * t;
276            let mt = 1.0 - t;
277            let mt2 = mt * mt;
278            let mt3 = mt2 * mt;
279
280            Point2::new(
281                mt3 * p0.x + 3.0 * mt2 * t * p1.x + 3.0 * mt * t2 * p2.x + t3 * p3.x,
282                mt3 * p0.y + 3.0 * mt2 * t * p1.y + 3.0 * mt * t2 * p2.y + t3 * p3.y,
283            )
284        })
285        .collect()
286}
287
288fn draw_arrow_head(draw: &Draw, from: Point2, to: Point2, size: f32, color: nannou::color::Rgb) {
289    let dir = Vec2::new(to.x - from.x, to.y - from.y);
290    let len = (dir.x * dir.x + dir.y * dir.y).sqrt();
291    if len < 0.001 {
292        return;
293    }
294
295    let dir = Vec2::new(dir.x / len, dir.y / len);
296    let perp = Vec2::new(-dir.y, dir.x);
297
298    let tip = to;
299    let left = Point2::new(
300        tip.x - dir.x * size + perp.x * size * 0.5,
301        tip.y - dir.y * size + perp.y * size * 0.5,
302    );
303    let right = Point2::new(
304        tip.x - dir.x * size - perp.x * size * 0.5,
305        tip.y - dir.y * size - perp.y * size * 0.5,
306    );
307
308    draw.tri().points(tip, left, right).color(color);
309}
310
311fn dashed_bezier_points(points: &[Point2], dash_length: f32, gap_length: f32) -> Vec<Vec<Point2>> {
312    let mut segments = Vec::new();
313    let mut current_segment = Vec::new();
314    let mut distance_in_pattern = 0.0;
315    let pattern_length = dash_length + gap_length;
316
317    for i in 0..points.len() {
318        let is_dash = (distance_in_pattern % pattern_length) < dash_length;
319
320        if is_dash {
321            current_segment.push(points[i]);
322        } else if !current_segment.is_empty() {
323            segments.push(current_segment);
324            current_segment = Vec::new();
325        }
326
327        if i + 1 < points.len() {
328            let dx = points[i + 1].x - points[i].x;
329            let dy = points[i + 1].y - points[i].y;
330            let dist = (dx * dx + dy * dy).sqrt();
331            distance_in_pattern += dist;
332        }
333    }
334
335    if !current_segment.is_empty() {
336        segments.push(current_segment);
337    }
338
339    segments
340}
341
342#[cfg(test)]
343mod tests {
344    use super::*;
345
346    #[test]
347    fn test_empty_topology() {
348        let topology = GraphTopologySnapshot {
349            blocks: vec![],
350            connections: vec![],
351            modulation_connections: vec![],
352        };
353        let visualizer = GraphTopologyVisualizer::new(topology);
354        assert!(visualizer.block_positions.is_empty());
355    }
356
357    #[test]
358    fn test_single_block() {
359        let topology = GraphTopologySnapshot {
360            blocks: vec![BlockSnapshot {
361                id: 0,
362                name: "Oscillator".to_string(),
363                category: BlockCategory::Generator,
364                input_count: 0,
365                output_count: 1,
366            }],
367            connections: vec![],
368            modulation_connections: vec![],
369        };
370        let visualizer = GraphTopologyVisualizer::new(topology);
371        assert_eq!(visualizer.block_positions.len(), 1);
372        assert_eq!(visualizer.depths[0], 0);
373    }
374
375    #[test]
376    fn test_depth_calculation() {
377        let topology = GraphTopologySnapshot {
378            blocks: vec![
379                BlockSnapshot {
380                    id: 0,
381                    name: "Osc".to_string(),
382                    category: BlockCategory::Generator,
383                    input_count: 0,
384                    output_count: 1,
385                },
386                BlockSnapshot {
387                    id: 1,
388                    name: "Gain".to_string(),
389                    category: BlockCategory::Effector,
390                    input_count: 1,
391                    output_count: 1,
392                },
393                BlockSnapshot {
394                    id: 2,
395                    name: "Output".to_string(),
396                    category: BlockCategory::IO,
397                    input_count: 1,
398                    output_count: 0,
399                },
400            ],
401            connections: vec![
402                ConnectionSnapshot {
403                    from_block: 0,
404                    from_output: 0,
405                    to_block: 1,
406                    to_input: 0,
407                },
408                ConnectionSnapshot {
409                    from_block: 1,
410                    from_output: 0,
411                    to_block: 2,
412                    to_input: 0,
413                },
414            ],
415            modulation_connections: vec![],
416        };
417        let visualizer = GraphTopologyVisualizer::new(topology);
418        assert_eq!(visualizer.depths[0], 0);
419        assert_eq!(visualizer.depths[1], 1);
420        assert_eq!(visualizer.depths[2], 2);
421    }
422}