"""``TileShape`` — abstract convex shape bounded by straight border lines. Ports the concrete (non-abstract) behaviour of ``geometry/planar/TileShape.java`` that is defined purely in terms of the border lines and corners: point containment, area, centre of gravity, and border tests. Concrete subclasses (:class:`~freeroute.geometry.simplex.Simplex`) supply ``border_line_count``, ``border_line``, ``corner`` and ``corner_approx``. **Arithmetic model:** containment and ``contains_on_border`` are **exact** (they use the exact :meth:`~freeroute.geometry.line.Line.side_of`); ``area`` and ``centre_of_gravity`` are approximate (``float``), as upstream. A convex tile is the intersection of the right half-planes of its directed border lines: a point is inside-or-on-border iff it is not on the strict left of any border line, and strictly inside iff it is on the strict right of every one. """ from __future__ import annotations from .float_point import FloatPoint from .point import Point from .side import Side __all__ = ["TileShape"] class TileShape: """Abstract convex shape; subclasses provide the border-line accessors.""" # --- accessors subclasses must provide --------------------------------- def border_line_count(self) -> int: # pragma: no cover - abstract raise NotImplementedError def border_line(self, no: int): # pragma: no cover - abstract raise NotImplementedError def corner(self, no: int) -> Point: # pragma: no cover - abstract raise NotImplementedError def corner_approx(self, no: int) -> FloatPoint: return self.corner(no).to_float() def corner_is_bounded(self, no: int) -> bool: # pragma: no cover - abstract raise NotImplementedError def is_empty(self) -> bool: # pragma: no cover - abstract raise NotImplementedError def is_bounded(self) -> bool: # pragma: no cover - abstract raise NotImplementedError def dimension(self) -> int: # pragma: no cover - abstract raise NotImplementedError # --- point containment (exact) ----------------------------------------- def is_outside(self, point: Point) -> bool: """True if ``point`` is neither inside nor on the border.""" line_count = self.border_line_count() if line_count == 0: return True for i in range(line_count): if self.border_line(i).side_of(point) == Side.ON_THE_LEFT: return True return False def contains(self, point: Point) -> bool: """True if ``point`` is inside or on the border (exact).""" return not self.is_outside(point) def contains_inside(self, point: Point) -> bool: """True if ``point`` is strictly interior (on the right of every line).""" line_count = self.border_line_count() if line_count == 0: return False for i in range(line_count): if self.border_line(i).side_of(point) != Side.ON_THE_RIGHT: return False return True def contains_on_border_line_no(self, point: Point) -> int: """Index of a border line containing ``point``, or -1 if not on border.""" line_count = self.border_line_count() if line_count == 0: return -1 containing = -1 for i in range(line_count): side = self.border_line(i).side_of(point) if side == Side.ON_THE_LEFT: return -1 if side == Side.COLLINEAR: containing = i return containing def contains_on_border(self, point: Point) -> bool: return self.contains_on_border_line_no(point) >= 0 def contains_tile(self, other: TileShape) -> bool: """True if every corner of ``other`` is contained in this shape.""" return all(self.contains(other.corner(i)) for i in range(other.border_line_count())) # --- measures (approximate) -------------------------------------------- def centre_of_gravity(self) -> FloatPoint: """Arithmetic mean of the (approximate) corners.""" n = self.border_line_count() x = 0.0 y = 0.0 for i in range(n): c = self.corner_approx(i) x += c.x y += c.y return FloatPoint(x / n, y / n) def area(self) -> float: """Absolute polygon area (0 if degenerate, ``inf`` if unbounded).""" if not self.is_bounded(): return float("inf") if self.dimension() < 2: return 0.0 n = self.border_line_count() result = 0.0 prev_corner = self.corner_approx(n - 2) curr_corner = self.corner_approx(n - 1) for i in range(n): next_corner = self.corner_approx(i) result += curr_corner.x * (next_corner.y - prev_corner.y) prev_corner = curr_corner curr_corner = next_corner return 0.5 * abs(result) def circumference(self) -> float: if not self.is_bounded(): return float("inf") n = self.border_line_count() result = 0.0 prev_corner = self.corner_approx(n - 1) for i in range(n): curr_corner = self.corner_approx(i) result += curr_corner.distance(prev_corner) prev_corner = curr_corner return result # --- convex decomposition (trivial for a tile) ------------------------- def split_to_convex(self) -> list[TileShape]: """A convex tile is already convex — returns ``[self]``.""" return [self]