1use crate::model::{BlockDiagramLayout, Bounds, LayoutEdge, LayoutLabel, LayoutNode, LayoutPoint};
2use crate::text::{TextMeasurer, TextStyle, WrapMode};
3use crate::{Error, Result};
4use serde::Deserialize;
5use serde_json::Value;
6use std::collections::HashMap;
7
8#[derive(Debug, Clone, Deserialize)]
9pub(crate) struct BlockDiagramModel {
10 #[allow(dead_code)]
12 #[serde(default)]
13 pub blocks: Vec<BlockNode>,
14 #[serde(default, rename = "blocksFlat")]
15 pub blocks_flat: Vec<BlockNode>,
16 #[serde(default)]
17 pub edges: Vec<BlockEdge>,
18 #[allow(dead_code)]
19 #[serde(default)]
20 pub warnings: Vec<String>,
21 #[allow(dead_code)]
22 #[serde(default)]
23 pub classes: HashMap<String, BlockClassDef>,
24}
25
26#[derive(Debug, Clone, Deserialize)]
27pub(crate) struct BlockClassDef {
28 #[allow(dead_code)]
29 pub id: String,
30 #[allow(dead_code)]
31 #[serde(default)]
32 pub styles: Vec<String>,
33 #[allow(dead_code)]
34 #[serde(default, rename = "textStyles")]
35 pub text_styles: Vec<String>,
36}
37
38#[derive(Debug, Clone, Deserialize)]
39pub(crate) struct BlockNode {
40 pub id: String,
41 #[serde(default)]
42 pub label: String,
43 #[serde(default, rename = "type")]
44 pub block_type: String,
45 #[serde(default)]
46 pub children: Vec<BlockNode>,
47 #[serde(default)]
48 pub columns: Option<i64>,
49 #[serde(default, rename = "widthInColumns")]
50 pub width_in_columns: Option<i64>,
51 #[serde(default)]
52 pub width: Option<i64>,
53 #[serde(default)]
54 pub classes: Vec<String>,
55 #[allow(dead_code)]
56 #[serde(default)]
57 pub styles: Vec<String>,
58 #[serde(default)]
59 pub directions: Vec<String>,
60}
61
62#[derive(Debug, Clone, Deserialize)]
63pub(crate) struct BlockEdge {
64 pub id: String,
65 pub start: String,
66 pub end: String,
67 #[serde(default, rename = "arrowTypeEnd")]
68 pub arrow_type_end: Option<String>,
69 #[serde(default, rename = "arrowTypeStart")]
70 pub arrow_type_start: Option<String>,
71 #[serde(default)]
72 pub label: String,
73}
74
75#[derive(Debug, Clone)]
76struct SizedBlock {
77 id: String,
78 block_type: String,
79 children: Vec<SizedBlock>,
80 columns: i64,
81 width_in_columns: i64,
82 width: f64,
83 height: f64,
84 x: f64,
85 y: f64,
86}
87
88fn json_f64(v: &Value) -> Option<f64> {
89 v.as_f64()
90 .or_else(|| v.as_i64().map(|n| n as f64))
91 .or_else(|| v.as_u64().map(|n| n as f64))
92}
93
94fn config_f64(cfg: &Value, path: &[&str]) -> Option<f64> {
95 let mut cur = cfg;
96 for key in path {
97 cur = cur.get(*key)?;
98 }
99 json_f64(cur)
100}
101
102fn to_sized_block(
103 node: &BlockNode,
104 padding: f64,
105 measurer: &dyn TextMeasurer,
106 text_style: &TextStyle,
107) -> SizedBlock {
108 let columns = node.columns.unwrap_or(-1);
109 let width_in_columns = node.width_in_columns.or(node.width).unwrap_or(1).max(1);
110
111 let mut width = 0.0;
112 let mut height = 0.0;
113
114 let label_decoded = node.label.replace(" ", "\u{00A0}");
120 let label_bbox_html =
121 measurer.measure_wrapped(&label_decoded, text_style, None, WrapMode::HtmlLike);
122 let label_bbox_svg =
123 measurer.measure_wrapped(&label_decoded, text_style, None, WrapMode::SvgLike);
124
125 match node.block_type.as_str() {
126 "composite" | "group" => {
130 if !label_decoded.trim().is_empty() {
131 width = label_bbox_html.width.max(1.0);
134 height = (label_bbox_svg.height + padding).max(1.0);
135 }
136 }
137 "block_arrow" => {
140 let h = (label_bbox_svg.height + 2.0 * padding).max(1.0);
141 let w = (label_bbox_html.width + h + padding).max(1.0);
142 width = w;
143 height = h;
144 }
145 t if t != "space" => {
147 width = (label_bbox_html.width + padding).max(1.0);
148 height = (label_bbox_svg.height + padding).max(1.0);
149 }
150 _ => {}
151 }
152
153 let children = node
154 .children
155 .iter()
156 .map(|c| to_sized_block(c, padding, measurer, text_style))
157 .collect::<Vec<_>>();
158
159 SizedBlock {
160 id: node.id.clone(),
161 block_type: node.block_type.clone(),
162 children,
163 columns,
164 width_in_columns,
165 width,
166 height,
167 x: 0.0,
168 y: 0.0,
169 }
170}
171
172fn get_max_child_size(block: &SizedBlock) -> (f64, f64) {
173 let mut max_width = 0.0;
174 let mut max_height = 0.0;
175 for child in &block.children {
176 if child.block_type == "space" {
177 continue;
178 }
179 if child.width > max_width {
180 max_width = child.width / (block.width_in_columns as f64);
181 }
182 if child.height > max_height {
183 max_height = child.height;
184 }
185 }
186 (max_width, max_height)
187}
188
189fn set_block_sizes(block: &mut SizedBlock, padding: f64, sibling_width: f64, sibling_height: f64) {
190 if block.width <= 0.0 {
191 block.width = sibling_width;
192 block.height = sibling_height;
193 block.x = 0.0;
194 block.y = 0.0;
195 }
196
197 if block.children.is_empty() {
198 return;
199 }
200
201 for child in &mut block.children {
202 set_block_sizes(child, padding, 0.0, 0.0);
203 }
204
205 let (mut max_width, mut max_height) = get_max_child_size(block);
206
207 for child in &mut block.children {
208 child.width = max_width * (child.width_in_columns as f64)
209 + padding * ((child.width_in_columns as f64) - 1.0);
210 child.height = max_height;
211 child.x = 0.0;
212 child.y = 0.0;
213 }
214
215 for child in &mut block.children {
216 set_block_sizes(child, padding, max_width, max_height);
217 }
218
219 let columns = block.columns;
220 let mut num_items = 0i64;
221 for child in &block.children {
222 num_items += child.width_in_columns.max(1);
223 }
224
225 let mut x_size = block.children.len() as i64;
226 if columns > 0 && columns < num_items {
227 x_size = columns;
228 }
229 let y_size = ((num_items as f64) / (x_size.max(1) as f64)).ceil() as i64;
230
231 let mut width = (x_size as f64) * (max_width + padding) + padding;
232 let mut height = (y_size as f64) * (max_height + padding) + padding;
233
234 if width < sibling_width {
235 width = sibling_width;
236 height = sibling_height;
237
238 let child_width = (sibling_width - (x_size as f64) * padding - padding) / (x_size as f64);
239 let child_height = (sibling_height - (y_size as f64) * padding - padding) / (y_size as f64);
240 for child in &mut block.children {
241 child.width = child_width;
242 child.height = child_height;
243 child.x = 0.0;
244 child.y = 0.0;
245 }
246 }
247
248 if width < block.width {
249 width = block.width;
250 let num = if columns > 0 {
251 (block.children.len() as i64).min(columns)
252 } else {
253 block.children.len() as i64
254 };
255 if num > 0 {
256 let child_width = (width - (num as f64) * padding - padding) / (num as f64);
257 for child in &mut block.children {
258 child.width = child_width;
259 }
260 }
261 }
262
263 block.width = width;
264 block.height = height;
265 block.x = 0.0;
266 block.y = 0.0;
267
268 max_width = max_width.max(0.0);
270 max_height = max_height.max(0.0);
271 let _ = (max_width, max_height);
272}
273
274fn calculate_block_position(columns: i64, position: i64) -> (i64, i64) {
275 if columns < 0 {
276 return (position, 0);
277 }
278 if columns == 1 {
279 return (0, position);
280 }
281 (position % columns, position / columns)
282}
283
284fn layout_blocks(block: &mut SizedBlock, padding: f64) {
285 if block.children.is_empty() {
286 return;
287 }
288
289 let columns = block.columns;
290 let mut column_pos = 0i64;
291
292 let mut starting_pos_x = if block.x != 0.0 {
294 block.x + (-block.width / 2.0)
295 } else {
296 -padding
297 };
298 let mut row_pos = 0i64;
299
300 for child in &mut block.children {
301 let (px, py) = calculate_block_position(columns, column_pos);
302
303 if py != row_pos {
304 row_pos = py;
305 starting_pos_x = if block.x != 0.0 {
306 block.x + (-block.width / 2.0)
307 } else {
308 -padding
309 };
310 }
311
312 let half_width = child.width / 2.0;
313 child.x = starting_pos_x + padding + half_width;
314 starting_pos_x = child.x + half_width;
315
316 child.y = block.y - block.height / 2.0
317 + (py as f64) * (child.height + padding)
318 + child.height / 2.0
319 + padding;
320
321 if !child.children.is_empty() {
322 layout_blocks(child, padding);
323 }
324
325 let mut columns_filled = child.width_in_columns.max(1);
326 if columns > 0 {
327 let rem = columns - (column_pos % columns);
328 columns_filled = columns_filled.min(rem.max(1));
329 }
330 column_pos += columns_filled;
331
332 let _ = px;
333 }
334}
335
336fn find_bounds(block: &SizedBlock, b: &mut Bounds) {
337 if block.id != "root" {
338 b.min_x = b.min_x.min(block.x - block.width / 2.0);
339 b.min_y = b.min_y.min(block.y - block.height / 2.0);
340 b.max_x = b.max_x.max(block.x + block.width / 2.0);
341 b.max_y = b.max_y.max(block.y + block.height / 2.0);
342 }
343 for child in &block.children {
344 find_bounds(child, b);
345 }
346}
347
348fn collect_nodes(block: &SizedBlock, out: &mut Vec<LayoutNode>) {
349 if block.id != "root" && block.block_type != "space" {
350 out.push(LayoutNode {
351 id: block.id.clone(),
352 x: block.x,
353 y: block.y,
354 width: block.width,
355 height: block.height,
356 is_cluster: false,
357 label_width: None,
358 label_height: None,
359 });
360 }
361 for child in &block.children {
362 collect_nodes(child, out);
363 }
364}
365
366pub fn layout_block_diagram(
367 semantic: &Value,
368 effective_config: &Value,
369 measurer: &dyn TextMeasurer,
370) -> Result<BlockDiagramLayout> {
371 let model: BlockDiagramModel = crate::json::from_value_ref(semantic)?;
372
373 let padding = config_f64(effective_config, &["block", "padding"]).unwrap_or(8.0);
374 let text_style = crate::text::TextStyle {
375 font_family: effective_config
376 .get("fontFamily")
377 .and_then(|v| v.as_str())
378 .or_else(|| {
379 effective_config
380 .get("themeVariables")
381 .and_then(|tv| tv.get("fontFamily"))
382 .and_then(|v| v.as_str())
383 })
384 .map(|s| s.to_string())
385 .or_else(|| Some("\"trebuchet ms\", verdana, arial, sans-serif".to_string())),
386 font_size: effective_config
387 .get("fontSize")
388 .and_then(|v| v.as_f64())
389 .unwrap_or(16.0)
390 .max(1.0),
391 font_weight: None,
392 };
393
394 let root = model
395 .blocks_flat
396 .iter()
397 .find(|b| b.id == "root" && b.block_type == "composite")
398 .ok_or_else(|| Error::InvalidModel {
399 message: "missing block root composite".to_string(),
400 })?;
401
402 let mut root = to_sized_block(root, padding, measurer, &text_style);
403 set_block_sizes(&mut root, padding, 0.0, 0.0);
404 layout_blocks(&mut root, padding);
405
406 let mut nodes: Vec<LayoutNode> = Vec::new();
407 collect_nodes(&root, &mut nodes);
408
409 let mut bounds = Bounds {
410 min_x: 0.0,
411 min_y: 0.0,
412 max_x: 0.0,
413 max_y: 0.0,
414 };
415 find_bounds(&root, &mut bounds);
416 let bounds = if nodes.is_empty() { None } else { Some(bounds) };
417
418 let nodes_by_id: HashMap<String, LayoutNode> =
419 nodes.iter().cloned().map(|n| (n.id.clone(), n)).collect();
420
421 let mut edges: Vec<LayoutEdge> = Vec::new();
422 for e in &model.edges {
423 let Some(from) = nodes_by_id.get(&e.start) else {
424 continue;
425 };
426 let Some(to) = nodes_by_id.get(&e.end) else {
427 continue;
428 };
429
430 let start = LayoutPoint {
431 x: from.x,
432 y: from.y,
433 };
434 let end = LayoutPoint { x: to.x, y: to.y };
435 let mid = LayoutPoint {
436 x: start.x + (end.x - start.x) / 2.0,
437 y: start.y + (end.y - start.y) / 2.0,
438 };
439
440 let label = if e.label.trim().is_empty() {
441 None
442 } else {
443 let metrics =
444 measurer.measure_wrapped(&e.label, &TextStyle::default(), None, WrapMode::HtmlLike);
445 Some(LayoutLabel {
446 x: mid.x,
447 y: mid.y,
448 width: metrics.width.max(1.0),
449 height: metrics.height.max(1.0),
450 })
451 };
452
453 edges.push(LayoutEdge {
454 id: e.id.clone(),
455 from: e.start.clone(),
456 to: e.end.clone(),
457 from_cluster: None,
458 to_cluster: None,
459 points: vec![start, mid, end],
460 label,
461 start_label_left: None,
462 start_label_right: None,
463 end_label_left: None,
464 end_label_right: None,
465 start_marker: e.arrow_type_start.clone(),
466 end_marker: e.arrow_type_end.clone(),
467 stroke_dasharray: None,
468 });
469 }
470
471 Ok(BlockDiagramLayout {
472 nodes,
473 edges,
474 bounds,
475 })
476}