1use std::cmp::Ordering;
2use std::collections::{BTreeMap, BTreeSet};
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_FREEFORM_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 MindMapFreeformLayoutEngine;
18
19impl LayoutEngine for MindMapFreeformLayoutEngine {
20 fn id(&self) -> LayoutEngineId {
21 LayoutEngineId::new(MIND_MAP_FREEFORM_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_freeform_context(graph, request, context)
31 }
32}
33
34pub fn layout_graph_with_mind_map_freeform(
36 graph: &Graph,
37 request: &LayoutRequest,
38) -> Result<LayoutResult, LayoutError> {
39 layout_graph_with_mind_map_freeform_context(graph, request, &LayoutContext::default())
40}
41
42pub fn layout_graph_to_transaction_with_mind_map_freeform(
44 graph: &Graph,
45 request: &LayoutRequest,
46) -> Result<GraphTransaction, LayoutError> {
47 layout_graph_with_mind_map_freeform(graph, request)?.to_transaction(graph)
48}
49
50fn layout_graph_with_mind_map_freeform_context(
51 graph: &Graph,
52 request: &LayoutRequest,
53 context: &LayoutContext,
54) -> Result<LayoutResult, LayoutError> {
55 validate_request(graph, request)?;
56
57 let mut projection = FreeformProjection::new(graph, request, context)?;
58 projection.layout(request.options);
59 projection.into_result()
60}
61
62struct FreeformProjection<'a> {
63 graph: &'a Graph,
64 request: &'a LayoutRequest,
65 node_infos: BTreeMap<NodeId, FreeformNodeInfo>,
66 visible_edges: Vec<FreeformEdge>,
67 placements: BTreeMap<NodeId, LayoutNodePosition>,
68}
69
70#[derive(Debug, Clone, Copy)]
71struct FreeformNodeInfo {
72 size: CanvasSize,
73 origin: (f32, f32),
74 current_center: CanvasPoint,
75 pinned: bool,
76}
77
78#[derive(Debug, Clone)]
79struct FreeformEdge {
80 id: EdgeId,
81 source: NodeId,
82 target: NodeId,
83}
84
85impl<'a> FreeformProjection<'a> {
86 fn new(
87 graph: &'a Graph,
88 request: &'a LayoutRequest,
89 context: &'a LayoutContext,
90 ) -> Result<Self, LayoutError> {
91 let mut node_infos = BTreeMap::new();
92 let mut visible_nodes = BTreeSet::new();
93
94 for (id, node) in &graph.nodes {
95 if node.hidden || !request.scope.contains(*id) {
96 continue;
97 }
98
99 let size = resolve_node_size(graph, request, context, *id)?;
100 let origin = resolve_node_origin(node.origin, request.options.node_origin, context);
101 let current_center = center_from_position(node.pos, size, origin);
102
103 node_infos.insert(
104 *id,
105 FreeformNodeInfo {
106 size,
107 origin,
108 current_center,
109 pinned: context.pinned_nodes.contains(id),
110 },
111 );
112 visible_nodes.insert(*id);
113 }
114
115 let visible_edges = build_visible_edges(graph, &visible_nodes)?;
116
117 Ok(Self {
118 graph,
119 request,
120 node_infos,
121 visible_edges,
122 placements: BTreeMap::new(),
123 })
124 }
125
126 fn layout(&mut self, options: LayoutOptions) {
127 if self.node_infos.is_empty() {
128 return;
129 }
130
131 let gap = freeform_gap(options);
132 let mut node_order = self.node_infos.keys().copied().collect::<Vec<_>>();
133 node_order.sort_by(|left, right| self.compare_nodes(*left, *right, options.direction));
134
135 for node in node_order {
136 let Some(info) = self.node_infos.get(&node).copied() else {
137 continue;
138 };
139
140 let center = if info.pinned {
141 info.current_center
142 } else {
143 self.resolve_node_center(info, gap, options.direction)
144 };
145 let pos = position_from_center(center, info.size, info.origin);
146
147 self.placements.insert(
148 node,
149 LayoutNodePosition {
150 node,
151 pos,
152 center,
153 size: info.size,
154 },
155 );
156 }
157 }
158
159 fn compare_nodes(&self, left: NodeId, right: NodeId, direction: LayoutDirection) -> Ordering {
160 let left_info = self.node_infos.get(&left).expect("left node info");
161 let right_info = self.node_infos.get(&right).expect("right node info");
162
163 right_info
164 .pinned
165 .cmp(&left_info.pinned)
166 .then_with(|| match direction {
167 LayoutDirection::TopToBottom | LayoutDirection::BottomToTop => left_info
168 .current_center
169 .y
170 .total_cmp(&right_info.current_center.y)
171 .then_with(|| {
172 left_info
173 .current_center
174 .x
175 .total_cmp(&right_info.current_center.x)
176 }),
177 LayoutDirection::LeftToRight | LayoutDirection::RightToLeft => left_info
178 .current_center
179 .x
180 .total_cmp(&right_info.current_center.x)
181 .then_with(|| {
182 left_info
183 .current_center
184 .y
185 .total_cmp(&right_info.current_center.y)
186 }),
187 })
188 .then_with(|| left.cmp(&right))
189 }
190
191 fn resolve_node_center(
192 &self,
193 info: FreeformNodeInfo,
194 gap: f32,
195 direction: LayoutDirection,
196 ) -> CanvasPoint {
197 let mut center = info.current_center;
198
199 for _ in 0..64 {
200 let rect = rect_from_center(center, info.size, info.origin);
201 let Some(other) = self
202 .placements
203 .values()
204 .map(node_rect_from_position)
205 .find(|other| rects_overlap_with_gap(rect, *other, gap))
206 else {
207 break;
208 };
209
210 center =
211 shift_center_out_of_rect(center, info.size, info.origin, other, direction, gap);
212 }
213
214 center
215 }
216
217 fn into_result(mut self) -> Result<LayoutResult, LayoutError> {
218 if self.placements.is_empty() {
219 return Ok(LayoutResult {
220 nodes: Vec::new(),
221 edge_routes: Vec::new(),
222 bounds: None,
223 });
224 }
225
226 let mut bounds = None;
227 for node in self.placements.values() {
228 bounds = union_bounds(bounds, node_rect_from_position(node));
229 }
230
231 let shift = bounds.map_or(
232 CanvasPoint {
233 x: self.request.options.margin.width,
234 y: self.request.options.margin.height,
235 },
236 |bounds| CanvasPoint {
237 x: self.request.options.margin.width - bounds.origin.x,
238 y: self.request.options.margin.height - bounds.origin.y,
239 },
240 );
241
242 if shift.x != 0.0 || shift.y != 0.0 {
243 for node in self.placements.values_mut() {
244 node.pos.x += shift.x;
245 node.pos.y += shift.y;
246 node.center.x += shift.x;
247 node.center.y += shift.y;
248 }
249 bounds = bounds.map(|bounds| CanvasRect {
250 origin: CanvasPoint {
251 x: bounds.origin.x + shift.x,
252 y: bounds.origin.y + shift.y,
253 },
254 size: bounds.size,
255 });
256 }
257
258 let nodes = self
259 .graph
260 .nodes
261 .keys()
262 .filter_map(|node| self.placements.get(node).copied())
263 .collect::<Vec<_>>();
264
265 let edge_routes = self
266 .visible_edges
267 .iter()
268 .filter_map(|edge| {
269 let source = self.placements.get(&edge.source)?;
270 let target = self.placements.get(&edge.target)?;
271 Some(LayoutEdgeRoute {
272 edge: edge.id,
273 points: vec![source.center, target.center],
274 })
275 })
276 .collect::<Vec<_>>();
277
278 Ok(LayoutResult {
279 nodes,
280 edge_routes,
281 bounds,
282 })
283 }
284}
285
286fn build_visible_edges(
287 graph: &Graph,
288 visible_nodes: &BTreeSet<NodeId>,
289) -> Result<Vec<FreeformEdge>, LayoutError> {
290 let mut visible_edges = Vec::new();
291
292 for (edge_id, edge) in &graph.edges {
293 if edge.hidden {
294 continue;
295 }
296
297 let source_port = graph
298 .ports
299 .get(&edge.from)
300 .ok_or(LayoutError::MissingSourcePort(*edge_id))?;
301 let target_port = graph
302 .ports
303 .get(&edge.to)
304 .ok_or(LayoutError::MissingTargetPort(*edge_id))?;
305 if !graph.nodes.contains_key(&source_port.node) {
306 return Err(LayoutError::MissingSourceNode { edge: *edge_id });
307 }
308 if !graph.nodes.contains_key(&target_port.node) {
309 return Err(LayoutError::MissingTargetNode { edge: *edge_id });
310 }
311 if !visible_nodes.contains(&source_port.node) || !visible_nodes.contains(&target_port.node)
312 {
313 continue;
314 }
315
316 visible_edges.push(FreeformEdge {
317 id: *edge_id,
318 source: source_port.node,
319 target: target_port.node,
320 });
321 }
322
323 Ok(visible_edges)
324}
325
326fn freeform_gap(options: LayoutOptions) -> f32 {
327 options.spacing.nodesep.max(24.0)
328}
329
330fn center_from_position(pos: CanvasPoint, size: CanvasSize, origin: (f32, f32)) -> CanvasPoint {
331 CanvasPoint {
332 x: pos.x + size.width * (0.5 - origin.0),
333 y: pos.y + size.height * (0.5 - origin.1),
334 }
335}
336
337fn rect_from_center(center: CanvasPoint, size: CanvasSize, origin: (f32, f32)) -> CanvasRect {
338 CanvasRect {
339 origin: position_from_center(center, size, origin),
340 size,
341 }
342}
343
344fn shift_center_out_of_rect(
345 center: CanvasPoint,
346 size: CanvasSize,
347 origin: (f32, f32),
348 other: CanvasRect,
349 direction: LayoutDirection,
350 gap: f32,
351) -> CanvasPoint {
352 let rect = rect_from_center(center, size, origin);
353 let expanded = expand_rect(other, gap);
354
355 match direction {
356 LayoutDirection::TopToBottom => CanvasPoint {
357 x: center.x,
358 y: center.y + (expanded.origin.y + expanded.size.height - rect.origin.y),
359 },
360 LayoutDirection::BottomToTop => CanvasPoint {
361 x: center.x,
362 y: center.y - (rect.origin.y + rect.size.height - expanded.origin.y),
363 },
364 LayoutDirection::LeftToRight => CanvasPoint {
365 x: center.x + (expanded.origin.x + expanded.size.width - rect.origin.x),
366 y: center.y,
367 },
368 LayoutDirection::RightToLeft => CanvasPoint {
369 x: center.x - (rect.origin.x + rect.size.width - expanded.origin.x),
370 y: center.y,
371 },
372 }
373}
374
375fn rects_overlap_with_gap(candidate: CanvasRect, other: CanvasRect, gap: f32) -> bool {
376 let other = expand_rect(other, gap);
377 let candidate_right = candidate.origin.x + candidate.size.width;
378 let candidate_bottom = candidate.origin.y + candidate.size.height;
379 let other_right = other.origin.x + other.size.width;
380 let other_bottom = other.origin.y + other.size.height;
381
382 candidate.origin.x < other_right
383 && candidate_right > other.origin.x
384 && candidate.origin.y < other_bottom
385 && candidate_bottom > other.origin.y
386}
387
388fn expand_rect(rect: CanvasRect, gap: f32) -> CanvasRect {
389 CanvasRect {
390 origin: CanvasPoint {
391 x: rect.origin.x - gap,
392 y: rect.origin.y - gap,
393 },
394 size: CanvasSize {
395 width: rect.size.width + gap * 2.0,
396 height: rect.size.height + gap * 2.0,
397 },
398 }
399}