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#[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
44pub 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
52pub 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 pub struct DugongBenchmarkWorkspace {
266 workspace: DugongLayoutWorkspace,
267 }
268
269 impl DugongBenchmarkWorkspace {
270 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 pub fn solve(&mut self) {
283 self.workspace.solve();
284 }
285
286 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}