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