Skip to main content

jellyflow_layout/
dugong.rs

1use dugong::graphlib::{Graph as DugongGraph, GraphOptions};
2use dugong::{EdgeLabel, GraphLabel, NodeLabel, RankDir};
3use jellyflow_core::{
4    CanvasPoint, CanvasRect, CanvasSize, EdgeId, Graph, GraphTransaction, NodeId,
5};
6
7use crate::engine::{
8    DUGONG_LAYOUT_ENGINE_ID, LayoutContext, LayoutDirection, LayoutEdgeRoute, LayoutEngine,
9    LayoutEngineId, LayoutError, LayoutNodePosition, LayoutOptions, LayoutRequest, LayoutResult,
10    node_rect_from_position, position_from_center, resolve_node_origin, resolve_node_size,
11    union_bounds, validate_request,
12};
13
14impl LayoutDirection {
15    fn as_dugong_rankdir(self) -> RankDir {
16        match self {
17            Self::TopToBottom => RankDir::TB,
18            Self::BottomToTop => RankDir::BT,
19            Self::LeftToRight => RankDir::LR,
20            Self::RightToLeft => RankDir::RL,
21        }
22    }
23}
24
25/// Built-in Dagre-compatible layout engine powered by `dugong`.
26#[derive(Debug, Default, Clone, Copy)]
27pub struct DugongLayoutEngine;
28
29impl LayoutEngine for DugongLayoutEngine {
30    fn id(&self) -> LayoutEngineId {
31        LayoutEngineId::new(DUGONG_LAYOUT_ENGINE_ID)
32    }
33
34    fn layout(
35        &self,
36        graph: &Graph,
37        request: &LayoutRequest,
38        context: &LayoutContext,
39    ) -> Result<LayoutResult, LayoutError> {
40        layout_graph_with_dugong_context(graph, request, context)
41    }
42}
43
44/// Runs a Dagre-compatible layout using the `dugong` backend.
45pub fn layout_graph_with_dugong(
46    graph: &Graph,
47    request: &LayoutRequest,
48) -> Result<LayoutResult, LayoutError> {
49    layout_graph_with_dugong_context(graph, request, &LayoutContext::default())
50}
51
52/// Runs `dugong` and converts the node positions into a Jellyflow transaction.
53pub fn layout_graph_to_transaction_with_dugong(
54    graph: &Graph,
55    request: &LayoutRequest,
56) -> Result<GraphTransaction, LayoutError> {
57    layout_graph_with_dugong(graph, request)?.to_transaction(graph)
58}
59
60fn layout_graph_with_dugong_context(
61    graph: &Graph,
62    request: &LayoutRequest,
63    context: &LayoutContext,
64) -> Result<LayoutResult, LayoutError> {
65    validate_request(graph, request)?;
66
67    let mut workspace = DugongLayoutWorkspace::new(graph, request, context)?;
68    workspace.solve();
69    workspace.into_result()
70}
71
72struct DugongLayoutWorkspace {
73    graph: DugongGraph<NodeLabel, EdgeLabel, GraphLabel>,
74    nodes: Vec<ProjectedNode>,
75    edges: Vec<ProjectedEdge>,
76    bounds: Option<CanvasRect>,
77}
78
79struct ProjectedNode {
80    id: NodeId,
81    key: String,
82    size: CanvasSize,
83    origin: (f32, f32),
84}
85
86struct ProjectedEdge {
87    id: EdgeId,
88    source: String,
89    target: String,
90    name: String,
91}
92
93impl DugongLayoutWorkspace {
94    fn new(
95        graph: &Graph,
96        request: &LayoutRequest,
97        context: &LayoutContext,
98    ) -> Result<Self, LayoutError> {
99        let mut projected = DugongGraph::with_capacity(
100            GraphOptions {
101                multigraph: true,
102                ..GraphOptions::default()
103            },
104            graph.nodes().len(),
105            graph.edges().len(),
106        );
107        projected.set_graph(graph_label(request.options));
108
109        let mut node_keys = std::collections::BTreeMap::new();
110        let mut nodes = Vec::new();
111
112        for (id, node) in graph.nodes() {
113            if node.hidden || !request.scope.contains(*id) {
114                continue;
115            }
116
117            let size = resolve_node_size(graph, request, context, *id)?;
118            let key = node_key(*id);
119            projected.set_node(
120                key.clone(),
121                NodeLabel {
122                    width: f64::from(size.width),
123                    height: f64::from(size.height),
124                    ..NodeLabel::default()
125                },
126            );
127            node_keys.insert(*id, key.clone());
128            nodes.push(ProjectedNode {
129                id: *id,
130                key,
131                size,
132                origin: resolve_node_origin(node.origin, request.options.node_origin, context),
133            });
134        }
135
136        let mut edges = Vec::new();
137        for (id, edge) in graph.edges() {
138            if edge.hidden {
139                continue;
140            }
141
142            let source_port = graph
143                .ports()
144                .get(&edge.from)
145                .ok_or(LayoutError::MissingSourcePort(*id))?;
146            let target_port = graph
147                .ports()
148                .get(&edge.to)
149                .ok_or(LayoutError::MissingTargetPort(*id))?;
150            if !graph.nodes().contains_key(&source_port.node) {
151                return Err(LayoutError::MissingSourceNode { edge: *id });
152            }
153            if !graph.nodes().contains_key(&target_port.node) {
154                return Err(LayoutError::MissingTargetNode { edge: *id });
155            }
156
157            let Some(source) = node_keys.get(&source_port.node).cloned() else {
158                continue;
159            };
160            let Some(target) = node_keys.get(&target_port.node).cloned() else {
161                continue;
162            };
163
164            let name = edge_key(*id);
165            projected.set_edge_named(
166                source.clone(),
167                target.clone(),
168                Some(name.clone()),
169                Some(EdgeLabel::default()),
170            );
171            edges.push(ProjectedEdge {
172                id: *id,
173                source,
174                target,
175                name,
176            });
177        }
178
179        Ok(Self {
180            graph: projected,
181            nodes,
182            edges,
183            bounds: None,
184        })
185    }
186
187    fn solve(&mut self) {
188        dugong::layout_dagreish(&mut self.graph);
189    }
190
191    fn into_result(self) -> Result<LayoutResult, LayoutError> {
192        let mut positions = Vec::with_capacity(self.nodes.len());
193        let mut bounds = self.bounds;
194
195        for node in self.nodes {
196            let label = self
197                .graph
198                .node(&node.key)
199                .ok_or(LayoutError::MissingNodePosition(node.id))?;
200            let x = label.x.ok_or(LayoutError::MissingNodePosition(node.id))?;
201            let y = label.y.ok_or(LayoutError::MissingNodePosition(node.id))?;
202            if !x.is_finite() || !y.is_finite() {
203                return Err(LayoutError::NonFiniteNodePosition {
204                    node: node.id,
205                    x,
206                    y,
207                });
208            }
209
210            let center = CanvasPoint {
211                x: x as f32,
212                y: y as f32,
213            };
214            let pos = position_from_center(center, node.size, node.origin);
215            let node_position = LayoutNodePosition {
216                node: node.id,
217                pos,
218                center,
219                size: node.size,
220            };
221            bounds = union_bounds(bounds, node_rect_from_position(&node_position));
222            positions.push(node_position);
223        }
224
225        let mut edge_routes = Vec::new();
226        for edge in self.edges {
227            let Some(label) = self
228                .graph
229                .edge(&edge.source, &edge.target, Some(&edge.name))
230            else {
231                continue;
232            };
233            let points = label
234                .points
235                .iter()
236                .map(|point| CanvasPoint {
237                    x: point.x as f32,
238                    y: point.y as f32,
239                })
240                .collect();
241            edge_routes.push(LayoutEdgeRoute {
242                edge: edge.id,
243                points,
244            });
245        }
246
247        Ok(LayoutResult {
248            nodes: positions,
249            edge_routes,
250            bounds,
251        })
252    }
253}
254
255#[cfg(feature = "benchmark-internals")]
256#[doc(hidden)]
257pub mod benchmark_internals {
258    use jellyflow_core::Graph;
259
260    use crate::engine::{LayoutContext, LayoutError, LayoutRequest, LayoutResult};
261
262    use super::DugongLayoutWorkspace;
263
264    /// Projected `dugong` state used by internal benchmarks.
265    pub struct DugongBenchmarkWorkspace {
266        workspace: DugongLayoutWorkspace,
267    }
268
269    impl DugongBenchmarkWorkspace {
270        /// Builds the `dugong` input graph without running layout.
271        pub fn project(
272            graph: &Graph,
273            request: &LayoutRequest,
274            context: &LayoutContext,
275        ) -> Result<Self, LayoutError> {
276            Ok(Self {
277                workspace: DugongLayoutWorkspace::new(graph, request, context)?,
278            })
279        }
280
281        /// Runs `dugong` layout on the projected graph.
282        pub fn solve(&mut self) {
283            self.workspace.solve();
284        }
285
286        /// Converts solved `dugong` output into Jellyflow layout output.
287        pub fn into_result(self) -> Result<LayoutResult, LayoutError> {
288            self.workspace.into_result()
289        }
290    }
291}
292
293fn graph_label(options: LayoutOptions) -> GraphLabel {
294    GraphLabel {
295        rankdir: options.direction.as_dugong_rankdir(),
296        nodesep: f64::from(options.spacing.nodesep),
297        ranksep: f64::from(options.spacing.ranksep),
298        edgesep: f64::from(options.spacing.edgesep),
299        marginx: f64::from(options.margin.width),
300        marginy: f64::from(options.margin.height),
301        ..GraphLabel::default()
302    }
303}
304
305fn node_key(node: NodeId) -> String {
306    node.0.to_string()
307}
308
309fn edge_key(edge: EdgeId) -> String {
310    edge.0.to_string()
311}