Ports the point/vector foundation of geometry/planar: Side and Signum (three-valued signs), Limits, FloatPoint (approximate), and the exact IntPoint/IntVector plus projective RationalPoint/RationalVector. Arithmetic model per type is documented in each module. The key simplification over the Java source: Python's unbounded int makes the exact orientation determinants and rational (x,y,z) coordinates trivial, so side_of is kept exact (upstream uses a double for speed) and no BigInteger or CRIT_INT overflow promotion is needed. Rational points use the projective triple with z=0 denoting the point at infinity. Tests cover collinearity, determinants beyond Java long range, rational equality/reduction, and integer/rational promotion.
237 lines
7.3 KiB
Python
237 lines
7.3 KiB
Python
"""Points: ``Point`` base, ``IntPoint``, ``RationalPoint``, and factories.
|
|
|
|
Ports ``geometry/planar/{Point,IntPoint,RationalPoint}.java``.
|
|
|
|
**Arithmetic model:**
|
|
|
|
* :class:`IntPoint` — exact, Python ``int`` ``(x, y)``.
|
|
* :class:`RationalPoint` — exact, projective ``(x, y, z)`` Python ``int``
|
|
representing the affine point ``(x/z, y/z)``. ``z == 0`` is a point at
|
|
infinity (the intersection of parallel lines). ``z`` is kept non-negative.
|
|
|
|
Two points of different concrete type are compared/combined by promoting the
|
|
integer one to the rational representation, exactly as the Java double-dispatch
|
|
does — but with unbounded ``int`` instead of ``BigInteger``.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import math
|
|
|
|
from .float_point import FloatPoint
|
|
from .side import Side
|
|
from .vector import IntVector, RationalVector, Vector, _add_rational
|
|
|
|
__all__ = ["Point", "IntPoint", "RationalPoint", "point", "rational_point", "ZERO_POINT"]
|
|
|
|
|
|
def _xyz(p: Point) -> tuple[int, int, int]:
|
|
if isinstance(p, IntPoint):
|
|
return (p.x, p.y, 1)
|
|
return (p.x, p.y, p.z) # RationalPoint
|
|
|
|
|
|
class Point:
|
|
"""Abstract base for exact plane points."""
|
|
|
|
def to_float(self) -> FloatPoint: # pragma: no cover - overridden
|
|
raise NotImplementedError
|
|
|
|
def is_infinite(self) -> bool: # pragma: no cover - overridden
|
|
raise NotImplementedError
|
|
|
|
def translate_by(self, vector: Vector) -> Point:
|
|
if vector.is_zero():
|
|
return self
|
|
return vector.add_to_point(self)
|
|
|
|
def difference_by(self, other: Point) -> Vector:
|
|
"""Return the vector ``self - other`` (exact)."""
|
|
if isinstance(self, IntPoint) and isinstance(other, IntPoint):
|
|
return IntVector(self.x - other.x, self.y - other.y)
|
|
sx, sy, sz = _xyz(self)
|
|
ox, oy, oz = _xyz(other)
|
|
x, y, z = _add_rational((sx, sy, sz), (-ox, -oy, oz))
|
|
return RationalVector(x, y, z)
|
|
|
|
def side_of(self, p1: Point, p2: Point) -> Side:
|
|
"""Side of ``self`` relative to the directed line ``p1 -> p2``."""
|
|
v1 = self.difference_by(p1)
|
|
v2 = p2.difference_by(p1)
|
|
return v1.side_of(v2)
|
|
|
|
def side_of_line(self, line) -> Side:
|
|
return self.side_of(line.a, line.b)
|
|
|
|
def compare_x(self, other: Point) -> int:
|
|
sx, _, sz = _xyz(self)
|
|
ox, _, oz = _xyz(other)
|
|
return _sign(sx * oz - ox * sz)
|
|
|
|
def compare_y(self, other: Point) -> int:
|
|
_, sy, sz = _xyz(self)
|
|
_, oy, oz = _xyz(other)
|
|
return _sign(sy * oz - oy * sz)
|
|
|
|
def compare_x_y(self, other: Point) -> int:
|
|
result = self.compare_x(other)
|
|
if result == 0:
|
|
result = self.compare_y(other)
|
|
return result
|
|
|
|
def turn_90_degree(self, factor: int, pole: Point) -> Point:
|
|
v = self.difference_by(pole).turn_90_degree(factor)
|
|
return pole.translate_by(v)
|
|
|
|
def mirror_vertical(self, pole: Point) -> Point:
|
|
v = self.difference_by(pole).mirror_at_y_axis()
|
|
return pole.translate_by(v)
|
|
|
|
def mirror_horizontal(self, pole: Point) -> Point:
|
|
v = self.difference_by(pole).mirror_at_x_axis()
|
|
return pole.translate_by(v)
|
|
|
|
|
|
class IntPoint(Point):
|
|
"""A point with exact integer coordinates."""
|
|
|
|
__slots__ = ("x", "y")
|
|
|
|
def __init__(self, x: int, y: int) -> None:
|
|
self.x = int(x)
|
|
self.y = int(y)
|
|
|
|
def __eq__(self, other: object) -> bool:
|
|
if not isinstance(other, IntPoint):
|
|
return NotImplemented
|
|
return self.x == other.x and self.y == other.y
|
|
|
|
def __hash__(self) -> int:
|
|
return hash((self.x, self.y))
|
|
|
|
def __repr__(self) -> str:
|
|
return f"IntPoint({self.x}, {self.y})"
|
|
|
|
def to_float(self) -> FloatPoint:
|
|
return FloatPoint(self.x, self.y)
|
|
|
|
def is_infinite(self) -> bool:
|
|
return False
|
|
|
|
def get_id_no(self) -> int:
|
|
return 31 * self.x + self.y
|
|
|
|
def determinant(self, other: IntPoint) -> int:
|
|
"""Exact determinant of the position vectors of ``self`` and ``other``."""
|
|
return self.x * other.y - self.y * other.x
|
|
|
|
def distance_square(self, other: IntPoint) -> float:
|
|
dx = other.x - self.x
|
|
dy = other.y - self.y
|
|
return float(dx * dx + dy * dy)
|
|
|
|
def distance(self, other: IntPoint) -> float:
|
|
return math.sqrt(self.distance_square(other))
|
|
|
|
def surrounding_box(self):
|
|
from .box import IntBox
|
|
|
|
return IntBox(self.x, self.y, self.x, self.y)
|
|
|
|
def is_contained_in(self, box) -> bool:
|
|
return box.ll.x <= self.x <= box.ur.x and box.ll.y <= self.y <= box.ur.y
|
|
|
|
|
|
class RationalPoint(Point):
|
|
"""A point with exact rational coordinates ``(x/z, y/z)``, ``z`` >= 0."""
|
|
|
|
__slots__ = ("x", "y", "z")
|
|
|
|
def __init__(self, x: int, y: int, z: int) -> None:
|
|
if z < 0:
|
|
raise ValueError("RationalPoint: z is expected to be >= 0")
|
|
self.x = int(x)
|
|
self.y = int(y)
|
|
self.z = int(z)
|
|
|
|
@staticmethod
|
|
def from_int_point(p: IntPoint) -> RationalPoint:
|
|
return RationalPoint(p.x, p.y, 1)
|
|
|
|
def __eq__(self, other: object) -> bool:
|
|
if not isinstance(other, RationalPoint):
|
|
return NotImplemented
|
|
return self.x * other.z - other.x * self.z == 0 and self.y * other.z - other.y * self.z == 0
|
|
|
|
def __hash__(self) -> int:
|
|
if self.z != 0:
|
|
g = math.gcd(math.gcd(abs(self.x), abs(self.y)), self.z) or 1
|
|
return hash((self.x // g, self.y // g, self.z // g))
|
|
return hash((self.x, self.y, 0))
|
|
|
|
def __repr__(self) -> str:
|
|
return f"RationalPoint({self.x}, {self.y}, {self.z})"
|
|
|
|
def to_float(self) -> FloatPoint:
|
|
if self.z == 0:
|
|
big = 3.4e38
|
|
return FloatPoint(big, big)
|
|
return FloatPoint(self.x / self.z, self.y / self.z)
|
|
|
|
def is_infinite(self) -> bool:
|
|
return self.z == 0
|
|
|
|
def is_contained_in(self, box) -> bool:
|
|
# Exact: compare numerators against z * boundary (z >= 0).
|
|
if self.x < box.ll.x * self.z:
|
|
return False
|
|
if self.y < box.ll.y * self.z:
|
|
return False
|
|
if self.x > box.ur.x * self.z:
|
|
return False
|
|
return self.y <= box.ur.y * self.z
|
|
|
|
def surrounding_box(self):
|
|
from .box import IntBox
|
|
|
|
fp = self.to_float()
|
|
return IntBox(math.floor(fp.x), math.floor(fp.y), math.ceil(fp.x), math.ceil(fp.y))
|
|
|
|
|
|
ZERO_POINT = IntPoint(0, 0)
|
|
|
|
|
|
# --- factories (Point.get_instance) -----------------------------------------
|
|
|
|
|
|
def point(x: int, y: int) -> IntPoint:
|
|
"""Create an :class:`IntPoint`.
|
|
|
|
FreeRouting promotes to ``RationalPoint`` past ``CRIT_INT`` to avoid ``long``
|
|
overflow; unbounded Python ``int`` makes that unnecessary, so an integer
|
|
coordinate always yields an :class:`IntPoint`.
|
|
"""
|
|
return IntPoint(x, y)
|
|
|
|
|
|
def rational_point(x: int, y: int, z: int) -> Point:
|
|
"""Create a point from projective coordinates, reducing to integer when exact.
|
|
|
|
Mirrors ``Point.get_instance(BigInteger, BigInteger, BigInteger)``: a
|
|
negative denominator is normalized, and if ``z`` divides both numerators the
|
|
result collapses to an :class:`IntPoint`.
|
|
"""
|
|
if z < 0:
|
|
x, y, z = -x, -y, -z
|
|
if z != 0 and x % z == 0 and y % z == 0:
|
|
return IntPoint(x // z, y // z)
|
|
return RationalPoint(x, y, z)
|
|
|
|
|
|
def _sign(value: int) -> int:
|
|
if value > 0:
|
|
return 1
|
|
if value < 0:
|
|
return -1
|
|
return 0
|