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:
@@ -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)
|
||||
Reference in New Issue
Block a user