Ports Direction/IntDirection (equivalence classes of vectors, gcd- normalized, exact angular compare) and Line. Line.side_of uses an exact integer determinant; Line.intersection returns an IntPoint when the crossing is integral and a RationalPoint otherwise, with the orthogonal and 45-degree fast paths from the source preserved. Parallel lines yield a point at infinity (z=0). BigIntDirection is folded into IntDirection since unbounded int always fits. Tests cover integral and rational intersections, parallel-line infinity, exactness beyond double precision (verified via exact collinearity of the result), and direction normalization/ordering.
153 lines
4.6 KiB
Python
153 lines
4.6 KiB
Python
"""Directions: ``Direction`` base and ``IntDirection``.
|
|
|
|
Ports ``geometry/planar/{Direction,IntDirection}.java``. A direction is an
|
|
equivalence class of vectors pointing the same way — FreeRouting prefers these
|
|
over angles because angle arithmetic is inexact.
|
|
|
|
**Arithmetic model: exact, Python ``int``.** ``BigIntDirection`` is folded away:
|
|
with unbounded ``int`` a normalized direction always fits in
|
|
:class:`IntDirection`. The angular :meth:`IntDirection.compare_to` uses an exact
|
|
integer determinant (upstream uses a ``double`` for speed).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from .side import Side, Signum
|
|
from .vector import IntVector, Vector
|
|
|
|
__all__ = ["Direction", "IntDirection"]
|
|
|
|
|
|
class Direction:
|
|
"""Abstract base for plane directions."""
|
|
|
|
def get_vector(self) -> Vector: # pragma: no cover - overridden
|
|
raise NotImplementedError
|
|
|
|
def is_orthogonal(self) -> bool: # pragma: no cover - overridden
|
|
raise NotImplementedError
|
|
|
|
def is_diagonal(self) -> bool: # pragma: no cover - overridden
|
|
raise NotImplementedError
|
|
|
|
def is_multiple_of_45_degree(self) -> bool:
|
|
return self.is_orthogonal() or self.is_diagonal()
|
|
|
|
def turn_45_degree(self, factor: int) -> Direction: # pragma: no cover
|
|
raise NotImplementedError
|
|
|
|
def opposite(self) -> Direction: # pragma: no cover - overridden
|
|
raise NotImplementedError
|
|
|
|
def side_of(self, other: Direction) -> Side:
|
|
return self.get_vector().side_of(other.get_vector())
|
|
|
|
def projection(self, other: Direction) -> Signum:
|
|
return self.get_vector().projection(other.get_vector())
|
|
|
|
def compare_to(self, other: Direction) -> int: # pragma: no cover - overridden
|
|
raise NotImplementedError
|
|
|
|
def equals(self, other: Direction | None) -> bool:
|
|
if self is other:
|
|
return True
|
|
if other is None:
|
|
return False
|
|
if self.side_of(other) != Side.COLLINEAR:
|
|
return False
|
|
# collinear — reject the opposite direction.
|
|
return self.get_vector().projection(other.get_vector()) == Signum.POSITIVE
|
|
|
|
@staticmethod
|
|
def get_instance(vector: Vector) -> Direction:
|
|
return vector.to_normalized_direction()
|
|
|
|
@staticmethod
|
|
def from_points(p_from, p_to) -> Direction | None:
|
|
if p_from == p_to:
|
|
return None
|
|
return Direction.get_instance(p_to.difference_by(p_from))
|
|
|
|
|
|
class IntDirection(Direction):
|
|
"""A direction represented by an integer vector (not necessarily reduced)."""
|
|
|
|
__slots__ = ("x", "y")
|
|
|
|
def __init__(self, x: int, y: int) -> None:
|
|
self.x = int(x)
|
|
self.y = int(y)
|
|
|
|
def __repr__(self) -> str:
|
|
return f"IntDirection({self.x}, {self.y})"
|
|
|
|
def get_vector(self) -> IntVector:
|
|
return IntVector(self.x, self.y)
|
|
|
|
def is_orthogonal(self) -> bool:
|
|
return self.x == 0 or self.y == 0
|
|
|
|
def is_diagonal(self) -> bool:
|
|
return abs(self.x) == abs(self.y)
|
|
|
|
def opposite(self) -> IntDirection:
|
|
return IntDirection(-self.x, -self.y)
|
|
|
|
def turn_45_degree(self, factor: int) -> IntDirection:
|
|
n = factor % 8
|
|
x, y = self.x, self.y
|
|
table = {
|
|
0: (x, y),
|
|
1: (x - y, x + y),
|
|
2: (-y, x),
|
|
3: (-x - y, x - y),
|
|
4: (-x, -y),
|
|
5: (y - x, -x - y),
|
|
6: (y, -x),
|
|
7: (x + y, y - x),
|
|
}
|
|
nx, ny = table[n]
|
|
return IntDirection(nx, ny)
|
|
|
|
def compare_to(self, other: Direction) -> int:
|
|
"""Angular comparison with the positive x-axis (exact).
|
|
|
|
Returns +1 if ``self`` has a strictly larger angle than ``other``, 0 if
|
|
equal, -1 otherwise. Ports the half-plane split in
|
|
``IntDirection.compareTo``.
|
|
"""
|
|
if not isinstance(other, IntDirection):
|
|
return -other.compare_to(self)
|
|
y, x = self.y, self.x
|
|
oy, ox = other.y, other.x
|
|
if y > 0:
|
|
if oy < 0:
|
|
return -1
|
|
if oy == 0:
|
|
return 1 if ox > 0 else -1
|
|
elif y < 0:
|
|
if oy >= 0:
|
|
return 1
|
|
else: # y == 0
|
|
if x > 0:
|
|
if oy != 0 or ox < 0:
|
|
return -1
|
|
return 0
|
|
# x < 0
|
|
if oy > 0 or (oy == 0 and ox > 0):
|
|
return 1
|
|
if oy < 0:
|
|
return -1
|
|
return 0
|
|
# same open horizontal half-plane: compare by exact determinant.
|
|
determinant = ox * y - oy * x
|
|
return _sign(determinant)
|
|
|
|
|
|
def _sign(value: int) -> int:
|
|
if value > 0:
|
|
return 1
|
|
if value < 0:
|
|
return -1
|
|
return 0
|