freeroute/tests/geometry/test_box.py
Ryan Malloy d4aa4c729d Add IntBox convex tile and geometry package exports
Ports IntBox, the simplest concrete convex tile (RegularTileShape): exact
integer-corner rectangle with contains (border-inclusive and interior),
intersection, union, intersects/overlaps, offset (round half up),
horizontal/vertical offset, shrink, box containment, translate, and
dimension. Adds the package __init__ exporting the geometry API.

The general convex machinery beyond IntBox — TileShape/Simplex/IntOctagon
and polygon split_to_convex — is deferred to the next geometry phase.

Tests cover boundary vs interior containment, degenerate tiles
(empty/point/segment), edge-touching intersects-vs-overlaps, rational
point on a border, and half-up offset rounding.
2026-07-11 18:01:37 -06:00

132 lines
3.9 KiB
Python

"""Tests for IntBox.
Oracles hand-derived from ``geometry/planar/IntBox.java``. Emphasis on boundary
containment and degenerate (empty / point / segment) tiles.
"""
from __future__ import annotations
from freeroute.geometry import IntBox, IntPoint, IntVector, RationalPoint
def test_measures():
b = IntBox(1, 2, 5, 8)
assert b.width() == 4
assert b.height() == 6
assert b.area() == 24
assert b.circumference() == 20
assert b.max_width() == 6
assert b.min_width() == 4
def test_corners_counter_clockwise():
b = IntBox(0, 0, 4, 3)
assert b.corners() == [
IntPoint(0, 0),
IntPoint(4, 0),
IntPoint(4, 3),
IntPoint(0, 3),
]
def test_dimension_variants():
assert IntBox(0, 0, 4, 3).dimension() == 2
assert IntBox(0, 0, 0, 3).dimension() == 1 # vertical segment
assert IntBox(0, 0, 4, 0).dimension() == 1 # horizontal segment
assert IntBox(2, 2, 2, 2).dimension() == 0 # single point
assert IntBox.empty().dimension() == -1
def test_boundary_containment_inclusive_vs_interior():
b = IntBox(0, 0, 10, 10)
corner = IntPoint(0, 0)
edge = IntPoint(0, 5)
inside = IntPoint(5, 5)
outside = IntPoint(11, 5)
# border-inclusive contains
assert b.contains(corner)
assert b.contains(edge)
assert b.contains(inside)
assert not b.contains(outside)
# strict interior
assert not b.contains_inside(corner)
assert not b.contains_inside(edge)
assert b.contains_inside(inside)
def test_contains_rational_point_on_border():
b = IntBox(0, 0, 3, 3)
# (3, 1.5) sits exactly on the right edge -> contained (inclusive)
assert b.contains(RationalPoint(6, 3, 2))
# (3.5, 1.5) just outside
assert not b.contains(RationalPoint(7, 3, 2))
def test_intersection_overlap_and_touch():
a = IntBox(0, 0, 10, 10)
assert a.intersection(IntBox(5, 5, 20, 20)) == IntBox(5, 5, 10, 10)
# touching along an edge -> 1-dimensional intersection, still non-empty
touch = a.intersection(IntBox(10, 0, 20, 10))
assert touch == IntBox(10, 0, 10, 10)
assert touch.dimension() == 1
# disjoint -> empty
assert a.intersection(IntBox(20, 20, 30, 30)).is_empty()
def test_intersects_vs_overlaps_on_touching_edge():
a = IntBox(0, 0, 10, 10)
edge_neighbor = IntBox(10, 0, 20, 10)
assert a.intersects(edge_neighbor) # closed-interval: touching counts
assert not a.overlaps(edge_neighbor) # open-interval: no 2D overlap
def test_union():
assert IntBox(0, 0, 2, 2).union(IntBox(5, 5, 8, 8)) == IntBox(0, 0, 8, 8)
def test_offset_expands_and_contracts():
b = IntBox(0, 0, 10, 10)
assert b.offset(2) == IntBox(-2, -2, 12, 12)
assert b.offset(-3) == IntBox(3, 3, 7, 7)
assert b.offset(0) is b
assert b.horizontal_offset(2) == IntBox(-2, 0, 12, 10)
assert b.vertical_offset(2) == IntBox(0, -2, 10, 12)
def test_offset_rounds_half_up():
b = IntBox(0, 0, 10, 10)
assert b.offset(2.5) == IntBox(-3, -3, 13, 13) # 2.5 -> 3 (half up)
def test_shrink_collapses_to_center_not_past():
b = IntBox(0, 0, 10, 10)
assert b.shrink(3) == IntBox(3, 3, 7, 7)
# over-shrink collapses to the center line rather than inverting
collapsed = b.shrink(20)
assert collapsed == IntBox(5, 5, 5, 5)
assert collapsed.dimension() == 0
def test_containment_between_boxes():
outer = IntBox(0, 0, 10, 10)
inner = IntBox(2, 2, 8, 8)
assert inner.is_contained_in(outer)
assert outer.contains_box(inner)
assert outer.contains_in_interior(inner)
edge = IntBox(0, 2, 8, 8) # touches left edge
assert edge.is_contained_in(outer)
assert not outer.contains_in_interior(edge)
def test_translate_by():
b = IntBox(0, 0, 4, 4)
assert b.translate_by(IntVector(3, -2)) == IntBox(3, -2, 7, 2)
assert b.translate_by(IntVector(0, 0)) is b
def test_empty_box_predicates():
e = IntBox.empty()
assert e.is_empty()
assert e.is_contained_in(IntBox(0, 0, 1, 1))
assert e.offset(5) is e