pub(crate) fn pt_in_rect(x: f64, y: f64, r: (f64, f64, f64, f64)) -> bool {
x >= r.0 && x <= r.2 && y >= r.1 && y <= r.3
}
pub(crate) fn orient(ax: f64, ay: f64, bx: f64, by: f64, cx: f64, cy: f64) -> f64 {
(bx - ax) * (cy - ay) - (by - ay) * (cx - ax)
}
pub(crate) fn on_seg(ax: f64, ay: f64, bx: f64, by: f64, cx: f64, cy: f64) -> bool {
cx >= ax.min(bx) && cx <= ax.max(bx) && cy >= ay.min(by) && cy <= ay.max(by)
}
pub(crate) fn segs_cross(a: (f64, f64), b: (f64, f64), c: (f64, f64), d: (f64, f64)) -> bool {
let (o1, o2, o3, o4) = (
orient(a.0, a.1, b.0, b.1, c.0, c.1),
orient(a.0, a.1, b.0, b.1, d.0, d.1),
orient(c.0, c.1, d.0, d.1, a.0, a.1),
orient(c.0, c.1, d.0, d.1, b.0, b.1),
);
((o1 > 0.0) != (o2 > 0.0) && (o3 > 0.0) != (o4 > 0.0))
|| (o1 == 0.0 && on_seg(a.0, a.1, b.0, b.1, c.0, c.1))
|| (o2 == 0.0 && on_seg(a.0, a.1, b.0, b.1, d.0, d.1))
|| (o3 == 0.0 && on_seg(c.0, c.1, d.0, d.1, a.0, a.1))
|| (o4 == 0.0 && on_seg(c.0, c.1, d.0, d.1, b.0, b.1))
}
pub(crate) fn seg_hits_rect(x1: f64, y1: f64, x2: f64, y2: f64, r: (f64, f64, f64, f64)) -> bool {
pt_in_rect(x1, y1, r)
|| pt_in_rect(x2, y2, r)
|| segs_cross((x1, y1), (x2, y2), (r.0, r.1), (r.2, r.1))
|| segs_cross((x1, y1), (x2, y2), (r.2, r.1), (r.2, r.3))
|| segs_cross((x1, y1), (x2, y2), (r.2, r.3), (r.0, r.3))
|| segs_cross((x1, y1), (x2, y2), (r.0, r.3), (r.0, r.1))
}
pub(crate) fn rects_overlap(a: (f64, f64, f64, f64), b: (f64, f64, f64, f64)) -> bool {
a.0 < b.2 && a.2 > b.0 && a.1 < b.3 && a.3 > b.1
}
pub(crate) fn node_box(x: f64, y: f64) -> (f64, f64, f64, f64) {
(x - 1.7, y - 0.45, x + 1.7, y + 0.45)
}
pub(crate) fn route_edge(
from: &str,
to: &str,
a: (f64, f64),
b: (f64, f64),
boxes: &[(String, (f64, f64, f64, f64))],
line: usize,
stmt: &str,
) -> Result<Option<(f64, f64)>, crate::error::Error> {
let blocked: Vec<(f64, f64, f64, f64)> = boxes
.iter()
.filter(|(id, _)| id != from && id != to)
.map(|(_, r)| *r)
.collect();
if !blocked.iter().any(|r| seg_hits_rect(a.0, a.1, b.0, b.1, *r)) {
return Ok(None);
}
let (dx, dy) = (b.0 - a.0, b.1 - a.1);
let len = (dx * dx + dy * dy).sqrt().max(1e-9);
let (px, py) = (-dy / len, dx / len);
let hit = blocked
.iter()
.find(|r| seg_hits_rect(a.0, a.1, b.0, b.1, **r))
.unwrap();
let (cx, cy) = ((hit.0 + hit.2) / 2.0, (hit.1 + hit.3) / 2.0);
let (mx, my) = ((a.0 + b.0) / 2.0, (a.1 + b.1) / 2.0);
let side = if (mx + px - cx) * px + (my + py - cy) * py >= 0.0 { 1.0 } else { -1.0 };
for k in 1..=8 {
let t = 0.6 * k as f64 * side;
let m = (mx + px * t, my + py * t);
let clear = !blocked.iter().any(|r| {
seg_hits_rect(a.0, a.1, m.0, m.1, *r) || seg_hits_rect(m.0, m.1, b.0, b.1, *r)
});
if clear {
return Ok(Some(m));
}
}
Err(crate::error::Error::new(
format!("edge {from}->{to} crosses a node box; reroute exhausted"),
line,
stmt.to_string(),
))
}
pub(crate) fn cluster_box(pts: &[(f64, f64)]) -> Option<((f64, f64, f64, f64), (f64, f64))> {
if pts.is_empty() {
return None;
}
let (x0, x1) = (
pts.iter().map(|p| p.0).fold(f64::INFINITY, f64::min) - 1.2,
pts.iter().map(|p| p.0).fold(f64::NEG_INFINITY, f64::max) + 1.2,
);
let (yt, yb) = (
pts.iter().map(|p| p.1).fold(f64::NEG_INFINITY, f64::max) + 1.1,
pts.iter().map(|p| p.1).fold(f64::INFINITY, f64::min) - 0.7,
);
Some(((x0, yt, x1, yb), (x0 + 0.1, yt - 0.1)))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn moved_helpers_still_green() {
assert!(segs_cross((0.0, 0.0), (2.0, 2.0), (0.0, 2.0), (2.0, 0.0)));
assert!(!segs_cross((0.0, 0.0), (1.0, 0.0), (0.0, 1.0), (1.0, 1.0)));
assert!(seg_hits_rect(0.0, 0.0, 2.0, 0.0, (0.9, -0.5, 1.1, 0.5)));
assert!(!seg_hits_rect(0.0, 0.0, 2.0, 0.0, (0.9, 0.6, 1.1, 1.0)));
assert!(rects_overlap((0.0, 0.0, 1.0, 1.0), (0.5, 0.5, 2.0, 2.0)));
}
#[test]
fn clean_edge_is_none() {
let b = vec![("c".to_string(), node_box(9.0, 9.0))];
assert_eq!(route_edge("a", "b", (0.0, 0.0), (2.0, 0.0), &b, 1, "a-->b").unwrap(), None);
}
#[test]
fn push_goes_away_from_the_box() {
let b = vec![("c".to_string(), node_box(0.0, -2.4))];
let m = route_edge("a", "d", (0.0, 0.0), (0.0, -4.8), &b, 2, "a-->d").unwrap().unwrap();
assert!(m.0.abs() > 0.5, "pushed aside, got {m:?}");
assert!(!seg_hits_rect(0.0, 0.0, m.0, m.1, b[0].1));
assert!(!seg_hits_rect(m.0, m.1, 0.0, -4.8, b[0].1));
}
#[test]
fn iteration_bound_errors() {
let wall: Vec<(String, (f64, f64, f64, f64))> = (-24..=24)
.map(|k| (format!("w{k}"), (k as f64 * 0.5 - 0.4, -3.0, k as f64 * 0.5 + 0.4, -1.0)))
.collect();
let e = route_edge("a", "b", (0.0, 0.0), (0.0, -4.8), &wall, 3, "a-->b").unwrap_err();
assert!(e.to_string().contains("a->b"), "names the edge, got: {e}");
assert!(e.to_string().contains(" --> line 3"), "line carried, got: {e}");
}
}