Skip to main content

ferrum_flow/plugins/edge/
mod.rs

1use gpui::{Bounds, Element, PathBuilder, Pixels, Point, canvas, px, rgb};
2
3use crate::{
4    Edge, EdgeId, Node, Port, PortId, PortKind, RenderContext,
5    plugin::{FlowEvent, Plugin, PluginContext},
6    plugins::edge::command::ClearEdgeCommand,
7};
8
9mod command;
10
11use command::SelectEdgeCommand;
12
13pub struct EdgePlugin;
14
15impl EdgePlugin {
16    pub fn new() -> Self {
17        Self {}
18    }
19}
20
21impl Plugin for EdgePlugin {
22    fn name(&self) -> &'static str {
23        "edge"
24    }
25    fn setup(&mut self, _ctx: &mut crate::plugin::InitPluginContext) {}
26    fn on_event(
27        &mut self,
28        event: &FlowEvent,
29        ctx: &mut crate::plugin::PluginContext,
30    ) -> crate::plugin::EventResult {
31        if let FlowEvent::Input(crate::plugin::InputEvent::MouseDown(ev)) = event {
32            let shift = ev.modifiers.shift;
33            if let Some(id) = hit_test_get_edge(ev.position, &ctx) {
34                ctx.execute_command(SelectEdgeCommand::new(id, shift, &ctx));
35                return crate::plugin::EventResult::Stop;
36            } else {
37                if !shift {
38                    ctx.execute_command(ClearEdgeCommand::new(&ctx));
39                }
40            }
41        }
42        crate::plugin::EventResult::Continue
43    }
44    fn priority(&self) -> i32 {
45        120
46    }
47    fn render_layer(&self) -> crate::plugin::RenderLayer {
48        crate::plugin::RenderLayer::Edges
49    }
50    fn render(&mut self, ctx: &mut crate::RenderContext) -> Option<gpui::AnyElement> {
51        let edges: Vec<_> = ctx
52            .graph
53            .edges
54            .iter()
55            .map(|(k, v)| (*k, edge_geometry2(v, &ctx)))
56            .collect();
57        let selected_edges = ctx.graph.selected_edge.clone();
58
59        Some(
60            canvas(
61                |_, _, _| (edges, selected_edges),
62                move |_, (edges, selected_edges), win, _| {
63                    for (id, geometry) in edges.iter() {
64                        let Some(EdgeGeometry { start, c1, c2, end }) = geometry else {
65                            return;
66                        };
67                        let mut line = PathBuilder::stroke(px(1.0));
68                        line.move_to(*start);
69                        line.cubic_bezier_to(*end, *c1, *c2);
70
71                        let selected = selected_edges.iter().find(|i| **i == *id).is_some();
72
73                        if let Ok(line) = line.build() {
74                            win.paint_path(line, rgb(if selected { 0xFF7800 } else { 0xb1b1b8 }));
75                        }
76                    }
77                },
78            )
79            .into_any(),
80        )
81    }
82}
83
84pub struct EdgeGeometry {
85    pub start: Point<Pixels>,
86    pub c1: Point<Pixels>,
87    pub c2: Point<Pixels>,
88    pub end: Point<Pixels>,
89}
90
91fn edge_geometry(edge: &Edge, ctx: &PluginContext) -> Option<EdgeGeometry> {
92    let Edge {
93        source_port,
94        target_port,
95        ..
96    } = edge;
97
98    let start = port_screen_position(*source_port, &ctx)?;
99    let end = port_screen_position(*target_port, &ctx)?;
100
101    Some(EdgeGeometry {
102        start,
103        c1: start + Point::new(px(50.0), px(0.0)),
104        c2: end - Point::new(px(50.0), px(0.0)),
105        end,
106    })
107}
108
109fn edge_geometry2(edge: &Edge, ctx: &RenderContext) -> Option<EdgeGeometry> {
110    let Edge {
111        source_port,
112        target_port,
113        ..
114    } = edge;
115
116    let start = port_screen_position2(*source_port, &ctx)?;
117    let end = port_screen_position2(*target_port, &ctx)?;
118
119    Some(EdgeGeometry {
120        start,
121        c1: start + Point::new(px(50.0), px(0.0)),
122        c2: end - Point::new(px(50.0), px(0.0)),
123        end,
124    })
125}
126
127fn port_screen_position(port_id: PortId, ctx: &PluginContext) -> Option<Point<Pixels>> {
128    let port = &ctx.graph.ports[&port_id];
129    let node = &ctx.graph.nodes().get(&port.node_id)?;
130
131    let node_pos = node.point();
132
133    let offset = port_offset(node, port);
134
135    Some(ctx.viewport.world_to_screen(node_pos + offset))
136}
137fn port_screen_position2(port_id: PortId, ctx: &RenderContext) -> Option<Point<Pixels>> {
138    let port = &ctx.graph.ports[&port_id];
139    let node = &ctx.graph.nodes().get(&port.node_id)?;
140
141    let node_pos = node.point();
142
143    let offset = port_offset(node, port);
144
145    Some(ctx.viewport.world_to_screen(node_pos + offset))
146}
147
148pub fn port_offset(node: &Node, port: &Port) -> Point<Pixels> {
149    let node_size = node.size;
150
151    match port.kind {
152        PortKind::Input => Point::new(px(0.0), node_size.height / 2.0),
153
154        PortKind::Output => Point::new(node_size.width, node_size.height / 2.0),
155    }
156}
157
158fn hit_test_get_edge(mouse: Point<Pixels>, ctx: &PluginContext) -> Option<EdgeId> {
159    for edge in ctx.graph.edges.values() {
160        let Some(geom) = edge_geometry(edge, ctx) else {
161            continue;
162        };
163
164        let bound = edge_bounds(&geom);
165        if !bound.contains(&mouse) {
166            continue;
167        }
168
169        if hit_test_edge(mouse, edge, ctx) {
170            return Some(edge.id);
171        }
172    }
173
174    None
175}
176
177pub fn edge_bounds(geom: &EdgeGeometry) -> Bounds<Pixels> {
178    let min_x = geom.start.x.min(geom.end.x).min(geom.c1.x).min(geom.c2.x);
179    let max_x = geom.start.x.max(geom.end.x).max(geom.c1.x).max(geom.c2.x);
180
181    let min_y = geom.start.y.min(geom.end.y).min(geom.c1.y).min(geom.c2.y);
182    let max_y = geom.start.y.max(geom.end.y).max(geom.c1.y).max(geom.c2.y);
183
184    Bounds::from_corners(
185        Point::new(min_x - px(10.0), min_y - px(10.0)),
186        Point::new(max_x + px(10.0), max_y + px(10.0)),
187    )
188}
189
190fn hit_test_edge(mouse: Point<Pixels>, edge: &Edge, ctx: &PluginContext) -> bool {
191    let Some(geom) = edge_geometry(edge, ctx) else {
192        return false;
193    };
194
195    let points = sample_bezier(&geom, 20);
196
197    for segment in points.windows(2) {
198        let d = distance_to_segment(mouse, segment[0], segment[1]);
199
200        if d < 8.0 {
201            return true;
202        }
203    }
204
205    false
206}
207
208fn sample_bezier(geom: &EdgeGeometry, steps: usize) -> Vec<Point<Pixels>> {
209    let mut points = Vec::new();
210
211    for i in 0..=steps {
212        let t = i as f32 / steps as f32;
213
214        let x = (1.0 - t).powi(3) * geom.start.x
215            + 3.0 * (1.0 - t).powi(2) * t * geom.c1.x
216            + 3.0 * (1.0 - t) * t * t * geom.c2.x
217            + t.powi(3) * geom.end.x;
218
219        let y = (1.0 - t).powi(3) * geom.start.y
220            + 3.0 * (1.0 - t).powi(2) * t * geom.c1.y
221            + 3.0 * (1.0 - t) * t * t * geom.c2.y
222            + t.powi(3) * geom.end.y;
223
224        points.push(Point::new(x, y));
225    }
226
227    points
228}
229pub fn distance_to_segment(p: Point<Pixels>, a: Point<Pixels>, b: Point<Pixels>) -> f32 {
230    let ap = vec_sub(p, a);
231    let ab = vec_sub(b, a);
232
233    let ab_len2 = ab.0 * ab.0 + ab.1 * ab.1;
234
235    if ab_len2 == 0.0 {
236        return vec_length(ap);
237    }
238
239    let t = (vec_dot(ap, ab) / ab_len2).clamp(0.0, 1.0);
240
241    let closest = Point::new(f32::from(a.x) + ab.0 * t, f32::from(a.y) + ab.1 * t);
242
243    let dx = f32::from(p.x) - closest.x;
244    let dy = f32::from(p.y) - closest.y;
245
246    (dx * dx + dy * dy).sqrt()
247}
248
249fn vec_sub(a: Point<Pixels>, b: Point<Pixels>) -> (f32, f32) {
250    (f32::from(a.x - b.x), f32::from(a.y - b.y))
251}
252
253fn vec_dot(a: (f32, f32), b: (f32, f32)) -> f32 {
254    a.0 * b.0 + a.1 * b.1
255}
256
257fn vec_length(v: (f32, f32)) -> f32 {
258    (v.0 * v.0 + v.1 * v.1).sqrt()
259}