1use std::cmp::Ordering;
2use std::collections::{BTreeMap, BTreeSet};
3
4use jellyflow_core::{CanvasPoint, CanvasRect, CanvasSize, Graph, GraphTransaction, NodeId};
5
6use crate::engine::{
7 LayoutContext, LayoutDirection, LayoutEngine, LayoutEngineId, LayoutError, LayoutNodePosition,
8 LayoutOptions, LayoutRequest, LayoutResult, MIND_MAP_FREEFORM_LAYOUT_ENGINE_ID,
9 node_rect_from_position, position_from_center, resolve_node_origin, resolve_node_size,
10 validate_request,
11};
12use crate::projection::{
13 VisibleLayoutEdge, build_visible_edge_list, center_from_position, result_from_placements,
14};
15
16#[derive(Debug, Default, Clone, Copy)]
18pub struct MindMapFreeformLayoutEngine;
19
20impl LayoutEngine for MindMapFreeformLayoutEngine {
21 fn id(&self) -> LayoutEngineId {
22 LayoutEngineId::new(MIND_MAP_FREEFORM_LAYOUT_ENGINE_ID)
23 }
24
25 fn layout(
26 &self,
27 graph: &Graph,
28 request: &LayoutRequest,
29 context: &LayoutContext,
30 ) -> Result<LayoutResult, LayoutError> {
31 layout_graph_with_mind_map_freeform_context(graph, request, context)
32 }
33}
34
35pub fn layout_graph_with_mind_map_freeform(
37 graph: &Graph,
38 request: &LayoutRequest,
39) -> Result<LayoutResult, LayoutError> {
40 layout_graph_with_mind_map_freeform_context(graph, request, &LayoutContext::default())
41}
42
43pub fn layout_graph_to_transaction_with_mind_map_freeform(
45 graph: &Graph,
46 request: &LayoutRequest,
47) -> Result<GraphTransaction, LayoutError> {
48 layout_graph_with_mind_map_freeform(graph, request)?.to_transaction(graph)
49}
50
51fn layout_graph_with_mind_map_freeform_context(
52 graph: &Graph,
53 request: &LayoutRequest,
54 context: &LayoutContext,
55) -> Result<LayoutResult, LayoutError> {
56 validate_request(graph, request)?;
57
58 let mut projection = FreeformProjection::new(graph, request, context)?;
59 projection.layout(request.options);
60 projection.into_result()
61}
62
63struct FreeformProjection<'a> {
64 graph: &'a Graph,
65 request: &'a LayoutRequest,
66 node_infos: BTreeMap<NodeId, FreeformNodeInfo>,
67 visible_edges: Vec<VisibleLayoutEdge>,
68 placements: BTreeMap<NodeId, LayoutNodePosition>,
69}
70
71#[derive(Debug, Clone, Copy)]
72struct FreeformNodeInfo {
73 size: CanvasSize,
74 origin: (f32, f32),
75 current_center: CanvasPoint,
76 pinned: bool,
77}
78
79impl<'a> FreeformProjection<'a> {
80 fn new(
81 graph: &'a Graph,
82 request: &'a LayoutRequest,
83 context: &'a LayoutContext,
84 ) -> Result<Self, LayoutError> {
85 let mut node_infos = BTreeMap::new();
86 let mut visible_nodes = BTreeSet::new();
87
88 for (id, node) in &graph.nodes {
89 if node.hidden || !request.scope.contains(*id) {
90 continue;
91 }
92
93 let size = resolve_node_size(graph, request, context, *id)?;
94 let origin = resolve_node_origin(node.origin, request.options.node_origin, context);
95 let current_center = center_from_position(node.pos, size, origin);
96
97 node_infos.insert(
98 *id,
99 FreeformNodeInfo {
100 size,
101 origin,
102 current_center,
103 pinned: context.pinned_nodes.contains(id),
104 },
105 );
106 visible_nodes.insert(*id);
107 }
108
109 let visible_edges = build_visible_edge_list(graph, &visible_nodes)?;
110
111 Ok(Self {
112 graph,
113 request,
114 node_infos,
115 visible_edges,
116 placements: BTreeMap::new(),
117 })
118 }
119
120 fn layout(&mut self, options: LayoutOptions) {
121 if self.node_infos.is_empty() {
122 return;
123 }
124
125 let gap = freeform_gap(options);
126 let mut node_order = self.node_infos.keys().copied().collect::<Vec<_>>();
127 node_order.sort_by(|left, right| self.compare_nodes(*left, *right, options.direction));
128
129 for node in node_order {
130 let Some(info) = self.node_infos.get(&node).copied() else {
131 continue;
132 };
133
134 let center = if info.pinned {
135 info.current_center
136 } else {
137 self.resolve_node_center(info, gap, options.direction)
138 };
139 let pos = position_from_center(center, info.size, info.origin);
140
141 self.placements.insert(
142 node,
143 LayoutNodePosition {
144 node,
145 pos,
146 center,
147 size: info.size,
148 },
149 );
150 }
151 }
152
153 fn compare_nodes(&self, left: NodeId, right: NodeId, direction: LayoutDirection) -> Ordering {
154 let left_info = self.node_infos.get(&left).expect("left node info");
155 let right_info = self.node_infos.get(&right).expect("right node info");
156
157 right_info
158 .pinned
159 .cmp(&left_info.pinned)
160 .then_with(|| match direction {
161 LayoutDirection::TopToBottom | LayoutDirection::BottomToTop => left_info
162 .current_center
163 .y
164 .total_cmp(&right_info.current_center.y)
165 .then_with(|| {
166 left_info
167 .current_center
168 .x
169 .total_cmp(&right_info.current_center.x)
170 }),
171 LayoutDirection::LeftToRight | LayoutDirection::RightToLeft => left_info
172 .current_center
173 .x
174 .total_cmp(&right_info.current_center.x)
175 .then_with(|| {
176 left_info
177 .current_center
178 .y
179 .total_cmp(&right_info.current_center.y)
180 }),
181 })
182 .then_with(|| left.cmp(&right))
183 }
184
185 fn resolve_node_center(
186 &self,
187 info: FreeformNodeInfo,
188 gap: f32,
189 direction: LayoutDirection,
190 ) -> CanvasPoint {
191 let mut center = info.current_center;
192
193 for _ in 0..64 {
194 let rect = rect_from_center(center, info.size, info.origin);
195 let Some(other) = self
196 .placements
197 .values()
198 .map(node_rect_from_position)
199 .find(|other| rects_overlap_with_gap(rect, *other, gap))
200 else {
201 break;
202 };
203
204 center =
205 shift_center_out_of_rect(center, info.size, info.origin, other, direction, gap);
206 }
207
208 center
209 }
210
211 fn into_result(mut self) -> Result<LayoutResult, LayoutError> {
212 Ok(result_from_placements(
213 self.graph,
214 self.request.options,
215 &mut self.placements,
216 &self.visible_edges,
217 ))
218 }
219}
220
221fn freeform_gap(options: LayoutOptions) -> f32 {
222 options.spacing.nodesep.max(24.0)
223}
224
225fn rect_from_center(center: CanvasPoint, size: CanvasSize, origin: (f32, f32)) -> CanvasRect {
226 CanvasRect {
227 origin: position_from_center(center, size, origin),
228 size,
229 }
230}
231
232fn shift_center_out_of_rect(
233 center: CanvasPoint,
234 size: CanvasSize,
235 origin: (f32, f32),
236 other: CanvasRect,
237 direction: LayoutDirection,
238 gap: f32,
239) -> CanvasPoint {
240 let rect = rect_from_center(center, size, origin);
241 let expanded = expand_rect(other, gap);
242
243 match direction {
244 LayoutDirection::TopToBottom => CanvasPoint {
245 x: center.x,
246 y: center.y + (expanded.origin.y + expanded.size.height - rect.origin.y),
247 },
248 LayoutDirection::BottomToTop => CanvasPoint {
249 x: center.x,
250 y: center.y - (rect.origin.y + rect.size.height - expanded.origin.y),
251 },
252 LayoutDirection::LeftToRight => CanvasPoint {
253 x: center.x + (expanded.origin.x + expanded.size.width - rect.origin.x),
254 y: center.y,
255 },
256 LayoutDirection::RightToLeft => CanvasPoint {
257 x: center.x - (rect.origin.x + rect.size.width - expanded.origin.x),
258 y: center.y,
259 },
260 }
261}
262
263fn rects_overlap_with_gap(candidate: CanvasRect, other: CanvasRect, gap: f32) -> bool {
264 let other = expand_rect(other, gap);
265 let candidate_right = candidate.origin.x + candidate.size.width;
266 let candidate_bottom = candidate.origin.y + candidate.size.height;
267 let other_right = other.origin.x + other.size.width;
268 let other_bottom = other.origin.y + other.size.height;
269
270 candidate.origin.x < other_right
271 && candidate_right > other.origin.x
272 && candidate.origin.y < other_bottom
273 && candidate_bottom > other.origin.y
274}
275
276fn expand_rect(rect: CanvasRect, gap: f32) -> CanvasRect {
277 CanvasRect {
278 origin: CanvasPoint {
279 x: rect.origin.x - gap,
280 y: rect.origin.y - gap,
281 },
282 size: CanvasSize {
283 width: rect.size.width + gap * 2.0,
284 height: rect.size.height + gap * 2.0,
285 },
286 }
287}