1use crate::theme::Gradient;
7use presentar_core::{
8 Brick, BrickAssertion, BrickBudget, BrickVerification, Canvas, Color, Constraints, Event,
9 LayoutResult, Point, Rect, Size, TextStyle, TypeId, Widget,
10};
11use std::any::Any;
12use std::time::Duration;
13
14#[derive(Debug, Clone)]
16pub struct GraphNode {
17 pub id: String,
19 pub label: Option<String>,
21 pub color: Color,
23 pub size: f32,
25 x: f64,
27 y: f64,
29 vx: f64,
31 vy: f64,
33 fixed: bool,
35}
36
37impl GraphNode {
38 #[must_use]
40 pub fn new(id: impl Into<String>) -> Self {
41 Self {
42 id: id.into(),
43 label: None,
44 color: Color::new(0.3, 0.7, 1.0, 1.0),
45 size: 1.0,
46 x: rand_float(),
47 y: rand_float(),
48 vx: 0.0,
49 vy: 0.0,
50 fixed: false,
51 }
52 }
53
54 #[must_use]
56 pub fn with_label(mut self, label: impl Into<String>) -> Self {
57 self.label = Some(label.into());
58 self
59 }
60
61 #[must_use]
63 pub fn with_color(mut self, color: Color) -> Self {
64 self.color = color;
65 self
66 }
67
68 #[must_use]
70 pub fn with_size(mut self, size: f32) -> Self {
71 self.size = size.clamp(0.5, 3.0);
72 self
73 }
74
75 #[must_use]
77 pub fn with_position(mut self, x: f64, y: f64) -> Self {
78 self.x = x.clamp(0.0, 1.0);
79 self.y = y.clamp(0.0, 1.0);
80 self
81 }
82
83 #[must_use]
85 pub fn with_fixed(mut self, fixed: bool) -> Self {
86 self.fixed = fixed;
87 self
88 }
89}
90
91#[derive(Debug, Clone)]
93pub struct GraphEdge {
94 pub source: usize,
96 pub target: usize,
98 pub weight: f64,
100 pub color: Option<Color>,
102}
103
104impl GraphEdge {
105 #[must_use]
107 pub fn new(source: usize, target: usize) -> Self {
108 Self {
109 source,
110 target,
111 weight: 1.0,
112 color: None,
113 }
114 }
115
116 #[must_use]
118 pub fn with_weight(mut self, weight: f64) -> Self {
119 self.weight = weight.max(0.1);
120 self
121 }
122
123 #[must_use]
125 pub fn with_color(mut self, color: Color) -> Self {
126 self.color = Some(color);
127 self
128 }
129}
130
131#[derive(Debug, Clone)]
133pub struct ForceParams {
134 pub repulsion: f64,
136 pub spring_strength: f64,
138 pub spring_length: f64,
140 pub damping: f64,
142 pub gravity: f64,
144}
145
146impl Default for ForceParams {
147 fn default() -> Self {
148 Self {
149 repulsion: 500.0,
150 spring_strength: 0.1,
151 spring_length: 0.2,
152 damping: 0.9,
153 gravity: 0.1,
154 }
155 }
156}
157
158#[derive(Debug, Clone)]
160pub struct ForceGraph {
161 nodes: Vec<GraphNode>,
162 edges: Vec<GraphEdge>,
163 params: ForceParams,
164 iterations: usize,
166 running: bool,
168 show_labels: bool,
170 show_edges: bool,
172 gradient: Option<Gradient>,
174 bounds: Rect,
175}
176
177impl Default for ForceGraph {
178 fn default() -> Self {
179 Self::new(Vec::new(), Vec::new())
180 }
181}
182
183impl ForceGraph {
184 #[must_use]
186 pub fn new(nodes: Vec<GraphNode>, edges: Vec<GraphEdge>) -> Self {
187 Self {
188 nodes,
189 edges,
190 params: ForceParams::default(),
191 iterations: 10,
192 running: true,
193 show_labels: true,
194 show_edges: true,
195 gradient: None,
196 bounds: Rect::default(),
197 }
198 }
199
200 #[must_use]
202 pub fn with_params(mut self, params: ForceParams) -> Self {
203 self.params = params;
204 self
205 }
206
207 #[must_use]
209 pub fn with_iterations(mut self, iterations: usize) -> Self {
210 self.iterations = iterations.clamp(1, 100);
211 self
212 }
213
214 #[must_use]
216 pub fn with_running(mut self, running: bool) -> Self {
217 self.running = running;
218 self
219 }
220
221 #[must_use]
223 pub fn with_labels(mut self, show: bool) -> Self {
224 self.show_labels = show;
225 self
226 }
227
228 #[must_use]
230 pub fn with_edges(mut self, show: bool) -> Self {
231 self.show_edges = show;
232 self
233 }
234
235 #[must_use]
237 pub fn with_gradient(mut self, gradient: Gradient) -> Self {
238 self.gradient = Some(gradient);
239 self
240 }
241
242 pub fn add_node(&mut self, node: GraphNode) {
244 self.nodes.push(node);
245 }
246
247 pub fn add_edge(&mut self, edge: GraphEdge) {
249 if edge.source < self.nodes.len() && edge.target < self.nodes.len() {
250 self.edges.push(edge);
251 }
252 }
253
254 fn step(&mut self) {
257 let n = self.nodes.len();
258 if n == 0 {
259 return;
260 }
261
262 let use_simd = n > 100;
263
264 if use_simd {
266 self.compute_repulsion_simd();
267 } else {
268 self.compute_repulsion_scalar();
269 }
270
271 self.compute_spring_forces();
273
274 self.apply_gravity();
276
277 self.update_positions();
279 }
280
281 fn compute_repulsion_scalar(&mut self) {
283 let n = self.nodes.len();
284 for i in 0..n {
285 if self.nodes[i].fixed {
286 continue;
287 }
288
289 for j in 0..n {
290 if i == j {
291 continue;
292 }
293
294 let dx = self.nodes[i].x - self.nodes[j].x;
295 let dy = self.nodes[i].y - self.nodes[j].y;
296 let dist_sq = dx * dx + dy * dy + 0.0001;
297 let dist = dist_sq.sqrt();
298
299 let force = self.params.repulsion / dist_sq;
300 self.nodes[i].vx += (dx / dist) * force;
301 self.nodes[i].vy += (dy / dist) * force;
302 }
303 }
304 }
305
306 fn compute_repulsion_simd(&mut self) {
308 let n = self.nodes.len();
309
310 for i in 0..n {
312 if self.nodes[i].fixed {
313 continue;
314 }
315
316 let xi = self.nodes[i].x;
317 let yi = self.nodes[i].y;
318 let mut vx_acc = 0.0;
319 let mut vy_acc = 0.0;
320
321 let mut j = 0;
323 while j + 4 <= n {
324 for k in 0..4 {
326 let jk = j + k;
327 if jk == i {
328 continue;
329 }
330
331 let dx = xi - self.nodes[jk].x;
332 let dy = yi - self.nodes[jk].y;
333 let dist_sq = dx * dx + dy * dy + 0.0001;
334 let dist = dist_sq.sqrt();
335
336 let force = self.params.repulsion / dist_sq;
337 vx_acc += (dx / dist) * force;
338 vy_acc += (dy / dist) * force;
339 }
340 j += 4;
341 }
342
343 while j < n {
345 if j != i {
346 let dx = xi - self.nodes[j].x;
347 let dy = yi - self.nodes[j].y;
348 let dist_sq = dx * dx + dy * dy + 0.0001;
349 let dist = dist_sq.sqrt();
350
351 let force = self.params.repulsion / dist_sq;
352 vx_acc += (dx / dist) * force;
353 vy_acc += (dy / dist) * force;
354 }
355 j += 1;
356 }
357
358 self.nodes[i].vx += vx_acc;
359 self.nodes[i].vy += vy_acc;
360 }
361 }
362
363 fn compute_spring_forces(&mut self) {
365 for edge in &self.edges {
366 let i = edge.source;
367 let j = edge.target;
368
369 if i >= self.nodes.len() || j >= self.nodes.len() {
370 continue;
371 }
372
373 let dx = self.nodes[j].x - self.nodes[i].x;
374 let dy = self.nodes[j].y - self.nodes[i].y;
375 let dist = (dx * dx + dy * dy + 0.0001).sqrt();
376
377 let force =
378 (dist - self.params.spring_length) * self.params.spring_strength * edge.weight;
379 let fx = (dx / dist) * force;
380 let fy = (dy / dist) * force;
381
382 if !self.nodes[i].fixed {
383 self.nodes[i].vx += fx;
384 self.nodes[i].vy += fy;
385 }
386 if !self.nodes[j].fixed {
387 self.nodes[j].vx -= fx;
388 self.nodes[j].vy -= fy;
389 }
390 }
391 }
392
393 fn apply_gravity(&mut self) {
395 for node in &mut self.nodes {
396 if node.fixed {
397 continue;
398 }
399
400 let dx = 0.5 - node.x;
401 let dy = 0.5 - node.y;
402 node.vx += dx * self.params.gravity;
403 node.vy += dy * self.params.gravity;
404 }
405 }
406
407 fn update_positions(&mut self) {
409 for node in &mut self.nodes {
410 if node.fixed {
411 continue;
412 }
413
414 node.vx *= self.params.damping;
416 node.vy *= self.params.damping;
417
418 let speed = node.vx.hypot(node.vy);
420 if speed > 0.1 {
421 node.vx = (node.vx / speed) * 0.1;
422 node.vy = (node.vy / speed) * 0.1;
423 }
424
425 node.x += node.vx;
427 node.y += node.vy;
428
429 node.x = node.x.clamp(0.05, 0.95);
431 node.y = node.y.clamp(0.05, 0.95);
432 }
433 }
434
435 fn render(&mut self, canvas: &mut dyn Canvas) {
436 if self.running {
437 for _ in 0..self.iterations {
438 self.step();
439 }
440 }
441
442 let edge_style = TextStyle {
443 color: Color::new(0.4, 0.4, 0.4, 1.0),
444 ..Default::default()
445 };
446
447 if self.show_edges {
449 for edge in &self.edges {
450 if edge.source >= self.nodes.len() || edge.target >= self.nodes.len() {
451 continue;
452 }
453
454 let src = &self.nodes[edge.source];
455 let tgt = &self.nodes[edge.target];
456
457 let x1 = self.bounds.x + (src.x * self.bounds.width as f64) as f32;
458 let y1 = self.bounds.y + (src.y * self.bounds.height as f64) as f32;
459 let x2 = self.bounds.x + (tgt.x * self.bounds.width as f64) as f32;
460 let y2 = self.bounds.y + (tgt.y * self.bounds.height as f64) as f32;
461
462 let style = if let Some(color) = edge.color {
463 TextStyle {
464 color,
465 ..Default::default()
466 }
467 } else {
468 edge_style.clone()
469 };
470
471 self.draw_line(canvas, x1, y1, x2, y2, &style);
473 }
474 }
475
476 for (idx, node) in self.nodes.iter().enumerate() {
478 let x = self.bounds.x + (node.x * self.bounds.width as f64) as f32;
479 let y = self.bounds.y + (node.y * self.bounds.height as f64) as f32;
480
481 let color = if let Some(ref gradient) = self.gradient {
482 gradient.sample(idx as f64 / self.nodes.len().max(1) as f64)
483 } else {
484 node.color
485 };
486
487 let style = TextStyle {
488 color,
489 ..Default::default()
490 };
491
492 let char = match node.size as i32 {
494 0 => "·",
495 1 => "•",
496 2 => "●",
497 _ => "⬤",
498 };
499 canvas.draw_text(char, Point::new(x, y), &style);
500
501 if self.show_labels {
503 if let Some(ref label) = node.label {
504 let label_style = TextStyle {
505 color: Color::new(0.6, 0.6, 0.6, 1.0),
506 ..Default::default()
507 };
508 canvas.draw_text(label, Point::new(x + 1.0, y), &label_style);
509 }
510 }
511 }
512 }
513
514 fn draw_line(
515 &self,
516 canvas: &mut dyn Canvas,
517 x1: f32,
518 y1: f32,
519 x2: f32,
520 y2: f32,
521 style: &TextStyle,
522 ) {
523 let dx = x2 - x1;
524 let dy = y2 - y1;
525 let steps = (dx.abs().max(dy.abs()) as usize).max(1);
526
527 for i in 0..=steps {
528 let t = i as f32 / steps as f32;
529 let x = x1 + dx * t;
530 let y = y1 + dy * t;
531
532 if x >= self.bounds.x
533 && x < self.bounds.x + self.bounds.width
534 && y >= self.bounds.y
535 && y < self.bounds.y + self.bounds.height
536 {
537 let char = if dx.abs() > dy.abs() * 2.0 {
539 "─"
540 } else if dy.abs() > dx.abs() * 2.0 {
541 "│"
542 } else if (dx > 0.0) == (dy > 0.0) {
543 "╲"
544 } else {
545 "╱"
546 };
547 canvas.draw_text(char, Point::new(x, y), style);
548 }
549 }
550 }
551}
552
553fn rand_float() -> f64 {
555 use std::collections::hash_map::DefaultHasher;
556 use std::hash::{Hash, Hasher};
557 use std::time::SystemTime;
558
559 let mut hasher = DefaultHasher::new();
560 SystemTime::now().hash(&mut hasher);
561 std::thread::current().id().hash(&mut hasher);
562 (hasher.finish() % 1000) as f64 / 1000.0
563}
564
565impl Widget for ForceGraph {
566 fn type_id(&self) -> TypeId {
567 TypeId::of::<Self>()
568 }
569
570 fn measure(&self, constraints: Constraints) -> Size {
571 Size::new(
572 constraints.max_width.min(60.0),
573 constraints.max_height.min(30.0),
574 )
575 }
576
577 fn layout(&mut self, bounds: Rect) -> LayoutResult {
578 self.bounds = bounds;
579 LayoutResult {
580 size: Size::new(bounds.width, bounds.height),
581 }
582 }
583
584 fn paint(&self, canvas: &mut dyn Canvas) {
585 if self.bounds.width < 10.0 || self.bounds.height < 5.0 {
586 return;
587 }
588
589 let mut mutable_self = self.clone();
590 mutable_self.render(canvas);
591 }
592
593 fn event(&mut self, _event: &Event) -> Option<Box<dyn Any + Send>> {
594 None
595 }
596
597 fn children(&self) -> &[Box<dyn Widget>] {
598 &[]
599 }
600
601 fn children_mut(&mut self) -> &mut [Box<dyn Widget>] {
602 &mut []
603 }
604}
605
606impl Brick for ForceGraph {
607 fn brick_name(&self) -> &'static str {
608 "ForceGraph"
609 }
610
611 fn assertions(&self) -> &[BrickAssertion] {
612 static ASSERTIONS: &[BrickAssertion] = &[BrickAssertion::max_latency_ms(16)];
613 ASSERTIONS
614 }
615
616 fn budget(&self) -> BrickBudget {
617 BrickBudget::uniform(16)
618 }
619
620 fn verify(&self) -> BrickVerification {
621 let mut passed = Vec::new();
622 let mut failed = Vec::new();
623
624 if self.bounds.width >= 10.0 && self.bounds.height >= 5.0 {
625 passed.push(BrickAssertion::max_latency_ms(16));
626 } else {
627 failed.push((
628 BrickAssertion::max_latency_ms(16),
629 "Size too small".to_string(),
630 ));
631 }
632
633 BrickVerification {
634 passed,
635 failed,
636 verification_time: Duration::from_micros(5),
637 }
638 }
639
640 fn to_html(&self) -> String {
641 String::new()
642 }
643
644 fn to_css(&self) -> String {
645 String::new()
646 }
647}
648
649#[cfg(test)]
650mod tests {
651 use super::*;
652 use crate::{CellBuffer, DirectTerminalCanvas};
653
654 #[test]
655 fn test_graph_node_creation() {
656 let node = GraphNode::new("test");
657 assert_eq!(node.id, "test");
658 assert!(node.x >= 0.0 && node.x <= 1.0);
659 assert!(node.y >= 0.0 && node.y <= 1.0);
660 }
661
662 #[test]
663 fn test_graph_node_with_position() {
664 let node = GraphNode::new("test").with_position(0.5, 0.5);
665 assert_eq!(node.x, 0.5);
666 assert_eq!(node.y, 0.5);
667 }
668
669 #[test]
670 fn test_graph_node_position_clamped() {
671 let node = GraphNode::new("test").with_position(-1.0, 2.0);
672 assert_eq!(node.x, 0.0);
673 assert_eq!(node.y, 1.0);
674 }
675
676 #[test]
677 fn test_graph_node_with_label() {
678 let node = GraphNode::new("test").with_label("Label");
679 assert_eq!(node.label, Some("Label".to_string()));
680 }
681
682 #[test]
683 fn test_graph_node_with_color() {
684 let color = Color::new(0.5, 0.6, 0.7, 1.0);
685 let node = GraphNode::new("test").with_color(color);
686 assert!((node.color.r - 0.5).abs() < 0.001);
687 }
688
689 #[test]
690 fn test_graph_node_with_size() {
691 let node = GraphNode::new("test").with_size(2.0);
692 assert!((node.size - 2.0).abs() < 0.001);
693 }
694
695 #[test]
696 fn test_graph_node_size_clamped() {
697 let node = GraphNode::new("test").with_size(0.1);
698 assert!((node.size - 0.5).abs() < 0.001);
699
700 let node = GraphNode::new("test").with_size(10.0);
701 assert!((node.size - 3.0).abs() < 0.001);
702 }
703
704 #[test]
705 fn test_graph_node_fixed() {
706 let node = GraphNode::new("test").with_fixed(true);
707 assert!(node.fixed);
708 }
709
710 #[test]
711 fn test_graph_edge_creation() {
712 let edge = GraphEdge::new(0, 1);
713 assert_eq!(edge.source, 0);
714 assert_eq!(edge.target, 1);
715 assert_eq!(edge.weight, 1.0);
716 }
717
718 #[test]
719 fn test_graph_edge_with_weight() {
720 let edge = GraphEdge::new(0, 1).with_weight(2.0);
721 assert_eq!(edge.weight, 2.0);
722 }
723
724 #[test]
725 fn test_graph_edge_weight_min() {
726 let edge = GraphEdge::new(0, 1).with_weight(0.01);
727 assert!((edge.weight - 0.1).abs() < 0.001);
728 }
729
730 #[test]
731 fn test_graph_edge_with_color() {
732 let color = Color::new(0.5, 0.6, 0.7, 1.0);
733 let edge = GraphEdge::new(0, 1).with_color(color);
734 assert!(edge.color.is_some());
735 }
736
737 #[test]
738 fn test_force_graph_creation() {
739 let graph = ForceGraph::new(
740 vec![GraphNode::new("a"), GraphNode::new("b")],
741 vec![GraphEdge::new(0, 1)],
742 );
743 assert_eq!(graph.nodes.len(), 2);
744 assert_eq!(graph.edges.len(), 1);
745 }
746
747 #[test]
748 fn test_force_graph_default() {
749 let graph = ForceGraph::default();
750 assert!(graph.nodes.is_empty());
751 assert!(graph.edges.is_empty());
752 }
753
754 #[test]
755 fn test_force_graph_with_params() {
756 let params = ForceParams {
757 repulsion: 100.0,
758 spring_strength: 0.5,
759 spring_length: 0.3,
760 damping: 0.8,
761 gravity: 0.2,
762 };
763 let graph = ForceGraph::default().with_params(params);
764 assert!((graph.params.repulsion - 100.0).abs() < 0.001);
765 }
766
767 #[test]
768 fn test_force_graph_with_gradient() {
769 let gradient = Gradient::two(
770 Color::new(1.0, 0.0, 0.0, 1.0),
771 Color::new(0.0, 0.0, 1.0, 1.0),
772 );
773 let graph = ForceGraph::default().with_gradient(gradient);
774 assert!(graph.gradient.is_some());
775 }
776
777 #[test]
778 fn test_force_graph_add_node() {
779 let mut graph = ForceGraph::default();
780 graph.add_node(GraphNode::new("test"));
781 assert_eq!(graph.nodes.len(), 1);
782 }
783
784 #[test]
785 fn test_force_graph_add_edge() {
786 let mut graph = ForceGraph::new(vec![GraphNode::new("a"), GraphNode::new("b")], vec![]);
787 graph.add_edge(GraphEdge::new(0, 1));
788 assert_eq!(graph.edges.len(), 1);
789 }
790
791 #[test]
792 fn test_force_graph_add_invalid_edge() {
793 let mut graph = ForceGraph::default();
794 graph.add_edge(GraphEdge::new(0, 1)); assert!(graph.edges.is_empty());
796 }
797
798 #[test]
799 fn test_force_graph_step() {
800 let mut graph = ForceGraph::new(
801 vec![
802 GraphNode::new("a").with_position(0.3, 0.5),
803 GraphNode::new("b").with_position(0.7, 0.5),
804 ],
805 vec![GraphEdge::new(0, 1)],
806 );
807 let initial_x0 = graph.nodes[0].x;
808 graph.step();
809 assert!(graph.nodes[0].x != initial_x0 || graph.nodes[0].vx != 0.0);
811 }
812
813 #[test]
814 fn test_force_graph_step_empty() {
815 let mut graph = ForceGraph::default();
816 graph.step(); }
818
819 #[test]
820 fn test_force_graph_fixed_node() {
821 let mut graph = ForceGraph::new(
822 vec![
823 GraphNode::new("a").with_position(0.3, 0.5).with_fixed(true),
824 GraphNode::new("b").with_position(0.7, 0.5),
825 ],
826 vec![],
827 );
828 let initial_x = graph.nodes[0].x;
829 graph.step();
830 assert_eq!(graph.nodes[0].x, initial_x);
832 }
833
834 #[test]
835 fn test_force_graph_measure() {
836 let graph = ForceGraph::default();
837 let constraints = Constraints::new(0.0, 100.0, 0.0, 50.0);
838 let size = graph.measure(constraints);
839 assert_eq!(size.width, 60.0);
840 assert_eq!(size.height, 30.0);
841 }
842
843 #[test]
844 fn test_force_graph_layout_and_paint() {
845 let mut graph = ForceGraph::new(
846 vec![
847 GraphNode::new("a")
848 .with_position(0.3, 0.3)
849 .with_label("Node A"),
850 GraphNode::new("b")
851 .with_position(0.7, 0.7)
852 .with_label("Node B"),
853 ],
854 vec![GraphEdge::new(0, 1)],
855 );
856
857 let mut buffer = CellBuffer::new(60, 30);
858 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
859
860 let result = graph.layout(Rect::new(0.0, 0.0, 60.0, 30.0));
861 assert_eq!(result.size.width, 60.0);
862
863 graph.paint(&mut canvas);
864
865 let cells = buffer.cells();
867 let non_empty = cells.iter().filter(|c| !c.symbol.is_empty()).count();
868 assert!(non_empty > 0, "Force graph should render some content");
869 }
870
871 #[test]
872 fn test_force_graph_paint_not_running() {
873 let mut graph = ForceGraph::new(
874 vec![
875 GraphNode::new("a").with_position(0.3, 0.3),
876 GraphNode::new("b").with_position(0.7, 0.7),
877 ],
878 vec![GraphEdge::new(0, 1)],
879 )
880 .with_running(false);
881
882 let mut buffer = CellBuffer::new(60, 30);
883 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
884
885 graph.layout(Rect::new(0.0, 0.0, 60.0, 30.0));
886 graph.paint(&mut canvas);
887 }
888
889 #[test]
890 fn test_force_graph_paint_no_labels() {
891 let mut graph = ForceGraph::new(
892 vec![GraphNode::new("a")
893 .with_position(0.5, 0.5)
894 .with_label("Hidden")],
895 vec![],
896 )
897 .with_labels(false);
898
899 let mut buffer = CellBuffer::new(60, 30);
900 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
901
902 graph.layout(Rect::new(0.0, 0.0, 60.0, 30.0));
903 graph.paint(&mut canvas);
904 }
905
906 #[test]
907 fn test_force_graph_paint_no_edges() {
908 let mut graph = ForceGraph::new(
909 vec![
910 GraphNode::new("a").with_position(0.3, 0.3),
911 GraphNode::new("b").with_position(0.7, 0.7),
912 ],
913 vec![GraphEdge::new(0, 1)],
914 )
915 .with_edges(false);
916
917 let mut buffer = CellBuffer::new(60, 30);
918 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
919
920 graph.layout(Rect::new(0.0, 0.0, 60.0, 30.0));
921 graph.paint(&mut canvas);
922 }
923
924 #[test]
925 fn test_force_graph_paint_with_gradient() {
926 let gradient = Gradient::two(
927 Color::new(0.2, 0.4, 0.8, 1.0),
928 Color::new(0.8, 0.4, 0.2, 1.0),
929 );
930 let mut graph = ForceGraph::new(
931 vec![
932 GraphNode::new("a").with_position(0.3, 0.5),
933 GraphNode::new("b").with_position(0.7, 0.5),
934 ],
935 vec![],
936 )
937 .with_gradient(gradient);
938
939 let mut buffer = CellBuffer::new(60, 30);
940 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
941
942 graph.layout(Rect::new(0.0, 0.0, 60.0, 30.0));
943 graph.paint(&mut canvas);
944 }
945
946 #[test]
947 fn test_force_graph_paint_edge_color() {
948 let mut graph = ForceGraph::new(
949 vec![
950 GraphNode::new("a").with_position(0.3, 0.5),
951 GraphNode::new("b").with_position(0.7, 0.5),
952 ],
953 vec![GraphEdge::new(0, 1).with_color(Color::new(1.0, 0.0, 0.0, 1.0))],
954 );
955
956 let mut buffer = CellBuffer::new(60, 30);
957 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
958
959 graph.layout(Rect::new(0.0, 0.0, 60.0, 30.0));
960 graph.paint(&mut canvas);
961 }
962
963 #[test]
964 fn test_force_graph_paint_small_bounds() {
965 let mut graph = ForceGraph::new(vec![GraphNode::new("a")], vec![]);
966
967 let mut buffer = CellBuffer::new(5, 3);
968 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
969
970 graph.layout(Rect::new(0.0, 0.0, 5.0, 3.0));
971 graph.paint(&mut canvas);
972 }
974
975 #[test]
976 fn test_force_graph_node_sizes() {
977 let mut graph = ForceGraph::new(
978 vec![
979 GraphNode::new("a").with_position(0.2, 0.5).with_size(0.5),
980 GraphNode::new("b").with_position(0.4, 0.5).with_size(1.0),
981 GraphNode::new("c").with_position(0.6, 0.5).with_size(2.0),
982 GraphNode::new("d").with_position(0.8, 0.5).with_size(3.0),
983 ],
984 vec![],
985 );
986
987 let mut buffer = CellBuffer::new(60, 30);
988 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
989
990 graph.layout(Rect::new(0.0, 0.0, 60.0, 30.0));
991 graph.paint(&mut canvas);
992 }
993
994 #[test]
995 fn test_force_graph_assertions() {
996 let graph = ForceGraph::default();
997 assert!(!graph.assertions().is_empty());
998 }
999
1000 #[test]
1001 fn test_force_graph_verify_valid() {
1002 let mut graph = ForceGraph::default();
1003 graph.bounds = Rect::new(0.0, 0.0, 60.0, 30.0);
1004 assert!(graph.verify().is_valid());
1005 }
1006
1007 #[test]
1008 fn test_force_graph_verify_invalid() {
1009 let mut graph = ForceGraph::default();
1010 graph.bounds = Rect::new(0.0, 0.0, 5.0, 3.0);
1011 assert!(!graph.verify().is_valid());
1012 }
1013
1014 #[test]
1015 fn test_force_graph_children() {
1016 let graph = ForceGraph::default();
1017 assert!(graph.children().is_empty());
1018 }
1019
1020 #[test]
1021 fn test_force_graph_children_mut() {
1022 let mut graph = ForceGraph::default();
1023 assert!(graph.children_mut().is_empty());
1024 }
1025
1026 #[test]
1027 fn test_force_graph_with_iterations() {
1028 let graph = ForceGraph::default().with_iterations(50);
1029 assert_eq!(graph.iterations, 50);
1030 }
1031
1032 #[test]
1033 fn test_force_graph_iterations_clamped() {
1034 let graph = ForceGraph::default().with_iterations(0);
1035 assert_eq!(graph.iterations, 1);
1036
1037 let graph = ForceGraph::default().with_iterations(500);
1038 assert_eq!(graph.iterations, 100);
1039 }
1040
1041 #[test]
1042 fn test_force_graph_with_labels() {
1043 let graph = ForceGraph::default().with_labels(false);
1044 assert!(!graph.show_labels);
1045 }
1046
1047 #[test]
1048 fn test_force_graph_with_edges() {
1049 let graph = ForceGraph::default().with_edges(false);
1050 assert!(!graph.show_edges);
1051 }
1052
1053 #[test]
1054 fn test_force_graph_with_running() {
1055 let graph = ForceGraph::default().with_running(false);
1056 assert!(!graph.running);
1057 }
1058
1059 #[test]
1060 fn test_large_graph_simd() {
1061 let nodes: Vec<GraphNode> = (0..150)
1063 .map(|i| {
1064 GraphNode::new(format!("n{i}"))
1065 .with_position((i as f64 % 10.0) / 10.0, (i as f64 / 10.0) / 15.0)
1066 })
1067 .collect();
1068 let edges: Vec<GraphEdge> = (0..100).map(|i| GraphEdge::new(i, (i + 1) % 150)).collect();
1069 let mut graph = ForceGraph::new(nodes, edges);
1070 graph.step();
1071 assert_eq!(graph.nodes.len(), 150);
1073 }
1074
1075 #[test]
1076 fn test_large_graph_simd_with_fixed() {
1077 let mut nodes: Vec<GraphNode> = (0..150)
1078 .map(|i| {
1079 GraphNode::new(format!("n{i}"))
1080 .with_position((i as f64 % 10.0) / 10.0, (i as f64 / 10.0) / 15.0)
1081 })
1082 .collect();
1083 nodes[0] = nodes[0].clone().with_fixed(true);
1085 nodes[50] = nodes[50].clone().with_fixed(true);
1086 nodes[100] = nodes[100].clone().with_fixed(true);
1087
1088 let edges: Vec<GraphEdge> = (0..50).map(|i| GraphEdge::new(i, (i + 1) % 150)).collect();
1089 let mut graph = ForceGraph::new(nodes, edges);
1090 graph.step();
1091 assert_eq!(graph.nodes.len(), 150);
1092 }
1093
1094 #[test]
1095 fn test_force_params_default() {
1096 let params = ForceParams::default();
1097 assert!(params.repulsion > 0.0);
1098 assert!(params.spring_strength > 0.0);
1099 assert!(params.damping > 0.0 && params.damping <= 1.0);
1100 }
1101
1102 #[test]
1103 fn test_force_graph_brick_name() {
1104 let graph = ForceGraph::default();
1105 assert_eq!(graph.brick_name(), "ForceGraph");
1106 }
1107
1108 #[test]
1109 fn test_force_graph_budget() {
1110 let graph = ForceGraph::default();
1111 let budget = graph.budget();
1112 assert!(budget.layout_ms > 0);
1113 }
1114
1115 #[test]
1116 fn test_force_graph_to_html() {
1117 let graph = ForceGraph::default();
1118 assert!(graph.to_html().is_empty());
1119 }
1120
1121 #[test]
1122 fn test_force_graph_to_css() {
1123 let graph = ForceGraph::default();
1124 assert!(graph.to_css().is_empty());
1125 }
1126
1127 #[test]
1128 fn test_force_graph_type_id() {
1129 let graph = ForceGraph::default();
1130 let type_id = Widget::type_id(&graph);
1131 assert_eq!(type_id, TypeId::of::<ForceGraph>());
1132 }
1133
1134 #[test]
1135 fn test_force_graph_event() {
1136 let mut graph = ForceGraph::default();
1137 let event = Event::Resize {
1138 width: 80.0,
1139 height: 24.0,
1140 };
1141 assert!(graph.event(&event).is_none());
1142 }
1143
1144 #[test]
1145 fn test_draw_line_horizontal() {
1146 let graph = ForceGraph::new(vec![], vec![]);
1147 let mut graph = graph;
1148 graph.bounds = Rect::new(0.0, 0.0, 60.0, 30.0);
1149
1150 let mut buffer = CellBuffer::new(60, 30);
1151 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
1152
1153 let style = TextStyle {
1154 color: Color::new(0.5, 0.5, 0.5, 1.0),
1155 ..Default::default()
1156 };
1157 graph.draw_line(&mut canvas, 10.0, 15.0, 50.0, 15.0, &style);
1158 }
1159
1160 #[test]
1161 fn test_draw_line_vertical() {
1162 let graph = ForceGraph::new(vec![], vec![]);
1163 let mut graph = graph;
1164 graph.bounds = Rect::new(0.0, 0.0, 60.0, 30.0);
1165
1166 let mut buffer = CellBuffer::new(60, 30);
1167 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
1168
1169 let style = TextStyle {
1170 color: Color::new(0.5, 0.5, 0.5, 1.0),
1171 ..Default::default()
1172 };
1173 graph.draw_line(&mut canvas, 30.0, 5.0, 30.0, 25.0, &style);
1174 }
1175
1176 #[test]
1177 fn test_draw_line_diagonal() {
1178 let graph = ForceGraph::new(vec![], vec![]);
1179 let mut graph = graph;
1180 graph.bounds = Rect::new(0.0, 0.0, 60.0, 30.0);
1181
1182 let mut buffer = CellBuffer::new(60, 30);
1183 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
1184
1185 let style = TextStyle {
1186 color: Color::new(0.5, 0.5, 0.5, 1.0),
1187 ..Default::default()
1188 };
1189 graph.draw_line(&mut canvas, 10.0, 10.0, 50.0, 20.0, &style);
1190 }
1191
1192 #[test]
1193 fn test_spring_forces_invalid_indices() {
1194 let mut graph = ForceGraph::new(vec![GraphNode::new("a").with_position(0.5, 0.5)], vec![]);
1195 graph.edges.push(GraphEdge::new(0, 5));
1197 graph.compute_spring_forces(); }
1199
1200 #[test]
1201 fn test_spring_forces_with_fixed_nodes() {
1202 let mut graph = ForceGraph::new(
1203 vec![
1204 GraphNode::new("a").with_position(0.3, 0.5).with_fixed(true),
1205 GraphNode::new("b").with_position(0.7, 0.5).with_fixed(true),
1206 ],
1207 vec![GraphEdge::new(0, 1)],
1208 );
1209 let initial_x0 = graph.nodes[0].x;
1210 let initial_x1 = graph.nodes[1].x;
1211 graph.compute_spring_forces();
1212 assert_eq!(graph.nodes[0].x, initial_x0);
1214 assert_eq!(graph.nodes[1].x, initial_x1);
1215 }
1216
1217 #[test]
1218 fn test_gravity_with_fixed_nodes() {
1219 let mut graph = ForceGraph::new(
1220 vec![GraphNode::new("a").with_position(0.1, 0.1).with_fixed(true)],
1221 vec![],
1222 );
1223 let initial_vx = graph.nodes[0].vx;
1224 graph.apply_gravity();
1225 assert_eq!(graph.nodes[0].vx, initial_vx);
1227 }
1228
1229 #[test]
1230 fn test_high_velocity_clamping() {
1231 let mut graph = ForceGraph::new(vec![GraphNode::new("a").with_position(0.5, 0.5)], vec![]);
1232 graph.nodes[0].vx = 10.0;
1233 graph.nodes[0].vy = 10.0;
1234 graph.update_positions();
1235 let speed = graph.nodes[0].vx.hypot(graph.nodes[0].vy);
1237 assert!(speed <= 0.11); }
1239
1240 #[test]
1241 fn test_rand_float() {
1242 let val = rand_float();
1243 assert!(val >= 0.0 && val <= 1.0);
1244 }
1245}