1use std::collections::{BTreeMap, BTreeSet};
2use std::fmt;
3use std::sync::Arc;
4
5use jellyflow_core::{
6 BindingEndpoint, CanvasPoint, CanvasRect, CanvasSize, EdgeId, Graph, GraphMutationFootprint,
7 GraphOp, GraphTransaction, NodeId,
8};
9use serde::{Deserialize, Serialize};
10
11use crate::family::{LayoutEngineMetadata, LayoutFamilyId, LayoutFamilyMetadata};
12
13pub const DUGONG_LAYOUT_ENGINE_ID: &str = "dugong";
15pub const TIDY_TREE_LAYOUT_ENGINE_ID: &str = "tidy_tree";
17pub const MIND_MAP_RADIAL_LAYOUT_ENGINE_ID: &str = "mind_map_radial";
19pub const MIND_MAP_FREEFORM_LAYOUT_ENGINE_ID: &str = "mind_map_freeform";
21
22#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
24#[serde(transparent)]
25pub struct LayoutEngineId(String);
26
27impl LayoutEngineId {
28 pub fn new(id: impl Into<String>) -> Self {
30 Self(id.into())
31 }
32
33 pub fn dugong() -> Self {
35 Self::new(DUGONG_LAYOUT_ENGINE_ID)
36 }
37
38 pub fn tidy_tree() -> Self {
40 Self::new(TIDY_TREE_LAYOUT_ENGINE_ID)
41 }
42
43 pub fn mind_map_radial() -> Self {
45 Self::new(MIND_MAP_RADIAL_LAYOUT_ENGINE_ID)
46 }
47
48 pub fn mind_map_freeform() -> Self {
50 Self::new(MIND_MAP_FREEFORM_LAYOUT_ENGINE_ID)
51 }
52
53 pub fn as_str(&self) -> &str {
55 &self.0
56 }
57}
58
59impl fmt::Display for LayoutEngineId {
60 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
61 f.write_str(&self.0)
62 }
63}
64
65impl From<&str> for LayoutEngineId {
66 fn from(value: &str) -> Self {
67 Self::new(value)
68 }
69}
70
71impl From<String> for LayoutEngineId {
72 fn from(value: String) -> Self {
73 Self::new(value)
74 }
75}
76
77#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
79#[serde(rename_all = "snake_case")]
80pub enum LayoutDirection {
81 #[default]
83 TopToBottom,
84 BottomToTop,
86 LeftToRight,
88 RightToLeft,
90}
91
92#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
94pub struct LayoutSpacing {
95 pub nodesep: f32,
96 pub ranksep: f32,
97 pub edgesep: f32,
98}
99
100impl Default for LayoutSpacing {
101 fn default() -> Self {
102 Self {
103 nodesep: 50.0,
104 ranksep: 50.0,
105 edgesep: 20.0,
106 }
107 }
108}
109
110#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
112pub struct LayoutOptions {
113 pub direction: LayoutDirection,
114 pub spacing: LayoutSpacing,
115 pub margin: CanvasSize,
116 pub default_node_size: CanvasSize,
117 pub node_origin: (f32, f32),
119}
120
121impl Default for LayoutOptions {
122 fn default() -> Self {
123 Self {
124 direction: LayoutDirection::TopToBottom,
125 spacing: LayoutSpacing::default(),
126 margin: CanvasSize {
127 width: 0.0,
128 height: 0.0,
129 },
130 default_node_size: CanvasSize {
131 width: 172.0,
132 height: 36.0,
133 },
134 node_origin: (0.0, 0.0),
135 }
136 }
137}
138
139impl LayoutOptions {
140 pub fn with_default_node_size(mut self, size: CanvasSize) -> Self {
142 self.default_node_size = size;
143 self
144 }
145
146 pub fn with_direction(mut self, direction: LayoutDirection) -> Self {
148 self.direction = direction;
149 self
150 }
151
152 pub fn with_node_origin(mut self, node_origin: (f32, f32)) -> Self {
154 self.node_origin = node_origin;
155 self
156 }
157}
158
159#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
161#[serde(tag = "kind", rename_all = "snake_case")]
162pub enum LayoutScope {
163 #[default]
165 All,
166 Nodes { nodes: BTreeSet<NodeId> },
168}
169
170impl LayoutScope {
171 pub fn from_footprint(graph: &Graph, footprint: &GraphMutationFootprint) -> Self {
177 let mut nodes = footprint
178 .nodes
179 .iter()
180 .copied()
181 .filter(|node| graph.nodes().contains_key(node))
182 .collect::<BTreeSet<_>>();
183
184 for port in &footprint.ports {
185 if let Some(port) = graph.ports().get(port) {
186 nodes.insert(port.node);
187 }
188 }
189
190 for edge in &footprint.edges {
191 if let Some(edge) = graph.edges().get(edge) {
192 push_port_owner(graph, edge.from, &mut nodes);
193 push_port_owner(graph, edge.to, &mut nodes);
194 }
195 }
196
197 for binding in &footprint.bindings {
198 if let Some(binding) = graph.bindings().get(binding) {
199 push_binding_endpoint_nodes(graph, &binding.subject, &mut nodes);
200 push_binding_endpoint_nodes(graph, &binding.target, &mut nodes);
201 }
202 }
203
204 Self::Nodes { nodes }
205 }
206
207 pub fn from_transaction(graph: &Graph, tx: &GraphTransaction) -> Self {
209 Self::from_footprint(graph, &tx.footprint())
210 }
211
212 pub fn is_empty(&self) -> bool {
214 matches!(self, Self::Nodes { nodes } if nodes.is_empty())
215 }
216
217 pub fn nodes(&self) -> Option<&BTreeSet<NodeId>> {
219 match self {
220 Self::All => None,
221 Self::Nodes { nodes } => Some(nodes),
222 }
223 }
224
225 pub(crate) fn contains(&self, node: NodeId) -> bool {
226 match self {
227 Self::All => true,
228 Self::Nodes { nodes } => nodes.contains(&node),
229 }
230 }
231}
232
233#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
235pub struct LayoutRequest {
236 pub options: LayoutOptions,
237 #[serde(default)]
238 pub scope: LayoutScope,
239 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
241 pub measured_node_sizes: BTreeMap<NodeId, CanvasSize>,
242}
243
244impl Default for LayoutRequest {
245 fn default() -> Self {
246 Self {
247 options: LayoutOptions::default(),
248 scope: LayoutScope::All,
249 measured_node_sizes: BTreeMap::new(),
250 }
251 }
252}
253
254impl LayoutRequest {
255 pub fn all() -> Self {
257 Self::default()
258 }
259
260 pub fn nodes(nodes: impl IntoIterator<Item = NodeId>) -> Self {
262 Self {
263 scope: LayoutScope::Nodes {
264 nodes: nodes.into_iter().collect(),
265 },
266 ..Self::default()
267 }
268 }
269
270 pub fn from_transaction(graph: &Graph, tx: &GraphTransaction) -> Self {
272 Self {
273 scope: LayoutScope::from_transaction(graph, tx),
274 ..Self::default()
275 }
276 }
277
278 pub fn with_measured_node_sizes(
280 mut self,
281 sizes: impl IntoIterator<Item = (NodeId, CanvasSize)>,
282 ) -> Self {
283 self.measured_node_sizes.extend(sizes);
284 self
285 }
286
287 pub fn with_options(mut self, options: LayoutOptions) -> Self {
289 self.options = options;
290 self
291 }
292
293 pub fn with_dirty_scope_from_transaction(
295 mut self,
296 graph: &Graph,
297 tx: &GraphTransaction,
298 ) -> Self {
299 self.scope = LayoutScope::from_transaction(graph, tx);
300 self
301 }
302
303 pub fn with_dirty_scope_from_footprint(
305 mut self,
306 graph: &Graph,
307 footprint: &GraphMutationFootprint,
308 ) -> Self {
309 self.scope = LayoutScope::from_footprint(graph, footprint);
310 self
311 }
312}
313
314#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
316pub struct LayoutEngineRequest {
317 pub engine: LayoutEngineId,
318 pub layout: LayoutRequest,
319}
320
321impl Default for LayoutEngineRequest {
322 fn default() -> Self {
323 Self {
324 engine: LayoutEngineId::dugong(),
325 layout: LayoutRequest::default(),
326 }
327 }
328}
329
330impl LayoutEngineRequest {
331 pub fn new(engine: impl Into<LayoutEngineId>, layout: LayoutRequest) -> Self {
333 Self {
334 engine: engine.into(),
335 layout,
336 }
337 }
338
339 pub fn dugong(layout: LayoutRequest) -> Self {
341 Self::new(LayoutEngineId::dugong(), layout)
342 }
343
344 pub fn tidy_tree(layout: LayoutRequest) -> Self {
346 Self::new(LayoutEngineId::tidy_tree(), layout)
347 }
348
349 pub fn mind_map_radial(layout: LayoutRequest) -> Self {
351 Self::new(LayoutEngineId::mind_map_radial(), layout)
352 }
353
354 pub fn mind_map_freeform(layout: LayoutRequest) -> Self {
356 Self::new(LayoutEngineId::mind_map_freeform(), layout)
357 }
358
359 pub fn with_engine(mut self, engine: impl Into<LayoutEngineId>) -> Self {
361 self.engine = engine.into();
362 self
363 }
364
365 pub fn with_layout(mut self, layout: LayoutRequest) -> Self {
367 self.layout = layout;
368 self
369 }
370}
371
372#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
374pub struct LayoutContext {
375 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
377 pub measured_node_sizes: BTreeMap<NodeId, CanvasSize>,
378 #[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
380 pub pinned_nodes: BTreeSet<NodeId>,
381 #[serde(default, skip_serializing_if = "Option::is_none")]
383 pub node_origin: Option<(f32, f32)>,
384}
385
386impl LayoutContext {
387 pub fn new() -> Self {
389 Self::default()
390 }
391
392 pub fn with_measured_node_sizes(
394 mut self,
395 sizes: impl IntoIterator<Item = (NodeId, CanvasSize)>,
396 ) -> Self {
397 self.measured_node_sizes.extend(sizes);
398 self
399 }
400
401 pub fn with_pinned_nodes(mut self, nodes: impl IntoIterator<Item = NodeId>) -> Self {
403 self.pinned_nodes.extend(nodes);
404 self
405 }
406
407 pub fn with_node_origin(mut self, node_origin: (f32, f32)) -> Self {
409 self.node_origin = Some(node_origin);
410 self
411 }
412}
413
414#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
416pub struct LayoutNodePosition {
417 pub node: NodeId,
418 pub pos: CanvasPoint,
419 pub center: CanvasPoint,
420 pub size: CanvasSize,
421}
422
423#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
425pub struct LayoutEdgeRoute {
426 pub edge: EdgeId,
427 pub points: Vec<CanvasPoint>,
428}
429
430#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
432pub struct LayoutResult {
433 pub nodes: Vec<LayoutNodePosition>,
436 #[serde(default, skip_serializing_if = "Vec::is_empty")]
437 pub edge_routes: Vec<LayoutEdgeRoute>,
438 #[serde(default, skip_serializing_if = "Option::is_none")]
439 pub bounds: Option<CanvasRect>,
440}
441
442impl LayoutResult {
443 pub fn node_position(&self, node: NodeId) -> Option<LayoutNodePosition> {
445 self.nodes
446 .iter()
447 .find(|position| position.node == node)
448 .copied()
449 }
450
451 pub fn to_transaction(&self, graph: &Graph) -> Result<GraphTransaction, LayoutError> {
453 let mut seen = BTreeSet::new();
454 let mut ops = Vec::new();
455
456 for node in &self.nodes {
457 if !seen.insert(node.node) {
458 return Err(LayoutError::DuplicateResultNode(node.node));
459 }
460
461 let from = graph
462 .nodes()
463 .get(&node.node)
464 .ok_or(LayoutError::MissingTransactionNode(node.node))?
465 .pos;
466 if from != node.pos {
467 ops.push(GraphOp::SetNodePos {
468 id: node.node,
469 from,
470 to: node.pos,
471 });
472 }
473 }
474
475 Ok(GraphTransaction::from_ops(ops).with_label("Layout graph"))
476 }
477}
478
479#[derive(Debug, thiserror::Error, PartialEq)]
481pub enum LayoutError {
482 #[error("layout engine id is already registered: {0}")]
483 DuplicateLayoutEngine(LayoutEngineId),
484 #[error("layout family id is already registered: {0}")]
485 DuplicateLayoutFamily(LayoutFamilyId),
486 #[error("layout engine metadata is already registered: {0}")]
487 DuplicateLayoutEngineMetadata(LayoutEngineId),
488 #[error("layout engine is not registered: {0}")]
489 MissingLayoutEngine(LayoutEngineId),
490 #[error("layout default node size must be positive and finite: {0:?}")]
491 InvalidDefaultNodeSize(CanvasSize),
492 #[error("layout spacing values must be non-negative and finite: {0:?}")]
493 InvalidSpacing(LayoutSpacing),
494 #[error("layout margin must be non-negative and finite: {0:?}")]
495 InvalidMargin(CanvasSize),
496 #[error("layout node size must be positive and finite for node {node:?}: {size:?}")]
497 InvalidNodeSize { node: NodeId, size: CanvasSize },
498 #[error("layout scope references missing node: {0:?}")]
499 MissingScopeNode(NodeId),
500 #[error("layout edge references missing source port: {0:?}")]
501 MissingSourcePort(EdgeId),
502 #[error("layout edge references missing target port: {0:?}")]
503 MissingTargetPort(EdgeId),
504 #[error("layout edge source port references missing node: {edge:?}")]
505 MissingSourceNode { edge: EdgeId },
506 #[error("layout edge target port references missing node: {edge:?}")]
507 MissingTargetNode { edge: EdgeId },
508 #[error("layout engine did not return a node position for node {0:?}")]
509 MissingNodePosition(NodeId),
510 #[error("layout result contains a duplicate node position for node {0:?}")]
511 DuplicateResultNode(NodeId),
512 #[error("layout result references missing graph node: {0:?}")]
513 MissingTransactionNode(NodeId),
514 #[error("layout engine returned a non-finite node position for node {node:?}: ({x}, {y})")]
515 NonFiniteNodePosition { node: NodeId, x: f64, y: f64 },
516}
517
518pub trait LayoutEngine: Send + Sync {
520 fn id(&self) -> LayoutEngineId;
522
523 fn layout(
525 &self,
526 graph: &Graph,
527 request: &LayoutRequest,
528 context: &LayoutContext,
529 ) -> Result<LayoutResult, LayoutError>;
530}
531
532#[derive(Default, Clone)]
534pub struct LayoutEngineRegistry {
535 engines: BTreeMap<LayoutEngineId, Arc<dyn LayoutEngine>>,
536 families: BTreeMap<LayoutFamilyId, LayoutFamilyMetadata>,
537 metadata: BTreeMap<LayoutEngineId, LayoutEngineMetadata>,
538}
539
540impl fmt::Debug for LayoutEngineRegistry {
541 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
542 f.debug_struct("LayoutEngineRegistry")
543 .field("engines", &self.engine_ids().collect::<Vec<_>>())
544 .field("families", &self.family_ids().collect::<Vec<_>>())
545 .finish()
546 }
547}
548
549impl LayoutEngineRegistry {
550 pub fn new() -> Self {
552 Self::default()
553 }
554
555 pub fn insert<E>(&mut self, engine: E) -> Result<(), LayoutError>
557 where
558 E: LayoutEngine + 'static,
559 {
560 self.insert_shared(Arc::new(engine))
561 }
562
563 pub fn insert_shared(&mut self, engine: Arc<dyn LayoutEngine>) -> Result<(), LayoutError> {
565 let id = engine.id();
566 if self.engines.contains_key(&id) {
567 return Err(LayoutError::DuplicateLayoutEngine(id));
568 }
569 self.engines.insert(id, engine);
570 Ok(())
571 }
572
573 pub fn insert_family(&mut self, family: LayoutFamilyMetadata) -> Result<(), LayoutError> {
575 let id = family.id.clone();
576 if self.families.contains_key(&id) {
577 return Err(LayoutError::DuplicateLayoutFamily(id));
578 }
579 self.families.insert(id, family);
580 Ok(())
581 }
582
583 pub fn insert_metadata(&mut self, metadata: LayoutEngineMetadata) -> Result<(), LayoutError> {
585 let id = metadata.engine.clone();
586 if self.metadata.contains_key(&id) {
587 return Err(LayoutError::DuplicateLayoutEngineMetadata(id));
588 }
589 self.metadata.insert(id, metadata);
590 Ok(())
591 }
592
593 pub fn get(&self, id: &LayoutEngineId) -> Option<&dyn LayoutEngine> {
595 self.engines.get(id).map(Arc::as_ref)
596 }
597
598 pub fn family(&self, id: &LayoutFamilyId) -> Option<&LayoutFamilyMetadata> {
600 self.families.get(id)
601 }
602
603 pub fn metadata(&self, id: &LayoutEngineId) -> Option<&LayoutEngineMetadata> {
605 self.metadata.get(id)
606 }
607
608 pub fn engine_ids(&self) -> impl Iterator<Item = &LayoutEngineId> {
610 self.engines.keys()
611 }
612
613 pub fn family_ids(&self) -> impl Iterator<Item = &LayoutFamilyId> {
615 self.families.keys()
616 }
617
618 pub fn families(&self) -> impl Iterator<Item = &LayoutFamilyMetadata> {
620 self.families.values()
621 }
622
623 pub fn engine_metadata(&self) -> impl Iterator<Item = &LayoutEngineMetadata> {
625 self.metadata.values()
626 }
627
628 pub fn engines_for_family(
630 &self,
631 family: &LayoutFamilyId,
632 ) -> impl Iterator<Item = &LayoutEngineMetadata> {
633 self.metadata
634 .values()
635 .filter(move |metadata| &metadata.family == family)
636 }
637
638 pub fn layout(
640 &self,
641 graph: &Graph,
642 request: &LayoutEngineRequest,
643 context: &LayoutContext,
644 ) -> Result<LayoutResult, LayoutError> {
645 let engine = self
646 .get(&request.engine)
647 .ok_or_else(|| LayoutError::MissingLayoutEngine(request.engine.clone()))?;
648 engine.layout(graph, &request.layout, context)
649 }
650}
651
652pub fn layout_graph_with_engine(
654 graph: &Graph,
655 request: &LayoutEngineRequest,
656 registry: &LayoutEngineRegistry,
657 context: &LayoutContext,
658) -> Result<LayoutResult, LayoutError> {
659 registry.layout(graph, request, context)
660}
661
662pub fn layout_graph_to_transaction_with_engine(
664 graph: &Graph,
665 request: &LayoutEngineRequest,
666 registry: &LayoutEngineRegistry,
667 context: &LayoutContext,
668) -> Result<GraphTransaction, LayoutError> {
669 layout_graph_with_engine(graph, request, registry, context)?.to_transaction(graph)
670}
671
672pub(crate) fn validate_request(graph: &Graph, request: &LayoutRequest) -> Result<(), LayoutError> {
673 if !request.options.default_node_size.is_positive_finite() {
674 return Err(LayoutError::InvalidDefaultNodeSize(
675 request.options.default_node_size,
676 ));
677 }
678 if !is_non_negative_finite_spacing(request.options.spacing) {
679 return Err(LayoutError::InvalidSpacing(request.options.spacing));
680 }
681 if !is_non_negative_finite_size(request.options.margin) {
682 return Err(LayoutError::InvalidMargin(request.options.margin));
683 }
684
685 if let LayoutScope::Nodes { nodes } = &request.scope {
686 for node in nodes {
687 if !graph.nodes().contains_key(node) {
688 return Err(LayoutError::MissingScopeNode(*node));
689 }
690 }
691 }
692
693 Ok(())
694}
695
696fn push_port_owner(graph: &Graph, port: jellyflow_core::PortId, nodes: &mut BTreeSet<NodeId>) {
697 if let Some(port) = graph.ports().get(&port)
698 && graph.nodes().contains_key(&port.node)
699 {
700 nodes.insert(port.node);
701 }
702}
703
704fn push_edge_endpoint_nodes(graph: &Graph, edge: EdgeId, nodes: &mut BTreeSet<NodeId>) {
705 if let Some(edge) = graph.edges().get(&edge) {
706 push_port_owner(graph, edge.from, nodes);
707 push_port_owner(graph, edge.to, nodes);
708 }
709}
710
711fn push_binding_endpoint_nodes(
712 graph: &Graph,
713 endpoint: &BindingEndpoint,
714 nodes: &mut BTreeSet<NodeId>,
715) {
716 let Some(target) = endpoint.graph_local_target() else {
717 return;
718 };
719
720 match target {
721 jellyflow_core::GraphLocalBindingTarget::Graph => {}
722 jellyflow_core::GraphLocalBindingTarget::Node { id } => {
723 if graph.nodes().contains_key(&id) {
724 nodes.insert(id);
725 }
726 }
727 jellyflow_core::GraphLocalBindingTarget::Port { id } => push_port_owner(graph, id, nodes),
728 jellyflow_core::GraphLocalBindingTarget::Edge { id } => {
729 push_edge_endpoint_nodes(graph, id, nodes);
730 }
731 jellyflow_core::GraphLocalBindingTarget::Group { .. }
732 | jellyflow_core::GraphLocalBindingTarget::StickyNote { .. } => {}
733 }
734}
735
736pub(crate) fn resolve_node_size(
737 graph: &Graph,
738 request: &LayoutRequest,
739 context: &LayoutContext,
740 node: NodeId,
741) -> Result<CanvasSize, LayoutError> {
742 let size = graph
743 .nodes()
744 .get(&node)
745 .and_then(|node| node.size)
746 .or_else(|| request.measured_node_sizes.get(&node).copied())
747 .or_else(|| context.measured_node_sizes.get(&node).copied())
748 .unwrap_or(request.options.default_node_size);
749
750 if size.is_positive_finite() {
751 Ok(size)
752 } else {
753 Err(LayoutError::InvalidNodeSize { node, size })
754 }
755}
756
757pub(crate) fn resolve_node_origin(
758 origin: Option<jellyflow_core::NodeOrigin>,
759 request_fallback: (f32, f32),
760 context: &LayoutContext,
761) -> (f32, f32) {
762 let fallback = context.node_origin.unwrap_or(request_fallback);
763 let (x, y) = origin.map(|origin| origin.as_tuple()).unwrap_or(fallback);
764 (normalize_origin_component(x), normalize_origin_component(y))
765}
766
767pub(crate) fn position_from_center(
768 center: CanvasPoint,
769 size: CanvasSize,
770 origin: (f32, f32),
771) -> CanvasPoint {
772 CanvasPoint {
773 x: center.x - size.width * (0.5 - origin.0),
774 y: center.y - size.height * (0.5 - origin.1),
775 }
776}
777
778pub(crate) fn node_rect_from_position(node: &LayoutNodePosition) -> CanvasRect {
779 CanvasRect {
780 origin: CanvasPoint {
781 x: node.center.x - node.size.width * 0.5,
782 y: node.center.y - node.size.height * 0.5,
783 },
784 size: node.size,
785 }
786}
787
788pub(crate) fn union_bounds(bounds: Option<CanvasRect>, next: CanvasRect) -> Option<CanvasRect> {
789 if !next.is_positive_finite() {
790 return bounds;
791 }
792
793 let Some(bounds) = bounds else {
794 return Some(next);
795 };
796
797 let min_x = bounds.origin.x.min(next.origin.x);
798 let min_y = bounds.origin.y.min(next.origin.y);
799 let max_x = (bounds.origin.x + bounds.size.width).max(next.origin.x + next.size.width);
800 let max_y = (bounds.origin.y + bounds.size.height).max(next.origin.y + next.size.height);
801
802 Some(CanvasRect {
803 origin: CanvasPoint { x: min_x, y: min_y },
804 size: CanvasSize {
805 width: max_x - min_x,
806 height: max_y - min_y,
807 },
808 })
809}
810
811fn is_non_negative_finite_spacing(spacing: LayoutSpacing) -> bool {
812 spacing.nodesep.is_finite()
813 && spacing.ranksep.is_finite()
814 && spacing.edgesep.is_finite()
815 && spacing.nodesep >= 0.0
816 && spacing.ranksep >= 0.0
817 && spacing.edgesep >= 0.0
818}
819
820fn is_non_negative_finite_size(size: CanvasSize) -> bool {
821 size.is_finite() && size.width >= 0.0 && size.height >= 0.0
822}
823
824fn normalize_origin_component(component: f32) -> f32 {
825 if component.is_finite() {
826 component.clamp(0.0, 1.0)
827 } else {
828 0.0
829 }
830}