1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
//! Layout engine for positioning diagram nodes
use crate::diagram::Diagram;
use unicode_width::UnicodeWidthStr;
/// Layout result with positioned nodes
#[derive(Debug, Clone)]
pub struct LayoutResult {
pub positions: std::collections::HashMap<String, Position>,
pub width: usize,
pub height: usize,
}
/// Position of a node in the layout
#[derive(Debug, Clone)]
pub struct Position {
pub x: usize,
pub y: usize,
pub width: usize,
pub height: usize,
}
/// Layout engine using grid-based positioning
pub struct Layout<'a> {
diagram: &'a Diagram,
}
impl<'a> Layout<'a> {
pub fn new(diagram: &'a Diagram) -> Self {
Self { diagram }
}
/// Perform layout calculation
pub fn layout(&self) -> LayoutResult {
match self.diagram.diagram_type {
crate::diagram::DiagramType::Flowchart => self.layout_flowchart(),
crate::diagram::DiagramType::Sequence => self.layout_sequence(),
crate::diagram::DiagramType::Class => self.layout_class(),
crate::diagram::DiagramType::State => self.layout_state(),
crate::diagram::DiagramType::Pie => self.layout_pie(),
_ => self.layout_flowchart(),
}
}
fn layout_flowchart(&self) -> LayoutResult {
use std::collections::HashMap;
let nodes = &self.diagram.nodes;
let edges = &self.diagram.edges;
// Map each node id to its parse-order index for stable ordering and
// O(1) lookup while relaxing edges.
let index_of: HashMap<&str, usize> = nodes
.iter()
.enumerate()
.map(|(i, n)| (n.id.as_str(), i))
.collect();
// Longest-path layering: a node's layer is the longest chain of edges
// reaching it from any source. We relax every edge until the layers
// stop changing (bounded by the node count, which also makes any cycle
// terminate safely). This guarantees each edge points to a strictly
// higher layer, so two edge-connected nodes never land in the same
// column — the overlap that previously made boxes overprint.
let mut layer: Vec<usize> = vec![0; nodes.len()];
for _ in 0..nodes.len() {
let mut changed = false;
for e in edges {
if let (Some(&u), Some(&v)) =
(index_of.get(e.from.as_str()), index_of.get(e.to.as_str()))
{
if u != v && layer[v] < layer[u] + 1 {
layer[v] = layer[u] + 1;
changed = true;
}
}
}
if !changed {
break;
}
}
// Group node indices by layer, preserving parse order within a layer so
// the vertical stacking is deterministic between runs.
let max_layer = layer.iter().copied().max().unwrap_or(0);
let mut layers: Vec<Vec<usize>> = vec![Vec::new(); max_layer + 1];
for (i, &l) in layer.iter().enumerate() {
layers[l].push(i);
}
// Vertical coordinate assignment. Because every edge points to a
// strictly higher layer, a node's predecessors are always in
// already-placed layers — so a single left-to-right sweep suffices.
let mut preds: Vec<Vec<usize>> = vec![Vec::new(); nodes.len()];
for e in edges {
if let (Some(&u), Some(&v)) =
(index_of.get(e.from.as_str()), index_of.get(e.to.as_str()))
{
if u != v {
preds[v].push(u);
}
}
}
// Sweep the layers left to right, centring each node under the mean row
// of its predecessors. Siblings that want the same row are pushed apart
// to keep a minimum separation, then the whole group is recentred on
// that shared target — so a decision node's branches straddle the trunk
// symmetrically (one up, one down) while the trunk stays on one line.
const MIN_SEP: f64 = 1.0;
let mut rows: Vec<f64> = vec![0.0; nodes.len()];
for layer_nodes in &layers {
if layer_nodes.is_empty() {
continue;
}
let mut targets: Vec<(usize, f64)> = layer_nodes
.iter()
.map(|&n| {
let t = if preds[n].is_empty() {
0.0
} else {
preds[n].iter().map(|&u| rows[u]).sum::<f64>() / preds[n].len() as f64
};
(n, t)
})
.collect();
// Order by target row, parse order breaking ties.
targets.sort_by(|a, b| {
a.1.partial_cmp(&b.1)
.unwrap_or(std::cmp::Ordering::Equal)
.then(a.0.cmp(&b.0))
});
// Push apart to maintain the minimum separation.
let mut placed: Vec<f64> = Vec::with_capacity(targets.len());
let mut prev = f64::NEG_INFINITY;
for &(_, t) in &targets {
let y = t.max(prev + MIN_SEP);
placed.push(y);
prev = y;
}
// Recenter the group on the mean target so it straddles the trunk.
let want = targets.iter().map(|&(_, t)| t).sum::<f64>() / targets.len() as f64;
let have = placed.iter().sum::<f64>() / placed.len() as f64;
let shift = want - have;
for (&(n, _), &y) in targets.iter().zip(placed.iter()) {
rows[n] = y + shift;
}
}
// Size boxes to the widest label, and the inter-column gap to the widest
// edge label, so branch labels like "通过"/"失败" render without clipping.
let box_height = 3;
let vertical_gap = 2;
let row_pitch = (box_height + vertical_gap) as f64;
let box_width = nodes
.iter()
.map(|n| UnicodeWidthStr::width(n.label.as_str()) + 2)
.max()
.unwrap_or(12)
.max(12);
let edge_label_w = edges
.iter()
.filter_map(|e| e.label.as_deref())
.map(UnicodeWidthStr::width)
.max()
.unwrap_or(0);
let horizontal_gap = 4.max(edge_label_w + 4);
// Map fractional rows onto the integer canvas grid.
let min_row = rows.iter().copied().fold(f64::INFINITY, f64::min);
let min_row = if min_row.is_finite() { min_row } else { 0.0 };
let mut positions: HashMap<String, Position> = HashMap::new();
let mut max_width = 0;
let mut max_height = 0;
for (l, layer_nodes) in layers.iter().enumerate() {
let x = l * (box_width + horizontal_gap);
for &n in layer_nodes {
let y = ((rows[n] - min_row) * row_pitch).round().max(0.0) as usize;
positions.insert(
nodes[n].id.clone(),
Position {
x,
y,
width: box_width,
height: box_height,
},
);
max_height = max_height.max(y + box_height);
}
if !layer_nodes.is_empty() {
max_width = max_width.max(x + box_width);
}
}
LayoutResult {
positions,
width: max_width + 2,
height: max_height + 2,
}
}
fn layout_sequence(&self) -> LayoutResult {
let mut positions: std::collections::HashMap<String, Position> =
std::collections::HashMap::new();
let box_width = 10;
let box_height = 3;
let horizontal_gap = 8;
for (i, participant) in self.diagram.participants.iter().enumerate() {
let x = i * (box_width + horizontal_gap);
positions.insert(
participant.clone(),
Position {
x,
y: 0,
width: box_width,
height: box_height,
},
);
}
LayoutResult {
positions,
width: self.diagram.participants.len() * (box_width + horizontal_gap) + 2,
height: 20,
}
}
fn layout_class(&self) -> LayoutResult {
let mut positions: std::collections::HashMap<String, Position> =
std::collections::HashMap::new();
let box_width = 16;
let box_height = 6;
let horizontal_gap = 4;
let vertical_gap = 2;
for (i, node) in self.diagram.nodes.iter().enumerate() {
let x = (i % 3) * (box_width + horizontal_gap);
let y = (i / 3) * (box_height + vertical_gap);
positions.insert(
node.id.clone(),
Position {
x,
y,
width: box_width,
height: box_height,
},
);
}
LayoutResult {
positions,
width: 3 * (box_width + horizontal_gap) + 2,
height: (self.diagram.nodes.len() / 3 + 1) * (box_height + vertical_gap) + 2,
}
}
fn layout_state(&self) -> LayoutResult {
// Similar to flowchart but vertical by default
let mut positions: std::collections::HashMap<String, Position> =
std::collections::HashMap::new();
let box_width = 12;
let box_height = 3;
let horizontal_gap = 4;
let vertical_gap = 3;
// Find start states
let mut processed: std::collections::HashSet<String> = std::collections::HashSet::new();
let mut queue: Vec<String> = self
.diagram
.edges
.iter()
.filter(|e| e.from.is_empty() || e.from == "[*]")
.map(|e| e.to.clone())
.collect();
if queue.is_empty() && !self.diagram.nodes.is_empty() {
queue.push(self.diagram.nodes[0].id.clone());
}
let mut y = 0;
while let Some(node_id) = queue.pop() {
if processed.contains(&node_id) {
continue;
}
processed.insert(node_id.clone());
positions.insert(
node_id.clone(),
Position {
x: 0,
y,
width: box_width,
height: box_height,
},
);
y += box_height + vertical_gap;
// Find next states
for edge in &self.diagram.edges {
if edge.from == node_id {
queue.push(edge.to.clone());
}
}
}
LayoutResult {
positions,
width: box_width + horizontal_gap + 2,
height: y + 2,
}
}
fn layout_pie(&self) -> LayoutResult {
// Pie charts are rendered as horizontal bar charts
let mut positions: std::collections::HashMap<String, Position> =
std::collections::HashMap::new();
let bar_height = 3;
let horizontal_gap = 1;
for (i, node) in self.diagram.nodes.iter().enumerate() {
positions.insert(
node.id.clone(),
Position {
x: 0,
y: i * (bar_height + horizontal_gap),
width: 50, // Will be scaled during rendering
height: bar_height,
},
);
}
LayoutResult {
positions,
width: 60,
height: self.diagram.nodes.len() * (bar_height + horizontal_gap) + 2,
}
}
}