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