use crate::render::backend::{DrawBackend, LineEnd, LineStyle, Linetype, RectStyle};
use crate::render::RenderError;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ArrowEnds {
Last,
First,
Both,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ArrowType {
Open,
Closed,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Arrow {
pub angle: f64,
pub length: f64,
pub ends: ArrowEnds,
pub kind: ArrowType,
pub position: f64,
pub length_per_linewidth: f64,
}
impl Default for Arrow {
fn default() -> Self {
Arrow {
angle: 30.0,
length: 18.0,
ends: ArrowEnds::Last,
kind: ArrowType::Open,
position: 1.0,
length_per_linewidth: 0.0,
}
}
}
impl Arrow {
pub fn angle(mut self, degrees: f64) -> Self {
self.angle = degrees;
self
}
pub fn length(mut self, px: f64) -> Self {
self.length = px;
self
}
pub fn ends(mut self, ends: ArrowEnds) -> Self {
self.ends = ends;
self
}
pub fn kind(mut self, kind: ArrowType) -> Self {
self.kind = kind;
self
}
pub fn closed(mut self) -> Self {
self.kind = ArrowType::Closed;
self
}
pub fn open(mut self) -> Self {
self.kind = ArrowType::Open;
self
}
pub fn position(mut self, fraction: f64) -> Self {
self.position = fraction;
self
}
pub fn length_per_linewidth(mut self, k: f64) -> Self {
self.length_per_linewidth = k;
self
}
}
pub fn arrow() -> Arrow {
Arrow::default()
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub(crate) struct ArrowHead {
pub tip: (f64, f64),
pub left: (f64, f64),
pub right: (f64, f64),
}
fn rotate((x, y): (f64, f64), radians: f64) -> (f64, f64) {
let (s, c) = radians.sin_cos();
(x * c - y * s, x * s + y * c)
}
pub(crate) fn heads(path_px: &[(f64, f64)], arrow: &Arrow, linewidth: f64) -> Vec<ArrowHead> {
if path_px.len() < 2
|| path_px
.iter()
.any(|(x, y)| !x.is_finite() || !y.is_finite())
{
return Vec::new();
}
let mut cum = vec![0.0f64; path_px.len()];
for i in 1..path_px.len() {
let (x0, y0) = path_px[i - 1];
let (x1, y1) = path_px[i];
cum[i] = cum[i - 1] + ((x1 - x0).powi(2) + (y1 - y0).powi(2)).sqrt();
}
let total = cum[path_px.len() - 1];
if total <= 0.0 || !total.is_finite() {
return Vec::new();
}
let effective_length = arrow.length.max(arrow.length_per_linewidth * linewidth);
let point_at = |frac: f64| -> ((f64, f64), (f64, f64)) {
let target = frac.clamp(0.0, 1.0) * total;
let mut idx = 1;
while idx < cum.len() - 1 && cum[idx] < target {
idx += 1;
}
let seg_start = cum[idx - 1];
let seg_end = cum[idx];
let seg_len = seg_end - seg_start;
let t = if seg_len > 0.0 {
((target - seg_start) / seg_len).clamp(0.0, 1.0)
} else {
0.0
};
let (x0, y0) = path_px[idx - 1];
let (x1, y1) = path_px[idx];
let px = x0 + (x1 - x0) * t;
let py = y0 + (y1 - y0) * t;
let dx = x1 - x0;
let dy = y1 - y0;
let len = (dx * dx + dy * dy).sqrt();
let unit = if len > 0.0 {
(dx / len, dy / len)
} else {
(0.0, 0.0)
};
((px, py), unit)
};
let make_head = |frac: f64, forward: bool| -> Option<ArrowHead> {
let (tip, tangent) = point_at(frac);
let dir = if forward {
tangent
} else {
(-tangent.0, -tangent.1)
};
if dir.0 == 0.0 && dir.1 == 0.0 {
return None;
}
let back = (-dir.0, -dir.1);
let angle = arrow.angle.to_radians();
let l = rotate(back, angle);
let r = rotate(back, -angle);
Some(ArrowHead {
tip,
left: (
tip.0 + effective_length * l.0,
tip.1 + effective_length * l.1,
),
right: (
tip.0 + effective_length * r.0,
tip.1 + effective_length * r.1,
),
})
};
let mut out = Vec::new();
match arrow.ends {
ArrowEnds::Last => out.extend(make_head(arrow.position, true)),
ArrowEnds::First => out.extend(make_head(1.0 - arrow.position, false)),
ArrowEnds::Both => {
out.extend(make_head(arrow.position, true));
out.extend(make_head(1.0 - arrow.position, false));
}
}
out
}
pub(crate) fn draw_heads(
backend: &mut dyn DrawBackend,
heads: &[ArrowHead],
arrow: &Arrow,
color: (u8, u8, u8),
alpha: f64,
linewidth: f64,
) -> Result<(), RenderError> {
for h in heads {
backend.set_mark_part(Some("arrow"));
let result = match arrow.kind {
ArrowType::Closed => backend.draw_polygon(
&[h.tip, h.left, h.right],
&RectStyle {
fill: Some(color),
stroke: Some(color),
stroke_width: linewidth,
alpha,
clip: true,
corner_radius: 0.0,
},
),
ArrowType::Open => backend.draw_line(
&[h.left, h.tip, h.right],
&LineStyle {
color,
alpha,
width: linewidth,
linetype: Linetype::Solid,
lineend: LineEnd::Round,
},
),
};
backend.set_mark_part(None);
result?;
}
Ok(())
}
pub fn rect_edge_point(
center: (f64, f64),
half_w: f64,
half_h: f64,
toward: (f64, f64),
) -> (f64, f64) {
let (dx, dy) = (toward.0 - center.0, toward.1 - center.1);
let sx = if dx != 0.0 {
half_w / dx.abs()
} else {
f64::INFINITY
};
let sy = if dy != 0.0 {
half_h / dy.abs()
} else {
f64::INFINITY
};
let k = sx.min(sy);
if k.is_finite() {
(center.0 + dx * k, center.1 + dy * k)
} else {
center
}
}
#[cfg(test)]
mod tests {
use super::*;
fn angle_between(a: (f64, f64), b: (f64, f64)) -> f64 {
let dot = a.0 * b.0 + a.1 * b.1;
let na = (a.0 * a.0 + a.1 * a.1).sqrt();
let nb = (b.0 * b.0 + b.1 * b.1).sqrt();
(dot / (na * nb)).clamp(-1.0, 1.0).acos()
}
#[test]
fn tip_lands_on_the_endpoint_at_position_1() {
let path = [(0.0, 0.0), (10.0, 0.0)];
let hs = heads(&path, &Arrow::default(), 1.0);
assert_eq!(hs.len(), 1);
assert!((hs[0].tip.0 - 10.0).abs() < 1e-9);
assert!((hs[0].tip.1 - 0.0).abs() < 1e-9);
}
#[test]
fn opening_angle_is_twice_the_half_angle() {
let path = [(0.0, 0.0), (10.0, 0.0)];
let a = Arrow::default().angle(30.0);
let hs = heads(&path, &a, 1.0);
assert_eq!(hs.len(), 1);
let h = hs[0];
let v_left = (h.left.0 - h.tip.0, h.left.1 - h.tip.1);
let v_right = (h.right.0 - h.tip.0, h.right.1 - h.tip.1);
let opening = angle_between(v_left, v_right).to_degrees();
assert!((opening - 60.0).abs() < 1e-6, "opening = {opening}");
}
#[test]
fn both_ends_give_two_heads() {
let path = [(0.0, 0.0), (10.0, 0.0)];
let a = Arrow::default().ends(ArrowEnds::Both);
let hs = heads(&path, &a, 1.0);
assert_eq!(hs.len(), 2);
}
#[test]
fn position_half_gives_the_midpoint() {
let path = [(0.0, 0.0), (10.0, 0.0)];
let a = Arrow::default().position(0.5);
let hs = heads(&path, &a, 1.0);
assert_eq!(hs.len(), 1);
assert!((hs[0].tip.0 - 5.0).abs() < 1e-9);
}
#[test]
fn length_per_linewidth_scales_the_wings() {
let path = [(0.0, 0.0), (10.0, 0.0)];
let a = Arrow::default().length(5.0).length_per_linewidth(2.2);
let hs = heads(&path, &a, 10.0); assert_eq!(hs.len(), 1);
let h = hs[0];
let d = ((h.left.0 - h.tip.0).powi(2) + (h.left.1 - h.tip.1).powi(2)).sqrt();
assert!((d - 22.0).abs() < 1e-6, "wing length = {d}");
}
#[test]
fn zero_length_path_gives_no_heads() {
let path = [(5.0, 5.0), (5.0, 5.0)];
assert!(heads(&path, &Arrow::default(), 1.0).is_empty());
}
#[test]
fn non_finite_path_gives_no_heads() {
let path = [(f64::NAN, 0.0), (1.0, 1.0)];
assert!(heads(&path, &Arrow::default(), 1.0).is_empty());
let path2 = [(0.0, 0.0), (f64::INFINITY, 1.0)];
assert!(heads(&path2, &Arrow::default(), 1.0).is_empty());
}
#[test]
fn first_end_places_tip_at_the_start() {
let path = [(0.0, 0.0), (10.0, 0.0)];
let a = Arrow::default().ends(ArrowEnds::First);
let hs = heads(&path, &a, 1.0);
assert_eq!(hs.len(), 1);
assert!((hs[0].tip.0 - 0.0).abs() < 1e-9);
}
#[test]
fn kind_and_open_builders_set_the_arrow_type() {
let a = Arrow::default().kind(ArrowType::Closed);
assert_eq!(a.kind, ArrowType::Closed);
let a = a.open();
assert_eq!(a.kind, ArrowType::Open);
}
#[test]
fn a_zero_length_segment_mid_path_does_not_produce_a_degenerate_head() {
let path = [(0.0, 0.0), (0.0, 0.0), (10.0, 0.0)];
let hs = heads(&path, &Arrow::default(), 1.0);
assert_eq!(hs.len(), 1);
assert!(hs[0].tip.0.is_finite() && hs[0].tip.1.is_finite());
assert!((hs[0].tip.0 - 10.0).abs() < 1e-9);
}
#[test]
fn position_on_a_degenerate_segment_gives_no_head() {
let path = [(0.0, 0.0), (0.0, 0.0), (10.0, 0.0)];
let a = Arrow::default().position(0.0);
assert!(heads(&path, &a, 1.0).is_empty());
}
#[test]
fn rect_edge_point_exits_through_the_right_edge() {
let p = rect_edge_point((0.0, 0.0), 40.0, 20.0, (200.0, 0.0));
assert!((p.0 - 40.0).abs() < 1e-9);
assert!((p.1 - 0.0).abs() < 1e-9);
}
#[test]
fn rect_edge_point_exits_through_the_top_edge() {
let p = rect_edge_point((0.0, 0.0), 40.0, 20.0, (0.0, -200.0));
assert!((p.0 - 0.0).abs() < 1e-9);
assert!((p.1 - -20.0).abs() < 1e-9);
}
#[test]
fn rect_edge_point_picks_the_axis_that_clips_first_on_a_diagonal() {
let p = rect_edge_point((0.0, 0.0), 10.0, 10.0, (100.0, 20.0));
assert!(
(p.0 - 10.0).abs() < 1e-9,
"expected the right edge, got {p:?}"
);
assert!(p.1.abs() <= 10.0 + 1e-9);
}
#[test]
fn rect_edge_point_toward_the_centre_itself_returns_the_centre() {
let p = rect_edge_point((5.0, 5.0), 10.0, 10.0, (5.0, 5.0));
assert_eq!(p, (5.0, 5.0));
}
#[test]
fn rect_edge_point_is_symmetric_for_two_equal_boxes_facing_each_other() {
let (a, b) = ((0.0, 0.0), (160.0, 0.0));
let start = rect_edge_point(a, 40.0, 20.0, b);
let end = rect_edge_point(b, 40.0, 20.0, a);
assert!((start.0 - 40.0).abs() < 1e-9);
assert!((end.0 - 120.0).abs() < 1e-9);
assert!((end.0 - start.0 - 80.0).abs() < 1e-9); }
}