1#![allow(clippy::too_many_arguments)]
2
3use crate::json::from_value_ref;
4use crate::model::{
5 Bounds, C4BoundaryLayout, C4DiagramLayout, C4ImageLayout, C4RelLayout, C4ShapeLayout,
6 C4TextBlockLayout, LayoutPoint,
7};
8use crate::text::{TextMeasurer, TextStyle, WrapMode};
9use crate::{Error, Result};
10use merman_core::MAX_DIAGRAM_NESTING_DEPTH;
11use serde::Deserialize;
12use serde_json::Value;
13use std::collections::HashMap;
14
15#[derive(Debug, Clone, Deserialize)]
16#[serde(untagged)]
17enum C4Text {
18 Wrapped { text: Value },
19 String(String),
20 Value(Value),
21}
22
23impl Default for C4Text {
24 fn default() -> Self {
25 Self::String(String::new())
26 }
27}
28
29impl C4Text {
30 fn as_str(&self) -> &str {
31 match self {
32 Self::Wrapped { text } => text.as_str().unwrap_or(""),
33 Self::String(s) => s.as_str(),
34 Self::Value(v) => v.as_str().unwrap_or(""),
35 }
36 }
37}
38
39#[derive(Debug, Clone, Default, Deserialize)]
40struct C4LayoutConfig {
41 #[serde(default, rename = "c4ShapeInRow")]
42 c4_shape_in_row: i64,
43 #[serde(default, rename = "c4BoundaryInRow")]
44 c4_boundary_in_row: i64,
45}
46
47#[derive(Debug, Clone, Deserialize)]
48struct C4Shape {
49 alias: String,
50 #[serde(default, rename = "parentBoundary")]
51 parent_boundary: String,
52 #[serde(default, rename = "typeC4Shape")]
53 type_c4_shape: C4Text,
54 #[serde(default)]
55 label: C4Text,
56 #[serde(default)]
57 #[allow(dead_code)]
58 wrap: bool,
59 #[serde(default)]
60 #[allow(dead_code)]
61 sprite: Option<Value>,
62 #[serde(default, rename = "type")]
63 ty: Option<C4Text>,
64 #[serde(default)]
65 techn: Option<C4Text>,
66 #[serde(default)]
67 descr: Option<C4Text>,
68}
69
70#[derive(Debug, Clone, Deserialize)]
71struct C4Boundary {
72 alias: String,
73 #[serde(default, rename = "parentBoundary")]
74 parent_boundary: String,
75 #[serde(default)]
76 label: C4Text,
77 #[serde(default, rename = "type")]
78 ty: Option<C4Text>,
79 #[serde(default)]
80 descr: Option<C4Text>,
81 #[serde(default)]
82 #[allow(dead_code)]
83 wrap: Option<bool>,
84 #[serde(default)]
85 #[allow(dead_code)]
86 sprite: Option<Value>,
87}
88
89#[derive(Debug, Clone, Deserialize)]
90struct C4Rel {
91 #[serde(rename = "from")]
92 from_alias: String,
93 #[serde(rename = "to")]
94 to_alias: String,
95 #[serde(rename = "type")]
96 rel_type: String,
97 #[serde(default)]
98 label: C4Text,
99 #[serde(default)]
100 techn: Option<C4Text>,
101 #[serde(default)]
102 descr: Option<C4Text>,
103 #[serde(default)]
104 #[allow(dead_code)]
105 wrap: bool,
106 #[serde(default, rename = "offsetX")]
107 offset_x: Option<i64>,
108 #[serde(default, rename = "offsetY")]
109 offset_y: Option<i64>,
110}
111
112#[derive(Debug, Clone, Deserialize)]
113struct C4Model {
114 #[serde(default, rename = "c4Type")]
115 c4_type: String,
116 #[serde(default)]
117 title: Option<String>,
118 #[serde(default)]
119 wrap: bool,
120 #[serde(default)]
121 layout: C4LayoutConfig,
122 #[serde(default)]
123 shapes: Vec<C4Shape>,
124 #[serde(default)]
125 boundaries: Vec<C4Boundary>,
126 #[serde(default)]
127 rels: Vec<C4Rel>,
128}
129
130fn json_f64(v: &Value) -> Option<f64> {
131 v.as_f64()
132 .or_else(|| v.as_i64().map(|n| n as f64))
133 .or_else(|| v.as_u64().map(|n| n as f64))
134}
135
136fn config_f64(cfg: &Value, path: &[&str]) -> Option<f64> {
137 let mut cur = cfg;
138 for key in path {
139 cur = cur.get(*key)?;
140 }
141 json_f64(cur)
142}
143
144fn config_bool(cfg: &Value, path: &[&str]) -> Option<bool> {
145 let mut cur = cfg;
146 for key in path {
147 cur = cur.get(*key)?;
148 }
149 cur.as_bool()
150}
151
152fn config_string(cfg: &Value, path: &[&str]) -> Option<String> {
153 let mut cur = cfg;
154 for key in path {
155 cur = cur.get(*key)?;
156 }
157 cur.as_str().map(|s| s.to_string())
158}
159
160#[derive(Debug, Clone)]
161struct C4Conf {
162 diagram_margin_x: f64,
163 diagram_margin_y: f64,
164 c4_shape_margin: f64,
165 c4_shape_padding: f64,
166 width: f64,
167 height: f64,
168 wrap: bool,
169 next_line_padding_x: f64,
170 boundary_font_family: Option<String>,
171 boundary_font_size: f64,
172 boundary_font_weight: Option<String>,
173 message_font_family: Option<String>,
174 message_font_size: f64,
175 message_font_weight: Option<String>,
176}
177
178impl C4Conf {
179 fn from_effective_config(effective_config: &Value) -> Self {
180 let message_font_family = config_string(effective_config, &["c4", "messageFontFamily"]);
185 let message_font_size =
186 config_f64(effective_config, &["c4", "messageFontSize"]).unwrap_or(12.0);
187 let message_font_weight = config_string(effective_config, &["c4", "messageFontWeight"]);
188
189 let boundary_font_family = config_string(effective_config, &["c4", "boundaryFontFamily"]);
190 let boundary_font_size =
191 config_f64(effective_config, &["c4", "boundaryFontSize"]).unwrap_or(14.0);
192 let boundary_font_weight = config_string(effective_config, &["c4", "boundaryFontWeight"]);
193
194 Self {
195 diagram_margin_x: config_f64(effective_config, &["c4", "diagramMarginX"])
196 .unwrap_or(50.0),
197 diagram_margin_y: config_f64(effective_config, &["c4", "diagramMarginY"])
198 .unwrap_or(10.0),
199 c4_shape_margin: config_f64(effective_config, &["c4", "c4ShapeMargin"]).unwrap_or(50.0),
200 c4_shape_padding: config_f64(effective_config, &["c4", "c4ShapePadding"])
201 .unwrap_or(20.0),
202 width: config_f64(effective_config, &["c4", "width"]).unwrap_or(216.0),
203 height: config_f64(effective_config, &["c4", "height"]).unwrap_or(60.0),
204 wrap: config_bool(effective_config, &["c4", "wrap"]).unwrap_or(true),
205 next_line_padding_x: config_f64(effective_config, &["c4", "nextLinePaddingX"])
206 .unwrap_or(0.0),
207 boundary_font_family,
208 boundary_font_size,
209 boundary_font_weight,
210 message_font_family,
211 message_font_size,
212 message_font_weight,
213 }
214 }
215
216 fn boundary_font(&self) -> TextStyle {
217 TextStyle {
218 font_family: self.boundary_font_family.clone(),
219 font_size: self.boundary_font_size,
220 font_weight: self.boundary_font_weight.clone(),
221 }
222 }
223
224 fn message_font(&self) -> TextStyle {
225 TextStyle {
226 font_family: self.message_font_family.clone(),
227 font_size: self.message_font_size,
228 font_weight: self.message_font_weight.clone(),
229 }
230 }
231
232 fn c4_shape_font(&self, effective_config: &Value, type_c4_shape: &str) -> TextStyle {
233 let key_family = format!("{type_c4_shape}FontFamily");
234 let key_size = format!("{type_c4_shape}FontSize");
235 let key_weight = format!("{type_c4_shape}FontWeight");
236
237 let font_family = config_string(effective_config, &["c4", &key_family]);
238 let font_size = config_f64(effective_config, &["c4", &key_size]).unwrap_or(14.0);
239 let font_weight = config_string(effective_config, &["c4", &key_weight]);
240
241 TextStyle {
242 font_family,
243 font_size,
244 font_weight,
245 }
246 }
247}
248
249#[derive(Debug, Clone, Copy)]
250struct TextMeasure {
251 width: f64,
252 height: f64,
253 line_count: usize,
254}
255
256fn measure_c4_text(
257 measurer: &dyn TextMeasurer,
258 text: &str,
259 style: &TextStyle,
260 wrap: bool,
261 text_limit_width: f64,
262) -> TextMeasure {
263 fn js_round_pos(v: f64) -> f64 {
267 if !(v.is_finite() && v >= 0.0) {
268 0.0
269 } else {
270 (v + 0.5).floor()
271 }
272 }
273
274 fn c4_svg_bbox_line_height_px(style: &TextStyle) -> f64 {
275 let fs = js_round_pos(style.font_size.max(1.0)) as i64;
285 crate::generated::c4_text_overrides_11_12_2::lookup_c4_svg_bbox_line_height_px(fs)
286 .unwrap_or_else(|| js_round_pos(style.font_size.max(1.0) * 1.1))
287 }
288
289 if wrap {
290 let m = measurer.measure_wrapped(text, style, Some(text_limit_width), WrapMode::SvgLike);
291 return TextMeasure {
292 width: text_limit_width,
293 height: c4_svg_bbox_line_height_px(style) * m.line_count.max(1) as f64,
294 line_count: m.line_count,
295 };
296 }
297
298 let mut width: f64 = 0.0;
299 let lines = crate::text::DeterministicTextMeasurer::normalized_text_lines(text);
300 for line in &lines {
301 let m = measurer.measure(line, style);
302 width = width.max(js_round_pos(m.width));
303 }
304 let height = c4_svg_bbox_line_height_px(style) * lines.len().max(1) as f64;
305 TextMeasure {
306 width,
307 height,
308 line_count: lines.len().max(1),
309 }
310}
311
312#[derive(Debug, Clone, Default)]
313struct BoundsData {
314 startx: Option<f64>,
315 stopx: Option<f64>,
316 starty: Option<f64>,
317 stopy: Option<f64>,
318 width_limit: f64,
319}
320
321#[derive(Debug, Clone, Default)]
322struct BoundsNext {
323 startx: f64,
324 stopx: f64,
325 starty: f64,
326 stopy: f64,
327 cnt: usize,
328}
329
330#[derive(Debug, Clone, Default)]
331struct BoundsState {
332 data: BoundsData,
333 next: BoundsNext,
334}
335
336impl BoundsState {
337 fn set_data(&mut self, startx: f64, stopx: f64, starty: f64, stopy: f64) {
338 self.next.startx = startx;
339 self.data.startx = Some(startx);
340 self.next.stopx = stopx;
341 self.data.stopx = Some(stopx);
342 self.next.starty = starty;
343 self.data.starty = Some(starty);
344 self.next.stopy = stopy;
345 self.data.stopy = Some(stopy);
346 }
347
348 fn bump_last_margin(&mut self, margin: f64) {
349 if let Some(v) = self.data.stopx.as_mut() {
350 *v += margin;
351 }
352 if let Some(v) = self.data.stopy.as_mut() {
353 *v += margin;
354 }
355 }
356
357 fn update_val_opt(target: &mut Option<f64>, val: f64, fun: fn(f64, f64) -> f64) {
358 match target {
359 None => *target = Some(val),
360 Some(existing) => *existing = fun(val, *existing),
361 }
362 }
363
364 fn update_val(target: &mut f64, val: f64, fun: fn(f64, f64) -> f64) {
365 *target = fun(val, *target);
366 }
367
368 fn insert_rect(&mut self, rect: &mut Rect, c4_shape_in_row: usize, conf: &C4Conf) {
369 self.next.cnt += 1;
370
371 let startx = if self.next.startx == self.next.stopx {
372 self.next.stopx + rect.margin
373 } else {
374 self.next.stopx + rect.margin * 2.0
375 };
376 let mut stopx = startx + rect.size.width;
377 let starty = self.next.starty + rect.margin * 2.0;
378 let mut stopy = starty + rect.size.height;
379
380 if startx >= self.data.width_limit
381 || stopx >= self.data.width_limit
382 || self.next.cnt > c4_shape_in_row
383 {
384 let startx2 = self.next.startx + rect.margin + conf.next_line_padding_x;
385 let starty2 = self.next.stopy + rect.margin * 2.0;
386
387 stopx = startx2 + rect.size.width;
388 stopy = starty2 + rect.size.height;
389
390 self.next.stopx = stopx;
391 self.next.starty = self.next.stopy;
392 self.next.stopy = stopy;
393 self.next.cnt = 1;
394
395 rect.origin.x = startx2;
396 rect.origin.y = starty2;
397 } else {
398 rect.origin.x = startx;
399 rect.origin.y = starty;
400 }
401
402 Self::update_val_opt(&mut self.data.startx, rect.origin.x, f64::min);
403 Self::update_val_opt(&mut self.data.starty, rect.origin.y, f64::min);
404 Self::update_val_opt(&mut self.data.stopx, stopx, f64::max);
405 Self::update_val_opt(&mut self.data.stopy, stopy, f64::max);
406
407 Self::update_val(&mut self.next.startx, rect.origin.x, f64::min);
408 Self::update_val(&mut self.next.starty, rect.origin.y, f64::min);
409 Self::update_val(&mut self.next.stopx, stopx, f64::max);
410 Self::update_val(&mut self.next.stopy, stopy, f64::max);
411 }
412}
413
414#[derive(Debug, Clone)]
415struct Rect {
416 origin: merman_core::geom::Point,
417 size: merman_core::geom::Size,
418 margin: f64,
419}
420
421fn has_sprite(v: &Option<Value>) -> bool {
422 v.as_ref().is_some_and(|v| match v {
423 Value::Null => false,
424 Value::Bool(b) => *b,
425 Value::Number(_) => true,
426 Value::String(s) => !s.trim().is_empty(),
427 Value::Array(a) => !a.is_empty(),
428 Value::Object(o) => !o.is_empty(),
429 })
430}
431
432fn intersect_point(from: &Rect, end_point: LayoutPoint) -> LayoutPoint {
433 let x1 = from.origin.x;
434 let y1 = from.origin.y;
435 let x2 = end_point.x;
436 let y2 = end_point.y;
437
438 let from_center_x = x1 + from.size.width / 2.0;
439 let from_center_y = y1 + from.size.height / 2.0;
440
441 let dx = (x1 - x2).abs();
442 let dy = (y1 - y2).abs();
443 let tan_dyx = dy / dx;
444 let from_dyx = from.size.height / from.size.width;
445
446 let mut return_point: Option<LayoutPoint> = None;
447
448 if y1 == y2 && x1 < x2 {
449 return_point = Some(LayoutPoint {
450 x: x1 + from.size.width,
451 y: from_center_y,
452 });
453 } else if y1 == y2 && x1 > x2 {
454 return_point = Some(LayoutPoint {
455 x: x1,
456 y: from_center_y,
457 });
458 } else if x1 == x2 && y1 < y2 {
459 return_point = Some(LayoutPoint {
460 x: from_center_x,
461 y: y1 + from.size.height,
462 });
463 } else if x1 == x2 && y1 > y2 {
464 return_point = Some(LayoutPoint {
465 x: from_center_x,
466 y: y1,
467 });
468 }
469
470 if x1 > x2 && y1 < y2 {
471 if from_dyx >= tan_dyx {
472 return_point = Some(LayoutPoint {
473 x: x1,
474 y: from_center_y + (tan_dyx * from.size.width) / 2.0,
475 });
476 } else {
477 return_point = Some(LayoutPoint {
478 x: from_center_x - ((dx / dy) * from.size.height) / 2.0,
479 y: y1 + from.size.height,
480 });
481 }
482 } else if x1 < x2 && y1 < y2 {
483 if from_dyx >= tan_dyx {
484 return_point = Some(LayoutPoint {
485 x: x1 + from.size.width,
486 y: from_center_y + (tan_dyx * from.size.width) / 2.0,
487 });
488 } else {
489 return_point = Some(LayoutPoint {
490 x: from_center_x + ((dx / dy) * from.size.height) / 2.0,
491 y: y1 + from.size.height,
492 });
493 }
494 } else if x1 < x2 && y1 > y2 {
495 if from_dyx >= tan_dyx {
496 return_point = Some(LayoutPoint {
497 x: x1 + from.size.width,
498 y: from_center_y - (tan_dyx * from.size.width) / 2.0,
499 });
500 } else {
501 return_point = Some(LayoutPoint {
502 x: from_center_x + ((from.size.height / 2.0) * dx) / dy,
503 y: y1,
504 });
505 }
506 } else if x1 > x2 && y1 > y2 {
507 if from_dyx >= tan_dyx {
508 return_point = Some(LayoutPoint {
509 x: x1,
510 y: from_center_y - (from.size.width / 2.0) * tan_dyx,
511 });
512 } else {
513 return_point = Some(LayoutPoint {
514 x: from_center_x - ((from.size.height / 2.0) * dx) / dy,
515 y: y1,
516 });
517 }
518 }
519
520 return_point.unwrap_or(LayoutPoint {
521 x: from_center_x,
522 y: from_center_y,
523 })
524}
525
526fn intersect_points(from: &Rect, to: &Rect) -> (LayoutPoint, LayoutPoint) {
527 let end_intersect_point = LayoutPoint {
528 x: to.origin.x + to.size.width / 2.0,
529 y: to.origin.y + to.size.height / 2.0,
530 };
531 let start_point = intersect_point(from, end_intersect_point);
532
533 let end_intersect_point = LayoutPoint {
534 x: from.origin.x + from.size.width / 2.0,
535 y: from.origin.y + from.size.height / 2.0,
536 };
537 let end_point = intersect_point(to, end_intersect_point);
538
539 (start_point, end_point)
540}
541
542fn layout_c4_shape_array(
543 current_bounds: &mut BoundsState,
544 shape_indices: &[usize],
545 model: &C4Model,
546 effective_config: &Value,
547 conf: &C4Conf,
548 c4_shape_in_row: usize,
549 measurer: &dyn TextMeasurer,
550 out_shapes: &mut HashMap<String, C4ShapeLayout>,
551) {
552 for idx in shape_indices {
553 let shape = &model.shapes[*idx];
554 let mut y = conf.c4_shape_padding;
555
556 let type_c4_shape = shape.type_c4_shape.as_str().to_string();
557 let mut type_conf = conf.c4_shape_font(effective_config, &type_c4_shape);
558 type_conf.font_size -= 2.0;
559
560 let type_text = format!("«{}»", type_c4_shape);
561 let type_metrics = measurer.measure(&type_text, &type_conf);
562 let type_block = C4TextBlockLayout {
563 text: type_text,
564 y,
565 width: type_metrics.width,
566 height: type_conf.font_size + 2.0,
567 line_count: 1,
568 };
569 y = y + type_block.height - 4.0;
570
571 let mut image = C4ImageLayout {
572 width: 0.0,
573 height: 0.0,
574 y: 0.0,
575 };
576 if matches!(type_c4_shape.as_str(), "person" | "external_person") {
577 image.width = 48.0;
578 image.height = 48.0;
579 image.y = y;
580 y = image.y + image.height;
581 }
582 if has_sprite(&shape.sprite) {
583 image.width = 48.0;
584 image.height = 48.0;
585 image.y = y;
586 y = image.y + image.height;
587 }
588
589 let text_wrap = shape.wrap && conf.wrap;
590 let text_limit_width = conf.width - conf.c4_shape_padding * 2.0;
591
592 let mut label_conf = conf.c4_shape_font(effective_config, &type_c4_shape);
593 label_conf.font_size += 2.0;
594 label_conf.font_weight = Some("bold".to_string());
595
596 let label_text = shape.label.as_str().to_string();
597 let label_m = measure_c4_text(
598 measurer,
599 &label_text,
600 &label_conf,
601 text_wrap,
602 text_limit_width,
603 );
604 let label = C4TextBlockLayout {
605 text: label_text,
606 y: y + 8.0,
607 width: label_m.width,
608 height: label_m.height,
609 line_count: label_m.line_count,
610 };
611 y = label.y + label.height;
612
613 let mut ty_block: Option<C4TextBlockLayout> = None;
614 let mut techn_block: Option<C4TextBlockLayout> = None;
615
616 if let Some(ty) = shape.ty.as_ref().filter(|t| !t.as_str().is_empty()) {
617 let type_text = format!("[{}]", ty.as_str());
618 let type_conf = conf.c4_shape_font(effective_config, &type_c4_shape);
619 let m = measure_c4_text(
620 measurer,
621 &type_text,
622 &type_conf,
623 text_wrap,
624 text_limit_width,
625 );
626 let block = C4TextBlockLayout {
627 text: type_text,
628 y: y + 5.0,
629 width: m.width,
630 height: m.height,
631 line_count: m.line_count,
632 };
633 y = block.y + block.height;
634 ty_block = Some(block);
635 } else if let Some(techn) = shape.techn.as_ref().filter(|t| !t.as_str().is_empty()) {
636 let techn_text = format!("[{}]", techn.as_str());
637 let techn_conf = TextStyle {
646 font_family: Some("Arial".to_string()),
647 font_size: 12.0,
648 font_weight: None,
649 };
650 let m = measure_c4_text(
651 measurer,
652 &techn_text,
653 &techn_conf,
654 text_wrap,
655 text_limit_width,
656 );
657 let block = C4TextBlockLayout {
658 text: techn_text,
659 y: y + 5.0,
660 width: m.width,
661 height: m.height,
662 line_count: m.line_count,
663 };
664 y = block.y + block.height;
665 techn_block = Some(block);
666 }
667
668 let mut rect_height = y;
669 let mut rect_width = label.width;
670
671 let mut descr_block: Option<C4TextBlockLayout> = None;
672 if let Some(descr) = shape.descr.as_ref().filter(|t| !t.as_str().is_empty()) {
673 let descr_text = descr.as_str().to_string();
674 let descr_conf = conf.c4_shape_font(effective_config, &type_c4_shape);
675 let m = measure_c4_text(
676 measurer,
677 &descr_text,
678 &descr_conf,
679 text_wrap,
680 text_limit_width,
681 );
682 let block = C4TextBlockLayout {
683 text: descr_text,
684 y: y + 20.0,
685 width: m.width,
686 height: m.height,
687 line_count: m.line_count,
688 };
689 y = block.y + block.height;
690 rect_width = rect_width.max(block.width);
691 rect_height = y - block.line_count as f64 * 5.0;
692 descr_block = Some(block);
693 }
694
695 rect_width += conf.c4_shape_padding;
696
697 let width = conf.width.max(rect_width);
698 let height = conf.height.max(rect_height);
699 let margin = conf.c4_shape_margin;
700
701 let mut rect = Rect {
702 origin: merman_core::geom::point(0.0, 0.0),
703 size: merman_core::geom::Size::new(width, height),
704 margin,
705 };
706 current_bounds.insert_rect(&mut rect, c4_shape_in_row, conf);
707
708 out_shapes.insert(
709 shape.alias.clone(),
710 C4ShapeLayout {
711 alias: shape.alias.clone(),
712 parent_boundary: shape.parent_boundary.clone(),
713 type_c4_shape: type_c4_shape.clone(),
714 x: rect.origin.x,
715 y: rect.origin.y,
716 width: rect.size.width,
717 height: rect.size.height,
718 margin: rect.margin,
719 image,
720 type_block,
721 label,
722 ty: ty_block,
723 techn: techn_block,
724 descr: descr_block,
725 },
726 );
727 }
728
729 current_bounds.bump_last_margin(conf.c4_shape_margin);
730}
731
732fn layout_inside_boundary(
733 parent_bounds: &mut BoundsState,
734 boundary_indices: &[usize],
735 model: &C4Model,
736 effective_config: &Value,
737 conf: &C4Conf,
738 c4_shape_in_row: usize,
739 c4_boundary_in_row: usize,
740 measurer: &dyn TextMeasurer,
741 boundary_children: &HashMap<String, Vec<usize>>,
742 shape_children: &HashMap<String, Vec<usize>>,
743 out_boundaries: &mut HashMap<String, C4BoundaryLayout>,
744 out_shapes: &mut HashMap<String, C4ShapeLayout>,
745 global_max_x: &mut f64,
746 global_max_y: &mut f64,
747) -> Result<()> {
748 let mut current_bounds = BoundsState::default();
749
750 let denom = c4_boundary_in_row.min(boundary_indices.len().max(1));
751 let width_limit = parent_bounds.data.width_limit / denom as f64;
752 current_bounds.data.width_limit = width_limit;
753
754 for (i, idx) in boundary_indices.iter().enumerate() {
755 let boundary = &model.boundaries[*idx];
756 let mut y = 0.0;
757
758 let mut image = C4ImageLayout {
759 width: 0.0,
760 height: 0.0,
761 y: 0.0,
762 };
763 if has_sprite(&boundary.sprite) {
764 image.width = 48.0;
765 image.height = 48.0;
766 image.y = y;
767 y = image.y + image.height;
768 }
769
770 let text_wrap = boundary.wrap.unwrap_or(model.wrap) && conf.wrap;
771 let mut label_conf = conf.boundary_font();
772 label_conf.font_size += 2.0;
773 label_conf.font_weight = Some("bold".to_string());
774
775 let label_text = boundary.label.as_str().to_string();
776 let label_m = measure_c4_text(measurer, &label_text, &label_conf, text_wrap, width_limit);
777 let label = C4TextBlockLayout {
778 text: label_text,
779 y: y + 8.0,
780 width: label_m.width,
781 height: label_m.height,
782 line_count: label_m.line_count,
783 };
784 y = label.y + label.height;
785
786 let mut ty_block: Option<C4TextBlockLayout> = None;
787 if let Some(ty) = boundary.ty.as_ref().filter(|t| !t.as_str().is_empty()) {
788 let ty_text = format!("[{}]", ty.as_str());
789 let ty_conf = conf.boundary_font();
790 let m = measure_c4_text(measurer, &ty_text, &ty_conf, text_wrap, width_limit);
791 let block = C4TextBlockLayout {
792 text: ty_text,
793 y: y + 5.0,
794 width: m.width,
795 height: m.height,
796 line_count: m.line_count,
797 };
798 y = block.y + block.height;
799 ty_block = Some(block);
800 }
801
802 let mut descr_block: Option<C4TextBlockLayout> = None;
803 if let Some(descr) = boundary.descr.as_ref().filter(|t| !t.as_str().is_empty()) {
804 let descr_text = descr.as_str().to_string();
805 let mut descr_conf = conf.boundary_font();
806 descr_conf.font_size -= 2.0;
807 let m = measure_c4_text(measurer, &descr_text, &descr_conf, text_wrap, width_limit);
808 let block = C4TextBlockLayout {
809 text: descr_text,
810 y: y + 20.0,
811 width: m.width,
812 height: m.height,
813 line_count: m.line_count,
814 };
815 y = block.y + block.height;
816 descr_block = Some(block);
817 }
818
819 let parent_startx = parent_bounds
820 .data
821 .startx
822 .ok_or_else(|| Error::InvalidModel {
823 message: "c4: parent bounds missing startx".to_string(),
824 })?;
825 let parent_stopy = parent_bounds
826 .data
827 .stopy
828 .ok_or_else(|| Error::InvalidModel {
829 message: "c4: parent bounds missing stopy".to_string(),
830 })?;
831
832 if i == 0 || i % c4_boundary_in_row == 0 {
833 let x = parent_startx + conf.diagram_margin_x;
834 let y0 = parent_stopy + conf.diagram_margin_y + y;
835 current_bounds.set_data(x, x, y0, y0);
836 } else {
837 let startx = current_bounds.data.startx.unwrap_or(parent_startx);
838 let stopx = current_bounds.data.stopx.unwrap_or(startx);
839 let x = if stopx != startx {
840 stopx + conf.diagram_margin_x
841 } else {
842 startx
843 };
844 let y0 = current_bounds.data.starty.unwrap_or(parent_stopy);
845 current_bounds.set_data(x, x, y0, y0);
846 }
847
848 if let Some(shape_indices) = shape_children.get(&boundary.alias) {
849 if !shape_indices.is_empty() {
850 layout_c4_shape_array(
851 &mut current_bounds,
852 shape_indices,
853 model,
854 effective_config,
855 conf,
856 c4_shape_in_row,
857 measurer,
858 out_shapes,
859 );
860 }
861 }
862
863 if let Some(next_boundaries) = boundary_children.get(&boundary.alias) {
864 if !next_boundaries.is_empty() {
865 layout_inside_boundary(
866 &mut current_bounds,
867 next_boundaries,
868 model,
869 effective_config,
870 conf,
871 c4_shape_in_row,
872 c4_boundary_in_row,
873 measurer,
874 boundary_children,
875 shape_children,
876 out_boundaries,
877 out_shapes,
878 global_max_x,
879 global_max_y,
880 )?;
881 }
882 }
883
884 let startx = current_bounds.data.startx.unwrap_or(0.0);
885 let stopx = current_bounds.data.stopx.unwrap_or(startx);
886 let starty = current_bounds.data.starty.unwrap_or(0.0);
887 let stopy = current_bounds.data.stopy.unwrap_or(starty);
888
889 out_boundaries.insert(
890 boundary.alias.clone(),
891 C4BoundaryLayout {
892 alias: boundary.alias.clone(),
893 parent_boundary: boundary.parent_boundary.clone(),
894 x: startx,
895 y: starty,
896 width: stopx - startx,
897 height: stopy - starty,
898 image,
899 label,
900 ty: ty_block,
901 descr: descr_block,
902 },
903 );
904
905 let stopx_with_margin = stopx + conf.c4_shape_margin;
906 let stopy_with_margin = stopy + conf.c4_shape_margin;
907 parent_bounds.data.stopx = Some(
908 parent_bounds
909 .data
910 .stopx
911 .unwrap_or(stopx_with_margin)
912 .max(stopx_with_margin),
913 );
914 parent_bounds.data.stopy = Some(
915 parent_bounds
916 .data
917 .stopy
918 .unwrap_or(stopy_with_margin)
919 .max(stopy_with_margin),
920 );
921
922 *global_max_x = global_max_x.max(parent_bounds.data.stopx.unwrap_or(*global_max_x));
923 *global_max_y = global_max_y.max(parent_bounds.data.stopy.unwrap_or(*global_max_y));
924 }
925
926 Ok(())
927}
928
929pub(crate) fn layout_c4_diagram(
930 model: &Value,
931 effective_config: &Value,
932 measurer: &dyn TextMeasurer,
933 viewport_width: f64,
934 viewport_height: f64,
935) -> Result<C4DiagramLayout> {
936 let model: C4Model = from_value_ref(model)?;
937 let conf = C4Conf::from_effective_config(effective_config);
938
939 let c4_shape_in_row = (model.layout.c4_shape_in_row.max(1)) as usize;
940 let c4_boundary_in_row = (model.layout.c4_boundary_in_row.max(1)) as usize;
941
942 let mut boundary_children: HashMap<String, Vec<usize>> = HashMap::new();
943 for (i, b) in model.boundaries.iter().enumerate() {
944 boundary_children
945 .entry(b.parent_boundary.clone())
946 .or_default()
947 .push(i);
948 }
949 validate_c4_boundary_depth(&model, &boundary_children)?;
950 let mut shape_children: HashMap<String, Vec<usize>> = HashMap::new();
951 for (i, s) in model.shapes.iter().enumerate() {
952 shape_children
953 .entry(s.parent_boundary.clone())
954 .or_default()
955 .push(i);
956 }
957
958 let mut out_boundaries: HashMap<String, C4BoundaryLayout> = HashMap::new();
959 let mut out_shapes: HashMap<String, C4ShapeLayout> = HashMap::new();
960
961 let mut screen_bounds = BoundsState::default();
962 screen_bounds.set_data(
963 conf.diagram_margin_x,
964 conf.diagram_margin_x,
965 conf.diagram_margin_y,
966 conf.diagram_margin_y,
967 );
968 screen_bounds.data.width_limit = viewport_width;
969
970 let mut global_max_x = conf.diagram_margin_x;
971 let mut global_max_y = conf.diagram_margin_y;
972
973 let root_boundaries = boundary_children.get("").cloned().unwrap_or_default();
974 if root_boundaries.is_empty() {
975 return Err(Error::InvalidModel {
976 message: "c4: expected at least the implicit global boundary".to_string(),
977 });
978 }
979
980 layout_inside_boundary(
981 &mut screen_bounds,
982 &root_boundaries,
983 &model,
984 effective_config,
985 &conf,
986 c4_shape_in_row,
987 c4_boundary_in_row,
988 measurer,
989 &boundary_children,
990 &shape_children,
991 &mut out_boundaries,
992 &mut out_shapes,
993 &mut global_max_x,
994 &mut global_max_y,
995 )?;
996
997 screen_bounds.data.stopx = Some(global_max_x);
998 screen_bounds.data.stopy = Some(global_max_y);
999
1000 let box_startx = screen_bounds.data.startx.unwrap_or(0.0);
1001 let box_starty = screen_bounds.data.starty.unwrap_or(0.0);
1002 let box_stopx = screen_bounds.data.stopx.unwrap_or(conf.diagram_margin_x);
1003 let box_stopy = screen_bounds.data.stopy.unwrap_or(conf.diagram_margin_y);
1004
1005 let width = (box_stopx - box_startx) + 2.0 * conf.diagram_margin_x;
1006 let height = (box_stopy - box_starty) + 2.0 * conf.diagram_margin_y;
1007
1008 let bounds = Some(Bounds {
1009 min_x: box_startx,
1010 min_y: box_starty,
1011 max_x: box_stopx,
1012 max_y: box_stopy,
1013 });
1014
1015 let mut shape_rects: HashMap<&str, Rect> = HashMap::new();
1016 for s in model.shapes.iter() {
1017 let Some(l) = out_shapes.get(&s.alias) else {
1018 continue;
1019 };
1020 shape_rects.insert(
1021 s.alias.as_str(),
1022 Rect {
1023 origin: merman_core::geom::point(l.x, l.y),
1024 size: merman_core::geom::Size::new(l.width, l.height),
1025 margin: l.margin,
1026 },
1027 );
1028 }
1029
1030 let rel_font = conf.message_font();
1031 let mut rels_out: Vec<C4RelLayout> = Vec::new();
1032 for (i, rel) in model.rels.iter().enumerate() {
1033 let mut label_text = rel.label.as_str().to_string();
1034 if model.c4_type == "C4Dynamic" {
1035 label_text = format!("{}: {}", i + 1, label_text);
1036 }
1037
1038 let rel_text_wrap = rel.wrap && conf.wrap;
1039
1040 let label_limit = measurer.measure(&label_text, &rel_font).width;
1041 let label_m = measure_c4_text(measurer, &label_text, &rel_font, rel_text_wrap, label_limit);
1042 let label = C4TextBlockLayout {
1043 text: label_text,
1044 y: 0.0,
1045 width: label_m.width,
1046 height: label_m.height,
1047 line_count: label_m.line_count,
1048 };
1049
1050 let techn = rel
1051 .techn
1052 .as_ref()
1053 .filter(|t| !t.as_str().is_empty())
1054 .map(|t| {
1055 let text = t.as_str().to_string();
1056 let limit = measurer.measure(&text, &rel_font).width;
1057 let m = measure_c4_text(measurer, &text, &rel_font, rel_text_wrap, limit);
1058 C4TextBlockLayout {
1059 text,
1060 y: 0.0,
1061 width: m.width,
1062 height: m.height,
1063 line_count: m.line_count,
1064 }
1065 });
1066
1067 let descr = rel
1068 .descr
1069 .as_ref()
1070 .filter(|t| !t.as_str().is_empty())
1071 .map(|t| {
1072 let text = t.as_str().to_string();
1073 let limit = measurer.measure(&text, &rel_font).width;
1074 let m = measure_c4_text(measurer, &text, &rel_font, rel_text_wrap, limit);
1075 C4TextBlockLayout {
1076 text,
1077 y: 0.0,
1078 width: m.width,
1079 height: m.height,
1080 line_count: m.line_count,
1081 }
1082 });
1083
1084 let from = shape_rects
1085 .get(rel.from_alias.as_str())
1086 .ok_or_else(|| Error::InvalidModel {
1087 message: format!(
1088 "c4: relationship references missing from shape {}",
1089 rel.from_alias
1090 ),
1091 })?;
1092 let to = shape_rects
1093 .get(rel.to_alias.as_str())
1094 .ok_or_else(|| Error::InvalidModel {
1095 message: format!(
1096 "c4: relationship references missing to shape {}",
1097 rel.to_alias
1098 ),
1099 })?;
1100
1101 let (start_point, end_point) = intersect_points(from, to);
1102
1103 rels_out.push(C4RelLayout {
1104 from: rel.from_alias.clone(),
1105 to: rel.to_alias.clone(),
1106 rel_type: rel.rel_type.clone(),
1107 start_point,
1108 end_point,
1109 offset_x: rel.offset_x,
1110 offset_y: rel.offset_y,
1111 label,
1112 techn,
1113 descr,
1114 });
1115 }
1116
1117 let mut boundaries_out = Vec::with_capacity(model.boundaries.len());
1118 for b in &model.boundaries {
1119 let Some(l) = out_boundaries.get(&b.alias) else {
1120 return Err(Error::InvalidModel {
1121 message: format!("c4: missing boundary layout for {}", b.alias),
1122 });
1123 };
1124 boundaries_out.push(l.clone());
1125 }
1126
1127 let mut shapes_out = Vec::with_capacity(model.shapes.len());
1128 for s in &model.shapes {
1129 let Some(l) = out_shapes.get(&s.alias) else {
1130 return Err(Error::InvalidModel {
1131 message: format!("c4: missing shape layout for {}", s.alias),
1132 });
1133 };
1134 shapes_out.push(l.clone());
1135 }
1136
1137 Ok(C4DiagramLayout {
1138 bounds,
1139 width,
1140 height,
1141 viewport_width,
1142 viewport_height,
1143 c4_type: model.c4_type,
1144 title: model.title,
1145 boundaries: boundaries_out,
1146 shapes: shapes_out,
1147 rels: rels_out,
1148 })
1149}
1150
1151fn validate_c4_boundary_depth(
1152 model: &C4Model,
1153 boundary_children: &HashMap<String, Vec<usize>>,
1154) -> Result<()> {
1155 let roots = boundary_children.get("").cloned().unwrap_or_default();
1156 let mut stack: Vec<(usize, usize)> = roots.into_iter().map(|idx| (idx, 1)).collect();
1157 while let Some((idx, depth)) = stack.pop() {
1158 if depth > MAX_DIAGRAM_NESTING_DEPTH {
1159 return Err(Error::InvalidModel {
1160 message: format!(
1161 "c4 boundary nesting depth exceeds maximum of {MAX_DIAGRAM_NESTING_DEPTH}"
1162 ),
1163 });
1164 }
1165 let Some(boundary) = model.boundaries.get(idx) else {
1166 continue;
1167 };
1168 if let Some(children) = boundary_children.get(&boundary.alias) {
1169 for &child in children {
1170 stack.push((child, depth + 1));
1171 }
1172 }
1173 }
1174 Ok(())
1175}
1176
1177#[cfg(test)]
1178mod tests {
1179 #[test]
1180 fn c4_svg_bbox_line_height_overrides_are_generated() {
1181 assert_eq!(
1182 crate::generated::c4_text_overrides_11_12_2::lookup_c4_svg_bbox_line_height_px(12),
1183 Some(14.0)
1184 );
1185 assert_eq!(
1186 crate::generated::c4_text_overrides_11_12_2::lookup_c4_svg_bbox_line_height_px(14),
1187 Some(16.0)
1188 );
1189 assert_eq!(
1190 crate::generated::c4_text_overrides_11_12_2::lookup_c4_svg_bbox_line_height_px(16),
1191 Some(17.0)
1192 );
1193 assert_eq!(
1194 crate::generated::c4_text_overrides_11_12_2::lookup_c4_svg_bbox_line_height_px(15),
1195 None
1196 );
1197 }
1198}