Skip to main content

jellyflow_layout/
tidy_tree.rs

1use std::collections::{BTreeMap, BTreeSet, VecDeque};
2
3use jellyflow_core::{CanvasPoint, CanvasSize, Graph, GraphTransaction, NodeId};
4
5use crate::engine::{
6    LayoutContext, LayoutDirection, LayoutEngine, LayoutEngineId, LayoutError, LayoutNodePosition,
7    LayoutOptions, LayoutRequest, LayoutResult, TIDY_TREE_LAYOUT_ENGINE_ID, position_from_center,
8    resolve_node_origin, resolve_node_size, validate_request,
9};
10use crate::projection::{VisibleLayoutEdge, build_visible_edge_projection, result_from_placements};
11
12/// Built-in tidy tree layout engine.
13#[derive(Debug, Default, Clone, Copy)]
14pub struct TidyTreeLayoutEngine;
15
16impl LayoutEngine for TidyTreeLayoutEngine {
17    fn id(&self) -> LayoutEngineId {
18        LayoutEngineId::new(TIDY_TREE_LAYOUT_ENGINE_ID)
19    }
20
21    fn layout(
22        &self,
23        graph: &Graph,
24        request: &LayoutRequest,
25        context: &LayoutContext,
26    ) -> Result<LayoutResult, LayoutError> {
27        layout_graph_with_tidy_tree_context(graph, request, context)
28    }
29}
30
31/// Runs the native tidy tree engine.
32pub fn layout_graph_with_tidy_tree(
33    graph: &Graph,
34    request: &LayoutRequest,
35) -> Result<LayoutResult, LayoutError> {
36    layout_graph_with_tidy_tree_context(graph, request, &LayoutContext::default())
37}
38
39/// Runs the native tidy tree engine and converts the result into a transaction.
40pub fn layout_graph_to_transaction_with_tidy_tree(
41    graph: &Graph,
42    request: &LayoutRequest,
43) -> Result<GraphTransaction, LayoutError> {
44    layout_graph_with_tidy_tree(graph, request)?.to_transaction(graph)
45}
46
47fn layout_graph_with_tidy_tree_context(
48    graph: &Graph,
49    request: &LayoutRequest,
50    context: &LayoutContext,
51) -> Result<LayoutResult, LayoutError> {
52    validate_request(graph, request)?;
53
54    let mut projection = TidyTreeProjection::new(graph, request, context)?;
55    projection.layout(request.options);
56    projection.into_result()
57}
58
59struct TidyTreeProjection<'a> {
60    graph: &'a Graph,
61    request: &'a LayoutRequest,
62    node_infos: BTreeMap<NodeId, TidyTreeNodeInfo>,
63    visible_edges: Vec<VisibleLayoutEdge>,
64    components: Vec<TidyTreeComponent>,
65    placements: BTreeMap<NodeId, LayoutNodePosition>,
66}
67
68#[derive(Debug, Clone, Copy)]
69struct TidyTreeNodeInfo {
70    size: CanvasSize,
71    origin: (f32, f32),
72}
73
74#[derive(Debug, Clone)]
75struct TidyTreeComponent {
76    root: NodeId,
77    children: BTreeMap<NodeId, Vec<NodeId>>,
78    depth: BTreeMap<NodeId, usize>,
79}
80
81#[derive(Debug, Clone, Copy)]
82struct TreeCoordinates {
83    main: f32,
84    depth: usize,
85}
86
87impl<'a> TidyTreeProjection<'a> {
88    fn new(
89        graph: &'a Graph,
90        request: &'a LayoutRequest,
91        context: &'a LayoutContext,
92    ) -> Result<Self, LayoutError> {
93        let mut node_infos = BTreeMap::new();
94        let mut visible_nodes = BTreeSet::new();
95
96        for (id, node) in &graph.nodes {
97            if node.hidden || !request.scope.contains(*id) {
98                continue;
99            }
100
101            let size = resolve_node_size(graph, request, context, *id)?;
102            let origin = resolve_node_origin(node.origin, request.options.node_origin, context);
103
104            node_infos.insert(*id, TidyTreeNodeInfo { size, origin });
105            visible_nodes.insert(*id);
106        }
107
108        let (outgoing, visible_edges) = build_visible_edge_projection(graph, &visible_nodes)?;
109        let components = build_components(&visible_nodes, &outgoing);
110
111        Ok(Self {
112            graph,
113            request,
114            node_infos,
115            visible_edges,
116            components,
117            placements: BTreeMap::new(),
118        })
119    }
120
121    fn layout(&mut self, options: LayoutOptions) {
122        if self.node_infos.is_empty() {
123            return;
124        }
125
126        let profile = TidyTreeProfile::new(options, &self.node_infos);
127        let components = self.components.clone();
128        let mut component_offset = 0.0;
129
130        for component in &components {
131            let coordinates = compute_component_coordinates(component, &profile);
132            let component_size = component_main_span(&coordinates, &profile);
133
134            for (node, coords) in coordinates {
135                let Some(info) = self.node_infos.get(&node).copied() else {
136                    continue;
137                };
138                let center = center_from_tree_coordinates(
139                    coords,
140                    component_offset,
141                    &profile,
142                    options.direction,
143                );
144                let pos = position_from_center(center, info.size, info.origin);
145
146                self.placements.insert(
147                    node,
148                    LayoutNodePosition {
149                        node,
150                        pos,
151                        center,
152                        size: info.size,
153                    },
154                );
155            }
156
157            component_offset += component_size + profile.component_gap;
158        }
159    }
160
161    fn into_result(mut self) -> Result<LayoutResult, LayoutError> {
162        Ok(result_from_placements(
163            self.graph,
164            self.request.options,
165            &mut self.placements,
166            &self.visible_edges,
167        ))
168    }
169}
170
171struct TidyTreeProfile {
172    max_width: f32,
173    max_height: f32,
174    sibling_gap: f32,
175    layer_gap: f32,
176    component_gap: f32,
177}
178
179impl TidyTreeProfile {
180    fn new(options: LayoutOptions, node_infos: &BTreeMap<NodeId, TidyTreeNodeInfo>) -> Self {
181        let max_width = node_infos
182            .values()
183            .map(|info| info.size.width)
184            .fold(0.0, f32::max);
185        let max_height = node_infos
186            .values()
187            .map(|info| info.size.height)
188            .fold(0.0, f32::max);
189        let sibling_gap = options.spacing.nodesep.max(0.0);
190        let layer_gap = options.spacing.ranksep.max(0.0);
191        let component_gap = (options.spacing.edgesep.max(sibling_gap) + max_width).max(1.0);
192
193        Self {
194            max_width,
195            max_height,
196            sibling_gap,
197            layer_gap,
198            component_gap,
199        }
200    }
201
202    fn sibling_stride(&self) -> f32 {
203        (self.max_width + self.sibling_gap).max(1.0)
204    }
205
206    fn layer_stride(&self) -> f32 {
207        (self.max_height + self.layer_gap).max(1.0)
208    }
209}
210
211fn build_components(
212    visible_nodes: &BTreeSet<NodeId>,
213    outgoing: &BTreeMap<NodeId, Vec<NodeId>>,
214) -> Vec<TidyTreeComponent> {
215    let mut roots = visible_nodes
216        .iter()
217        .copied()
218        .filter(|node| incoming_count(*node, outgoing) == 0)
219        .collect::<Vec<_>>();
220    let root_set = roots.iter().copied().collect::<BTreeSet<_>>();
221    roots.extend(
222        visible_nodes
223            .iter()
224            .copied()
225            .filter(|node| !root_set.contains(node)),
226    );
227
228    let mut visited = BTreeSet::new();
229    let mut components = Vec::new();
230
231    for root in roots {
232        if !visited.insert(root) {
233            continue;
234        }
235
236        let mut children: BTreeMap<NodeId, Vec<NodeId>> = BTreeMap::new();
237        let mut depth: BTreeMap<NodeId, usize> = BTreeMap::new();
238        let mut queue = VecDeque::new();
239
240        queue.push_back(root);
241        depth.insert(root, 0);
242
243        while let Some(node) = queue.pop_front() {
244            let child_depth = depth.get(&node).copied().unwrap_or(0) + 1;
245            let next_nodes = outgoing.get(&node).cloned().unwrap_or_default();
246
247            for child in next_nodes {
248                if !visible_nodes.contains(&child) || !visited.insert(child) {
249                    continue;
250                }
251                children.entry(node).or_default().push(child);
252                depth.insert(child, child_depth);
253                queue.push_back(child);
254            }
255        }
256
257        components.push(TidyTreeComponent {
258            root,
259            children,
260            depth,
261        });
262    }
263
264    components
265}
266
267fn compute_component_coordinates(
268    component: &TidyTreeComponent,
269    profile: &TidyTreeProfile,
270) -> BTreeMap<NodeId, TreeCoordinates> {
271    let mut coordinates = BTreeMap::new();
272    let mut next_leaf = 0.0;
273
274    assign_subtree_main(
275        component.root,
276        component,
277        profile,
278        &mut next_leaf,
279        &mut coordinates,
280    );
281
282    let min_main = coordinates
283        .values()
284        .map(|coords| coords.main)
285        .fold(f32::INFINITY, f32::min);
286    if min_main.is_finite() && min_main != 0.0 {
287        for coords in coordinates.values_mut() {
288            coords.main -= min_main;
289        }
290    }
291
292    coordinates
293}
294
295fn assign_subtree_main(
296    node: NodeId,
297    component: &TidyTreeComponent,
298    profile: &TidyTreeProfile,
299    next_leaf: &mut f32,
300    coordinates: &mut BTreeMap<NodeId, TreeCoordinates>,
301) -> f32 {
302    let depth = component.depth.get(&node).copied().unwrap_or(0);
303    let Some(children) = component.children.get(&node) else {
304        let main = *next_leaf;
305        *next_leaf += profile.sibling_stride();
306        coordinates.insert(node, TreeCoordinates { main, depth });
307        return main;
308    };
309
310    if children.is_empty() {
311        let main = *next_leaf;
312        *next_leaf += profile.sibling_stride();
313        coordinates.insert(node, TreeCoordinates { main, depth });
314        return main;
315    }
316
317    let mut child_mains = Vec::with_capacity(children.len());
318    for child in children {
319        child_mains.push(assign_subtree_main(
320            *child,
321            component,
322            profile,
323            next_leaf,
324            coordinates,
325        ));
326    }
327
328    let first = child_mains.first().copied().unwrap_or(*next_leaf);
329    let last = child_mains.last().copied().unwrap_or(first);
330    let main = (first + last) * 0.5;
331    coordinates.insert(node, TreeCoordinates { main, depth });
332    main
333}
334
335fn component_main_span(
336    coordinates: &BTreeMap<NodeId, TreeCoordinates>,
337    profile: &TidyTreeProfile,
338) -> f32 {
339    let mut min = f32::INFINITY;
340    let mut max = f32::NEG_INFINITY;
341
342    for coords in coordinates.values() {
343        min = min.min(coords.main);
344        max = max.max(coords.main);
345    }
346
347    if min.is_finite() && max.is_finite() {
348        (max - min + profile.max_width).max(profile.max_width)
349    } else {
350        profile.max_width
351    }
352}
353
354fn center_from_tree_coordinates(
355    coords: TreeCoordinates,
356    component_offset: f32,
357    profile: &TidyTreeProfile,
358    direction: LayoutDirection,
359) -> CanvasPoint {
360    let main = component_offset + coords.main + profile.max_width * 0.5;
361    let cross = coords.depth as f32 * profile.layer_stride() + profile.max_height * 0.5;
362
363    match direction {
364        LayoutDirection::TopToBottom => CanvasPoint { x: main, y: cross },
365        LayoutDirection::BottomToTop => CanvasPoint { x: main, y: -cross },
366        LayoutDirection::LeftToRight => CanvasPoint { x: cross, y: main },
367        LayoutDirection::RightToLeft => CanvasPoint { x: -cross, y: main },
368    }
369}
370
371fn incoming_count(node: NodeId, outgoing: &BTreeMap<NodeId, Vec<NodeId>>) -> usize {
372    outgoing
373        .values()
374        .flat_map(|children| children.iter())
375        .filter(|child| **child == node)
376        .count()
377}