1use crate::types::{EdgeKind, Point, Rect, Side};
5
6#[derive(Clone, Copy, PartialEq, Debug)]
8pub struct EdgeGeometry {
9 pub source: Point,
10 pub source_side: Side,
11 pub target: Point,
12 pub target_side: Side,
13 pub source_rect: Option<Rect>,
16 pub target_rect: Option<Rect>,
18}
19
20impl EdgeGeometry {
21 pub fn new(source: Point, source_side: Side, target: Point, target_side: Side) -> Self {
22 Self {
23 source,
24 source_side,
25 target,
26 target_side,
27 source_rect: None,
28 target_rect: None,
29 }
30 }
31
32 pub fn with_rects(mut self, source_rect: Rect, target_rect: Rect) -> Self {
33 self.source_rect = Some(source_rect);
34 self.target_rect = Some(target_rect);
35 self
36 }
37}
38
39#[derive(Clone, PartialEq, Debug)]
41pub struct EdgePath {
42 pub d: String,
43 pub label: Point,
44}
45
46pub fn edge_path(kind: EdgeKind, geo: &EdgeGeometry) -> EdgePath {
48 match kind {
49 EdgeKind::Bezier => bezier_path(geo, 0.25),
50 EdgeKind::Straight => straight_path(geo),
51 EdgeKind::SmoothStep => smooth_step_path(geo, 8.0),
52 }
53}
54
55fn fmt(v: f64) -> f64 {
56 (v * 100.0).round() / 100.0
58}
59
60pub fn edge_bounds(kind: EdgeKind, geo: &EdgeGeometry) -> Rect {
71 let mut points = vec![geo.source, geo.target];
72 match kind {
73 EdgeKind::Bezier => {
74 points.push(control_point(geo.source, geo.source_side, geo.target, 0.25));
75 points.push(control_point(geo.target, geo.target_side, geo.source, 0.25));
76 }
77 EdgeKind::Straight => {}
78 EdgeKind::SmoothStep => points.extend(step_points(geo, 20.0)),
79 }
80 let mut bounds = Rect::new(points[0].x, points[0].y, 0.0, 0.0);
81 for point in &points[1..] {
82 bounds = bounds.union(&Rect::new(point.x, point.y, 0.0, 0.0));
83 }
84 const ROUNDING: f64 = 0.01;
88 Rect::new(
89 bounds.x - ROUNDING,
90 bounds.y - ROUNDING,
91 bounds.width + 2.0 * ROUNDING,
92 bounds.height + 2.0 * ROUNDING,
93 )
94}
95
96fn control_offset(dist: f64, curvature: f64) -> f64 {
100 if dist >= 0.0 {
101 0.5 * dist
102 } else {
103 curvature * 25.0 * (-dist).sqrt()
104 }
105}
106
107fn control_point(p: Point, side: Side, other: Point, curvature: f64) -> Point {
108 match side {
109 Side::Left => Point::new(p.x - control_offset(p.x - other.x, curvature), p.y),
110 Side::Right => Point::new(p.x + control_offset(other.x - p.x, curvature), p.y),
111 Side::Top => Point::new(p.x, p.y - control_offset(p.y - other.y, curvature)),
112 Side::Bottom => Point::new(p.x, p.y + control_offset(other.y - p.y, curvature)),
113 }
114}
115
116pub fn bezier_path(geo: &EdgeGeometry, curvature: f64) -> EdgePath {
118 let s = geo.source;
119 let t = geo.target;
120 let c1 = control_point(s, geo.source_side, t, curvature);
121 let c2 = control_point(t, geo.target_side, s, curvature);
122 let d = format!(
123 "M{},{} C{},{} {},{} {},{}",
124 fmt(s.x),
125 fmt(s.y),
126 fmt(c1.x),
127 fmt(c1.y),
128 fmt(c2.x),
129 fmt(c2.y),
130 fmt(t.x),
131 fmt(t.y)
132 );
133 let label = Point::new(
135 (s.x + 3.0 * c1.x + 3.0 * c2.x + t.x) / 8.0,
136 (s.y + 3.0 * c1.y + 3.0 * c2.y + t.y) / 8.0,
137 );
138 EdgePath { d, label }
139}
140
141pub fn straight_path(geo: &EdgeGeometry) -> EdgePath {
143 let s = geo.source;
144 let t = geo.target;
145 EdgePath {
146 d: format!("M{},{} L{},{}", fmt(s.x), fmt(s.y), fmt(t.x), fmt(t.y)),
147 label: s.lerp(t, 0.5),
148 }
149}
150
151pub fn smooth_step_path(geo: &EdgeGeometry, radius: f64) -> EdgePath {
153 let points = step_points(geo, 20.0);
154 let label = polyline_midpoint(&points);
155 EdgePath {
156 d: rounded_polyline(&points, radius),
157 label,
158 }
159}
160
161fn step_points(geo: &EdgeGeometry, stub: f64) -> Vec<Point> {
165 let s = geo.source;
166 let t = geo.target;
167 let ss = geo.source_side;
168 let ts = geo.target_side;
169 let stub = if ss.opposite() == ts {
173 let normal = ss.normal();
174 let gap = (t.x - s.x) * normal.x + (t.y - s.y) * normal.y;
175 if gap >= 0.0 {
176 stub.min(gap / 2.0)
177 } else {
178 stub
179 }
180 } else {
181 stub
182 };
183 let s2 = s + ss.normal() * stub;
184 let t2 = t + ts.normal() * stub;
185 let sr = inflate(
188 geo.source_rect
189 .unwrap_or_else(|| Rect::new(s.x, s.y, 0.0, 0.0)),
190 stub,
191 );
192 let tr = inflate(
193 geo.target_rect
194 .unwrap_or_else(|| Rect::new(t.x, t.y, 0.0, 0.0)),
195 stub,
196 );
197
198 let mut pts = vec![s, s2];
199 match (ss, ts) {
200 (Side::Right, Side::Left) | (Side::Left, Side::Right) => {
203 let forward = if ss == Side::Right {
204 t.x >= s.x
205 } else {
206 t.x <= s.x
207 };
208 if forward {
209 let mid_x = (s2.x + t2.x) / 2.0;
210 pts.push(Point::new(mid_x, s2.y));
211 pts.push(Point::new(mid_x, t2.y));
212 } else {
213 let mid_y = clear_lane(sr.y, sr.max_y(), tr.y, tr.max_y(), s2.y, t2.y);
214 pts.push(Point::new(s2.x, mid_y));
215 pts.push(Point::new(t2.x, mid_y));
216 }
217 }
218 (Side::Bottom, Side::Top) | (Side::Top, Side::Bottom) => {
220 let forward = if ss == Side::Bottom {
221 t.y >= s.y
222 } else {
223 t.y <= s.y
224 };
225 if forward {
226 let mid_y = (s2.y + t2.y) / 2.0;
227 pts.push(Point::new(s2.x, mid_y));
228 pts.push(Point::new(t2.x, mid_y));
229 } else {
230 let mid_x = clear_lane(sr.x, sr.max_x(), tr.x, tr.max_x(), s2.x, t2.x);
231 pts.push(Point::new(mid_x, s2.y));
232 pts.push(Point::new(mid_x, t2.y));
233 }
234 }
235 (Side::Right, Side::Right) => {
237 let outer = s2.x.max(t2.x).max(sr.max_x()).max(tr.max_x());
238 pts.push(Point::new(outer, s2.y));
239 pts.push(Point::new(outer, t2.y));
240 }
241 (Side::Left, Side::Left) => {
242 let outer = s2.x.min(t2.x).min(sr.x).min(tr.x);
243 pts.push(Point::new(outer, s2.y));
244 pts.push(Point::new(outer, t2.y));
245 }
246 (Side::Bottom, Side::Bottom) => {
248 let outer = s2.y.max(t2.y).max(sr.max_y()).max(tr.max_y());
249 pts.push(Point::new(s2.x, outer));
250 pts.push(Point::new(t2.x, outer));
251 }
252 (Side::Top, Side::Top) => {
253 let outer = s2.y.min(t2.y).min(sr.y).min(tr.y);
254 pts.push(Point::new(s2.x, outer));
255 pts.push(Point::new(t2.x, outer));
256 }
257 (Side::Right | Side::Left, _) => {
260 let source_ok = if ss == Side::Right {
261 t2.x >= s2.x
262 } else {
263 t2.x <= s2.x
264 };
265 let target_ok = if ts == Side::Top {
266 s2.y <= t2.y
267 } else {
268 s2.y >= t2.y
269 };
270 if source_ok && target_ok {
271 pts.push(Point::new(t2.x, s2.y));
272 } else {
273 let x_out = if ss == Side::Right {
274 s2.x.max(tr.max_x())
275 } else {
276 s2.x.min(tr.x)
277 };
278 pts.push(Point::new(x_out, s2.y));
279 pts.push(Point::new(x_out, t2.y));
280 }
281 }
282 (_, Side::Right | Side::Left) => {
284 let source_ok = if ss == Side::Bottom {
285 t2.y >= s2.y
286 } else {
287 t2.y <= s2.y
288 };
289 let target_ok = if ts == Side::Left {
290 s2.x <= t2.x
291 } else {
292 s2.x >= t2.x
293 };
294 if source_ok && target_ok {
295 pts.push(Point::new(s2.x, t2.y));
296 } else {
297 let y_out = if ss == Side::Bottom {
298 s2.y.max(tr.max_y())
299 } else {
300 s2.y.min(tr.y)
301 };
302 pts.push(Point::new(s2.x, y_out));
303 pts.push(Point::new(t2.x, y_out));
304 }
305 }
306 }
307 pts.push(t2);
308 pts.push(t);
309 simplify_points(pts)
310}
311
312fn inflate(r: Rect, m: f64) -> Rect {
313 Rect::new(r.x - m, r.y - m, r.width + 2.0 * m, r.height + 2.0 * m)
314}
315
316fn clear_lane(a0: f64, a1: f64, b0: f64, b1: f64, stub_a: f64, stub_b: f64) -> f64 {
320 if a1 < b0 {
321 return (a1 + b0) / 2.0;
322 }
323 if b1 < a0 {
324 return (b1 + a0) / 2.0;
325 }
326 let lo = a0.min(b0);
327 let hi = a1.max(b1);
328 let before = (stub_a - lo) + (stub_b - lo);
329 let after = (hi - stub_a) + (hi - stub_b);
330 if before <= after {
331 lo
332 } else {
333 hi
334 }
335}
336
337fn simplify_points(pts: Vec<Point>) -> Vec<Point> {
340 let mut out: Vec<Point> = Vec::with_capacity(pts.len());
341 for p in pts {
342 if out.last().map(|l| l.distance(p) > 0.01).unwrap_or(true) {
343 out.push(p);
344 }
345 }
346 let mut i = 1;
347 while i + 1 < out.len() {
348 let ab = out[i] - out[i - 1];
349 let bc = out[i + 1] - out[i];
350 let cross = ab.x * bc.y - ab.y * bc.x;
351 let dot = ab.x * bc.x + ab.y * bc.y;
352 if cross.abs() < 1e-6 && dot > 0.0 {
353 out.remove(i);
354 } else {
355 i += 1;
356 }
357 }
358 out
359}
360
361fn rounded_polyline(pts: &[Point], radius: f64) -> String {
364 if pts.is_empty() {
365 return String::new();
366 }
367 let mut d = format!("M{},{}", fmt(pts[0].x), fmt(pts[0].y));
368 for i in 1..pts.len().saturating_sub(1) {
369 let prev = pts[i - 1];
370 let p = pts[i];
371 let next = pts[i + 1];
372 let len_in = prev.distance(p);
373 let len_out = p.distance(next);
374 let ab = p - prev;
377 let bc = next - p;
378 let collinear = (ab.x * bc.y - ab.y * bc.x).abs() < 1e-6;
379 let r = radius.min(len_in / 2.0).min(len_out / 2.0);
380 if r < 0.1 || collinear {
381 d.push_str(&format!(" L{},{}", fmt(p.x), fmt(p.y)));
382 continue;
383 }
384 let a = p + (prev - p) * (r / len_in);
385 let b = p + (next - p) * (r / len_out);
386 d.push_str(&format!(
387 " L{},{} Q{},{} {},{}",
388 fmt(a.x),
389 fmt(a.y),
390 fmt(p.x),
391 fmt(p.y),
392 fmt(b.x),
393 fmt(b.y)
394 ));
395 }
396 if let Some(last) = pts.last() {
397 if pts.len() > 1 {
398 d.push_str(&format!(" L{},{}", fmt(last.x), fmt(last.y)));
399 }
400 }
401 d
402}
403
404fn polyline_midpoint(pts: &[Point]) -> Point {
406 if pts.is_empty() {
407 return Point::ZERO;
408 }
409 let total: f64 = pts.windows(2).map(|w| w[0].distance(w[1])).sum();
410 if total <= f64::EPSILON {
411 return pts[0];
412 }
413 let mut remaining = total / 2.0;
414 for w in pts.windows(2) {
415 let len = w[0].distance(w[1]);
416 if remaining <= len {
417 return w[0].lerp(w[1], remaining / len);
418 }
419 remaining -= len;
420 }
421 *pts.last().unwrap()
422}
423
424pub fn connection_path(from: Point, from_side: Side, to: Point, to_side: Option<Side>) -> String {
427 let geo = EdgeGeometry::new(
428 from,
429 from_side,
430 to,
431 to_side.unwrap_or_else(|| from_side.opposite()),
432 );
433 bezier_path(&geo, 0.25).d
434}
435
436#[cfg(test)]
437mod tests {
438 use super::*;
439
440 fn geo(s: (f64, f64), ss: Side, t: (f64, f64), ts: Side) -> EdgeGeometry {
441 EdgeGeometry::new(s.into(), ss, t.into(), ts)
442 }
443
444 fn drawn_points(d: &str) -> Vec<Point> {
448 let numbers: Vec<f64> = d
449 .split(|c: char| !(c.is_ascii_digit() || c == '.' || c == '-'))
450 .filter(|s| !s.is_empty())
451 .filter_map(|s| s.parse().ok())
452 .collect();
453 numbers
454 .chunks(2)
455 .filter(|c| c.len() == 2)
456 .map(|c| Point::new(c[0], c[1]))
457 .collect()
458 }
459
460 #[test]
465 fn the_bound_contains_everything_drawn() {
466 let sides = [Side::Left, Side::Right, Side::Top, Side::Bottom];
467 let kinds = [EdgeKind::Bezier, EdgeKind::Straight, EdgeKind::SmoothStep];
468 let offsets = [
469 (0.0, 0.0),
470 (200.0, 0.0),
471 (-200.0, 0.0),
472 (0.0, 200.0),
473 (0.0, -200.0),
474 (300.0, 180.0),
475 (-300.0, 180.0),
476 (300.0, -180.0),
477 (-300.0, -180.0),
478 (12.0, 5.0),
479 (-7.0, -3.0),
480 (1200.0, -900.0),
481 ];
482 for kind in kinds {
483 for ss in sides {
484 for ts in sides {
485 for (dx, dy) in offsets {
486 let g = geo((140.0, 90.0), ss, (140.0 + dx, 90.0 + dy), ts);
487 let bounds = edge_bounds(kind, &g);
488 for p in drawn_points(&edge_path(kind, &g).d) {
489 assert!(
490 p.x >= bounds.x - 1e-6
491 && p.x <= bounds.max_x() + 1e-6
492 && p.y >= bounds.y - 1e-6
493 && p.y <= bounds.max_y() + 1e-6,
494 "{kind:?} {ss:?}->{ts:?} d=({dx},{dy}): drawn {p:?} \
495 escapes bound {bounds:?}",
496 );
497 }
498 }
499 }
500 }
501 }
502 }
503
504 #[test]
507 fn a_degenerate_edge_still_has_a_finite_bound() {
508 for kind in [EdgeKind::Bezier, EdgeKind::Straight, EdgeKind::SmoothStep] {
509 let bounds = edge_bounds(
510 kind,
511 &geo((10.0, 10.0), Side::Right, (10.0, 10.0), Side::Left),
512 );
513 assert!(bounds.x.is_finite() && bounds.y.is_finite());
514 assert!(bounds.width.is_finite() && bounds.height.is_finite());
515 assert!(bounds.width >= 0.0 && bounds.height >= 0.0);
516 }
517 }
518
519 fn assert_no_reversals(pts: &[Point]) {
520 for w in pts.windows(3) {
521 let ab = w[1] - w[0];
522 let bc = w[2] - w[1];
523 let cross = ab.x * bc.y - ab.y * bc.x;
524 let dot = ab.x * bc.x + ab.y * bc.y;
525 assert!(
526 cross.abs() > 1e-6 || dot > 0.0,
527 "route doubles back at {:?} in {:?}",
528 w[1],
529 pts
530 );
531 }
532 }
533
534 fn strictly_inside(r: &Rect, p: Point) -> bool {
535 p.x > r.x && p.x < r.max_x() && p.y > r.y && p.y < r.max_y()
536 }
537
538 #[test]
539 fn bezier_endpoints() {
540 let g = geo((0.0, 0.0), Side::Bottom, (100.0, 200.0), Side::Top);
541 let p = bezier_path(&g, 0.25);
542 assert!(p.d.starts_with("M0,0 C"));
543 assert!(p.d.ends_with("100,200"));
544 assert!(p.label.y > 0.0 && p.label.y < 200.0);
546 assert_eq!(p.label.x, 50.0);
547 }
548
549 #[test]
550 fn straight_midpoint() {
551 let g = geo((0.0, 0.0), Side::Right, (10.0, 10.0), Side::Left);
552 let p = straight_path(&g);
553 assert_eq!(p.label, Point::new(5.0, 5.0));
554 }
555
556 #[test]
557 fn smooth_step_valid_path() {
558 let g = geo((0.0, 0.0), Side::Right, (200.0, 100.0), Side::Left);
559 let p = smooth_step_path(&g, 8.0);
560 assert!(p.d.starts_with("M0,0"));
561 assert!(p.d.ends_with("L200,100"));
562 assert!(p.d.contains('Q'), "expected rounded corners: {}", p.d);
563 }
564
565 #[test]
566 fn smooth_step_facing_handles_stay_between_endpoints() {
567 for side in [Side::Right, Side::Left, Side::Bottom, Side::Top] {
568 let normal = side.normal();
569 let lateral = Point::new(-normal.y, normal.x);
570 for gap in [0.0, 0.5, 30.0, 39.99, 40.0, 40.01, 200.0] {
573 for offset in [-12.0, 0.0, 12.0] {
574 let source = Point::new(100.0, 100.0);
575 let target = source + normal * gap + lateral * offset;
576 let geometry = EdgeGeometry::new(source, side, target, side.opposite());
577 let source_corner = source - normal * 85.0 - Point::new(80.0, 80.0);
578 let target_corner = target + normal * 85.0 - Point::new(80.0, 80.0);
579 let with_rects = geometry.with_rects(
580 Rect::new(source_corner.x, source_corner.y, 160.0, 160.0),
581 Rect::new(target_corner.x, target_corner.y, 160.0, 160.0),
582 );
583 for geometry in [geometry, with_rects] {
584 let path = smooth_step_path(&geometry, 8.0);
585 let points = drawn_points(&path.d);
586 assert!(points.first().unwrap().distance(source) < 0.01);
587 assert!(points.last().unwrap().distance(target) < 0.01);
588 assert_no_reversals(&step_points(&geometry, 20.0));
589 for point in points.iter().chain(std::iter::once(&path.label)) {
593 assert!(
594 point.x.is_finite()
595 && point.y.is_finite()
596 && point.x >= source.x.min(target.x) - 0.01
597 && point.x <= source.x.max(target.x) + 0.01
598 && point.y >= source.y.min(target.y) - 0.01
599 && point.y <= source.y.max(target.y) + 0.01,
600 "{side:?}, gap={gap}, offset={offset}: {point:?} escapes {}",
601 path.d,
602 );
603 }
604 }
605 }
606 }
607 }
608 }
609
610 #[test]
611 fn smooth_step_mixed_sides() {
612 let g = geo((0.0, 0.0), Side::Bottom, (200.0, 100.0), Side::Left);
613 let p = smooth_step_path(&g, 8.0);
614 assert!(p.d.starts_with("M0,0"));
615 assert!(p.d.ends_with("L200,100"));
616 }
617
618 #[test]
619 fn smooth_step_degenerate_same_point() {
620 let g = geo((50.0, 50.0), Side::Right, (50.0, 50.0), Side::Left);
621 let p = smooth_step_path(&g, 8.0);
622 assert!(p.d.starts_with("M50,50"));
623 }
624
625 #[test]
626 fn smooth_step_back_edge_detours_horizontally() {
627 let source_rect = Rect::new(400.0, 80.0, 160.0, 56.0);
631 let target_rect = Rect::new(100.0, 80.0, 160.0, 56.0);
632 let g = geo((560.0, 108.0), Side::Right, (100.0, 108.0), Side::Left)
633 .with_rects(source_rect, target_rect);
634 let pts = step_points(&g, 20.0);
635 assert_no_reversals(&pts);
636 for p in &pts {
637 assert!(
638 !strictly_inside(&source_rect, *p) && !strictly_inside(&target_rect, *p),
639 "waypoint {p:?} crosses a node in {pts:?}"
640 );
641 }
642 assert!(
643 pts.iter().any(|p| p.y <= 60.0 || p.y >= 156.0),
644 "no clear lane in {pts:?}"
645 );
646 }
647
648 #[test]
649 fn smooth_step_back_edge_detours_vertically() {
650 let target_rect = Rect::new(160.0, 450.0, 180.0, 56.0);
654 let source_rect = Rect::new(160.0, 600.0, 180.0, 56.0);
655 let g = geo((250.0, 656.0), Side::Bottom, (250.0, 450.0), Side::Top)
656 .with_rects(source_rect, target_rect);
657 let pts = step_points(&g, 20.0);
658 assert_no_reversals(&pts);
659 for p in &pts {
660 assert!(
661 !strictly_inside(&source_rect, *p) && !strictly_inside(&target_rect, *p),
662 "waypoint {p:?} crosses a node in {pts:?}"
663 );
664 }
665 assert!(
666 pts.iter().any(|p| p.x <= 140.0 || p.x >= 360.0),
667 "route never left the forward edge's column: {pts:?}"
668 );
669 }
670
671 #[test]
672 fn smooth_step_same_side_routes_outside_both() {
673 let source_rect = Rect::new(0.0, -20.0, 100.0, 40.0);
674 let target_rect = Rect::new(200.0, 180.0, 100.0, 40.0);
675 let g = geo((100.0, 0.0), Side::Right, (300.0, 200.0), Side::Right)
676 .with_rects(source_rect, target_rect);
677 let pts = step_points(&g, 20.0);
678 assert_no_reversals(&pts);
679 let max_x = pts.iter().fold(f64::MIN, |m, p| m.max(p.x));
680 assert!(max_x >= 320.0, "same-side route stayed inside: {pts:?}");
681 assert_eq!(*pts.last().unwrap(), Point::new(300.0, 200.0));
682 }
683}