Ports geometry/planar/Polygon.java (corner de-duplication and collinear removal, winding number) and PolygonShape.java, including the recursive split_to_convex that decomposes a simple polygon into convex Simplex tiles by dividing at concave corners along minimal axis-parallel lines. Orientation and convexity tests are exact; the division-point search is approximate (float line evaluation, split point rounded to an integer corner) as upstream. The concave-corner search starts deterministically at corner 0 rather than a seeded PRNG; this only affects which valid decomposition is produced. Supporting additions: Simplex.from_corners (convex polygon to simplex) and Line.function_value_approx / function_in_y_value_approx. Invariant tests over L, plus, staircase and square polygons: tile areas sum to the polygon area (no gaps, no overlap), a point is in the polygon iff in some tile, and no point is strictly inside more than one tile (interiors disjoint). Also covers Polygon normalization and orientation.
302 lines
11 KiB
Python
302 lines
11 KiB
Python
"""``Line`` — a directed line through two integer points.
|
|
|
|
Ports ``geometry/planar/Line.java`` (the subset needed for the router's
|
|
half-plane work). Like the upstream class, a ``Line`` is defined by two
|
|
:class:`~freeroute.geometry.point.IntPoint` endpoints.
|
|
|
|
**Arithmetic model: exact.** :meth:`side_of` uses an exact integer determinant,
|
|
and :meth:`intersection` returns an :class:`~freeroute.geometry.point.IntPoint`
|
|
when the crossing is integral and a
|
|
:class:`~freeroute.geometry.point.RationalPoint` otherwise (``z == 0`` when the
|
|
lines are parallel — the "point at infinity"). :meth:`intersection_approx` is
|
|
the fast ``float`` path.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import math
|
|
|
|
from .direction import Direction
|
|
from .float_point import FloatPoint
|
|
from .point import IntPoint, Point, rational_point
|
|
from .side import Side
|
|
from .vector import IntVector, Vector
|
|
|
|
__all__ = ["Line"]
|
|
|
|
|
|
class Line:
|
|
"""A directed line ``a -> b`` with integer endpoints."""
|
|
|
|
__slots__ = ("a", "b", "_dir")
|
|
|
|
def __init__(self, a: Point, b: Point) -> None:
|
|
self.a = a
|
|
self.b = b
|
|
self._dir: Direction | None = None
|
|
|
|
@staticmethod
|
|
def from_coords(ax: int, ay: int, bx: int, by: int) -> Line:
|
|
return Line(IntPoint(ax, ay), IntPoint(bx, by))
|
|
|
|
@staticmethod
|
|
def from_point_direction(a: Point, direction: Direction) -> Line:
|
|
return Line(a, a.translate_by(direction.get_vector()))
|
|
|
|
def __eq__(self, other: object) -> bool:
|
|
if not isinstance(other, Line):
|
|
return NotImplemented
|
|
if self.side_of(other.a) != Side.COLLINEAR:
|
|
return False
|
|
return self.direction().equals(other.direction())
|
|
|
|
def __hash__(self) -> int:
|
|
return hash((self.a, self.b))
|
|
|
|
def __repr__(self) -> str:
|
|
return f"Line({self.a!r}, {self.b!r})"
|
|
|
|
def direction(self) -> Direction:
|
|
if self._dir is None:
|
|
self._dir = Direction.get_instance(self.b.difference_by(self.a))
|
|
return self._dir
|
|
|
|
def opposite(self) -> Line:
|
|
return Line(self.b, self.a)
|
|
|
|
# --- orientation --------------------------------------------------------
|
|
|
|
def side_of(self, point: Point) -> Side:
|
|
"""Side of this line relative to ``point`` (exact).
|
|
|
|
``ON_THE_LEFT`` if the line passes to the left of the point,
|
|
``COLLINEAR`` if the point lies on the line.
|
|
"""
|
|
return point.side_of(self.a, self.b).negate()
|
|
|
|
def side_of_float(self, point: FloatPoint, tolerance: float = 0.0) -> Side:
|
|
"""Approximate side test for a :class:`FloatPoint` with a tolerance band."""
|
|
ax, ay = self.a.x, self.a.y
|
|
det = (self.b.y - ay) * (point.x - ax) - (self.b.x - ax) * (point.y - ay)
|
|
if det - tolerance > 0:
|
|
return Side.ON_THE_LEFT
|
|
if det + tolerance < 0:
|
|
return Side.ON_THE_RIGHT
|
|
return Side.COLLINEAR
|
|
|
|
def signed_distance(self, point: FloatPoint) -> float:
|
|
ax, ay = self.a.x, self.a.y
|
|
dx = self.b.x - ax
|
|
dy = self.b.y - ay
|
|
det = dy * (point.x - ax) - dx * (point.y - ay)
|
|
return det / math.sqrt(dx * dx + dy * dy)
|
|
|
|
def is_orthogonal(self) -> bool:
|
|
return self.direction().is_orthogonal()
|
|
|
|
def is_diagonal(self) -> bool:
|
|
return self.direction().is_diagonal()
|
|
|
|
def is_multiple_of_45_degree(self) -> bool:
|
|
return self.direction().is_multiple_of_45_degree()
|
|
|
|
def is_parallel(self, other: Line) -> bool:
|
|
return self.direction().side_of(other.direction()) == Side.COLLINEAR
|
|
|
|
def is_perpendicular(self, other: Line) -> bool:
|
|
v1 = self.direction().get_vector()
|
|
v2 = other.direction().get_vector()
|
|
return v1.projection(v2).value == 0
|
|
|
|
def overlaps(self, other: Line) -> bool:
|
|
return self.side_of(other.a) == Side.COLLINEAR and self.side_of(other.b) == Side.COLLINEAR
|
|
|
|
is_equal_or_opposite = overlaps
|
|
|
|
def fast_equals(self, other: Line) -> bool:
|
|
"""Same line and same direction; fast path for integer lines."""
|
|
dx1 = other.a.x - self.a.x
|
|
dy1 = other.a.y - self.a.y
|
|
dx2 = self.b.x - self.a.x
|
|
dy2 = self.b.y - self.a.y
|
|
if dx1 * dy2 - dx2 * dy1 != 0:
|
|
return False
|
|
return self.direction().equals(other.direction())
|
|
|
|
# --- translation --------------------------------------------------------
|
|
|
|
def translate_by(self, vector: Vector) -> Line:
|
|
if vector.is_zero():
|
|
return self
|
|
return Line(self.a.translate_by(vector), self.b.translate_by(vector))
|
|
|
|
def translate(self, dist: float) -> Line:
|
|
"""Translate the line perpendicular by ``dist`` (left if positive).
|
|
|
|
Ports ``Line.translate`` — approximate (the offset endpoint is rounded to
|
|
integer coordinates), used by ``Simplex.offset``.
|
|
"""
|
|
v = self.direction().get_vector()
|
|
vxvx = v.x * v.x
|
|
vyvy = v.y * v.y
|
|
length = math.sqrt(vxvx + vyvy)
|
|
if vxvx <= vyvy:
|
|
rel_x = _round_half_up(dist * length / v.y)
|
|
new_a = IntPoint(self.a.x - rel_x, self.a.y)
|
|
else:
|
|
rel_y = _round_half_up(dist * length / v.x)
|
|
new_a = IntPoint(self.a.x, self.a.y + rel_y)
|
|
return Line.from_point_direction(new_a, self.direction())
|
|
|
|
# --- ordering (by direction angle) -------------------------------------
|
|
|
|
def compare_to(self, other: Line) -> int:
|
|
"""Order lines by the angle of their direction with the +x axis (exact).
|
|
|
|
Ports ``Line.compareTo`` — the ordering ``Simplex`` relies on to sort its
|
|
half-plane border lines counter-clockwise.
|
|
"""
|
|
dx1 = self.b.x - self.a.x
|
|
dy1 = self.b.y - self.a.y
|
|
dx2 = other.b.x - other.a.x
|
|
dy2 = other.b.y - other.a.y
|
|
if dy1 > 0:
|
|
if dy2 < 0:
|
|
return -1
|
|
if dy2 == 0:
|
|
return 1 if dx2 > 0 else -1
|
|
elif dy1 < 0:
|
|
if dy2 >= 0:
|
|
return 1
|
|
else: # dy1 == 0
|
|
if dx1 > 0:
|
|
if dy2 != 0 or dx2 < 0:
|
|
return -1
|
|
return 0
|
|
if dy2 > 0 or (dy2 == 0 and dx2 > 0):
|
|
return 1
|
|
if dy2 < 0:
|
|
return -1
|
|
return 0
|
|
determinant = dx2 * dy1 - dy2 * dx1
|
|
if determinant > 0:
|
|
return 1
|
|
if determinant < 0:
|
|
return -1
|
|
return 0
|
|
|
|
def __lt__(self, other: Line) -> bool:
|
|
return self.compare_to(other) < 0
|
|
|
|
def side_of_intersection(self, line_1: Line, line_2: Line) -> Side:
|
|
"""Side of this line relative to the intersection of ``line_1`` and
|
|
``line_2`` (exact fallback after a fast float test). Ports
|
|
``Line.side_of_intersection``."""
|
|
approx = line_1.intersection_approx(line_2)
|
|
result = self.side_of_float(approx, 1.0)
|
|
if result == Side.COLLINEAR:
|
|
result = self.side_of(line_1.intersection(line_2))
|
|
return result
|
|
|
|
def function_value_approx(self, x: float) -> float:
|
|
"""Approximate ``y`` on this (non-vertical) line at abscissa ``x``."""
|
|
p1 = self.a.to_float()
|
|
p2 = self.b.to_float()
|
|
dx = p2.x - p1.x
|
|
if dx == 0:
|
|
return 0.0
|
|
dy = p2.y - p1.y
|
|
det = p1.x * p2.y - p2.x * p1.y
|
|
return (dy * x - det) / dx
|
|
|
|
def function_in_y_value_approx(self, y: float) -> float:
|
|
"""Approximate ``x`` on this (non-horizontal) line at ordinate ``y``."""
|
|
p1 = self.a.to_float()
|
|
p2 = self.b.to_float()
|
|
dy = p2.y - p1.y
|
|
if dy == 0:
|
|
return 0.0
|
|
dx = p2.x - p1.x
|
|
det = p1.x * p2.y - p2.x * p1.y
|
|
return (dx * y + det) / dy
|
|
|
|
# --- intersection (exact) ----------------------------------------------
|
|
|
|
def intersection(self, other: Line) -> Point:
|
|
"""Exact intersection point; ``result.is_infinite()`` iff parallel."""
|
|
a = self.a
|
|
b = self.b
|
|
oa = other.a
|
|
ob = other.b
|
|
delta_1 = b.difference_by(a)
|
|
delta_2 = ob.difference_by(oa)
|
|
if not isinstance(delta_1, IntVector) or not isinstance(delta_2, IntVector):
|
|
raise NotImplementedError("Line.intersection only supports integer lines")
|
|
|
|
fast = _fast_intersection(a, oa, delta_1, delta_2)
|
|
if fast is not None:
|
|
return fast
|
|
|
|
det_1 = a.determinant(b)
|
|
det_2 = oa.determinant(ob)
|
|
det = delta_2.determinant(delta_1)
|
|
is_x = det_1 * delta_2.x - det_2 * delta_1.x
|
|
is_y = det_1 * delta_2.y - det_2 * delta_1.y
|
|
if det == 0:
|
|
return rational_point(is_x, is_y, 0)
|
|
return rational_point(is_x, is_y, det)
|
|
|
|
def intersection_approx(self, other: Line) -> FloatPoint:
|
|
"""Fast ``float`` intersection; parallel lines give a huge coordinate."""
|
|
a, b = self.a, self.b
|
|
oa, ob = other.a, other.b
|
|
d1x = b.x - a.x
|
|
d1y = b.y - a.y
|
|
d2x = ob.x - oa.x
|
|
d2y = ob.y - oa.y
|
|
det_1 = a.x * b.y - a.y * b.x
|
|
det_2 = oa.x * ob.y - oa.y * ob.x
|
|
det = d2x * d1y - d2y * d1x
|
|
if det == 0:
|
|
big = float(2**31 - 1)
|
|
return FloatPoint(big, big)
|
|
return FloatPoint(
|
|
(d2x * det_1 - d1x * det_2) / det,
|
|
(d2y * det_1 - d1y * det_2) / det,
|
|
)
|
|
|
|
|
|
def _round_half_up(value: float) -> int:
|
|
return math.floor(value + 0.5)
|
|
|
|
|
|
def _fast_intersection(
|
|
a: IntPoint, oa: IntPoint, delta_1: IntVector, delta_2: IntVector
|
|
) -> Point | None:
|
|
"""Closed-form intersection for the orthogonal/45-degree combinations."""
|
|
if delta_1.x == 0: # this line vertical
|
|
if delta_2.y == 0: # other horizontal
|
|
return IntPoint(a.x, oa.y)
|
|
if delta_2.x == delta_2.y: # other right diagonal
|
|
return IntPoint(a.x, oa.y + a.x - oa.x)
|
|
if delta_2.x == -delta_2.y: # other left diagonal
|
|
return IntPoint(a.x, oa.y + oa.x - a.x)
|
|
elif delta_1.y == 0: # this line horizontal
|
|
if delta_2.x == 0: # other vertical
|
|
return IntPoint(oa.x, a.y)
|
|
if delta_2.x == delta_2.y: # other right diagonal
|
|
return IntPoint(oa.x + a.y - oa.y, a.y)
|
|
if delta_2.x == -delta_2.y: # other left diagonal
|
|
return IntPoint(oa.x + oa.y - a.y, a.y)
|
|
elif delta_1.x == delta_1.y: # this right diagonal
|
|
if delta_2.x == 0: # other vertical
|
|
return IntPoint(oa.x, a.y + oa.x - a.x)
|
|
if delta_2.y == 0: # other horizontal
|
|
return IntPoint(a.x + oa.y - a.y, oa.y)
|
|
elif delta_1.x == -delta_1.y: # this left diagonal
|
|
if delta_2.x == 0: # other vertical
|
|
return IntPoint(oa.x, a.y + a.x - oa.x)
|
|
if delta_2.y == 0: # other horizontal
|
|
return IntPoint(a.x + a.y - oa.y, oa.y)
|
|
return None
|