1use gpui::{Bounds, Hsla, PathBuilder, Pixels, Point, SharedString, Window, point, px};
4use gpui_kit_theme::Theme;
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
8pub enum EdgeKind {
9 #[default]
10 Flow,
11 Feedback,
12}
13
14impl EdgeKind {
15 pub fn color(self, theme: &Theme) -> Hsla {
16 match self {
17 Self::Flow => theme.colors.hairline_strong,
18 Self::Feedback => theme.colors.danger,
19 }
20 }
21
22 fn dashes(self) -> Option<[Pixels; 2]> {
23 (self == Self::Feedback).then(|| [px(5.0), px(4.0)])
24 }
25}
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
29pub enum PortSide {
30 Top,
31 Right,
32 Bottom,
33 #[default]
34 Left,
35}
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38pub(crate) enum Axis {
39 Horizontal,
40 Vertical,
41}
42
43impl PortSide {
44 pub(crate) fn outward(self) -> Point<f32> {
45 match self {
46 Self::Top => point(0.0, -1.0),
47 Self::Right => point(1.0, 0.0),
48 Self::Bottom => point(0.0, 1.0),
49 Self::Left => point(-1.0, 0.0),
50 }
51 }
52
53 pub(crate) fn axis(self) -> Axis {
54 match self {
55 Self::Left | Self::Right => Axis::Horizontal,
56 Self::Top | Self::Bottom => Axis::Vertical,
57 }
58 }
59}
60
61#[derive(Debug, Clone, PartialEq, Eq)]
63pub struct GraphEndpoint {
64 pub node: SharedString,
65 pub port: SharedString,
66}
67
68impl GraphEndpoint {
69 pub fn new(node: impl Into<SharedString>, port: impl Into<SharedString>) -> Self {
71 Self {
72 node: node.into(),
73 port: port.into(),
74 }
75 }
76}
77
78#[derive(Debug, Clone, PartialEq, Eq)]
80pub struct GraphEdge {
81 from: SharedString,
82 to: SharedString,
83 kind: EdgeKind,
84 id: Option<SharedString>,
85 from_port: Option<SharedString>,
86 to_port: Option<SharedString>,
87 label: Option<SharedString>,
88 active: bool,
89 lane: i16,
90}
91
92impl GraphEdge {
93 pub fn new(from: impl Into<SharedString>, to: impl Into<SharedString>) -> Self {
94 Self {
95 from: from.into(),
96 to: to.into(),
97 kind: EdgeKind::Flow,
98 id: None,
99 from_port: None,
100 to_port: None,
101 label: None,
102 active: false,
103 lane: 0,
104 }
105 }
106
107 pub fn from(&self) -> &SharedString {
108 &self.from
109 }
110 pub fn to(&self) -> &SharedString {
111 &self.to
112 }
113 pub fn kind(&self) -> EdgeKind {
114 self.kind
115 }
116 pub fn id(mut self, id: impl Into<SharedString>) -> Self {
117 self.id = Some(id.into());
118 self
119 }
120 pub fn ports(mut self, from: impl Into<SharedString>, to: impl Into<SharedString>) -> Self {
121 self.from_port = Some(from.into());
122 self.to_port = Some(to.into());
123 self
124 }
125 pub fn label(mut self, label: impl Into<SharedString>) -> Self {
126 self.label = Some(label.into());
127 self
128 }
129 pub fn active(mut self, active: bool) -> Self {
130 self.active = active;
131 self
132 }
133 pub fn lane(mut self, lane: i16) -> Self {
134 self.lane = lane;
135 self
136 }
137 pub fn feedback(mut self) -> Self {
138 self.kind = EdgeKind::Feedback;
139 self
140 }
141
142 pub(crate) fn source_port(&self) -> Option<&SharedString> {
143 self.from_port.as_ref()
144 }
145 pub(crate) fn target_port(&self) -> Option<&SharedString> {
146 self.to_port.as_ref()
147 }
148 pub(crate) fn edge_label(&self) -> Option<&SharedString> {
149 self.label.as_ref()
150 }
151 pub(crate) fn is_active(&self) -> bool {
152 self.active
153 }
154 pub(crate) fn edge_lane(&self) -> i16 {
155 self.lane
156 }
157 pub(crate) fn identity(&self) -> SharedString {
158 if let Some(id) = &self.id {
159 return id.clone();
160 }
161 let kind = match self.kind {
163 EdgeKind::Flow => "flow",
164 EdgeKind::Feedback => "feedback",
165 };
166 format!(
167 "{}:{}|{}:{}|{}:{}|{}:{}|{}|{}",
168 self.from.len(),
169 self.from,
170 self.to.len(),
171 self.to,
172 self.from_port.as_ref().map_or(0, |v| v.len()),
173 self.from_port.as_deref().unwrap_or(""),
174 self.to_port.as_ref().map_or(0, |v| v.len()),
175 self.to_port.as_deref().unwrap_or(""),
176 kind,
177 self.lane
178 )
179 .into()
180 }
181}
182
183#[derive(Debug, Clone, Copy, PartialEq)]
184pub(crate) struct Anchor {
185 pub(crate) point: Point<f32>,
186 pub(crate) side: PortSide,
187}
188
189#[derive(Debug, Clone)]
190pub(crate) struct OrthogonalRoute {
191 points: Vec<Point<f32>>,
192 cumulative: Vec<f32>,
193 total: f32,
194}
195
196impl OrthogonalRoute {
197 fn new(points: Vec<Point<f32>>) -> Self {
198 let points = normalize(points);
199 let mut cumulative = vec![0.0];
200 for pair in points.windows(2) {
201 cumulative.push(
202 cumulative.last().copied().unwrap_or(0.0)
203 + (pair[1].x - pair[0].x).abs()
204 + (pair[1].y - pair[0].y).abs(),
205 );
206 }
207 let total = cumulative.last().copied().unwrap_or(0.0);
208 Self {
209 points,
210 cumulative,
211 total,
212 }
213 }
214 pub(crate) fn points(&self) -> &[Point<f32>] {
215 &self.points
216 }
217 #[cfg(test)]
218 pub(crate) fn total_length(&self) -> f32 {
219 self.total
220 }
221 pub(crate) fn sample(&self, progress: f32) -> Point<f32> {
222 let Some(&first) = self.points.first() else {
223 return point(0.0, 0.0);
224 };
225 if self.total == 0.0 {
226 return first;
227 }
228 let target = progress.clamp(0.0, 1.0) * self.total;
229 let index = self
230 .cumulative
231 .partition_point(|&length| length < target)
232 .clamp(1, self.points.len() - 1);
233 let start_length = self.cumulative[index - 1];
234 let segment = self.cumulative[index] - start_length;
235 let t = if segment == 0.0 {
236 0.0
237 } else {
238 (target - start_length) / segment
239 };
240 point(
241 self.points[index - 1].x + (self.points[index].x - self.points[index - 1].x) * t,
242 self.points[index - 1].y + (self.points[index].y - self.points[index - 1].y) * t,
243 )
244 }
245 pub(crate) fn midpoint(&self) -> Point<f32> {
246 self.sample(0.5)
247 }
248
249 pub(crate) fn midpoint_axis(&self) -> Axis {
250 if self.points.len() < 2 {
251 return Axis::Horizontal;
252 }
253 let target = self.total * 0.5;
254 let index = self
255 .cumulative
256 .partition_point(|length| *length < target)
257 .clamp(1, self.points.len() - 1);
258 if self.points[index - 1].x == self.points[index].x {
259 Axis::Vertical
260 } else {
261 Axis::Horizontal
262 }
263 }
264}
265
266const LEAD: f32 = 24.0;
267const CORRIDOR: f32 = 36.0;
268const LANE_SPACING: f32 = 12.0;
269const MIN_LEAD: f32 = 4.0;
270
271pub(crate) fn route_orthogonal(
272 from: Anchor,
273 to: Anchor,
274 from_bounds: Bounds<f32>,
275 to_bounds: Bounds<f32>,
276 kind: EdgeKind,
277 lane: i16,
278) -> Option<OrthogonalRoute> {
279 if from.point == to.point {
280 return Some(self_route(from, from_bounds, lane));
281 }
282 let lane_offset = lane as f32 * LANE_SPACING;
283 let preferred_lead = (LEAD + lane_offset).max(MIN_LEAD);
287 let a = from.outward_point(lead_distance(from, to_bounds, preferred_lead)?);
288 let b = to.outward_point(lead_distance(to, from_bounds, preferred_lead)?);
289 let left = from_bounds.left().min(to_bounds.left()) - CORRIDOR;
290 let right = from_bounds.right().max(to_bounds.right()) + CORRIDOR;
291 let top = from_bounds.top().min(to_bounds.top()) - CORRIDOR;
292 let bottom = from_bounds.bottom().max(to_bounds.bottom()) + CORRIDOR;
293
294 let finish = |middle: Vec<Point<f32>>| {
295 let mut points = Vec::with_capacity(middle.len() + 2);
296 points.push(from.point);
297 points.extend(middle);
298 points.push(to.point);
299 let route = OrthogonalRoute::new(points);
300 let clear = route.points().windows(2).all(|pair| {
301 segment_clear(pair[0], pair[1], from_bounds)
302 && segment_clear(pair[0], pair[1], to_bounds)
303 });
304 (clear && route_is_directional(&route, from, to)).then_some(route)
305 };
306
307 if kind == EdgeKind::Feedback {
311 let y = bottom + lane_offset;
312 let middle = vec![a, point(a.x, y), point(b.x, y), b];
313 if let Some(route) = finish(middle) {
314 return Some(route);
315 }
316 }
317
318 if lane != 0 {
322 let candidates = match from.side.axis() {
323 Axis::Horizontal => {
324 let near = (a.y + b.y) / 2.0 + lane_offset;
325 let outside = if lane > 0 {
326 bottom + lane_offset.abs()
327 } else {
328 top - lane_offset.abs()
329 };
330 vec![
331 vec![a, point(a.x, near), point(b.x, near), b],
332 vec![a, point(a.x, outside), point(b.x, outside), b],
333 ]
334 }
335 Axis::Vertical => {
336 let near = (a.x + b.x) / 2.0 + lane_offset;
337 let outside = if lane > 0 {
338 right + lane_offset.abs()
339 } else {
340 left - lane_offset.abs()
341 };
342 vec![
343 vec![a, point(near, a.y), point(near, b.y), b],
344 vec![a, point(outside, a.y), point(outside, b.y), b],
345 ]
346 }
347 };
348 if let Some(route) = candidates.into_iter().find_map(&finish) {
349 return Some(route);
350 }
351 }
352
353 let mut candidates = vec![Vec::new()];
354 if a.x == b.x || a.y == b.y {
355 candidates.push(vec![a, b]);
356 }
357 candidates.push(vec![a, point(b.x, a.y), b]);
358 candidates.push(vec![a, point(a.x, b.y), b]);
359
360 let middle_x = (a.x + b.x) / 2.0;
361 let middle_y = (a.y + b.y) / 2.0;
362 for x in [middle_x, left, right] {
363 candidates.push(vec![a, point(x, a.y), point(x, b.y), b]);
364 }
365 for y in [middle_y, top, bottom] {
366 candidates.push(vec![a, point(a.x, y), point(b.x, y), b]);
367 }
368
369 candidates
370 .into_iter()
371 .filter_map(finish)
372 .min_by(|left, right| {
373 path_cost(left.points())
374 .partial_cmp(&path_cost(right.points()))
375 .unwrap_or(std::cmp::Ordering::Equal)
376 })
377}
378
379fn lead_distance(anchor: Anchor, obstacle: Bounds<f32>, preferred: f32) -> Option<f32> {
380 const EPSILON: f32 = 0.001;
381 let point = anchor.point;
382 if point.x > obstacle.left() + EPSILON
383 && point.x < obstacle.right() - EPSILON
384 && point.y > obstacle.top() + EPSILON
385 && point.y < obstacle.bottom() - EPSILON
386 {
387 return None;
388 }
389 let crosses_vertical_span =
390 point.y > obstacle.top() + EPSILON && point.y < obstacle.bottom() - EPSILON;
391 let crosses_horizontal_span =
392 point.x > obstacle.left() + EPSILON && point.x < obstacle.right() - EPSILON;
393 let clearance = match anchor.side {
394 PortSide::Right if crosses_vertical_span && obstacle.left() >= point.x => {
395 Some(obstacle.left() - point.x)
396 }
397 PortSide::Left if crosses_vertical_span && obstacle.right() <= point.x => {
398 Some(point.x - obstacle.right())
399 }
400 PortSide::Bottom if crosses_horizontal_span && obstacle.top() >= point.y => {
401 Some(obstacle.top() - point.y)
402 }
403 PortSide::Top if crosses_horizontal_span && obstacle.bottom() <= point.y => {
404 Some(point.y - obstacle.bottom())
405 }
406 _ => None,
407 };
408 match clearance {
409 Some(clearance) if clearance <= EPSILON => None,
410 Some(clearance) => Some(preferred.min(clearance * 0.5)),
411 None => Some(preferred),
412 }
413}
414
415fn route_is_directional(route: &OrthogonalRoute, from: Anchor, to: Anchor) -> bool {
416 let Some(first) = route.points().get(1) else {
417 return false;
418 };
419 let Some(before) = route.points().get(route.points().len().saturating_sub(2)) else {
420 return false;
421 };
422 let from_normal = from.side.outward();
423 let to_normal = to.side.outward();
424 (first.x - from.point.x) * from_normal.x + (first.y - from.point.y) * from_normal.y > 0.0
425 && (before.x - to.point.x) * to_normal.x + (before.y - to.point.y) * to_normal.y > 0.0
426}
427
428pub(crate) fn route_preview(from: Anchor, to: Point<f32>) -> OrthogonalRoute {
432 let lead = from.outward_point(LEAD);
433 let elbow = match from.side.axis() {
434 Axis::Horizontal => point(to.x, lead.y),
435 Axis::Vertical => point(lead.x, to.y),
436 };
437 OrthogonalRoute::new(vec![from.point, lead, elbow, to])
438}
439
440impl Anchor {
441 fn outward_point(self, distance: f32) -> Point<f32> {
442 let normal = self.side.outward();
443 point(
444 self.point.x + normal.x * distance,
445 self.point.y + normal.y * distance,
446 )
447 }
448}
449
450fn self_route(anchor: Anchor, bounds: Bounds<f32>, lane: i16) -> OrthogonalRoute {
451 let lead = anchor.outward_point(LEAD);
452 let reach = CORRIDOR + lane.unsigned_abs() as f32 * LANE_SPACING;
453 let normal = anchor.side.outward();
454 let perpendicular = point(-normal.y, normal.x);
455 let far = point(lead.x + normal.x * reach, lead.y + normal.y * reach);
456 let corner = |origin: Point<f32>, direction: f32| {
457 point(
458 origin.x + perpendicular.x * reach * direction,
459 origin.y + perpendicular.y * reach * direction,
460 )
461 };
462 let direction = if lane < 0 { -1.0 } else { 1.0 };
463 let route = OrthogonalRoute::new(vec![
464 anchor.point,
465 lead,
466 corner(lead, direction),
467 corner(far, direction),
468 far,
469 lead,
470 anchor.point,
471 ]);
472 debug_assert!(route.points().iter().all(|point| {
473 point.x.is_finite()
474 && point.y.is_finite()
475 && (point.x <= bounds.left()
476 || point.x >= bounds.right()
477 || point.y <= bounds.top()
478 || point.y >= bounds.bottom())
479 }));
480 route
481}
482
483fn segment_clear(from: Point<f32>, to: Point<f32>, bounds: Bounds<f32>) -> bool {
484 const EPSILON: f32 = 0.001;
485 if from.x == to.x {
486 let low = from.y.min(to.y);
487 let high = from.y.max(to.y);
488 !(from.x > bounds.left() + EPSILON
489 && from.x < bounds.right() - EPSILON
490 && high > bounds.top() + EPSILON
491 && low < bounds.bottom() - EPSILON)
492 } else if from.y == to.y {
493 let low = from.x.min(to.x);
494 let high = from.x.max(to.x);
495 !(from.y > bounds.top() + EPSILON
496 && from.y < bounds.bottom() - EPSILON
497 && high > bounds.left() + EPSILON
498 && low < bounds.right() - EPSILON)
499 } else {
500 false
501 }
502}
503
504fn path_cost(points: &[Point<f32>]) -> f32 {
505 let distance: f32 = points
506 .windows(2)
507 .map(|pair| (pair[1].x - pair[0].x).abs() + (pair[1].y - pair[0].y).abs())
508 .sum();
509 distance + points.len().saturating_sub(2) as f32 * 4.0
510}
511
512fn normalize(points: Vec<Point<f32>>) -> Vec<Point<f32>> {
513 let mut out: Vec<Point<f32>> = Vec::new();
514 for point in points
515 .into_iter()
516 .filter(|p| p.x.is_finite() && p.y.is_finite())
517 {
518 if out.last() == Some(&point) {
519 continue;
520 }
521 while out.len() >= 2 {
522 let a = out[out.len() - 2];
523 let b = out[out.len() - 1];
524 let same_axis = (a.x == b.x && b.x == point.x) || (a.y == b.y && b.y == point.y);
525 let same_direction =
526 (b.x - a.x) * (point.x - b.x) >= 0.0 && (b.y - a.y) * (point.y - b.y) >= 0.0;
527 if same_axis && same_direction {
528 out.pop();
529 } else {
530 break;
531 }
532 }
533 out.push(point);
534 }
535 out
536}
537
538#[derive(Debug, Clone, Copy)]
539pub(crate) struct RouteTransform {
540 origin: Point<Pixels>,
541 offset: Point<f32>,
542 zoom: f32,
543}
544
545impl RouteTransform {
546 pub(crate) fn new(origin: Point<Pixels>, offset: Point<f32>, zoom: f32) -> Self {
547 Self {
548 origin,
549 offset,
550 zoom,
551 }
552 }
553
554 fn point(self, world: Point<f32>) -> Point<Pixels> {
555 point(
556 self.origin.x + px(world.x * self.zoom + self.offset.x),
557 self.origin.y + px(world.y * self.zoom + self.offset.y),
558 )
559 }
560}
561
562pub(crate) fn paint_route(
563 window: &mut Window,
564 theme: &Theme,
565 edge: &GraphEdge,
566 route: &OrthogonalRoute,
567 transform: RouteTransform,
568 width: f32,
569 phase: Option<f32>,
570) {
571 let active_color = match edge.kind {
572 EdgeKind::Flow => theme.colors.accent,
573 EdgeKind::Feedback => theme.colors.danger,
574 };
575 if edge.active {
576 paint_route_stroke(
577 window,
578 route,
579 transform,
580 width * 5.0,
581 active_color.opacity(0.14),
582 edge.kind.dashes(),
583 );
584 }
585 paint_route_stroke(
586 window,
587 route,
588 transform,
589 width,
590 edge.kind.color(theme),
591 edge.kind.dashes(),
592 );
593 if edge.active {
594 paint_route_stroke(
595 window,
596 route,
597 transform,
598 width * 1.2,
599 active_color.opacity(0.72),
600 edge.kind.dashes(),
601 );
602 if let Some(phase) = phase {
603 paint_comets(
604 window,
605 route,
606 transform,
607 width.max(1.0),
608 phase,
609 active_color,
610 );
611 }
612 }
613}
614
615fn paint_comets(
618 window: &mut Window,
619 route: &OrthogonalRoute,
620 transform: RouteTransform,
621 width: f32,
622 phase: f32,
623 color: Hsla,
624) {
625 const COMETS: usize = 3;
626 const TAIL_STEPS: usize = 7;
627 const TAIL: f32 = 0.075;
628
629 for comet in 0..COMETS {
630 let head = (phase + comet as f32 / COMETS as f32).rem_euclid(1.0);
631 for step in 0..TAIL_STEPS {
632 let end = head - TAIL * step as f32 / TAIL_STEPS as f32;
633 let start = head - TAIL * (step + 1) as f32 / TAIL_STEPS as f32;
634 if start < 0.0 || end < 0.0 {
637 continue;
638 }
639 let mut builder = PathBuilder::stroke(px(width * (1.9 - step as f32 * 0.1)));
640 builder.move_to(transform.point(route.sample(start)));
641 builder.line_to(transform.point(route.sample(end)));
642 if let Ok(path) = builder.build() {
643 let opacity = 0.82 * (1.0 - step as f32 / TAIL_STEPS as f32).powf(1.4);
644 window.paint_path(path, color.opacity(opacity));
645 }
646 }
647 }
648}
649
650pub(crate) fn paint_route_stroke(
651 window: &mut Window,
652 route: &OrthogonalRoute,
653 transform: RouteTransform,
654 width: f32,
655 color: Hsla,
656 dashes: Option<[Pixels; 2]>,
657) {
658 let Some(first) = route.points.first() else {
659 return;
660 };
661 let mut builder = PathBuilder::stroke(px(width));
662 if let Some(dashes) = dashes {
663 builder = builder.dash_array(&dashes);
664 }
665 builder.move_to(transform.point(*first));
666 for point in &route.points[1..] {
667 builder.line_to(transform.point(*point));
668 }
669 if let Ok(path) = builder.build() {
670 window.paint_path(path, color);
671 }
672}
673
674#[cfg(test)]
675mod tests {
676 use super::*;
677 use gpui::size;
678
679 fn bounds(x: f32, y: f32) -> Bounds<f32> {
680 Bounds::new(point(x, y), size(40.0, 30.0))
681 }
682 fn anchor(side: PortSide, b: Bounds<f32>) -> Anchor {
683 let p = match side {
684 PortSide::Top => point(b.center().x, b.top()),
685 PortSide::Right => point(b.right(), b.center().y),
686 PortSide::Bottom => point(b.center().x, b.bottom()),
687 PortSide::Left => point(b.left(), b.center().y),
688 };
689 Anchor { point: p, side }
690 }
691 fn assert_valid(route: &OrthogonalRoute, from: Anchor, to: Anchor) {
692 assert_eq!(route.points()[0], from.point);
693 assert_eq!(*route.points().last().expect("route endpoint"), to.point);
694 for pair in route.points().windows(2) {
695 assert!(pair.iter().all(|p| p.x.is_finite() && p.y.is_finite()));
696 assert_ne!(pair[0], pair[1]);
697 assert!(pair[0].x == pair[1].x || pair[0].y == pair[1].y);
698 }
699 if route.points().len() > 1 {
700 let n = from.side.outward();
701 let first = route.points()[1];
702 assert!((first.x - from.point.x) * n.x + (first.y - from.point.y) * n.y > 0.0);
703 let n = to.side.outward();
704 let before = route.points()[route.points().len() - 2];
705 assert!((before.x - to.point.x) * n.x + (before.y - to.point.y) * n.y > 0.0);
706 }
707 }
708
709 #[test]
710 fn all_side_pairs_are_finite_orthogonal_and_directional() {
711 let sides = [
712 PortSide::Top,
713 PortSide::Right,
714 PortSide::Bottom,
715 PortSide::Left,
716 ];
717 let a = bounds(0.0, 0.0);
718 let b = bounds(100.0, 80.0);
719 for from_side in sides {
720 for to_side in sides {
721 let from = anchor(from_side, a);
722 let to = anchor(to_side, b);
723 assert_valid(
724 &route_orthogonal(from, to, a, b, EdgeKind::Flow, 0)
725 .expect("separated cards route"),
726 from,
727 to,
728 );
729 }
730 }
731 }
732 #[test]
733 fn overlapping_cards_are_omitted_and_self_links_route() {
734 let a = bounds(50.0, 20.0);
735 let overlapping = bounds(55.0, 25.0);
736 assert!(
737 route_orthogonal(
738 anchor(PortSide::Right, a),
739 anchor(PortSide::Left, overlapping),
740 a,
741 overlapping,
742 EdgeKind::Flow,
743 0,
744 )
745 .is_none()
746 );
747 let from = anchor(PortSide::Bottom, a);
748 let to = anchor(PortSide::Top, a);
749 let route = route_orthogonal(from, to, a, a, EdgeKind::Feedback, 0)
750 .expect("one card can route around itself");
751 assert_valid(&route, from, to);
752 }
753 #[test]
754 fn feedback_passes_below_the_deeper_box() {
755 let a = bounds(0.0, 0.0);
756 let b = Bounds::new(point(100.0, 10.0), size(40.0, 100.0));
757 let route = route_orthogonal(
758 anchor(PortSide::Bottom, a),
759 anchor(PortSide::Bottom, b),
760 a,
761 b,
762 EdgeKind::Feedback,
763 0,
764 )
765 .expect("feedback route");
766 assert!(route.points().iter().any(|p| p.y > b.bottom()));
767 }
768 #[test]
769 fn lanes_keep_anchors_but_distinguish_corridors() {
770 let a = bounds(0.0, 0.0);
771 let b = bounds(100.0, 50.0);
772 let from = anchor(PortSide::Right, a);
773 let to = anchor(PortSide::Left, b);
774 let x = route_orthogonal(from, to, a, b, EdgeKind::Flow, 0).expect("direct lane");
775 let y = route_orthogonal(from, to, a, b, EdgeKind::Flow, 2).expect("offset lane");
776 assert_eq!(
777 (x.points()[0], x.points().last()),
778 (y.points()[0], y.points().last())
779 );
780 assert_ne!(x.points(), y.points());
781 }
782 #[test]
783 fn opposite_lanes_do_not_share_terminal_segments() {
784 let upper = Bounds::new(point(0.0, 0.0), size(100.0, 60.0));
785 let lower = Bounds::new(point(20.0, 200.0), size(100.0, 60.0));
786 let flow_from = Anchor {
787 point: point(70.0, upper.bottom()),
788 side: PortSide::Bottom,
789 };
790 let flow_to = Anchor {
791 point: point(50.0, lower.top()),
792 side: PortSide::Top,
793 };
794 let retry_from = Anchor {
795 point: point(100.0, lower.top()),
796 side: PortSide::Top,
797 };
798 let retry_to = Anchor {
799 point: point(30.0, upper.bottom()),
800 side: PortSide::Bottom,
801 };
802 let flow = route_orthogonal(flow_from, flow_to, upper, lower, EdgeKind::Flow, -1)
803 .expect("forward lane");
804 let retry = route_orthogonal(retry_from, retry_to, lower, upper, EdgeKind::Feedback, 1)
805 .expect("return lane");
806
807 let overlaps = |a: &[Point<f32>], b: &[Point<f32>]| {
808 a.windows(2).any(|left| {
809 b.windows(2).any(|right| {
810 if left[0].y == left[1].y && right[0].y == right[1].y && left[0].y == right[0].y
811 {
812 left[0].x.max(left[1].x).min(right[0].x.max(right[1].x))
813 > left[0].x.min(left[1].x).max(right[0].x.min(right[1].x))
814 } else if left[0].x == left[1].x
815 && right[0].x == right[1].x
816 && left[0].x == right[0].x
817 {
818 left[0].y.max(left[1].y).min(right[0].y.max(right[1].y))
819 > left[0].y.min(left[1].y).max(right[0].y.min(right[1].y))
820 } else {
821 false
822 }
823 })
824 })
825 };
826 assert!(!overlaps(flow.points(), retry.points()));
827 }
828 #[test]
829 fn close_facing_cards_clamp_their_leads_without_crossing_either_card() {
830 let a = bounds(0.0, 0.0);
831 let b = bounds(50.0, 0.0);
832 let from = anchor(PortSide::Right, a);
833 let to = anchor(PortSide::Left, b);
834 let route = route_orthogonal(from, to, a, b, EdgeKind::Flow, 0)
835 .expect("the ten-unit corridor is routable");
836 assert_valid(&route, from, to);
837 for segment in route.points().windows(2) {
838 assert!(segment_clear(segment[0], segment[1], a));
839 assert!(segment_clear(segment[0], segment[1], b));
840 }
841 }
842 #[test]
843 fn sampling_uses_arc_length() {
844 let r = OrthogonalRoute::new(vec![point(0.0, 0.0), point(10.0, 0.0), point(10.0, 30.0)]);
845 assert_eq!(r.total_length(), 40.0);
846 assert_eq!(r.midpoint(), point(10.0, 10.0));
847 assert_eq!(r.sample(2.0), point(10.0, 30.0));
848 }
849 #[test]
850 fn zero_length_is_safe_and_finite() {
851 let r = OrthogonalRoute::new(vec![point(2.0, 3.0), point(2.0, 3.0)]);
852 assert_eq!(r.total_length(), 0.0);
853 assert_eq!(r.sample(f32::NAN), point(2.0, 3.0));
854 }
855 #[test]
856 fn identity_and_builders_are_stable() {
857 let a = GraphEdge::new("one", "two")
858 .ports("out", "in")
859 .label("work")
860 .active(true)
861 .lane(3)
862 .feedback();
863 let other = GraphEdge::new("x", "y");
864 assert_eq!(
865 a.identity(),
866 GraphEdge::new("one", "two")
867 .ports("out", "in")
868 .lane(3)
869 .feedback()
870 .identity()
871 );
872 assert_ne!(a.identity(), other.identity());
873 assert_eq!(a.from(), "one");
874 assert_eq!(a.to(), "two");
875 assert_eq!(a.kind(), EdgeKind::Feedback);
876 assert_eq!(a.source_port().expect("source port"), "out");
877 assert_eq!(a.target_port().expect("target port"), "in");
878 assert_eq!(a.edge_label().expect("edge label"), "work");
879 assert!(a.is_active());
880 assert_eq!(a.edge_lane(), 3);
881 assert_eq!(
882 a.clone().id("business").identity(),
883 SharedString::from("business")
884 );
885 }
886}