1use serde::{Deserialize, Serialize};
4
5const BBOX_EPSILON: f64 = 0.0001;
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct BoundingBox {
22 pub page_number: Option<u32>,
24 pub last_page_number: Option<u32>,
26 pub left_x: f64,
28 pub bottom_y: f64,
30 pub right_x: f64,
32 pub top_y: f64,
34}
35
36impl PartialEq for BoundingBox {
37 fn eq(&self, other: &Self) -> bool {
38 self.page_number == other.page_number
39 && self.last_page_number == other.last_page_number
40 && (self.left_x - other.left_x).abs() < BBOX_EPSILON
41 && (self.bottom_y - other.bottom_y).abs() < BBOX_EPSILON
42 && (self.right_x - other.right_x).abs() < BBOX_EPSILON
43 && (self.top_y - other.top_y).abs() < BBOX_EPSILON
44 }
45}
46
47impl BoundingBox {
48 pub fn new(page: Option<u32>, left_x: f64, bottom_y: f64, right_x: f64, top_y: f64) -> Self {
50 Self {
51 page_number: page,
52 last_page_number: page,
53 left_x,
54 bottom_y,
55 right_x,
56 top_y,
57 }
58 }
59
60 pub fn empty() -> Self {
62 Self {
63 page_number: None,
64 last_page_number: None,
65 left_x: 0.0,
66 bottom_y: 0.0,
67 right_x: 0.0,
68 top_y: 0.0,
69 }
70 }
71
72 pub fn width(&self) -> f64 {
74 self.right_x - self.left_x
75 }
76
77 pub fn height(&self) -> f64 {
79 self.top_y - self.bottom_y
80 }
81
82 pub fn area(&self) -> f64 {
84 let w = self.width();
85 let h = self.height();
86 if w < 0.0 || h < 0.0 {
87 0.0
88 } else {
89 w * h
90 }
91 }
92
93 pub fn center_x(&self) -> f64 {
95 (self.left_x + self.right_x) / 2.0
96 }
97
98 pub fn center_y(&self) -> f64 {
100 (self.bottom_y + self.top_y) / 2.0
101 }
102
103 pub fn is_empty(&self) -> bool {
105 self.width() <= BBOX_EPSILON || self.height() <= BBOX_EPSILON
106 }
107
108 pub fn is_one_page(&self) -> bool {
110 match (self.page_number, self.last_page_number) {
111 (Some(p), Some(lp)) => p == lp,
112 (Some(_), None) | (None, None) => true,
113 _ => false,
114 }
115 }
116
117 pub fn is_multi_page(&self) -> bool {
119 !self.is_one_page()
120 }
121
122 pub fn normalize(&mut self) {
124 if self.left_x > self.right_x {
125 std::mem::swap(&mut self.left_x, &mut self.right_x);
126 }
127 if self.bottom_y > self.top_y {
128 std::mem::swap(&mut self.bottom_y, &mut self.top_y);
129 }
130 }
131
132 pub fn union(&self, other: &BoundingBox) -> BoundingBox {
134 BoundingBox {
135 page_number: self.page_number.or(other.page_number),
136 last_page_number: match (self.last_page_number, other.last_page_number) {
137 (Some(a), Some(b)) => Some(a.max(b)),
138 (Some(a), None) => Some(a),
139 (None, Some(b)) => Some(b),
140 (None, None) => None,
141 },
142 left_x: self.left_x.min(other.left_x),
143 bottom_y: self.bottom_y.min(other.bottom_y),
144 right_x: self.right_x.max(other.right_x),
145 top_y: self.top_y.max(other.top_y),
146 }
147 }
148
149 pub fn overlaps(&self, other: &BoundingBox) -> bool {
151 self.left_x < other.right_x
152 && self.right_x > other.left_x
153 && self.bottom_y < other.top_y
154 && self.top_y > other.bottom_y
155 }
156
157 pub fn contains(&self, other: &BoundingBox) -> bool {
159 self.left_x <= other.left_x + BBOX_EPSILON
160 && self.right_x >= other.right_x - BBOX_EPSILON
161 && self.bottom_y <= other.bottom_y + BBOX_EPSILON
162 && self.top_y >= other.top_y - BBOX_EPSILON
163 }
164
165 pub fn weakly_contains(&self, other: &BoundingBox) -> bool {
167 let margin = 1.0; self.left_x <= other.left_x + margin
169 && self.right_x >= other.right_x - margin
170 && self.bottom_y <= other.bottom_y + margin
171 && self.top_y >= other.top_y - margin
172 }
173
174 pub fn intersection_percent(&self, other: &BoundingBox) -> f64 {
176 let ix_left = self.left_x.max(other.left_x);
177 let ix_right = self.right_x.min(other.right_x);
178 let iy_bottom = self.bottom_y.max(other.bottom_y);
179 let iy_top = self.top_y.min(other.top_y);
180
181 if ix_left >= ix_right || iy_bottom >= iy_top {
182 return 0.0;
183 }
184
185 let intersection_area = (ix_right - ix_left) * (iy_top - iy_bottom);
186 let other_area = other.area();
187
188 if other_area <= BBOX_EPSILON {
189 return 0.0;
190 }
191
192 intersection_area / other_area
193 }
194
195 pub fn vertical_intersection_percent(&self, other: &BoundingBox) -> f64 {
197 let iy_bottom = self.bottom_y.max(other.bottom_y);
198 let iy_top = self.top_y.min(other.top_y);
199
200 if iy_bottom >= iy_top {
201 return 0.0;
202 }
203
204 let intersection_height = iy_top - iy_bottom;
205 let other_height = other.height();
206
207 if other_height <= BBOX_EPSILON {
208 return 0.0;
209 }
210
211 intersection_height / other_height
212 }
213
214 pub fn vertical_gap(&self, other: &BoundingBox) -> f64 {
216 if self.top_y < other.bottom_y {
217 other.bottom_y - self.top_y
218 } else if other.top_y < self.bottom_y {
219 self.bottom_y - other.top_y
220 } else {
221 0.0 }
223 }
224
225 pub fn horizontal_gap(&self, other: &BoundingBox) -> f64 {
227 if self.right_x < other.left_x {
228 other.left_x - self.right_x
229 } else if other.right_x < self.left_x {
230 self.left_x - other.right_x
231 } else {
232 0.0 }
234 }
235
236 pub fn are_horizontal_overlapping(&self, other: &BoundingBox) -> bool {
238 self.left_x < other.right_x && self.right_x > other.left_x
239 }
240
241 pub fn are_vertical_overlapping(&self, other: &BoundingBox) -> bool {
243 self.bottom_y < other.top_y && self.top_y > other.bottom_y
244 }
245
246 pub fn scale(&mut self, factor: f64) {
248 let cx = self.center_x();
249 let cy = self.center_y();
250 let half_w = self.width() * factor / 2.0;
251 let half_h = self.height() * factor / 2.0;
252 self.left_x = cx - half_w;
253 self.right_x = cx + half_w;
254 self.bottom_y = cy - half_h;
255 self.top_y = cy + half_h;
256 }
257
258 pub fn translate(&mut self, dx: f64, dy: f64) {
260 self.left_x += dx;
261 self.right_x += dx;
262 self.bottom_y += dy;
263 self.top_y += dy;
264 }
265
266 pub fn to_json_array(&self) -> [f64; 4] {
268 [self.left_x, self.bottom_y, self.right_x, self.top_y]
269 }
270}
271
272#[derive(Debug, Clone, Serialize, Deserialize)]
274pub struct MultiBoundingBox {
275 pub outer: BoundingBox,
277 pub inner: Vec<BoundingBox>,
279}
280
281#[derive(Debug, Clone, Serialize, Deserialize)]
283pub struct Vertex {
284 pub x: f64,
286 pub y: f64,
288 pub radius: f64,
290}
291
292impl Vertex {
293 pub fn new(x: f64, y: f64, radius: f64) -> Self {
295 Self { x, y, radius }
296 }
297}
298
299#[cfg(test)]
300mod tests {
301 use super::*;
302
303 #[test]
304 fn test_bbox_new() {
305 let bbox = BoundingBox::new(Some(1), 10.0, 20.0, 100.0, 80.0);
306 assert_eq!(bbox.page_number, Some(1));
307 assert_eq!(bbox.left_x, 10.0);
308 assert_eq!(bbox.bottom_y, 20.0);
309 assert_eq!(bbox.right_x, 100.0);
310 assert_eq!(bbox.top_y, 80.0);
311 }
312
313 #[test]
314 fn test_bbox_dimensions() {
315 let bbox = BoundingBox::new(Some(1), 10.0, 20.0, 110.0, 80.0);
316 assert!((bbox.width() - 100.0).abs() < BBOX_EPSILON);
317 assert!((bbox.height() - 60.0).abs() < BBOX_EPSILON);
318 assert!((bbox.area() - 6000.0).abs() < BBOX_EPSILON);
319 assert!((bbox.center_x() - 60.0).abs() < BBOX_EPSILON);
320 assert!((bbox.center_y() - 50.0).abs() < BBOX_EPSILON);
321 }
322
323 #[test]
324 fn test_bbox_empty() {
325 let bbox = BoundingBox::empty();
326 assert!(bbox.is_empty());
327 assert_eq!(bbox.area(), 0.0);
328 }
329
330 #[test]
331 fn test_bbox_normalize() {
332 let mut bbox = BoundingBox::new(Some(1), 100.0, 80.0, 10.0, 20.0);
333 bbox.normalize();
334 assert_eq!(bbox.left_x, 10.0);
335 assert_eq!(bbox.bottom_y, 20.0);
336 assert_eq!(bbox.right_x, 100.0);
337 assert_eq!(bbox.top_y, 80.0);
338 }
339
340 #[test]
341 fn test_bbox_union() {
342 let a = BoundingBox::new(Some(1), 10.0, 20.0, 50.0, 60.0);
343 let b = BoundingBox::new(Some(1), 30.0, 10.0, 80.0, 50.0);
344 let u = a.union(&b);
345 assert_eq!(u.left_x, 10.0);
346 assert_eq!(u.bottom_y, 10.0);
347 assert_eq!(u.right_x, 80.0);
348 assert_eq!(u.top_y, 60.0);
349 }
350
351 #[test]
352 fn test_bbox_overlaps() {
353 let a = BoundingBox::new(Some(1), 0.0, 0.0, 50.0, 50.0);
354 let b = BoundingBox::new(Some(1), 25.0, 25.0, 75.0, 75.0);
355 let c = BoundingBox::new(Some(1), 60.0, 60.0, 100.0, 100.0);
356 assert!(a.overlaps(&b));
357 assert!(!a.overlaps(&c));
358 }
359
360 #[test]
361 fn test_bbox_contains() {
362 let outer = BoundingBox::new(Some(1), 0.0, 0.0, 100.0, 100.0);
363 let inner = BoundingBox::new(Some(1), 10.0, 10.0, 90.0, 90.0);
364 let partial = BoundingBox::new(Some(1), 50.0, 50.0, 150.0, 150.0);
365 assert!(outer.contains(&inner));
366 assert!(!outer.contains(&partial));
367 }
368
369 #[test]
370 fn test_bbox_intersection_percent() {
371 let a = BoundingBox::new(Some(1), 0.0, 0.0, 100.0, 100.0);
372 let b = BoundingBox::new(Some(1), 50.0, 50.0, 150.0, 150.0);
373 let pct = a.intersection_percent(&b);
375 assert!((pct - 0.25).abs() < 0.01);
376 }
377
378 #[test]
379 fn test_bbox_vertical_gap() {
380 let a = BoundingBox::new(Some(1), 0.0, 50.0, 100.0, 100.0);
381 let b = BoundingBox::new(Some(1), 0.0, 0.0, 100.0, 30.0);
382 assert!((a.vertical_gap(&b) - 20.0).abs() < BBOX_EPSILON);
383 }
384
385 #[test]
386 fn test_bbox_horizontal_gap() {
387 let a = BoundingBox::new(Some(1), 0.0, 0.0, 50.0, 100.0);
388 let b = BoundingBox::new(Some(1), 80.0, 0.0, 150.0, 100.0);
389 assert!((a.horizontal_gap(&b) - 30.0).abs() < BBOX_EPSILON);
390 }
391
392 #[test]
393 fn test_bbox_is_one_page() {
394 let mut bbox = BoundingBox::new(Some(1), 0.0, 0.0, 100.0, 100.0);
395 assert!(bbox.is_one_page());
396 bbox.last_page_number = Some(3);
397 assert!(bbox.is_multi_page());
398 }
399
400 #[test]
401 fn test_bbox_translate() {
402 let mut bbox = BoundingBox::new(Some(1), 10.0, 20.0, 50.0, 60.0);
403 bbox.translate(5.0, -10.0);
404 assert!((bbox.left_x - 15.0).abs() < BBOX_EPSILON);
405 assert!((bbox.bottom_y - 10.0).abs() < BBOX_EPSILON);
406 assert!((bbox.right_x - 55.0).abs() < BBOX_EPSILON);
407 assert!((bbox.top_y - 50.0).abs() < BBOX_EPSILON);
408 }
409
410 #[test]
411 fn test_bbox_to_json_array() {
412 let bbox = BoundingBox::new(Some(1), 10.0, 20.0, 100.0, 80.0);
413 let arr = bbox.to_json_array();
414 assert_eq!(arr, [10.0, 20.0, 100.0, 80.0]);
415 }
416
417 #[test]
418 fn test_vertex() {
419 let v = Vertex::new(10.0, 20.0, 1.5);
420 assert_eq!(v.x, 10.0);
421 assert_eq!(v.y, 20.0);
422 assert_eq!(v.radius, 1.5);
423 }
424}