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#[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
34pub 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
42pub 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}