Files
rpmroute/src/freeroute/geometry/tile.py
T

150 lines
5.4 KiB
Python

"""``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]