use crate::style::EdgeCurve;
const BEZIER_SEGMENT_LENGTH: f32 = 80.0;
fn adaptive_bezier_length(start: [f32; 2], end: [f32; 2]) -> f32 {
let dx = end[0] - start[0];
let dy = end[1] - start[1];
let d = (dx * dx + dy * dy).sqrt();
BEZIER_SEGMENT_LENGTH.min(d * 0.5).max(1.0)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Border {
Left,
Right,
Top,
Bottom,
}
impl Border {
pub(crate) fn normal(self) -> [f32; 2] {
match self {
Border::Left => [-1.0, 0.0],
Border::Right => [1.0, 0.0],
Border::Top => [0.0, -1.0],
Border::Bottom => [0.0, 1.0],
}
}
pub(crate) fn opposite(self) -> Border {
match self {
Border::Left => Border::Right,
Border::Right => Border::Left,
Border::Top => Border::Bottom,
Border::Bottom => Border::Top,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub(crate) enum Hand {
Clockwise,
CounterClockwise,
}
impl Hand {
pub(crate) fn flip(self) -> Hand {
match self {
Hand::Clockwise => Hand::CounterClockwise,
Hand::CounterClockwise => Hand::Clockwise,
}
}
pub(crate) fn sign(self) -> f32 {
match self {
Hand::Clockwise => 1.0,
Hand::CounterClockwise => -1.0,
}
}
}
pub(crate) type PathSeg = iced_nodegraph_sdf::PathSeg;
#[derive(Debug, Clone, Copy, PartialEq)]
pub(crate) struct Orbit {
pub center: [f32; 2],
pub radius: f32,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub(crate) struct Attachment {
pub point: [f32; 2],
pub direction: [f32; 2],
}
impl Orbit {
pub(crate) fn attachment(&self, p: [f32; 2], hand: Hand) -> Option<Attachment> {
let to_p = [p[0] - self.center[0], p[1] - self.center[1]];
let d = (to_p[0] * to_p[0] + to_p[1] * to_p[1]).sqrt();
if d <= self.radius {
return None;
}
let alpha = (self.radius / d).acos();
let theta = to_p[1].atan2(to_p[0]) + hand.sign() * alpha;
Some(Attachment {
point: [
self.center[0] + self.radius * theta.cos(),
self.center[1] + self.radius * theta.sin(),
],
direction: travel_dir(theta, hand),
})
}
pub(crate) fn sweep(&self, from: [f32; 2], to: [f32; 2], hand: Hand) -> f32 {
let angle = |p: [f32; 2]| (p[1] - self.center[1]).atan2(p[0] - self.center[0]);
let delta = (angle(to) - angle(from)) * hand.sign();
hand.sign() * delta.rem_euclid(std::f32::consts::TAU)
}
pub(crate) fn ring_distance(&self, p: [f32; 2]) -> f32 {
let d = [p[0] - self.center[0], p[1] - self.center[1]];
((d[0] * d[0] + d[1] * d[1]).sqrt() - self.radius).abs()
}
}
fn travel_dir(theta: f32, hand: Hand) -> [f32; 2] {
let s = hand.sign();
[-s * theta.sin(), s * theta.cos()]
}
fn arc_start_angle(cursor: [f32; 2], center: [f32; 2]) -> f32 {
(cursor[1] - center[1]).atan2(cursor[0] - center[0])
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub(crate) enum Hop {
Pin { point: [f32; 2], side: Border },
Wrap { orbit: Orbit },
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct EdgePath {
pub start: [f32; 2],
pub segs: Vec<PathSeg>,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub(crate) struct Nearest {
pub distance: f32,
pub arc_len: f32,
}
impl EdgePath {
pub(crate) fn into_shape(self) -> iced_nodegraph_sdf::Shape {
iced_nodegraph_sdf::Shape::path(self.start, self.segs)
}
pub(crate) fn distance(&self, p: [f32; 2]) -> f32 {
let mut cursor = self.start;
let mut best = f32::MAX;
for seg in &self.segs {
best = best.min(seg_nearest(cursor, seg, p).distance);
cursor = seg_end(cursor, seg);
}
best
}
pub(crate) fn intersects(&self, a: [f32; 2], b: [f32; 2]) -> bool {
let mut cursor = self.start;
for seg in &self.segs {
if seg_intersects(cursor, seg, a, b) {
return true;
}
cursor = seg_end(cursor, seg);
}
false
}
pub(crate) fn nearest(&self, p: [f32; 2]) -> Nearest {
let mut cursor = self.start;
let mut walked = 0.0;
let mut best = Nearest {
distance: dist2(p, self.start).sqrt(),
arc_len: 0.0,
};
for seg in &self.segs {
let near = seg_nearest(cursor, seg, p);
if near.distance < best.distance {
best = Nearest {
distance: near.distance,
arc_len: walked + near.along,
};
}
walked += near.length;
cursor = seg_end(cursor, seg);
}
best
}
pub(crate) fn total_len(&self) -> f32 {
let mut cursor = self.start;
let mut walked = 0.0;
for seg in &self.segs {
walked += seg_length(cursor, seg);
cursor = seg_end(cursor, seg);
}
walked
}
pub(crate) fn slice(&self, from_len: f32, to_len: f32) -> EdgePath {
let total = self.total_len();
let from = from_len.clamp(0.0, total);
let to = to_len.clamp(0.0, total);
let mut out = EdgePath {
start: self.point_at(from),
segs: Vec::new(),
};
if to <= from {
return out;
}
let mut cursor = self.start;
let mut walked = 0.0;
for seg in &self.segs {
let len = seg_length(cursor, seg);
let end = walked + len;
if end > from && walked < to && len > 1e-6 {
let t0 = seg_param_at_len(cursor, seg, from - walked);
let t1 = seg_param_at_len(cursor, seg, to - walked);
if t1 > t0 {
out.segs.push(seg_slice(cursor, seg, t0, t1));
}
}
walked = end;
cursor = seg_end(cursor, seg);
}
out
}
pub(crate) fn point_at(&self, len: f32) -> [f32; 2] {
let mut cursor = self.start;
let mut walked = 0.0;
for seg in &self.segs {
let seg_len = seg_length(cursor, seg);
if len <= walked + seg_len {
let t = seg_param_at_len(cursor, seg, len - walked);
return seg_point_at(cursor, seg, t);
}
walked += seg_len;
cursor = seg_end(cursor, seg);
}
cursor
}
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct Built {
pub path: EdgePath,
pub touches: Vec<RingTouch>,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub(crate) struct RingTouch {
pub hop: usize,
pub entry: [f32; 2],
pub exit: [f32; 2],
pub span: (f32, f32),
}
pub(crate) fn build(hops: &[Hop], curve: &EdgeCurve) -> Built {
let empty = Built {
path: EdgePath {
start: [0.0, 0.0],
segs: Vec::new(),
},
touches: Vec::new(),
};
let n = hops.len();
if n == 0 {
return empty;
}
let aim = |i: usize| match hops[i] {
Hop::Pin { point, .. } => point,
Hop::Wrap { orbit } => orbit.center,
};
let hand_at = |i: usize| -> Option<Hand> {
if i == 0 || i + 1 == n {
return None;
}
let Hop::Wrap { orbit } = hops[i] else {
return None;
};
pick_hand(orbit, aim(i - 1), aim(i + 1))
};
let exit_toward = |hop: usize, orbit: Orbit, hand: Hand| -> Option<Attachment> {
if hop + 1 == n {
return None;
}
match hops[hop + 1] {
Hop::Wrap { orbit: far } => Some(belt((orbit, hand), (far, hand_at(hop + 1)?))?.0),
Hop::Pin { .. } => {
let a = orbit.attachment(aim(hop + 1), hand.flip())?;
Some(Attachment {
point: a.point,
direction: negate(a.direction),
})
}
}
};
let mut segs = Vec::with_capacity(n * 2);
let mut touches = Vec::new();
let (start, start_side) = match hops[0] {
Hop::Pin { point, side } => (point, side),
Hop::Wrap { .. } => return empty,
};
let mut dir = start_side.normal();
let mut on_tangent = false;
let mut cursor = start;
let mut walked = 0.0;
let mut bound_hand = None;
for i in 1..n {
let bound = bound_hand.take();
let from = Leg {
point: cursor,
dir,
tangent: on_tangent,
};
match hops[i] {
Hop::Pin { point, side } => {
let normal = side.normal();
push_leg(
&mut segs,
&mut walked,
from,
Leg {
point,
dir: normal,
tangent: false,
},
curve,
);
cursor = point;
dir = normal;
on_tangent = false;
}
Hop::Wrap { orbit } => {
if i + 1 == n {
continue;
}
let realized = |hand: Hand| -> Option<(Attachment, Attachment, Option<Hand>)> {
let entry = tangent_from_the_run(orbit, hand, from)?;
let Hop::Wrap { orbit: far } = hops[i + 1] else {
return Some((entry, exit_toward(i, orbit, hand)?, None));
};
let (exit, far_hand, _) = [Hand::Clockwise, Hand::CounterClockwise]
.into_iter()
.filter_map(|far_hand| {
let (exit, landing) = belt((orbit, hand), (far, far_hand))?;
let leaving = exit_toward(i + 1, far, far_hand)?;
let sweep = far.sweep(landing.point, leaving.point, far_hand);
Some((exit, far_hand, sweep))
})
.min_by(|a, b| a.2.abs().total_cmp(&b.2.abs()))?;
Some((entry, exit, Some(far_hand)))
};
let wrap = [Hand::Clockwise, Hand::CounterClockwise]
.into_iter()
.filter(|hand| bound.is_none_or(|committed| committed == *hand))
.filter_map(|hand| {
let (entry, exit, beyond) = realized(hand)?;
let sweep = orbit.sweep(entry.point, exit.point, hand);
Some((entry, exit, sweep, beyond))
})
.min_by(|a, b| a.2.abs().total_cmp(&b.2.abs()));
let Some((entry, exit, sweep, beyond)) = wrap else {
continue;
};
push_leg(
&mut segs,
&mut walked,
from,
Leg {
point: entry.point,
dir: negate(entry.direction),
tangent: true,
},
curve,
);
let arc = PathSeg::Arc {
center: orbit.center,
radius: orbit.radius,
sweep,
};
let span_start = walked;
walked += seg_length(entry.point, &arc);
segs.push(arc);
touches.push(RingTouch {
hop: i,
entry: entry.point,
exit: exit.point,
span: (span_start, walked),
});
cursor = exit.point;
dir = exit.direction;
on_tangent = true;
bound_hand = beyond;
}
}
}
Built {
path: EdgePath { start, segs },
touches,
}
}
fn pick_hand(orbit: Orbit, prev_aim: [f32; 2], next_aim: [f32; 2]) -> Option<Hand> {
let swept = |hand: Hand| {
let entry = orbit.attachment(prev_aim, hand)?;
let exit = orbit.attachment(next_aim, hand.flip())?;
Some(orbit.sweep(entry.point, exit.point, hand).abs())
};
match (swept(Hand::Clockwise), swept(Hand::CounterClockwise)) {
(Some(cw), Some(ccw)) if ccw < cw => Some(Hand::CounterClockwise),
(Some(_), _) => Some(Hand::Clockwise),
(None, Some(_)) => Some(Hand::CounterClockwise),
(None, None) => None,
}
}
fn negate(v: [f32; 2]) -> [f32; 2] {
[-v[0], -v[1]]
}
fn rot90(v: [f32; 2]) -> [f32; 2] {
[-v[1], v[0]]
}
pub(crate) fn belt(from: (Orbit, Hand), to: (Orbit, Hand)) -> Option<(Attachment, Attachment)> {
let (a, ha) = from;
let (b, hb) = to;
let d = [b.center[0] - a.center[0], b.center[1] - a.center[1]];
let span = (d[0] * d[0] + d[1] * d[1]).sqrt();
let offset = ha.sign() * a.radius - hb.sign() * b.radius;
if span <= offset.abs() {
return None;
}
let heading = d[1].atan2(d[0]) + (offset / span).asin();
let u = [heading.cos(), heading.sin()];
let n = rot90(u);
let touch = |orbit: Orbit, hand: Hand| {
let s = -hand.sign();
Attachment {
point: [
orbit.center[0] + orbit.radius * s * n[0],
orbit.center[1] + orbit.radius * s * n[1],
],
direction: u,
}
};
Some((touch(a, ha), touch(b, hb)))
}
const TAUT_EXIT: f32 = 45.0;
const TANGENT_PASSES: usize = 4;
const TANGENT_SETTLED: f32 = 0.05;
fn tangent_from_the_run(orbit: Orbit, hand: Hand, from: Leg) -> Option<Attachment> {
let mut touch = orbit.attachment(from.point, hand)?;
if from.tangent {
return Some(touch);
}
for _ in 0..TANGENT_PASSES {
let (reach, _) = leg_reaches(
from,
Leg {
point: touch.point,
dir: negate(touch.direction),
tangent: true,
},
);
let pivot = [
from.point[0] + from.dir[0] * reach,
from.point[1] + from.dir[1] * reach,
];
let Some(next) = orbit.attachment(pivot, hand) else {
break;
};
let moved = (next.point[0] - touch.point[0]).hypot(next.point[1] - touch.point[1]);
touch = next;
if moved < TANGENT_SETTLED {
break;
}
}
Some(touch)
}
#[derive(Debug, Clone, Copy)]
struct Leg {
point: [f32; 2],
dir: [f32; 2],
tangent: bool,
}
fn leg_reaches(from: Leg, to: Leg) -> (f32, f32) {
let l = adaptive_bezier_length(from.point, to.point);
let d = [to.point[0] - from.point[0], to.point[1] - from.point[1]];
match (from.tangent, to.tangent) {
(false, true) => (bow_limited(l, from.dir, d), l),
(true, false) => (l, bow_limited(l, to.dir, d)),
_ => (l, l),
}
}
fn push_leg(segs: &mut Vec<PathSeg>, walked: &mut f32, from: Leg, to: Leg, curve: &EdgeCurve) {
let Leg {
point: from_point,
dir: from_dir,
..
} = from;
let Leg {
point: to_point,
dir: to_dir,
..
} = to;
let seg = match curve {
EdgeCurve::Line => PathSeg::Line { to: to_point },
EdgeCurve::BezierCubic => {
let (l_from, l_to) = leg_reaches(from, to);
let c1 = [
from_point[0] + from_dir[0] * l_from,
from_point[1] + from_dir[1] * l_from,
];
let c2 = [
to_point[0] + to_dir[0] * l_to,
to_point[1] + to_dir[1] * l_to,
];
PathSeg::Bezier {
c1,
c2,
to: to_point,
}
}
};
*walked += seg_length(from_point, &seg);
segs.push(seg);
}
fn bow_limited(l: f32, dir: [f32; 2], span: [f32; 2]) -> f32 {
let len = (span[0] * span[0] + span[1] * span[1]).sqrt();
if len <= f32::EPSILON {
return l;
}
let turn = (dir[0] * span[1] - dir[1] * span[0]).abs() / len;
l.min(TAUT_EXIT / turn.max(f32::EPSILON))
}
const CURVE_FLATTEN_SEGMENTS: usize = 32;
fn seg_end(cursor: [f32; 2], seg: &PathSeg) -> [f32; 2] {
match *seg {
PathSeg::Line { to } | PathSeg::Bezier { to, .. } => to,
PathSeg::Arc {
center,
radius,
sweep,
} => {
let end = arc_start_angle(cursor, center) + sweep;
[
center[0] + radius * end.cos(),
center[1] + radius * end.sin(),
]
}
}
}
struct SegNearest {
distance: f32,
along: f32,
length: f32,
}
fn seg_nearest(cursor: [f32; 2], seg: &PathSeg, p: [f32; 2]) -> SegNearest {
let by_param = |(distance, t): (f32, f32)| {
let length = seg_length(cursor, seg);
SegNearest {
distance,
along: t * length,
length,
}
};
match *seg {
PathSeg::Line { to } => by_param(nearest_on_segment(p, cursor, to)),
PathSeg::Arc {
center,
radius,
sweep,
} => by_param(nearest_on_arc(p, cursor, center, radius, sweep)),
PathSeg::Bezier { c1, c2, to } => nearest_on_bezier(p, cursor, c1, c2, to),
}
}
fn seg_point_at(cursor: [f32; 2], seg: &PathSeg, t: f32) -> [f32; 2] {
match *seg {
PathSeg::Line { to } => lerp(cursor, to, t),
PathSeg::Arc {
center,
radius,
sweep,
} => {
let a = arc_start_angle(cursor, center) + sweep * t;
[center[0] + radius * a.cos(), center[1] + radius * a.sin()]
}
PathSeg::Bezier { c1, c2, to } => cubic_point(cursor, c1, c2, to, t),
}
}
fn lerp(a: [f32; 2], b: [f32; 2], t: f32) -> [f32; 2] {
[a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t]
}
fn seg_length(cursor: [f32; 2], seg: &PathSeg) -> f32 {
match *seg {
PathSeg::Line { to } => dist2(cursor, to).sqrt(),
PathSeg::Arc { radius, sweep, .. } => sweep.abs() * radius,
PathSeg::Bezier { c1, c2, to } => {
let mut prev = cursor;
let mut len = 0.0;
for i in 1..=CURVE_FLATTEN_SEGMENTS {
let cur = cubic_point(cursor, c1, c2, to, i as f32 / CURVE_FLATTEN_SEGMENTS as f32);
len += dist2(prev, cur).sqrt();
prev = cur;
}
len
}
}
}
fn seg_param_at_len(cursor: [f32; 2], seg: &PathSeg, len: f32) -> f32 {
match *seg {
PathSeg::Line { .. } | PathSeg::Arc { .. } => {
(len / seg_length(cursor, seg).max(1e-6)).clamp(0.0, 1.0)
}
PathSeg::Bezier { c1, c2, to } => cubic_param_at_len([cursor, c1, c2, to], len),
}
}
fn seg_slice(cursor: [f32; 2], seg: &PathSeg, t0: f32, t1: f32) -> PathSeg {
match *seg {
PathSeg::Line { to } => PathSeg::Line {
to: lerp(cursor, to, t1),
},
PathSeg::Arc {
center,
radius,
sweep,
} => PathSeg::Arc {
center,
radius,
sweep: sweep * (t1 - t0),
},
PathSeg::Bezier { c1, c2, to } => {
let [_, c1, c2, to] = cubic_between([cursor, c1, c2, to], t0, t1);
PathSeg::Bezier { c1, c2, to }
}
}
}
fn seg_intersects(cursor: [f32; 2], seg: &PathSeg, a: [f32; 2], b: [f32; 2]) -> bool {
match *seg {
PathSeg::Line { to } => segments_intersect(cursor, to, a, b),
PathSeg::Arc { .. } | PathSeg::Bezier { .. } => {
let mut prev = cursor;
for i in 1..=CURVE_FLATTEN_SEGMENTS {
let t = i as f32 / CURVE_FLATTEN_SEGMENTS as f32;
let cur = seg_point_at(cursor, seg, t);
if segments_intersect(prev, cur, a, b) {
return true;
}
prev = cur;
}
false
}
}
}
fn angle_in_sweep(angle: f32, start: f32, sweep: f32) -> bool {
if sweep == 0.0 {
return (angle - start).rem_euclid(std::f32::consts::TAU) < 1e-6;
}
let off = ((angle - start) * sweep.signum()).rem_euclid(std::f32::consts::TAU);
off <= sweep.abs() + 1e-6
}
fn dist2(a: [f32; 2], b: [f32; 2]) -> f32 {
let dx = a[0] - b[0];
let dy = a[1] - b[1];
dx * dx + dy * dy
}
fn nearest_on_segment(p: [f32; 2], a: [f32; 2], b: [f32; 2]) -> (f32, f32) {
let ab = [b[0] - a[0], b[1] - a[1]];
let len2 = ab[0] * ab[0] + ab[1] * ab[1];
let t = if len2 > 1e-12 {
(((p[0] - a[0]) * ab[0] + (p[1] - a[1]) * ab[1]) / len2).clamp(0.0, 1.0)
} else {
0.0
};
let proj = [a[0] + ab[0] * t, a[1] + ab[1] * t];
(dist2(p, proj).sqrt(), t)
}
fn nearest_on_arc(
p: [f32; 2],
cursor: [f32; 2],
center: [f32; 2],
radius: f32,
sweep: f32,
) -> (f32, f32) {
let start = arc_start_angle(cursor, center);
let rel = [p[0] - center[0], p[1] - center[1]];
let ang = rel[1].atan2(rel[0]);
if angle_in_sweep(ang, start, sweep) {
let off = ((ang - start) * sweep.signum()).rem_euclid(std::f32::consts::TAU);
let t = (off / sweep.abs().max(1e-6)).clamp(0.0, 1.0);
return (
((rel[0] * rel[0] + rel[1] * rel[1]).sqrt() - radius).abs(),
t,
);
}
let end = start + sweep;
let a = [
center[0] + radius * start.cos(),
center[1] + radius * start.sin(),
];
let b = [
center[0] + radius * end.cos(),
center[1] + radius * end.sin(),
];
let (to_start, to_end) = (dist2(p, a).sqrt(), dist2(p, b).sqrt());
if to_end < to_start {
return (to_end, 1.0);
}
(to_start, 0.0)
}
fn cubic_point(p0: [f32; 2], p1: [f32; 2], p2: [f32; 2], p3: [f32; 2], t: f32) -> [f32; 2] {
let mt = 1.0 - t;
let a = mt * mt * mt;
let b = 3.0 * mt * mt * t;
let c = 3.0 * mt * t * t;
let d = t * t * t;
[
a * p0[0] + b * p1[0] + c * p2[0] + d * p3[0],
a * p0[1] + b * p1[1] + c * p2[1] + d * p3[1],
]
}
fn cubic_split(p: [[f32; 2]; 4], t: f32) -> ([[f32; 2]; 4], [[f32; 2]; 4]) {
let a = lerp(p[0], p[1], t);
let b = lerp(p[1], p[2], t);
let c = lerp(p[2], p[3], t);
let d = lerp(a, b, t);
let e = lerp(b, c, t);
let f = lerp(d, e, t);
([p[0], a, d, f], [f, e, c, p[3]])
}
fn cubic_between(p: [[f32; 2]; 4], t0: f32, t1: f32) -> [[f32; 2]; 4] {
let (left, _) = cubic_split(p, t1);
cubic_split(left, t0 / t1.max(f32::EPSILON)).1
}
fn nearest_on_bezier(
p: [f32; 2],
p0: [f32; 2],
p1: [f32; 2],
p2: [f32; 2],
p3: [f32; 2],
) -> SegNearest {
let mut prev = p0;
let mut walked = 0.0;
let mut best = SegNearest {
distance: f32::MAX,
along: 0.0,
length: 0.0,
};
for i in 1..=CURVE_FLATTEN_SEGMENTS {
let cur = cubic_point(p0, p1, p2, p3, i as f32 / CURVE_FLATTEN_SEGMENTS as f32);
let chord = dist2(prev, cur).sqrt();
let (distance, along) = nearest_on_segment(p, prev, cur);
if distance < best.distance {
best.distance = distance;
best.along = walked + along * chord;
}
walked += chord;
prev = cur;
}
best.length = walked;
best
}
fn cubic_param_at_len(p: [[f32; 2]; 4], len: f32) -> f32 {
if len <= 0.0 {
return 0.0;
}
let step = 1.0 / CURVE_FLATTEN_SEGMENTS as f32;
let mut prev = p[0];
let mut walked = 0.0;
for i in 1..=CURVE_FLATTEN_SEGMENTS {
let t = i as f32 * step;
let cur = cubic_point(p[0], p[1], p[2], p[3], t);
let chord = dist2(prev, cur).sqrt();
if walked + chord >= len {
let along = if chord > 1e-6 {
(len - walked) / chord
} else {
0.0
};
return t - step + along * step;
}
walked += chord;
prev = cur;
}
1.0
}
fn cross(o: [f32; 2], a: [f32; 2], b: [f32; 2]) -> f32 {
(a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0])
}
fn segments_intersect(p1: [f32; 2], p2: [f32; 2], p3: [f32; 2], p4: [f32; 2]) -> bool {
let d1 = cross(p3, p4, p1);
let d2 = cross(p3, p4, p2);
let d3 = cross(p1, p2, p3);
let d4 = cross(p1, p2, p4);
((d1 > 0.0) != (d2 > 0.0)) && ((d3 > 0.0) != (d4 > 0.0))
}
fn chord_crossing(s: [[f32; 2]; 2], t: [[f32; 2]; 2]) -> Option<[f32; 2]> {
if !segments_intersect(s[0], s[1], t[0], t[1]) || shares_end(s, t) {
return None;
}
let d1 = cross(t[0], t[1], s[0]);
let d2 = cross(t[0], t[1], s[1]);
Some(lerp(s[0], s[1], d1 / (d1 - d2)))
}
fn shares_end(s: [[f32; 2]; 2], t: [[f32; 2]; 2]) -> bool {
s.iter().any(|p| t.contains(p))
}
pub(crate) fn polyline(path: &EdgePath) -> Vec<[f32; 2]> {
let mut points = Vec::with_capacity(1 + path.segs.len() * CURVE_FLATTEN_SEGMENTS);
points.push(path.start);
let mut cursor = path.start;
for seg in &path.segs {
match seg {
PathSeg::Line { to } => points.push(*to),
PathSeg::Arc { .. } | PathSeg::Bezier { .. } => {
for i in 1..=CURVE_FLATTEN_SEGMENTS {
let t = i as f32 / CURVE_FLATTEN_SEGMENTS as f32;
points.push(seg_point_at(cursor, seg, t));
}
}
}
cursor = seg_end(cursor, seg);
}
points
}
#[cfg(test)]
pub(crate) fn crossing_points(a: &EdgePath, b: &EdgePath) -> Vec<[f32; 2]> {
let (pa, pb) = (polyline(a), polyline(b));
let mut points = Vec::new();
for s in pa.windows(2) {
for t in pb.windows(2) {
if let Some(p) = chord_crossing([s[0], s[1]], [t[0], t[1]]) {
points.push(p);
}
}
}
points
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub(crate) struct Corridor {
pub from: Orbit,
pub to: Orbit,
}
struct Band {
from: Orbit,
to: Orbit,
axis: [f32; 2],
len2: f32,
}
impl Band {
fn new(corridor: &Corridor) -> Option<Self> {
let axis = [
corridor.to.center[0] - corridor.from.center[0],
corridor.to.center[1] - corridor.from.center[1],
];
let len2 = axis[0] * axis[0] + axis[1] * axis[1];
(len2 >= 1e-6).then_some(Self {
from: corridor.from,
to: corridor.to,
axis,
len2,
})
}
fn along(&self, point: [f32; 2]) -> f32 {
((point[0] - self.from.center[0]) * self.axis[0]
+ (point[1] - self.from.center[1]) * self.axis[1])
/ self.len2
}
fn holds(&self, point: [f32; 2]) -> bool {
(0.0..=1.0).contains(&self.along(point))
&& dist2(point, self.from.center) > self.from.radius * self.from.radius
&& dist2(point, self.to.center) > self.to.radius * self.to.radius
}
fn rejects(&self, chord: [[f32; 2]; 2]) -> bool {
let (one, other) = (self.along(chord[0]), self.along(chord[1]));
if (one < 0.0 && other < 0.0) || (one > 1.0 && other > 1.0) {
return true;
}
let inside = |circle: &Orbit| {
chord
.iter()
.all(|end| dist2(*end, circle.center) <= circle.radius * circle.radius)
};
inside(&self.from) || inside(&self.to)
}
}
fn chord_bounds(chord: [[f32; 2]; 2]) -> [f32; 4] {
[
chord[0][0].min(chord[1][0]),
chord[0][1].min(chord[1][1]),
chord[0][0].max(chord[1][0]),
chord[0][1].max(chord[1][1]),
]
}
fn bounds_overlap(s: [f32; 4], t: [f32; 4]) -> bool {
s[0] <= t[2] && t[0] <= s[2] && s[1] <= t[3] && t[1] <= s[3]
}
pub(crate) fn crossings_between_flattened(
a: &[[f32; 2]],
b: &[[f32; 2]],
corridors: &[Corridor],
) -> usize {
let bands: Vec<Band> = corridors.iter().filter_map(Band::new).collect();
if bands.is_empty() {
return 0;
}
let live = |points: &[[f32; 2]]| -> Vec<([[f32; 2]; 2], [f32; 4])> {
points
.windows(2)
.map(|pair| [pair[0], pair[1]])
.filter(|chord| bands.iter().any(|band| !band.rejects(*chord)))
.map(|chord| (chord, chord_bounds(chord)))
.collect()
};
let (one, other) = (live(a), live(b));
let mut count = 0;
for (s, s_bounds) in &one {
for (t, t_bounds) in &other {
if !bounds_overlap(*s_bounds, *t_bounds) {
continue;
}
if let Some(point) = chord_crossing(*s, *t)
&& bands.iter().any(|band| band.holds(point))
{
count += 1;
}
}
}
count
}
#[cfg(test)]
pub(crate) fn crossings_between(a: &EdgePath, b: &EdgePath, corridors: &[Corridor]) -> usize {
crossings_between_flattened(&polyline(a), &polyline(b), corridors)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::node_graph::PinRef;
use crate::node_graph::cable::{Station, wrap_span};
use crate::node_pin::PinDirection;
use iced_widget::core::Point;
use std::f32::consts::{FRAC_PI_2, PI, TAU};
const ORBIT: Orbit = Orbit {
center: [200.0, 200.0],
radius: 40.0,
};
fn dist(a: [f32; 2], b: [f32; 2]) -> f32 {
((a[0] - b[0]).powi(2) + (a[1] - b[1]).powi(2)).sqrt()
}
fn cross(a: [f32; 2], b: [f32; 2]) -> f32 {
a[0] * b[1] - a[1] * b[0]
}
fn dot(a: [f32; 2], b: [f32; 2]) -> f32 {
a[0] * b[0] + a[1] * b[1]
}
fn unit(v: [f32; 2]) -> Option<[f32; 2]> {
let len = (v[0] * v[0] + v[1] * v[1]).sqrt();
(len > f32::EPSILON).then(|| [v[0] / len, v[1] / len])
}
fn walked_end(path: &EdgePath) -> [f32; 2] {
path.point_at(path.total_len())
}
fn exit_pin_for(orbit: Orbit, pin: [f32; 2], hand: Hand) -> [f32; 2] {
let u = unit([orbit.center[0] - pin[0], orbit.center[1] - pin[1]])
.expect("the pin is not on the centre");
let n = rot90(u);
let s = 400.0 * hand.sign();
[orbit.center[0] + s * n[0], orbit.center[1] + s * n[1]]
}
#[test]
fn a_leg_onto_a_tangent_hugs_its_line() {
let pin = [0.0, 0.0];
let orbit = Orbit {
center: [0.0, 300.0],
radius: 20.0,
};
let path = build(
&[
Hop::Pin {
point: pin,
side: Border::Right,
},
Hop::Wrap { orbit },
Hop::Pin {
point: exit_pin_for(orbit, pin, Hand::Clockwise),
side: Border::Right,
},
],
&EdgeCurve::BezierCubic,
)
.path;
let Some(PathSeg::Bezier { c1, c2, to }) = path.segs.first().copied() else {
panic!("expected a bezier entry leg, got {:?}", path.segs);
};
let span = [to[0] - pin[0], to[1] - pin[1]];
let len = (span[0] * span[0] + span[1] * span[1]).sqrt();
let worst = (0..=64)
.map(|i| {
let p = seg_point_at(pin, &PathSeg::Bezier { c1, c2, to }, i as f32 / 64.0);
((p[0] - pin[0]) * span[1] - (p[1] - pin[1]) * span[0]).abs() / len
})
.fold(0.0f32, f32::max);
assert!(
worst <= TAUT_EXIT * 0.7,
"the leg bows {worst} off its line, too far for the {TAUT_EXIT} reach \
it is allowed",
);
assert!(
worst > 1.0,
"the leg left the pin along the line instead of square to it",
);
}
#[test]
fn pin_to_pin_matches_tangent_bezier_formula() {
let (p0, p1) = ([10.0, 20.0], [300.0, 140.0]);
let path = build(
&[
Hop::Pin {
point: p0,
side: Border::Right,
},
Hop::Pin {
point: p1,
side: Border::Left,
},
],
&EdgeCurve::BezierCubic,
)
.path;
let l = adaptive_bezier_length(p0, p1);
let (d0, d1) = (Border::Right.normal(), Border::Left.normal());
assert_eq!(path.start, p0);
assert_eq!(
path.segs,
vec![PathSeg::Bezier {
c1: [p0[0] + d0[0] * l, p0[1] + d0[1] * l],
c2: [p1[0] + d1[0] * l, p1[1] + d1[1] * l],
to: p1,
}]
);
}
#[test]
fn a_loose_end_faces_back_at_its_pin() {
assert_eq!(Border::Left.opposite(), Border::Right);
assert_eq!(Border::Right.opposite(), Border::Left);
assert_eq!(Border::Top.opposite(), Border::Bottom);
assert_eq!(Border::Bottom.opposite(), Border::Top);
}
#[test]
fn pin_to_pin_line() {
let (p0, p1) = ([0.0, 0.0], [80.0, 0.0]);
let path = build(
&[
Hop::Pin {
point: p0,
side: Border::Right,
},
Hop::Pin {
point: p1,
side: Border::Left,
},
],
&EdgeCurve::Line,
)
.path;
assert_eq!(path.segs, vec![PathSeg::Line { to: p1 }]);
}
#[test]
fn attachment_is_tangent() {
let p = [420.0, 60.0];
for hand in [Hand::Clockwise, Hand::CounterClockwise] {
let a = ORBIT.attachment(p, hand).expect("point is outside");
assert!(
(dist(a.point, ORBIT.center) - ORBIT.radius).abs() < 1e-3,
"{hand:?}: attachment is off the circle",
);
let radius = [a.point[0] - ORBIT.center[0], a.point[1] - ORBIT.center[1]];
let leg = [p[0] - a.point[0], p[1] - a.point[1]];
assert!(
dot(radius, leg).abs() < 1e-2,
"{hand:?}: leg is not perpendicular to the radius",
);
assert!(
cross(a.direction, leg).abs() < 1e-2,
"{hand:?}: travel direction is not tangential",
);
assert!(
dot(a.direction, leg) < 0.0,
"{hand:?}: travel direction points back at the source",
);
}
}
#[test]
fn the_two_hands_are_distinct_tangents() {
let p = [420.0, 60.0];
let cw = ORBIT.attachment(p, Hand::Clockwise).unwrap();
let ccw = ORBIT.attachment(p, Hand::CounterClockwise).unwrap();
assert!(
dist(cw.point, ccw.point) > 1.0,
"both hands resolved to the same tangent point",
);
assert!((dist(cw.point, p) - dist(ccw.point, p)).abs() < 1e-2);
}
#[test]
fn a_point_inside_the_orbit_has_no_tangent() {
let inside = [ORBIT.center[0] + ORBIT.radius * 0.5, ORBIT.center[1]];
assert!(ORBIT.attachment(inside, Hand::Clockwise).is_none());
assert!(ORBIT.attachment(ORBIT.center, Hand::Clockwise).is_none());
}
#[test]
fn sweep_is_signed_by_hand() {
let east = [ORBIT.center[0] + ORBIT.radius, ORBIT.center[1]];
let south = [ORBIT.center[0], ORBIT.center[1] + ORBIT.radius];
let cw = ORBIT.sweep(east, south, Hand::Clockwise);
assert!((cw - PI / 2.0).abs() < 1e-3, "clockwise east->south: {cw}");
let ccw = ORBIT.sweep(east, south, Hand::CounterClockwise);
assert!(
(ccw + 3.0 * PI / 2.0).abs() < 1e-3,
"counter-clockwise east->south takes the long way: {ccw}",
);
assert!(cw.abs() < TAU && ccw.abs() < TAU);
}
#[test]
fn closed_chain_is_leg_arc_leg() {
let path = build(
&[
Hop::Pin {
point: [0.0, 200.0],
side: Border::Right,
},
Hop::Wrap { orbit: ORBIT },
Hop::Pin {
point: [200.0, 500.0],
side: Border::Top,
},
],
&EdgeCurve::BezierCubic,
)
.path;
assert_eq!(path.segs.len(), 3);
let PathSeg::Bezier { to: entry, .. } = path.segs[0] else {
panic!("first segment is not a leg: {:?}", path.segs[0]);
};
let PathSeg::Arc {
center,
radius,
sweep,
} = path.segs[1]
else {
panic!("middle segment is not an arc: {:?}", path.segs[1]);
};
assert_eq!(center, ORBIT.center);
assert_eq!(radius, ORBIT.radius);
assert!(sweep > 0.0, "clockwise wrap must sweep positive: {sweep}");
assert!((dist(entry, ORBIT.center) - ORBIT.radius).abs() < 1e-3);
assert!(matches!(path.segs[2], PathSeg::Bezier { .. }));
}
#[test]
fn a_leg_arrives_straight_down_its_tangent() {
let cases = [
([0.0f32, 0.0], [360.0f32, 147.0], 16.0f32),
([0.0, 0.0], [0.0, 300.0], 20.0),
([0.0, 0.0], [70.0, 45.0], 16.0),
];
for (pin, center, radius) in cases {
let orbit = Orbit { center, radius };
for hand in [Hand::Clockwise, Hand::CounterClockwise] {
let path = build(
&[
Hop::Pin {
point: pin,
side: Border::Right,
},
Hop::Wrap { orbit },
Hop::Pin {
point: exit_pin_for(orbit, pin, hand),
side: Border::Right,
},
],
&EdgeCurve::BezierCubic,
)
.path;
let Some(PathSeg::Bezier { c1, c2, to }) = path.segs.first().copied() else {
panic!("expected a bezier entry leg, got {:?}", path.segs);
};
let PathSeg::Arc { sweep, .. } = path.segs[1] else {
panic!("{center:?} {hand:?}: no wrap: {:?}", path.segs);
};
assert_eq!(
sweep > 0.0,
hand == Hand::Clockwise,
"{center:?} {hand:?}: the run derived the other way round",
);
let (a, b) = (
[to[0] - c2[0], to[1] - c2[1]],
[c2[0] - c1[0], c2[1] - c1[1]],
);
let arrival = (2.0 / 3.0) * cross(a, b) / dist(a, [0.0, 0.0]).powi(3);
assert!(
arrival.abs() < 1e-4,
"{center:?} {hand:?}: the leg arrives bending {arrival}, not down \
the tangent",
);
assert!(
(dist(to, center) - radius).abs() < 1e-2,
"{center:?} {hand:?}: the leg ends off the ring",
);
}
}
}
#[test]
fn the_leg_meets_the_arc_without_a_kink() {
let path = build(
&[
Hop::Pin {
point: [0.0, 200.0],
side: Border::Right,
},
Hop::Wrap { orbit: ORBIT },
Hop::Pin {
point: [200.0, 500.0],
side: Border::Top,
},
],
&EdgeCurve::BezierCubic,
)
.path;
let PathSeg::Bezier { c2, to, .. } = path.segs[0] else {
unreachable!()
};
let PathSeg::Arc { sweep, .. } = path.segs[1] else {
unreachable!()
};
let hand = if sweep > 0.0 {
Hand::Clockwise
} else {
Hand::CounterClockwise
};
let arrival = [to[0] - c2[0], to[1] - c2[1]];
let theta = (to[1] - ORBIT.center[1]).atan2(to[0] - ORBIT.center[0]);
let expected = travel_dir(theta, hand);
assert!(
cross(arrival, expected).abs() / dist(to, c2) < 1e-2,
"leg arrives at {arrival:?}, arc leaves along {expected:?}",
);
assert!(dot(arrival, expected) > 0.0, "leg arrives against the wrap");
}
#[test]
fn a_wrap_takes_the_short_way_round() {
let head = [0.0, 200.0];
let mut sweeps = Vec::new();
for exit in [[200.0f32, 500.0f32], [200.0, -100.0]] {
let path = build(
&[
Hop::Pin {
point: head,
side: Border::Right,
},
Hop::Wrap { orbit: ORBIT },
Hop::Pin {
point: exit,
side: Border::Top,
},
],
&EdgeCurve::BezierCubic,
)
.path;
let PathSeg::Arc { sweep, .. } = path.segs[1] else {
panic!("{exit:?}: no wrap: {:?}", path.segs);
};
assert!(
sweep.abs() <= PI + 1e-3,
"{exit:?}: the wrap sweeps {sweep}, the long way round",
);
sweeps.push(sweep);
}
assert!(
sweeps[0] * sweeps[1] < 0.0,
"mirrored exits wrap the same way: {sweeps:?}",
);
}
fn chords(path: &EdgePath) -> Vec<[[f32; 2]; 2]> {
polyline(path).windows(2).map(|w| [w[0], w[1]]).collect()
}
fn chords_cross(a: &[[[f32; 2]; 2]], b: &[[[f32; 2]; 2]]) -> bool {
a.iter()
.any(|s| b.iter().any(|t| chord_crossing(*s, *t).is_some()))
}
fn cables_cross(a: &EdgePath, b: &EdgePath) -> bool {
!crossing_points(a, b).is_empty()
}
fn straight_cable(from: [f32; 2], to: [f32; 2]) -> EdgePath {
build(
&[
Hop::Pin {
point: from,
side: Border::Right,
},
Hop::Pin {
point: to,
side: Border::Left,
},
],
&EdgeCurve::Line,
)
.path
}
#[test]
fn two_straight_cables_cross_where_their_lines_meet() {
let a = straight_cable([0.0, 0.0], [100.0, 100.0]);
let b = straight_cable([0.0, 100.0], [100.0, 0.0]);
let points = crossing_points(&a, &b);
assert_eq!(points.len(), 1, "one crossing, read as {points:?}");
let off = dist2(points[0], [50.0, 50.0]).sqrt();
assert!(
off < 1e-3,
"the crossing reads {:?}, {off} off (50, 50)",
points[0],
);
}
#[test]
fn cables_held_apart_do_not_cross() {
let a = straight_cable([0.0, 0.0], [200.0, 0.0]);
let b = straight_cable([0.0, 30.0], [200.0, 30.0]);
let parallel = crossing_points(&a, &b);
assert!(parallel.is_empty(), "parallel lines cross at {parallel:?}");
let leg = |y: f32| {
build(
&[
Hop::Pin {
point: [0.0, y],
side: Border::Right,
},
Hop::Pin {
point: [200.0, y + 40.0],
side: Border::Left,
},
],
&EdgeCurve::BezierCubic,
)
.path
};
let offset = crossing_points(&leg(0.0), &leg(60.0));
assert!(offset.is_empty(), "offset legs cross at {offset:?}");
}
#[test]
fn cables_sharing_a_pin_do_not_cross_at_it() {
let pin = [0.0f32, 0.0];
let leaving = |target: [f32; 2]| {
build(
&[
Hop::Pin {
point: pin,
side: Border::Right,
},
Hop::Pin {
point: target,
side: Border::Left,
},
],
&EdgeCurve::BezierCubic,
)
.path
};
let down = leaving([220.0, -160.0]);
let diverging = crossing_points(&down, &leaving([220.0, 160.0]));
assert!(
diverging.is_empty(),
"two cables out of one pin cross at {diverging:?}",
);
let arriving = build(
&[
Hop::Pin {
point: [-260.0, 200.0],
side: Border::Right,
},
Hop::Pin {
point: pin,
side: Border::Left,
},
],
&EdgeCurve::BezierCubic,
)
.path;
let met = crossing_points(&down, &arriving);
assert!(
met.is_empty(),
"the cable arriving at the pin crosses the one leaving it at {met:?}",
);
}
#[test]
fn a_cable_crossing_another_twice_reports_both() {
let line = straight_cable([0.0, 0.0], [400.0, 0.0]);
let over = build(
&[
Hop::Pin {
point: [50.0, 60.0],
side: Border::Right,
},
Hop::Wrap {
orbit: Orbit {
center: [200.0, -80.0],
radius: 40.0,
},
},
Hop::Pin {
point: [350.0, 60.0],
side: Border::Left,
},
],
&EdgeCurve::BezierCubic,
)
.path;
let points = crossing_points(&line, &over);
assert_eq!(points.len(), 2, "two crossings, read as {points:?}");
assert!(
dist2(points[0], points[1]).sqrt() > 100.0,
"the two crossings are one place read twice: {points:?}",
);
for p in points {
assert!(
line.distance(p) < 1e-3 && over.distance(p) < 1e-2,
"{p:?} sits {} off the straight cable and {} off the wrapping one",
line.distance(p),
over.distance(p),
);
}
}
#[test]
fn a_crossing_on_the_wrap_lands_inside_the_ring_by_the_chord_sagitta() {
let (path, entry, sweep) = wrapped_cable();
let step = sweep / CURVE_FLATTEN_SEGMENTS as f32;
let angle = arc_start_angle(entry, ORBIT.center) + sweep / 2.0 + step / 2.0;
let probe = straight_cable(ORBIT.center, polar(angle, 2.0 * ORBIT.radius));
let points = crossing_points(&path, &probe);
assert_eq!(
points.len(),
1,
"one crossing on the wrap, read as {points:?}"
);
let sagitta = ORBIT.radius * (1.0 - (step / 2.0).cos());
let off = dist2(points[0], polar(angle, ORBIT.radius)).sqrt();
assert!(
off <= sagitta + 1e-4,
"the crossing reads {off} off the ring, past the {sagitta} a chord falls short",
);
let radius = dist2(points[0], ORBIT.center).sqrt();
assert!(
radius < ORBIT.radius,
"the crossing reads {radius} out, not inside the {} ring",
ORBIT.radius,
);
}
fn crosses_with_inner(inner: (f32, f32), outer: (f32, f32)) -> bool {
const REACH: f32 = 300.0;
let cable = |ends: (f32, f32), radius: f32| {
let at = |angle: f32| [REACH * angle.cos(), REACH * angle.sin()];
build(
&[
Hop::Pin {
point: at(ends.0),
side: Border::Right,
},
Hop::Wrap {
orbit: Orbit {
center: [0.0, 0.0],
radius,
},
},
Hop::Pin {
point: at(ends.1),
side: Border::Bottom,
},
],
&EdgeCurve::BezierCubic,
)
.path
};
cables_cross(&cable(inner, 16.0), &cable(outer, 26.0))
}
#[test]
fn ordering_wraps_by_span_never_makes_a_layout_worse() {
let short_way = |a: f32, b: f32| {
let d = (b - a).rem_euclid(TAU);
d.min(TAU - d)
};
let mut narrower_inside = 0usize;
let mut wider_inside = 0usize;
let mut worse = Vec::new();
let mut repaired = 0usize;
let mut cases = 0usize;
let step = TAU / 12.0;
for a0 in 0..12 {
for a1 in 0..12 {
for b0 in 0..12 {
for b1 in 0..12 {
let a = (a0 as f32 * step, a1 as f32 * step);
let b = (b0 as f32 * step, b1 as f32 * step);
if a0 == a1 || b0 == b1 {
continue;
}
let (sa, sb) = (short_way(a.0, a.1), short_way(b.0, b.1));
if (sa - sb).abs() < 1e-4 {
continue;
}
cases += 1;
let (narrow, wide) = if sa < sb { (a, b) } else { (b, a) };
let good = crosses_with_inner(narrow, wide);
let bad = crosses_with_inner(wide, narrow);
narrower_inside += usize::from(good);
wider_inside += usize::from(bad);
if good && !bad {
worse.push((narrow, wide));
}
repaired += usize::from(bad && !good);
}
}
}
}
assert!(cases > 10_000, "the sweep collapsed to {cases} layouts");
assert!(
worse.is_empty(),
"{} of {cases} layouts are made WORSE by the rule, e.g. {:?}",
worse.len(),
worse.first(),
);
assert!(
repaired > 1_000,
"the rule only repairs {repaired} of {cases} layouts \
({narrower_inside} crossings against {wider_inside}): \
not worth deriving",
);
}
const CORRIDOR_INNER: f32 = 16.0;
const CORRIDOR_OUTER: f32 = 26.0;
const CORRIDOR_CLEARANCE: f32 = CORRIDOR_OUTER + 30.0;
fn corridor_cable(pins: ([f32; 2], [f32; 2]), a: Orbit, b: Orbit) -> EdgePath {
build(
&[
Hop::Pin {
point: pins.0,
side: Border::Right,
},
Hop::Wrap { orbit: a },
Hop::Wrap { orbit: b },
Hop::Pin {
point: pins.1,
side: Border::Bottom,
},
],
&EdgeCurve::BezierCubic,
)
.path
}
fn corridor_hands(path: &EdgePath) -> Option<(bool, bool)> {
let sign = |i: usize| match path.segs.get(i) {
Some(PathSeg::Arc { sweep, .. }) => Some(*sweep > 0.0),
_ => None,
};
Some((sign(1)?, sign(3)?))
}
fn corridor_chords(path: &EdgePath, a: [f32; 2], b: [f32; 2]) -> Vec<[[f32; 2]; 2]> {
let axis = [b[0] - a[0], b[1] - a[1]];
let len2 = axis[0] * axis[0] + axis[1] * axis[1];
let margin = CORRIDOR_CLEARANCE / len2.sqrt();
let inside = |p: [f32; 2]| {
let along = ((p[0] - a[0]) * axis[0] + (p[1] - a[1]) * axis[1]) / len2;
(margin..=1.0 - margin).contains(&along)
};
chords(path)
.into_iter()
.filter(|c| inside(c[0]) && inside(c[1]))
.collect()
}
#[test]
fn two_cables_nested_alike_do_not_cross_between_the_anchors() {
let (a, b) = ([0.0f32, 0.0], [360.0f32, 0.0]);
let orbit = |center: [f32; 2], radius: f32| Orbit { center, radius };
let near = ([-260.0f32, 150.0], [620.0f32, 150.0]);
let far = ([-240.0f32, 300.0], [600.0f32, 300.0]);
for (nesting, radii, expected) in [
(
"alike",
[
CORRIDOR_INNER,
CORRIDOR_INNER,
CORRIDOR_OUTER,
CORRIDOR_OUTER,
],
false,
),
(
"flipped",
[
CORRIDOR_INNER,
CORRIDOR_OUTER,
CORRIDOR_OUTER,
CORRIDOR_INNER,
],
true,
),
] {
let u = corridor_cable(near, orbit(a, radii[0]), orbit(b, radii[1]));
let v = corridor_cable(far, orbit(a, radii[2]), orbit(b, radii[3]));
assert_eq!(
(corridor_hands(&u), corridor_hands(&v)),
(Some((true, true)), Some((true, true))),
"{nesting}: the layout no longer wraps both anchors one way, \
so its belts are not the outer tangents this measures",
);
assert!(
cables_cross(&u, &v),
"{nesting}: the cables no longer meet outside the corridor, \
so the clip has nothing left to separate",
);
let crossed = chords_cross(&corridor_chords(&u, a, b), &corridor_chords(&v, a, b));
assert_eq!(
crossed, expected,
"{nesting}: crossing between the anchors reads {crossed}",
);
}
}
#[test]
fn a_wrap_the_cable_doubles_back_to_stays_short() {
let cases = [
(
Orbit {
center: [400.0, 100.0],
radius: 20.0,
},
[-200.0f32, 200.0f32],
),
(
Orbit {
center: [-25.0, -125.0],
radius: 29.0,
},
[-50.0, -300.0],
),
];
for (orbit, far) in cases {
let built = build(
&[
Hop::Pin {
point: [0.0, 0.0],
side: Border::Right,
},
Hop::Wrap { orbit },
Hop::Pin {
point: far,
side: Border::Left,
},
],
&EdgeCurve::BezierCubic,
);
let sweeps: Vec<f32> = built
.path
.segs
.iter()
.filter_map(|seg| match *seg {
PathSeg::Arc { sweep, .. } => Some(sweep),
_ => None,
})
.collect();
assert_eq!(
sweeps.len(),
1,
"{orbit:?} -> {far:?}: the wrap was dropped, so the cable \
ignores an anchor its route names: {:?}",
built.path.segs,
);
assert!(
sweeps[0].abs() < PI,
"{orbit:?} -> {far:?}: the cable takes {} rad round the ring; \
doubling back should hook the near side, not lap it",
sweeps[0],
);
}
}
const AIM_DRIFT: f32 = 0.5;
#[test]
fn no_wrap_takes_the_long_way_round() {
let pin = [0.0, 0.0];
let aimed = |orbit: Orbit, far: [f32; 2]| {
[Hand::Clockwise, Hand::CounterClockwise]
.into_iter()
.filter_map(|hand| {
let entry = orbit.attachment(pin, hand)?;
let exit = orbit.attachment(far, hand.flip())?;
Some(orbit.sweep(entry.point, exit.point, hand).abs())
})
.min_by(f32::total_cmp)
};
let mut wraps = 0usize;
let mut over: Vec<(Orbit, [f32; 2], f32, f32)> = Vec::new();
for radius in [11.0f32, 17.0, 23.0, 29.0] {
for cx in (-300..=500).step_by(100) {
for cy in (-300..=500).step_by(100) {
for fx in (-400..=600).step_by(250) {
for fy in (-400..=600).step_by(250) {
let orbit = Orbit {
center: [cx as f32, cy as f32],
radius,
};
let far = [fx as f32, fy as f32];
let built = build(
&[
Hop::Pin {
point: pin,
side: Border::Right,
},
Hop::Wrap { orbit },
Hop::Pin {
point: far,
side: Border::Left,
},
],
&EdgeCurve::BezierCubic,
);
let Some(sweep) = built.path.segs.iter().find_map(|seg| match *seg {
PathSeg::Arc { sweep, .. } => Some(sweep),
_ => None,
}) else {
continue;
};
wraps += 1;
let ideal = aimed(orbit, far).expect("the wrap resolved a tangent");
let ceiling = PI.max(ideal + AIM_DRIFT);
if sweep.abs() > ceiling {
over.push((orbit, far, sweep, ideal));
}
}
}
}
}
}
assert!(
wraps > 5_000,
"the grid wrapped only {wraps} of its layouts, so it says little",
);
assert!(
over.is_empty(),
"{} of {wraps} layouts wrap further than the ring asks for, worst \
{:?}",
over.len(),
over.iter().max_by(|a, b| a.2.abs().total_cmp(&b.2.abs())),
);
}
#[test]
fn ring_distance_measures_to_the_circle() {
let outer = Orbit {
center: ORBIT.center,
radius: 100.0,
};
let on_outer = [ORBIT.center[0] + 105.0, ORBIT.center[1]];
assert!(outer.ring_distance(on_outer) < ORBIT.ring_distance(on_outer));
let on_ring = [ORBIT.center[0] + ORBIT.radius, ORBIT.center[1]];
assert!(ORBIT.ring_distance(on_ring) < 1e-3);
assert!((ORBIT.ring_distance(ORBIT.center) - ORBIT.radius).abs() < 1e-3);
}
#[test]
fn the_run_between_two_orbits_is_tangent_to_both() {
let mut wraps = Vec::new();
let cases = [
(
[0.0f32, 200.0f32],
ORBIT,
Orbit {
center: [520.0, 260.0],
radius: 25.0,
},
[700.0f32, 500.0f32],
),
(
[0.0, 200.0],
ORBIT,
Orbit {
center: [520.0, 260.0],
radius: 25.0,
},
[700.0, 20.0],
),
(
[-300.0, -200.0],
Orbit {
center: [200.0, 200.0],
radius: 16.0,
},
Orbit {
center: [300.0, 100.0],
radius: 26.0,
},
[500.0, -300.0],
),
];
for (head, a, b, tail) in cases {
let path = build(
&[
Hop::Pin {
point: head,
side: Border::Right,
},
Hop::Wrap { orbit: a },
Hop::Wrap { orbit: b },
Hop::Pin {
point: tail,
side: Border::Left,
},
],
&EdgeCurve::BezierCubic,
)
.path;
assert_eq!(path.segs.len(), 5, "{tail:?}: {:?}", path.segs);
let mut cursor = path.start;
for seg in &path.segs[..2] {
cursor = seg_end(cursor, seg);
}
let PathSeg::Bezier { c1, c2, to } = path.segs[2] else {
panic!("{tail:?}: the run is not a leg: {:?}", path.segs[2]);
};
for (label, on, ring, control) in [("leaves", cursor, a, c1), ("lands", to, b, c2)] {
assert!(
(dist(on, ring.center) - ring.radius).abs() < 1e-2,
"{tail:?}: the run {label} off its ring",
);
let along = [control[0] - on[0], control[1] - on[1]];
let radial = [on[0] - ring.center[0], on[1] - ring.center[1]];
assert!(
dot(along, radial).abs() / (dist(control, on) * ring.radius) < 1e-2,
"{tail:?}: the run is not tangent where it {label}",
);
}
let chord = [to[0] - cursor[0], to[1] - cursor[1]];
let span = dist(cursor, to);
let off = |p: [f32; 2]| {
((p[0] - cursor[0]) * chord[1] - (p[1] - cursor[1]) * chord[0]).abs() / span
};
assert!(
off(c1).max(off(c2)) < 1e-2,
"{tail:?}: the run leaves its own chord by {}",
off(c1).max(off(c2)),
);
let (PathSeg::Arc { sweep: first, .. }, PathSeg::Arc { sweep: second, .. }) =
(path.segs[1], path.segs[3])
else {
panic!("{tail:?}: the wraps are not arcs: {:?}", path.segs);
};
wraps.push((first > 0.0, second > 0.0));
}
assert_ne!(
wraps[0], wraps[1],
"both layouts take the same belt, so the crossed one is untested",
);
}
#[test]
fn a_swallowed_station_drops_its_wrap() {
let far = [600.0, 200.0];
let path = build(
&[
Hop::Pin {
point: ORBIT.center,
side: Border::Right,
},
Hop::Wrap { orbit: ORBIT },
Hop::Pin {
point: far,
side: Border::Left,
},
],
&EdgeCurve::BezierCubic,
)
.path;
assert_eq!(path.segs.len(), 1);
assert!(matches!(path.segs[0], PathSeg::Bezier { to, .. } if to == far));
}
fn wrapped_cable() -> (EdgePath, [f32; 2], f32) {
let built = wrapped_built();
let PathSeg::Bezier { to: entry, .. } = built.path.segs[0] else {
panic!("first segment is not a leg: {:?}", built.path.segs[0]);
};
let PathSeg::Arc { sweep, .. } = built.path.segs[1] else {
panic!("middle segment is not an arc: {:?}", built.path.segs[1]);
};
(built.path, entry, sweep)
}
fn wrapped_built() -> Built {
build(
&[
Hop::Pin {
point: [0.0, 200.0],
side: Border::Right,
},
Hop::Wrap { orbit: ORBIT },
Hop::Pin {
point: [200.0, 500.0],
side: Border::Top,
},
],
&EdgeCurve::BezierCubic,
)
}
fn polar(angle: f32, radius: f32) -> [f32; 2] {
[
ORBIT.center[0] + radius * angle.cos(),
ORBIT.center[1] + radius * angle.sin(),
]
}
fn line_cable() -> EdgePath {
build(
&[
Hop::Pin {
point: [0.0, 0.0],
side: Border::Right,
},
Hop::Pin {
point: [100.0, 0.0],
side: Border::Left,
},
],
&EdgeCurve::Line,
)
.path
}
#[test]
fn distance_is_zero_on_the_wrap() {
let (path, entry, sweep) = wrapped_cable();
let mid = arc_start_angle(entry, ORBIT.center) + sweep / 2.0;
let on_wrap = polar(mid, ORBIT.radius);
assert!(
path.distance(on_wrap) < 1e-2,
"point on the wrap reads {} away",
path.distance(on_wrap),
);
}
#[test]
fn distance_beside_the_wrap_is_the_radial_offset() {
let (path, entry, sweep) = wrapped_cable();
let mid = arc_start_angle(entry, ORBIT.center) + sweep / 2.0;
let outside = path.distance(polar(mid, ORBIT.radius + 12.0));
assert!(
(outside - 12.0).abs() < 1e-2,
"12px outside reads {outside}"
);
let inside = path.distance(polar(mid, ORBIT.radius - 10.0));
assert!((inside - 10.0).abs() < 1e-2, "10px inside reads {inside}");
}
#[test]
fn a_point_past_the_sweep_measures_to_the_arc_end() {
let (path, entry, sweep) = wrapped_cable();
let arc = EdgePath {
start: entry,
segs: vec![path.segs[1]],
};
let mid = arc_start_angle(entry, ORBIT.center) + sweep / 2.0;
let opposite = polar(mid + PI, ORBIT.radius);
assert!(
ORBIT.ring_distance(opposite) < 1e-3,
"the probe must lie on the circle for this to mean anything",
);
let exit = seg_end(entry, &path.segs[1]);
let nearer_end = dist(opposite, entry).min(dist(opposite, exit));
let measured = arc.distance(opposite);
assert!(
(measured - nearer_end).abs() < 1e-2,
"measured {measured}, nearer arc end is {nearer_end} away",
);
assert!(
measured > ORBIT.radius,
"measured {measured} hugs the circle"
);
}
#[test]
fn the_anchor_centre_is_a_radius_from_the_cable() {
let (path, _, _) = wrapped_cable();
let measured = path.distance(ORBIT.center);
assert!(
(measured - ORBIT.radius).abs() < 1e-2,
"centre reads {measured} from the cable, radius is {}",
ORBIT.radius,
);
}
#[test]
fn distance_on_a_straight_cable_is_point_to_segment() {
let path = line_cable();
assert!((path.distance([50.0, 25.0]) - 25.0).abs() < 1e-3);
assert!((path.distance([-30.0, 0.0]) - 30.0).abs() < 1e-3);
}
#[test]
fn intersects_catches_legs_and_wraps() {
let (path, entry, _) = wrapped_cable();
assert!(
path.intersects([60.0, 150.0], [60.0, 250.0]),
"a probe across the first leg must cross the cable",
);
let arc = EdgePath {
start: entry,
segs: vec![path.segs[1]],
};
let out = [ORBIT.center[0] + ORBIT.radius * 2.0, ORBIT.center[1]];
let inner = [ORBIT.center[0] + ORBIT.radius * 0.25, ORBIT.center[1]];
assert!(
arc.intersects(inner, out),
"a probe radially through the wrap must cross it",
);
assert!(
!path.intersects([500.0, 500.0], [600.0, 600.0]),
"a probe well clear of the cable must not cross it",
);
}
#[test]
fn a_probe_that_stops_short_does_not_cross() {
let path = line_cable();
assert!(
!path.intersects([50.0, 20.0], [50.0, 10.0]),
"the probe stops 10px above the cable",
);
assert!(
path.intersects([50.0, 20.0], [50.0, -10.0]),
"extended through the cable, the same probe crosses",
);
}
#[test]
fn a_slice_spans_its_arc_length_window() {
let path = line_cable();
let total = path.total_len();
assert!((total - 100.0).abs() < 1e-3, "the cable reads {total} long");
let cut = path.slice(20.0, 70.0);
assert!(
dist(cut.start, path.point_at(20.0)) < 1e-3,
"the slice starts at {:?}, 20 along is {:?}",
cut.start,
path.point_at(20.0),
);
assert!(
dist(walked_end(&cut), path.point_at(70.0)) < 1e-3,
"the slice ends at {:?}, 70 along is {:?}",
walked_end(&cut),
path.point_at(70.0),
);
assert!(
(cut.total_len() - 50.0).abs() < 1e-3,
"a 50 wide window sliced {} of cable",
cut.total_len(),
);
}
#[test]
fn a_slice_through_a_wrap_starts_on_the_arc() {
let built = wrapped_built();
let touch = built.touches[0];
let arc_len = touch.span.1 - touch.span.0;
let from = touch.span.0 + arc_len * 0.25;
let to = touch.span.0 + arc_len * 0.75;
let cut = built.path.slice(from, to);
assert_eq!(cut.segs.len(), 1, "not just the arc: {:?}", cut.segs);
assert!(matches!(cut.segs[0], PathSeg::Arc { .. }));
assert!(
ORBIT.ring_distance(cut.start) < 1e-2,
"the slice starts at {:?}, off the ring it cuts",
cut.start,
);
assert!(
(cut.total_len() - (to - from)).abs() < 1e-2,
"a {} wide window sliced {} of arc",
to - from,
cut.total_len(),
);
assert!(
dist(walked_end(&cut), built.path.point_at(to)) < 1e-2,
"the slice ends at {:?}, the window ends at {:?}",
walked_end(&cut),
built.path.point_at(to),
);
}
#[test]
fn a_slice_clamps_to_the_cable() {
let path = line_cable();
let inverted = path.slice(60.0, 20.0);
assert!(inverted.segs.is_empty(), "{:?}", inverted.segs);
assert!(dist(inverted.start, path.point_at(60.0)) < 1e-3);
let over = path.slice(-50.0, 500.0);
assert!(
(over.total_len() - path.total_len()).abs() < 1e-3,
"the whole cable sliced {} of {}",
over.total_len(),
path.total_len(),
);
assert!(dist(over.start, path.start) < 1e-3);
assert!(dist(walked_end(&over), walked_end(&path)) < 1e-3);
}
#[test]
fn nearest_reports_how_far_along_it_hit() {
let path = line_cable();
let near = path.nearest([50.0, 25.0]);
assert!(
(near.distance - 25.0).abs() < 1e-3,
"25 beside the cable reads {}",
near.distance,
);
assert!(
(near.arc_len - path.total_len() / 2.0).abs() < 1e-3,
"hit {} along a cable {} long",
near.arc_len,
path.total_len(),
);
}
#[test]
fn nearest_measures_arc_length_along_a_bezier() {
let path = build(
&[
Hop::Pin {
point: [0.0, 0.0],
side: Border::Right,
},
Hop::Pin {
point: [912.0, 406.0],
side: Border::Left,
},
],
&EdgeCurve::BezierCubic,
)
.path;
assert_eq!(path.segs.len(), 1, "not one leg: {:?}", path.segs);
let PathSeg::Bezier { c1, c2, to } = path.segs[0] else {
panic!("the leg is not a bezier: {:?}", path.segs[0]);
};
let control = [path.start, c1, c2, to];
const FINE: usize = 8192;
let sample = |i: usize| {
cubic_point(
control[0],
control[1],
control[2],
control[3],
i as f32 / FINE as f32,
)
};
let length: f32 = (1..=FINE).map(|i| dist(sample(i - 1), sample(i))).sum();
let point_at = |target: f32| -> [f32; 2] {
let mut walked = 0.0;
for i in 1..=FINE {
let (prev, cur) = (sample(i - 1), sample(i));
let chord = dist(prev, cur);
if walked + chord >= target {
return lerp(prev, cur, (target - walked) / chord);
}
walked += chord;
}
control[3]
};
assert!(
(length - 1000.1).abs() < 0.1,
"the leg measures {length}, not the thousand units this case is about",
);
assert!(
(path.total_len() - length).abs() < 0.2,
"the cable reads {} long against {length}",
path.total_len(),
);
for along in [24.0, 400.0, 800.0] {
let probe = point_at(along);
let near = path.nearest(probe);
assert!(
near.distance < 0.1,
"the probe sits on the curve, and reads {} off it",
near.distance,
);
assert!(
(near.arc_len - along).abs() < 0.1,
"a probe {along} along the leg reports {}",
near.arc_len,
);
let back = path.point_at(near.arc_len);
assert!(
dist(back, probe) < 0.5,
"{along} along came back at {back:?}, {} from the probe",
dist(back, probe),
);
let window = path.slice(near.arc_len - 12.0, near.arc_len + 12.0);
assert!(
(window.total_len() - 24.0).abs() < 0.5,
"a 24 wide window at {along} covers {} of cable",
window.total_len(),
);
}
}
#[test]
fn nearest_on_a_wrap_lands_inside_its_span() {
let built = wrapped_built();
let touch = built.touches[0];
let PathSeg::Arc { sweep, .. } = built.path.segs[1] else {
panic!("middle segment is not an arc: {:?}", built.path.segs[1]);
};
let mid = arc_start_angle(touch.entry, ORBIT.center) + sweep / 2.0;
let near = built.path.nearest(polar(mid, ORBIT.radius + 12.0));
assert!(
(near.distance - 12.0).abs() < 1e-2,
"12 outside the ring reads {}",
near.distance,
);
let middle = (touch.span.0 + touch.span.1) / 2.0;
assert!(
(near.arc_len - middle).abs() < 1e-2,
"mid-sweep hit {} along, the wrap's span is {:?}",
near.arc_len,
touch.span,
);
}
#[test]
fn a_ring_touch_spans_its_arc() {
let built = wrapped_built();
let touch = built.touches[0];
let PathSeg::Arc { radius, sweep, .. } = built.path.segs[1] else {
panic!("middle segment is not an arc: {:?}", built.path.segs[1]);
};
assert_eq!(touch.hop, 1, "the touch names the wrong hop");
let arc_len = sweep.abs() * radius;
assert!(
(touch.span.1 - touch.span.0 - arc_len).abs() < 1e-2,
"the span covers {}, the arc is {arc_len} long",
touch.span.1 - touch.span.0,
);
assert!(
touch.span.0 > 0.0,
"the span starts at the cable's own start, with no entry leg",
);
assert!(
touch.span.1 < built.path.total_len(),
"the span reaches the cable's end, with no exit leg",
);
}
const GATE_INNER: f32 = 16.0;
const GATE_STEP: f32 = 10.0;
const GATE_ANCHOR_ID: usize = 0;
const GATE_MARGIN: f32 = 0.06;
const GATE_ORDER: f32 = 0.04;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
struct GateIds;
impl crate::Ids for GateIds {
type NodeId = usize;
type PinId = usize;
type EdgeId = usize;
type AnchorId = usize;
type Payload = ();
}
type GateGraph<'a> = crate::node_graph::NodeGraph<
'a,
GateIds,
(),
iced_widget::core::Theme,
iced_widget::renderer::Renderer,
>;
struct Lcg(u64);
impl Lcg {
fn seeded(seed: u64) -> Self {
let mut lcg = Self(seed);
lcg.bits();
lcg
}
fn bits(&mut self) -> u32 {
self.0 = self
.0
.wrapping_mul(6_364_136_223_846_793_005)
.wrapping_add(1_442_695_040_888_963_407);
(self.0 >> 33) as u32
}
fn range(&mut self, lo: f32, hi: f32) -> f32 {
lo + (hi - lo) * (self.bits() as f32 / (1u64 << 31) as f32)
}
}
#[derive(Debug, Clone, Copy)]
struct Ends {
head: [f32; 2],
head_side: Border,
tail: [f32; 2],
tail_side: Border,
}
#[derive(Debug)]
struct Scene {
seed: u64,
anchors: Vec<[f32; 2]>,
cables: Vec<Ends>,
}
fn facing_side(pin: [f32; 2], toward: [f32; 2]) -> Border {
let want = [toward[0] - pin[0], toward[1] - pin[1]];
let score = |side: Border| {
let d = side.normal();
d[0] * want[0] + d[1] * want[1]
};
[Border::Left, Border::Right, Border::Top, Border::Bottom]
.into_iter()
.max_by(|&a, &b| score(a).total_cmp(&score(b)))
.unwrap_or(Border::Right)
}
fn gate_scene(seed: u64, anchors: usize, cables: usize) -> Option<Scene> {
let mut rng = Lcg::seeded(seed);
let axis = rng.range(0.0, TAU);
let (cos, sin) = (axis.cos(), axis.sin());
let mut centers: Vec<[f32; 2]> = Vec::with_capacity(anchors);
let mut along = 0.0;
for _ in 0..anchors {
let across = rng.range(-70.0, 70.0);
centers.push([cos * along - sin * across, sin * along + cos * across]);
along += rng.range(300.0, 460.0);
}
let (first, last) = (centers[0], centers[anchors - 1]);
let mut ends = Vec::with_capacity(cables);
for _ in 0..cables {
let out = axis + PI + rng.range(-FRAC_PI_2, FRAC_PI_2);
let back = axis + rng.range(-FRAC_PI_2, FRAC_PI_2);
let (near, far) = (rng.range(220.0, 380.0), rng.range(220.0, 380.0));
let head = [first[0] + near * out.cos(), first[1] + near * out.sin()];
let tail = [last[0] + far * back.cos(), last[1] + far * back.sin()];
ends.push(Ends {
head,
head_side: facing_side(head, first),
tail,
tail_side: facing_side(tail, last),
});
}
let transits = |end: &Ends| {
let run = [end.tail[0] - end.head[0], end.tail[1] - end.head[1]];
let len2 = run[0] * run[0] + run[1] * run[1];
if len2 < 1e-6 {
return false;
}
let mut previous = GATE_MARGIN - GATE_ORDER;
centers.iter().all(|c| {
let at = ((c[0] - end.head[0]) * run[0] + (c[1] - end.head[1]) * run[1]) / len2;
let ordered = at >= previous + GATE_ORDER && at <= 1.0 - GATE_MARGIN;
previous = at;
ordered
})
};
if !ends.iter().all(transits) {
return None;
}
Some(Scene {
seed,
anchors: centers,
cables: ends,
})
}
fn gate_corridors(centers: &[[f32; 2]], reach: f32) -> (Vec<Corridor>, Vec<Corridor>) {
let ring = |center: [f32; 2]| Orbit {
center,
radius: reach,
};
let consecutive: Vec<Corridor> = centers
.windows(2)
.map(|pair| Corridor {
from: ring(pair[0]),
to: ring(pair[1]),
})
.collect();
let every: Vec<Corridor> = centers
.iter()
.enumerate()
.flat_map(|(i, &from)| {
centers[i + 1..].iter().map(move |&to| Corridor {
from: ring(from),
to: ring(to),
})
})
.collect();
(consecutive, every)
}
fn corridor_crossings(a: &EdgePath, b: &EdgePath, bands: &[Corridor]) -> usize {
let holds = |point: [f32; 2]| {
bands.iter().any(|corridor| {
let (from, to) = (corridor.from, corridor.to);
let axis = [to.center[0] - from.center[0], to.center[1] - from.center[1]];
let len2 = axis[0] * axis[0] + axis[1] * axis[1];
if len2 < 1e-6 {
return false;
}
let at = ((point[0] - from.center[0]) * axis[0]
+ (point[1] - from.center[1]) * axis[1])
/ len2;
if !(0.0..=1.0).contains(&at) {
return false;
}
dist2(point, from.center) > from.radius * from.radius
&& dist2(point, to.center) > to.radius * to.radius
})
};
let filtered = crossing_points(a, b)
.into_iter()
.filter(|point| holds(*point))
.count();
let region = crossings_between(a, b, bands);
assert_eq!(
region, filtered,
"the region-limited count reads {region} where filtering every \
crossing through the corridor rule reads {filtered}",
);
region
}
struct Arrangements {
template: Vec<Vec<Hop>>,
at: Vec<Vec<usize>>,
centers: Vec<[f32; 2]>,
bands: Vec<Corridor>,
every: Vec<Corridor>,
radix: usize,
paths: Vec<Vec<Option<EdgePath>>>,
pairs: Vec<Vec<Option<usize>>>,
}
impl Arrangements {
fn new(template: Vec<Vec<Hop>>, at: Vec<Vec<usize>>, centers: Vec<[f32; 2]>) -> Self {
let cables = template.len();
let radix = cables.pow(centers.len() as u32);
let reach = GATE_INNER + GATE_STEP * (cables - 1) as f32;
let (bands, every) = gate_corridors(¢ers, reach);
Self {
template,
at,
centers,
bands,
every,
radix,
paths: vec![vec![None; radix]; cables],
pairs: vec![vec![None; radix * radix]; cables * (cables - 1) / 2],
}
}
fn tuple(&self, orbits: &[u8]) -> usize {
orbits
.iter()
.rev()
.fold(0, |id, &orbit| id * self.template.len() + orbit as usize)
}
fn ensure(&mut self, cable: usize, tuple: usize) {
if self.paths[cable][tuple].is_some() {
return;
}
let cables = self.template.len();
let mut hops = self.template[cable].clone();
let mut rest = tuple;
for (anchor, &hop) in self.at[cable].iter().enumerate() {
let ring = rest % cables;
rest /= cables;
hops[hop] = Hop::Wrap {
orbit: Orbit {
center: self.centers[anchor],
radius: GATE_INNER + GATE_STEP * ring as f32,
},
};
}
self.paths[cable][tuple] = Some(build(&hops, &EdgeCurve::default()).path);
}
fn crossings(&mut self, orbits: &[Vec<u8>]) -> usize {
let tuples: Vec<usize> = orbits.iter().map(|rings| self.tuple(rings)).collect();
let mut total = 0;
let mut pair = 0;
for u in 0..tuples.len() {
for v in u + 1..tuples.len() {
let key = tuples[u] * self.radix + tuples[v];
if self.pairs[pair][key].is_none() {
self.ensure(u, tuples[u]);
self.ensure(v, tuples[v]);
let count = corridor_crossings(
self.paths[u][tuples[u]].as_ref().expect("built just above"),
self.paths[v][tuples[v]].as_ref().expect("built just above"),
&self.bands,
);
self.pairs[pair][key] = Some(count);
}
total += self.pairs[pair][key].expect("filled just above");
pair += 1;
}
}
total
}
fn spanning(&mut self, orbits: &[Vec<u8>]) -> usize {
let tuples: Vec<usize> = orbits.iter().map(|rings| self.tuple(rings)).collect();
let mut extra = 0;
for u in 0..tuples.len() {
for v in u + 1..tuples.len() {
self.ensure(u, tuples[u]);
self.ensure(v, tuples[v]);
let one = self.paths[u][tuples[u]].as_ref().expect("built just above");
let other = self.paths[v][tuples[v]].as_ref().expect("built just above");
extra += crossings_between(one, other, &self.every)
- crossings_between(one, other, &self.bands);
}
}
extra
}
}
fn permutations(n: usize) -> Vec<Vec<u8>> {
let mut out: Vec<Vec<u8>> = vec![Vec::new()];
for value in 0..n as u8 {
let mut grown = Vec::with_capacity(out.len() * (value as usize + 1));
for perm in &out {
for at in 0..=perm.len() {
let mut next = perm.clone();
next.insert(at, value);
grown.push(next);
}
}
out = grown;
}
out
}
fn containment_orbits(scene: &Scene) -> Vec<Vec<u8>> {
let anchors = scene.anchors.len();
let mut orbits = vec![vec![0u8; anchors]; scene.cables.len()];
for (anchor, ¢er) in scene.anchors.iter().enumerate() {
let mut order: Vec<(f32, usize)> = scene
.cables
.iter()
.enumerate()
.map(|(cable, ends)| {
let previous = if anchor == 0 {
ends.head
} else {
scene.anchors[anchor - 1]
};
let next = scene.anchors.get(anchor + 1).copied().unwrap_or(ends.tail);
(wrap_span(center, previous, next), cable)
})
.collect();
order.sort_by(|a, b| a.0.total_cmp(&b.0).then(a.1.cmp(&b.1)));
for (ring, &(_, cable)) in order.iter().enumerate() {
orbits[cable][anchor] = ring as u8;
}
}
orbits
}
#[derive(Debug, Clone, Copy, Default)]
struct Escapes {
local: bool,
coupled: bool,
partial: bool,
reseated: bool,
}
impl Escapes {
fn within_reach(&self) -> bool {
self.local || self.coupled
}
fn none(&self) -> bool {
!self.local && !self.coupled && !self.partial && !self.reseated
}
}
#[derive(Debug, Clone, Copy)]
struct Measured {
chosen: usize,
containment: usize,
best: usize,
escapes: Escapes,
spanning: usize,
}
fn measure(scene: &Scene) -> Option<Measured> {
let anchors = scene.anchors.len();
let cables = scene.cables.len();
let mut graph = GateGraph::default();
for (index, center) in scene.anchors.iter().enumerate() {
graph = graph.push_anchor(crate::node_graph::anchor(
GATE_ANCHOR_ID + index,
Point::new(center[0], center[1]),
));
}
for cable in 0..cables {
graph = graph.push_edge(
crate::node_graph::edge(
cable,
PinRef::new(2 * cable, 0),
PinRef::new(2 * cable + 1, 0),
)
.route((0..anchors).map(|anchor| GATE_ANCHOR_ID + anchor)),
);
}
let station = |pin: &PinRef<GateIds>| -> Option<Station> {
let ends = scene.cables.get(pin.node_id / 2)?;
Some(if pin.node_id.is_multiple_of(2) {
Station::at(ends.head, ends.head_side, Some(PinDirection::Output))
} else {
Station::at(ends.tail, ends.tail_side, Some(PinDirection::Input))
})
};
let centers = scene.anchors.clone();
let ring = |anchor: usize, orbit: u8| -> Option<Orbit> {
Some(Orbit {
center: *centers.get(anchor)?,
radius: GATE_INNER + GATE_STEP * orbit as f32,
})
};
let curve = |_edge: usize| EdgeCurve::default();
let built = graph.edge_hops(&station, &ring, &curve, None);
assert_eq!(
built.len(),
cables,
"seed {}: {} of {cables} edges lowered to a cable",
scene.seed,
built.len(),
);
let mut template = Vec::with_capacity(cables);
let mut at = Vec::with_capacity(cables);
let mut production = Vec::with_capacity(cables);
let mut chosen = vec![vec![0u8; anchors]; cables];
for (cable, geometry) in built.iter().enumerate() {
assert_eq!(
geometry.edge, cable,
"seed {}: the cables came back out of edge order",
scene.seed,
);
assert_eq!(
geometry.rings.len(),
anchors,
"seed {}: cable {cable} wraps {} of {anchors} anchors",
scene.seed,
geometry.rings.len(),
);
let mut hops = Vec::with_capacity(anchors);
for (visited, &(hop, (anchor, orbit))) in geometry.rings.iter().enumerate() {
assert_eq!(
anchor, visited,
"seed {}: cable {cable} reaches anchor {anchor} in position \
{visited}, so its run doubles back",
scene.seed,
);
chosen[cable][anchor] = orbit;
hops.push(hop);
}
let path = build(&geometry.hops, &EdgeCurve::default()).path;
let arcs = path
.segs
.iter()
.filter(|seg| matches!(seg, PathSeg::Arc { .. }))
.count();
if arcs != anchors {
return None;
}
template.push(geometry.hops.clone());
at.push(hops);
production.push(path);
}
let mut arrangements = Arrangements::new(template, at, scene.anchors.clone());
for (cable, path) in production.iter().enumerate() {
let tuple = arrangements.tuple(&chosen[cable]);
arrangements.ensure(cable, tuple);
assert_eq!(
arrangements.paths[cable][tuple].as_ref(),
Some(path),
"seed {}: cable {cable} re-geared onto its own rings is not the \
cable production built",
scene.seed,
);
}
let chosen_count = arrangements.crossings(&chosen);
let containment = arrangements.crossings(&containment_orbits(scene));
let perms = permutations(cables);
let mut picks: Vec<&[u8]> = Vec::with_capacity(anchors);
let mut orbits = vec![vec![0u8; anchors]; cables];
let mut best = usize::MAX;
for combination in 0..perms.len().pow(anchors as u32) {
let mut rest = combination;
picks.clear();
for _ in 0..anchors {
picks.push(perms[rest % perms.len()].as_slice());
rest /= perms.len();
}
for (cable, rings) in orbits.iter_mut().enumerate() {
for (ring, ordering) in rings.iter_mut().zip(&picks) {
*ring = ordering[cable];
}
}
best = best.min(arrangements.crossings(&orbits));
}
let escapes = if chosen_count > best {
escapes(&mut arrangements, &chosen, chosen_count, &perms)
} else {
Escapes::default()
};
Some(Measured {
chosen: chosen_count,
containment,
best,
escapes,
spanning: arrangements.spanning(&chosen),
})
}
fn escapes(
arrangements: &mut Arrangements,
chosen: &[Vec<u8>],
chosen_count: usize,
perms: &[Vec<u8>],
) -> Escapes {
let cables = chosen.len();
let anchors = chosen[0].len();
let mut found = Escapes::default();
let mut candidate = chosen.to_vec();
for inner in 0..cables {
for outer in inner + 1..cables {
for part in 1u32..1 << anchors {
candidate[inner].copy_from_slice(&chosen[inner]);
candidate[outer].copy_from_slice(&chosen[outer]);
for anchor in 0..anchors {
if part & (1 << anchor) != 0 {
candidate[inner][anchor] = chosen[outer][anchor];
candidate[outer][anchor] = chosen[inner][anchor];
}
}
if arrangements.crossings(&candidate) >= chosen_count {
continue;
}
match part.count_ones() as usize {
1 => found.local = true,
touched if touched == anchors => found.coupled = true,
_ => found.partial = true,
}
}
candidate[inner].copy_from_slice(&chosen[inner]);
candidate[outer].copy_from_slice(&chosen[outer]);
}
}
for anchor in 0..anchors {
for ordering in perms {
for (cable, rings) in candidate.iter_mut().enumerate() {
rings.copy_from_slice(&chosen[cable]);
rings[anchor] = ordering[cable];
}
found.reseated |= arrangements.crossings(&candidate) < chosen_count;
}
}
found
}
#[derive(Debug, Clone, Copy)]
struct Worst {
gap: usize,
seed: u64,
anchors: usize,
cables: usize,
measured: Measured,
}
impl Worst {
fn describe(&self) -> String {
format!(
"{} anchors, {} cables, seed {}: the search left {} corridor \
crossings, containment leaves {}, the best arrangement there is \
leaves {} - a gap of {}, {}",
self.anchors,
self.cables,
self.seed,
self.measured.chosen,
self.measured.containment,
self.measured.best,
self.gap,
if self.gap == 0 {
"which is the best arrangement there is"
} else if self.measured.escapes.within_reach() {
"with a move the search does look at still improving, so the \
budget ran out"
} else if self.measured.escapes.partial {
"a local minimum of both searched neighbourhoods that one \
exchange over PART of a shared route steps out of"
} else if self.measured.escapes.reseated {
"a local minimum of both searched neighbourhoods that one \
anchor reordered outright steps out of"
} else {
"a local minimum that no exchange over any part of a shared \
route and no reordering of one anchor steps out of"
},
)
}
}
#[derive(Debug, Default)]
struct Tally {
anchors: usize,
cables: usize,
layouts: usize,
skipped: usize,
dropped: usize,
gap: Vec<usize>,
crossing: usize,
improved: usize,
stalled: usize,
budgeted: usize,
partial: usize,
reseated: usize,
sealed: usize,
spanning: usize,
worst: Option<Worst>,
}
impl Tally {
fn exact(&self) -> usize {
self.gap.first().copied().unwrap_or(0)
}
fn absorb(&mut self, other: &Tally) {
self.layouts += other.layouts;
self.skipped += other.skipped;
self.dropped += other.dropped;
self.crossing += other.crossing;
self.improved += other.improved;
self.stalled += other.stalled;
self.budgeted += other.budgeted;
self.partial += other.partial;
self.reseated += other.reseated;
self.sealed += other.sealed;
self.spanning += other.spanning;
if self.gap.len() < other.gap.len() {
self.gap.resize(other.gap.len(), 0);
}
for (total, count) in self.gap.iter_mut().zip(&other.gap) {
*total += count;
}
if other
.worst
.is_some_and(|worst| self.worst.is_none_or(|held| held.gap < worst.gap))
{
self.worst = other.worst;
}
}
}
fn measure_seeds(anchors: usize, cables: usize, seeds: std::ops::Range<u64>) -> Tally {
let mut tally = Tally {
anchors,
cables,
..Tally::default()
};
for seed in seeds {
let Some(scene) = gate_scene(seed, anchors, cables) else {
tally.skipped += 1;
continue;
};
let Some(measured) = measure(&scene) else {
tally.dropped += 1;
continue;
};
tally.layouts += 1;
let shape = format!("{anchors} anchors, {cables} cables, seed {seed}");
assert!(
measured.chosen <= measured.containment,
"{shape}: the search left {} corridor crossings where containment \
alone leaves {} - a measured swap was accepted that made the \
arrangement worse",
measured.chosen,
measured.containment,
);
assert!(
measured.best <= measured.chosen,
"{shape}: the brute force bottomed out at {} corridor crossings, \
above the {} the search chose, so it does not cover every \
arrangement",
measured.best,
measured.chosen,
);
let gap = measured.chosen - measured.best;
if tally.gap.len() <= gap {
tally.gap.resize(gap + 1, 0);
}
tally.gap[gap] += 1;
tally.crossing += usize::from(measured.containment > 0);
tally.improved += usize::from(measured.chosen < measured.containment);
tally.spanning += usize::from(measured.spanning > 0);
if gap > 0 {
if measured.escapes.within_reach() {
tally.budgeted += 1;
} else {
tally.stalled += 1;
tally.partial += usize::from(measured.escapes.partial);
tally.reseated += usize::from(measured.escapes.reseated);
tally.sealed += usize::from(measured.escapes.none());
}
}
if tally.worst.is_none_or(|worst| worst.gap < gap) {
tally.worst = Some(Worst {
gap,
seed,
anchors,
cables,
measured,
});
}
}
tally
}
fn sweep(anchors: usize, cables: usize, count: u64) -> Tally {
let threads = std::thread::available_parallelism()
.map_or(1, std::num::NonZero::get)
.clamp(1, count.max(1) as usize) as u64;
let chunks: Vec<Tally> = std::thread::scope(|scope| {
let running: Vec<_> = (0..threads)
.map(|chunk| {
let seeds = count * chunk / threads..count * (chunk + 1) / threads;
scope.spawn(move || measure_seeds(anchors, cables, seeds))
})
.collect();
running
.into_iter()
.map(|handle| {
handle
.join()
.unwrap_or_else(|panic| std::panic::resume_unwind(panic))
})
.collect()
});
let mut tally = Tally {
anchors,
cables,
..Tally::default()
};
for chunk in &chunks {
tally.absorb(chunk);
}
tally
}
const GATE_SHAPES: [(usize, usize, u64); 6] = [
(2, 2, 300),
(2, 3, 300),
(3, 2, 300),
(2, 4, 150),
(3, 3, 200),
(3, 4, 40),
];
#[test]
#[ignore = "search-quality sweep: ~15s over 1111 layouts"]
fn the_orbit_search_holds_up_against_every_arrangement() {
let shapes: Vec<Tally> = GATE_SHAPES
.iter()
.map(|&(anchors, cables, count)| sweep(anchors, cables, count))
.collect();
let mut total = Tally::default();
for shape in &shapes {
total.absorb(shape);
}
let report = |tally: &Tally| {
format!(
"{} layouts ({} skipped as doubling back, \
{} with a wrap the geometry dropped), {} found the best \
arrangement, gaps {:?}, {} crossing under containment alone and \
{} of those improved on it, {} out of budget and {} in a local \
minimum - of those {} yield to an exchange over part of a route, \
{} to one anchor reordered, {} to nothing tried; {} carry a \
crossing only a spanning band would count; worst {}",
tally.layouts,
tally.skipped,
tally.dropped,
tally.exact(),
tally.gap,
tally.crossing,
tally.improved,
tally.budgeted,
tally.stalled,
tally.partial,
tally.reseated,
tally.sealed,
tally.spanning,
tally
.worst
.map_or("nothing measured".to_owned(), |worst| worst.describe()),
)
};
let named = |shape: &Tally| {
format!(
"{} anchors, {} cables: {}",
shape.anchors,
shape.cables,
report(shape),
)
};
let sweep: String = shapes.iter().map(|shape| named(shape) + "\n").collect();
assert!(
total.layouts > 900 && total.skipped > 0,
"the sweep did not generate the situation it measures: {}\n{sweep}",
report(&total),
);
assert!(
total.crossing * 2 > total.layouts,
"containment crosses a corridor in only {} of {} layouts, so the \
comparison mostly measures layouts with nothing to fix: {}\n{sweep}",
total.crossing,
total.layouts,
report(&total),
);
for shape in &shapes {
assert!(
shape.crossing * 3 >= shape.layouts,
"one shape carries almost no crossings to fix: {}\n{sweep}",
named(shape),
);
}
assert!(
total.improved * 3 >= total.crossing * 2,
"the search improved on containment in only {} of the {} layouts where \
containment crosses, so it is barely doing anything: {}\n{sweep}",
total.improved,
total.crossing,
report(&total),
);
let ample: Vec<(usize, usize)> = shapes
.iter()
.filter(|shape| shape.budgeted == 0)
.map(|shape| (shape.anchors, shape.cables))
.collect();
for shape in [(2, 2), (2, 3), (3, 2)] {
assert!(
ample.contains(&shape),
"{} anchors and {} cables ran out of budget, and the exactness \
floor only holds shapes that did not: it is asked of {ample:?}\
\n{sweep}",
shape.0,
shape.1,
);
}
for shape in shapes.iter().filter(|shape| shape.budgeted == 0) {
assert!(
shape.exact() * 20 >= shape.layouts * 17,
"with the budget seeing every move, the search found the best \
arrangement in {} of {} layouts of one shape, under the 85% this \
sweep measured 92.1% at worst for: {}\n{sweep}",
shape.exact(),
shape.layouts,
named(shape),
);
}
}
}