1use 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
14pub struct GraphTopologyVisualizer {
19 topology: GraphTopologySnapshot,
20 config: GraphTopologyConfig,
21 block_positions: Vec<Point2>,
22 depths: Vec<usize>,
23}
24
25impl GraphTopologyVisualizer {
26 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 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 pub fn topology(&self) -> &GraphTopologySnapshot {
52 &self.topology
53 }
54
55 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}