1use crate::error::Result;
4use flow_rs_core::{Edge, EdgeId, Graph, Node, NodeId, Position, Rect, Size, Viewport};
5use js_sys;
6use serde_json;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
10pub enum RendererType {
11 Canvas2D,
12 WebGL2,
13 WebGPU,
14}
15
16impl RendererType {
17 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 pub fn all() -> &'static [Self] {
28 &[Self::Canvas2D, Self::WebGL2, Self::WebGPU]
29 }
30}
31
32#[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>, }
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#[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, }
74
75#[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#[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#[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#[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 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#[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#[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#[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
366pub trait GraphRenderer {
368 fn get_nodes(&self) -> Vec<Box<dyn NodeRenderer>>;
370 fn get_edges(&self) -> Vec<Box<dyn EdgeRenderer>>;
372 fn get_node(&self, id: &NodeId) -> Option<Box<dyn NodeRenderer>>;
374 fn get_edge(&self, id: &EdgeId) -> Option<Box<dyn EdgeRenderer>>;
376}
377
378pub trait NodeRenderer {
380 fn get_id(&self) -> NodeId;
382 fn get_position(&self) -> Position;
384 fn get_size(&self) -> Size;
386 fn get_data_json(&self) -> String;
388 fn is_selected(&self) -> bool;
390}
391
392pub trait EdgeRenderer {
394 fn get_id(&self) -> EdgeId;
396 fn get_source(&self) -> NodeId;
398 fn get_target(&self) -> NodeId;
400 fn get_data_json(&self) -> String;
402 fn is_selected(&self) -> bool;
404}
405
406pub trait Renderer {
408 fn new(canvas: &web_sys::HtmlCanvasElement) -> Result<Self>
410 where
411 Self: Sized;
412
413 fn capabilities(&self) -> &RendererCapabilities;
415
416 fn resize(&mut self, width: u32, height: u32) -> Result<()>;
418
419 fn clear(&mut self, color: Option<&str>) -> Result<()>;
421
422 fn set_viewport(&mut self, viewport: &Viewport);
424
425 fn render_graph_dyn(
427 &mut self,
428 graph: &dyn GraphRenderer,
429 viewport: &Viewport,
430 ) -> Result<RenderStats>;
431
432 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 fn render_nodes_dyn(&mut self, nodes: &dyn NodeRenderer, viewport: &Viewport) -> Result<()>;
442
443 fn render_edges_dyn(&mut self, edges: &dyn EdgeRenderer, viewport: &Viewport) -> Result<()>;
445
446 fn render_selection(&mut self, selected_bounds: &[Rect], style: &SelectionStyle) -> Result<()>;
448
449 fn render_animated_selection(
451 &mut self,
452 selected_bounds: &[Rect],
453 style: &AnimatedSelectionStyle,
454 ) -> Result<()>;
455
456 fn render_multi_selection(
458 &mut self,
459 selected_bounds: &[Rect],
460 style: &MultiSelectionStyle,
461 ) -> Result<()>;
462
463 fn render_selection_hover(
465 &mut self,
466 bounds: &Rect,
467 hover_position: &Position,
468 style: &SelectionHoverStyle,
469 ) -> Result<()>;
470
471 fn is_hover_active(&self, bounds: &Rect) -> bool;
473
474 fn get_selection_count(&self) -> usize;
476
477 fn is_multi_selection_active(&self) -> bool;
479
480 fn render_background(&mut self, config: &BackgroundConfig, viewport: &Viewport) -> Result<()>;
482
483 fn present(&mut self) -> Result<()>;
485
486 fn get_stats(&self) -> &RenderStats;
488
489 fn is_context_valid(&self) -> bool;
491
492 fn recover_context(&mut self) -> Result<()>;
494}
495
496pub trait CustomNodeRenderer: Renderer {
498 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
509pub trait CustomEdgeRenderer: Renderer {
511 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
524pub trait BatchRenderer: Renderer {
526 fn begin_batch(&mut self) -> Result<()>;
528
529 fn batch_node<N>(&mut self, node: &Node<N>) -> Result<()>
531 where
532 N: Clone + 'static;
533
534 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 fn flush_batch(&mut self) -> Result<()>;
546}
547
548pub mod utils {
550 use super::*;
551 use flow_rs_core::Position;
552
553 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 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 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 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 pub fn is_visible(bounds: &flow_rs_core::Rect, viewport: &Viewport) -> bool {
665 viewport.intersects_rect(*bounds)
666 }
667
668 pub fn node_bounds_with_padding(node: &Node<impl Clone>, padding: f64) -> flow_rs_core::Rect {
670 node.bounds().expand(padding)
671 }
672}
673
674impl<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
705struct 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
741struct 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 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 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 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); 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}