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#[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<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}