use super::{read_domain, read_extend};
use crate::names;
use kurbo::Point;
use pdfrum_object::{Dict, Resolve};
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Axial {
pub start: Point,
pub end: Point,
pub t_min: f32,
pub t_max: f32,
pub extend_start: bool,
pub extend_end: bool,
}
impl Axial {
pub(super) fn load(dict: &Dict, r: &impl Resolve) -> Option<Self> {
let coords = dict.array(names::COORDS, r)?;
let at = |i: usize| f64::from(coords.number_at_or_zero(i));
let (t_min, t_max) = read_domain(dict, r);
let (extend_start, extend_end) = read_extend(dict, r);
Some(Self {
start: Point::new(at(0), at(1)),
end: Point::new(at(2), at(3)),
t_min,
t_max,
extend_start,
extend_end,
})
}
#[must_use]
pub fn axis_len_squared(&self) -> f64 {
let dx = self.end.x - self.start.x;
let dy = self.end.y - self.start.y;
dx * dx + dy * dy
}
#[must_use]
pub fn position(&self, p: Point) -> Option<f32> {
let len_sq = self.axis_len_squared();
if len_sq == 0.0 {
return None;
}
let dx = self.end.x - self.start.x;
let dy = self.end.y - self.start.y;
let scale = ((p.x - self.start.x) * dx + (p.y - self.start.y) * dy) / len_sq;
#[expect(
clippy::cast_possible_truncation,
reason = "the shading LUT indexes with f32 throughout, matching the C++"
)]
Some(scale as f32)
}
}
#[cfg(test)]
mod tests {
#![allow(
clippy::unreadable_literal,
clippy::float_cmp,
clippy::indexing_slicing,
clippy::cast_precision_loss,
clippy::cast_possible_truncation,
reason = "test fixtures quote oracle vectors verbatim and compare exactly"
)]
use super::Axial;
use kurbo::Point;
fn horizontal() -> Axial {
Axial {
start: Point::new(0.0, 0.0),
end: Point::new(10.0, 0.0),
t_min: 0.0,
t_max: 1.0,
extend_start: false,
extend_end: false,
}
}
#[test]
fn the_position_is_the_normalized_projection() {
let a = horizontal();
assert!(
a.position(Point::new(0.0, 0.0))
.is_some_and(|v| v.abs() < 1e-6)
);
assert!(
a.position(Point::new(5.0, 99.0))
.is_some_and(|v| (v - 0.5).abs() < 1e-6),
"the off-axis component does not matter"
);
assert!(
a.position(Point::new(10.0, 0.0))
.is_some_and(|v| (v - 1.0).abs() < 1e-6)
);
assert!(a.position(Point::new(20.0, 0.0)).is_some_and(|v| v > 1.9));
assert!(a.position(Point::new(-10.0, 0.0)).is_some_and(|v| v < -0.9));
}
#[test]
fn a_degenerate_axis_has_no_position() {
let a = Axial {
start: Point::new(3.0, 4.0),
end: Point::new(3.0, 4.0),
..horizontal()
};
assert_eq!(a.axis_len_squared(), 0.0);
assert!(a.position(Point::new(0.0, 0.0)).is_none());
}
}