Ports geometry/planar/TileShape.java (the border-line-based containment, area, and centre-of-gravity logic) and Simplex.java (a convex region as the intersection of directed half-planes). Corners are exact intersections of consecutive border lines; point containment uses exact side_of. The remove_redundant_lines normalization — dropping lines that do not contribute and detecting emptiness — is ported line-for-line. Supporting additions: Line.compare_to/__lt__ (angular sort order), Line.fast_equals, Line.side_of_intersection, Line.translate (perpendicular offset), IntDirection.determinant, and IntBox.to_simplex. offset is approximate (rounded translated lines, as upstream); enlarge clips to the enlarged bounding box pending the IntOctagon port. Since there is no JVM oracle, tests assert invariants: corners lie exactly on their border lines (exact side_of == 0), IntBox -> Simplex preserves the region over a sampled grid, intersection is contained in both operands and a point is in the result iff in both, and get_instance normalization drops redundant lines and detects empty half-plane pairs.
189 lines
6.1 KiB
Python
189 lines
6.1 KiB
Python
"""Invariant tests for TileShape / Simplex.
|
|
|
|
There is no external oracle (no JVM; FreeRouting ships no unit tests for
|
|
``geometry.planar``), so these assert properties that must hold regardless of
|
|
implementation, per the phase-2 brief:
|
|
|
|
* corners lie exactly on their two border lines (exact ``Line.side_of == 0``),
|
|
* ``IntBox -> Simplex -> region`` preserves the region,
|
|
* ``Simplex.intersection`` is contained in both operands and a point is in the
|
|
result iff it is in both,
|
|
* ``get_instance`` normalization drops redundant lines without changing the
|
|
region.
|
|
|
|
Source: ``geometry/planar/{TileShape,Simplex}.java``.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from freeroute.geometry import IntBox, IntPoint, Line, Side, Simplex
|
|
|
|
|
|
def _grid(lo: int, hi: int):
|
|
for x in range(lo, hi + 1):
|
|
for y in range(lo, hi + 1):
|
|
yield IntPoint(x, y)
|
|
|
|
|
|
def right_triangle() -> Simplex:
|
|
# legs of length 10 along the axes; hypotenuse x + y = 10
|
|
return Simplex.get_instance(
|
|
[
|
|
Line.from_coords(0, 0, 10, 0),
|
|
Line.from_coords(10, 0, 0, 10),
|
|
Line.from_coords(0, 10, 0, 0),
|
|
]
|
|
)
|
|
|
|
|
|
def unit_box_simplex() -> Simplex:
|
|
return IntBox(0, 0, 10, 10).to_simplex()
|
|
|
|
|
|
# --- corners lie exactly on their border lines ------------------------------
|
|
|
|
|
|
def test_corners_are_exact_on_border_lines():
|
|
for shape in (unit_box_simplex(), right_triangle()):
|
|
n = shape.border_line_count()
|
|
for i in range(n):
|
|
corner = shape.corner(i)
|
|
prev = shape.border_line(n - 1 if i == 0 else i - 1)
|
|
curr = shape.border_line(i)
|
|
# the corner is the exact intersection of these two lines
|
|
assert curr.side_of(corner) == Side.COLLINEAR
|
|
assert prev.side_of(corner) == Side.COLLINEAR
|
|
|
|
|
|
# --- IntBox -> Simplex -> region round trip ---------------------------------
|
|
|
|
|
|
def test_box_to_simplex_preserves_region():
|
|
box = IntBox(-3, 2, 7, 11)
|
|
s = box.to_simplex()
|
|
assert s.bounding_box() == box
|
|
for p in _grid(-6, 14):
|
|
assert box.contains(p) == s.contains(p)
|
|
|
|
|
|
def test_box_to_simplex_is_a_box_and_simplifies_back():
|
|
box = IntBox(0, 0, 10, 10)
|
|
s = box.to_simplex()
|
|
assert s.is_int_box()
|
|
assert s.simplify() == box
|
|
|
|
|
|
# --- containment semantics --------------------------------------------------
|
|
|
|
|
|
def test_triangle_containment():
|
|
tri = right_triangle()
|
|
assert tri.contains(IntPoint(0, 0)) # corner
|
|
assert tri.contains(IntPoint(5, 5)) # on hypotenuse
|
|
assert tri.contains_inside(IntPoint(2, 2))
|
|
assert not tri.contains_inside(IntPoint(5, 5)) # on border, not inside
|
|
assert not tri.contains(IntPoint(6, 6)) # x+y=12 > 10
|
|
assert tri.contains_on_border(IntPoint(3, 0)) # on the bottom edge
|
|
|
|
|
|
def test_triangle_area_and_bounds():
|
|
tri = right_triangle()
|
|
assert tri.is_bounded()
|
|
assert tri.dimension() == 2
|
|
assert tri.area() == 50.0
|
|
assert tri.bounding_box() == IntBox(0, 0, 10, 10)
|
|
|
|
|
|
# --- intersection invariants ------------------------------------------------
|
|
|
|
|
|
def test_intersection_contained_in_both_and_iff():
|
|
a = IntBox(0, 0, 12, 8).to_simplex()
|
|
b = right_triangle() # x,y >= 0, x+y <= 10
|
|
inter = a.intersection(b)
|
|
assert not inter.is_empty()
|
|
for p in _grid(-3, 15):
|
|
in_both = a.contains(p) and b.contains(p)
|
|
in_inter = inter.contains(p)
|
|
assert in_inter == in_both
|
|
# every corner of the intersection lies inside both operands
|
|
for i in range(inter.border_line_count()):
|
|
c = inter.corner(i)
|
|
assert a.contains(c)
|
|
assert b.contains(c)
|
|
|
|
|
|
def test_intersection_of_two_boxes_matches_box_intersection():
|
|
a_box = IntBox(0, 0, 10, 10)
|
|
b_box = IntBox(4, -2, 20, 6)
|
|
inter = a_box.to_simplex().intersection(b_box.to_simplex())
|
|
expected = a_box.intersection(b_box)
|
|
for p in _grid(-5, 22):
|
|
assert inter.contains(p) == expected.contains(p)
|
|
|
|
|
|
def test_disjoint_intersection_is_empty():
|
|
a = IntBox(0, 0, 3, 3).to_simplex()
|
|
b = IntBox(10, 10, 13, 13).to_simplex()
|
|
assert a.intersection(b).is_empty()
|
|
assert not a.intersects(b)
|
|
|
|
|
|
def test_intersection_is_idempotent_with_self():
|
|
tri = right_triangle()
|
|
inter = tri.intersection(tri)
|
|
for p in _grid(-3, 13):
|
|
assert inter.contains(p) == tri.contains(p)
|
|
|
|
|
|
# --- normalization (get_instance drops redundant lines) ---------------------
|
|
|
|
|
|
def test_get_instance_removes_redundant_line():
|
|
# a unit box plus a far-away redundant half-plane that does not cut it.
|
|
# A DOWN-directed vertical line has its interior to the east, so this is
|
|
# x >= -100, which contains the whole box and is therefore redundant.
|
|
box_lines = list(IntBox(0, 0, 10, 10).to_simplex()._arr)
|
|
redundant = Line.from_coords(-100, 1, -100, 0) # x >= -100
|
|
s = Simplex.get_instance(box_lines + [redundant])
|
|
assert s.border_line_count() == 4 # redundant line dropped
|
|
for p in _grid(-5, 15):
|
|
assert s.contains(p) == IntBox(0, 0, 10, 10).contains(p)
|
|
|
|
|
|
def test_get_instance_detects_empty_from_opposing_halfplanes():
|
|
# x >= 5 (DOWN line, interior east) and x <= 0 (UP line, interior west)
|
|
# cannot both hold.
|
|
ge5 = Line.from_coords(5, 1, 5, 0) # DOWN -> interior x >= 5
|
|
le0 = Line.from_coords(0, 0, 0, 1) # UP -> interior x <= 0
|
|
s = Simplex.get_instance([ge5, le0])
|
|
assert s.is_empty()
|
|
|
|
|
|
def test_translate_preserves_shape():
|
|
tri = right_triangle()
|
|
from freeroute.geometry import IntVector
|
|
|
|
moved = tri.translate_by(IntVector(100, 50))
|
|
for p in _grid(-3, 13):
|
|
assert tri.contains(p) == moved.contains(IntPoint(p.x + 100, p.y + 50))
|
|
|
|
|
|
def test_offset_outward_enlarges_region():
|
|
box = IntBox(0, 0, 10, 10).to_simplex()
|
|
bigger = box.offset(2)
|
|
# every point of the original box is still contained after outward offset
|
|
for p in _grid(0, 10):
|
|
assert bigger.contains(p)
|
|
# a point 2 outside the original right edge is now contained
|
|
assert bigger.contains(IntPoint(12, 5))
|
|
assert not box.contains(IntPoint(12, 5))
|
|
|
|
|
|
def test_empty_simplex_predicates():
|
|
e = Simplex.empty()
|
|
assert e.is_empty()
|
|
assert e.dimension() == -1
|
|
assert e.is_outside(IntPoint(0, 0))
|
|
assert not e.contains(IntPoint(0, 0))
|