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