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