150 lines
5.4 KiB
Python
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]
|