pdfrum_page/shading/
axial.rs1use super::{read_domain, read_extend};
12use crate::names;
13use kurbo::Point;
14use pdfrum_object::{Dict, Resolve};
15
16#[derive(Debug, Clone, Copy, PartialEq)]
18pub struct Axial {
19 pub start: Point,
21 pub end: Point,
23 pub t_min: f32,
25 pub t_max: f32,
27 pub extend_start: bool,
29 pub extend_end: bool,
31}
32
33impl Axial {
34 pub(super) fn load(dict: &Dict, r: &impl Resolve) -> Option<Self> {
39 let coords = dict.array(names::COORDS, r)?;
40 let at = |i: usize| f64::from(coords.number_at_or_zero(i));
41 let (t_min, t_max) = read_domain(dict, r);
42 let (extend_start, extend_end) = read_extend(dict, r);
43 Some(Self {
44 start: Point::new(at(0), at(1)),
45 end: Point::new(at(2), at(3)),
46 t_min,
47 t_max,
48 extend_start,
49 extend_end,
50 })
51 }
52
53 #[must_use]
57 pub fn axis_len_squared(&self) -> f64 {
58 let dx = self.end.x - self.start.x;
59 let dy = self.end.y - self.start.y;
60 dx * dx + dy * dy
61 }
62
63 #[must_use]
69 pub fn position(&self, p: Point) -> Option<f32> {
70 let len_sq = self.axis_len_squared();
71 if len_sq == 0.0 {
72 return None;
73 }
74 let dx = self.end.x - self.start.x;
75 let dy = self.end.y - self.start.y;
76 let scale = ((p.x - self.start.x) * dx + (p.y - self.start.y) * dy) / len_sq;
77 #[expect(
78 clippy::cast_possible_truncation,
79 reason = "the shading LUT indexes with f32 throughout, matching the C++"
80 )]
81 Some(scale as f32)
82 }
83}
84
85#[cfg(test)]
86mod tests {
87 #![allow(
91 clippy::unreadable_literal,
92 clippy::float_cmp,
93 clippy::indexing_slicing,
94 clippy::cast_precision_loss,
95 clippy::cast_possible_truncation,
96 reason = "test fixtures quote oracle vectors verbatim and compare exactly"
97 )]
98
99 use super::Axial;
100 use kurbo::Point;
101
102 fn horizontal() -> Axial {
103 Axial {
104 start: Point::new(0.0, 0.0),
105 end: Point::new(10.0, 0.0),
106 t_min: 0.0,
107 t_max: 1.0,
108 extend_start: false,
109 extend_end: false,
110 }
111 }
112
113 #[test]
114 fn the_position_is_the_normalized_projection() {
115 let a = horizontal();
116 assert!(
117 a.position(Point::new(0.0, 0.0))
118 .is_some_and(|v| v.abs() < 1e-6)
119 );
120 assert!(
121 a.position(Point::new(5.0, 99.0))
122 .is_some_and(|v| (v - 0.5).abs() < 1e-6),
123 "the off-axis component does not matter"
124 );
125 assert!(
126 a.position(Point::new(10.0, 0.0))
127 .is_some_and(|v| (v - 1.0).abs() < 1e-6)
128 );
129 assert!(a.position(Point::new(20.0, 0.0)).is_some_and(|v| v > 1.9));
131 assert!(a.position(Point::new(-10.0, 0.0)).is_some_and(|v| v < -0.9));
132 }
133
134 #[test]
135 fn a_degenerate_axis_has_no_position() {
136 let a = Axial {
137 start: Point::new(3.0, 4.0),
138 end: Point::new(3.0, 4.0),
139 ..horizontal()
140 };
141 assert_eq!(a.axis_len_squared(), 0.0);
142 assert!(a.position(Point::new(0.0, 0.0)).is_none());
143 }
144}