Skip to main content

jellyflow_layout/
mind_map.rs

1use std::collections::{BTreeMap, BTreeSet, VecDeque};
2use std::f32::consts::{PI, TAU};
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_RADIAL_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 radial mind-map engine.
16#[derive(Debug, Default, Clone, Copy)]
17pub struct MindMapRadialLayoutEngine;
18
19impl LayoutEngine for MindMapRadialLayoutEngine {
20    fn id(&self) -> LayoutEngineId {
21        LayoutEngineId::new(MIND_MAP_RADIAL_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_radial_context(graph, request, context)
31    }
32}
33
34/// Runs the native radial mind-map engine.
35pub fn layout_graph_with_mind_map_radial(
36    graph: &Graph,
37    request: &LayoutRequest,
38) -> Result<LayoutResult, LayoutError> {
39    layout_graph_with_mind_map_radial_context(graph, request, &LayoutContext::default())
40}
41
42/// Runs the native radial mind-map engine and converts the result into a transaction.
43pub fn layout_graph_to_transaction_with_mind_map_radial(
44    graph: &Graph,
45    request: &LayoutRequest,
46) -> Result<GraphTransaction, LayoutError> {
47    layout_graph_with_mind_map_radial(graph, request)?.to_transaction(graph)
48}
49
50fn layout_graph_with_mind_map_radial_context(
51    graph: &Graph,
52    request: &LayoutRequest,
53    context: &LayoutContext,
54) -> Result<LayoutResult, LayoutError> {
55    validate_request(graph, request)?;
56
57    let mut projection = MindMapProjection::new(graph, request, context)?;
58    projection.layout(request);
59    projection.into_result()
60}
61
62struct MindMapProjection<'a> {
63    graph: &'a Graph,
64    request: &'a LayoutRequest,
65    context: &'a LayoutContext,
66    node_infos: BTreeMap<NodeId, MindMapNodeInfo>,
67    visible_edges: Vec<MindMapEdge>,
68    components: Vec<MindMapComponent>,
69    placements: BTreeMap<NodeId, LayoutNodePosition>,
70}
71
72#[derive(Debug, Clone, Copy)]
73struct MindMapNodeInfo {
74    size: CanvasSize,
75    origin: (f32, f32),
76    current_center: CanvasPoint,
77    arc_demand: f32,
78    pinned: bool,
79}
80
81#[derive(Debug, Clone)]
82struct MindMapEdge {
83    id: EdgeId,
84    source: NodeId,
85    target: NodeId,
86}
87
88#[derive(Debug, Clone)]
89struct MindMapComponent {
90    root: NodeId,
91    children: BTreeMap<NodeId, Vec<NodeId>>,
92    demand: BTreeMap<NodeId, f32>,
93    max_depth: usize,
94}
95
96struct MindMapLayoutNode<'a> {
97    node: NodeId,
98    center: CanvasPoint,
99    start_angle: f32,
100    end_angle: f32,
101    ring_gap: f32,
102    component: &'a MindMapComponent,
103    depth: usize,
104}
105
106type VisibleEdgeProjection = (BTreeMap<NodeId, Vec<NodeId>>, Vec<MindMapEdge>);
107
108impl<'a> MindMapProjection<'a> {
109    fn new(
110        graph: &'a Graph,
111        request: &'a LayoutRequest,
112        context: &'a LayoutContext,
113    ) -> Result<Self, LayoutError> {
114        let mut node_infos = BTreeMap::new();
115        let mut visible_nodes = BTreeSet::new();
116
117        for (id, node) in &graph.nodes {
118            if node.hidden || !request.scope.contains(*id) {
119                continue;
120            }
121
122            let size = resolve_node_size(graph, request, context, *id)?;
123            let origin = resolve_node_origin(node.origin, request.options.node_origin, context);
124            let current_center = center_from_position(node.pos, size, origin);
125            let arc_demand = node_arc_demand(size, request.options);
126
127            node_infos.insert(
128                *id,
129                MindMapNodeInfo {
130                    size,
131                    origin,
132                    current_center,
133                    arc_demand,
134                    pinned: context.pinned_nodes.contains(id),
135                },
136            );
137            visible_nodes.insert(*id);
138        }
139
140        let (outgoing, visible_edges) = build_visible_edges(graph, &visible_nodes)?;
141        let components = build_components(&visible_nodes, &outgoing, &node_infos);
142
143        Ok(Self {
144            graph,
145            request,
146            context,
147            node_infos,
148            visible_edges,
149            components,
150            placements: BTreeMap::new(),
151        })
152    }
153
154    fn layout(&mut self, request: &LayoutRequest) {
155        if self.node_infos.is_empty() {
156            return;
157        }
158
159        let ring_gap = compute_ring_gap(request, &self.node_infos);
160        let component_radius =
161            compute_component_radius(ring_gap, &self.components, &self.node_infos);
162        let angle_offset = direction_angle_offset(request.options.direction);
163        let components = self.components.clone();
164        let component_count = components.len().max(1) as f32;
165
166        for (index, component) in components.iter().enumerate() {
167            let root_center = if self.context.pinned_nodes.contains(&component.root) {
168                self.node_infos[&component.root].current_center
169            } else if self.components.len() == 1 {
170                CanvasPoint { x: 0.0, y: 0.0 }
171            } else {
172                polar_point(
173                    CanvasPoint { x: 0.0, y: 0.0 },
174                    component_radius,
175                    angle_offset + TAU * (index as f32 / component_count),
176                )
177            };
178
179            self.layout_node(MindMapLayoutNode {
180                node: component.root,
181                center: root_center,
182                start_angle: angle_offset,
183                end_angle: angle_offset + TAU,
184                ring_gap,
185                component,
186                depth: 0,
187            });
188        }
189    }
190
191    fn layout_node(&mut self, frame: MindMapLayoutNode<'_>) {
192        let MindMapLayoutNode {
193            node,
194            center,
195            start_angle,
196            end_angle,
197            ring_gap,
198            component,
199            depth,
200        } = frame;
201        let Some(info) = self.node_infos.get(&node).copied() else {
202            return;
203        };
204        let final_center = if info.pinned {
205            info.current_center
206        } else {
207            center
208        };
209        let pos = position_from_center(final_center, info.size, info.origin);
210
211        self.placements.insert(
212            node,
213            LayoutNodePosition {
214                node,
215                pos,
216                center: final_center,
217                size: info.size,
218            },
219        );
220
221        let Some(children) = component.children.get(&node) else {
222            return;
223        };
224        if children.is_empty() {
225            return;
226        }
227
228        let total_demand = children
229            .iter()
230            .map(|child| component.demand.get(child).copied().unwrap_or(1.0))
231            .sum::<f32>()
232            .max(1.0);
233        let sector_span = end_angle - start_angle;
234        let mut cursor = start_angle;
235
236        for child in children {
237            let child_demand = component.demand.get(child).copied().unwrap_or(1.0);
238            let child_span = sector_span * (child_demand / total_demand);
239            let child_angle = cursor + child_span * 0.5;
240            let child_center =
241                polar_point(final_center, ring_gap * (depth as f32 + 1.0), child_angle);
242            self.layout_node(MindMapLayoutNode {
243                node: *child,
244                center: child_center,
245                start_angle: cursor,
246                end_angle: cursor + child_span,
247                ring_gap,
248                component,
249                depth: depth + 1,
250            });
251            cursor += child_span;
252        }
253    }
254
255    fn into_result(mut self) -> Result<LayoutResult, LayoutError> {
256        if self.placements.is_empty() {
257            return Ok(LayoutResult {
258                nodes: Vec::new(),
259                edge_routes: Vec::new(),
260                bounds: None,
261            });
262        }
263
264        let mut bounds = None;
265        for node in self.placements.values() {
266            bounds = union_bounds(bounds, node_rect_from_position(node));
267        }
268
269        let shift = bounds.map_or(
270            CanvasPoint {
271                x: self.request.options.margin.width,
272                y: self.request.options.margin.height,
273            },
274            |bounds| CanvasPoint {
275                x: self.request.options.margin.width - bounds.origin.x,
276                y: self.request.options.margin.height - bounds.origin.y,
277            },
278        );
279
280        if shift.x != 0.0 || shift.y != 0.0 {
281            for node in self.placements.values_mut() {
282                node.pos.x += shift.x;
283                node.pos.y += shift.y;
284                node.center.x += shift.x;
285                node.center.y += shift.y;
286            }
287            bounds = bounds.map(|bounds| CanvasRect {
288                origin: CanvasPoint {
289                    x: bounds.origin.x + shift.x,
290                    y: bounds.origin.y + shift.y,
291                },
292                size: bounds.size,
293            });
294        }
295
296        let nodes = self
297            .graph
298            .nodes
299            .keys()
300            .filter_map(|node| self.placements.get(node).copied())
301            .collect::<Vec<_>>();
302
303        let edge_routes = self
304            .visible_edges
305            .iter()
306            .filter_map(|edge| {
307                let source = self.placements.get(&edge.source)?;
308                let target = self.placements.get(&edge.target)?;
309                Some(LayoutEdgeRoute {
310                    edge: edge.id,
311                    points: vec![source.center, target.center],
312                })
313            })
314            .collect::<Vec<_>>();
315
316        Ok(LayoutResult {
317            nodes,
318            edge_routes,
319            bounds,
320        })
321    }
322}
323
324fn build_visible_edges(
325    graph: &Graph,
326    visible_nodes: &BTreeSet<NodeId>,
327) -> Result<VisibleEdgeProjection, LayoutError> {
328    let mut outgoing: BTreeMap<NodeId, Vec<NodeId>> = BTreeMap::new();
329    let mut visible_edges = Vec::new();
330
331    for (edge_id, edge) in &graph.edges {
332        if edge.hidden {
333            continue;
334        }
335
336        let source_port = graph
337            .ports
338            .get(&edge.from)
339            .ok_or(LayoutError::MissingSourcePort(*edge_id))?;
340        let target_port = graph
341            .ports
342            .get(&edge.to)
343            .ok_or(LayoutError::MissingTargetPort(*edge_id))?;
344        if !graph.nodes.contains_key(&source_port.node) {
345            return Err(LayoutError::MissingSourceNode { edge: *edge_id });
346        }
347        if !graph.nodes.contains_key(&target_port.node) {
348            return Err(LayoutError::MissingTargetNode { edge: *edge_id });
349        }
350        if !visible_nodes.contains(&source_port.node) || !visible_nodes.contains(&target_port.node)
351        {
352            continue;
353        }
354
355        outgoing
356            .entry(source_port.node)
357            .or_default()
358            .push(target_port.node);
359        visible_edges.push(MindMapEdge {
360            id: *edge_id,
361            source: source_port.node,
362            target: target_port.node,
363        });
364    }
365
366    for children in outgoing.values_mut() {
367        children.sort();
368        children.dedup();
369    }
370    Ok((outgoing, visible_edges))
371}
372
373fn build_components(
374    visible_nodes: &BTreeSet<NodeId>,
375    outgoing: &BTreeMap<NodeId, Vec<NodeId>>,
376    node_infos: &BTreeMap<NodeId, MindMapNodeInfo>,
377) -> Vec<MindMapComponent> {
378    let mut roots = visible_nodes
379        .iter()
380        .copied()
381        .filter(|node| incoming_count(*node, outgoing) == 0)
382        .collect::<Vec<_>>();
383    let root_set = roots.iter().copied().collect::<BTreeSet<_>>();
384    roots.extend(
385        visible_nodes
386            .iter()
387            .copied()
388            .filter(|node| !root_set.contains(node)),
389    );
390
391    let mut visited = BTreeSet::new();
392    let mut components = Vec::new();
393
394    for root in roots {
395        if !visited.insert(root) {
396            continue;
397        }
398
399        let mut children: BTreeMap<NodeId, Vec<NodeId>> = BTreeMap::new();
400        let mut depth: BTreeMap<NodeId, usize> = BTreeMap::new();
401        let mut queue = VecDeque::new();
402
403        queue.push_back(root);
404        depth.insert(root, 0);
405
406        while let Some(node) = queue.pop_front() {
407            let child_depth = depth.get(&node).copied().unwrap_or(0) + 1;
408            let next_nodes = outgoing.get(&node).cloned().unwrap_or_default();
409
410            for child in next_nodes {
411                if !visible_nodes.contains(&child) || !visited.insert(child) {
412                    continue;
413                }
414                children.entry(node).or_default().push(child);
415                depth.insert(child, child_depth);
416                queue.push_back(child);
417            }
418        }
419
420        let mut demand = BTreeMap::new();
421        let max_depth = depth.values().copied().max().unwrap_or(0);
422        compute_subtree_demand(root, &children, node_infos, &mut demand);
423
424        components.push(MindMapComponent {
425            root,
426            children,
427            demand,
428            max_depth,
429        });
430    }
431
432    components
433}
434
435fn compute_subtree_demand(
436    node: NodeId,
437    children: &BTreeMap<NodeId, Vec<NodeId>>,
438    node_infos: &BTreeMap<NodeId, MindMapNodeInfo>,
439    memo: &mut BTreeMap<NodeId, f32>,
440) -> f32 {
441    if let Some(value) = memo.get(&node) {
442        return *value;
443    }
444
445    let mut total = node_infos
446        .get(&node)
447        .map(|info| info.arc_demand)
448        .unwrap_or(1.0);
449    if let Some(next) = children.get(&node) {
450        for child in next {
451            total += compute_subtree_demand(*child, children, node_infos, memo);
452        }
453    }
454
455    memo.insert(node, total);
456    total
457}
458
459fn incoming_count(node: NodeId, outgoing: &BTreeMap<NodeId, Vec<NodeId>>) -> usize {
460    outgoing
461        .values()
462        .flat_map(|children| children.iter())
463        .filter(|child| **child == node)
464        .count()
465}
466
467fn compute_ring_gap(
468    request: &LayoutRequest,
469    node_infos: &BTreeMap<NodeId, MindMapNodeInfo>,
470) -> f32 {
471    let widest = node_infos
472        .values()
473        .map(|info| info.size.width.max(info.size.height))
474        .fold(0.0, f32::max);
475    let spacing = request.options.spacing;
476    (widest * 1.25 + spacing.ranksep.max(spacing.nodesep.max(24.0))).max(80.0)
477}
478
479fn compute_component_radius(
480    ring_gap: f32,
481    components: &[MindMapComponent],
482    node_infos: &BTreeMap<NodeId, MindMapNodeInfo>,
483) -> f32 {
484    if components.len() <= 1 {
485        return 0.0;
486    }
487
488    let max_depth = components
489        .iter()
490        .map(|component| component.max_depth)
491        .max()
492        .unwrap_or(0);
493    let widest = node_infos
494        .values()
495        .map(|info| info.size.width.max(info.size.height))
496        .fold(0.0, f32::max);
497
498    ring_gap * (max_depth as f32 + 2.0) * 2.0 + widest.max(ring_gap)
499}
500
501fn node_arc_demand(size: CanvasSize, options: LayoutOptions) -> f32 {
502    let baseline = options.spacing.nodesep.max(24.0);
503    let arc = size.width.max(size.height) + baseline;
504    (arc / options.spacing.ranksep.max(1.0)).max(1.0)
505}
506
507fn direction_angle_offset(direction: LayoutDirection) -> f32 {
508    match direction {
509        LayoutDirection::TopToBottom => -PI / 2.0,
510        LayoutDirection::BottomToTop => PI / 2.0,
511        LayoutDirection::LeftToRight => 0.0,
512        LayoutDirection::RightToLeft => PI,
513    }
514}
515
516fn polar_point(center: CanvasPoint, radius: f32, angle: f32) -> CanvasPoint {
517    CanvasPoint {
518        x: center.x + radius * angle.cos(),
519        y: center.y + radius * angle.sin(),
520    }
521}
522
523fn center_from_position(pos: CanvasPoint, size: CanvasSize, origin: (f32, f32)) -> CanvasPoint {
524    CanvasPoint {
525        x: pos.x + size.width * (0.5 - origin.0),
526        y: pos.y + size.height * (0.5 - origin.1),
527    }
528}