freeroute/tests/geometry/test_direction.py
Ryan Malloy 55a869ef7d Add directions and lines with exact rational intersection
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.
2026-07-11 18:01:29 -06:00

62 lines
2.0 KiB
Python

"""Tests for directions.
Oracles hand-derived from ``geometry/planar/{Direction,IntDirection}.java``.
"""
from __future__ import annotations
from freeroute.geometry import Direction, IntDirection, IntPoint, IntVector, Side
def test_normalized_direction_reduces_by_gcd():
d = IntVector(2, 4).to_normalized_direction()
assert isinstance(d, IntDirection)
assert (d.x, d.y) == (1, 2)
def test_normalized_direction_beyond_java_int():
# gcd reduction stays exact for coordinates far past Java's int range.
d = IntVector(6 * 10**12, 9 * 10**12).to_normalized_direction()
assert (d.x, d.y) == (2, 3)
def test_turn_45_degree_cycles():
right = IntDirection(1, 0)
assert (right.turn_45_degree(1).x, right.turn_45_degree(1).y) == (1, 1)
assert (right.turn_45_degree(2).x, right.turn_45_degree(2).y) == (0, 1)
assert (right.turn_45_degree(4).x, right.turn_45_degree(4).y) == (-1, 0)
assert (right.turn_45_degree(8).x, right.turn_45_degree(8).y) == (1, 0)
def test_opposite():
o = IntDirection(1, 2).opposite()
assert (o.x, o.y) == (-1, -2)
def test_direction_equals_ignores_magnitude_not_sign():
a = IntDirection(1, 1)
assert a.equals(IntDirection(3, 3)) # same ray
assert not a.equals(IntDirection(-1, -1)) # opposite ray
assert not a.equals(IntDirection(1, 0))
def test_from_points_returns_none_for_equal():
assert Direction.from_points(IntPoint(1, 1), IntPoint(1, 1)) is None
d = Direction.from_points(IntPoint(0, 0), IntPoint(4, 4))
assert (d.x, d.y) == (1, 1)
def test_compare_to_angular_order():
right = IntDirection(1, 0)
up = IntDirection(0, 1)
down = IntDirection(0, -1)
# angle(right)=0, angle(up)=90, angle(down)=270
assert right.compare_to(up) == -1
assert up.compare_to(right) == 1
assert up.compare_to(down) == -1
assert right.compare_to(IntDirection(2, 0)) == 0
def test_side_of_direction():
assert IntDirection(0, 1).side_of(IntDirection(1, 0)) == Side.ON_THE_LEFT