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 ctx = ProjectionContext::new(graph, request, context)?;
68    dugong::layout_dagreish(&mut ctx.graph);
69    ctx.into_result()
70}
71
72struct ProjectionContext {
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 ProjectionContext {
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 into_result(self) -> Result<LayoutResult, LayoutError> {
188        let mut positions = Vec::with_capacity(self.nodes.len());
189        let mut bounds = self.bounds;
190
191        for node in self.nodes {
192            let label = self
193                .graph
194                .node(&node.key)
195                .ok_or(LayoutError::MissingNodePosition(node.id))?;
196            let x = label.x.ok_or(LayoutError::MissingNodePosition(node.id))?;
197            let y = label.y.ok_or(LayoutError::MissingNodePosition(node.id))?;
198            if !x.is_finite() || !y.is_finite() {
199                return Err(LayoutError::NonFiniteNodePosition {
200                    node: node.id,
201                    x,
202                    y,
203                });
204            }
205
206            let center = CanvasPoint {
207                x: x as f32,
208                y: y as f32,
209            };
210            let pos = position_from_center(center, node.size, node.origin);
211            let node_position = LayoutNodePosition {
212                node: node.id,
213                pos,
214                center,
215                size: node.size,
216            };
217            bounds = union_bounds(bounds, node_rect_from_position(&node_position));
218            positions.push(node_position);
219        }
220
221        let mut edge_routes = Vec::new();
222        for edge in self.edges {
223            let Some(label) = self
224                .graph
225                .edge(&edge.source, &edge.target, Some(&edge.name))
226            else {
227                continue;
228            };
229            let points = label
230                .points
231                .iter()
232                .map(|point| CanvasPoint {
233                    x: point.x as f32,
234                    y: point.y as f32,
235                })
236                .collect();
237            edge_routes.push(LayoutEdgeRoute {
238                edge: edge.id,
239                points,
240            });
241        }
242
243        Ok(LayoutResult {
244            nodes: positions,
245            edge_routes,
246            bounds,
247        })
248    }
249}
250
251fn graph_label(options: LayoutOptions) -> GraphLabel {
252    GraphLabel {
253        rankdir: options.direction.as_dugong_rankdir(),
254        nodesep: f64::from(options.spacing.nodesep),
255        ranksep: f64::from(options.spacing.ranksep),
256        edgesep: f64::from(options.spacing.edgesep),
257        marginx: f64::from(options.margin.width),
258        marginy: f64::from(options.margin.height),
259        ..GraphLabel::default()
260    }
261}
262
263fn node_key(node: NodeId) -> String {
264    node.0.to_string()
265}
266
267fn edge_key(edge: EdgeId) -> String {
268    edge.0.to_string()
269}