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