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
//! Where edges meet their steps: the sides across the flow they are asked to leave and reach,
//! and their ports on the cross axis.
use super::{
TRACK,
graph::Graph,
measure::Model,
model::{Diagram, Shape},
};
use crate::{error::ChartWarning, layout::count, spec::PortSide};
/// A side of a step across the flow: towards smaller cross positions, left in portrait and top in
/// landscape, or towards larger ones.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum Across {
Low,
High,
}
/// Per edge, the side across the flow it is asked to leave its first step on and reach its last
/// on, in the order the layout runs it: an edge the layout turned around leaves its `to` and
/// reaches its `from`. A side along the flow is where the edge meets its step anyway, or else
/// would turn the edge back over that step and its other edges: then it is reported and the edge
/// meets the step where the flow leads.
pub(super) fn across_sides(
diagram: &Diagram,
graph: &Graph,
warnings: &mut Vec<ChartWarning>,
) -> Vec<(Option<Across>, Option<Across>)> {
diagram
.edges
.iter()
.zip(&graph.chains)
.map(|(edge, chain)| {
let Some(chain) = chain else {
return (None, None);
};
let mut side = |side: Option<PortSide>, field: &str, along: PortSide| match side {
Some(PortSide::Left) => Some(Across::Low),
Some(PortSide::Right) => Some(Across::High),
Some(side) if side != along => {
warnings.push(ChartWarning::new(
"port_side_ignored",
format!("{}/{field}", edge.sides.path),
"on this side the edge would turn back over its node; it meets the node \
where the flow leads, use left or right to leave the flow",
));
None
}
_ => None,
};
let (from_along, to_along) = if chain.reversed {
(PortSide::Start, PortSide::End)
} else {
(PortSide::End, PortSide::Start)
};
let from = side(edge.sides.from, "fromSide", from_along);
let to = side(edge.sides.to, "toSide", to_along);
if chain.reversed {
(to, from)
} else {
(from, to)
}
})
.collect()
}
/// Per step, the room the edges that meet it across the flow take beside it on its low and its
/// high side: a stub out of the step, and a track more for every further edge on one side.
pub(super) fn side_room(
nodes: usize,
graph: &Graph,
across: &[(Option<Across>, Option<Across>)],
stub: f64,
) -> Vec<(f64, f64)> {
(0..nodes)
.map(|node| {
let room = |side: Across| {
let (mut leaving, mut reaching) = (0, 0);
for (chain, (leave, reach)) in graph.chains.iter().zip(across) {
let Some(chain) = chain else { continue };
if chain.items[0] == node && *leave == Some(side) {
leaving += 1;
}
if chain.items[chain.items.len() - 1] == node && *reach == Some(side) {
reaching += 1;
}
}
let most = usize::max(leaving, reaching);
if most == 0 {
0.0
} else {
stub + TRACK * count(most - 1)
}
};
(room(Across::Low), room(Across::High))
})
.collect()
}
/// Where edges leave and reach their steps on the cross axis, as offsets from the step's middle.
pub(super) struct Ports {
/// Per edge: the offset where it leaves its first step and where it reaches its last. For an
/// edge that meets its step across the flow, the offset of the stub beside the step it runs
/// along.
pub(super) leave: Vec<f64>,
pub(super) reach: Vec<f64>,
/// Per edge that leaves or reaches its step across the flow: where on the main axis it meets
/// the side of the step, as an offset from the step's middle.
pub(super) leave_along: Vec<Option<f64>>,
pub(super) reach_along: Vec<Option<f64>>,
/// Per edge that leaves or reaches a block: where on the main axis, from the block's middle,
/// it meets the node inside the block it joins.
pub(super) leave_into: Vec<Option<f64>>,
pub(super) reach_into: Vec<Option<f64>>,
}
impl Ports {
pub(super) fn new(
graph: &Graph,
cross: &[f64],
cross_size: &impl Fn(usize) -> f64,
model: &Model,
landscape: bool,
) -> Self {
let edges = graph.chains.len();
let mut leave = vec![0.0; edges];
let mut reach = vec![0.0; edges];
for node in 0..model.diagram.nodes.len() {
// The edges of a block meet it where the nodes they join lie inside.
if model.diagram.nodes[node].shape == Shape::Block {
continue;
}
for (outgoing, offsets) in [(true, &mut leave), (false, &mut reach)] {
let edges: Vec<(f64, usize)> = graph
.chains
.iter()
.enumerate()
.filter_map(|(edge, chain)| {
let chain = chain.as_ref()?;
let items = &chain.items;
let (end, next, side) = if outgoing {
(items[0], items[1], model.across[edge].0)
} else {
(
items[items.len() - 1],
items[items.len() - 2],
model.across[edge].1,
)
};
(end == node && side.is_none()).then_some((cross[next], edge))
})
.collect();
// The edges of a shared trunk take one port, where they lie on average.
let mut shared: Vec<(f64, Vec<usize>)> = Vec::new();
for (at, edge) in edges {
let trunk = model.trunks.at(edge, outgoing);
match shared.iter_mut().find(|(_, members)| {
trunk.is_some() && model.trunks.at(members[0], outgoing) == trunk
}) {
Some((sum, members)) => {
*sum += at;
members.push(edge);
}
None => shared.push((at, vec![edge])),
}
}
let mut edges: Vec<(f64, Vec<usize>)> = shared
.into_iter()
.map(|(sum, members)| (sum / count(members.len()), members))
.collect();
crate::sort::by(&mut edges, |a, b| a.0.total_cmp(&b.0).then(a.1.cmp(&b.1)));
let ports = count(edges.len());
if edges.len() < 2 || model.diagram.nodes[node].shape == Shape::Interface {
continue;
}
let size = cross_size(node);
let share = if model.diagram.nodes[node].shape == Shape::Decision {
0.3
} else {
0.6
};
let mut step = (size * share / (ports - 1.0)).min(24.0);
if landscape && outgoing {
step = step.max(18.0).min(size / ports);
}
// An edge of the main path keeps the middle, so that the path runs straight; the
// others keep their order on either side of it.
let main = edges
.iter()
.position(|(_, members)| model.diagram.edges[members[0]].main)
.map(count);
let middle = main.unwrap_or((ports - 1.0) / 2.0);
let reach = (middle).max(ports - 1.0 - middle);
let step = step.min((size / 2.0 - 8.0) / reach);
for (index, (_, members)) in edges.iter().enumerate() {
for edge in members {
offsets[*edge] = (count(index) - middle) * step;
}
}
}
}
let mut ports = Self {
leave,
reach,
leave_along: vec![None; edges],
reach_along: vec![None; edges],
leave_into: vec![None; edges],
reach_into: vec![None; edges],
};
ports.across(graph, cross, cross_size, model, landscape);
ports.blocks(graph, model, landscape);
ports
}
/// The ports of the edges that meet their steps across the flow. On each side of a step, the
/// edges that reach it come first along the flow and those that leave it after them, spread
/// over the step's depth; each runs along a stub of its own beside the step, the edge that
/// runs further along the step outside the others, so that none crosses another there.
fn across(
&mut self,
graph: &Graph,
cross: &[f64],
cross_size: &impl Fn(usize) -> f64,
model: &Model,
landscape: bool,
) {
let stub = model.spacing.passing_gap;
let edges = graph.chains.len();
let (mut leave_alone, mut reach_alone) = (vec![false; edges], vec![false; edges]);
for node in 0..model.diagram.nodes.len() {
let depth = if landscape {
model.sizes[node].0
} else {
model.sizes[node].1
};
for side in [Across::Low, Across::High] {
let meeting = |leaving: bool| -> Vec<usize> {
(0..graph.chains.len())
.filter(|&edge| {
graph.chains[edge].as_ref().is_some_and(|chain| {
let items = &chain.items;
if leaving {
items[0] == node && model.across[edge].0 == Some(side)
} else {
items[items.len() - 1] == node
&& model.across[edge].1 == Some(side)
}
})
})
.collect()
};
let (reaching, leaving) = (meeting(false), meeting(true));
let ports = reaching.len() + leaving.len();
if ports == 0 {
continue;
}
let step = if ports > 1 {
(depth * 0.6 / count(ports - 1)).min(16.0)
} else {
0.0
};
let sign = if side == Across::High { 1.0 } else { -1.0 };
let middle = count(ports - 1) / 2.0;
for (index, &edge) in reaching.iter().enumerate() {
self.reach_along[edge] = Some((count(index) - middle) * step);
self.reach[edge] =
sign * (cross_size(node) / 2.0 + stub + TRACK * count(index));
reach_alone[edge] = ports == 1;
}
for (index, &edge) in leaving.iter().enumerate() {
let outside = leaving.len() - 1 - index;
self.leave_along[edge] = Some((count(reaching.len() + index) - middle) * step);
self.leave[edge] =
sign * (cross_size(node) / 2.0 + stub + TRACK * count(outside));
leave_alone[edge] = ports == 1;
}
}
}
// An edge alone on its side of a step runs on in line with what it leads to beside the
// step, where the step's layer leaves room: the item that carries it on, or its stub at
// the other end on the same side. It then needs no track to change course.
let room = |node: usize, offset: f64| {
let layer = &graph.layers[graph.layer[node]];
let position = graph.position[node];
let neighbour = if offset > 0.0 {
layer.get(position + 1)
} else {
position.checked_sub(1).map(|before| &layer[before])
};
neighbour.map_or(f64::INFINITY, |&other| {
(cross[other] - cross[node]).abs() - cross_size(other) / 2.0 - stub
})
};
let outer = |offset: f64, a: f64, b: f64| if offset > 0.0 { a.max(b) } else { a.min(b) };
for (edge, chain) in graph.chains.iter().enumerate() {
let Some(chain) = chain else { continue };
let items = &chain.items;
let (first, last) = (items[0], items[items.len() - 1]);
let (next, before) = (items[1], items[items.len() - 2]);
let (leave, reach) = (self.leave[edge], self.reach[edge]);
let fits =
|node: usize, at: f64| (at - cross[node]).abs() <= room(node, at - cross[node]);
if leave_alone[edge]
&& reach_alone[edge]
&& items.len() == 2
&& model.across[edge].0 == model.across[edge].1
{
let at = outer(leave, cross[first] + leave, cross[last] + reach);
if fits(first, at) && fits(last, at) {
self.leave[edge] = at - cross[first];
self.reach[edge] = at - cross[last];
}
continue;
}
if leave_alone[edge] && graph.items[next].node.is_none() {
let at = outer(leave, cross[first] + leave, cross[next]);
if fits(first, at) {
self.leave[edge] = at - cross[first];
}
}
if reach_alone[edge] && graph.items[before].node.is_none() {
let at = outer(reach, cross[last] + reach, cross[before]);
if fits(last, at) {
self.reach[edge] = at - cross[last];
}
}
}
}
}