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
60fn control_offset(dist: f64, curvature: f64) -> f64 {
64 if dist >= 0.0 {
65 0.5 * dist
66 } else {
67 curvature * 25.0 * (-dist).sqrt()
68 }
69}
70
71fn control_point(p: Point, side: Side, other: Point, curvature: f64) -> Point {
72 match side {
73 Side::Left => Point::new(p.x - control_offset(p.x - other.x, curvature), p.y),
74 Side::Right => Point::new(p.x + control_offset(other.x - p.x, curvature), p.y),
75 Side::Top => Point::new(p.x, p.y - control_offset(p.y - other.y, curvature)),
76 Side::Bottom => Point::new(p.x, p.y + control_offset(other.y - p.y, curvature)),
77 }
78}
79
80pub fn bezier_path(geo: &EdgeGeometry, curvature: f64) -> EdgePath {
82 let s = geo.source;
83 let t = geo.target;
84 let c1 = control_point(s, geo.source_side, t, curvature);
85 let c2 = control_point(t, geo.target_side, s, curvature);
86 let d = format!(
87 "M{},{} C{},{} {},{} {},{}",
88 fmt(s.x),
89 fmt(s.y),
90 fmt(c1.x),
91 fmt(c1.y),
92 fmt(c2.x),
93 fmt(c2.y),
94 fmt(t.x),
95 fmt(t.y)
96 );
97 let label = Point::new(
99 (s.x + 3.0 * c1.x + 3.0 * c2.x + t.x) / 8.0,
100 (s.y + 3.0 * c1.y + 3.0 * c2.y + t.y) / 8.0,
101 );
102 EdgePath { d, label }
103}
104
105pub fn straight_path(geo: &EdgeGeometry) -> EdgePath {
107 let s = geo.source;
108 let t = geo.target;
109 EdgePath {
110 d: format!("M{},{} L{},{}", fmt(s.x), fmt(s.y), fmt(t.x), fmt(t.y)),
111 label: s.lerp(t, 0.5),
112 }
113}
114
115pub fn smooth_step_path(geo: &EdgeGeometry, radius: f64) -> EdgePath {
117 let points = step_points(geo, 20.0);
118 let label = polyline_midpoint(&points);
119 EdgePath {
120 d: rounded_polyline(&points, radius),
121 label,
122 }
123}
124
125fn step_points(geo: &EdgeGeometry, stub: f64) -> Vec<Point> {
129 let s = geo.source;
130 let t = geo.target;
131 let ss = geo.source_side;
132 let ts = geo.target_side;
133 let s2 = s + ss.normal() * stub;
134 let t2 = t + ts.normal() * stub;
135 let sr = inflate(
138 geo.source_rect
139 .unwrap_or_else(|| Rect::new(s.x, s.y, 0.0, 0.0)),
140 stub,
141 );
142 let tr = inflate(
143 geo.target_rect
144 .unwrap_or_else(|| Rect::new(t.x, t.y, 0.0, 0.0)),
145 stub,
146 );
147
148 let mut pts = vec![s, s2];
149 match (ss, ts) {
150 (Side::Right, Side::Left) | (Side::Left, Side::Right) => {
153 let forward = if ss == Side::Right {
154 t2.x >= s2.x
155 } else {
156 t2.x <= s2.x
157 };
158 if forward {
159 let mid_x = (s2.x + t2.x) / 2.0;
160 pts.push(Point::new(mid_x, s2.y));
161 pts.push(Point::new(mid_x, t2.y));
162 } else {
163 let mid_y = clear_lane(sr.y, sr.max_y(), tr.y, tr.max_y(), s2.y, t2.y);
164 pts.push(Point::new(s2.x, mid_y));
165 pts.push(Point::new(t2.x, mid_y));
166 }
167 }
168 (Side::Bottom, Side::Top) | (Side::Top, Side::Bottom) => {
170 let forward = if ss == Side::Bottom {
171 t2.y >= s2.y
172 } else {
173 t2.y <= s2.y
174 };
175 if forward {
176 let mid_y = (s2.y + t2.y) / 2.0;
177 pts.push(Point::new(s2.x, mid_y));
178 pts.push(Point::new(t2.x, mid_y));
179 } else {
180 let mid_x = clear_lane(sr.x, sr.max_x(), tr.x, tr.max_x(), s2.x, t2.x);
181 pts.push(Point::new(mid_x, s2.y));
182 pts.push(Point::new(mid_x, t2.y));
183 }
184 }
185 (Side::Right, Side::Right) => {
187 let outer = s2.x.max(t2.x).max(sr.max_x()).max(tr.max_x());
188 pts.push(Point::new(outer, s2.y));
189 pts.push(Point::new(outer, t2.y));
190 }
191 (Side::Left, Side::Left) => {
192 let outer = s2.x.min(t2.x).min(sr.x).min(tr.x);
193 pts.push(Point::new(outer, s2.y));
194 pts.push(Point::new(outer, t2.y));
195 }
196 (Side::Bottom, Side::Bottom) => {
198 let outer = s2.y.max(t2.y).max(sr.max_y()).max(tr.max_y());
199 pts.push(Point::new(s2.x, outer));
200 pts.push(Point::new(t2.x, outer));
201 }
202 (Side::Top, Side::Top) => {
203 let outer = s2.y.min(t2.y).min(sr.y).min(tr.y);
204 pts.push(Point::new(s2.x, outer));
205 pts.push(Point::new(t2.x, outer));
206 }
207 (Side::Right | Side::Left, _) => {
210 let source_ok = if ss == Side::Right {
211 t2.x >= s2.x
212 } else {
213 t2.x <= s2.x
214 };
215 let target_ok = if ts == Side::Top {
216 s2.y <= t2.y
217 } else {
218 s2.y >= t2.y
219 };
220 if source_ok && target_ok {
221 pts.push(Point::new(t2.x, s2.y));
222 } else {
223 let x_out = if ss == Side::Right {
224 s2.x.max(tr.max_x())
225 } else {
226 s2.x.min(tr.x)
227 };
228 pts.push(Point::new(x_out, s2.y));
229 pts.push(Point::new(x_out, t2.y));
230 }
231 }
232 (_, Side::Right | Side::Left) => {
234 let source_ok = if ss == Side::Bottom {
235 t2.y >= s2.y
236 } else {
237 t2.y <= s2.y
238 };
239 let target_ok = if ts == Side::Left {
240 s2.x <= t2.x
241 } else {
242 s2.x >= t2.x
243 };
244 if source_ok && target_ok {
245 pts.push(Point::new(s2.x, t2.y));
246 } else {
247 let y_out = if ss == Side::Bottom {
248 s2.y.max(tr.max_y())
249 } else {
250 s2.y.min(tr.y)
251 };
252 pts.push(Point::new(s2.x, y_out));
253 pts.push(Point::new(t2.x, y_out));
254 }
255 }
256 }
257 pts.push(t2);
258 pts.push(t);
259 simplify_points(pts)
260}
261
262fn inflate(r: Rect, m: f64) -> Rect {
263 Rect::new(r.x - m, r.y - m, r.width + 2.0 * m, r.height + 2.0 * m)
264}
265
266fn clear_lane(a0: f64, a1: f64, b0: f64, b1: f64, stub_a: f64, stub_b: f64) -> f64 {
270 if a1 < b0 {
271 return (a1 + b0) / 2.0;
272 }
273 if b1 < a0 {
274 return (b1 + a0) / 2.0;
275 }
276 let lo = a0.min(b0);
277 let hi = a1.max(b1);
278 let before = (stub_a - lo) + (stub_b - lo);
279 let after = (hi - stub_a) + (hi - stub_b);
280 if before <= after {
281 lo
282 } else {
283 hi
284 }
285}
286
287fn simplify_points(pts: Vec<Point>) -> Vec<Point> {
290 let mut out: Vec<Point> = Vec::with_capacity(pts.len());
291 for p in pts {
292 if out.last().map(|l| l.distance(p) > 0.01).unwrap_or(true) {
293 out.push(p);
294 }
295 }
296 let mut i = 1;
297 while i + 1 < out.len() {
298 let ab = out[i] - out[i - 1];
299 let bc = out[i + 1] - out[i];
300 let cross = ab.x * bc.y - ab.y * bc.x;
301 let dot = ab.x * bc.x + ab.y * bc.y;
302 if cross.abs() < 1e-6 && dot > 0.0 {
303 out.remove(i);
304 } else {
305 i += 1;
306 }
307 }
308 out
309}
310
311fn rounded_polyline(pts: &[Point], radius: f64) -> String {
314 if pts.is_empty() {
315 return String::new();
316 }
317 let mut d = format!("M{},{}", fmt(pts[0].x), fmt(pts[0].y));
318 for i in 1..pts.len().saturating_sub(1) {
319 let prev = pts[i - 1];
320 let p = pts[i];
321 let next = pts[i + 1];
322 let len_in = prev.distance(p);
323 let len_out = p.distance(next);
324 let ab = p - prev;
327 let bc = next - p;
328 let collinear = (ab.x * bc.y - ab.y * bc.x).abs() < 1e-6;
329 let r = radius.min(len_in / 2.0).min(len_out / 2.0);
330 if r < 0.1 || collinear {
331 d.push_str(&format!(" L{},{}", fmt(p.x), fmt(p.y)));
332 continue;
333 }
334 let a = p + (prev - p) * (r / len_in);
335 let b = p + (next - p) * (r / len_out);
336 d.push_str(&format!(
337 " L{},{} Q{},{} {},{}",
338 fmt(a.x),
339 fmt(a.y),
340 fmt(p.x),
341 fmt(p.y),
342 fmt(b.x),
343 fmt(b.y)
344 ));
345 }
346 if let Some(last) = pts.last() {
347 if pts.len() > 1 {
348 d.push_str(&format!(" L{},{}", fmt(last.x), fmt(last.y)));
349 }
350 }
351 d
352}
353
354fn polyline_midpoint(pts: &[Point]) -> Point {
356 if pts.is_empty() {
357 return Point::ZERO;
358 }
359 let total: f64 = pts.windows(2).map(|w| w[0].distance(w[1])).sum();
360 if total <= f64::EPSILON {
361 return pts[0];
362 }
363 let mut remaining = total / 2.0;
364 for w in pts.windows(2) {
365 let len = w[0].distance(w[1]);
366 if remaining <= len {
367 return w[0].lerp(w[1], remaining / len);
368 }
369 remaining -= len;
370 }
371 *pts.last().unwrap()
372}
373
374pub fn connection_path(from: Point, from_side: Side, to: Point, to_side: Option<Side>) -> String {
377 let geo = EdgeGeometry::new(
378 from,
379 from_side,
380 to,
381 to_side.unwrap_or_else(|| from_side.opposite()),
382 );
383 bezier_path(&geo, 0.25).d
384}
385
386#[cfg(test)]
387mod tests {
388 use super::*;
389
390 fn geo(s: (f64, f64), ss: Side, t: (f64, f64), ts: Side) -> EdgeGeometry {
391 EdgeGeometry::new(s.into(), ss, t.into(), ts)
392 }
393
394 fn assert_no_reversals(pts: &[Point]) {
395 for w in pts.windows(3) {
396 let ab = w[1] - w[0];
397 let bc = w[2] - w[1];
398 let cross = ab.x * bc.y - ab.y * bc.x;
399 let dot = ab.x * bc.x + ab.y * bc.y;
400 assert!(
401 cross.abs() > 1e-6 || dot > 0.0,
402 "route doubles back at {:?} in {:?}",
403 w[1],
404 pts
405 );
406 }
407 }
408
409 fn strictly_inside(r: &Rect, p: Point) -> bool {
410 p.x > r.x && p.x < r.max_x() && p.y > r.y && p.y < r.max_y()
411 }
412
413 #[test]
414 fn bezier_endpoints() {
415 let g = geo((0.0, 0.0), Side::Bottom, (100.0, 200.0), Side::Top);
416 let p = bezier_path(&g, 0.25);
417 assert!(p.d.starts_with("M0,0 C"));
418 assert!(p.d.ends_with("100,200"));
419 assert!(p.label.y > 0.0 && p.label.y < 200.0);
421 assert_eq!(p.label.x, 50.0);
422 }
423
424 #[test]
425 fn straight_midpoint() {
426 let g = geo((0.0, 0.0), Side::Right, (10.0, 10.0), Side::Left);
427 let p = straight_path(&g);
428 assert_eq!(p.label, Point::new(5.0, 5.0));
429 }
430
431 #[test]
432 fn smooth_step_valid_path() {
433 let g = geo((0.0, 0.0), Side::Right, (200.0, 100.0), Side::Left);
434 let p = smooth_step_path(&g, 8.0);
435 assert!(p.d.starts_with("M0,0"));
436 assert!(p.d.ends_with("L200,100"));
437 assert!(p.d.contains('Q'), "expected rounded corners: {}", p.d);
438 }
439
440 #[test]
441 fn smooth_step_mixed_sides() {
442 let g = geo((0.0, 0.0), Side::Bottom, (200.0, 100.0), Side::Left);
443 let p = smooth_step_path(&g, 8.0);
444 assert!(p.d.starts_with("M0,0"));
445 assert!(p.d.ends_with("L200,100"));
446 }
447
448 #[test]
449 fn smooth_step_degenerate_same_point() {
450 let g = geo((50.0, 50.0), Side::Right, (50.0, 50.0), Side::Left);
451 let p = smooth_step_path(&g, 8.0);
452 assert!(p.d.starts_with("M50,50"));
453 }
454
455 #[test]
456 fn smooth_step_back_edge_detours_horizontally() {
457 let source_rect = Rect::new(400.0, 80.0, 160.0, 56.0);
461 let target_rect = Rect::new(100.0, 80.0, 160.0, 56.0);
462 let g = geo((560.0, 108.0), Side::Right, (100.0, 108.0), Side::Left)
463 .with_rects(source_rect, target_rect);
464 let pts = step_points(&g, 20.0);
465 assert_no_reversals(&pts);
466 for p in &pts {
467 assert!(
468 !strictly_inside(&source_rect, *p) && !strictly_inside(&target_rect, *p),
469 "waypoint {p:?} crosses a node in {pts:?}"
470 );
471 }
472 assert!(
473 pts.iter().any(|p| p.y <= 60.0 || p.y >= 156.0),
474 "no clear lane in {pts:?}"
475 );
476 }
477
478 #[test]
479 fn smooth_step_back_edge_detours_vertically() {
480 let target_rect = Rect::new(160.0, 450.0, 180.0, 56.0);
484 let source_rect = Rect::new(160.0, 600.0, 180.0, 56.0);
485 let g = geo((250.0, 656.0), Side::Bottom, (250.0, 450.0), Side::Top)
486 .with_rects(source_rect, target_rect);
487 let pts = step_points(&g, 20.0);
488 assert_no_reversals(&pts);
489 for p in &pts {
490 assert!(
491 !strictly_inside(&source_rect, *p) && !strictly_inside(&target_rect, *p),
492 "waypoint {p:?} crosses a node in {pts:?}"
493 );
494 }
495 assert!(
496 pts.iter().any(|p| p.x <= 140.0 || p.x >= 360.0),
497 "route never left the forward edge's column: {pts:?}"
498 );
499 }
500
501 #[test]
502 fn smooth_step_same_side_routes_outside_both() {
503 let source_rect = Rect::new(0.0, -20.0, 100.0, 40.0);
504 let target_rect = Rect::new(200.0, 180.0, 100.0, 40.0);
505 let g = geo((100.0, 0.0), Side::Right, (300.0, 200.0), Side::Right)
506 .with_rects(source_rect, target_rect);
507 let pts = step_points(&g, 20.0);
508 assert_no_reversals(&pts);
509 let max_x = pts.iter().fold(f64::MIN, |m, p| m.max(p.x));
510 assert!(max_x >= 320.0, "same-side route stayed inside: {pts:?}");
511 assert_eq!(*pts.last().unwrap(), Point::new(300.0, 200.0));
512 }
513}