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 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}