Add TileShape and Simplex convex-shape core
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.
This commit is contained in:
@@ -0,0 +1,151 @@
|
||||
"""``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]
|
||||
Reference in New Issue
Block a user