1use std::collections::{BTreeMap, BTreeSet};
2use std::fmt;
3use std::sync::Arc;
4
5use jellyflow_core::{
6 CanvasPoint, CanvasRect, CanvasSize, EdgeId, Graph, GraphOp, GraphTransaction, NodeId,
7};
8use serde::{Deserialize, Serialize};
9
10use crate::family::{LayoutEngineMetadata, LayoutFamilyId, LayoutFamilyMetadata};
11
12pub const DUGONG_LAYOUT_ENGINE_ID: &str = "dugong";
14pub const TIDY_TREE_LAYOUT_ENGINE_ID: &str = "tidy_tree";
16pub const MIND_MAP_RADIAL_LAYOUT_ENGINE_ID: &str = "mind_map_radial";
18pub const MIND_MAP_FREEFORM_LAYOUT_ENGINE_ID: &str = "mind_map_freeform";
20
21#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
23#[serde(transparent)]
24pub struct LayoutEngineId(String);
25
26impl LayoutEngineId {
27 pub fn new(id: impl Into<String>) -> Self {
29 Self(id.into())
30 }
31
32 pub fn dugong() -> Self {
34 Self::new(DUGONG_LAYOUT_ENGINE_ID)
35 }
36
37 pub fn tidy_tree() -> Self {
39 Self::new(TIDY_TREE_LAYOUT_ENGINE_ID)
40 }
41
42 pub fn mind_map_radial() -> Self {
44 Self::new(MIND_MAP_RADIAL_LAYOUT_ENGINE_ID)
45 }
46
47 pub fn mind_map_freeform() -> Self {
49 Self::new(MIND_MAP_FREEFORM_LAYOUT_ENGINE_ID)
50 }
51
52 pub fn as_str(&self) -> &str {
54 &self.0
55 }
56}
57
58impl fmt::Display for LayoutEngineId {
59 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
60 f.write_str(&self.0)
61 }
62}
63
64impl From<&str> for LayoutEngineId {
65 fn from(value: &str) -> Self {
66 Self::new(value)
67 }
68}
69
70impl From<String> for LayoutEngineId {
71 fn from(value: String) -> Self {
72 Self::new(value)
73 }
74}
75
76#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
78#[serde(rename_all = "snake_case")]
79pub enum LayoutDirection {
80 #[default]
82 TopToBottom,
83 BottomToTop,
85 LeftToRight,
87 RightToLeft,
89}
90
91#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
93pub struct LayoutSpacing {
94 pub nodesep: f32,
95 pub ranksep: f32,
96 pub edgesep: f32,
97}
98
99impl Default for LayoutSpacing {
100 fn default() -> Self {
101 Self {
102 nodesep: 50.0,
103 ranksep: 50.0,
104 edgesep: 20.0,
105 }
106 }
107}
108
109#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
111pub struct LayoutOptions {
112 pub direction: LayoutDirection,
113 pub spacing: LayoutSpacing,
114 pub margin: CanvasSize,
115 pub default_node_size: CanvasSize,
116 pub node_origin: (f32, f32),
118}
119
120impl Default for LayoutOptions {
121 fn default() -> Self {
122 Self {
123 direction: LayoutDirection::TopToBottom,
124 spacing: LayoutSpacing::default(),
125 margin: CanvasSize {
126 width: 0.0,
127 height: 0.0,
128 },
129 default_node_size: CanvasSize {
130 width: 172.0,
131 height: 36.0,
132 },
133 node_origin: (0.0, 0.0),
134 }
135 }
136}
137
138impl LayoutOptions {
139 pub fn with_default_node_size(mut self, size: CanvasSize) -> Self {
141 self.default_node_size = size;
142 self
143 }
144
145 pub fn with_direction(mut self, direction: LayoutDirection) -> Self {
147 self.direction = direction;
148 self
149 }
150
151 pub fn with_node_origin(mut self, node_origin: (f32, f32)) -> Self {
153 self.node_origin = node_origin;
154 self
155 }
156}
157
158#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
160#[serde(tag = "kind", rename_all = "snake_case")]
161pub enum LayoutScope {
162 #[default]
164 All,
165 Nodes { nodes: BTreeSet<NodeId> },
167}
168
169impl LayoutScope {
170 pub(crate) fn contains(&self, node: NodeId) -> bool {
171 match self {
172 Self::All => true,
173 Self::Nodes { nodes } => nodes.contains(&node),
174 }
175 }
176}
177
178#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
180pub struct LayoutRequest {
181 pub options: LayoutOptions,
182 #[serde(default)]
183 pub scope: LayoutScope,
184 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
186 pub measured_node_sizes: BTreeMap<NodeId, CanvasSize>,
187}
188
189impl Default for LayoutRequest {
190 fn default() -> Self {
191 Self {
192 options: LayoutOptions::default(),
193 scope: LayoutScope::All,
194 measured_node_sizes: BTreeMap::new(),
195 }
196 }
197}
198
199impl LayoutRequest {
200 pub fn all() -> Self {
202 Self::default()
203 }
204
205 pub fn nodes(nodes: impl IntoIterator<Item = NodeId>) -> Self {
207 Self {
208 scope: LayoutScope::Nodes {
209 nodes: nodes.into_iter().collect(),
210 },
211 ..Self::default()
212 }
213 }
214
215 pub fn with_measured_node_sizes(
217 mut self,
218 sizes: impl IntoIterator<Item = (NodeId, CanvasSize)>,
219 ) -> Self {
220 self.measured_node_sizes.extend(sizes);
221 self
222 }
223
224 pub fn with_options(mut self, options: LayoutOptions) -> Self {
226 self.options = options;
227 self
228 }
229}
230
231#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
233pub struct LayoutEngineRequest {
234 pub engine: LayoutEngineId,
235 pub layout: LayoutRequest,
236}
237
238impl Default for LayoutEngineRequest {
239 fn default() -> Self {
240 Self {
241 engine: LayoutEngineId::dugong(),
242 layout: LayoutRequest::default(),
243 }
244 }
245}
246
247impl LayoutEngineRequest {
248 pub fn new(engine: impl Into<LayoutEngineId>, layout: LayoutRequest) -> Self {
250 Self {
251 engine: engine.into(),
252 layout,
253 }
254 }
255
256 pub fn dugong(layout: LayoutRequest) -> Self {
258 Self::new(LayoutEngineId::dugong(), layout)
259 }
260
261 pub fn tidy_tree(layout: LayoutRequest) -> Self {
263 Self::new(LayoutEngineId::tidy_tree(), layout)
264 }
265
266 pub fn mind_map_radial(layout: LayoutRequest) -> Self {
268 Self::new(LayoutEngineId::mind_map_radial(), layout)
269 }
270
271 pub fn mind_map_freeform(layout: LayoutRequest) -> Self {
273 Self::new(LayoutEngineId::mind_map_freeform(), layout)
274 }
275
276 pub fn with_engine(mut self, engine: impl Into<LayoutEngineId>) -> Self {
278 self.engine = engine.into();
279 self
280 }
281
282 pub fn with_layout(mut self, layout: LayoutRequest) -> Self {
284 self.layout = layout;
285 self
286 }
287}
288
289#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
291pub struct LayoutContext {
292 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
294 pub measured_node_sizes: BTreeMap<NodeId, CanvasSize>,
295 #[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
297 pub pinned_nodes: BTreeSet<NodeId>,
298 #[serde(default, skip_serializing_if = "Option::is_none")]
300 pub node_origin: Option<(f32, f32)>,
301}
302
303impl LayoutContext {
304 pub fn new() -> Self {
306 Self::default()
307 }
308
309 pub fn with_measured_node_sizes(
311 mut self,
312 sizes: impl IntoIterator<Item = (NodeId, CanvasSize)>,
313 ) -> Self {
314 self.measured_node_sizes.extend(sizes);
315 self
316 }
317
318 pub fn with_pinned_nodes(mut self, nodes: impl IntoIterator<Item = NodeId>) -> Self {
320 self.pinned_nodes.extend(nodes);
321 self
322 }
323
324 pub fn with_node_origin(mut self, node_origin: (f32, f32)) -> Self {
326 self.node_origin = Some(node_origin);
327 self
328 }
329}
330
331#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
333pub struct LayoutNodePosition {
334 pub node: NodeId,
335 pub pos: CanvasPoint,
336 pub center: CanvasPoint,
337 pub size: CanvasSize,
338}
339
340#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
342pub struct LayoutEdgeRoute {
343 pub edge: EdgeId,
344 pub points: Vec<CanvasPoint>,
345}
346
347#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
349pub struct LayoutResult {
350 pub nodes: Vec<LayoutNodePosition>,
353 #[serde(default, skip_serializing_if = "Vec::is_empty")]
354 pub edge_routes: Vec<LayoutEdgeRoute>,
355 #[serde(default, skip_serializing_if = "Option::is_none")]
356 pub bounds: Option<CanvasRect>,
357}
358
359impl LayoutResult {
360 pub fn node_position(&self, node: NodeId) -> Option<LayoutNodePosition> {
362 self.nodes
363 .iter()
364 .find(|position| position.node == node)
365 .copied()
366 }
367
368 pub fn to_transaction(&self, graph: &Graph) -> Result<GraphTransaction, LayoutError> {
370 let mut seen = BTreeSet::new();
371 let mut ops = Vec::new();
372
373 for node in &self.nodes {
374 if !seen.insert(node.node) {
375 return Err(LayoutError::DuplicateResultNode(node.node));
376 }
377
378 let from = graph
379 .nodes
380 .get(&node.node)
381 .ok_or(LayoutError::MissingTransactionNode(node.node))?
382 .pos;
383 if from != node.pos {
384 ops.push(GraphOp::SetNodePos {
385 id: node.node,
386 from,
387 to: node.pos,
388 });
389 }
390 }
391
392 Ok(GraphTransaction::from_ops(ops).with_label("Layout graph"))
393 }
394}
395
396#[derive(Debug, thiserror::Error, PartialEq)]
398pub enum LayoutError {
399 #[error("layout engine id is already registered: {0}")]
400 DuplicateLayoutEngine(LayoutEngineId),
401 #[error("layout family id is already registered: {0}")]
402 DuplicateLayoutFamily(LayoutFamilyId),
403 #[error("layout engine metadata is already registered: {0}")]
404 DuplicateLayoutEngineMetadata(LayoutEngineId),
405 #[error("layout engine is not registered: {0}")]
406 MissingLayoutEngine(LayoutEngineId),
407 #[error("layout default node size must be positive and finite: {0:?}")]
408 InvalidDefaultNodeSize(CanvasSize),
409 #[error("layout spacing values must be non-negative and finite: {0:?}")]
410 InvalidSpacing(LayoutSpacing),
411 #[error("layout margin must be non-negative and finite: {0:?}")]
412 InvalidMargin(CanvasSize),
413 #[error("layout node size must be positive and finite for node {node:?}: {size:?}")]
414 InvalidNodeSize { node: NodeId, size: CanvasSize },
415 #[error("layout scope references missing node: {0:?}")]
416 MissingScopeNode(NodeId),
417 #[error("layout edge references missing source port: {0:?}")]
418 MissingSourcePort(EdgeId),
419 #[error("layout edge references missing target port: {0:?}")]
420 MissingTargetPort(EdgeId),
421 #[error("layout edge source port references missing node: {edge:?}")]
422 MissingSourceNode { edge: EdgeId },
423 #[error("layout edge target port references missing node: {edge:?}")]
424 MissingTargetNode { edge: EdgeId },
425 #[error("layout engine did not return a node position for node {0:?}")]
426 MissingNodePosition(NodeId),
427 #[error("layout result contains a duplicate node position for node {0:?}")]
428 DuplicateResultNode(NodeId),
429 #[error("layout result references missing graph node: {0:?}")]
430 MissingTransactionNode(NodeId),
431 #[error("layout engine returned a non-finite node position for node {node:?}: ({x}, {y})")]
432 NonFiniteNodePosition { node: NodeId, x: f64, y: f64 },
433}
434
435pub trait LayoutEngine: Send + Sync {
437 fn id(&self) -> LayoutEngineId;
439
440 fn layout(
442 &self,
443 graph: &Graph,
444 request: &LayoutRequest,
445 context: &LayoutContext,
446 ) -> Result<LayoutResult, LayoutError>;
447}
448
449#[derive(Default, Clone)]
451pub struct LayoutEngineRegistry {
452 engines: BTreeMap<LayoutEngineId, Arc<dyn LayoutEngine>>,
453 families: BTreeMap<LayoutFamilyId, LayoutFamilyMetadata>,
454 metadata: BTreeMap<LayoutEngineId, LayoutEngineMetadata>,
455}
456
457impl fmt::Debug for LayoutEngineRegistry {
458 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
459 f.debug_struct("LayoutEngineRegistry")
460 .field("engines", &self.engine_ids().collect::<Vec<_>>())
461 .field("families", &self.family_ids().collect::<Vec<_>>())
462 .finish()
463 }
464}
465
466impl LayoutEngineRegistry {
467 pub fn new() -> Self {
469 Self::default()
470 }
471
472 pub fn insert<E>(&mut self, engine: E) -> Result<(), LayoutError>
474 where
475 E: LayoutEngine + 'static,
476 {
477 self.insert_shared(Arc::new(engine))
478 }
479
480 pub fn insert_shared(&mut self, engine: Arc<dyn LayoutEngine>) -> Result<(), LayoutError> {
482 let id = engine.id();
483 if self.engines.contains_key(&id) {
484 return Err(LayoutError::DuplicateLayoutEngine(id));
485 }
486 self.engines.insert(id, engine);
487 Ok(())
488 }
489
490 pub fn insert_family(&mut self, family: LayoutFamilyMetadata) -> Result<(), LayoutError> {
492 let id = family.id.clone();
493 if self.families.contains_key(&id) {
494 return Err(LayoutError::DuplicateLayoutFamily(id));
495 }
496 self.families.insert(id, family);
497 Ok(())
498 }
499
500 pub fn insert_metadata(&mut self, metadata: LayoutEngineMetadata) -> Result<(), LayoutError> {
502 let id = metadata.engine.clone();
503 if self.metadata.contains_key(&id) {
504 return Err(LayoutError::DuplicateLayoutEngineMetadata(id));
505 }
506 self.metadata.insert(id, metadata);
507 Ok(())
508 }
509
510 pub fn get(&self, id: &LayoutEngineId) -> Option<&dyn LayoutEngine> {
512 self.engines.get(id).map(Arc::as_ref)
513 }
514
515 pub fn family(&self, id: &LayoutFamilyId) -> Option<&LayoutFamilyMetadata> {
517 self.families.get(id)
518 }
519
520 pub fn metadata(&self, id: &LayoutEngineId) -> Option<&LayoutEngineMetadata> {
522 self.metadata.get(id)
523 }
524
525 pub fn engine_ids(&self) -> impl Iterator<Item = &LayoutEngineId> {
527 self.engines.keys()
528 }
529
530 pub fn family_ids(&self) -> impl Iterator<Item = &LayoutFamilyId> {
532 self.families.keys()
533 }
534
535 pub fn families(&self) -> impl Iterator<Item = &LayoutFamilyMetadata> {
537 self.families.values()
538 }
539
540 pub fn engine_metadata(&self) -> impl Iterator<Item = &LayoutEngineMetadata> {
542 self.metadata.values()
543 }
544
545 pub fn engines_for_family(
547 &self,
548 family: &LayoutFamilyId,
549 ) -> impl Iterator<Item = &LayoutEngineMetadata> {
550 self.metadata
551 .values()
552 .filter(move |metadata| &metadata.family == family)
553 }
554
555 pub fn layout(
557 &self,
558 graph: &Graph,
559 request: &LayoutEngineRequest,
560 context: &LayoutContext,
561 ) -> Result<LayoutResult, LayoutError> {
562 let engine = self
563 .get(&request.engine)
564 .ok_or_else(|| LayoutError::MissingLayoutEngine(request.engine.clone()))?;
565 engine.layout(graph, &request.layout, context)
566 }
567}
568
569pub fn layout_graph_with_engine(
571 graph: &Graph,
572 request: &LayoutEngineRequest,
573 registry: &LayoutEngineRegistry,
574 context: &LayoutContext,
575) -> Result<LayoutResult, LayoutError> {
576 registry.layout(graph, request, context)
577}
578
579pub fn layout_graph_to_transaction_with_engine(
581 graph: &Graph,
582 request: &LayoutEngineRequest,
583 registry: &LayoutEngineRegistry,
584 context: &LayoutContext,
585) -> Result<GraphTransaction, LayoutError> {
586 layout_graph_with_engine(graph, request, registry, context)?.to_transaction(graph)
587}
588
589pub(crate) fn validate_request(graph: &Graph, request: &LayoutRequest) -> Result<(), LayoutError> {
590 if !request.options.default_node_size.is_positive_finite() {
591 return Err(LayoutError::InvalidDefaultNodeSize(
592 request.options.default_node_size,
593 ));
594 }
595 if !is_non_negative_finite_spacing(request.options.spacing) {
596 return Err(LayoutError::InvalidSpacing(request.options.spacing));
597 }
598 if !is_non_negative_finite_size(request.options.margin) {
599 return Err(LayoutError::InvalidMargin(request.options.margin));
600 }
601
602 if let LayoutScope::Nodes { nodes } = &request.scope {
603 for node in nodes {
604 if !graph.nodes.contains_key(node) {
605 return Err(LayoutError::MissingScopeNode(*node));
606 }
607 }
608 }
609
610 Ok(())
611}
612
613pub(crate) fn resolve_node_size(
614 graph: &Graph,
615 request: &LayoutRequest,
616 context: &LayoutContext,
617 node: NodeId,
618) -> Result<CanvasSize, LayoutError> {
619 let size = graph
620 .nodes
621 .get(&node)
622 .and_then(|node| node.size)
623 .or_else(|| request.measured_node_sizes.get(&node).copied())
624 .or_else(|| context.measured_node_sizes.get(&node).copied())
625 .unwrap_or(request.options.default_node_size);
626
627 if size.is_positive_finite() {
628 Ok(size)
629 } else {
630 Err(LayoutError::InvalidNodeSize { node, size })
631 }
632}
633
634pub(crate) fn resolve_node_origin(
635 origin: Option<jellyflow_core::NodeOrigin>,
636 request_fallback: (f32, f32),
637 context: &LayoutContext,
638) -> (f32, f32) {
639 let fallback = context.node_origin.unwrap_or(request_fallback);
640 let (x, y) = origin.map(|origin| origin.as_tuple()).unwrap_or(fallback);
641 (normalize_origin_component(x), normalize_origin_component(y))
642}
643
644pub(crate) fn position_from_center(
645 center: CanvasPoint,
646 size: CanvasSize,
647 origin: (f32, f32),
648) -> CanvasPoint {
649 CanvasPoint {
650 x: center.x - size.width * (0.5 - origin.0),
651 y: center.y - size.height * (0.5 - origin.1),
652 }
653}
654
655pub(crate) fn node_rect_from_position(node: &LayoutNodePosition) -> CanvasRect {
656 CanvasRect {
657 origin: CanvasPoint {
658 x: node.center.x - node.size.width * 0.5,
659 y: node.center.y - node.size.height * 0.5,
660 },
661 size: node.size,
662 }
663}
664
665pub(crate) fn union_bounds(bounds: Option<CanvasRect>, next: CanvasRect) -> Option<CanvasRect> {
666 if !next.is_positive_finite() {
667 return bounds;
668 }
669
670 let Some(bounds) = bounds else {
671 return Some(next);
672 };
673
674 let min_x = bounds.origin.x.min(next.origin.x);
675 let min_y = bounds.origin.y.min(next.origin.y);
676 let max_x = (bounds.origin.x + bounds.size.width).max(next.origin.x + next.size.width);
677 let max_y = (bounds.origin.y + bounds.size.height).max(next.origin.y + next.size.height);
678
679 Some(CanvasRect {
680 origin: CanvasPoint { x: min_x, y: min_y },
681 size: CanvasSize {
682 width: max_x - min_x,
683 height: max_y - min_y,
684 },
685 })
686}
687
688fn is_non_negative_finite_spacing(spacing: LayoutSpacing) -> bool {
689 spacing.nodesep.is_finite()
690 && spacing.ranksep.is_finite()
691 && spacing.edgesep.is_finite()
692 && spacing.nodesep >= 0.0
693 && spacing.ranksep >= 0.0
694 && spacing.edgesep >= 0.0
695}
696
697fn is_non_negative_finite_size(size: CanvasSize) -> bool {
698 size.is_finite() && size.width >= 0.0 && size.height >= 0.0
699}
700
701fn normalize_origin_component(component: f32) -> f32 {
702 if component.is_finite() {
703 component.clamp(0.0, 1.0)
704 } else {
705 0.0
706 }
707}