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 s2 = s + ss.normal() * stub;
170 let t2 = t + ts.normal() * stub;
171 let sr = inflate(
174 geo.source_rect
175 .unwrap_or_else(|| Rect::new(s.x, s.y, 0.0, 0.0)),
176 stub,
177 );
178 let tr = inflate(
179 geo.target_rect
180 .unwrap_or_else(|| Rect::new(t.x, t.y, 0.0, 0.0)),
181 stub,
182 );
183
184 let mut pts = vec![s, s2];
185 match (ss, ts) {
186 (Side::Right, Side::Left) | (Side::Left, Side::Right) => {
189 let forward = if ss == Side::Right {
190 t2.x >= s2.x
191 } else {
192 t2.x <= s2.x
193 };
194 if forward {
195 let mid_x = (s2.x + t2.x) / 2.0;
196 pts.push(Point::new(mid_x, s2.y));
197 pts.push(Point::new(mid_x, t2.y));
198 } else {
199 let mid_y = clear_lane(sr.y, sr.max_y(), tr.y, tr.max_y(), s2.y, t2.y);
200 pts.push(Point::new(s2.x, mid_y));
201 pts.push(Point::new(t2.x, mid_y));
202 }
203 }
204 (Side::Bottom, Side::Top) | (Side::Top, Side::Bottom) => {
206 let forward = if ss == Side::Bottom {
207 t2.y >= s2.y
208 } else {
209 t2.y <= s2.y
210 };
211 if forward {
212 let mid_y = (s2.y + t2.y) / 2.0;
213 pts.push(Point::new(s2.x, mid_y));
214 pts.push(Point::new(t2.x, mid_y));
215 } else {
216 let mid_x = clear_lane(sr.x, sr.max_x(), tr.x, tr.max_x(), s2.x, t2.x);
217 pts.push(Point::new(mid_x, s2.y));
218 pts.push(Point::new(mid_x, t2.y));
219 }
220 }
221 (Side::Right, Side::Right) => {
223 let outer = s2.x.max(t2.x).max(sr.max_x()).max(tr.max_x());
224 pts.push(Point::new(outer, s2.y));
225 pts.push(Point::new(outer, t2.y));
226 }
227 (Side::Left, Side::Left) => {
228 let outer = s2.x.min(t2.x).min(sr.x).min(tr.x);
229 pts.push(Point::new(outer, s2.y));
230 pts.push(Point::new(outer, t2.y));
231 }
232 (Side::Bottom, Side::Bottom) => {
234 let outer = s2.y.max(t2.y).max(sr.max_y()).max(tr.max_y());
235 pts.push(Point::new(s2.x, outer));
236 pts.push(Point::new(t2.x, outer));
237 }
238 (Side::Top, Side::Top) => {
239 let outer = s2.y.min(t2.y).min(sr.y).min(tr.y);
240 pts.push(Point::new(s2.x, outer));
241 pts.push(Point::new(t2.x, outer));
242 }
243 (Side::Right | Side::Left, _) => {
246 let source_ok = if ss == Side::Right {
247 t2.x >= s2.x
248 } else {
249 t2.x <= s2.x
250 };
251 let target_ok = if ts == Side::Top {
252 s2.y <= t2.y
253 } else {
254 s2.y >= t2.y
255 };
256 if source_ok && target_ok {
257 pts.push(Point::new(t2.x, s2.y));
258 } else {
259 let x_out = if ss == Side::Right {
260 s2.x.max(tr.max_x())
261 } else {
262 s2.x.min(tr.x)
263 };
264 pts.push(Point::new(x_out, s2.y));
265 pts.push(Point::new(x_out, t2.y));
266 }
267 }
268 (_, Side::Right | Side::Left) => {
270 let source_ok = if ss == Side::Bottom {
271 t2.y >= s2.y
272 } else {
273 t2.y <= s2.y
274 };
275 let target_ok = if ts == Side::Left {
276 s2.x <= t2.x
277 } else {
278 s2.x >= t2.x
279 };
280 if source_ok && target_ok {
281 pts.push(Point::new(s2.x, t2.y));
282 } else {
283 let y_out = if ss == Side::Bottom {
284 s2.y.max(tr.max_y())
285 } else {
286 s2.y.min(tr.y)
287 };
288 pts.push(Point::new(s2.x, y_out));
289 pts.push(Point::new(t2.x, y_out));
290 }
291 }
292 }
293 pts.push(t2);
294 pts.push(t);
295 simplify_points(pts)
296}
297
298fn inflate(r: Rect, m: f64) -> Rect {
299 Rect::new(r.x - m, r.y - m, r.width + 2.0 * m, r.height + 2.0 * m)
300}
301
302fn clear_lane(a0: f64, a1: f64, b0: f64, b1: f64, stub_a: f64, stub_b: f64) -> f64 {
306 if a1 < b0 {
307 return (a1 + b0) / 2.0;
308 }
309 if b1 < a0 {
310 return (b1 + a0) / 2.0;
311 }
312 let lo = a0.min(b0);
313 let hi = a1.max(b1);
314 let before = (stub_a - lo) + (stub_b - lo);
315 let after = (hi - stub_a) + (hi - stub_b);
316 if before <= after {
317 lo
318 } else {
319 hi
320 }
321}
322
323fn simplify_points(pts: Vec<Point>) -> Vec<Point> {
326 let mut out: Vec<Point> = Vec::with_capacity(pts.len());
327 for p in pts {
328 if out.last().map(|l| l.distance(p) > 0.01).unwrap_or(true) {
329 out.push(p);
330 }
331 }
332 let mut i = 1;
333 while i + 1 < out.len() {
334 let ab = out[i] - out[i - 1];
335 let bc = out[i + 1] - out[i];
336 let cross = ab.x * bc.y - ab.y * bc.x;
337 let dot = ab.x * bc.x + ab.y * bc.y;
338 if cross.abs() < 1e-6 && dot > 0.0 {
339 out.remove(i);
340 } else {
341 i += 1;
342 }
343 }
344 out
345}
346
347fn rounded_polyline(pts: &[Point], radius: f64) -> String {
350 if pts.is_empty() {
351 return String::new();
352 }
353 let mut d = format!("M{},{}", fmt(pts[0].x), fmt(pts[0].y));
354 for i in 1..pts.len().saturating_sub(1) {
355 let prev = pts[i - 1];
356 let p = pts[i];
357 let next = pts[i + 1];
358 let len_in = prev.distance(p);
359 let len_out = p.distance(next);
360 let ab = p - prev;
363 let bc = next - p;
364 let collinear = (ab.x * bc.y - ab.y * bc.x).abs() < 1e-6;
365 let r = radius.min(len_in / 2.0).min(len_out / 2.0);
366 if r < 0.1 || collinear {
367 d.push_str(&format!(" L{},{}", fmt(p.x), fmt(p.y)));
368 continue;
369 }
370 let a = p + (prev - p) * (r / len_in);
371 let b = p + (next - p) * (r / len_out);
372 d.push_str(&format!(
373 " L{},{} Q{},{} {},{}",
374 fmt(a.x),
375 fmt(a.y),
376 fmt(p.x),
377 fmt(p.y),
378 fmt(b.x),
379 fmt(b.y)
380 ));
381 }
382 if let Some(last) = pts.last() {
383 if pts.len() > 1 {
384 d.push_str(&format!(" L{},{}", fmt(last.x), fmt(last.y)));
385 }
386 }
387 d
388}
389
390fn polyline_midpoint(pts: &[Point]) -> Point {
392 if pts.is_empty() {
393 return Point::ZERO;
394 }
395 let total: f64 = pts.windows(2).map(|w| w[0].distance(w[1])).sum();
396 if total <= f64::EPSILON {
397 return pts[0];
398 }
399 let mut remaining = total / 2.0;
400 for w in pts.windows(2) {
401 let len = w[0].distance(w[1]);
402 if remaining <= len {
403 return w[0].lerp(w[1], remaining / len);
404 }
405 remaining -= len;
406 }
407 *pts.last().unwrap()
408}
409
410pub fn connection_path(from: Point, from_side: Side, to: Point, to_side: Option<Side>) -> String {
413 let geo = EdgeGeometry::new(
414 from,
415 from_side,
416 to,
417 to_side.unwrap_or_else(|| from_side.opposite()),
418 );
419 bezier_path(&geo, 0.25).d
420}
421
422#[cfg(test)]
423mod tests {
424 use super::*;
425
426 fn geo(s: (f64, f64), ss: Side, t: (f64, f64), ts: Side) -> EdgeGeometry {
427 EdgeGeometry::new(s.into(), ss, t.into(), ts)
428 }
429
430 fn drawn_points(d: &str) -> Vec<Point> {
434 let numbers: Vec<f64> = d
435 .split(|c: char| !(c.is_ascii_digit() || c == '.' || c == '-'))
436 .filter(|s| !s.is_empty())
437 .filter_map(|s| s.parse().ok())
438 .collect();
439 numbers
440 .chunks(2)
441 .filter(|c| c.len() == 2)
442 .map(|c| Point::new(c[0], c[1]))
443 .collect()
444 }
445
446 #[test]
451 fn the_bound_contains_everything_drawn() {
452 let sides = [Side::Left, Side::Right, Side::Top, Side::Bottom];
453 let kinds = [EdgeKind::Bezier, EdgeKind::Straight, EdgeKind::SmoothStep];
454 let offsets = [
455 (0.0, 0.0),
456 (200.0, 0.0),
457 (-200.0, 0.0),
458 (0.0, 200.0),
459 (0.0, -200.0),
460 (300.0, 180.0),
461 (-300.0, 180.0),
462 (300.0, -180.0),
463 (-300.0, -180.0),
464 (12.0, 5.0),
465 (-7.0, -3.0),
466 (1200.0, -900.0),
467 ];
468 for kind in kinds {
469 for ss in sides {
470 for ts in sides {
471 for (dx, dy) in offsets {
472 let g = geo((140.0, 90.0), ss, (140.0 + dx, 90.0 + dy), ts);
473 let bounds = edge_bounds(kind, &g);
474 for p in drawn_points(&edge_path(kind, &g).d) {
475 assert!(
476 p.x >= bounds.x - 1e-6
477 && p.x <= bounds.max_x() + 1e-6
478 && p.y >= bounds.y - 1e-6
479 && p.y <= bounds.max_y() + 1e-6,
480 "{kind:?} {ss:?}->{ts:?} d=({dx},{dy}): drawn {p:?} \
481 escapes bound {bounds:?}",
482 );
483 }
484 }
485 }
486 }
487 }
488 }
489
490 #[test]
493 fn a_degenerate_edge_still_has_a_finite_bound() {
494 for kind in [EdgeKind::Bezier, EdgeKind::Straight, EdgeKind::SmoothStep] {
495 let bounds = edge_bounds(
496 kind,
497 &geo((10.0, 10.0), Side::Right, (10.0, 10.0), Side::Left),
498 );
499 assert!(bounds.x.is_finite() && bounds.y.is_finite());
500 assert!(bounds.width.is_finite() && bounds.height.is_finite());
501 assert!(bounds.width >= 0.0 && bounds.height >= 0.0);
502 }
503 }
504
505 fn assert_no_reversals(pts: &[Point]) {
506 for w in pts.windows(3) {
507 let ab = w[1] - w[0];
508 let bc = w[2] - w[1];
509 let cross = ab.x * bc.y - ab.y * bc.x;
510 let dot = ab.x * bc.x + ab.y * bc.y;
511 assert!(
512 cross.abs() > 1e-6 || dot > 0.0,
513 "route doubles back at {:?} in {:?}",
514 w[1],
515 pts
516 );
517 }
518 }
519
520 fn strictly_inside(r: &Rect, p: Point) -> bool {
521 p.x > r.x && p.x < r.max_x() && p.y > r.y && p.y < r.max_y()
522 }
523
524 #[test]
525 fn bezier_endpoints() {
526 let g = geo((0.0, 0.0), Side::Bottom, (100.0, 200.0), Side::Top);
527 let p = bezier_path(&g, 0.25);
528 assert!(p.d.starts_with("M0,0 C"));
529 assert!(p.d.ends_with("100,200"));
530 assert!(p.label.y > 0.0 && p.label.y < 200.0);
532 assert_eq!(p.label.x, 50.0);
533 }
534
535 #[test]
536 fn straight_midpoint() {
537 let g = geo((0.0, 0.0), Side::Right, (10.0, 10.0), Side::Left);
538 let p = straight_path(&g);
539 assert_eq!(p.label, Point::new(5.0, 5.0));
540 }
541
542 #[test]
543 fn smooth_step_valid_path() {
544 let g = geo((0.0, 0.0), Side::Right, (200.0, 100.0), Side::Left);
545 let p = smooth_step_path(&g, 8.0);
546 assert!(p.d.starts_with("M0,0"));
547 assert!(p.d.ends_with("L200,100"));
548 assert!(p.d.contains('Q'), "expected rounded corners: {}", p.d);
549 }
550
551 #[test]
552 fn smooth_step_mixed_sides() {
553 let g = geo((0.0, 0.0), Side::Bottom, (200.0, 100.0), Side::Left);
554 let p = smooth_step_path(&g, 8.0);
555 assert!(p.d.starts_with("M0,0"));
556 assert!(p.d.ends_with("L200,100"));
557 }
558
559 #[test]
560 fn smooth_step_degenerate_same_point() {
561 let g = geo((50.0, 50.0), Side::Right, (50.0, 50.0), Side::Left);
562 let p = smooth_step_path(&g, 8.0);
563 assert!(p.d.starts_with("M50,50"));
564 }
565
566 #[test]
567 fn smooth_step_back_edge_detours_horizontally() {
568 let source_rect = Rect::new(400.0, 80.0, 160.0, 56.0);
572 let target_rect = Rect::new(100.0, 80.0, 160.0, 56.0);
573 let g = geo((560.0, 108.0), Side::Right, (100.0, 108.0), Side::Left)
574 .with_rects(source_rect, target_rect);
575 let pts = step_points(&g, 20.0);
576 assert_no_reversals(&pts);
577 for p in &pts {
578 assert!(
579 !strictly_inside(&source_rect, *p) && !strictly_inside(&target_rect, *p),
580 "waypoint {p:?} crosses a node in {pts:?}"
581 );
582 }
583 assert!(
584 pts.iter().any(|p| p.y <= 60.0 || p.y >= 156.0),
585 "no clear lane in {pts:?}"
586 );
587 }
588
589 #[test]
590 fn smooth_step_back_edge_detours_vertically() {
591 let target_rect = Rect::new(160.0, 450.0, 180.0, 56.0);
595 let source_rect = Rect::new(160.0, 600.0, 180.0, 56.0);
596 let g = geo((250.0, 656.0), Side::Bottom, (250.0, 450.0), Side::Top)
597 .with_rects(source_rect, target_rect);
598 let pts = step_points(&g, 20.0);
599 assert_no_reversals(&pts);
600 for p in &pts {
601 assert!(
602 !strictly_inside(&source_rect, *p) && !strictly_inside(&target_rect, *p),
603 "waypoint {p:?} crosses a node in {pts:?}"
604 );
605 }
606 assert!(
607 pts.iter().any(|p| p.x <= 140.0 || p.x >= 360.0),
608 "route never left the forward edge's column: {pts:?}"
609 );
610 }
611
612 #[test]
613 fn smooth_step_same_side_routes_outside_both() {
614 let source_rect = Rect::new(0.0, -20.0, 100.0, 40.0);
615 let target_rect = Rect::new(200.0, 180.0, 100.0, 40.0);
616 let g = geo((100.0, 0.0), Side::Right, (300.0, 200.0), Side::Right)
617 .with_rects(source_rect, target_rect);
618 let pts = step_points(&g, 20.0);
619 assert_no_reversals(&pts);
620 let max_x = pts.iter().fold(f64::MIN, |m, p| m.max(p.x));
621 assert!(max_x >= 320.0, "same-side route stayed inside: {pts:?}");
622 assert_eq!(*pts.last().unwrap(), Point::new(300.0, 200.0));
623 }
624}