1#[derive(Debug, Clone, Copy, PartialEq)]
3#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
4pub struct Point {
5 pub x: f64,
7 pub y: f64,
9}
10
11impl Point {
12 pub fn new(x: f64, y: f64) -> Self {
14 Self { x, y }
15 }
16}
17
18#[derive(Debug, Clone, Copy, PartialEq)]
28#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
29pub struct Ctm {
30 pub a: f64,
32 pub b: f64,
34 pub c: f64,
36 pub d: f64,
38 pub e: f64,
40 pub f: f64,
42}
43
44impl Default for Ctm {
45 fn default() -> Self {
46 Self::identity()
47 }
48}
49
50impl Ctm {
51 pub fn new(a: f64, b: f64, c: f64, d: f64, e: f64, f: f64) -> Self {
53 Self { a, b, c, d, e, f }
54 }
55
56 pub fn identity() -> Self {
58 Self {
59 a: 1.0,
60 b: 0.0,
61 c: 0.0,
62 d: 1.0,
63 e: 0.0,
64 f: 0.0,
65 }
66 }
67
68 pub fn transform_point(&self, p: Point) -> Point {
70 Point {
71 x: self.a * p.x + self.c * p.y + self.e,
72 y: self.b * p.x + self.d * p.y + self.f,
73 }
74 }
75
76 pub fn concat(&self, other: &Ctm) -> Ctm {
78 Ctm {
79 a: self.a * other.a + self.b * other.c,
80 b: self.a * other.b + self.b * other.d,
81 c: self.c * other.a + self.d * other.c,
82 d: self.c * other.b + self.d * other.d,
83 e: self.e * other.a + self.f * other.c + other.e,
84 f: self.e * other.b + self.f * other.d + other.f,
85 }
86 }
87}
88
89#[derive(Debug, Clone, Copy, PartialEq, Eq)]
91#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
92pub enum Orientation {
93 Horizontal,
95 Vertical,
97 Diagonal,
99}
100
101#[derive(Debug, Clone, Copy, PartialEq)]
109#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
110pub struct BBox {
111 pub x0: f64,
113 pub top: f64,
115 pub x1: f64,
117 pub bottom: f64,
119}
120
121impl BBox {
122 pub fn new(x0: f64, top: f64, x1: f64, bottom: f64) -> Self {
124 Self {
125 x0,
126 top,
127 x1,
128 bottom,
129 }
130 }
131
132 pub fn width(&self) -> f64 {
134 self.x1 - self.x0
135 }
136
137 pub fn height(&self) -> f64 {
139 self.bottom - self.top
140 }
141
142 pub fn union(&self, other: &BBox) -> BBox {
144 BBox {
145 x0: self.x0.min(other.x0),
146 top: self.top.min(other.top),
147 x1: self.x1.max(other.x1),
148 bottom: self.bottom.max(other.bottom),
149 }
150 }
151}
152
153#[cfg(test)]
154mod tests {
155 use super::*;
156
157 fn assert_point_approx(p: Point, x: f64, y: f64) {
158 assert!((p.x - x).abs() < 1e-10, "x: expected {x}, got {}", p.x);
159 assert!((p.y - y).abs() < 1e-10, "y: expected {y}, got {}", p.y);
160 }
161
162 #[test]
165 fn test_point_new() {
166 let p = Point::new(3.0, 4.0);
167 assert_eq!(p.x, 3.0);
168 assert_eq!(p.y, 4.0);
169 }
170
171 #[test]
174 fn test_ctm_identity() {
175 let ctm = Ctm::identity();
176 assert_eq!(ctm.a, 1.0);
177 assert_eq!(ctm.b, 0.0);
178 assert_eq!(ctm.c, 0.0);
179 assert_eq!(ctm.d, 1.0);
180 assert_eq!(ctm.e, 0.0);
181 assert_eq!(ctm.f, 0.0);
182 }
183
184 #[test]
185 fn test_ctm_default_is_identity() {
186 assert_eq!(Ctm::default(), Ctm::identity());
187 }
188
189 #[test]
190 fn test_ctm_transform_identity() {
191 let ctm = Ctm::identity();
192 let p = ctm.transform_point(Point::new(5.0, 10.0));
193 assert_point_approx(p, 5.0, 10.0);
194 }
195
196 #[test]
197 fn test_ctm_transform_translation() {
198 let ctm = Ctm::new(1.0, 0.0, 0.0, 1.0, 100.0, 200.0);
200 let p = ctm.transform_point(Point::new(5.0, 10.0));
201 assert_point_approx(p, 105.0, 210.0);
202 }
203
204 #[test]
205 fn test_ctm_transform_scaling() {
206 let ctm = Ctm::new(2.0, 0.0, 0.0, 3.0, 0.0, 0.0);
208 let p = ctm.transform_point(Point::new(5.0, 10.0));
209 assert_point_approx(p, 10.0, 30.0);
210 }
211
212 #[test]
213 fn test_ctm_transform_scale_and_translate() {
214 let ctm = Ctm::new(2.0, 0.0, 0.0, 2.0, 10.0, 20.0);
216 let p = ctm.transform_point(Point::new(5.0, 10.0));
217 assert_point_approx(p, 20.0, 40.0);
218 }
219
220 #[test]
221 fn test_ctm_concat_identity() {
222 let a = Ctm::new(2.0, 0.0, 0.0, 3.0, 10.0, 20.0);
223 let id = Ctm::identity();
224 assert_eq!(a.concat(&id), a);
225 }
226
227 #[test]
228 fn test_ctm_concat_two_translations() {
229 let a = Ctm::new(1.0, 0.0, 0.0, 1.0, 10.0, 20.0);
230 let b = Ctm::new(1.0, 0.0, 0.0, 1.0, 5.0, 7.0);
231 let c = a.concat(&b);
232 let p = c.transform_point(Point::new(0.0, 0.0));
233 assert_point_approx(p, 15.0, 27.0);
234 }
235
236 #[test]
237 fn test_ctm_concat_scale_then_translate() {
238 let scale = Ctm::new(2.0, 0.0, 0.0, 2.0, 0.0, 0.0);
240 let translate = Ctm::new(1.0, 0.0, 0.0, 1.0, 10.0, 20.0);
241 let combined = scale.concat(&translate);
242 let p = combined.transform_point(Point::new(3.0, 4.0));
243 assert_point_approx(p, 16.0, 28.0);
245 }
246
247 #[test]
250 fn test_bbox_new() {
251 let bbox = BBox::new(10.0, 20.0, 30.0, 40.0);
252 assert_eq!(bbox.x0, 10.0);
253 assert_eq!(bbox.top, 20.0);
254 assert_eq!(bbox.x1, 30.0);
255 assert_eq!(bbox.bottom, 40.0);
256 }
257
258 #[test]
259 fn test_bbox_dimensions() {
260 let bbox = BBox::new(10.0, 20.0, 50.0, 60.0);
261 assert_eq!(bbox.width(), 40.0);
262 assert_eq!(bbox.height(), 40.0);
263 }
264
265 #[test]
266 fn test_bbox_zero_size() {
267 let bbox = BBox::new(10.0, 20.0, 10.0, 20.0);
268 assert_eq!(bbox.width(), 0.0);
269 assert_eq!(bbox.height(), 0.0);
270 }
271
272 #[test]
275 fn test_orientation_variants() {
276 let h = Orientation::Horizontal;
277 let v = Orientation::Vertical;
278 let d = Orientation::Diagonal;
279 assert_ne!(h, v);
280 assert_ne!(v, d);
281 assert_ne!(h, d);
282 }
283
284 #[test]
285 fn test_orientation_clone_copy() {
286 let o = Orientation::Horizontal;
287 let o2 = o; let o3 = o.clone(); assert_eq!(o, o2);
290 assert_eq!(o, o3);
291 }
292
293 #[test]
294 fn test_bbox_union() {
295 let a = BBox::new(10.0, 20.0, 30.0, 40.0);
296 let b = BBox::new(5.0, 25.0, 35.0, 45.0);
297 let u = a.union(&b);
298 assert_eq!(u.x0, 5.0);
299 assert_eq!(u.top, 20.0);
300 assert_eq!(u.x1, 35.0);
301 assert_eq!(u.bottom, 45.0);
302 }
303}