1use crate::model::{BlockDiagramLayout, Bounds, LayoutEdge, LayoutLabel, LayoutNode, LayoutPoint};
2use crate::text::{TextMeasurer, TextStyle, WrapMode};
3use crate::{Error, Result};
4use merman_core::MAX_DIAGRAM_NESTING_DEPTH;
5use serde::Deserialize;
6use serde_json::Value;
7use std::collections::HashMap;
8
9#[derive(Debug, Clone, Deserialize)]
10pub(crate) struct BlockDiagramModel {
11 #[allow(dead_code)]
13 #[serde(default)]
14 pub blocks: Vec<BlockNode>,
15 #[serde(default, rename = "blocksFlat")]
16 pub blocks_flat: Vec<BlockNode>,
17 #[serde(default)]
18 pub edges: Vec<BlockEdge>,
19 #[allow(dead_code)]
20 #[serde(default)]
21 pub warnings: Vec<String>,
22 #[allow(dead_code)]
23 #[serde(default)]
24 pub classes: HashMap<String, BlockClassDef>,
25}
26
27#[derive(Debug, Clone, Deserialize)]
28pub(crate) struct BlockClassDef {
29 #[allow(dead_code)]
30 pub id: String,
31 #[allow(dead_code)]
32 #[serde(default)]
33 pub styles: Vec<String>,
34 #[allow(dead_code)]
35 #[serde(default, rename = "textStyles")]
36 pub text_styles: Vec<String>,
37}
38
39#[derive(Debug, Clone, Deserialize)]
40pub(crate) struct BlockNode {
41 pub id: String,
42 #[serde(default)]
43 pub label: String,
44 #[serde(default, rename = "type")]
45 pub block_type: String,
46 #[serde(default)]
47 pub children: Vec<BlockNode>,
48 #[serde(default)]
49 pub columns: Option<i64>,
50 #[serde(default, rename = "widthInColumns")]
51 pub width_in_columns: Option<i64>,
52 #[allow(dead_code)]
53 #[serde(default)]
54 pub width: Option<i64>,
55 #[serde(default)]
56 pub classes: Vec<String>,
57 #[allow(dead_code)]
58 #[serde(default)]
59 pub styles: Vec<String>,
60 #[serde(default)]
61 pub directions: Vec<String>,
62}
63
64#[derive(Debug, Clone, Deserialize)]
65pub(crate) struct BlockEdge {
66 pub id: String,
67 pub start: String,
68 pub end: String,
69 #[serde(default, rename = "arrowTypeEnd")]
70 pub arrow_type_end: Option<String>,
71 #[serde(default, rename = "arrowTypeStart")]
72 pub arrow_type_start: Option<String>,
73 #[serde(default)]
74 pub label: String,
75}
76
77#[derive(Debug, Clone)]
78struct SizedBlock {
79 id: String,
80 block_type: String,
81 children: Vec<SizedBlock>,
82 columns: i64,
83 width_in_columns: i64,
84 width: f64,
85 height: f64,
86 label_width: f64,
87 label_height: f64,
88 x: f64,
89 y: f64,
90}
91
92fn json_f64(v: &Value) -> Option<f64> {
93 v.as_f64()
94 .or_else(|| v.as_i64().map(|n| n as f64))
95 .or_else(|| v.as_u64().map(|n| n as f64))
96}
97
98fn config_f64(cfg: &Value, path: &[&str]) -> Option<f64> {
99 let mut cur = cfg;
100 for key in path {
101 cur = cur.get(*key)?;
102 }
103 json_f64(cur)
104}
105
106fn config_string(cfg: &Value, path: &[&str]) -> Option<String> {
107 let mut cur = cfg;
108 for key in path {
109 cur = cur.get(*key)?;
110 }
111 cur.as_str().map(|s| s.to_string()).or_else(|| {
112 cur.as_array()
113 .and_then(|values| values.first()?.as_str())
114 .map(|s| s.to_string())
115 })
116}
117
118fn parse_css_px_to_f64(s: &str) -> Option<f64> {
119 let raw = s.trim().trim_end_matches(';').trim();
120 let raw = raw.trim_end_matches("!important").trim();
121 let raw = raw.strip_suffix("px").unwrap_or(raw).trim();
122 raw.parse::<f64>().ok().filter(|value| value.is_finite())
123}
124
125fn config_f64_css_px(cfg: &Value, path: &[&str]) -> Option<f64> {
126 config_f64(cfg, path).or_else(|| {
127 let raw = config_string(cfg, path)?;
128 parse_css_px_to_f64(&raw)
129 })
130}
131
132fn decode_block_label_html(raw: &str) -> String {
133 raw.replace(" ", "\u{00A0}")
134}
135
136pub(crate) fn block_label_is_effectively_empty(text: &str) -> bool {
137 !text.is_empty()
138 && text
139 .chars()
140 .all(|ch| ch != '\u{00A0}' && ch.is_whitespace())
141}
142
143#[derive(Debug, Clone, Copy)]
144pub(crate) struct BlockArrowPoint {
145 pub(crate) x: f64,
146 pub(crate) y: f64,
147}
148
149pub(crate) fn block_arrow_points(
150 directions: &[String],
151 bbox_w: f64,
152 bbox_h: f64,
153 node_padding: f64,
154) -> Vec<BlockArrowPoint> {
155 fn expand_and_dedup(directions: &[String]) -> std::collections::BTreeSet<String> {
156 let mut out = std::collections::BTreeSet::new();
157 for d in directions {
158 match d.trim() {
159 "x" => {
160 out.insert("right".to_string());
161 out.insert("left".to_string());
162 }
163 "y" => {
164 out.insert("up".to_string());
165 out.insert("down".to_string());
166 }
167 other if !other.is_empty() => {
168 out.insert(other.to_string());
169 }
170 _ => {}
171 }
172 }
173 out
174 }
175
176 let dirs = expand_and_dedup(directions);
177 let height = bbox_h + 2.0 * node_padding;
178 let midpoint = height / 2.0;
179 let width = bbox_w + 2.0 * midpoint + node_padding;
180 let pad = node_padding / 2.0;
181
182 let has = |name: &str| dirs.contains(name);
183
184 if has("right") && has("left") && has("up") && has("down") {
185 return vec![
186 BlockArrowPoint { x: 0.0, y: 0.0 },
187 BlockArrowPoint {
188 x: midpoint,
189 y: 0.0,
190 },
191 BlockArrowPoint {
192 x: width / 2.0,
193 y: 2.0 * pad,
194 },
195 BlockArrowPoint {
196 x: width - midpoint,
197 y: 0.0,
198 },
199 BlockArrowPoint { x: width, y: 0.0 },
200 BlockArrowPoint {
201 x: width,
202 y: -height / 3.0,
203 },
204 BlockArrowPoint {
205 x: width + 2.0 * pad,
206 y: -height / 2.0,
207 },
208 BlockArrowPoint {
209 x: width,
210 y: (-2.0 * height) / 3.0,
211 },
212 BlockArrowPoint {
213 x: width,
214 y: -height,
215 },
216 BlockArrowPoint {
217 x: width - midpoint,
218 y: -height,
219 },
220 BlockArrowPoint {
221 x: width / 2.0,
222 y: -height - 2.0 * pad,
223 },
224 BlockArrowPoint {
225 x: midpoint,
226 y: -height,
227 },
228 BlockArrowPoint { x: 0.0, y: -height },
229 BlockArrowPoint {
230 x: 0.0,
231 y: (-2.0 * height) / 3.0,
232 },
233 BlockArrowPoint {
234 x: -2.0 * pad,
235 y: -height / 2.0,
236 },
237 BlockArrowPoint {
238 x: 0.0,
239 y: -height / 3.0,
240 },
241 ];
242 }
243 if has("right") && has("left") && has("up") {
244 return vec![
245 BlockArrowPoint {
246 x: midpoint,
247 y: 0.0,
248 },
249 BlockArrowPoint {
250 x: width - midpoint,
251 y: 0.0,
252 },
253 BlockArrowPoint {
254 x: width,
255 y: -height / 2.0,
256 },
257 BlockArrowPoint {
258 x: width - midpoint,
259 y: -height,
260 },
261 BlockArrowPoint {
262 x: midpoint,
263 y: -height,
264 },
265 BlockArrowPoint {
266 x: 0.0,
267 y: -height / 2.0,
268 },
269 ];
270 }
271 if has("right") && has("left") && has("down") {
272 return vec![
273 BlockArrowPoint { x: 0.0, y: 0.0 },
274 BlockArrowPoint {
275 x: midpoint,
276 y: -height,
277 },
278 BlockArrowPoint {
279 x: width - midpoint,
280 y: -height,
281 },
282 BlockArrowPoint { x: width, y: 0.0 },
283 ];
284 }
285 if has("right") && has("up") && has("down") {
286 return vec![
287 BlockArrowPoint { x: 0.0, y: 0.0 },
288 BlockArrowPoint {
289 x: width,
290 y: -midpoint,
291 },
292 BlockArrowPoint {
293 x: width,
294 y: -height + midpoint,
295 },
296 BlockArrowPoint { x: 0.0, y: -height },
297 ];
298 }
299 if has("left") && has("up") && has("down") {
300 return vec![
301 BlockArrowPoint { x: width, y: 0.0 },
302 BlockArrowPoint {
303 x: 0.0,
304 y: -midpoint,
305 },
306 BlockArrowPoint {
307 x: 0.0,
308 y: -height + midpoint,
309 },
310 BlockArrowPoint {
311 x: width,
312 y: -height,
313 },
314 ];
315 }
316 if has("right") && has("left") {
317 return vec![
318 BlockArrowPoint {
319 x: midpoint,
320 y: 0.0,
321 },
322 BlockArrowPoint {
323 x: midpoint,
324 y: -pad,
325 },
326 BlockArrowPoint {
327 x: width - midpoint,
328 y: -pad,
329 },
330 BlockArrowPoint {
331 x: width - midpoint,
332 y: 0.0,
333 },
334 BlockArrowPoint {
335 x: width,
336 y: -height / 2.0,
337 },
338 BlockArrowPoint {
339 x: width - midpoint,
340 y: -height,
341 },
342 BlockArrowPoint {
343 x: width - midpoint,
344 y: -height + pad,
345 },
346 BlockArrowPoint {
347 x: midpoint,
348 y: -height + pad,
349 },
350 BlockArrowPoint {
351 x: midpoint,
352 y: -height,
353 },
354 BlockArrowPoint {
355 x: 0.0,
356 y: -height / 2.0,
357 },
358 ];
359 }
360 if has("up") && has("down") {
361 return vec![
362 BlockArrowPoint {
363 x: width / 2.0,
364 y: 0.0,
365 },
366 BlockArrowPoint { x: 0.0, y: -pad },
367 BlockArrowPoint {
368 x: midpoint,
369 y: -pad,
370 },
371 BlockArrowPoint {
372 x: midpoint,
373 y: -height + pad,
374 },
375 BlockArrowPoint {
376 x: 0.0,
377 y: -height + pad,
378 },
379 BlockArrowPoint {
380 x: width / 2.0,
381 y: -height,
382 },
383 BlockArrowPoint {
384 x: width,
385 y: -height + pad,
386 },
387 BlockArrowPoint {
388 x: width - midpoint,
389 y: -height + pad,
390 },
391 BlockArrowPoint {
392 x: width - midpoint,
393 y: -pad,
394 },
395 BlockArrowPoint { x: width, y: -pad },
396 ];
397 }
398 if has("right") && has("up") {
399 return vec![
400 BlockArrowPoint { x: 0.0, y: 0.0 },
401 BlockArrowPoint {
402 x: width,
403 y: -midpoint,
404 },
405 BlockArrowPoint { x: 0.0, y: -height },
406 ];
407 }
408 if has("right") && has("down") {
409 return vec![
410 BlockArrowPoint { x: 0.0, y: 0.0 },
411 BlockArrowPoint { x: width, y: 0.0 },
412 BlockArrowPoint { x: 0.0, y: -height },
413 ];
414 }
415 if has("left") && has("up") {
416 return vec![
417 BlockArrowPoint { x: width, y: 0.0 },
418 BlockArrowPoint {
419 x: 0.0,
420 y: -midpoint,
421 },
422 BlockArrowPoint {
423 x: width,
424 y: -height,
425 },
426 ];
427 }
428 if has("left") && has("down") {
429 return vec![
430 BlockArrowPoint { x: width, y: 0.0 },
431 BlockArrowPoint { x: 0.0, y: 0.0 },
432 BlockArrowPoint {
433 x: width,
434 y: -height,
435 },
436 ];
437 }
438 if has("right") {
439 return vec![
440 BlockArrowPoint {
441 x: midpoint,
442 y: -pad,
443 },
444 BlockArrowPoint {
445 x: midpoint,
446 y: -pad,
447 },
448 BlockArrowPoint {
449 x: width - midpoint,
450 y: -pad,
451 },
452 BlockArrowPoint {
453 x: width - midpoint,
454 y: 0.0,
455 },
456 BlockArrowPoint {
457 x: width,
458 y: -height / 2.0,
459 },
460 BlockArrowPoint {
461 x: width - midpoint,
462 y: -height,
463 },
464 BlockArrowPoint {
465 x: width - midpoint,
466 y: -height + pad,
467 },
468 BlockArrowPoint {
469 x: midpoint,
470 y: -height + pad,
471 },
472 BlockArrowPoint {
473 x: midpoint,
474 y: -height + pad,
475 },
476 ];
477 }
478 if has("left") {
479 return vec![
480 BlockArrowPoint {
481 x: midpoint,
482 y: 0.0,
483 },
484 BlockArrowPoint {
485 x: midpoint,
486 y: -pad,
487 },
488 BlockArrowPoint {
489 x: width - midpoint,
490 y: -pad,
491 },
492 BlockArrowPoint {
493 x: width - midpoint,
494 y: -height + pad,
495 },
496 BlockArrowPoint {
497 x: midpoint,
498 y: -height + pad,
499 },
500 BlockArrowPoint {
501 x: midpoint,
502 y: -height,
503 },
504 BlockArrowPoint {
505 x: 0.0,
506 y: -height / 2.0,
507 },
508 ];
509 }
510 if has("up") {
511 return vec![
512 BlockArrowPoint {
513 x: midpoint,
514 y: -pad,
515 },
516 BlockArrowPoint {
517 x: midpoint,
518 y: -height + pad,
519 },
520 BlockArrowPoint {
521 x: 0.0,
522 y: -height + pad,
523 },
524 BlockArrowPoint {
525 x: width / 2.0,
526 y: -height,
527 },
528 BlockArrowPoint {
529 x: width,
530 y: -height + pad,
531 },
532 BlockArrowPoint {
533 x: width - midpoint,
534 y: -height + pad,
535 },
536 BlockArrowPoint {
537 x: width - midpoint,
538 y: -pad,
539 },
540 ];
541 }
542 if has("down") {
543 return vec![
544 BlockArrowPoint {
545 x: width / 2.0,
546 y: 0.0,
547 },
548 BlockArrowPoint { x: 0.0, y: -pad },
549 BlockArrowPoint {
550 x: midpoint,
551 y: -pad,
552 },
553 BlockArrowPoint {
554 x: midpoint,
555 y: -height + pad,
556 },
557 BlockArrowPoint {
558 x: width - midpoint,
559 y: -height + pad,
560 },
561 BlockArrowPoint {
562 x: width - midpoint,
563 y: -pad,
564 },
565 BlockArrowPoint { x: width, y: -pad },
566 ];
567 }
568
569 vec![BlockArrowPoint { x: 0.0, y: 0.0 }]
570}
571
572fn polygon_bounds(points: &[BlockArrowPoint]) -> (f64, f64) {
573 if points.is_empty() {
574 return (0.0, 0.0);
575 }
576
577 let mut min_x = points[0].x;
578 let mut max_x = points[0].x;
579 let mut min_y = points[0].y;
580 let mut max_y = points[0].y;
581 for point in &points[1..] {
582 min_x = min_x.min(point.x);
583 max_x = max_x.max(point.x);
584 min_y = min_y.min(point.y);
585 max_y = max_y.max(point.y);
586 }
587
588 ((max_x - min_x).max(0.0), (max_y - min_y).max(0.0))
589}
590
591fn block_shape_size(
592 block_type: &str,
593 directions: &[String],
594 label_width: f64,
595 label_height: f64,
596 padding: f64,
597 has_label: bool,
598) -> Option<(f64, f64)> {
599 let rect_w = (label_width + padding).max(1.0);
600 let rect_h = (label_height + padding).max(1.0);
601
602 match block_type {
603 "composite" => has_label.then(|| (label_width.max(1.0), (label_height + padding).max(1.0))),
604 "group" => has_label.then(|| (rect_w, rect_h)),
605 "space" => None,
606 "circle" => Some((rect_w, rect_w)),
607 "doublecircle" => {
608 let outer_diameter = rect_w + 10.0;
609 Some((outer_diameter, outer_diameter))
610 }
611 "stadium" => Some(((label_width + rect_h / 4.0 + padding).max(1.0), rect_h)),
612 "cylinder" => {
613 let rx = rect_w / 2.0;
614 let ry = rx / (2.5 + rect_w / 50.0);
615 let body_h = (label_height + ry + padding).max(1.0);
616 Some((rect_w, body_h + 2.0 * ry))
617 }
618 "diamond" => {
619 let side = (rect_w + rect_h).max(1.0);
620 Some((side, side))
621 }
622 "hexagon" => {
623 let shoulder = rect_h / 4.0;
624 Some(((label_width + 2.0 * shoulder + padding).max(1.0), rect_h))
625 }
626 "rect_left_inv_arrow" => Some((rect_w + rect_h / 2.0, rect_h)),
627 "subroutine" => Some((rect_w + 16.0, rect_h)),
628 "lean_right" | "trapezoid" | "inv_trapezoid" => {
629 Some((rect_w + (2.0 * rect_h) / 3.0, rect_h))
630 }
631 "lean_left" => Some((rect_w + rect_h / 3.0, rect_h)),
632 "block_arrow" => Some(polygon_bounds(&block_arrow_points(
633 directions,
634 label_width,
635 label_height,
636 padding,
637 ))),
638 _ => Some((rect_w, rect_h)),
639 }
640}
641
642fn to_sized_block(
643 node: &BlockNode,
644 padding: f64,
645 measurer: &dyn TextMeasurer,
646 text_style: &TextStyle,
647) -> SizedBlock {
648 let columns = node.columns.unwrap_or(-1);
649 let width_in_columns = node.width_in_columns.unwrap_or(1).max(1);
650
651 let mut width = 0.0;
652 let mut height = 0.0;
653
654 let label_decoded = decode_block_label_html(&node.label);
660 let label_effectively_empty = block_label_is_effectively_empty(&label_decoded);
661 let (label_width, label_height) = if label_effectively_empty {
662 (0.0, 0.0)
663 } else {
664 let label_bbox_html =
665 measurer.measure_wrapped(&label_decoded, text_style, None, WrapMode::HtmlLike);
666 let label_bbox_svg =
667 measurer.measure_wrapped(&label_decoded, text_style, None, WrapMode::SvgLike);
668 (
669 label_bbox_html.width.max(0.0),
670 crate::generated::block_text_overrides_11_12_2::lookup_html_height_px(
671 text_style.font_size,
672 &label_decoded,
673 )
674 .unwrap_or(label_bbox_svg.height.max(0.0)),
675 )
676 };
677 let shape_label_height = label_height;
678
679 if let Some((computed_width, computed_height)) = block_shape_size(
680 node.block_type.as_str(),
681 &node.directions,
682 label_width,
683 shape_label_height,
684 padding,
685 !label_effectively_empty && !label_decoded.trim().is_empty(),
686 ) {
687 width = computed_width;
688 height = computed_height;
689 }
690
691 let children = node
692 .children
693 .iter()
694 .map(|c| to_sized_block(c, padding, measurer, text_style))
695 .collect::<Vec<_>>();
696
697 SizedBlock {
698 id: node.id.clone(),
699 block_type: node.block_type.clone(),
700 children,
701 columns,
702 width_in_columns,
703 width,
704 height,
705 label_width,
706 label_height,
707 x: 0.0,
708 y: 0.0,
709 }
710}
711
712fn get_max_child_size(block: &SizedBlock) -> (f64, f64) {
713 let mut max_width = 0.0;
714 let mut max_height = 0.0;
715 for child in &block.children {
716 if child.block_type == "space" {
717 continue;
718 }
719 if child.width > max_width {
720 max_width = child.width / (block.width_in_columns as f64);
721 }
722 if child.height > max_height {
723 max_height = child.height;
724 }
725 }
726 (max_width, max_height)
727}
728
729fn set_block_sizes(block: &mut SizedBlock, padding: f64, sibling_width: f64, sibling_height: f64) {
730 if block.width <= 0.0 {
731 block.width = sibling_width;
732 block.height = sibling_height;
733 block.x = 0.0;
734 block.y = 0.0;
735 }
736
737 if block.children.is_empty() {
738 return;
739 }
740
741 for child in &mut block.children {
742 set_block_sizes(child, padding, 0.0, 0.0);
743 }
744
745 let (mut max_width, mut max_height) = get_max_child_size(block);
746
747 for child in &mut block.children {
748 child.width = max_width * (child.width_in_columns as f64)
749 + padding * ((child.width_in_columns as f64) - 1.0);
750 child.height = max_height;
751 child.x = 0.0;
752 child.y = 0.0;
753 }
754
755 for child in &mut block.children {
756 set_block_sizes(child, padding, max_width, max_height);
757 }
758
759 let columns = block.columns;
760 let mut num_items = 0i64;
761 for child in &block.children {
762 num_items += child.width_in_columns.max(1);
763 }
764
765 let mut x_size = block.children.len() as i64;
766 if columns > 0 && columns < num_items {
767 x_size = columns;
768 }
769 let y_size = ((num_items as f64) / (x_size.max(1) as f64)).ceil() as i64;
770
771 let mut width = (x_size as f64) * (max_width + padding) + padding;
772 let mut height = (y_size as f64) * (max_height + padding) + padding;
773
774 if width < sibling_width {
775 width = sibling_width;
776 height = sibling_height;
777
778 let child_width = (sibling_width - (x_size as f64) * padding - padding) / (x_size as f64);
779 let child_height = (sibling_height - (y_size as f64) * padding - padding) / (y_size as f64);
780 for child in &mut block.children {
781 child.width = child_width;
782 child.height = child_height;
783 child.x = 0.0;
784 child.y = 0.0;
785 }
786 }
787
788 if width < block.width {
789 width = block.width;
790 let num = if columns > 0 {
791 (block.children.len() as i64).min(columns)
792 } else {
793 block.children.len() as i64
794 };
795 if num > 0 {
796 let child_width = (width - (num as f64) * padding - padding) / (num as f64);
797 for child in &mut block.children {
798 child.width = child_width;
799 }
800 }
801 }
802
803 block.width = width;
804 block.height = height;
805 block.x = 0.0;
806 block.y = 0.0;
807
808 max_width = max_width.max(0.0);
810 max_height = max_height.max(0.0);
811 let _ = (max_width, max_height);
812}
813
814fn calculate_block_position(columns: i64, position: i64) -> (i64, i64) {
815 if columns < 0 {
816 return (position, 0);
817 }
818 if columns == 1 {
819 return (0, position);
820 }
821 (position % columns, position / columns)
822}
823
824fn layout_blocks(block: &mut SizedBlock, padding: f64) {
825 if block.children.is_empty() {
826 return;
827 }
828
829 let columns = block.columns;
830 let mut column_pos = 0i64;
831
832 let mut starting_pos_x = if block.x != 0.0 {
834 block.x + (-block.width / 2.0)
835 } else {
836 -padding
837 };
838 let mut row_pos = 0i64;
839
840 for child in &mut block.children {
841 let (px, py) = calculate_block_position(columns, column_pos);
842
843 if py != row_pos {
844 row_pos = py;
845 starting_pos_x = if block.x != 0.0 {
846 block.x + (-block.width / 2.0)
847 } else {
848 -padding
849 };
850 }
851
852 let half_width = child.width / 2.0;
853 child.x = starting_pos_x + padding + half_width;
854 starting_pos_x = child.x + half_width;
855
856 child.y = block.y - block.height / 2.0
857 + (py as f64) * (child.height + padding)
858 + child.height / 2.0
859 + padding;
860
861 if !child.children.is_empty() {
862 layout_blocks(child, padding);
863 }
864
865 let mut columns_filled = child.width_in_columns.max(1);
866 if columns > 0 {
867 let rem = columns - (column_pos % columns);
868 columns_filled = columns_filled.min(rem.max(1));
869 }
870 column_pos += columns_filled;
871
872 let _ = px;
873 }
874}
875
876fn find_bounds(block: &SizedBlock, b: &mut Bounds) {
877 if block.id != "root" {
878 b.min_x = b.min_x.min(block.x - block.width / 2.0);
879 b.min_y = b.min_y.min(block.y - block.height / 2.0);
880 b.max_x = b.max_x.max(block.x + block.width / 2.0);
881 b.max_y = b.max_y.max(block.y + block.height / 2.0);
882 }
883 for child in &block.children {
884 find_bounds(child, b);
885 }
886}
887
888fn collect_nodes(block: &SizedBlock, out: &mut Vec<LayoutNode>) {
889 if block.id != "root" && block.block_type != "space" {
890 out.push(LayoutNode {
891 id: block.id.clone(),
892 x: block.x,
893 y: block.y,
894 width: block.width,
895 height: block.height,
896 is_cluster: false,
897 label_width: Some(block.label_width.max(0.0)),
898 label_height: Some(block.label_height.max(0.0)),
899 });
900 }
901 for child in &block.children {
902 collect_nodes(child, out);
903 }
904}
905
906pub fn layout_block_diagram(
907 semantic: &Value,
908 effective_config: &Value,
909 measurer: &dyn TextMeasurer,
910) -> Result<BlockDiagramLayout> {
911 let model: BlockDiagramModel = crate::json::from_value_ref(semantic)?;
912 validate_block_model_depth(&model)?;
913 let padding = config_f64(effective_config, &["block", "padding"]).unwrap_or(8.0);
914 let text_style = crate::text::TextStyle {
915 font_family: config_string(effective_config, &["themeVariables", "fontFamily"])
916 .or_else(|| config_string(effective_config, &["fontFamily"]))
917 .or_else(|| Some("\"trebuchet ms\", verdana, arial, sans-serif".to_string())),
918 font_size: config_f64_css_px(effective_config, &["themeVariables", "fontSize"])
919 .or_else(|| config_f64_css_px(effective_config, &["fontSize"]))
920 .unwrap_or(16.0)
921 .max(1.0),
922 font_weight: None,
923 };
924
925 let root = model
926 .blocks_flat
927 .iter()
928 .find(|b| b.id == "root" && b.block_type == "composite")
929 .ok_or_else(|| Error::InvalidModel {
930 message: "missing block root composite".to_string(),
931 })?;
932
933 let mut root = to_sized_block(root, padding, measurer, &text_style);
934 set_block_sizes(&mut root, padding, 0.0, 0.0);
935 layout_blocks(&mut root, padding);
936
937 let mut nodes: Vec<LayoutNode> = Vec::new();
938 collect_nodes(&root, &mut nodes);
939
940 let mut bounds = Bounds {
941 min_x: 0.0,
942 min_y: 0.0,
943 max_x: 0.0,
944 max_y: 0.0,
945 };
946 find_bounds(&root, &mut bounds);
947 let bounds = if nodes.is_empty() { None } else { Some(bounds) };
948
949 let nodes_by_id: HashMap<String, LayoutNode> =
950 nodes.iter().cloned().map(|n| (n.id.clone(), n)).collect();
951
952 let mut edges: Vec<LayoutEdge> = Vec::new();
953 for e in &model.edges {
954 let Some(from) = nodes_by_id.get(&e.start) else {
955 continue;
956 };
957 let Some(to) = nodes_by_id.get(&e.end) else {
958 continue;
959 };
960
961 let start = LayoutPoint {
962 x: from.x,
963 y: from.y,
964 };
965 let end = LayoutPoint { x: to.x, y: to.y };
966 let mid = LayoutPoint {
967 x: start.x + (end.x - start.x) / 2.0,
968 y: start.y + (end.y - start.y) / 2.0,
969 };
970
971 let label = if e.label.trim().is_empty() {
972 None
973 } else {
974 let edge_label = decode_block_label_html(&e.label);
975 let width_metrics =
976 measurer.measure_wrapped(&edge_label, &text_style, None, WrapMode::HtmlLike);
977 let height_metrics =
978 measurer.measure_wrapped(&edge_label, &text_style, None, WrapMode::SvgLike);
979 Some(LayoutLabel {
980 x: mid.x,
981 y: mid.y,
982 width: width_metrics.width.max(1.0),
983 height: crate::generated::block_text_overrides_11_12_2::lookup_html_height_px(
984 text_style.font_size,
985 &edge_label,
986 )
987 .unwrap_or(height_metrics.height.max(1.0)),
988 })
989 };
990
991 edges.push(LayoutEdge {
992 id: e.id.clone(),
993 from: e.start.clone(),
994 to: e.end.clone(),
995 from_cluster: None,
996 to_cluster: None,
997 points: vec![start, mid, end],
998 label,
999 start_label_left: None,
1000 start_label_right: None,
1001 end_label_left: None,
1002 end_label_right: None,
1003 start_marker: e.arrow_type_start.clone(),
1004 end_marker: e.arrow_type_end.clone(),
1005 stroke_dasharray: None,
1006 });
1007 }
1008
1009 Ok(BlockDiagramLayout {
1010 nodes,
1011 edges,
1012 bounds,
1013 })
1014}
1015
1016fn validate_block_model_depth(model: &BlockDiagramModel) -> Result<()> {
1017 let mut stack: Vec<(&BlockNode, usize)> =
1018 model.blocks_flat.iter().map(|block| (block, 1)).collect();
1019 while let Some((block, depth)) = stack.pop() {
1020 if depth > MAX_DIAGRAM_NESTING_DEPTH {
1021 return Err(Error::InvalidModel {
1022 message: format!(
1023 "block diagram nesting depth exceeds maximum of {MAX_DIAGRAM_NESTING_DEPTH}"
1024 ),
1025 });
1026 }
1027 for child in &block.children {
1028 stack.push((child, depth + 1));
1029 }
1030 }
1031 Ok(())
1032}