Oregami
Repositories/oxedyne/fe2o3

oxedyne/fe2o3/fe2o3_austenite/src/diagram/layout.rs

9.7 KiB, 7 runs

created by r1870400018:36163, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1//! One-pass placement, edge routing and arrowheads.
2//!
3//! Placement is a single dependency-ordered pass, as the design requires: a node is placed
4//! absolutely, or relative to a node already placed, and never by a solver. Nodes are aligned by
5//! centre so a `below` chain reads as a column. An edge is a polyline between two ports -- straight,
6//! or orthogonal with axis-aligned segments and a perpendicular stub out of each port -- ended with a
7//! small filled triangle for the arrowhead.
8
9use crate::diagram::shape::{
10 port_of,
11 Port,
12 Rect,
13};
14use crate::ir::Sp;
15
16use oxedyne_fe2o3_core::prelude::*;
17use oxedyne_fe2o3_graphics::path::{
18 Path,
19 PathBuilder,
20 Pt,
21};
22
23/// Where a node is placed. The relative arms name an earlier node by its index, so resolution is one
24/// forward pass with no back-references.
25#[derive(Clone, Copy, Debug)]
26pub enum Placement {
27 At { x: Sp, y: Sp }, // centre at an absolute point
28 Below { of: usize, gap: Sp }, // centred under an earlier node, its box bottom plus the gap
29 Right { of: usize, gap: Sp }, // centred to the right of an earlier node, its box right plus the gap
30}
31
32/// How an edge is routed between its two ports.
33#[derive(Clone, Copy, Debug, PartialEq, Eq)]
34pub enum Route {
35 Straight,
36 Orthogonal,
37 // A rectangular feedback loop: out of both ports to the right, clear of every box by `out` past the
38 // wider port, then a single vertical run joining the two levels. This is the flowchart's return arrow.
39 Feedback { out: Sp },
40}
41
42/// A cardinal direction, the way a port faces out of its box: an edge leaves and enters along it.
43#[derive(Clone, Copy, Debug, PartialEq, Eq)]
44pub enum Dir4 {
45 Up,
46 Down,
47 Left,
48 Right,
49}
50
51impl Dir4 {
52 /// The unit step in the figure frame (y down), as integer signs.
53 fn step(&self) -> (i32, i32) {
54 match self {
55 Dir4::Up => (0, -1),
56 Dir4::Down => (0, 1),
57 Dir4::Left => (-1, 0),
58 Dir4::Right => (1, 0),
59 }
60 }
61
62 fn is_vertical(&self) -> bool {
63 matches!(self, Dir4::Up | Dir4::Down)
64 }
65}
66
67/// The direction a port faces, or `None` for the centre, which faces nowhere.
68pub fn facing(port: Port) -> Option<Dir4> {
69 match port {
70 Port::North => Some(Dir4::Up),
71 Port::South => Some(Dir4::Down),
72 Port::East => Some(Dir4::Right),
73 Port::West => Some(Dir4::Left),
74 Port::Centre => None,
75 }
76}
77
78/// Places every node in one pass, aligning relative placements by centre. Each entry is a node's
79/// placement and its already-sized box width and height; the result is the boxes in a provisional
80/// frame, which the caller normalises to the figure origin once the ink is known.
81pub fn place(specs: &[(Placement, Sp, Sp)]) -> Outcome<Vec<Rect>> {
82 let mut rects: Vec<Rect> = Vec::with_capacity(specs.len());
83 for (i, (placement, w, h)) in specs.iter().enumerate() {
84 let (cx, cy) = match *placement {
85 Placement::At { x, y } => (x, y),
86 Placement::Below { of, gap } => {
87 let anchor = res!(rects.get(of).ok_or_else(|| err!(
88 "Node {} is placed below node {}, which is not yet placed.", i, of;
89 Invalid, Input)));
90 (anchor.centre_x(), anchor.bottom() + gap + Sp(h.raw() / 2))
91 },
92 Placement::Right { of, gap } => {
93 let anchor = res!(rects.get(of).ok_or_else(|| err!(
94 "Node {} is placed right of node {}, which is not yet placed.", i, of;
95 Invalid, Input)));
96 (anchor.right() + gap + Sp(w.raw() / 2), anchor.centre_y())
97 },
98 };
99 // The centre fixes the box; store its top-left corner.
100 rects.push(Rect::new(cx - Sp(w.raw() / 2), cy - Sp(h.raw() / 2), *w, *h));
101 }
102 Ok(rects)
103}
104
105/// The side port of `r` whose coordinate is nearest `target`, so an edge given a node but no explicit
106/// port attaches on the side facing the other end.
107pub fn nearest_port(r: &Rect, target: (Sp, Sp)) -> Port {
108 let tx = target.0.to_pt() as f32;
109 let ty = target.1.to_pt() as f32;
110 let mut best = Port::North;
111 let mut best_d = f32::INFINITY;
112 for port in [Port::North, Port::South, Port::East, Port::West] {
113 let (px, py) = port_of(r, port);
114 let dx = (px.to_pt() as f32) - tx;
115 let dy = (py.to_pt() as f32) - ty;
116 let d = dx * dx + dy * dy;
117 if d < best_d {
118 best_d = d;
119 best = port;
120 }
121 }
122 best
123}
124
125/// The polyline an edge follows, from one port to the other. A straight edge is the two endpoints; an
126/// orthogonal edge leaves each port along a short perpendicular stub, then bends once to join the two
127/// stubs with axis-aligned segments, so every segment is horizontal or vertical.
128pub fn route_points(
129 from: (Sp, Sp),
130 from_dir: Option<Dir4>,
131 to: (Sp, Sp),
132 to_dir: Option<Dir4>,
133 route: Route,
134 stub: Sp,
135)
136 -> Vec<(Sp, Sp)>
137{
138 match route {
139 Route::Straight => vec![from, to],
140 Route::Feedback { out } => {
141 // Both ends leave to the right; the vertical run sits `out` past whichever port reaches
142 // furthest right, so the loop clears every box between the two levels.
143 let x_far = from.0.max(to.0);
144 let x_det = x_far + out;
145 let mut pts = vec![from, (x_det, from.1), (x_det, to.1), to];
146 dedup(&mut pts);
147 pts
148 },
149 Route::Orthogonal => {
150 let p1 = offset(from, from_dir, stub);
151 let p2 = offset(to, to_dir, stub);
152 // Bend so the segment arriving at the far stub is perpendicular to the near stub's axis: a
153 // vertical near stub runs to the far y first, a horizontal one to the far x first.
154 let vertical_first = match from_dir {
155 Some(d) => d.is_vertical(),
156 None => (to.1.raw() - from.1.raw()).abs() >= (to.0.raw() - from.0.raw()).abs(),
157 };
158 let bend = if vertical_first {
159 (p1.0, p2.1)
160 } else {
161 (p2.0, p1.1)
162 };
163 let mut pts = vec![from, p1, bend, p2, to];
164 dedup(&mut pts);
165 pts
166 },
167 }
168}
169
170/// Moves a point out along a facing direction by `stub`; a centre port (no facing) is left where it is.
171fn offset(p: (Sp, Sp), dir: Option<Dir4>, stub: Sp) -> (Sp, Sp) {
172 match dir {
173 Some(d) => {
174 let (sx, sy) = d.step();
175 (p.0 + Sp(stub.raw() * sx), p.1 + Sp(stub.raw() * sy))
176 },
177 None => p,
178 }
179}
180
181/// Drops points equal to their predecessor, which a degenerate bend or a zero stub can leave.
182fn dedup(pts: &mut Vec<(Sp, Sp)>) {
183 pts.dedup_by(|a, b| a.0 == b.0 && a.1 == b.1);
184}
185
186/// A stroked polyline as an open path in the figure frame.
187pub fn stroke_path(pts: &[(Sp, Sp)]) -> Outcome<Path> {
188 if pts.len() < 2 {
189 return Err(err!(
190 "An edge needs at least two points to stroke, but {} were given.", pts.len();
191 Invalid, Input));
192 }
193 let mut pb = PathBuilder::new();
194 pb.move_to(sp_pt(pts[0]));
195 for p in &pts[1..] {
196 pb.line_to(sp_pt(*p));
197 }
198 pb.finish()
199}
200
201/// The point pulled back from `tip` towards `prev` by `len` points, so a stroke stops at the base of
202/// the arrowhead rather than running under its tip.
203pub fn retract(tip: (Sp, Sp), prev: (Sp, Sp), len: f32) -> (Sp, Sp) {
204 let (ux, uy) = match unit(prev, tip) {
205 Some(u) => u,
206 None => return tip, // coincident points: nothing to pull back along
207 };
208 (
209 Sp::from_pt((tip.0.to_pt() as f32 - ux * len) as f64),
210 Sp::from_pt((tip.1.to_pt() as f32 - uy * len) as f64),
211 )
212}
213
214/// A filled triangle for the arrowhead: tip at the edge's end, base two corners back along the
215/// incoming direction, spread `half` either side of the centreline.
216pub fn arrowhead(
217 tip: (Sp, Sp),
218 prev: (Sp, Sp),
219 len: f32,
220 half: f32,
221)
222 -> Outcome<Path>
223{
224 let (ux, uy) = res!(unit(prev, tip).ok_or_else(|| err!(
225 "An arrowhead has no direction: its edge ends where it begins."; Invalid, Input)));
226 let tx = tip.0.to_pt() as f32;
227 let ty = tip.1.to_pt() as f32;
228 let bx = tx - ux * len; // base centre
229 let by = ty - uy * len;
230 let (px, py) = (-uy, ux); // unit perpendicular
231 let mut pb = PathBuilder::new();
232 pb.move_to(Pt::new(tx, ty));
233 pb.line_to(Pt::new(bx + px * half, by + py * half));
234 pb.line_to(Pt::new(bx - px * half, by - py * half));
235 pb.close();
236 pb.finish()
237}
238
239/// The midpoint of the polyline's longest segment, and the unit perpendicular to it, so a caller can
240/// seat an edge label clear of the line. `None` for a polyline of one point.
241pub fn label_anchor(pts: &[(Sp, Sp)]) -> Option<((Sp, Sp), (f32, f32))> {
242 let mut best = 0usize;
243 let mut best_len = -1.0f32;
244 for i in 1..pts.len() {
245 let dx = (pts[i].0.raw() - pts[i - 1].0.raw()) as f32;
246 let dy = (pts[i].1.raw() - pts[i - 1].1.raw()) as f32;
247 let l = dx * dx + dy * dy;
248 if l > best_len {
249 best_len = l;
250 best = i;
251 }
252 }
253 if best == 0 {
254 return None;
255 }
256 let a = pts[best - 1];
257 let b = pts[best];
258 let mid = (
259 Sp((a.0.raw() + b.0.raw()) / 2),
260 Sp((a.1.raw() + b.1.raw()) / 2),
261 );
262 let perp = match unit(a, b) {
263 Some((ux, uy)) => (-uy, ux),
264 None => (0.0, -1.0),
265 };
266 Some((mid, perp))
267}
268
269/// An anchor a little way along the first segment from the source, and the unit perpendicular to that
270/// segment, so a branch label ("Y"/"N") sits by the decision it leaves rather than out at a far corner.
271/// The offset is `lead` past the source, clamped to the segment so a short first segment still anchors.
272pub fn label_anchor_near_source(pts: &[(Sp, Sp)], lead: Sp) -> Option<((Sp, Sp), (f32, f32))> {
273 if pts.len() < 2 {
274 return None;
275 }
276 let a = pts[0];
277 let b = pts[1];
278 let (ux, uy) = unit(a, b)?;
279 // The segment length in points, so the lead does not overshoot a short first segment.
280 let seg_len = {
281 let dx = (b.0.to_pt() - a.0.to_pt()) as f32;
282 let dy = (b.1.to_pt() - a.1.to_pt()) as f32;
283 (dx * dx + dy * dy).sqrt()
284 };
285 let d = (lead.to_pt() as f32).min(seg_len * 0.6);
286 let anchor = (
287 Sp::from_pt((a.0.to_pt() as f32 + ux * d) as f64),
288 Sp::from_pt((a.1.to_pt() as f32 + uy * d) as f64),
289 );
290 Some((anchor, (-uy, ux)))
291}
292
293/// The unit vector from `a` to `b` in points, or `None` if they coincide.
294fn unit(a: (Sp, Sp), b: (Sp, Sp)) -> Option<(f32, f32)> {
295 let dx = (b.0.to_pt() - a.0.to_pt()) as f32;
296 let dy = (b.1.to_pt() - a.1.to_pt()) as f32;
297 let d = (dx * dx + dy * dy).sqrt();
298 if d <= f32::EPSILON {
299 return None;
300 }
301 Some((dx / d, dy / d))
302}
303
304/// A scaled-point pair as a graphics point.
305fn sp_pt(p: (Sp, Sp)) -> Pt {
306 Pt::new(p.0.to_pt() as f32, p.1.to_pt() as f32)
307}