"""Rect2 — 2D axis-aligned bounding rectangle."""
from __future__ import annotations
from .types import Vec2
# ============================================================================
# Rect2
# ============================================================================
[docs]
class Rect2:
"""2D axis-aligned bounding rectangle."""
__slots__ = ("x", "y", "width", "height")
def __init__(self, x=0.0, y=0.0, width=0.0, height=0.0):
self.x = float(x)
self.y = float(y)
self.width = float(width)
self.height = float(height)
@property
def position(self) -> tuple[float, float]:
return (self.x, self.y)
@property
def size(self) -> tuple[float, float]:
return (self.width, self.height)
@property
def end(self) -> tuple[float, float]:
return (self.x + self.width, self.y + self.height)
@property
def area(self) -> float:
return abs(self.width * self.height)
@property
def center(self) -> tuple[float, float]:
return (self.x + self.width / 2, self.y + self.height / 2)
[docs]
def contains_point(self, x: float, y: float) -> bool:
"""Return True if (x, y) lies inside (or on the boundary of) this rect."""
r = self.abs()
return r.x <= x <= r.x + r.width and r.y <= y <= r.y + r.height
[docs]
def intersects(self, other: Rect2) -> bool:
"""Return True if this rect overlaps with other."""
a, b = self.abs(), other.abs()
return not (a.x + a.width < b.x or b.x + b.width < a.x or a.y + a.height < b.y or b.y + b.height < a.y)
[docs]
def intersection(self, other: Rect2) -> Rect2:
"""Return the overlapping area, or a zero rect if none."""
a, b = self.abs(), other.abs()
x1 = max(a.x, b.x)
y1 = max(a.y, b.y)
x2 = min(a.x + a.width, b.x + b.width)
y2 = min(a.y + a.height, b.y + b.height)
if x2 < x1 or y2 < y1:
return Rect2()
return Rect2(x1, y1, x2 - x1, y2 - y1)
[docs]
def merge(self, other: Rect2) -> Rect2:
"""Return the smallest rect enclosing both rects (union)."""
a, b = self.abs(), other.abs()
x1 = min(a.x, b.x)
y1 = min(a.y, b.y)
x2 = max(a.x + a.width, b.x + b.width)
y2 = max(a.y + a.height, b.y + b.height)
return Rect2(x1, y1, x2 - x1, y2 - y1)
[docs]
def expand(self, point) -> Rect2:
"""Return a rect expanded to include the given point (x, y) or Vec2."""
if isinstance(point, Vec2):
px, py = point.x, point.y
else:
px, py = float(point[0]), float(point[1])
r = self.abs()
x1 = min(r.x, px)
y1 = min(r.y, py)
x2 = max(r.x + r.width, px)
y2 = max(r.y + r.height, py)
return Rect2(x1, y1, x2 - x1, y2 - y1)
[docs]
def grow(self, amount: float) -> Rect2:
"""Return a rect grown by amount on each side."""
return Rect2(self.x - amount, self.y - amount, self.width + 2 * amount, self.height + 2 * amount)
[docs]
def abs(self) -> Rect2:
"""Return a rect with guaranteed positive width/height."""
x, y, w, h = self.x, self.y, self.width, self.height
if w < 0:
x, w = x + w, -w
if h < 0:
y, h = y + h, -h
return Rect2(x, y, w, h)
[docs]
def __eq__(self, other):
if isinstance(other, Rect2):
return self.x == other.x and self.y == other.y and self.width == other.width and self.height == other.height
return NotImplemented
[docs]
def __repr__(self):
return f"Rect2({self.x:.4g}, {self.y:.4g}, {self.width:.4g}, {self.height:.4g})"