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.
103 lines
3.5 KiB
Python
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)
|