Skip to main content

merman_render/
block.rs

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    // Keep the full upstream semantic model shape for future parity work.
12    #[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("&nbsp;", "\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    // Mermaid renders block diagram labels via `labelHelper(...)`, which decodes HTML entities
655    // and measures the resulting HTML content (`getBoundingClientRect()` for width/height).
656    //
657    // Block diagrams frequently use `&nbsp;` placeholders (notably for block arrows), so we must
658    // decode those before measuring; otherwise node widths drift drastically.
659    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    // Keep behavior consistent with Mermaid even when all children were `space`.
809    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    // JS truthiness: treat `0` as falsy (Mermaid uses `block?.size?.x ? ... : -padding`).
833    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}