math2/
rect.rs

1use super::vector2::Vector2;
2
3/// Represents a side of a rectangle.
4#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5pub enum RectangleSide {
6    Top,
7    Right,
8    Bottom,
9    Left,
10}
11
12/// Cardinal directions including diagonals.
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum CardinalDirection {
15    N,
16    E,
17    S,
18    W,
19    NE,
20    SE,
21    SW,
22    NW,
23}
24
25/// A rectangle defined by its top-left corner `(x, y)` and `width` and `height`.
26#[derive(Debug, Clone, Copy, PartialEq)]
27pub struct Rectangle {
28    pub x: f32,
29    pub y: f32,
30    pub width: f32,
31    pub height: f32,
32}
33
34impl Rectangle {
35    /// Returns the center point of the rectangle.
36    pub fn center(&self) -> Vector2 {
37        [self.x + self.width / 2.0, self.y + self.height / 2.0]
38    }
39
40    /// Returns a new rectangle translated by the given vector.
41    pub fn translate(&self, delta: Vector2) -> Self {
42        Self {
43            x: self.x + delta[0],
44            y: self.y + delta[1],
45            ..*self
46        }
47    }
48
49    /// Scales the rectangle relative to the given origin.
50    pub fn scale(&self, origin: Vector2, scale: Vector2) -> Self {
51        let [sx, sy] = scale;
52        let [ox, oy] = origin;
53        Self {
54            x: ox + (self.x - ox) * sx,
55            y: oy + (self.y - oy) * sy,
56            width: self.width * sx,
57            height: self.height * sy,
58        }
59    }
60
61    /// Returns the dimension (`width` or `height`) for the given axis.
62    pub fn axis_dimension(&self, axis: super::vector2::Axis) -> f32 {
63        match axis {
64            super::vector2::Axis::X => self.width,
65            super::vector2::Axis::Y => self.height,
66        }
67    }
68
69    /// Returns true if `self` fully contains `other`.
70    pub fn contains(&self, other: &Rectangle) -> bool {
71        self.x <= other.x
72            && self.y <= other.y
73            && self.x + self.width >= other.x + other.width
74            && self.y + self.height >= other.y + other.height
75    }
76
77    /// Returns true if the point is inside the rectangle (inclusive).
78    pub fn contains_point(&self, point: Vector2) -> bool {
79        let [px, py] = point;
80        px >= self.x && px <= self.x + self.width && py >= self.y && py <= self.y + self.height
81    }
82
83    /// Returns the signed offset from the point to the nearest edge.
84    pub fn offset_to(&self, point: Vector2) -> Vector2 {
85        let clamped_x = point[0].max(self.x).min(self.x + self.width);
86        let clamped_y = point[1].max(self.y).min(self.y + self.height);
87        [point[0] - clamped_x, point[1] - clamped_y]
88    }
89
90    /// Returns `true` if two rectangles intersect or touch at the edges.
91    pub fn intersects(&self, other: &Rectangle) -> bool {
92        let a_right = self.x + self.width;
93        let a_bottom = self.y + self.height;
94        let b_right = other.x + other.width;
95        let b_bottom = other.y + other.height;
96
97        !(self.x > b_right || self.y > b_bottom || a_right < other.x || a_bottom < other.y)
98    }
99
100    /// Returns the intersection of two rectangles, or `None` if they do not overlap.
101    pub fn intersection(&self, other: &Rectangle) -> Option<Rectangle> {
102        let x1 = self.x.max(other.x);
103        let y1 = self.y.max(other.y);
104        let x2 = (self.x + self.width).min(other.x + other.width);
105        let y2 = (self.y + self.height).min(other.y + other.height);
106
107        if x2 <= x1 || y2 <= y1 {
108            return None;
109        }
110
111        Some(Rectangle {
112            x: x1,
113            y: y1,
114            width: x2 - x1,
115            height: y2 - y1,
116        })
117    }
118
119    /// Subtracts `other` from this rectangle, returning the remaining subregions.
120    pub fn subtract(&self, other: Rectangle) -> Vec<Rectangle> {
121        boolean::subtract(*self, other)
122    }
123}
124
125/// Computes the smallest rectangle that encloses all provided points.
126pub fn from_points(points: &[Vector2]) -> Rectangle {
127    assert!(!points.is_empty(), "at least one point is required");
128    let mut min_x = f32::INFINITY;
129    let mut min_y = f32::INFINITY;
130    let mut max_x = f32::NEG_INFINITY;
131    let mut max_y = f32::NEG_INFINITY;
132    for &[x, y] in points {
133        if x < min_x {
134            min_x = x;
135        }
136        if y < min_y {
137            min_y = y;
138        }
139        if x > max_x {
140            max_x = x;
141        }
142        if y > max_y {
143            max_y = y;
144        }
145    }
146    Rectangle {
147        x: min_x,
148        y: min_y,
149        width: max_x - min_x,
150        height: max_y - min_y,
151    }
152}
153
154/// Returns an object containing the nine control points of a rectangle.
155#[derive(Debug, Clone, Copy, PartialEq)]
156pub struct Rect9Points {
157    pub top_left: Vector2,
158    pub top_right: Vector2,
159    pub bottom_right: Vector2,
160    pub bottom_left: Vector2,
161    pub top_center: Vector2,
162    pub right_center: Vector2,
163    pub bottom_center: Vector2,
164    pub left_center: Vector2,
165    pub center: Vector2,
166}
167
168/// Computes the nine control points of the rectangle.
169pub fn to_9points(rect: &Rectangle) -> Rect9Points {
170    let Rectangle {
171        x,
172        y,
173        width,
174        height,
175    } = *rect;
176    let center_x = x + width / 2.0;
177    let center_y = y + height / 2.0;
178    Rect9Points {
179        top_left: [x, y],
180        top_right: [x + width, y],
181        bottom_right: [x + width, y + height],
182        bottom_left: [x, y + height],
183        top_center: [center_x, y],
184        right_center: [x + width, center_y],
185        bottom_center: [center_x, y + height],
186        left_center: [x, center_y],
187        center: [center_x, center_y],
188    }
189}
190
191/// Same as [`to_9points`] but returns the points in an array ordered as:
192/// topLeft, topRight, bottomRight, bottomLeft, topCenter, rightCenter,
193/// bottomCenter, leftCenter, center.
194pub fn to_9points_chunk(rect: &Rectangle) -> [Vector2; 9] {
195    let p = to_9points(rect);
196    [
197        p.top_left,
198        p.top_right,
199        p.bottom_right,
200        p.bottom_left,
201        p.top_center,
202        p.right_center,
203        p.bottom_center,
204        p.left_center,
205        p.center,
206    ]
207}
208
209/// Returns true if rectangle `a` fully contains rectangle `b`.
210pub fn contains(a: &Rectangle, b: &Rectangle) -> bool {
211    b.contains(a)
212}
213
214/// Returns true if the point is inside the rectangle (inclusive).
215pub fn contains_point(rect: &Rectangle, point: Vector2) -> bool {
216    rect.contains_point(point)
217}
218
219/// Returns the signed offset from the point to the nearest edge of the rectangle.
220pub fn offset(rect: &Rectangle, point: Vector2) -> Vector2 {
221    rect.offset_to(point)
222}
223
224/// Returns `true` if two rectangles intersect or touch at the edges.
225pub fn intersects(a: &Rectangle, b: &Rectangle) -> bool {
226    a.intersects(b)
227}
228
229/// Returns the intersection of two rectangles, or `None` if they do not overlap.
230pub fn intersection(a: &Rectangle, b: &Rectangle) -> Option<Rectangle> {
231    a.intersection(b)
232}
233
234/// Computes the bounding rectangle of all input rectangles.
235pub fn union(rects: &[Rectangle]) -> Rectangle {
236    assert!(!rects.is_empty(), "rectangles array cannot be empty");
237    let mut min_x = f32::INFINITY;
238    let mut min_y = f32::INFINITY;
239    let mut max_x = f32::NEG_INFINITY;
240    let mut max_y = f32::NEG_INFINITY;
241    for r in rects {
242        if r.x < min_x {
243            min_x = r.x;
244        }
245        if r.y < min_y {
246            min_y = r.y;
247        }
248        if r.x + r.width > max_x {
249            max_x = r.x + r.width;
250        }
251        if r.y + r.height > max_y {
252            max_y = r.y + r.height;
253        }
254    }
255    Rectangle {
256        x: min_x,
257        y: min_y,
258        width: max_x - min_x,
259        height: max_y - min_y,
260    }
261}
262
263/// Boolean operations on rectangles.
264pub mod boolean {
265    use super::{Rectangle, intersection};
266
267    /// Subtracts rectangle `b` from rectangle `a`, returning the remaining subregions.
268    pub fn subtract(a: Rectangle, b: Rectangle) -> Vec<Rectangle> {
269        let inter = match intersection(&a, &b) {
270            Some(i) if i.width > 0.0 && i.height > 0.0 => i,
271            _ => return vec![a],
272        };
273
274        let mut result = Vec::new();
275
276        // Top region
277        if a.y < inter.y {
278            result.push(Rectangle {
279                x: a.x,
280                y: a.y,
281                width: a.width,
282                height: inter.y - a.y,
283            });
284        }
285
286        // Bottom region
287        if a.y + a.height > inter.y + inter.height {
288            result.push(Rectangle {
289                x: a.x,
290                y: inter.y + inter.height,
291                width: a.width,
292                height: a.y + a.height - (inter.y + inter.height),
293            });
294        }
295
296        // Left region
297        if a.x < inter.x {
298            result.push(Rectangle {
299                x: a.x,
300                y: inter.y,
301                width: inter.x - a.x,
302                height: inter.height,
303            });
304        }
305
306        // Right region
307        if a.x + a.width > inter.x + inter.width {
308            result.push(Rectangle {
309                x: inter.x + inter.width,
310                y: inter.y,
311                width: a.x + a.width - (inter.x + inter.width),
312                height: inter.height,
313            });
314        }
315
316        result
317    }
318}
319
320/// Calculates the gaps between adjacent rectangles along an axis.
321///
322/// The rectangles are first sorted by their starting position on the
323/// given axis. The returned vector contains the spacing between the end
324/// of each rectangle and the start of the next one.
325pub fn get_gaps(rectangles: &[Rectangle], axis: super::vector2::Axis) -> Vec<f32> {
326    if rectangles.len() < 2 {
327        return Vec::new();
328    }
329
330    let mut sorted: Vec<&Rectangle> = rectangles.iter().collect();
331    sorted.sort_by(|a, b| {
332        if axis == super::vector2::Axis::X {
333            a.x.partial_cmp(&b.x).unwrap()
334        } else {
335            a.y.partial_cmp(&b.y).unwrap()
336        }
337    });
338
339    let mut gaps = Vec::new();
340    for i in 0..sorted.len() - 1 {
341        let end = if axis == super::vector2::Axis::X {
342            sorted[i].x + sorted[i].width
343        } else {
344            sorted[i].y + sorted[i].height
345        };
346        let next_start = if axis == super::vector2::Axis::X {
347            sorted[i + 1].x
348        } else {
349            sorted[i + 1].y
350        };
351        gaps.push(next_start - end);
352    }
353    gaps
354}
355
356/// Calculates the uniform gap between rectangles if present.
357/// Returns `(Some(gap), gaps)` if all gaps are equal within `tolerance`.
358pub fn get_uniform_gap(
359    rectangles: &[Rectangle],
360    axis: super::vector2::Axis,
361    tolerance: f32,
362) -> (Option<f32>, Vec<f32>) {
363    let gaps = get_gaps(rectangles, axis);
364    if gaps.is_empty() {
365        return (None, gaps);
366    }
367
368    if crate::utils::is_uniform(&gaps, tolerance) {
369        let mut best_val = gaps[0];
370        let mut best_count = 0;
371        for &g in &gaps {
372            let count = gaps.iter().filter(|&&x| x == g).count();
373            if count > best_count {
374                best_count = count;
375                best_val = g;
376            }
377        }
378        let most = best_val;
379        (Some(most), gaps)
380    } else {
381        (None, gaps)
382    }
383}
384
385/// Repositions rectangles so they are evenly distributed along the axis while
386/// preserving the original ordering.
387pub fn distribute_evenly(rectangles: &[Rectangle], axis: super::vector2::Axis) -> Vec<Rectangle> {
388    if rectangles.len() < 2 {
389        return rectangles.to_vec();
390    }
391
392    let bbox = union(rectangles);
393    let start = if axis == super::vector2::Axis::X {
394        bbox.x
395    } else {
396        bbox.y
397    };
398    let total_size = if axis == super::vector2::Axis::X {
399        bbox.width
400    } else {
401        bbox.height
402    };
403    let total_rect_size: f32 = rectangles
404        .iter()
405        .map(|r| {
406            if axis == super::vector2::Axis::X {
407                r.width
408            } else {
409                r.height
410            }
411        })
412        .sum();
413
414    let gap_size = (total_size - total_rect_size) / (rectangles.len() as f32 - 1.0);
415
416    let mut sorted_indices: Vec<usize> = (0..rectangles.len()).collect();
417    sorted_indices.sort_by(|&a, &b| {
418        if axis == super::vector2::Axis::X {
419            rectangles[a].x.partial_cmp(&rectangles[b].x).unwrap()
420        } else {
421            rectangles[a].y.partial_cmp(&rectangles[b].y).unwrap()
422        }
423    });
424
425    let mut current = start;
426    let mut distributed = vec![
427        Rectangle {
428            x: 0.0,
429            y: 0.0,
430            width: 0.0,
431            height: 0.0
432        };
433        rectangles.len()
434    ];
435    for idx in sorted_indices {
436        let r = rectangles[idx];
437        let mut new_r = r;
438        if axis == super::vector2::Axis::X {
439            new_r.x = current;
440            current += r.width + gap_size;
441        } else {
442            new_r.y = current;
443            current += r.height + gap_size;
444        }
445        distributed[idx] = new_r;
446    }
447
448    distributed
449}
450
451/// Padding or margin values for each side of a rectangle.
452#[derive(Debug, Clone, Copy)]
453pub struct Sides {
454    pub top: f32,
455    pub right: f32,
456    pub bottom: f32,
457    pub left: f32,
458}
459
460impl From<f32> for Sides {
461    fn from(all: f32) -> Self {
462        Self {
463            top: all,
464            right: all,
465            bottom: all,
466            left: all,
467        }
468    }
469}
470
471impl From<[f32; 4]> for Sides {
472    fn from(v: [f32; 4]) -> Self {
473        Self {
474            top: v[0],
475            right: v[1],
476            bottom: v[2],
477            left: v[3],
478        }
479    }
480}
481
482/// Quantizes the rectangle position and size by the given step.
483pub fn quantize(rect: Rectangle, step: impl super::vector2::IntoVector2) -> Rectangle {
484    let s = step.into_vector2();
485    Rectangle {
486        x: crate::quantize(rect.x, s[0]),
487        y: crate::quantize(rect.y, s[1]),
488        width: crate::quantize(rect.width, s[0]),
489        height: crate::quantize(rect.height, s[1]),
490    }
491}
492
493/// Normalizes the rectangle so width and height are positive.
494pub fn positive(rect: Rectangle) -> Rectangle {
495    Rectangle {
496        x: rect.x.min(rect.x + rect.width),
497        y: rect.y.min(rect.y + rect.height),
498        width: rect.width.abs(),
499        height: rect.height.abs(),
500    }
501}
502
503/// Returns the aspect ratio `width / height`.
504pub fn aspect_ratio(rect: Rectangle) -> f32 {
505    rect.width / rect.height
506}
507
508/// Returns `[scale_x, scale_y]` required to scale `a` to match `b`.
509pub fn get_scale_factors(a: Rectangle, b: Rectangle) -> Vector2 {
510    [b.width / a.width, b.height / a.height]
511}
512
513use super::transform::AffineTransform;
514
515/// Returns the transform mapping rectangle `a` onto rectangle `b`.
516pub fn get_relative_transform(a: Rectangle, b: Rectangle) -> AffineTransform {
517    let sx = if a.width == 0.0 {
518        1.0
519    } else {
520        b.width / a.width
521    };
522    let sy = if a.height == 0.0 {
523        1.0
524    } else {
525        b.height / a.height
526    };
527
528    let t1 = AffineTransform::translate(-a.x, -a.y);
529    let t2 = AffineTransform {
530        matrix: [[sx, 0.0, 0.0], [0.0, sy, 0.0]],
531    };
532    let t3 = AffineTransform::translate(b.x, b.y);
533
534    t3.compose(&t2.compose(&t1))
535}
536
537/// Applies an affine transform to the rectangle and returns the bounding box.
538pub fn transform(rect: Rectangle, t: &AffineTransform) -> Rectangle {
539    let corners = [
540        [rect.x, rect.y],
541        [rect.x + rect.width, rect.y],
542        [rect.x, rect.y + rect.height],
543        [rect.x + rect.width, rect.y + rect.height],
544    ];
545    let transformed: Vec<Vector2> = corners
546        .iter()
547        .map(|&p| super::vector2::transform(p, t))
548        .collect();
549    from_points(&transformed)
550}
551
552/// Rotates the rectangle around its center and returns the bounding box.
553pub fn rotate(rect: Rectangle, degrees: f32) -> Rectangle {
554    let center = rect.center();
555    let rad = degrees.to_radians();
556    let (sin, cos) = rad.sin_cos();
557    let rotate_point = |p: Vector2| -> Vector2 {
558        let dx = p[0] - center[0];
559        let dy = p[1] - center[1];
560        [
561            center[0] + dx * cos - dy * sin,
562            center[1] + dx * sin + dy * cos,
563        ]
564    };
565    let pts = [
566        rotate_point([rect.x, rect.y]),
567        rotate_point([rect.x + rect.width, rect.y]),
568        rotate_point([rect.x, rect.y + rect.height]),
569        rotate_point([rect.x + rect.width, rect.y + rect.height]),
570    ];
571    from_points(&pts)
572}
573
574/// Returns the requested cardinal point of the rectangle.
575pub fn get_cardinal_point(rect: Rectangle, dir: CardinalDirection) -> Vector2 {
576    match dir {
577        CardinalDirection::N => [rect.x + rect.width / 2.0, rect.y],
578        CardinalDirection::E => [rect.x + rect.width, rect.y + rect.height / 2.0],
579        CardinalDirection::S => [rect.x + rect.width / 2.0, rect.y + rect.height],
580        CardinalDirection::W => [rect.x, rect.y + rect.height / 2.0],
581        CardinalDirection::NE => [rect.x + rect.width, rect.y],
582        CardinalDirection::SE => [rect.x + rect.width, rect.y + rect.height],
583        CardinalDirection::SW => [rect.x, rect.y + rect.height],
584        CardinalDirection::NW => [rect.x, rect.y],
585    }
586}
587
588/// Returns the center of the rectangle.
589pub fn get_center(rect: Rectangle) -> Vector2 {
590    rect.center()
591}
592
593/// Returns the overlapping projection range of rectangles along the counter axis.
594pub fn axis_projection_intersection(
595    rects: &[Rectangle],
596    axis: super::vector2::Axis,
597) -> Option<Vector2> {
598    assert!(rects.len() >= 2, "At least two rectangles are required");
599    let projections: Vec<Vector2> = rects
600        .iter()
601        .map(|r| {
602            if axis == super::vector2::Axis::X {
603                [r.y, r.y + r.height]
604            } else {
605                [r.x, r.x + r.width]
606            }
607        })
608        .collect();
609
610    projections
611        .iter()
612        .skip(1)
613        .fold(Some(projections[0]), |acc, p| {
614            acc.and_then(|cur| super::vector2::intersection(cur, *p))
615        })
616}
617
618/// Returns true if two rectangles are exactly equal.
619pub fn is_identical(a: Rectangle, b: Rectangle) -> bool {
620    a.x == b.x && a.y == b.y && a.width == b.width && a.height == b.height
621}
622
623/// Returns true if all rectangles in the slice are identical.
624pub fn is_uniform(rects: &[Rectangle]) -> bool {
625    rects.windows(2).all(|w| is_identical(w[0], w[1]))
626}
627
628/// Expands the rectangle by the given padding while keeping its center.
629pub fn pad(rect: Rectangle, padding: impl Into<Sides>) -> Rectangle {
630    let p = padding.into();
631    let cx = rect.x + rect.width / 2.0;
632    let cy = rect.y + rect.height / 2.0;
633    let w = rect.width + p.left + p.right;
634    let h = rect.height + p.top + p.bottom;
635    Rectangle {
636        x: cx - w / 2.0,
637        y: cy - h / 2.0,
638        width: w,
639        height: h,
640    }
641}
642
643/// Insets the rectangle by the given margin while keeping its center.
644pub fn inset(rect: Rectangle, margin: impl Into<Sides>) -> Rectangle {
645    let m = margin.into();
646    let cx = rect.x + rect.width / 2.0;
647    let cy = rect.y + rect.height / 2.0;
648    let mut w = rect.width - (m.left + m.right);
649    let mut h = rect.height - (m.top + m.bottom);
650    if w < 0.0 {
651        w = 0.0;
652    }
653    if h < 0.0 {
654        h = 0.0;
655    }
656    Rectangle {
657        x: cx - w / 2.0,
658        y: cy - h / 2.0,
659        width: w,
660        height: h,
661    }
662}
663
664/// Alignment kind for rectangle positioning.
665#[derive(Clone, Copy, Debug, PartialEq, Eq)]
666pub enum AlignKind {
667    None,
668    Min,
669    Max,
670    Center,
671}
672
673/// Horizontal/vertical alignment options.
674#[derive(Clone, Copy, Debug)]
675pub struct Alignment {
676    pub horizontal: AlignKind,
677    pub vertical: AlignKind,
678}
679
680impl Default for Alignment {
681    fn default() -> Self {
682        Self {
683            horizontal: AlignKind::None,
684            vertical: AlignKind::None,
685        }
686    }
687}
688
689/// Aligns rectangles within their bounding box.
690pub fn align(rects: &[Rectangle], options: Alignment) -> Vec<Rectangle> {
691    if rects.len() < 2 {
692        return rects.to_vec();
693    }
694    let bbox = union(rects);
695    rects
696        .iter()
697        .map(|r| {
698            let mut n = *r;
699            match options.horizontal {
700                AlignKind::Min => n.x = bbox.x,
701                AlignKind::Max => n.x = bbox.x + bbox.width - r.width,
702                AlignKind::Center => n.x = bbox.x + (bbox.width - r.width) / 2.0,
703                AlignKind::None => {}
704            }
705            match options.vertical {
706                AlignKind::Min => n.y = bbox.y,
707                AlignKind::Max => n.y = bbox.y + bbox.height - r.height,
708                AlignKind::Center => n.y = bbox.y + (bbox.height - r.height) / 2.0,
709                AlignKind::None => {}
710            }
711            n
712        })
713        .collect()
714}
715
716/// Aligns rectangle `a` relative to rectangle `b`.
717pub fn align_a(a: Rectangle, b: Rectangle, options: Alignment) -> Rectangle {
718    let mut r = a;
719    match options.horizontal {
720        AlignKind::Min => r.x = b.x,
721        AlignKind::Max => r.x = b.x + b.width - a.width,
722        AlignKind::Center => r.x = b.x + (b.width - a.width) / 2.0,
723        AlignKind::None => {}
724    }
725    match options.vertical {
726        AlignKind::Min => r.y = b.y,
727        AlignKind::Max => r.y = b.y + b.height - a.height,
728        AlignKind::Center => r.y = b.y + (b.height - a.height) / 2.0,
729        AlignKind::None => {}
730    }
731    r
732}