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