ferrum_flow/plugins/edge/
mod.rs1use 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}