Add exact planar points and vectors

Ports the point/vector foundation of geometry/planar: Side and Signum
(three-valued signs), Limits, FloatPoint (approximate), and the exact
IntPoint/IntVector plus projective RationalPoint/RationalVector.

Arithmetic model per type is documented in each module. The key
simplification over the Java source: Python's unbounded int makes the
exact orientation determinants and rational (x,y,z) coordinates trivial,
so side_of is kept exact (upstream uses a double for speed) and no
BigInteger or CRIT_INT overflow promotion is needed. Rational points use
the projective triple with z=0 denoting the point at infinity.

Tests cover collinearity, determinants beyond Java long range, rational
equality/reduction, and integer/rational promotion.
This commit is contained in:
2026-07-11 18:01:21 -06:00
parent 12b0a231f0
commit 33cb196bc8
6 changed files with 905 additions and 0 deletions
+161
View File
@@ -0,0 +1,161 @@
"""Exact-arithmetic tests for points and vectors.
Oracles are hand-derived from the geometry defined in FreeRouting's
``geometry/planar/{IntPoint,IntVector,RationalPoint,RationalVector}.java``.
Emphasis is on exactness: collinearity, large coordinates that would overflow a
Java ``long``, and rational equality.
"""
from __future__ import annotations
from freeroute.geometry import (
IntPoint,
IntVector,
RationalPoint,
RationalVector,
Side,
Signum,
point,
rational_point,
)
# --- IntVector determinant / side_of ----------------------------------------
def test_determinant_exact():
# determinant of (1,0),(0,1) = 1
assert IntVector(1, 0).determinant(IntVector(0, 1)) == 1
assert IntVector(0, 1).determinant(IntVector(1, 0)) == -1
assert IntVector(2, 3).determinant(IntVector(4, 6)) == 0 # parallel
def test_determinant_exact_beyond_java_long():
# 10**12 * 10**12 = 10**24 overflows a Java long (~9.2e18); Python int is exact.
a = IntVector(10**12, 0)
b = IntVector(0, 10**12)
assert a.determinant(b) == 10**24
def test_side_of_collinear_left_right():
# line from origin toward (1,0): +y is left, -y is right, on axis collinear.
right_dir = IntVector(1, 0)
assert IntVector(0, 1).side_of(right_dir) == Side.ON_THE_LEFT
assert IntVector(0, -1).side_of(right_dir) == Side.ON_THE_RIGHT
assert IntVector(5, 0).side_of(right_dir) == Side.COLLINEAR
def test_side_of_exact_near_collinear():
# (10**9, 10**9) vs (10**9, 10**9 + 1): a hair off collinear, exact int detects it.
base = IntVector(10**9, 10**9)
assert IntVector(10**9, 10**9 + 1).side_of(base) != Side.COLLINEAR
assert IntVector(2 * 10**9, 2 * 10**9).side_of(base) == Side.COLLINEAR
def test_vector_add_and_negate():
assert IntVector(1, 2).add(IntVector(3, 4)) == IntVector(4, 6)
assert IntVector(1, 2).negate() == IntVector(-1, -2)
def test_turn_90_degree():
v = IntVector(1, 0)
assert v.turn_90_degree(1) == IntVector(0, 1)
assert v.turn_90_degree(2) == IntVector(-1, 0)
assert v.turn_90_degree(3) == IntVector(0, -1)
assert v.turn_90_degree(4) == IntVector(1, 0)
assert v.turn_90_degree(-1) == IntVector(0, -1)
def test_projection_signum():
assert IntVector(1, 0).projection(IntVector(1, 0)) == Signum.POSITIVE
assert IntVector(1, 0).projection(IntVector(-1, 0)) == Signum.NEGATIVE
assert IntVector(1, 0).projection(IntVector(0, 1)) == Signum.ZERO
def test_orthogonal_diagonal():
assert IntVector(3, 0).is_orthogonal()
assert IntVector(0, -4).is_orthogonal()
assert IntVector(2, 2).is_diagonal()
assert IntVector(-5, 5).is_diagonal()
assert IntVector(1, 2).is_multiple_of_45_degree() is False
# --- IntPoint ---------------------------------------------------------------
def test_point_difference_and_translate():
p = IntPoint(3, 4)
q = IntPoint(1, 1)
d = p.difference_by(q)
assert isinstance(d, IntVector)
assert d == IntVector(2, 3)
assert q.translate_by(d) == p
def test_point_side_of_directed_line():
p0 = IntPoint(0, 0)
p1 = IntPoint(2, 2)
assert IntPoint(1, 1).side_of(p0, p1) == Side.COLLINEAR
assert IntPoint(0, 1).side_of(p0, p1) == Side.ON_THE_LEFT
assert IntPoint(1, 0).side_of(p0, p1) == Side.ON_THE_RIGHT
def test_point_compare():
assert IntPoint(1, 5).compare_x(IntPoint(3, 2)) == -1
assert IntPoint(3, 5).compare_x(IntPoint(3, 2)) == 0
assert IntPoint(3, 5).compare_y(IntPoint(3, 2)) == 1
assert IntPoint(3, 2).compare_x_y(IntPoint(3, 5)) == -1 # x tie, y smaller
def test_point_turn_90_around_pole():
pole = IntPoint(1, 1)
# (2,1) rotated 90 about (1,1) -> (1,2)
assert IntPoint(2, 1).turn_90_degree(1, pole) == IntPoint(1, 2)
# --- RationalPoint / factory ------------------------------------------------
def test_rational_point_equality_scale_invariant():
assert RationalPoint(3, 2, 2) == RationalPoint(6, 4, 4)
assert RationalPoint(3, 2, 2) != RationalPoint(3, 2, 3)
def test_rational_point_negative_denominator_normalized_via_factory():
p = rational_point(3, 2, -2) # -> (−3/−2)=... factory flips sign -> (-3,-2,2)
assert isinstance(p, RationalPoint)
assert p == RationalPoint(-3, -2, 2)
def test_rational_point_reduces_to_int_when_divisible():
p = rational_point(6, 4, 2)
assert isinstance(p, IntPoint)
assert p == IntPoint(3, 2)
def test_rational_point_infinite():
inf = rational_point(1, 1, 0)
assert isinstance(inf, RationalPoint)
assert inf.is_infinite()
def test_rational_point_contained_in_box_exact():
from freeroute.geometry import IntBox
box = IntBox(0, 0, 2, 2)
assert RationalPoint(3, 2, 2).is_contained_in(box) # (1.5, 1.0) inside
assert not RationalPoint(5, 2, 2).is_contained_in(box) # (2.5, 1.0) outside
def test_int_and_rational_difference_promotes():
p = IntPoint(2, 2)
q = RationalPoint(3, 2, 2) # (1.5, 1.0)
d = p.difference_by(q)
assert isinstance(d, RationalVector)
# (2,2) - (1.5,1.0) = (0.5, 1.0) == (1,2,2)
assert d == RationalVector(1, 2, 2)
def test_factory_point_is_intpoint_even_for_huge_coords():
p = point(10**15, -(10**15))
assert isinstance(p, IntPoint)
assert (p.x, p.y) == (10**15, -(10**15))