freeroute/tests/geometry/test_line.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

103 lines
3.5 KiB
Python

"""Tests for lines, emphasizing exact intersection.
Oracles hand-derived from ``geometry/planar/Line.java``.
"""
from __future__ import annotations
from freeroute.geometry import IntPoint, Line, RationalPoint, Side
def test_side_of_point():
# Line.side_of reports where the *line* is relative to the point
# (point.side_of(line).negate() in the source): a point above a +x line has
# the line on its right.
line = Line.from_coords(0, 0, 2, 0) # along +x
assert line.side_of(IntPoint(1, 1)) == Side.ON_THE_RIGHT
assert line.side_of(IntPoint(1, -1)) == Side.ON_THE_LEFT
assert line.side_of(IntPoint(5, 0)) == Side.COLLINEAR
def test_integral_intersection_fast_paths():
# vertical x horizontal
v = Line.from_coords(3, 0, 3, 9)
h = Line.from_coords(0, 4, 9, 4)
assert v.intersection(h) == IntPoint(3, 4)
# horizontal x right-diagonal (y=x): y=4 -> (4,4)
diag = Line.from_coords(0, 0, 5, 5)
assert h.intersection(diag) == IntPoint(4, 4)
def test_general_integral_intersection():
# y = x/2 and y = 1 -> x = 2
a = Line.from_coords(0, 0, 2, 1)
b = Line.from_coords(0, 1, 4, 1)
assert a.intersection(b) == IntPoint(2, 1)
def test_rational_intersection_exact():
# y = 2x/3 and y = 1 -> x = 1.5 -> RationalPoint (3,2,2)
a = Line.from_coords(0, 0, 3, 2)
b = Line.from_coords(0, 1, 5, 1)
result = a.intersection(b)
assert isinstance(result, RationalPoint)
assert result == RationalPoint(3, 2, 2)
assert not result.is_infinite()
# float approximation agrees
fp = result.to_float()
assert abs(fp.x - 1.5) < 1e-9 and abs(fp.y - 1.0) < 1e-9
def test_parallel_intersection_is_infinite():
a = Line.from_coords(0, 0, 1, 0)
b = Line.from_coords(0, 5, 1, 5)
result = a.intersection(b)
assert result.is_infinite()
assert a.is_parallel(b)
def test_intersection_exact_beyond_double_precision():
# Two nearly-parallel steep lines crossing at a non-integer point far out.
# Coordinates chosen so a double determinant would lose bits; int stays exact.
a = Line.from_coords(0, 0, 1000000, 999999)
b = Line.from_coords(0, 1, 1000000, 1000000)
result = a.intersection(b)
assert isinstance(result, RationalPoint)
# Verify exactness: the intersection lies on both lines (exact side test).
assert a.side_of(result) == Side.COLLINEAR
assert b.side_of(result) == Side.COLLINEAR
def test_intersection_approx_matches_exact():
a = Line.from_coords(0, 0, 3, 2)
b = Line.from_coords(0, 1, 5, 1)
approx = a.intersection_approx(b)
assert abs(approx.x - 1.5) < 1e-9
assert abs(approx.y - 1.0) < 1e-9
def test_translate_and_opposite():
line = Line.from_coords(0, 0, 4, 0)
assert line.opposite().direction().equals(line.direction()) is False
from freeroute.geometry import IntVector
moved = line.translate_by(IntVector(0, 5))
assert moved.side_of(IntPoint(2, 5)) == Side.COLLINEAR
def test_perpendicular_and_parallel_predicates():
horizontal = Line.from_coords(0, 0, 1, 0)
vertical = Line.from_coords(0, 0, 0, 1)
assert horizontal.is_perpendicular(vertical)
assert not horizontal.is_parallel(vertical)
assert horizontal.is_parallel(Line.from_coords(3, 7, 10, 7))
def test_line_equality_same_set_same_direction():
a = Line.from_coords(0, 0, 2, 0)
b = Line.from_coords(-5, 0, 9, 0) # same line, same direction
c = Line.from_coords(9, 0, -5, 0) # same set, opposite direction
assert a == b
assert a != c
assert a.is_equal_or_opposite(c)