Skip to main content

presentar_terminal/widgets/
force_graph.rs

1//! Force-directed graph layout widget.
2//!
3//! Implements SIMD/WGPU-first architecture per SPEC-024 Section 16.
4//! Uses SIMD acceleration for force calculations on large graphs (>100 nodes).
5
6use 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/// A node in the force graph.
15#[derive(Debug, Clone)]
16pub struct GraphNode {
17    /// Node identifier.
18    pub id: String,
19    /// Node label.
20    pub label: Option<String>,
21    /// Node color.
22    pub color: Color,
23    /// Node size (for rendering).
24    pub size: f32,
25    /// Current X position (normalized 0-1).
26    x: f64,
27    /// Current Y position (normalized 0-1).
28    y: f64,
29    /// X velocity.
30    vx: f64,
31    /// Y velocity.
32    vy: f64,
33    /// Whether node position is fixed.
34    fixed: bool,
35}
36
37impl GraphNode {
38    /// Create a new node.
39    #[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    /// Set label.
55    #[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    /// Set color.
62    #[must_use]
63    pub fn with_color(mut self, color: Color) -> Self {
64        self.color = color;
65        self
66    }
67
68    /// Set size.
69    #[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    /// Set initial position.
76    #[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    /// Fix node position.
84    #[must_use]
85    pub fn with_fixed(mut self, fixed: bool) -> Self {
86        self.fixed = fixed;
87        self
88    }
89}
90
91/// An edge in the force graph.
92#[derive(Debug, Clone)]
93pub struct GraphEdge {
94    /// Source node index.
95    pub source: usize,
96    /// Target node index.
97    pub target: usize,
98    /// Edge weight (affects spring strength).
99    pub weight: f64,
100    /// Edge color.
101    pub color: Option<Color>,
102}
103
104impl GraphEdge {
105    /// Create a new edge.
106    #[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    /// Set weight.
117    #[must_use]
118    pub fn with_weight(mut self, weight: f64) -> Self {
119        self.weight = weight.max(0.1);
120        self
121    }
122
123    /// Set color.
124    #[must_use]
125    pub fn with_color(mut self, color: Color) -> Self {
126        self.color = Some(color);
127        self
128    }
129}
130
131/// Force simulation parameters.
132#[derive(Debug, Clone)]
133pub struct ForceParams {
134    /// Repulsion strength between nodes.
135    pub repulsion: f64,
136    /// Spring strength for edges.
137    pub spring_strength: f64,
138    /// Ideal spring length.
139    pub spring_length: f64,
140    /// Damping factor.
141    pub damping: f64,
142    /// Gravity toward center.
143    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/// Force-directed graph widget.
159#[derive(Debug, Clone)]
160pub struct ForceGraph {
161    nodes: Vec<GraphNode>,
162    edges: Vec<GraphEdge>,
163    params: ForceParams,
164    /// Number of simulation iterations per paint.
165    iterations: usize,
166    /// Whether simulation is running.
167    running: bool,
168    /// Show node labels.
169    show_labels: bool,
170    /// Show edge lines.
171    show_edges: bool,
172    /// Optional gradient for node coloring.
173    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    /// Create a new force graph.
185    #[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    /// Set force parameters.
201    #[must_use]
202    pub fn with_params(mut self, params: ForceParams) -> Self {
203        self.params = params;
204        self
205    }
206
207    /// Set iterations per frame.
208    #[must_use]
209    pub fn with_iterations(mut self, iterations: usize) -> Self {
210        self.iterations = iterations.clamp(1, 100);
211        self
212    }
213
214    /// Toggle simulation.
215    #[must_use]
216    pub fn with_running(mut self, running: bool) -> Self {
217        self.running = running;
218        self
219    }
220
221    /// Toggle labels.
222    #[must_use]
223    pub fn with_labels(mut self, show: bool) -> Self {
224        self.show_labels = show;
225        self
226    }
227
228    /// Toggle edges.
229    #[must_use]
230    pub fn with_edges(mut self, show: bool) -> Self {
231        self.show_edges = show;
232        self
233    }
234
235    /// Set gradient for node coloring.
236    #[must_use]
237    pub fn with_gradient(mut self, gradient: Gradient) -> Self {
238        self.gradient = Some(gradient);
239        self
240    }
241
242    /// Add a node.
243    pub fn add_node(&mut self, node: GraphNode) {
244        self.nodes.push(node);
245    }
246
247    /// Add an edge.
248    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    /// Run one simulation step.
255    /// Uses SIMD for large graphs (>100 nodes).
256    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        // Compute repulsion forces between all pairs
265        if use_simd {
266            self.compute_repulsion_simd();
267        } else {
268            self.compute_repulsion_scalar();
269        }
270
271        // Compute spring forces for edges
272        self.compute_spring_forces();
273
274        // Apply gravity toward center
275        self.apply_gravity();
276
277        // Update positions
278        self.update_positions();
279    }
280
281    /// Scalar repulsion force computation.
282    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    /// SIMD-optimized repulsion force computation.
307    fn compute_repulsion_simd(&mut self) {
308        let n = self.nodes.len();
309
310        // Process in blocks for SIMD-friendly computation
311        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            // Process 4 nodes at a time
322            let mut j = 0;
323            while j + 4 <= n {
324                // Skip self
325                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            // Handle remaining nodes
344            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    /// Compute spring forces for edges.
364    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    /// Apply gravity toward center.
394    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    /// Update node positions.
408    fn update_positions(&mut self) {
409        for node in &mut self.nodes {
410            if node.fixed {
411                continue;
412            }
413
414            // Apply damping
415            node.vx *= self.params.damping;
416            node.vy *= self.params.damping;
417
418            // Limit velocity
419            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            // Update position
426            node.x += node.vx;
427            node.y += node.vy;
428
429            // Keep within bounds
430            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        // Draw edges
448        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                // Draw line using braille/ASCII
472                self.draw_line(canvas, x1, y1, x2, y2, &style);
473            }
474        }
475
476        // Draw nodes
477        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            // Draw node
493            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            // Draw label
502            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                // Choose line character based on direction
538                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
553/// Simple pseudo-random float generator for initial positions.
554fn 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)); // Invalid: no nodes
795        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        // Position should change after step
810        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(); // Should not panic
817    }
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        // Fixed node should not move
831        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        // Verify something was rendered
866        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        // Should not crash
973    }
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        // Test SIMD path (>100 nodes)
1062        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        // Should not panic
1072        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        // Fix some nodes
1084        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        // Manually add an invalid edge
1196        graph.edges.push(GraphEdge::new(0, 5));
1197        graph.compute_spring_forces(); // Should not panic
1198    }
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        // Both fixed, velocities should change but positions won't be updated since step updates positions
1213        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        // Fixed node should not have velocity changed
1226        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        // Velocity should be clamped
1236        let speed = graph.nodes[0].vx.hypot(graph.nodes[0].vy);
1237        assert!(speed <= 0.11); // Some tolerance for damping
1238    }
1239
1240    #[test]
1241    fn test_rand_float() {
1242        let val = rand_float();
1243        assert!(val >= 0.0 && val <= 1.0);
1244    }
1245}