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 | |
| 9 | use crate::diagram::shape::{ |
| 10 | port_of, |
| 11 | Port, |
| 12 | Rect, |
| 13 | }; |
| 14 | use crate::ir::Sp; |
| 15 | |
| 16 | use oxedyne_fe2o3_core::prelude::*; |
| 17 | use 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)] |
| 26 | pub 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)] |
| 34 | pub 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)] |
| 44 | pub enum Dir4 { |
| 45 | Up, |
| 46 | Down, |
| 47 | Left, |
| 48 | Right, |
| 49 | } |
| 50 | |
| 51 | impl 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. |
| 68 | pub 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. |
| 81 | pub 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. |
| 107 | pub 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. |
| 128 | pub 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. |
| 171 | fn 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. |
| 182 | fn 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. |
| 187 | pub 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. |
| 203 | pub 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. |
| 216 | pub 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. |
| 241 | pub 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. |
| 272 | pub 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. |
| 294 | fn 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. |
| 305 | fn sp_pt(p: (Sp, Sp)) -> Pt { |
| 306 | Pt::new(p.0.to_pt() as f32, p.1.to_pt() as f32) |
| 307 | } |