rvimage_domain/
bb.rs

1use std::{
2    fmt::Display,
3    ops::{Neg, Range},
4    str::FromStr,
5};
6
7use serde::{Deserialize, Serialize};
8
9use super::{
10    core::{
11        clamp_sub_zero, max_from_partial, max_squaredist, min_from_partial, CoordinateBox, Max,
12        Min, Shape,
13    },
14    Calc, OutOfBoundsMode, Point, PtF, PtI, TPtF, TPtI, TPtS,
15};
16use crate::{
17    result::{to_rv, RvError, RvResult},
18    rverr, ShapeI,
19};
20
21pub type BbI = BB<TPtI>;
22pub type BbS = BB<TPtS>;
23pub type BbF = BB<TPtF>;
24
25#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, Default)]
26pub struct BB<T> {
27    pub x: T,
28    pub y: T,
29    pub w: T,
30    pub h: T,
31}
32
33impl<T> BB<T>
34where
35    T: Calc + CoordinateBox,
36{
37    /// `[x, y, w, h]`
38    pub fn from_arr(a: &[T; 4]) -> Self {
39        BB {
40            x: a[0],
41            y: a[1],
42            w: a[2],
43            h: a[3],
44        }
45    }
46
47    pub fn merge(&self, other: Self) -> Self {
48        let x = self.x.min(other.x);
49        let y = self.y.min(other.y);
50        let x_max = self.x_max().max(other.x_max());
51        let y_max = self.y_max().max(other.y_max());
52        BB::from_points((x, y).into(), (x_max, y_max).into())
53    }
54
55    // TODO: replace Point<T> by &Point<T>
56    pub fn from_points_iter(points: impl Iterator<Item = Point<T>> + Clone) -> RvResult<Self> {
57        let x_iter = points.clone().map(|p| p.x);
58        let y_iter = points.map(|p| p.y);
59        let min_x = x_iter
60            .clone()
61            .min_by(min_from_partial)
62            .ok_or_else(|| rverr!("empty iterator"))?;
63        let min_y = y_iter
64            .clone()
65            .min_by(min_from_partial)
66            .ok_or_else(|| rverr!("empty iterator"))?;
67        let max_x = x_iter
68            .max_by(max_from_partial)
69            .ok_or_else(|| rverr!("empty iterator"))?;
70        let max_y = y_iter
71            .max_by(max_from_partial)
72            .ok_or_else(|| rverr!("empty iterator"))?;
73        Ok(BB::from_points(
74            Point { x: min_x, y: min_y },
75            Point { x: max_x, y: max_y },
76        ))
77    }
78    pub fn from_vec(points: &[Point<T>]) -> RvResult<Self> {
79        Self::from_points_iter(points.iter().copied())
80    }
81
82    pub fn distance_to_boundary(&self, pos: Point<T>) -> T {
83        let dx = (self.x - pos.x).abs();
84        let dw = ((self.x + self.w) - pos.x).abs();
85        let dy = (self.y - pos.y).abs();
86        let dh = ((self.y + self.h) - pos.y).abs();
87        dx.min(dw).min(dy).min(dh)
88    }
89
90    pub fn split_horizontally(&self, y: T) -> (Self, Self) {
91        let top = BB::from_arr(&[self.x, self.y, self.w, y - self.y]);
92        let btm = BB::from_arr(&[self.x, y, self.w, self.y_max() - y]);
93        (top, btm)
94    }
95    pub fn split_vertically(&self, x: T) -> (Self, Self) {
96        let left = BB::from_arr(&[self.x, self.y, x - self.x, self.h]);
97        let right = BB::from_arr(&[x, self.y, self.x_max() - x, self.h]);
98        (left, right)
99    }
100    #[must_use]
101    pub fn from_shape_int(shape: ShapeI) -> Self {
102        BB {
103            x: T::from(0),
104            y: T::from(0),
105            w: T::from(shape.w),
106            h: T::from(shape.h),
107        }
108    }
109
110    pub fn from_shape(shape: Shape<T>) -> Self {
111        BB {
112            x: T::from(0),
113            y: T::from(0),
114            w: shape.w,
115            h: shape.h,
116        }
117    }
118
119    pub fn y_max(&self) -> T {
120        // y_max is still part of the box, hence -1
121        self.y + self.h - T::size_addon()
122    }
123
124    pub fn x_max(&self) -> T {
125        // x_max is still part of the box, hence -1
126        self.x + self.w - T::size_addon()
127    }
128
129    pub fn intersect(self, other: BB<T>) -> BB<T> {
130        BB::from_points(
131            Point {
132                x: self.x.max(other.x),
133                y: self.y.max(other.y),
134            },
135            Point {
136                x: self.x_max().min(other.x_max()),
137                y: self.y_max().min(other.y_max()),
138            },
139        )
140    }
141
142    pub fn points(&self) -> [Point<T>; 4] {
143        [
144            self.corner(0),
145            self.corner(1),
146            self.corner(2),
147            self.corner(3),
148        ]
149    }
150
151    pub fn intersect_or_self(&self, other: Option<BB<T>>) -> BB<T> {
152        if let Some(other) = other {
153            self.intersect(other)
154        } else {
155            *self
156        }
157    }
158
159    /// Return points of greatest distance between self and other
160    pub fn max_squaredist<'a>(
161        &'a self,
162        other: impl Iterator<Item = Point<T>> + 'a + Clone,
163    ) -> (Point<T>, Point<T>, T) {
164        max_squaredist(self.points_iter(), other)
165    }
166
167    pub fn min_max(&self, axis: usize) -> (T, T) {
168        if axis == 0 {
169            (self.x, self.x + self.w)
170        } else {
171            (self.y, self.y + self.h)
172        }
173    }
174
175    /// Iteration order of corners
176    /// 0   3
177    /// v   ĘŚ
178    /// 1 > 2
179    #[allow(clippy::needless_lifetimes)]
180    pub fn points_iter<'a>(&'a self) -> impl Iterator<Item = Point<T>> + 'a + Clone {
181        (0..4).map(|idx| self.corner(idx))
182    }
183
184    pub fn corner(&self, idx: usize) -> Point<T> {
185        let (x, y, w, h) = (self.x, self.y, self.w, self.h);
186        match idx {
187            0 => Point { x, y },
188            1 => Point {
189                x,
190                y: y + h - T::size_addon(),
191            },
192            2 => (x + w - T::size_addon(), y + h - T::size_addon()).into(),
193            3 => (x + w - T::size_addon(), y).into(),
194            _ => panic!("bounding boxes only have 4, {idx} is out of bounds"),
195        }
196    }
197    pub fn opposite_corner(&self, idx: usize) -> Point<T> {
198        self.corner((idx + 2) % 4)
199    }
200
201    pub fn shape(&self) -> Shape<T> {
202        Shape {
203            w: self.w,
204            h: self.h,
205        }
206    }
207
208    pub fn from_points(p1: Point<T>, p2: Point<T>) -> Self {
209        let x_min = p1.x.min(p2.x);
210        let y_min = p1.y.min(p2.y);
211        let x_max = p1.x.max(p2.x);
212        let y_max = p1.y.max(p2.y);
213        Self {
214            x: x_min,
215            y: y_min,
216            w: x_max - x_min + T::size_addon(), // x_min and x_max are both contained in the bb
217            h: y_max - y_min + T::size_addon(),
218        }
219    }
220
221    pub fn x_range(&self) -> Range<T> {
222        self.x..(self.x + self.w)
223    }
224
225    pub fn y_range(&self) -> Range<T> {
226        self.y..(self.y + self.h)
227    }
228
229    pub fn center_f(&self) -> (f64, f64)
230    where
231        T: Into<f64>,
232    {
233        (
234            self.w.into() * 0.5 + self.x.into(),
235            self.h.into() * 0.5 + self.y.into(),
236        )
237    }
238
239    pub fn min(&self) -> Point<T> {
240        Point {
241            x: self.x,
242            y: self.y,
243        }
244    }
245
246    pub fn max(&self) -> Point<T> {
247        Point {
248            x: self.x_max(),
249            y: self.y_max(),
250        }
251    }
252
253    pub fn covers_y(&self, y: T) -> bool {
254        self.y_max() >= y && self.y <= y
255    }
256    pub fn covers_x(&self, x: T) -> bool {
257        self.x_max() >= x && self.x <= x
258    }
259
260    pub fn contains<P>(&self, p: P) -> bool
261    where
262        P: Into<Point<T>>,
263    {
264        let p = p.into();
265        self.covers_x(p.x) && self.covers_y(p.y)
266    }
267
268    pub fn contains_bb(&self, other: Self) -> bool {
269        self.contains(other.min()) && self.contains(other.max())
270    }
271
272    pub fn is_contained_in_image(&self, shape: ShapeI) -> bool {
273        self.x + self.w <= shape.w.into() && self.y + self.h <= shape.h.into()
274    }
275
276    pub fn new_shape_checked(
277        x: T,
278        y: T,
279        w: T,
280        h: T,
281        orig_im_shape: ShapeI,
282        mode: OutOfBoundsMode<T>,
283    ) -> Option<Self> {
284        match mode {
285            OutOfBoundsMode::Deny => {
286                if x < T::zero() || y < T::zero() || w < T::one() || h < T::one() {
287                    None
288                } else {
289                    let bb = Self { x, y, w, h };
290                    if bb.is_contained_in_image(orig_im_shape) {
291                        Some(bb)
292                    } else {
293                        None
294                    }
295                }
296            }
297            OutOfBoundsMode::Resize(min_bb_shape) => {
298                let bb = Self {
299                    x: x.min(clamp_sub_zero(orig_im_shape.w.into(), min_bb_shape.w)),
300                    y: y.min(clamp_sub_zero(orig_im_shape.h.into(), min_bb_shape.h)),
301                    w: (w + x.min(T::zero())).max(min_bb_shape.w),
302                    h: (h + y.min(T::zero())).max(min_bb_shape.h),
303                };
304                let mut bb_resized = bb.intersect(BB::from_shape_int(orig_im_shape));
305                bb_resized.w = bb_resized.w.max(min_bb_shape.w);
306                bb_resized.h = bb_resized.h.max(min_bb_shape.h);
307                Some(bb_resized)
308            }
309        }
310    }
311
312    pub fn has_overlap(&self, other: &Self) -> bool {
313        if self.points_iter().any(|c| other.contains(c)) {
314            true
315        } else {
316            other.points_iter().any(|c| self.contains(c))
317        }
318    }
319
320    pub fn rot90_with_image_ntimes(&self, shape: ShapeI, n: u8) -> Self
321    where
322        T: Neg<Output = T>,
323    {
324        let p_min = self.min().rot90_with_image_ntimes(shape, n);
325        let p_max = self.max().rot90_with_image_ntimes(shape, n);
326        Self::from_points(p_min, p_max)
327    }
328}
329
330impl BbF {
331    #[must_use]
332    pub fn translate(
333        self,
334        x_shift: f64,
335        y_shift: f64,
336        shape: ShapeI,
337        oob_mode: OutOfBoundsMode<f64>,
338    ) -> Option<Self> {
339        let x = self.x + x_shift;
340        let y = self.y + y_shift;
341        Self::new_shape_checked(x, y, self.w, self.h, shape, oob_mode)
342    }
343    #[must_use]
344    pub fn follow_movement(
345        &self,
346        from: PtF,
347        to: PtF,
348        shape: ShapeI,
349        oob_mode: OutOfBoundsMode<f64>,
350    ) -> Option<Self> {
351        let x_shift = to.x - from.x;
352        let y_shift = to.y - from.y;
353        self.translate(x_shift, y_shift, shape, oob_mode)
354    }
355
356    #[must_use]
357    pub fn new_fit_to_image(x: f64, y: f64, w: f64, h: f64, shape: ShapeI) -> Self {
358        let clip = |var: f64, size_bx: f64, size_im: f64| {
359            if var < 0.0 {
360                let size_bx = size_bx + var;
361                (0.0, size_bx.min(size_im))
362            } else {
363                (var, (size_bx + var).min(size_im) - var)
364            }
365        };
366        let (x, w) = clip(x, w, shape.w.into());
367        let (y, h) = clip(y, h, shape.h.into());
368
369        Self::from_arr(&[x, y, w, h])
370    }
371
372    #[must_use]
373    pub fn center_scale(
374        &self,
375        x_factor: f64,
376        y_factor: f64,
377        shape: ShapeI,
378        center: Option<PtF>,
379    ) -> Self {
380        let x = self.x;
381        let y = self.y;
382        let w = self.w;
383        let h = self.h;
384        let c = center.unwrap_or(PtF {
385            x: w * 0.5 + x,
386            y: h * 0.5 + y,
387        });
388        let topleft = (c.x + x_factor * (x - c.x), c.y + y_factor * (y - c.y));
389        let btmright = (
390            c.x + x_factor * (x + w - c.x),
391            c.y + y_factor * (y + h - c.y),
392        );
393        let (x_tl, y_tl) = topleft;
394        let (x_br, y_br) = btmright;
395        let w = x_br - x_tl;
396        let h = y_br - y_tl;
397        let x = x_tl.round();
398        let y = y_tl.round();
399
400        Self::new_fit_to_image(x, y, w, h, shape)
401    }
402
403    #[must_use]
404    pub fn shift_max(&self, x_shift: f64, y_shift: f64, shape: ShapeI) -> Option<Self> {
405        let (w, h) = (self.w + x_shift, self.h + y_shift);
406        Self::new_shape_checked(self.x, self.y, w, h, shape, OutOfBoundsMode::Deny)
407    }
408
409    #[must_use]
410    pub fn shift_min(&self, x_shift: f64, y_shift: f64, shape: ShapeI) -> Option<Self> {
411        let (x, y) = (self.x + x_shift, self.y + y_shift);
412        let (w, h) = (self.w - x_shift, self.h - y_shift);
413        Self::new_shape_checked(x, y, w, h, shape, OutOfBoundsMode::Deny)
414    }
415
416    #[must_use]
417    pub fn all_corners_close(&self, other: BbF) -> bool {
418        fn close_floats(a: f64, b: f64) -> bool {
419            (a - b).abs() < 1e-8
420        }
421        close_floats(self.x, other.x)
422            && close_floats(self.y, other.y)
423            && close_floats(self.w, other.w)
424            && close_floats(self.h, other.h)
425    }
426}
427
428impl From<BbF> for BbI {
429    fn from(box_f: BbF) -> Self {
430        let p_min: PtI = box_f.min().into();
431        let p_max: PtI = box_f.max().into();
432        let x = p_min.x;
433        let y = p_min.y;
434        let x_max = p_max.x - TPtI::size_addon();
435        let y_max = p_max.y - TPtI::size_addon();
436        BbI::from_points((x, y).into(), (x_max, y_max).into())
437    }
438}
439impl From<BbI> for BbF {
440    fn from(box_int: BbI) -> Self {
441        let x = box_int.min().x;
442        let y = box_int.min().y;
443        let x_max = box_int.max().x + TPtI::size_addon();
444        let y_max = box_int.max().y + TPtI::size_addon();
445        BbF::from_points((x, y).into(), (x_max, y_max).into())
446    }
447}
448
449impl From<BbI> for BbS {
450    fn from(bb: BbI) -> Self {
451        BbS::from_points(bb.min().into(), bb.max().into())
452    }
453}
454impl From<BbS> for BbI {
455    fn from(bb: BbS) -> Self {
456        BbI::from_points(bb.min().into(), bb.max().into())
457    }
458}
459
460impl BbI {
461    #[must_use]
462    pub fn expand(&self, x_expand: TPtI, y_expand: TPtI, shape: ShapeI) -> Self {
463        let (x, y) = (
464            self.x.saturating_sub(x_expand),
465            self.y.saturating_sub(y_expand),
466        );
467        let (w, h) = (self.w + 2 * x_expand, self.h + 2 * y_expand);
468        let (w, h) = (w.clamp(1, shape.w), h.clamp(1, shape.h));
469        Self { x, y, w, h }
470    }
471}
472
473impl<T> From<&[T; 4]> for BB<T>
474where
475    T: Calc + CoordinateBox,
476{
477    fn from(a: &[T; 4]) -> Self {
478        Self::from_arr(a)
479    }
480}
481
482impl Display for BbI {
483    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
484        let bb_str = format!("[{}, {}, {} ,{}]", self.x, self.y, self.w, self.h);
485        f.write_str(bb_str.as_str())
486    }
487}
488impl FromStr for BbI {
489    type Err = RvError;
490    fn from_str(s: &str) -> RvResult<Self> {
491        let err_parse = rverr!("could not parse '{}' into a bounding box", s);
492        let mut int_iter = s[1..(s.len() - 1)]
493            .split(',')
494            .map(|cse| cse.trim().parse::<u32>().map_err(to_rv));
495        let x = int_iter.next().ok_or_else(|| err_parse.clone())??;
496        let y = int_iter.next().ok_or_else(|| err_parse.clone())??;
497        let w = int_iter.next().ok_or_else(|| err_parse.clone())??;
498        let h = int_iter.next().ok_or(err_parse)??;
499        Ok(BbI { x, y, w, h })
500    }
501}
502
503#[cfg(test)]
504use crate::PtS;
505
506#[test]
507fn test_rot() {
508    let shape = Shape::new(150, 123);
509    let p_min = PtS { x: 1, y: 3 };
510    let p_max = PtS { x: 6, y: 15 };
511    let bb = BB::from_points(p_min, p_max);
512    for n in 0..6 {
513        let b_rotated = BB::from_points(
514            p_min.rot90_with_image_ntimes(shape, n),
515            p_max.rot90_with_image_ntimes(shape, n),
516        );
517        assert_eq!(b_rotated, bb.rot90_with_image_ntimes(shape, n));
518    }
519    let shape = Shape::new(5, 10);
520    let p_min = PtF { x: 1.0, y: 2.0 };
521    let p_max = PtF { x: 2.0, y: 4.0 };
522    let bb = BB::from_points(p_min, p_max);
523    let p_min = PtF { x: 2.0, y: 4.0 };
524    let p_max = PtF { x: 4.0, y: 3.0 };
525    let bb_ref_1 = BB::from_points(p_min, p_max);
526    assert_eq!(bb.rot90_with_image_ntimes(shape, 1), bb_ref_1);
527}
528
529#[test]
530fn test_expand() {
531    let bb = BbI::from_arr(&[0, 0, 10, 10]).expand(1, 1, Shape::new(10, 10));
532    assert_eq!(bb, BbI::from_arr(&[0, 0, 10, 10]));
533
534    let bb = BbI::from_arr(&[5, 5, 10, 10]).expand(1, 2, Shape::new(20, 20));
535    assert_eq!(bb, BbI::from_arr(&[4, 3, 12, 14]));
536}