oak_visualize/tree/
mod.rs

1#![doc = "Tree layout algorithms for AST visualization"]
2
3use crate::{
4    geometry::{Point, Rect, Size},
5    layout::{Edge, Layout},
6};
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9
10/// Tree node for visualization
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct TreeNode {
13    pub id: String,
14    pub label: String,
15    pub node_type: String,
16    pub children: Vec<TreeNode>,
17    pub attributes: HashMap<String, String>,
18    pub size: Option<Size>,
19}
20
21impl TreeNode {
22    pub fn new(id: String, label: String, node_type: String) -> Self {
23        Self { id, label, node_type, children: Vec::new(), attributes: HashMap::new(), size: None }
24    }
25
26    pub fn with_child(mut self, child: TreeNode) -> Self {
27        self.children.push(child);
28        self
29    }
30
31    pub fn with_children(mut self, children: Vec<TreeNode>) -> Self {
32        self.children = children;
33        self
34    }
35
36    pub fn with_attribute(mut self, key: String, value: String) -> Self {
37        self.attributes.insert(key, value);
38        self
39    }
40
41    pub fn with_size(mut self, size: Size) -> Self {
42        self.size = Some(size);
43        self
44    }
45
46    pub fn is_leaf(&self) -> bool {
47        self.children.is_empty()
48    }
49
50    pub fn depth(&self) -> usize {
51        if self.children.is_empty() { 1 } else { 1 + self.children.iter().map(|child| child.depth()).max().unwrap_or(0) }
52    }
53
54    pub fn node_count(&self) -> usize {
55        1 + self.children.iter().map(|child| child.node_count()).sum::<usize>()
56    }
57
58    pub fn leaf_count(&self) -> usize {
59        if self.children.is_empty() { 1 } else { self.children.iter().map(|child| child.leaf_count()).sum() }
60    }
61}
62
63/// Tree layout engine
64pub struct TreeLayout {
65    algorithm: TreeLayoutAlgorithm,
66    config: TreeLayoutConfig,
67}
68
69impl TreeLayout {
70    pub fn new(algorithm: TreeLayoutAlgorithm) -> Self {
71        Self { algorithm, config: TreeLayoutConfig::default() }
72    }
73
74    pub fn with_config(mut self, config: TreeLayoutConfig) -> Self {
75        self.config = config;
76        self
77    }
78
79    pub fn layout_tree(&self, tree: &TreeNode) -> crate::Result<Layout> {
80        match self.algorithm {
81            TreeLayoutAlgorithm::Layered => self.layered_layout(tree),
82            TreeLayoutAlgorithm::Radial => self.radial_layout(tree),
83            TreeLayoutAlgorithm::Compact => self.compact_layout(tree),
84            TreeLayoutAlgorithm::Balloon => self.balloon_layout(tree),
85        }
86    }
87
88    /// Layered tree layout (traditional top-down or left-right)
89    fn layered_layout(&self, tree: &TreeNode) -> crate::Result<Layout> {
90        let mut layout = Layout::new();
91        let mut node_positions = HashMap::new();
92        let mut level_widths = HashMap::new();
93
94        // Calculate level widths
95        self.calculate_level_widths(tree, 0, &mut level_widths);
96
97        // Position nodes
98        let mut current_positions = HashMap::new();
99        self.position_layered_node(tree, 0, 0.0, &level_widths, &mut current_positions, &mut node_positions);
100
101        // Add nodes to layout
102        for (id, (position, size)) in node_positions {
103            let rect = Rect::new(position, size);
104            layout.add_node(id, rect);
105        }
106
107        // Add edges
108        self.add_tree_edges(tree, &mut layout);
109
110        Ok(layout)
111    }
112
113    /// Radial tree layout (root at center, children in circles)
114    fn radial_layout(&self, tree: &TreeNode) -> crate::Result<Layout> {
115        let mut layout = Layout::new();
116        let mut node_positions = HashMap::new();
117
118        // Position root at center
119        let root_size = self.get_node_size(tree);
120        let root_pos = Point::new(0.0, 0.0);
121        node_positions.insert(tree.id.clone(), (root_pos, root_size));
122
123        // Position children in concentric circles
124        if !tree.children.is_empty() {
125            let radius = self.config.level_distance;
126            self.position_radial_children(
127                &tree.children,
128                root_pos,
129                radius,
130                0.0,
131                2.0 * std::f64::consts::PI,
132                1,
133                &mut node_positions,
134            );
135        }
136
137        // Add nodes to layout
138        for (id, (position, size)) in node_positions {
139            let rect = Rect::new(Point::new(position.x - size.width / 2.0, position.y - size.height / 2.0), size);
140            layout.add_node(id, rect);
141        }
142
143        // Add edges
144        self.add_tree_edges(tree, &mut layout);
145
146        Ok(layout)
147    }
148
149    /// Compact tree layout (minimize space usage)
150    fn compact_layout(&self, tree: &TreeNode) -> crate::Result<Layout> {
151        let mut layout = Layout::new();
152        let mut node_positions = HashMap::new();
153
154        // Use a modified layered approach with tighter packing
155        let positioned_tree = self.position_compact_node(tree, 0.0, 0.0, 0);
156        self.extract_positions(&positioned_tree, &mut node_positions);
157
158        // Add nodes to layout
159        for (id, (position, size)) in node_positions {
160            let rect = Rect::new(position, size);
161            layout.add_node(id, rect);
162        }
163
164        // Add edges
165        self.add_tree_edges(tree, &mut layout);
166
167        Ok(layout)
168    }
169
170    /// Balloon tree layout (children arranged in balloon-like clusters)
171    fn balloon_layout(&self, tree: &TreeNode) -> crate::Result<Layout> {
172        let mut layout = Layout::new();
173        let mut node_positions = HashMap::new();
174
175        // Similar to radial but with balloon-like clustering
176        let root_size = self.get_node_size(tree);
177        let root_pos = Point::new(0.0, 0.0);
178        node_positions.insert(tree.id.clone(), (root_pos, root_size));
179
180        // Position children in balloon clusters
181        if !tree.children.is_empty() {
182            let cluster_radius = self.config.level_distance;
183            self.position_balloon_children(&tree.children, root_pos, cluster_radius, &mut node_positions);
184        }
185
186        // Add nodes to layout
187        for (id, (position, size)) in node_positions {
188            let rect = Rect::new(Point::new(position.x - size.width / 2.0, position.y - size.height / 2.0), size);
189            layout.add_node(id, rect);
190        }
191
192        // Add edges
193        self.add_tree_edges(tree, &mut layout);
194
195        Ok(layout)
196    }
197
198    // Helper methods
199    fn get_node_size(&self, node: &TreeNode) -> Size {
200        node.size.unwrap_or(Size::new(self.config.node_width, self.config.node_height))
201    }
202
203    fn calculate_level_widths(&self, node: &TreeNode, level: usize, level_widths: &mut HashMap<usize, f64>) {
204        let node_size = self.get_node_size(node);
205        let current_width = level_widths.get(&level).unwrap_or(&0.0);
206        level_widths.insert(level, current_width + node_size.width + self.config.sibling_distance);
207
208        for child in &node.children {
209            self.calculate_level_widths(child, level + 1, level_widths);
210        }
211    }
212
213    fn position_layered_node(
214        &self,
215        node: &TreeNode,
216        level: usize,
217        _parent_x: f64,
218        level_widths: &HashMap<usize, f64>,
219        current_positions: &mut HashMap<usize, f64>,
220        node_positions: &mut HashMap<String, (Point, Size)>,
221    ) {
222        let node_size = self.get_node_size(node);
223        let level_width = level_widths.get(&level).unwrap_or(&0.0);
224        let default_x = -level_width / 2.0;
225        let current_x = current_positions.get(&level).unwrap_or(&default_x);
226
227        let x = if level == 0 {
228            0.0 // Root at center
229        }
230        else {
231            *current_x + node_size.width / 2.0
232        };
233
234        let y = level as f64 * self.config.level_distance;
235
236        node_positions.insert(node.id.clone(), (Point::new(x, y), node_size));
237        current_positions.insert(level, current_x + node_size.width + self.config.sibling_distance);
238
239        // Position children
240        for child in &node.children {
241            self.position_layered_node(child, level + 1, x, level_widths, current_positions, node_positions);
242        }
243    }
244
245    fn position_radial_children(
246        &self,
247        children: &[TreeNode],
248        center: Point,
249        radius: f64,
250        start_angle: f64,
251        angle_span: f64,
252        level: usize,
253        node_positions: &mut HashMap<String, (Point, Size)>,
254    ) {
255        if children.is_empty() {
256            return;
257        }
258
259        let angle_step = angle_span / children.len() as f64;
260
261        for (i, child) in children.iter().enumerate() {
262            let angle = start_angle + i as f64 * angle_step + angle_step / 2.0;
263            let child_pos = Point::new(center.x + radius * angle.cos(), center.y + radius * angle.sin());
264
265            let child_size = self.get_node_size(child);
266            node_positions.insert(child.id.clone(), (child_pos, child_size));
267
268            // Recursively position grandchildren
269            if !child.children.is_empty() {
270                let child_radius = radius + self.config.level_distance;
271                let child_angle_span = angle_step * 0.8; // Reduce angle span for children
272                self.position_radial_children(
273                    &child.children,
274                    child_pos,
275                    child_radius,
276                    angle - child_angle_span / 2.0,
277                    child_angle_span,
278                    level + 1,
279                    node_positions,
280                );
281            }
282        }
283    }
284
285    fn position_compact_node(&self, node: &TreeNode, x: f64, y: f64, level: usize) -> PositionedTreeNode {
286        let size = self.get_node_size(node);
287        let mut positioned_children = Vec::new();
288        let mut child_x = x;
289
290        for child in &node.children {
291            let positioned_child = self.position_compact_node(child, child_x, y + self.config.level_distance, level + 1);
292            child_x += positioned_child.subtree_width + self.config.sibling_distance;
293            positioned_children.push(positioned_child);
294        }
295
296        let subtree_width = if positioned_children.is_empty() {
297            size.width
298        }
299        else {
300            positioned_children.iter().map(|c| c.subtree_width).sum::<f64>()
301                + (positioned_children.len() - 1) as f64 * self.config.sibling_distance
302        };
303
304        // Center the node over its children
305        let node_x = if positioned_children.is_empty() { x } else { x + subtree_width / 2.0 - size.width / 2.0 };
306
307        PositionedTreeNode {
308            id: node.id.clone(),
309            position: Point::new(node_x, y),
310            size,
311            subtree_width,
312            children: positioned_children,
313        }
314    }
315
316    fn position_balloon_children(
317        &self,
318        children: &[TreeNode],
319        center: Point,
320        radius: f64,
321        node_positions: &mut HashMap<String, (Point, Size)>,
322    ) {
323        if children.is_empty() {
324            return;
325        }
326
327        // Arrange children in a circle around the parent
328        let angle_step = 2.0 * std::f64::consts::PI / children.len() as f64;
329
330        for (i, child) in children.iter().enumerate() {
331            let angle = i as f64 * angle_step;
332            let child_pos = Point::new(center.x + radius * angle.cos(), center.y + radius * angle.sin());
333
334            let child_size = self.get_node_size(child);
335            node_positions.insert(child.id.clone(), (child_pos, child_size));
336
337            // Recursively position grandchildren in smaller balloons
338            if !child.children.is_empty() {
339                let child_radius = radius * 0.6; // Smaller radius for child balloons
340                self.position_balloon_children(&child.children, child_pos, child_radius, node_positions);
341            }
342        }
343    }
344
345    fn extract_positions(&self, positioned_node: &PositionedTreeNode, positions: &mut HashMap<String, (Point, Size)>) {
346        positions.insert(positioned_node.id.clone(), (positioned_node.position, positioned_node.size));
347
348        for child in &positioned_node.children {
349            self.extract_positions(child, positions);
350        }
351    }
352
353    fn add_tree_edges(&self, node: &TreeNode, layout: &mut Layout) {
354        for child in &node.children {
355            let edge = Edge::new(node.id.clone(), child.id.clone());
356            layout.add_edge(edge);
357
358            // Recursively add edges for children
359            self.add_tree_edges(child, layout);
360        }
361    }
362}
363
364/// Tree layout configuration
365#[derive(Debug, Clone)]
366pub struct TreeLayoutConfig {
367    pub node_width: f64,
368    pub node_height: f64,
369    pub level_distance: f64,
370    pub sibling_distance: f64,
371    pub subtree_distance: f64,
372}
373
374impl Default for TreeLayoutConfig {
375    fn default() -> Self {
376        Self { node_width: 100.0, node_height: 40.0, level_distance: 80.0, sibling_distance: 20.0, subtree_distance: 40.0 }
377    }
378}
379
380/// Tree layout algorithms
381#[derive(Debug, Clone, Copy, PartialEq, Eq)]
382pub enum TreeLayoutAlgorithm {
383    Layered, // Traditional hierarchical layout
384    Radial,  // Radial/circular layout
385    Compact, // Space-efficient layout
386    Balloon, // Balloon-style clustering
387}
388
389/// Positioned tree node (internal representation)
390#[derive(Debug, Clone)]
391struct PositionedTreeNode {
392    id: String,
393    position: Point,
394    size: Size,
395    subtree_width: f64,
396    children: Vec<PositionedTreeNode>,
397}
398
399/// Tree renderer for generating visual output
400pub struct TreeRenderer {
401    config: TreeRenderConfig,
402}
403
404impl TreeRenderer {
405    pub fn new() -> Self {
406        Self { config: TreeRenderConfig::default() }
407    }
408
409    pub fn with_config(mut self, config: TreeRenderConfig) -> Self {
410        self.config = config;
411        self
412    }
413
414    // SVG rendering functionality would be implemented here
415    // Currently disabled due to missing svg dependency
416    // #[cfg(feature = "svg")]
417    // pub fn render_svg(&self, layout: &Layout, tree: &TreeNode) -> crate::Result<String> {
418    //     use svg::{
419    //         Document,
420    //         node::element::{Group, Line, Rectangle, Text},
421    //     };
422    //
423    //     let mut document = Document::new().set("viewBox", format!("0 0 {} {}", layout.bounds.width(), layout.bounds.height()));
424    //
425    //     // Render nodes
426    //     for (node_id, rect) in &layout.nodes {
427    //         let node_group = Group::new().set("id", format!("node-{}", node_id));
428    //
429    //         // Node rectangle
430    //         let node_rect = Rectangle::new()
431    //             .set("x", rect.x())
432    //             .set("y", rect.y())
433    //             .set("width", rect.width())
434    //             .set("height", rect.height())
435    //             .set("fill", &self.config.node_fill_color)
436    //             .set("stroke", &self.config.node_stroke_color)
437    //             .set("stroke-width", self.config.node_stroke_width);
438    //
439    //         // Node label
440    //         let label = self.get_node_label(tree, node_id);
441    //         let text = Text::new()
442    //             .set("x", rect.center().x)
443    //             .set("y", rect.center().y)
444    //             .set("text-anchor", "middle")
445    //             .set("dominant-baseline", "middle")
446    //             .set("font-family", &self.config.font_family)
447    //             .set("font-size", self.config.font_size)
448    //             .set("fill", &self.config.text_color)
449    //             .add(svg::node::Text::new(label));
450    //
451    //         document = document.add(node_group.add(node_rect).add(text));
452    //     }
453    //
454    //     // Render edges
455    //     for edge in &layout.edges {
456    //         if let (Some(from_rect), Some(to_rect)) = (layout.nodes.get(&edge.from), layout.nodes.get(&edge.to)) {
457    //             let from_point = from_rect.center();
458    //             let to_point = to_rect.center();
459    //
460    //             let line = Line::new()
461    //                 .set("x1", from_point.x)
462    //                 .set("y1", from_point.y)
463    //                 .set("x2", to_point.x)
464    //                 .set("y2", to_point.y)
465    //                 .set("stroke", &self.config.edge_color)
466    //                 .set("stroke-width", self.config.edge_width);
467    //
468    //             document = document.add(line);
469    //         }
470    //     }
471    //
472    //     Ok(document.to_string())
473    // }
474
475    fn get_node_label(&self, tree: &TreeNode, node_id: &str) -> String {
476        self.find_node_label(tree, node_id).unwrap_or_else(|| node_id.to_string())
477    }
478
479    fn find_node_label(&self, node: &TreeNode, target_id: &str) -> Option<String> {
480        if node.id == target_id {
481            return Some(node.label.clone());
482        }
483
484        for child in &node.children {
485            if let Some(label) = self.find_node_label(child, target_id) {
486                return Some(label);
487            }
488        }
489
490        None
491    }
492}
493
494impl Default for TreeRenderer {
495    fn default() -> Self {
496        Self::new()
497    }
498}
499
500/// Tree rendering configuration
501#[derive(Debug, Clone)]
502pub struct TreeRenderConfig {
503    pub node_fill_color: String,
504    pub node_stroke_color: String,
505    pub node_stroke_width: f64,
506    pub edge_color: String,
507    pub edge_width: f64,
508    pub text_color: String,
509    pub font_family: String,
510    pub font_size: f64,
511}
512
513impl Default for TreeRenderConfig {
514    fn default() -> Self {
515        Self {
516            node_fill_color: "#ffffff".to_string(),
517            node_stroke_color: "#000000".to_string(),
518            node_stroke_width: 1.0,
519            edge_color: "#666666".to_string(),
520            edge_width: 1.0,
521            text_color: "#000000".to_string(),
522            font_family: "Arial, sans-serif".to_string(),
523            font_size: 12.0,
524        }
525    }
526}