Skip to main content

jellyflow_layout/
freeform.rs

1use std::cmp::Ordering;
2use std::collections::{BTreeMap, BTreeSet};
3
4use jellyflow_core::{
5    CanvasPoint, CanvasRect, CanvasSize, EdgeId, Graph, GraphTransaction, NodeId,
6};
7
8use crate::engine::{
9    LayoutContext, LayoutDirection, LayoutEdgeRoute, LayoutEngine, LayoutEngineId, LayoutError,
10    LayoutNodePosition, LayoutOptions, LayoutRequest, LayoutResult,
11    MIND_MAP_FREEFORM_LAYOUT_ENGINE_ID, node_rect_from_position, position_from_center,
12    resolve_node_origin, resolve_node_size, union_bounds, validate_request,
13};
14
15/// Built-in freeform mind-map engine that only resolves overlaps.
16#[derive(Debug, Default, Clone, Copy)]
17pub struct MindMapFreeformLayoutEngine;
18
19impl LayoutEngine for MindMapFreeformLayoutEngine {
20    fn id(&self) -> LayoutEngineId {
21        LayoutEngineId::new(MIND_MAP_FREEFORM_LAYOUT_ENGINE_ID)
22    }
23
24    fn layout(
25        &self,
26        graph: &Graph,
27        request: &LayoutRequest,
28        context: &LayoutContext,
29    ) -> Result<LayoutResult, LayoutError> {
30        layout_graph_with_mind_map_freeform_context(graph, request, context)
31    }
32}
33
34/// Runs the native freeform mind-map engine.
35pub fn layout_graph_with_mind_map_freeform(
36    graph: &Graph,
37    request: &LayoutRequest,
38) -> Result<LayoutResult, LayoutError> {
39    layout_graph_with_mind_map_freeform_context(graph, request, &LayoutContext::default())
40}
41
42/// Runs the native freeform mind-map engine and converts the result into a transaction.
43pub fn layout_graph_to_transaction_with_mind_map_freeform(
44    graph: &Graph,
45    request: &LayoutRequest,
46) -> Result<GraphTransaction, LayoutError> {
47    layout_graph_with_mind_map_freeform(graph, request)?.to_transaction(graph)
48}
49
50fn layout_graph_with_mind_map_freeform_context(
51    graph: &Graph,
52    request: &LayoutRequest,
53    context: &LayoutContext,
54) -> Result<LayoutResult, LayoutError> {
55    validate_request(graph, request)?;
56
57    let mut projection = FreeformProjection::new(graph, request, context)?;
58    projection.layout(request.options);
59    projection.into_result()
60}
61
62struct FreeformProjection<'a> {
63    graph: &'a Graph,
64    request: &'a LayoutRequest,
65    node_infos: BTreeMap<NodeId, FreeformNodeInfo>,
66    visible_edges: Vec<FreeformEdge>,
67    placements: BTreeMap<NodeId, LayoutNodePosition>,
68}
69
70#[derive(Debug, Clone, Copy)]
71struct FreeformNodeInfo {
72    size: CanvasSize,
73    origin: (f32, f32),
74    current_center: CanvasPoint,
75    pinned: bool,
76}
77
78#[derive(Debug, Clone)]
79struct FreeformEdge {
80    id: EdgeId,
81    source: NodeId,
82    target: NodeId,
83}
84
85impl<'a> FreeformProjection<'a> {
86    fn new(
87        graph: &'a Graph,
88        request: &'a LayoutRequest,
89        context: &'a LayoutContext,
90    ) -> Result<Self, LayoutError> {
91        let mut node_infos = BTreeMap::new();
92        let mut visible_nodes = BTreeSet::new();
93
94        for (id, node) in &graph.nodes {
95            if node.hidden || !request.scope.contains(*id) {
96                continue;
97            }
98
99            let size = resolve_node_size(graph, request, context, *id)?;
100            let origin = resolve_node_origin(node.origin, request.options.node_origin, context);
101            let current_center = center_from_position(node.pos, size, origin);
102
103            node_infos.insert(
104                *id,
105                FreeformNodeInfo {
106                    size,
107                    origin,
108                    current_center,
109                    pinned: context.pinned_nodes.contains(id),
110                },
111            );
112            visible_nodes.insert(*id);
113        }
114
115        let visible_edges = build_visible_edges(graph, &visible_nodes)?;
116
117        Ok(Self {
118            graph,
119            request,
120            node_infos,
121            visible_edges,
122            placements: BTreeMap::new(),
123        })
124    }
125
126    fn layout(&mut self, options: LayoutOptions) {
127        if self.node_infos.is_empty() {
128            return;
129        }
130
131        let gap = freeform_gap(options);
132        let mut node_order = self.node_infos.keys().copied().collect::<Vec<_>>();
133        node_order.sort_by(|left, right| self.compare_nodes(*left, *right, options.direction));
134
135        for node in node_order {
136            let Some(info) = self.node_infos.get(&node).copied() else {
137                continue;
138            };
139
140            let center = if info.pinned {
141                info.current_center
142            } else {
143                self.resolve_node_center(info, gap, options.direction)
144            };
145            let pos = position_from_center(center, info.size, info.origin);
146
147            self.placements.insert(
148                node,
149                LayoutNodePosition {
150                    node,
151                    pos,
152                    center,
153                    size: info.size,
154                },
155            );
156        }
157    }
158
159    fn compare_nodes(&self, left: NodeId, right: NodeId, direction: LayoutDirection) -> Ordering {
160        let left_info = self.node_infos.get(&left).expect("left node info");
161        let right_info = self.node_infos.get(&right).expect("right node info");
162
163        right_info
164            .pinned
165            .cmp(&left_info.pinned)
166            .then_with(|| match direction {
167                LayoutDirection::TopToBottom | LayoutDirection::BottomToTop => left_info
168                    .current_center
169                    .y
170                    .total_cmp(&right_info.current_center.y)
171                    .then_with(|| {
172                        left_info
173                            .current_center
174                            .x
175                            .total_cmp(&right_info.current_center.x)
176                    }),
177                LayoutDirection::LeftToRight | LayoutDirection::RightToLeft => left_info
178                    .current_center
179                    .x
180                    .total_cmp(&right_info.current_center.x)
181                    .then_with(|| {
182                        left_info
183                            .current_center
184                            .y
185                            .total_cmp(&right_info.current_center.y)
186                    }),
187            })
188            .then_with(|| left.cmp(&right))
189    }
190
191    fn resolve_node_center(
192        &self,
193        info: FreeformNodeInfo,
194        gap: f32,
195        direction: LayoutDirection,
196    ) -> CanvasPoint {
197        let mut center = info.current_center;
198
199        for _ in 0..64 {
200            let rect = rect_from_center(center, info.size, info.origin);
201            let Some(other) = self
202                .placements
203                .values()
204                .map(node_rect_from_position)
205                .find(|other| rects_overlap_with_gap(rect, *other, gap))
206            else {
207                break;
208            };
209
210            center =
211                shift_center_out_of_rect(center, info.size, info.origin, other, direction, gap);
212        }
213
214        center
215    }
216
217    fn into_result(mut self) -> Result<LayoutResult, LayoutError> {
218        if self.placements.is_empty() {
219            return Ok(LayoutResult {
220                nodes: Vec::new(),
221                edge_routes: Vec::new(),
222                bounds: None,
223            });
224        }
225
226        let mut bounds = None;
227        for node in self.placements.values() {
228            bounds = union_bounds(bounds, node_rect_from_position(node));
229        }
230
231        let shift = bounds.map_or(
232            CanvasPoint {
233                x: self.request.options.margin.width,
234                y: self.request.options.margin.height,
235            },
236            |bounds| CanvasPoint {
237                x: self.request.options.margin.width - bounds.origin.x,
238                y: self.request.options.margin.height - bounds.origin.y,
239            },
240        );
241
242        if shift.x != 0.0 || shift.y != 0.0 {
243            for node in self.placements.values_mut() {
244                node.pos.x += shift.x;
245                node.pos.y += shift.y;
246                node.center.x += shift.x;
247                node.center.y += shift.y;
248            }
249            bounds = bounds.map(|bounds| CanvasRect {
250                origin: CanvasPoint {
251                    x: bounds.origin.x + shift.x,
252                    y: bounds.origin.y + shift.y,
253                },
254                size: bounds.size,
255            });
256        }
257
258        let nodes = self
259            .graph
260            .nodes
261            .keys()
262            .filter_map(|node| self.placements.get(node).copied())
263            .collect::<Vec<_>>();
264
265        let edge_routes = self
266            .visible_edges
267            .iter()
268            .filter_map(|edge| {
269                let source = self.placements.get(&edge.source)?;
270                let target = self.placements.get(&edge.target)?;
271                Some(LayoutEdgeRoute {
272                    edge: edge.id,
273                    points: vec![source.center, target.center],
274                })
275            })
276            .collect::<Vec<_>>();
277
278        Ok(LayoutResult {
279            nodes,
280            edge_routes,
281            bounds,
282        })
283    }
284}
285
286fn build_visible_edges(
287    graph: &Graph,
288    visible_nodes: &BTreeSet<NodeId>,
289) -> Result<Vec<FreeformEdge>, LayoutError> {
290    let mut visible_edges = Vec::new();
291
292    for (edge_id, edge) in &graph.edges {
293        if edge.hidden {
294            continue;
295        }
296
297        let source_port = graph
298            .ports
299            .get(&edge.from)
300            .ok_or(LayoutError::MissingSourcePort(*edge_id))?;
301        let target_port = graph
302            .ports
303            .get(&edge.to)
304            .ok_or(LayoutError::MissingTargetPort(*edge_id))?;
305        if !graph.nodes.contains_key(&source_port.node) {
306            return Err(LayoutError::MissingSourceNode { edge: *edge_id });
307        }
308        if !graph.nodes.contains_key(&target_port.node) {
309            return Err(LayoutError::MissingTargetNode { edge: *edge_id });
310        }
311        if !visible_nodes.contains(&source_port.node) || !visible_nodes.contains(&target_port.node)
312        {
313            continue;
314        }
315
316        visible_edges.push(FreeformEdge {
317            id: *edge_id,
318            source: source_port.node,
319            target: target_port.node,
320        });
321    }
322
323    Ok(visible_edges)
324}
325
326fn freeform_gap(options: LayoutOptions) -> f32 {
327    options.spacing.nodesep.max(24.0)
328}
329
330fn center_from_position(pos: CanvasPoint, size: CanvasSize, origin: (f32, f32)) -> CanvasPoint {
331    CanvasPoint {
332        x: pos.x + size.width * (0.5 - origin.0),
333        y: pos.y + size.height * (0.5 - origin.1),
334    }
335}
336
337fn rect_from_center(center: CanvasPoint, size: CanvasSize, origin: (f32, f32)) -> CanvasRect {
338    CanvasRect {
339        origin: position_from_center(center, size, origin),
340        size,
341    }
342}
343
344fn shift_center_out_of_rect(
345    center: CanvasPoint,
346    size: CanvasSize,
347    origin: (f32, f32),
348    other: CanvasRect,
349    direction: LayoutDirection,
350    gap: f32,
351) -> CanvasPoint {
352    let rect = rect_from_center(center, size, origin);
353    let expanded = expand_rect(other, gap);
354
355    match direction {
356        LayoutDirection::TopToBottom => CanvasPoint {
357            x: center.x,
358            y: center.y + (expanded.origin.y + expanded.size.height - rect.origin.y),
359        },
360        LayoutDirection::BottomToTop => CanvasPoint {
361            x: center.x,
362            y: center.y - (rect.origin.y + rect.size.height - expanded.origin.y),
363        },
364        LayoutDirection::LeftToRight => CanvasPoint {
365            x: center.x + (expanded.origin.x + expanded.size.width - rect.origin.x),
366            y: center.y,
367        },
368        LayoutDirection::RightToLeft => CanvasPoint {
369            x: center.x - (rect.origin.x + rect.size.width - expanded.origin.x),
370            y: center.y,
371        },
372    }
373}
374
375fn rects_overlap_with_gap(candidate: CanvasRect, other: CanvasRect, gap: f32) -> bool {
376    let other = expand_rect(other, gap);
377    let candidate_right = candidate.origin.x + candidate.size.width;
378    let candidate_bottom = candidate.origin.y + candidate.size.height;
379    let other_right = other.origin.x + other.size.width;
380    let other_bottom = other.origin.y + other.size.height;
381
382    candidate.origin.x < other_right
383        && candidate_right > other.origin.x
384        && candidate.origin.y < other_bottom
385        && candidate_bottom > other.origin.y
386}
387
388fn expand_rect(rect: CanvasRect, gap: f32) -> CanvasRect {
389    CanvasRect {
390        origin: CanvasPoint {
391            x: rect.origin.x - gap,
392            y: rect.origin.y - gap,
393        },
394        size: CanvasSize {
395            width: rect.size.width + gap * 2.0,
396            height: rect.size.height + gap * 2.0,
397        },
398    }
399}