1use dendryform_core::{Diagram, Layer, Tier, TierLayout};
4
5use crate::error::LayoutError;
6use crate::geometry::{
7 ConnectorGeometry, ContainerGeometry, FlowLabelsGeometry, HeaderGeometry, LayerGeometry,
8 LayoutPlan, LegendGeometry, NodeGeometry, TierGeometry, ViewportHint,
9};
10
11pub fn compute_layout(diagram: &Diagram) -> Result<LayoutPlan<'_>, LayoutError> {
16 let header = HeaderGeometry {
17 title_text: diagram.header().title().text().to_owned(),
18 title_accent: diagram.header().title().accent().to_owned(),
19 subtitle: diagram.header().subtitle().to_owned(),
20 };
21
22 let layers = compute_layers(diagram.layers(), false)?;
23
24 let legend = LegendGeometry {
25 entries: diagram.legend().to_vec(),
26 };
27
28 Ok(LayoutPlan {
29 viewport: ViewportHint::default(),
30 header,
31 layers,
32 legend,
33 })
34}
35
36fn compute_layers<'a>(
38 layers: &'a [Layer],
39 is_internal: bool,
40) -> Result<Vec<LayerGeometry<'a>>, LayoutError> {
41 let mut result = Vec::with_capacity(layers.len());
42
43 for layer in layers {
44 match layer {
45 Layer::Tier(tier) => {
46 result.push(LayerGeometry::Tier(compute_tier(tier)?));
47 }
48 Layer::Connector(conn) => {
49 result.push(LayerGeometry::Connector(ConnectorGeometry {
50 style: conn.style(),
51 label: conn.label().map(|s| s.to_owned()),
52 is_internal,
53 }));
54 }
55 Layer::FlowLabels(labels) => {
56 result.push(LayerGeometry::FlowLabels(FlowLabelsGeometry {
57 items: labels.items().iter().map(|s| s.to_owned()).collect(),
58 }));
59 }
60 _ => {
61 }
63 }
64 }
65
66 Ok(result)
67}
68
69fn compute_tier(tier: &Tier) -> Result<TierGeometry<'_>, LayoutError> {
71 let columns = resolve_columns(tier.layout(), tier.nodes().len());
72
73 let nodes: Vec<NodeGeometry<'_>> = tier
74 .nodes()
75 .iter()
76 .enumerate()
77 .map(|(i, node)| NodeGeometry {
78 node,
79 grid_column: i % columns,
80 grid_row: i / columns,
81 })
82 .collect();
83
84 let container = if let Some(c) = tier.container() {
85 let nested_layers = compute_layers(c.layers(), true)?;
86 Some(ContainerGeometry {
87 label: c.label().to_owned(),
88 border: c.border(),
89 label_color: c.label_color(),
90 layers: nested_layers,
91 })
92 } else {
93 None
94 };
95
96 Ok(TierGeometry {
97 id: tier.id().clone(),
98 label: tier.label().map(|s| s.to_owned()),
99 layout: tier.layout().clone(),
100 columns,
101 nodes,
102 container,
103 })
104}
105
106fn resolve_columns(layout: &TierLayout, node_count: usize) -> usize {
108 match layout {
109 TierLayout::Single => 1,
110 TierLayout::Grid { columns } => *columns as usize,
111 TierLayout::Auto => node_count.clamp(1, 4),
112 _ => node_count.clamp(1, 4), }
114}
115
116#[cfg(test)]
117mod tests {
118 use super::*;
119 use dendryform_core::{
120 Color, Connector, ConnectorStyle, Container, ContainerBorder, Diagram, DiagramHeader, Edge,
121 FlowLabels, Layer, LegendEntry, Node, NodeId, NodeKind, RawDiagram, Tier, TierLayout,
122 };
123
124 fn test_node(id: &str) -> Node {
125 Node::builder()
126 .id(NodeId::new(id).unwrap())
127 .kind(NodeKind::System)
128 .color(Color::Blue)
129 .icon("◇")
130 .title(id)
131 .description("test node")
132 .build()
133 .unwrap()
134 }
135
136 fn make_diagram(layers: Vec<Layer>, edges: Vec<Edge>, legend: Vec<LegendEntry>) -> Diagram {
137 let raw = RawDiagram {
138 diagram: DiagramHeader::new(Title::new("test", "accent"), "subtitle", "dark"),
139 layers,
140 legend,
141 edges,
142 };
143 Diagram::try_from(raw).unwrap()
144 }
145
146 use dendryform_core::Title;
147
148 #[test]
149 fn test_single_tier_layout() {
150 let diagram = make_diagram(
151 vec![Layer::Tier(Tier::new(
152 NodeId::new("main").unwrap(),
153 vec![test_node("a"), test_node("b"), test_node("c")],
154 ))],
155 vec![],
156 vec![],
157 );
158
159 let plan = compute_layout(&diagram).unwrap();
160 assert_eq!(plan.header.title_text, "test");
161 assert_eq!(plan.header.title_accent, "accent");
162 assert_eq!(plan.header.subtitle, "subtitle");
163 assert_eq!(plan.layers.len(), 1);
164
165 if let LayerGeometry::Tier(tier) = &plan.layers[0] {
166 assert_eq!(tier.nodes.len(), 3);
167 assert_eq!(tier.columns, 3);
169 assert_eq!(tier.nodes[0].grid_column, 0);
170 assert_eq!(tier.nodes[1].grid_column, 1);
171 assert_eq!(tier.nodes[2].grid_column, 2);
172 assert_eq!(tier.nodes[0].grid_row, 0);
173 } else {
174 panic!("expected tier layer");
175 }
176 }
177
178 #[test]
179 fn test_grid_layout_columns() {
180 let mut tier = Tier::new(
181 NodeId::new("grid").unwrap(),
182 vec![
183 test_node("a"),
184 test_node("b"),
185 test_node("c"),
186 test_node("d"),
187 test_node("e"),
188 ],
189 );
190 tier.set_layout(TierLayout::Grid { columns: 3 });
191 let diagram = make_diagram(vec![Layer::Tier(tier)], vec![], vec![]);
192
193 let plan = compute_layout(&diagram).unwrap();
194 if let LayerGeometry::Tier(tier) = &plan.layers[0] {
195 assert_eq!(tier.columns, 3);
196 assert_eq!(tier.nodes[0].grid_column, 0);
198 assert_eq!(tier.nodes[0].grid_row, 0);
199 assert_eq!(tier.nodes[3].grid_column, 0);
200 assert_eq!(tier.nodes[3].grid_row, 1);
201 assert_eq!(tier.nodes[4].grid_column, 1);
202 assert_eq!(tier.nodes[4].grid_row, 1);
203 } else {
204 panic!("expected tier layer");
205 }
206 }
207
208 #[test]
209 fn test_single_layout_one_column() {
210 let mut tier = Tier::new(NodeId::new("single").unwrap(), vec![test_node("a")]);
211 tier.set_layout(TierLayout::Single);
212 let diagram = make_diagram(vec![Layer::Tier(tier)], vec![], vec![]);
213
214 let plan = compute_layout(&diagram).unwrap();
215 if let LayerGeometry::Tier(tier) = &plan.layers[0] {
216 assert_eq!(tier.columns, 1);
217 } else {
218 panic!("expected tier layer");
219 }
220 }
221
222 #[test]
223 fn test_auto_layout_caps_at_four() {
224 let tier = Tier::new(
225 NodeId::new("many").unwrap(),
226 vec![
227 test_node("a"),
228 test_node("b"),
229 test_node("c"),
230 test_node("d"),
231 test_node("e"),
232 test_node("f"),
233 ],
234 );
235 let diagram = make_diagram(vec![Layer::Tier(tier)], vec![], vec![]);
236
237 let plan = compute_layout(&diagram).unwrap();
238 if let LayerGeometry::Tier(tier) = &plan.layers[0] {
239 assert_eq!(tier.columns, 4);
240 } else {
241 panic!("expected tier layer");
242 }
243 }
244
245 #[test]
246 fn test_connector_geometry() {
247 let diagram = make_diagram(
248 vec![
249 Layer::Tier(Tier::new(NodeId::new("top").unwrap(), vec![test_node("a")])),
250 Layer::Connector(Connector::with_label(ConnectorStyle::Line, "HTTPS")),
251 Layer::Tier(Tier::new(
252 NodeId::new("bottom").unwrap(),
253 vec![test_node("b")],
254 )),
255 ],
256 vec![],
257 vec![],
258 );
259
260 let plan = compute_layout(&diagram).unwrap();
261 assert_eq!(plan.layers.len(), 3);
262
263 if let LayerGeometry::Connector(conn) = &plan.layers[1] {
264 assert_eq!(conn.style, ConnectorStyle::Line);
265 assert_eq!(conn.label.as_deref(), Some("HTTPS"));
266 assert!(!conn.is_internal);
267 } else {
268 panic!("expected connector layer");
269 }
270 }
271
272 #[test]
273 fn test_container_nesting() {
274 let container = Container::new(
275 "server",
276 ContainerBorder::Solid,
277 Color::Green,
278 vec![Layer::Tier(Tier::new(
279 NodeId::new("inner").unwrap(),
280 vec![test_node("api")],
281 ))],
282 );
283 let diagram = make_diagram(
284 vec![Layer::Tier(Tier::with_container(
285 NodeId::new("server").unwrap(),
286 container,
287 ))],
288 vec![],
289 vec![],
290 );
291
292 let plan = compute_layout(&diagram).unwrap();
293 if let LayerGeometry::Tier(tier) = &plan.layers[0] {
294 assert!(tier.container.is_some());
295 let c = tier.container.as_ref().unwrap();
296 assert_eq!(c.label, "server");
297 assert_eq!(c.border, ContainerBorder::Solid);
298 assert_eq!(c.label_color, Color::Green);
299 assert_eq!(c.layers.len(), 1);
300
301 if let LayerGeometry::Tier(inner) = &c.layers[0] {
302 assert_eq!(inner.nodes.len(), 1);
303 assert_eq!(inner.nodes[0].node.id().as_str(), "api");
304 } else {
305 panic!("expected inner tier");
306 }
307 } else {
308 panic!("expected tier layer");
309 }
310 }
311
312 #[test]
313 fn test_internal_connector_flag() {
314 let container = Container::new(
315 "server",
316 ContainerBorder::Solid,
317 Color::Green,
318 vec![
319 Layer::Tier(Tier::new(NodeId::new("top").unwrap(), vec![test_node("a")])),
320 Layer::Connector(Connector::new(ConnectorStyle::Dots)),
321 Layer::Tier(Tier::new(
322 NodeId::new("bottom").unwrap(),
323 vec![test_node("b")],
324 )),
325 ],
326 );
327 let diagram = make_diagram(
328 vec![Layer::Tier(Tier::with_container(
329 NodeId::new("server").unwrap(),
330 container,
331 ))],
332 vec![],
333 vec![],
334 );
335
336 let plan = compute_layout(&diagram).unwrap();
337 if let LayerGeometry::Tier(tier) = &plan.layers[0] {
338 let c = tier.container.as_ref().unwrap();
339 if let LayerGeometry::Connector(conn) = &c.layers[1] {
340 assert!(conn.is_internal, "container connectors should be internal");
341 assert_eq!(conn.style, ConnectorStyle::Dots);
342 } else {
343 panic!("expected connector");
344 }
345 } else {
346 panic!("expected tier");
347 }
348 }
349
350 #[test]
351 fn test_flow_labels_geometry() {
352 let diagram = make_diagram(
353 vec![
354 Layer::Tier(Tier::new(NodeId::new("top").unwrap(), vec![test_node("a")])),
355 Layer::FlowLabels(FlowLabels::new(vec![
356 "SQL queries".to_owned(),
357 "cache reads".to_owned(),
358 ])),
359 Layer::Tier(Tier::new(
360 NodeId::new("bottom").unwrap(),
361 vec![test_node("b")],
362 )),
363 ],
364 vec![],
365 vec![],
366 );
367
368 let plan = compute_layout(&diagram).unwrap();
369 if let LayerGeometry::FlowLabels(fl) = &plan.layers[1] {
370 assert_eq!(fl.items.len(), 2);
371 assert_eq!(fl.items[0], "SQL queries");
372 } else {
373 panic!("expected flow labels");
374 }
375 }
376
377 #[test]
378 fn test_legend_geometry() {
379 let diagram = make_diagram(
380 vec![Layer::Tier(Tier::new(
381 NodeId::new("main").unwrap(),
382 vec![test_node("a")],
383 ))],
384 vec![],
385 vec![
386 LegendEntry::new(Color::Blue, "Clients"),
387 LegendEntry::new(Color::Green, "Servers"),
388 ],
389 );
390
391 let plan = compute_layout(&diagram).unwrap();
392 assert_eq!(plan.legend.entries.len(), 2);
393 assert_eq!(plan.legend.entries[0].label(), "Clients");
394 assert_eq!(plan.legend.entries[1].color(), Color::Green);
395 }
396
397 #[test]
398 fn test_viewport_defaults() {
399 let diagram = make_diagram(
400 vec![Layer::Tier(Tier::new(
401 NodeId::new("main").unwrap(),
402 vec![test_node("a")],
403 ))],
404 vec![],
405 vec![],
406 );
407
408 let plan = compute_layout(&diagram).unwrap();
409 assert_eq!(plan.viewport.width, 1100.0);
410 assert_eq!(plan.viewport.padding_x, 32.0);
411 }
412
413 #[test]
414 fn test_taproot_layout() {
415 let yaml = include_str!("../../../examples/taproot/architecture.yaml");
416 let diagram: Diagram = dendryform_parse::parse_yaml(yaml).unwrap();
417 let plan = compute_layout(&diagram).unwrap();
418
419 assert_eq!(plan.header.title_accent, "taproot");
420 assert_eq!(plan.layers.len(), 5);
421 assert_eq!(plan.legend.entries.len(), 6);
422
423 if let LayerGeometry::Tier(tier) = &plan.layers[0] {
425 assert_eq!(tier.nodes.len(), 1);
426 assert_eq!(tier.columns, 1);
427 } else {
428 panic!("expected client tier");
429 }
430
431 assert!(matches!(plan.layers[1], LayerGeometry::Connector(_)));
433
434 if let LayerGeometry::Tier(tier) = &plan.layers[2] {
436 assert!(tier.container.is_some());
437 let c = tier.container.as_ref().unwrap();
438 assert_eq!(c.label, "taproot server · cloud run");
439 assert!(c.layers.len() >= 4);
441 } else {
442 panic!("expected server tier");
443 }
444
445 if let LayerGeometry::FlowLabels(fl) = &plan.layers[3] {
447 assert_eq!(fl.items.len(), 3);
448 } else {
449 panic!("expected flow labels");
450 }
451
452 if let LayerGeometry::Tier(tier) = &plan.layers[4] {
454 assert_eq!(tier.nodes.len(), 3);
455 assert_eq!(tier.columns, 3);
456 } else {
457 panic!("expected external services tier");
458 }
459 }
460
461 #[test]
462 fn test_ai_kasu_layout() {
463 let yaml = include_str!("../../../examples/ai-kasu/architecture.yaml");
464 let diagram: Diagram = dendryform_parse::parse_yaml(yaml).unwrap();
465 let plan = compute_layout(&diagram).unwrap();
466
467 assert_eq!(plan.header.title_accent, "ai-kasu");
468 assert_eq!(plan.layers.len(), 7);
469 assert_eq!(plan.legend.entries.len(), 6);
470
471 if let LayerGeometry::Tier(tier) = &plan.layers[0] {
473 assert_eq!(tier.nodes.len(), 3);
474 assert_eq!(tier.columns, 3);
475 } else {
476 panic!("expected clients tier");
477 }
478
479 assert!(matches!(plan.layers[1], LayerGeometry::Connector(_)));
481
482 if let LayerGeometry::Tier(tier) = &plan.layers[2] {
484 assert!(tier.container.is_some());
485 let c = tier.container.as_ref().unwrap();
486 assert_eq!(c.label, "kasu-server · rust binary");
487 } else {
488 panic!("expected server tier");
489 }
490 }
491
492 #[test]
493 fn test_oxur_lisp_layout() {
494 let yaml = include_str!("../../../examples/oxur-lisp/architecture.yaml");
495 let diagram: Diagram = dendryform_parse::parse_yaml(yaml).unwrap();
496 let plan = compute_layout(&diagram).unwrap();
497
498 assert_eq!(plan.header.title_accent, "oxur");
499 assert_eq!(plan.layers.len(), 9);
500 assert_eq!(plan.legend.entries.len(), 6);
501
502 if let LayerGeometry::Tier(tier) = &plan.layers[0] {
504 assert_eq!(tier.nodes.len(), 1);
505 assert_eq!(tier.columns, 1);
506 } else {
507 panic!("expected source input tier");
508 }
509
510 assert!(matches!(plan.layers[1], LayerGeometry::Connector(_)));
512
513 if let LayerGeometry::Tier(tier) = &plan.layers[2] {
515 assert!(tier.container.is_some());
516 let c = tier.container.as_ref().unwrap();
517 assert_eq!(c.label, "oxur compilation pipeline");
518 } else {
519 panic!("expected pipeline tier");
520 }
521 }
522}