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::{CanvasPoint, CanvasSize, Graph, GraphTransaction, NodeId};
5
6use crate::engine::{
7    LayoutContext, LayoutDirection, LayoutEngine, LayoutEngineId, LayoutError, LayoutNodePosition,
8    LayoutOptions, LayoutRequest, LayoutResult, MIND_MAP_RADIAL_LAYOUT_ENGINE_ID,
9    position_from_center, resolve_node_origin, resolve_node_size, validate_request,
10};
11use crate::projection::{
12    VisibleLayoutEdge, build_visible_edge_projection, center_from_position, result_from_placements,
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<VisibleLayoutEdge>,
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 MindMapComponent {
83    root: NodeId,
84    children: BTreeMap<NodeId, Vec<NodeId>>,
85    demand: BTreeMap<NodeId, f32>,
86    max_depth: usize,
87}
88
89struct MindMapLayoutNode<'a> {
90    node: NodeId,
91    center: CanvasPoint,
92    start_angle: f32,
93    end_angle: f32,
94    ring_gap: f32,
95    component: &'a MindMapComponent,
96    depth: usize,
97}
98
99impl<'a> MindMapProjection<'a> {
100    fn new(
101        graph: &'a Graph,
102        request: &'a LayoutRequest,
103        context: &'a LayoutContext,
104    ) -> Result<Self, LayoutError> {
105        let mut node_infos = BTreeMap::new();
106        let mut visible_nodes = BTreeSet::new();
107
108        for (id, node) in &graph.nodes {
109            if node.hidden || !request.scope.contains(*id) {
110                continue;
111            }
112
113            let size = resolve_node_size(graph, request, context, *id)?;
114            let origin = resolve_node_origin(node.origin, request.options.node_origin, context);
115            let current_center = center_from_position(node.pos, size, origin);
116            let arc_demand = node_arc_demand(size, request.options);
117
118            node_infos.insert(
119                *id,
120                MindMapNodeInfo {
121                    size,
122                    origin,
123                    current_center,
124                    arc_demand,
125                    pinned: context.pinned_nodes.contains(id),
126                },
127            );
128            visible_nodes.insert(*id);
129        }
130
131        let (outgoing, visible_edges) = build_visible_edge_projection(graph, &visible_nodes)?;
132        let components = build_components(&visible_nodes, &outgoing, &node_infos);
133
134        Ok(Self {
135            graph,
136            request,
137            context,
138            node_infos,
139            visible_edges,
140            components,
141            placements: BTreeMap::new(),
142        })
143    }
144
145    fn layout(&mut self, request: &LayoutRequest) {
146        if self.node_infos.is_empty() {
147            return;
148        }
149
150        let ring_gap = compute_ring_gap(request, &self.node_infos);
151        let component_radius =
152            compute_component_radius(ring_gap, &self.components, &self.node_infos);
153        let angle_offset = direction_angle_offset(request.options.direction);
154        let components = self.components.clone();
155        let component_count = components.len().max(1) as f32;
156
157        for (index, component) in components.iter().enumerate() {
158            let root_center = if self.context.pinned_nodes.contains(&component.root) {
159                self.node_infos[&component.root].current_center
160            } else if self.components.len() == 1 {
161                CanvasPoint { x: 0.0, y: 0.0 }
162            } else {
163                polar_point(
164                    CanvasPoint { x: 0.0, y: 0.0 },
165                    component_radius,
166                    angle_offset + TAU * (index as f32 / component_count),
167                )
168            };
169
170            self.layout_node(MindMapLayoutNode {
171                node: component.root,
172                center: root_center,
173                start_angle: angle_offset,
174                end_angle: angle_offset + TAU,
175                ring_gap,
176                component,
177                depth: 0,
178            });
179        }
180    }
181
182    fn layout_node(&mut self, frame: MindMapLayoutNode<'_>) {
183        let MindMapLayoutNode {
184            node,
185            center,
186            start_angle,
187            end_angle,
188            ring_gap,
189            component,
190            depth,
191        } = frame;
192        let Some(info) = self.node_infos.get(&node).copied() else {
193            return;
194        };
195        let final_center = if info.pinned {
196            info.current_center
197        } else {
198            center
199        };
200        let pos = position_from_center(final_center, info.size, info.origin);
201
202        self.placements.insert(
203            node,
204            LayoutNodePosition {
205                node,
206                pos,
207                center: final_center,
208                size: info.size,
209            },
210        );
211
212        let Some(children) = component.children.get(&node) else {
213            return;
214        };
215        if children.is_empty() {
216            return;
217        }
218
219        let total_demand = children
220            .iter()
221            .map(|child| component.demand.get(child).copied().unwrap_or(1.0))
222            .sum::<f32>()
223            .max(1.0);
224        let sector_span = end_angle - start_angle;
225        let mut cursor = start_angle;
226
227        for child in children {
228            let child_demand = component.demand.get(child).copied().unwrap_or(1.0);
229            let child_span = sector_span * (child_demand / total_demand);
230            let child_angle = cursor + child_span * 0.5;
231            let child_center =
232                polar_point(final_center, ring_gap * (depth as f32 + 1.0), child_angle);
233            self.layout_node(MindMapLayoutNode {
234                node: *child,
235                center: child_center,
236                start_angle: cursor,
237                end_angle: cursor + child_span,
238                ring_gap,
239                component,
240                depth: depth + 1,
241            });
242            cursor += child_span;
243        }
244    }
245
246    fn into_result(mut self) -> Result<LayoutResult, LayoutError> {
247        Ok(result_from_placements(
248            self.graph,
249            self.request.options,
250            &mut self.placements,
251            &self.visible_edges,
252        ))
253    }
254}
255
256fn build_components(
257    visible_nodes: &BTreeSet<NodeId>,
258    outgoing: &BTreeMap<NodeId, Vec<NodeId>>,
259    node_infos: &BTreeMap<NodeId, MindMapNodeInfo>,
260) -> Vec<MindMapComponent> {
261    let mut roots = visible_nodes
262        .iter()
263        .copied()
264        .filter(|node| incoming_count(*node, outgoing) == 0)
265        .collect::<Vec<_>>();
266    let root_set = roots.iter().copied().collect::<BTreeSet<_>>();
267    roots.extend(
268        visible_nodes
269            .iter()
270            .copied()
271            .filter(|node| !root_set.contains(node)),
272    );
273
274    let mut visited = BTreeSet::new();
275    let mut components = Vec::new();
276
277    for root in roots {
278        if !visited.insert(root) {
279            continue;
280        }
281
282        let mut children: BTreeMap<NodeId, Vec<NodeId>> = BTreeMap::new();
283        let mut depth: BTreeMap<NodeId, usize> = BTreeMap::new();
284        let mut queue = VecDeque::new();
285
286        queue.push_back(root);
287        depth.insert(root, 0);
288
289        while let Some(node) = queue.pop_front() {
290            let child_depth = depth.get(&node).copied().unwrap_or(0) + 1;
291            let next_nodes = outgoing.get(&node).cloned().unwrap_or_default();
292
293            for child in next_nodes {
294                if !visible_nodes.contains(&child) || !visited.insert(child) {
295                    continue;
296                }
297                children.entry(node).or_default().push(child);
298                depth.insert(child, child_depth);
299                queue.push_back(child);
300            }
301        }
302
303        let mut demand = BTreeMap::new();
304        let max_depth = depth.values().copied().max().unwrap_or(0);
305        compute_subtree_demand(root, &children, node_infos, &mut demand);
306
307        components.push(MindMapComponent {
308            root,
309            children,
310            demand,
311            max_depth,
312        });
313    }
314
315    components
316}
317
318fn compute_subtree_demand(
319    node: NodeId,
320    children: &BTreeMap<NodeId, Vec<NodeId>>,
321    node_infos: &BTreeMap<NodeId, MindMapNodeInfo>,
322    memo: &mut BTreeMap<NodeId, f32>,
323) -> f32 {
324    if let Some(value) = memo.get(&node) {
325        return *value;
326    }
327
328    let mut total = node_infos
329        .get(&node)
330        .map(|info| info.arc_demand)
331        .unwrap_or(1.0);
332    if let Some(next) = children.get(&node) {
333        for child in next {
334            total += compute_subtree_demand(*child, children, node_infos, memo);
335        }
336    }
337
338    memo.insert(node, total);
339    total
340}
341
342fn incoming_count(node: NodeId, outgoing: &BTreeMap<NodeId, Vec<NodeId>>) -> usize {
343    outgoing
344        .values()
345        .flat_map(|children| children.iter())
346        .filter(|child| **child == node)
347        .count()
348}
349
350fn compute_ring_gap(
351    request: &LayoutRequest,
352    node_infos: &BTreeMap<NodeId, MindMapNodeInfo>,
353) -> f32 {
354    let widest = node_infos
355        .values()
356        .map(|info| info.size.width.max(info.size.height))
357        .fold(0.0, f32::max);
358    let spacing = request.options.spacing;
359    (widest * 1.25 + spacing.ranksep.max(spacing.nodesep.max(24.0))).max(80.0)
360}
361
362fn compute_component_radius(
363    ring_gap: f32,
364    components: &[MindMapComponent],
365    node_infos: &BTreeMap<NodeId, MindMapNodeInfo>,
366) -> f32 {
367    if components.len() <= 1 {
368        return 0.0;
369    }
370
371    let max_depth = components
372        .iter()
373        .map(|component| component.max_depth)
374        .max()
375        .unwrap_or(0);
376    let widest = node_infos
377        .values()
378        .map(|info| info.size.width.max(info.size.height))
379        .fold(0.0, f32::max);
380
381    ring_gap * (max_depth as f32 + 2.0) * 2.0 + widest.max(ring_gap)
382}
383
384fn node_arc_demand(size: CanvasSize, options: LayoutOptions) -> f32 {
385    let baseline = options.spacing.nodesep.max(24.0);
386    let arc = size.width.max(size.height) + baseline;
387    (arc / options.spacing.ranksep.max(1.0)).max(1.0)
388}
389
390fn direction_angle_offset(direction: LayoutDirection) -> f32 {
391    match direction {
392        LayoutDirection::TopToBottom => -PI / 2.0,
393        LayoutDirection::BottomToTop => PI / 2.0,
394        LayoutDirection::LeftToRight => 0.0,
395        LayoutDirection::RightToLeft => PI,
396    }
397}
398
399fn polar_point(center: CanvasPoint, radius: f32, angle: f32) -> CanvasPoint {
400    CanvasPoint {
401        x: center.x + radius * angle.cos(),
402        y: center.y + radius * angle.sin(),
403    }
404}