flow_rs_renderer/
traits.rs

1//! Renderer trait definitions and shared types
2
3use crate::error::Result;
4use flow_rs_core::{Edge, EdgeId, Graph, Node, NodeId, Position, Rect, Size, Viewport};
5use js_sys;
6use serde_json;
7
8/// Supported rendering backends
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
10pub enum RendererType {
11    Canvas2D,
12    WebGL2,
13    WebGPU,
14}
15
16impl RendererType {
17    /// Get the name of the renderer
18    pub fn name(self) -> &'static str {
19        match self {
20            Self::Canvas2D => "Canvas2D",
21            Self::WebGL2 => "WebGL2",
22            Self::WebGPU => "WebGPU",
23        }
24    }
25
26    /// Get all available renderer types
27    pub fn all() -> &'static [Self] {
28        &[Self::Canvas2D, Self::WebGL2, Self::WebGPU]
29    }
30}
31
32/// Renderer capabilities and features
33#[derive(Debug, Clone, PartialEq)]
34pub struct RendererCapabilities {
35    pub name: String,
36    pub max_texture_size: u32,
37    pub max_textures: u32,
38    pub supports_instancing: bool,
39    pub supports_compute_shaders: bool,
40    pub supports_msaa: bool,
41    pub max_msaa_samples: u32,
42    pub max_viewport_size: (u32, u32),
43    pub memory_budget: Option<u64>, // bytes
44}
45
46impl Default for RendererCapabilities {
47    fn default() -> Self {
48        Self {
49            name: "Unknown".to_string(),
50            max_texture_size: 2048,
51            max_textures: 16,
52            supports_instancing: false,
53            supports_compute_shaders: false,
54            supports_msaa: false,
55            max_msaa_samples: 1,
56            max_viewport_size: (4096, 4096),
57            memory_budget: None,
58        }
59    }
60}
61
62/// Render statistics for performance monitoring
63#[derive(Debug, Clone, Default)]
64pub struct RenderStats {
65    pub frame_time_ms: f64,
66    pub nodes_rendered: usize,
67    pub edges_rendered: usize,
68    pub nodes_culled: usize,
69    pub edges_culled: usize,
70    pub draw_calls: usize,
71    pub triangles: usize,
72    pub memory_used: u64, // bytes
73}
74
75/// Styling information for nodes
76#[derive(Debug, Clone, PartialEq)]
77pub struct NodeStyle {
78    pub background_color: Option<String>,
79    pub border_color: Option<String>,
80    pub border_width: Option<f64>,
81    pub border_radius: Option<f64>,
82    pub shadow_color: Option<String>,
83    pub shadow_offset: Option<Position>,
84    pub shadow_blur: Option<f64>,
85    pub opacity: Option<f64>,
86}
87
88impl Default for NodeStyle {
89    fn default() -> Self {
90        Self {
91            background_color: Some("#ffffff".to_string()),
92            border_color: Some("#cccccc".to_string()),
93            border_width: Some(1.0),
94            border_radius: Some(4.0),
95            shadow_color: None,
96            shadow_offset: None,
97            shadow_blur: None,
98            opacity: Some(1.0),
99        }
100    }
101}
102
103/// Styling information for edges
104#[derive(Debug, Clone, PartialEq)]
105pub struct EdgeStyle {
106    pub stroke_color: Option<String>,
107    pub stroke_width: Option<f64>,
108    pub stroke_dasharray: Option<String>,
109    pub marker_start: Option<String>,
110    pub marker_end: Option<String>,
111    pub opacity: Option<f64>,
112    pub animated: bool,
113}
114
115impl Default for EdgeStyle {
116    fn default() -> Self {
117        Self {
118            stroke_color: Some("#999999".to_string()),
119            stroke_width: Some(2.0),
120            stroke_dasharray: None,
121            marker_start: None,
122            marker_end: None,
123            opacity: Some(1.0),
124            animated: false,
125        }
126    }
127}
128
129/// Selection visual state
130#[derive(Debug, Clone, PartialEq)]
131pub struct SelectionStyle {
132    pub color: String,
133    pub width: f64,
134    pub dasharray: Option<String>,
135    pub glow_color: Option<String>,
136    pub glow_blur: Option<f64>,
137}
138
139impl Default for SelectionStyle {
140    fn default() -> Self {
141        Self {
142            color: "#1a73e8".to_string(),
143            width: 2.0,
144            dasharray: None,
145            glow_color: Some("#1a73e8".to_string()),
146            glow_blur: Some(4.0),
147        }
148    }
149}
150
151/// Animated selection style with timing and effects
152#[derive(Debug, Clone, PartialEq)]
153pub struct AnimatedSelectionStyle {
154    pub base_style: SelectionStyle,
155    pub animation_duration_ms: f64,
156    pub pulse_enabled: bool,
157    pub fade_in_enabled: bool,
158    pub performance_mode: bool,
159    pub batch_rendering: bool,
160    // Animation state
161    pub is_animating: bool,
162    pub animation_start_time: Option<f64>,
163    pub animation_progress: f64,
164}
165
166impl AnimatedSelectionStyle {
167    pub fn new() -> Self {
168        Self {
169            base_style: SelectionStyle::default(),
170            animation_duration_ms: 300.0,
171            pulse_enabled: false,
172            fade_in_enabled: false,
173            performance_mode: false,
174            batch_rendering: false,
175            is_animating: false,
176            animation_start_time: None,
177            animation_progress: 0.0,
178        }
179    }
180
181    pub fn with_animation_duration(mut self, duration_ms: f64) -> Self {
182        self.animation_duration_ms = duration_ms;
183        self
184    }
185
186    pub fn with_pulse_enabled(mut self, enabled: bool) -> Self {
187        self.pulse_enabled = enabled;
188        self
189    }
190
191    pub fn with_fade_in_enabled(mut self, enabled: bool) -> Self {
192        self.fade_in_enabled = enabled;
193        self
194    }
195
196    pub fn with_performance_mode(mut self, enabled: bool) -> Self {
197        self.performance_mode = enabled;
198        self
199    }
200
201    pub fn with_batch_rendering(mut self, enabled: bool) -> Self {
202        self.batch_rendering = enabled;
203        self
204    }
205
206    pub fn animation_duration_ms(&self) -> f64 {
207        self.animation_duration_ms
208    }
209
210    pub fn pulse_enabled(&self) -> bool {
211        self.pulse_enabled
212    }
213
214    pub fn fade_in_enabled(&self) -> bool {
215        self.fade_in_enabled
216    }
217
218    pub fn is_animating(&self) -> bool {
219        self.is_animating
220    }
221
222    pub fn animation_progress(&self) -> f64 {
223        self.animation_progress
224    }
225
226    pub fn start_animation(&mut self) {
227        self.is_animating = true;
228        self.animation_start_time = Some(js_sys::Date::now());
229        self.animation_progress = 0.0;
230    }
231
232    pub fn update_animation(&mut self, elapsed_ms: f64) {
233        if !self.is_animating {
234            return;
235        }
236
237        self.animation_progress = (elapsed_ms / self.animation_duration_ms).min(1.0);
238
239        if self.animation_progress >= 1.0 {
240            self.is_animating = false;
241            self.animation_progress = 1.0;
242        }
243    }
244}
245
246impl Default for AnimatedSelectionStyle {
247    fn default() -> Self {
248        Self::new()
249    }
250}
251
252/// Multi-selection indicators style
253#[derive(Debug, Clone, PartialEq)]
254pub struct MultiSelectionStyle {
255    pub connection_lines: bool,
256    pub selection_count_indicator: bool,
257    pub connection_color: String,
258    pub connection_width: f64,
259    pub count_background_color: String,
260    pub count_text_color: String,
261}
262
263impl MultiSelectionStyle {
264    pub fn new() -> Self {
265        Self {
266            connection_lines: false,
267            selection_count_indicator: false,
268            connection_color: "#1a73e8".to_string(),
269            connection_width: 1.0,
270            count_background_color: "#1a73e8".to_string(),
271            count_text_color: "#ffffff".to_string(),
272        }
273    }
274
275    pub fn with_connection_lines(mut self, enabled: bool) -> Self {
276        self.connection_lines = enabled;
277        self
278    }
279
280    pub fn with_selection_count_indicator(mut self, enabled: bool) -> Self {
281        self.selection_count_indicator = enabled;
282        self
283    }
284}
285
286impl Default for MultiSelectionStyle {
287    fn default() -> Self {
288        Self::new()
289    }
290}
291
292/// Selection hover effects style
293#[derive(Debug, Clone, PartialEq)]
294pub struct SelectionHoverStyle {
295    pub hover_highlight_color: String,
296    pub hover_scale_factor: f64,
297    pub hover_glow_enabled: bool,
298    pub hover_glow_color: String,
299    pub hover_glow_blur: f64,
300}
301
302impl SelectionHoverStyle {
303    pub fn new() -> Self {
304        Self {
305            hover_highlight_color: "#ffeb3b".to_string(),
306            hover_scale_factor: 1.0,
307            hover_glow_enabled: false,
308            hover_glow_color: "#ffeb3b".to_string(),
309            hover_glow_blur: 8.0,
310        }
311    }
312
313    pub fn with_hover_highlight_color(mut self, color: String) -> Self {
314        self.hover_highlight_color = color;
315        self
316    }
317
318    pub fn with_hover_scale_factor(mut self, scale: f64) -> Self {
319        self.hover_scale_factor = scale;
320        self
321    }
322
323    pub fn with_hover_glow_enabled(mut self, enabled: bool) -> Self {
324        self.hover_glow_enabled = enabled;
325        self
326    }
327}
328
329impl Default for SelectionHoverStyle {
330    fn default() -> Self {
331        Self::new()
332    }
333}
334
335/// Background pattern configuration
336#[derive(Debug, Clone, PartialEq)]
337pub struct BackgroundConfig {
338    pub variant: BackgroundVariant,
339    pub color: String,
340    pub pattern_color: String,
341    pub size: f64,
342    pub opacity: f64,
343}
344
345#[derive(Debug, Clone, PartialEq)]
346pub enum BackgroundVariant {
347    None,
348    Dots,
349    Lines,
350    Grid,
351    Cross,
352}
353
354impl Default for BackgroundConfig {
355    fn default() -> Self {
356        Self {
357            variant: BackgroundVariant::None,
358            color: "#ffffff".to_string(),
359            pattern_color: "#e1e5e9".to_string(),
360            size: 20.0,
361            opacity: 0.8,
362        }
363    }
364}
365
366/// Trait for type-erased graph rendering
367pub trait GraphRenderer {
368    /// Get all nodes in the graph
369    fn get_nodes(&self) -> Vec<Box<dyn NodeRenderer>>;
370    /// Get all edges in the graph
371    fn get_edges(&self) -> Vec<Box<dyn EdgeRenderer>>;
372    /// Get node by ID
373    fn get_node(&self, id: &NodeId) -> Option<Box<dyn NodeRenderer>>;
374    /// Get edge by ID
375    fn get_edge(&self, id: &EdgeId) -> Option<Box<dyn EdgeRenderer>>;
376}
377
378/// Trait for type-erased node rendering
379pub trait NodeRenderer {
380    /// Get node ID
381    fn get_id(&self) -> NodeId;
382    /// Get node position
383    fn get_position(&self) -> Position;
384    /// Get node size
385    fn get_size(&self) -> Size;
386    /// Get node data as JSON string
387    fn get_data_json(&self) -> String;
388    /// Check if node is selected
389    fn is_selected(&self) -> bool;
390}
391
392/// Trait for type-erased edge rendering
393pub trait EdgeRenderer {
394    /// Get edge ID
395    fn get_id(&self) -> EdgeId;
396    /// Get source node ID
397    fn get_source(&self) -> NodeId;
398    /// Get target node ID
399    fn get_target(&self) -> NodeId;
400    /// Get edge data as JSON string
401    fn get_data_json(&self) -> String;
402    /// Check if edge is selected
403    fn is_selected(&self) -> bool;
404}
405
406/// Main renderer trait
407pub trait Renderer {
408    /// Initialize the renderer with a canvas element
409    fn new(canvas: &web_sys::HtmlCanvasElement) -> Result<Self>
410    where
411        Self: Sized;
412
413    /// Get renderer capabilities
414    fn capabilities(&self) -> &RendererCapabilities;
415
416    /// Set the viewport size
417    fn resize(&mut self, width: u32, height: u32) -> Result<()>;
418
419    /// Clear the render target
420    fn clear(&mut self, color: Option<&str>) -> Result<()>;
421
422    /// Set the viewport for rendering
423    fn set_viewport(&mut self, viewport: &Viewport);
424
425    /// Render a complete graph (dyn-compatible version)
426    fn render_graph_dyn(
427        &mut self,
428        graph: &dyn GraphRenderer,
429        viewport: &Viewport,
430    ) -> Result<RenderStats>;
431
432    /// Render a complete graph with selection state (dyn-compatible version)
433    fn render_graph_with_selection_dyn(
434        &mut self,
435        graph: &dyn GraphRenderer,
436        viewport: &Viewport,
437        selected_nodes: &[NodeId],
438    ) -> Result<RenderStats>;
439
440    /// Render nodes only (dyn-compatible version)
441    fn render_nodes_dyn(&mut self, nodes: &dyn NodeRenderer, viewport: &Viewport) -> Result<()>;
442
443    /// Render edges only (dyn-compatible version)
444    fn render_edges_dyn(&mut self, edges: &dyn EdgeRenderer, viewport: &Viewport) -> Result<()>;
445
446    /// Render selection indicators
447    fn render_selection(&mut self, selected_bounds: &[Rect], style: &SelectionStyle) -> Result<()>;
448
449    /// Render animated selection indicators
450    fn render_animated_selection(
451        &mut self,
452        selected_bounds: &[Rect],
453        style: &AnimatedSelectionStyle,
454    ) -> Result<()>;
455
456    /// Render multi-selection indicators
457    fn render_multi_selection(
458        &mut self,
459        selected_bounds: &[Rect],
460        style: &MultiSelectionStyle,
461    ) -> Result<()>;
462
463    /// Render selection hover effects
464    fn render_selection_hover(
465        &mut self,
466        bounds: &Rect,
467        hover_position: &Position,
468        style: &SelectionHoverStyle,
469    ) -> Result<()>;
470
471    /// Check if hover is active for given bounds
472    fn is_hover_active(&self, bounds: &Rect) -> bool;
473
474    /// Get current selection count
475    fn get_selection_count(&self) -> usize;
476
477    /// Check if multi-selection is active
478    fn is_multi_selection_active(&self) -> bool;
479
480    /// Render background pattern
481    fn render_background(&mut self, config: &BackgroundConfig, viewport: &Viewport) -> Result<()>;
482
483    /// Present the rendered frame
484    fn present(&mut self) -> Result<()>;
485
486    /// Get current render statistics
487    fn get_stats(&self) -> &RenderStats;
488
489    /// Check if the renderer context is valid
490    fn is_context_valid(&self) -> bool;
491
492    /// Recover from context loss (if supported)
493    fn recover_context(&mut self) -> Result<()>;
494}
495
496/// Trait for renderers that support node customization
497pub trait CustomNodeRenderer: Renderer {
498    /// Render a single node with custom style
499    fn render_custom_node<N>(
500        &mut self,
501        node: &Node<N>,
502        style: &NodeStyle,
503        viewport: &Viewport,
504    ) -> Result<()>
505    where
506        N: Clone + 'static;
507}
508
509/// Trait for renderers that support edge customization
510pub trait CustomEdgeRenderer: Renderer {
511    /// Render a single edge with custom style
512    fn render_custom_edge<E>(
513        &mut self,
514        edge: &Edge<E>,
515        style: &EdgeStyle,
516        source_pos: Position,
517        target_pos: Position,
518        viewport: &Viewport,
519    ) -> Result<()>
520    where
521        E: Clone + 'static;
522}
523
524/// Trait for high-performance batch rendering
525pub trait BatchRenderer: Renderer {
526    /// Begin a new render batch
527    fn begin_batch(&mut self) -> Result<()>;
528
529    /// Add a node to the current batch
530    fn batch_node<N>(&mut self, node: &Node<N>) -> Result<()>
531    where
532        N: Clone + 'static;
533
534    /// Add an edge to the current batch
535    fn batch_edge<E>(
536        &mut self,
537        edge: &Edge<E>,
538        source_pos: Position,
539        target_pos: Position,
540    ) -> Result<()>
541    where
542        E: Clone + 'static;
543
544    /// Render and flush the current batch
545    fn flush_batch(&mut self) -> Result<()>;
546}
547
548/// Utility functions for rendering
549pub mod utils {
550    use super::*;
551    use flow_rs_core::Position;
552
553    /// Parse CSS color string to RGBA values
554    pub fn parse_color(color: &str) -> Option<[f32; 4]> {
555        if color.starts_with('#') {
556            parse_hex_color(color)
557        } else if color.starts_with("rgb") {
558            parse_rgb_color(color)
559        } else {
560            parse_named_color(color)
561        }
562    }
563
564    fn parse_hex_color(color: &str) -> Option<[f32; 4]> {
565        let hex = color.trim_start_matches('#');
566
567        let (r, g, b, a) = match hex.len() {
568            3 => {
569                let r = u8::from_str_radix(&hex[0..1], 16).ok()? * 17;
570                let g = u8::from_str_radix(&hex[1..2], 16).ok()? * 17;
571                let b = u8::from_str_radix(&hex[2..3], 16).ok()? * 17;
572                (r, g, b, 255)
573            }
574            6 => {
575                let r = u8::from_str_radix(&hex[0..2], 16).ok()?;
576                let g = u8::from_str_radix(&hex[2..4], 16).ok()?;
577                let b = u8::from_str_radix(&hex[4..6], 16).ok()?;
578                (r, g, b, 255)
579            }
580            8 => {
581                let r = u8::from_str_radix(&hex[0..2], 16).ok()?;
582                let g = u8::from_str_radix(&hex[2..4], 16).ok()?;
583                let b = u8::from_str_radix(&hex[4..6], 16).ok()?;
584                let a = u8::from_str_radix(&hex[6..8], 16).ok()?;
585                (r, g, b, a)
586            }
587            _ => return None,
588        };
589
590        Some([
591            r as f32 / 255.0,
592            g as f32 / 255.0,
593            b as f32 / 255.0,
594            a as f32 / 255.0,
595        ])
596    }
597
598    fn parse_rgb_color(color: &str) -> Option<[f32; 4]> {
599        // Simple RGB parsing - could be enhanced
600        if color.starts_with("rgb(") {
601            let values = color.trim_start_matches("rgb(").trim_end_matches(')');
602            let parts: Vec<&str> = values.split(',').map(|s| s.trim()).collect();
603
604            if parts.len() == 3 {
605                let r = parts[0].parse::<u8>().ok()?;
606                let g = parts[1].parse::<u8>().ok()?;
607                let b = parts[2].parse::<u8>().ok()?;
608
609                return Some([r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0, 1.0]);
610            }
611        }
612        None
613    }
614
615    fn parse_named_color(color: &str) -> Option<[f32; 4]> {
616        // Basic named colors
617        match color.to_lowercase().as_str() {
618            "black" => Some([0.0, 0.0, 0.0, 1.0]),
619            "white" => Some([1.0, 1.0, 1.0, 1.0]),
620            "red" => Some([1.0, 0.0, 0.0, 1.0]),
621            "green" => Some([0.0, 1.0, 0.0, 1.0]),
622            "blue" => Some([0.0, 0.0, 1.0, 1.0]),
623            "transparent" => Some([0.0, 0.0, 0.0, 0.0]),
624            _ => None,
625        }
626    }
627
628    /// Calculate bezier curve points for edge rendering
629    pub fn calculate_bezier_curve(
630        start: Position,
631        end: Position,
632        control1: Option<Position>,
633        control2: Option<Position>,
634        steps: usize,
635    ) -> Vec<Position> {
636        let c1 = control1.unwrap_or_else(|| {
637            let mid_x = (start.x + end.x) / 2.0;
638            Position::new(mid_x, start.y)
639        });
640
641        let c2 = control2.unwrap_or_else(|| {
642            let mid_x = (start.x + end.x) / 2.0;
643            Position::new(mid_x, end.y)
644        });
645
646        (0..=steps)
647            .map(|i| {
648                let t = i as f64 / steps as f64;
649                let t2 = t * t;
650                let t3 = t2 * t;
651                let mt = 1.0 - t;
652                let mt2 = mt * mt;
653                let mt3 = mt2 * mt;
654
655                Position::new(
656                    mt3 * start.x + 3.0 * mt2 * t * c1.x + 3.0 * mt * t2 * c2.x + t3 * end.x,
657                    mt3 * start.y + 3.0 * mt2 * t * c1.y + 3.0 * mt * t2 * c2.y + t3 * end.y,
658                )
659            })
660            .collect()
661    }
662
663    /// Check if a rectangle is visible in the viewport
664    pub fn is_visible(bounds: &flow_rs_core::Rect, viewport: &Viewport) -> bool {
665        viewport.intersects_rect(*bounds)
666    }
667
668    /// Calculate node bounds with padding
669    pub fn node_bounds_with_padding(node: &Node<impl Clone>, padding: f64) -> flow_rs_core::Rect {
670        node.bounds().expand(padding)
671    }
672}
673
674// Trait implementations for type erasure
675impl<N, E> GraphRenderer for Graph<N, E>
676where
677    N: Clone + serde::Serialize + 'static,
678    E: Clone + serde::Serialize + 'static,
679{
680    fn get_nodes(&self) -> Vec<Box<dyn NodeRenderer>> {
681        self.nodes()
682            .map(|node| Box::new(ErasedNode::new(node.clone())) as Box<dyn NodeRenderer>)
683            .collect()
684    }
685
686    fn get_edges(&self) -> Vec<Box<dyn EdgeRenderer>> {
687        self.edges()
688            .map(|edge| Box::new(ErasedEdge::new(edge.clone())) as Box<dyn EdgeRenderer>)
689            .collect()
690    }
691
692    fn get_node(&self, id: &NodeId) -> Option<Box<dyn NodeRenderer>> {
693        self.nodes()
694            .find(|node| &node.id == id)
695            .map(|node| Box::new(ErasedNode::new(node.clone())) as Box<dyn NodeRenderer>)
696    }
697
698    fn get_edge(&self, id: &EdgeId) -> Option<Box<dyn EdgeRenderer>> {
699        self.edges()
700            .find(|edge| &edge.id == id)
701            .map(|edge| Box::new(ErasedEdge::new(edge.clone())) as Box<dyn EdgeRenderer>)
702    }
703}
704
705// Type-erased wrapper for nodes
706struct ErasedNode<N> {
707    node: Node<N>,
708}
709
710impl<N> ErasedNode<N> {
711    fn new(node: Node<N>) -> Self {
712        Self { node }
713    }
714}
715
716impl<N> NodeRenderer for ErasedNode<N>
717where
718    N: Clone + serde::Serialize + 'static,
719{
720    fn get_id(&self) -> NodeId {
721        self.node.id.clone()
722    }
723
724    fn get_position(&self) -> Position {
725        self.node.position
726    }
727
728    fn get_size(&self) -> Size {
729        self.node.size
730    }
731
732    fn get_data_json(&self) -> String {
733        serde_json::to_string(&self.node.data).unwrap_or_else(|_| "{}".to_string())
734    }
735
736    fn is_selected(&self) -> bool {
737        self.node.selected
738    }
739}
740
741// Type-erased wrapper for edges
742struct ErasedEdge<E> {
743    edge: Edge<E>,
744}
745
746impl<E> ErasedEdge<E> {
747    fn new(edge: Edge<E>) -> Self {
748        Self { edge }
749    }
750}
751
752impl<E> EdgeRenderer for ErasedEdge<E>
753where
754    E: Clone + serde::Serialize + 'static,
755{
756    fn get_id(&self) -> EdgeId {
757        self.edge.id.clone()
758    }
759
760    fn get_source(&self) -> NodeId {
761        self.edge.source.clone()
762    }
763
764    fn get_target(&self) -> NodeId {
765        self.edge.target.clone()
766    }
767
768    fn get_data_json(&self) -> String {
769        serde_json::to_string(&self.edge.data).unwrap_or_else(|_| "{}".to_string())
770    }
771
772    fn is_selected(&self) -> bool {
773        self.edge.selected
774    }
775}
776
777#[cfg(test)]
778mod tests {
779    use super::utils::*;
780    use super::*;
781
782    #[test]
783    fn test_color_parsing() {
784        // Test hex colors
785        assert_eq!(parse_color("#ff0000"), Some([1.0, 0.0, 0.0, 1.0]));
786        assert_eq!(parse_color("#00ff00"), Some([0.0, 1.0, 0.0, 1.0]));
787        assert_eq!(parse_color("#f00"), Some([1.0, 0.0, 0.0, 1.0]));
788
789        // Test named colors
790        assert_eq!(parse_color("black"), Some([0.0, 0.0, 0.0, 1.0]));
791        assert_eq!(parse_color("white"), Some([1.0, 1.0, 1.0, 1.0]));
792
793        // Test RGB colors
794        assert_eq!(parse_color("rgb(255, 0, 0)"), Some([1.0, 0.0, 0.0, 1.0]));
795    }
796
797    #[test]
798    fn test_bezier_curve_calculation() {
799        let start = Position::new(0.0, 0.0);
800        let end = Position::new(100.0, 100.0);
801
802        let points = calculate_bezier_curve(start, end, None, None, 10);
803        assert_eq!(points.len(), 11); // 0 to 10 inclusive
804        assert_eq!(points[0], start);
805        assert_eq!(points[10], end);
806    }
807
808    #[test]
809    fn test_renderer_type_name() {
810        assert_eq!(RendererType::Canvas2D.name(), "Canvas2D");
811        assert_eq!(RendererType::WebGL2.name(), "WebGL2");
812        assert_eq!(RendererType::WebGPU.name(), "WebGPU");
813    }
814
815    #[test]
816    fn test_default_styles() {
817        let node_style = NodeStyle::default();
818        assert_eq!(node_style.background_color, Some("#ffffff".to_string()));
819        assert_eq!(node_style.border_width, Some(1.0));
820
821        let edge_style = EdgeStyle::default();
822        assert_eq!(edge_style.stroke_color, Some("#999999".to_string()));
823        assert_eq!(edge_style.stroke_width, Some(2.0));
824    }
825}