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.
This commit is contained in:
2026-07-11 18:01:29 -06:00
parent 33cb196bc8
commit 55a869ef7d
4 changed files with 512 additions and 0 deletions
+61
View File
@@ -0,0 +1,61 @@
"""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
+102
View File
@@ -0,0 +1,102 @@
"""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)