1use crate::render::backend::{DrawBackend, LineEnd, LineStyle, Linetype, RectStyle};
9use crate::render::RenderError;
10
11#[derive(Clone, Copy, Debug, PartialEq, Eq)]
13pub enum ArrowEnds {
14 Last,
16 First,
18 Both,
20}
21
22#[derive(Clone, Copy, Debug, PartialEq, Eq)]
24pub enum ArrowType {
25 Open,
27 Closed,
29}
30
31#[derive(Clone, Copy, Debug, PartialEq)]
35pub struct Arrow {
36 pub angle: f64,
39 pub length: f64,
41 pub ends: ArrowEnds,
43 pub kind: ArrowType,
46 pub position: f64,
51 pub length_per_linewidth: f64,
55}
56
57impl Default for Arrow {
58 fn default() -> Self {
59 Arrow {
60 angle: 30.0,
61 length: 18.0,
62 ends: ArrowEnds::Last,
63 kind: ArrowType::Open,
64 position: 1.0,
65 length_per_linewidth: 0.0,
66 }
67 }
68}
69
70impl Arrow {
71 pub fn angle(mut self, degrees: f64) -> Self {
73 self.angle = degrees;
74 self
75 }
76
77 pub fn length(mut self, px: f64) -> Self {
79 self.length = px;
80 self
81 }
82
83 pub fn ends(mut self, ends: ArrowEnds) -> Self {
85 self.ends = ends;
86 self
87 }
88
89 pub fn kind(mut self, kind: ArrowType) -> Self {
91 self.kind = kind;
92 self
93 }
94
95 pub fn closed(mut self) -> Self {
97 self.kind = ArrowType::Closed;
98 self
99 }
100
101 pub fn open(mut self) -> Self {
103 self.kind = ArrowType::Open;
104 self
105 }
106
107 pub fn position(mut self, fraction: f64) -> Self {
110 self.position = fraction;
111 self
112 }
113
114 pub fn length_per_linewidth(mut self, k: f64) -> Self {
117 self.length_per_linewidth = k;
118 self
119 }
120}
121
122pub fn arrow() -> Arrow {
126 Arrow::default()
127}
128
129#[derive(Clone, Copy, Debug, PartialEq)]
131pub(crate) struct ArrowHead {
132 pub tip: (f64, f64),
133 pub left: (f64, f64),
134 pub right: (f64, f64),
135}
136
137fn rotate((x, y): (f64, f64), radians: f64) -> (f64, f64) {
138 let (s, c) = radians.sin_cos();
139 (x * c - y * s, x * s + y * c)
140}
141
142pub(crate) fn heads(path_px: &[(f64, f64)], arrow: &Arrow, linewidth: f64) -> Vec<ArrowHead> {
147 if path_px.len() < 2
148 || path_px
149 .iter()
150 .any(|(x, y)| !x.is_finite() || !y.is_finite())
151 {
152 return Vec::new();
153 }
154 let mut cum = vec![0.0f64; path_px.len()];
155 for i in 1..path_px.len() {
156 let (x0, y0) = path_px[i - 1];
157 let (x1, y1) = path_px[i];
158 cum[i] = cum[i - 1] + ((x1 - x0).powi(2) + (y1 - y0).powi(2)).sqrt();
159 }
160 let total = cum[path_px.len() - 1];
161 if total <= 0.0 || !total.is_finite() {
162 return Vec::new();
163 }
164
165 let effective_length = arrow.length.max(arrow.length_per_linewidth * linewidth);
166
167 let point_at = |frac: f64| -> ((f64, f64), (f64, f64)) {
169 let target = frac.clamp(0.0, 1.0) * total;
170 let mut idx = 1;
171 while idx < cum.len() - 1 && cum[idx] < target {
172 idx += 1;
173 }
174 let seg_start = cum[idx - 1];
175 let seg_end = cum[idx];
176 let seg_len = seg_end - seg_start;
177 let t = if seg_len > 0.0 {
178 ((target - seg_start) / seg_len).clamp(0.0, 1.0)
179 } else {
180 0.0
181 };
182 let (x0, y0) = path_px[idx - 1];
183 let (x1, y1) = path_px[idx];
184 let px = x0 + (x1 - x0) * t;
185 let py = y0 + (y1 - y0) * t;
186 let dx = x1 - x0;
187 let dy = y1 - y0;
188 let len = (dx * dx + dy * dy).sqrt();
189 let unit = if len > 0.0 {
190 (dx / len, dy / len)
191 } else {
192 (0.0, 0.0)
193 };
194 ((px, py), unit)
195 };
196
197 let make_head = |frac: f64, forward: bool| -> Option<ArrowHead> {
198 let (tip, tangent) = point_at(frac);
199 let dir = if forward {
200 tangent
201 } else {
202 (-tangent.0, -tangent.1)
203 };
204 if dir.0 == 0.0 && dir.1 == 0.0 {
205 return None;
206 }
207 let back = (-dir.0, -dir.1);
208 let angle = arrow.angle.to_radians();
209 let l = rotate(back, angle);
210 let r = rotate(back, -angle);
211 Some(ArrowHead {
212 tip,
213 left: (
214 tip.0 + effective_length * l.0,
215 tip.1 + effective_length * l.1,
216 ),
217 right: (
218 tip.0 + effective_length * r.0,
219 tip.1 + effective_length * r.1,
220 ),
221 })
222 };
223
224 let mut out = Vec::new();
225 match arrow.ends {
226 ArrowEnds::Last => out.extend(make_head(arrow.position, true)),
227 ArrowEnds::First => out.extend(make_head(1.0 - arrow.position, false)),
228 ArrowEnds::Both => {
229 out.extend(make_head(arrow.position, true));
230 out.extend(make_head(1.0 - arrow.position, false));
231 }
232 }
233 out
234}
235
236pub(crate) fn draw_heads(
242 backend: &mut dyn DrawBackend,
243 heads: &[ArrowHead],
244 arrow: &Arrow,
245 color: (u8, u8, u8),
246 alpha: f64,
247 linewidth: f64,
248) -> Result<(), RenderError> {
249 for h in heads {
250 backend.set_mark_part(Some("arrow"));
251 let result = match arrow.kind {
252 ArrowType::Closed => backend.draw_polygon(
253 &[h.tip, h.left, h.right],
254 &RectStyle {
255 fill: Some(color),
256 stroke: Some(color),
257 stroke_width: linewidth,
258 alpha,
259 clip: true,
260 },
261 ),
262 ArrowType::Open => backend.draw_line(
263 &[h.left, h.tip, h.right],
264 &LineStyle {
265 color,
266 alpha,
267 width: linewidth,
268 linetype: Linetype::Solid,
269 lineend: LineEnd::Round,
270 },
271 ),
272 };
273 backend.set_mark_part(None);
274 result?;
275 }
276 Ok(())
277}
278
279#[cfg(test)]
280mod tests {
281 use super::*;
282
283 fn angle_between(a: (f64, f64), b: (f64, f64)) -> f64 {
284 let dot = a.0 * b.0 + a.1 * b.1;
285 let na = (a.0 * a.0 + a.1 * a.1).sqrt();
286 let nb = (b.0 * b.0 + b.1 * b.1).sqrt();
287 (dot / (na * nb)).clamp(-1.0, 1.0).acos()
288 }
289
290 #[test]
291 fn tip_lands_on_the_endpoint_at_position_1() {
292 let path = [(0.0, 0.0), (10.0, 0.0)];
293 let hs = heads(&path, &Arrow::default(), 1.0);
294 assert_eq!(hs.len(), 1);
295 assert!((hs[0].tip.0 - 10.0).abs() < 1e-9);
296 assert!((hs[0].tip.1 - 0.0).abs() < 1e-9);
297 }
298
299 #[test]
300 fn opening_angle_is_twice_the_half_angle() {
301 let path = [(0.0, 0.0), (10.0, 0.0)];
302 let a = Arrow::default().angle(30.0);
303 let hs = heads(&path, &a, 1.0);
304 assert_eq!(hs.len(), 1);
305 let h = hs[0];
306 let v_left = (h.left.0 - h.tip.0, h.left.1 - h.tip.1);
307 let v_right = (h.right.0 - h.tip.0, h.right.1 - h.tip.1);
308 let opening = angle_between(v_left, v_right).to_degrees();
309 assert!((opening - 60.0).abs() < 1e-6, "opening = {opening}");
310 }
311
312 #[test]
313 fn both_ends_give_two_heads() {
314 let path = [(0.0, 0.0), (10.0, 0.0)];
315 let a = Arrow::default().ends(ArrowEnds::Both);
316 let hs = heads(&path, &a, 1.0);
317 assert_eq!(hs.len(), 2);
318 }
319
320 #[test]
321 fn position_half_gives_the_midpoint() {
322 let path = [(0.0, 0.0), (10.0, 0.0)];
323 let a = Arrow::default().position(0.5);
324 let hs = heads(&path, &a, 1.0);
325 assert_eq!(hs.len(), 1);
326 assert!((hs[0].tip.0 - 5.0).abs() < 1e-9);
327 }
328
329 #[test]
330 fn length_per_linewidth_scales_the_wings() {
331 let path = [(0.0, 0.0), (10.0, 0.0)];
332 let a = Arrow::default().length(5.0).length_per_linewidth(2.2);
333 let hs = heads(&path, &a, 10.0); assert_eq!(hs.len(), 1);
335 let h = hs[0];
336 let d = ((h.left.0 - h.tip.0).powi(2) + (h.left.1 - h.tip.1).powi(2)).sqrt();
337 assert!((d - 22.0).abs() < 1e-6, "wing length = {d}");
338 }
339
340 #[test]
341 fn zero_length_path_gives_no_heads() {
342 let path = [(5.0, 5.0), (5.0, 5.0)];
343 assert!(heads(&path, &Arrow::default(), 1.0).is_empty());
344 }
345
346 #[test]
347 fn non_finite_path_gives_no_heads() {
348 let path = [(f64::NAN, 0.0), (1.0, 1.0)];
349 assert!(heads(&path, &Arrow::default(), 1.0).is_empty());
350 let path2 = [(0.0, 0.0), (f64::INFINITY, 1.0)];
351 assert!(heads(&path2, &Arrow::default(), 1.0).is_empty());
352 }
353
354 #[test]
355 fn first_end_places_tip_at_the_start() {
356 let path = [(0.0, 0.0), (10.0, 0.0)];
357 let a = Arrow::default().ends(ArrowEnds::First);
358 let hs = heads(&path, &a, 1.0);
359 assert_eq!(hs.len(), 1);
360 assert!((hs[0].tip.0 - 0.0).abs() < 1e-9);
361 }
362
363 #[test]
364 fn kind_and_open_builders_set_the_arrow_type() {
365 let a = Arrow::default().kind(ArrowType::Closed);
366 assert_eq!(a.kind, ArrowType::Closed);
367 let a = a.open();
369 assert_eq!(a.kind, ArrowType::Open);
370 }
371
372 #[test]
376 fn a_zero_length_segment_mid_path_does_not_produce_a_degenerate_head() {
377 let path = [(0.0, 0.0), (0.0, 0.0), (10.0, 0.0)];
378 let hs = heads(&path, &Arrow::default(), 1.0);
379 assert_eq!(hs.len(), 1);
380 assert!(hs[0].tip.0.is_finite() && hs[0].tip.1.is_finite());
381 assert!((hs[0].tip.0 - 10.0).abs() < 1e-9);
383 }
384
385 #[test]
389 fn position_on_a_degenerate_segment_gives_no_head() {
390 let path = [(0.0, 0.0), (0.0, 0.0), (10.0, 0.0)];
391 let a = Arrow::default().position(0.0);
392 assert!(heads(&path, &a, 1.0).is_empty());
393 }
394}