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#[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
31pub 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
39pub 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}