use std::collections::HashMap;
use super::db::{C4Db, C4Shape, ElementRef};
use super::route::{Box, HEAD, LABEL_PAD, LANE, NARROW};
use crate::js::chars::{to_lower, trim};
use crate::js::math::{max, min};
use crate::scene::MARKER_SIZE;
use crate::scene::text::{Face, Label};
use crate::text::{Setting, layout_label};
const SHAPE_W: f64 = 216.0;
const SHAPE_MIN_H: f64 = 64.0;
const PAD_X: f64 = 14.0;
const PAD_Y: f64 = 12.0;
const GAP_X: f64 = NARROW + LABEL_PAD + 3.0 * MARKER_SIZE;
const GAP_Y: f64 = 88.0;
const GROUP_PAD: f64 = LANE + 8.0;
const GROUP_GAP: f64 = 48.0;
pub(crate) const HEAD_LEFT: f64 = 16.0;
pub(crate) const HEAD_TOP: f64 = 10.0;
fn form_named(name: &str) -> Option<&'static str> {
Some(match name {
"person" => "person",
"box" | "rounded" | "component" => "rect",
"cylinder" | "database" | "db" => "cyl",
"queue" | "pipe" => "h-cyl",
_ => return None,
})
}
pub(crate) struct Node {
pub(crate) b: Box,
pub(crate) shape: usize,
pub(crate) labels: [Label; 4],
pub(crate) text_h: f64,
}
pub(crate) struct Group {
pub(crate) b: Box,
pub(crate) kids: Vec<usize>,
pub(crate) nodes: Vec<Node>,
pub(crate) labels: Vec<Label>,
pub(crate) head: f64,
pub(crate) head_w: f64,
pub(crate) head_x: f64,
}
pub(crate) struct Layout {
pub(crate) groups: Vec<Group>,
pub(crate) order: Vec<usize>,
pub(crate) boxes: HashMap<ElementRef, Box>,
}
fn form_of(shape: &C4Shape) -> &'static str {
let named = |s: &Option<String>| form_named(&to_lower(s.as_deref().unwrap_or("")));
if let Some(form) = named(&shape.el.shape).or_else(|| named(&shape.el.sprite)) {
return form;
}
if let Some(tags) = shape.el.tags.as_deref().filter(|t| !t.is_empty()) {
for tag in tags.split(',') {
if let Some(form) = form_named(&to_lower(trim(tag))) {
return form;
}
}
}
let kind = shape.kind.as_str();
if kind.ends_with("person") {
"person"
} else if kind.ends_with("_db") {
"cyl"
} else if kind.ends_with("_queue") {
"h-cyl"
} else {
"rect"
}
}
pub(crate) fn bracketed(s: Option<&str>) -> String {
match s {
Some(s) if !s.is_empty() => format!("[{s}]"),
_ => String::new(),
}
}
fn measure(db: &C4Db, at: usize, set: &Setting, size: f64, small: f64, width: f64) -> Node {
let shape = &db.shapes[at];
let form = form_of(shape);
let cap_w = if form == "h-cyl" { 36.0 } else { 0.0 };
let cap_h = match form {
"person" => HEAD,
"cyl" => 24.0,
_ => 0.0,
};
let inner = width - 2.0 * PAD_X - cap_w;
let el = &shape.el;
let kind = format!("«{}»", shape.kind.replace('_', " "));
let labels = [
layout_label(&kind, false, set, small - 1.0, inner, Face::REGULAR),
layout_label(&el.label, false, set, size, inner, Face::BOLD),
layout_label(
&bracketed(el.techn.as_deref()),
false,
set,
small,
inner,
Face::REGULAR,
),
layout_label(
el.descr.as_deref().unwrap_or(""),
false,
set,
small,
inner,
Face::REGULAR,
),
];
let mut tw = 0.0;
let mut text_h = if labels[3].height > 0.0 { 4.0 } else { 0.0 };
for label in &labels {
tw = max(tw, label.width);
text_h += label.height;
}
Node {
b: Box {
x: 0.0,
y: 0.0,
w: max(width, tw + 2.0 * PAD_X + cap_w),
h: max(SHAPE_MIN_H, text_h + 2.0 * PAD_Y + cap_h),
form,
id: at,
},
shape: at,
labels,
text_h,
}
}
fn rows(boxes: &mut [Box], per_row: f64, gap_x: f64, gap_y: f64, stretch: bool) -> (f64, f64) {
let mut width = 0.0;
let mut y = 0.0;
let count = boxes.len() as f64;
let mut from = 0.0;
while from < count {
let to = min(count, from + per_row) as usize;
let row = &mut boxes[from as usize..to];
let mut x = 0.0;
let mut row_h = 0.0;
for b in row.iter() {
row_h = max(row_h, b.h);
}
for b in row.iter_mut() {
b.x = x;
b.y = y;
if stretch {
b.h = row_h;
}
x += b.w + gap_x;
}
width = max(width, x - gap_x);
y += row_h + gap_y;
from += per_row;
}
(width, if boxes.is_empty() { 0.0 } else { y - gap_y })
}
pub(crate) fn layout_c4(
db: &C4Db,
set: &Setting,
size: f64,
small: f64,
room: Option<f64>,
gap: f64,
) -> Layout {
let group_pad = if room.is_none() { GROUP_PAD } else { 12.0 };
let first_boundary = db.shapes.len();
let mut groups: Vec<Group> = Vec::with_capacity(db.boundaries.len());
for (at, boundary) in db.boundaries.iter().enumerate() {
let label_room = room.map_or(4000.0, |room| {
max(80.0, room - 2.0 * HEAD_LEFT - 2.0 * GROUP_PAD * 2.0)
});
let el = &boundary.el;
let labels = if el.parent.is_some() {
vec![
layout_label(&el.label, false, set, size, label_room, Face::BOLD),
layout_label(
&bracketed(el.r#type.as_deref()),
false,
set,
small,
label_room,
Face::REGULAR,
),
layout_label(
el.descr.as_deref().unwrap_or(""),
false,
set,
small,
label_room,
Face::REGULAR,
),
]
} else {
Vec::new()
};
let mut head = 0.0;
let mut head_w = 0.0;
for label in &labels {
head += label.height;
head_w = max(head_w, label.width);
}
groups.push(Group {
b: Box {
x: 0.0,
y: 0.0,
w: 0.0,
h: 0.0,
form: "rect",
id: first_boundary + at,
},
kids: Vec::new(),
nodes: Vec::new(),
labels,
head: if head > 0.0 {
head + HEAD_TOP + 6.0 + LANE
} else {
group_pad
},
head_w,
head_x: HEAD_LEFT,
});
}
for (at, boundary) in db.boundaries.iter().enumerate() {
if let Some(parent) = boundary.el.parent {
groups[parent].kids.push(at);
}
}
let mut order = vec![0];
let mut depths: HashMap<usize, f64> = HashMap::from([(0, 0.0)]);
let mut i = 0;
while i < order.len() {
let at = order[i];
for k in 0..groups[at].kids.len() {
let kid = groups[at].kids[k];
depths.insert(kid, depths[&at] + 1.0);
order.push(kid);
}
i += 1;
}
for (at, shape) in db.shapes.iter().enumerate() {
if let Some(parent) = shape.el.parent {
let depth = depths.get(&parent).copied().unwrap_or(0.0);
let node_width = room.map_or(SHAPE_W, |room| {
max(100.0, min(SHAPE_W, room - 2.0 * depth * group_pad - 32.0))
});
let node = measure(db, at, set, size, small, node_width);
groups[parent].nodes.push(node);
}
}
let per_row = max(1.0, db.shape_in_row);
let groups_per_row = max(1.0, db.boundary_in_row);
let mut kids_top: HashMap<usize, f64> = HashMap::new();
for &at in order.iter().rev() {
let gap_x = if room.is_none() { GAP_X } else { gap };
let count = |boxes: &[Box], limit: f64, gap: f64| {
let Some(room) = room else {
return limit;
};
let mut widest = 1.0;
for b in boxes {
widest = max(widest, b.w);
}
max(
1.0,
min(
limit,
((room + gap - 2.0 * group_pad) / (widest + gap)).floor(),
),
)
};
let mut nodes: Vec<Box> = groups[at].nodes.iter().map(|n| n.b).collect();
let mut kids: Vec<Box> = groups[at].kids.iter().map(|&k| groups[k].b).collect();
let node_count = count(&nodes, per_row, gap_x);
let (grid_w, grid_h) = rows(&mut nodes, node_count, gap_x, GAP_Y, true);
let kid_count = count(&kids, groups_per_row, GROUP_GAP);
let (kids_w, kids_h) = rows(&mut kids, kid_count, GROUP_GAP, GROUP_GAP, false);
for (n, b) in groups[at].nodes.iter_mut().zip(nodes) {
n.b = b;
}
for (k, b) in groups[at].kids.clone().into_iter().zip(kids) {
groups[k].b = b;
}
let between = if grid_h > 0.0 && kids_h > 0.0 {
GROUP_GAP
} else {
0.0
};
let frame = if at == 0 { 0.0 } else { group_pad };
kids_top.insert(at, grid_h + between);
let g = &mut groups[at];
g.b.w = max(
max(grid_w, kids_w),
g.head_w + 2.0 * HEAD_LEFT - 2.0 * frame,
) + 2.0 * frame;
let top = if frame != 0.0 { g.head } else { 0.0 };
g.b.h = top + grid_h + between + kids_h + frame;
}
let mut boxes = HashMap::new();
for &at in &order {
let g = &mut groups[at];
let (left, top) = if at == 0 {
(g.b.x, g.b.y)
} else {
(g.b.x + group_pad, g.b.y + g.head)
};
for n in &mut g.nodes {
n.b.x += left;
n.b.y += top;
boxes.insert(ElementRef::Shape(n.shape), n.b);
}
let below = top + kids_top[&at];
for k in groups[at].kids.clone() {
groups[k].b.x += left;
groups[k].b.y += below;
}
if at != 0 {
boxes.insert(ElementRef::Boundary(at), groups[at].b);
}
}
Layout {
groups,
order,
boxes,
}
}