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