Ports geometry/planar/TileShape.java (the border-line-based containment, area, and centre-of-gravity logic) and Simplex.java (a convex region as the intersection of directed half-planes). Corners are exact intersections of consecutive border lines; point containment uses exact side_of. The remove_redundant_lines normalization — dropping lines that do not contribute and detecting emptiness — is ported line-for-line. Supporting additions: Line.compare_to/__lt__ (angular sort order), Line.fast_equals, Line.side_of_intersection, Line.translate (perpendicular offset), IntDirection.determinant, and IntBox.to_simplex. offset is approximate (rounded translated lines, as upstream); enlarge clips to the enlarged bounding box pending the IntOctagon port. Since there is no JVM oracle, tests assert invariants: corners lie exactly on their border lines (exact side_of == 0), IntBox -> Simplex preserves the region over a sampled grid, intersection is contained in both operands and a point is in the result iff in both, and get_instance normalization drops redundant lines and detects empty half-plane pairs.
256 lines
8.4 KiB
Python
256 lines
8.4 KiB
Python
"""``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)
|