"""``IntBox`` — an axis-parallel rectangle with integer corners. Ports ``geometry/planar/IntBox.java``, the simplest concrete convex tile (``RegularTileShape``) and the one the router leans on most for bounding-box queries. The general convex machinery it also participates in — ``TileShape`` / ``Simplex`` / ``IntOctagon`` and ``split_to_convex`` — is deferred to the next geometry phase; the operations here (contains, intersection, union, offset, dimension, containment) are self-contained and exact. **Arithmetic model: exact, Python ``int`` corners.** """ from __future__ import annotations import math from .limits import CRIT_INT from .point import IntPoint, Point __all__ = ["IntBox"] def _round_half_up(value: float) -> int: return math.floor(value + 0.5) class IntBox: """The rectangle ``[ll.x, ur.x] x [ll.y, ur.y]`` with integer corners.""" __slots__ = ("ll", "ur") def __init__(self, *args) -> None: if len(args) == 2: ll, ur = args self.ll: IntPoint = ll self.ur: IntPoint = ur elif len(args) == 4: llx, lly, urx, ury = args self.ll = IntPoint(llx, lly) self.ur = IntPoint(urx, ury) else: # pragma: no cover - misuse guard raise TypeError("IntBox expects (ll, ur) or (llx, lly, urx, ury)") def __eq__(self, other: object) -> bool: if not isinstance(other, IntBox): return NotImplemented return self.ll == other.ll and self.ur == other.ur def __hash__(self) -> int: return hash((self.ll, self.ur)) def __repr__(self) -> str: return f"IntBox({self.ll.x}, {self.ll.y}, {self.ur.x}, {self.ur.y})" # --- basic measures ----------------------------------------------------- def is_empty(self) -> bool: return self.ll.x > self.ur.x or self.ll.y > self.ur.y def width(self) -> int: return self.ur.x - self.ll.x def height(self) -> int: return self.ur.y - self.ll.y def max_width(self) -> int: return max(self.ur.x - self.ll.x, self.ur.y - self.ll.y) def min_width(self) -> int: return min(self.ur.x - self.ll.x, self.ur.y - self.ll.y) def area(self) -> float: return float((self.ur.x - self.ll.x) * (self.ur.y - self.ll.y)) def circumference(self) -> int: return 2 * ((self.ur.x - self.ll.x) + (self.ur.y - self.ll.y)) def border_line_count(self) -> int: return 4 def corner(self, no: int) -> IntPoint: """Corners counter-clockwise from the lower-left (``no`` in 0..3).""" if no == 0: return self.ll if no == 1: return IntPoint(self.ur.x, self.ll.y) if no == 2: return self.ur if no == 3: return IntPoint(self.ll.x, self.ur.y) raise ValueError("IntBox.corner: no out of range") def corners(self) -> list[IntPoint]: return [self.corner(i) for i in range(4)] def dimension(self) -> int: """-1 empty, 0 a point, 1 a segment, 2 a proper rectangle.""" if self.is_empty(): return -1 if self.ll == self.ur: return 0 if self.ur.x == self.ll.x or self.ll.y == self.ur.y: return 1 return 2 # --- containment -------------------------------------------------------- def contains(self, point: Point) -> bool: """True if ``point`` is inside or on the border (exact for any Point).""" return point.is_contained_in(self) def contains_inside(self, point: IntPoint) -> bool: """True if ``point`` is strictly in the interior.""" return self.ll.x < point.x < self.ur.x and self.ll.y < point.y < self.ur.y def is_contained_in(self, other: IntBox) -> bool: if self.is_empty() or self is other: return True return ( self.ll.x >= other.ll.x and self.ll.y >= other.ll.y and self.ur.x <= other.ur.x and self.ur.y <= other.ur.y ) def contains_box(self, other: IntBox) -> bool: return other.is_contained_in(self) def contains_in_interior(self, other: IntBox) -> bool: if other.is_empty(): return True return ( other.ll.x > self.ll.x and other.ll.y > self.ll.y and other.ur.x < self.ur.x and other.ur.y < self.ur.y ) # --- set operations ----------------------------------------------------- def intersection(self, other: IntBox) -> IntBox: if ( other.ll.x > self.ur.x or other.ll.y > self.ur.y or self.ll.x > other.ur.x or self.ll.y > other.ur.y ): return IntBox.empty() return IntBox( max(self.ll.x, other.ll.x), max(self.ll.y, other.ll.y), min(self.ur.x, other.ur.x), min(self.ur.y, other.ur.y), ) def union(self, other: IntBox) -> IntBox: return IntBox( min(self.ll.x, other.ll.x), min(self.ll.y, other.ll.y), max(self.ur.x, other.ur.x), max(self.ur.y, other.ur.y), ) def intersects(self, other: IntBox) -> bool: return not ( other.ll.x > self.ur.x or other.ll.y > self.ur.y or self.ll.x > other.ur.x or self.ll.y > other.ur.y ) def overlaps(self, other: IntBox) -> bool: """True if the intersection is 2-dimensional (open-interval overlap).""" return not ( other.ll.x >= self.ur.x or other.ll.y >= self.ur.y or self.ll.x >= other.ur.x or self.ll.y >= other.ur.y ) # --- offset / shrink ---------------------------------------------------- def offset(self, dist: float) -> IntBox: """Expand (``dist > 0``) or contract every side by ``dist``.""" if dist == 0 or self.is_empty(): return self d = _round_half_up(dist) return IntBox(self.ll.x - d, self.ll.y - d, self.ur.x + d, self.ur.y + d) def horizontal_offset(self, dist: float) -> IntBox: if dist == 0 or self.is_empty(): return self d = _round_half_up(dist) return IntBox(self.ll.x - d, self.ll.y, self.ur.x + d, self.ur.y) def vertical_offset(self, dist: float) -> IntBox: if dist == 0 or self.is_empty(): return self d = _round_half_up(dist) return IntBox(self.ll.x, self.ll.y - d, self.ur.x, self.ur.y + d) def shrink(self, width: int) -> IntBox: """Shrink each side by ``width``; the box collapses to its center, not past.""" if 2 * width <= self.ur.x - self.ll.x: ll_x = self.ll.x + width ur_x = self.ur.x - width else: ll_x = ur_x = (self.ll.x + self.ur.x) // 2 if 2 * width <= self.ur.y - self.ll.y: ll_y = self.ll.y + width ur_y = self.ur.y - width else: ll_y = ur_y = (self.ll.y + self.ur.y) // 2 return IntBox(ll_x, ll_y, ur_x, ur_y) # --- misc --------------------------------------------------------------- def bounding_box(self) -> IntBox: return self def translate_by(self, vector) -> IntBox: if vector.is_zero(): return self new_ll = self.ll.translate_by(vector) new_ur = self.ur.translate_by(vector) return IntBox(new_ll, new_ur) def to_float_corners(self): from .float_point import FloatPoint return [FloatPoint(c.x, c.y) for c in self.corners()] def to_simplex(self): """Return this box as a :class:`~freeroute.geometry.simplex.Simplex`. Four directed border lines whose common right side is the box interior (ports ``IntBox.to_Simplex``). """ from .direction import IntDirection from .line import Line from .simplex import Simplex if self.is_empty(): return Simplex(()) lines = ( Line.from_point_direction(self.ll, IntDirection(1, 0)), # bottom, +x Line.from_point_direction(self.ur, IntDirection(0, 1)), # right, +y Line.from_point_direction(self.ur, IntDirection(-1, 0)), # top, -x Line.from_point_direction(self.ll, IntDirection(0, -1)), # left, -y ) return Simplex(lines) @staticmethod def empty() -> IntBox: return IntBox(CRIT_INT, CRIT_INT, -CRIT_INT, -CRIT_INT)