Skip to main content

gpui_kit/canvas/
edge.rs

1//! Static edge data and orthogonal geometry for a node graph.
2
3use gpui::{Bounds, Hsla, PathBuilder, Pixels, Point, SharedString, Window, point, px};
4use gpui_kit_theme::Theme;
5
6/// The routing treatment of an edge.
7#[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/// The side of a node on which a port is placed.
28#[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/// A caller-owned node and port identity used by connection proposals.
62#[derive(Debug, Clone, PartialEq, Eq)]
63pub struct GraphEndpoint {
64    pub node: SharedString,
65    pub port: SharedString,
66}
67
68impl GraphEndpoint {
69    /// Creates an endpoint from business identities, not display labels.
70    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/// A controlled connection between two graph nodes.
79#[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        // Length prefixes make the compatibility identity unambiguous even if ids contain separators.
162        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    // Separate lanes at the ports as well as in their middle corridor. Without
284    // this, opposite routes between two same-side port groups can share their
285    // first or last horizontal segment even though their trunks are distinct.
286    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    // A feedback path is a return lane, so its first choice remains the
308    // corridor below both endpoint cards. Explicit side choices that make
309    // that route cross a card fall through to the general router.
310    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    // A non-zero lane deliberately takes a parallel corridor. The first
319    // candidate stays near the direct route; if that would cross an endpoint,
320    // the sign of the lane selects the corresponding outside corridor.
321    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
428/// Routes a connection gesture from a real port to the pointer without
429/// inventing a target node. The preview leaves the source in its declared
430/// direction and then takes one square corner to the pointer.
431pub(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
615/// Three phase-shifted traffic trails. Each trail is made from short straight
616/// samples, so it follows square corners without reintroducing a curve mode.
617fn 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            // A wrapped tail resumes at the start of the route on the next
635            // frame instead of drawing one false segment across the graph.
636            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}