Skip to main content

jellyflow_runtime/runtime/connection/
handles.rs

1use serde::{Deserialize, Serialize};
2
3use jellyflow_core::core::{CanvasPoint, CanvasRect, NodeId, PortDirection, PortId};
4
5use crate::runtime::geometry::{HandleBounds, HandlePosition, handle_center_position};
6
7/// Stable identity for a renderer handle participating in a connection gesture.
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
9pub struct ConnectionHandleRef {
10    pub node: NodeId,
11    pub port: PortId,
12    pub direction: PortDirection,
13}
14
15impl ConnectionHandleRef {
16    pub fn new(node: NodeId, port: PortId, direction: PortDirection) -> Self {
17        Self {
18            node,
19            port,
20            direction,
21        }
22    }
23}
24
25/// Renderer-normalized handle geometry in canvas coordinates.
26#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
27pub struct ConnectionHandleCandidate {
28    pub handle: ConnectionHandleRef,
29    pub node_rect: CanvasRect,
30    pub bounds: HandleBounds,
31}
32
33impl ConnectionHandleCandidate {
34    pub fn new(handle: ConnectionHandleRef, node_rect: CanvasRect, bounds: HandleBounds) -> Self {
35        Self {
36            handle,
37            node_rect,
38            bounds,
39        }
40    }
41}
42
43/// Input for resolving the closest connection handle near a pointer.
44#[derive(Debug, Clone, Copy, PartialEq)]
45pub struct ClosestConnectionHandleInput<'a> {
46    pub pointer: CanvasPoint,
47    pub radius: f32,
48    pub from: ConnectionHandleRef,
49    pub candidates: &'a [ConnectionHandleCandidate],
50}
51
52impl<'a> ClosestConnectionHandleInput<'a> {
53    pub fn new(
54        pointer: CanvasPoint,
55        radius: f32,
56        from: ConnectionHandleRef,
57        candidates: &'a [ConnectionHandleCandidate],
58    ) -> Self {
59        Self {
60            pointer,
61            radius,
62            from,
63            candidates,
64        }
65    }
66}
67
68/// Closest handle resolution result.
69#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
70pub struct ClosestConnectionHandle {
71    pub handle: ConnectionHandleRef,
72    pub center: CanvasPoint,
73    pub position: HandlePosition,
74    pub distance: f32,
75}
76
77/// XyFlow-compatible validity state for a connection target candidate.
78#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
79#[serde(rename_all = "snake_case")]
80pub enum ConnectionHandleValidity {
81    /// A candidate handle is present and can accept the connection.
82    Valid,
83    /// A candidate handle is present or inside the connection radius, but cannot accept it.
84    Invalid,
85    /// No handle is close enough to report valid or invalid feedback.
86    NoHandle,
87}
88
89/// Returns the nearest handle inside `connection_radius`, matching XyFlow tie semantics.
90///
91/// XyFlow skips the starting handle, measures distance to handle centers, keeps all equal-distance
92/// nearest handles, and prefers the opposite handle type when handles overlap at the same distance.
93pub fn closest_connection_handle(
94    input: ClosestConnectionHandleInput<'_>,
95) -> Option<ClosestConnectionHandle> {
96    if !input.pointer.is_finite() || !input.radius.is_finite() || input.radius < 0.0 {
97        return None;
98    }
99
100    let mut closest: Vec<ClosestConnectionHandle> = Vec::new();
101    let mut min_distance = f32::INFINITY;
102
103    for candidate in input.candidates {
104        if candidate.handle == input.from {
105            continue;
106        }
107
108        let Some(center) = handle_center_position(
109            candidate.node_rect,
110            Some(candidate.bounds),
111            candidate.bounds.position,
112        ) else {
113            continue;
114        };
115        let distance = (center.x - input.pointer.x).hypot(center.y - input.pointer.y);
116        if !distance.is_finite() || distance > input.radius {
117            continue;
118        }
119
120        let resolved = ClosestConnectionHandle {
121            handle: candidate.handle,
122            center,
123            position: candidate.bounds.position,
124            distance,
125        };
126        if distance < min_distance {
127            closest.clear();
128            closest.push(resolved);
129            min_distance = distance;
130        } else if distance == min_distance {
131            closest.push(resolved);
132        }
133    }
134
135    let preferred_direction = opposite_direction(input.from.direction);
136    closest
137        .iter()
138        .find(|candidate| candidate.handle.direction == preferred_direction)
139        .copied()
140        .or_else(|| closest.first().copied())
141}
142
143fn opposite_direction(direction: PortDirection) -> PortDirection {
144    match direction {
145        PortDirection::In => PortDirection::Out,
146        PortDirection::Out => PortDirection::In,
147    }
148}
149
150/// Resolves XyFlow's `true | false | null` connection feedback into a Rust enum.
151///
152/// A valid handle wins even if the adapter did not separately mark it inside the radius. Otherwise,
153/// an inside-radius candidate is invalid, and no candidate remains neutral.
154pub fn connection_handle_validity(
155    is_inside_connection_radius: bool,
156    is_handle_valid: bool,
157) -> ConnectionHandleValidity {
158    if is_handle_valid {
159        ConnectionHandleValidity::Valid
160    } else if is_inside_connection_radius {
161        ConnectionHandleValidity::Invalid
162    } else {
163        ConnectionHandleValidity::NoHandle
164    }
165}