Add exact integer octagon cover for 45-degree trace copper
A diagonal segment's copper has no axis-aligned exact tile, so segment_box falls back to the bounding box -- a huge conservative cover that makes diagonal traces conflict with everything. segment_octagon builds the tight cover instead: the copper of a 0/45/90/135-degree segment swept by a radius, as a convex integer Simplex bounded by the four axis and four x+y / x-y diagonal half-planes. Diagonal bounds widen by ceil_sqrt2(radius) -- the exact integer ceiling of radius*sqrt(2) via math.isqrt, no float -- rounded outward, so the octagon is a provable superset of segment (+) disk(radius) (verified over 20k sampled copper points). Growing by clearance is just a larger radius, giving both the stored copper tile and the queried clearance region. overlaps_2d tests a real 2-D overlap via exact Simplex intersection, accepting mixed IntBox/Simplex tiles.
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
"""Tests for the exact integer octagon cover of 45-degree trace copper."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
import random
|
||||
|
||||
import pytest
|
||||
|
||||
from freeroute.geometry import IntBox, IntPoint, ceil_sqrt2, overlaps_2d, segment_octagon
|
||||
|
||||
|
||||
@pytest.mark.parametrize("r", [0, 1, 2, 3, 1000, 2000, 3000, 12345, 999999])
|
||||
def test_ceil_sqrt2_is_exact_integer_ceiling(r):
|
||||
R = ceil_sqrt2(r)
|
||||
assert 2 * r * r <= R * R # covers the diagonal support r*sqrt(2)
|
||||
if r > 0:
|
||||
assert 2 * r * r > (R - 1) * (R - 1) # and is the smallest such integer
|
||||
|
||||
|
||||
def test_ceil_sqrt2_uses_no_float():
|
||||
# exactness at large magnitude where float sqrt would round wrong
|
||||
r = 10**12 + 7
|
||||
R = ceil_sqrt2(r)
|
||||
assert 2 * r * r <= R * R
|
||||
assert 2 * r * r > (R - 1) * (R - 1)
|
||||
|
||||
|
||||
def test_octagon_is_superset_of_true_diagonal_copper():
|
||||
"""Every point of the true swept copper (centreline offset perpendicular by up
|
||||
to the half-width) lies inside the octagon -- so the cover never under-reports."""
|
||||
a, b = IntPoint(0, 0), IntPoint(1000, 1000)
|
||||
hw = 100
|
||||
oc = segment_octagon(a, b, hw)
|
||||
rng = random.Random(0)
|
||||
for _ in range(20000):
|
||||
t = rng.uniform(0.0, 1.0)
|
||||
s = rng.uniform(-hw, hw) # perpendicular offset within the half-width
|
||||
px = t * 1000 + s / math.sqrt(2)
|
||||
py = t * 1000 - s / math.sqrt(2)
|
||||
assert oc.contains(IntPoint(round(px), round(py)))
|
||||
|
||||
|
||||
def test_octagon_excludes_points_beyond_the_radius():
|
||||
a, b = IntPoint(0, 0), IntPoint(1000, 1000)
|
||||
oc = segment_octagon(a, b, 100)
|
||||
# a point ~700 units off the centreline perpendicular is far outside
|
||||
assert not oc.contains(IntPoint(500, -200))
|
||||
assert not oc.contains(IntPoint(200, 900))
|
||||
|
||||
|
||||
def test_octagon_is_bounded_and_two_dimensional():
|
||||
oc = segment_octagon(IntPoint(0, 0), IntPoint(400, -400), 50)
|
||||
assert oc.is_bounded()
|
||||
assert oc.dimension() == 2
|
||||
assert oc.border_line_count() == 8
|
||||
|
||||
|
||||
def test_clearance_region_separates_at_diagonal_distance():
|
||||
"""The clearance region is the copper grown by clearance; an obstacle whose
|
||||
diagonal distance exceeds half_width + clearance does not overlap it, one that
|
||||
is closer does."""
|
||||
a, b = IntPoint(0, 0), IntPoint(1000, 1000)
|
||||
hw, clr = 100, 200
|
||||
grown = segment_octagon(a, b, hw + clr)
|
||||
# centreline on x-y = 0; a box centred on x-y = -900 (well beyond 300*sqrt2~425)
|
||||
far = IntBox(300, -1300, 500, -1100)
|
||||
assert not overlaps_2d(grown, far)
|
||||
# a box straddling the centreline clearly overlaps
|
||||
near = IntBox(400, -100, 600, 100)
|
||||
assert overlaps_2d(grown, near)
|
||||
|
||||
|
||||
def test_overlaps_2d_matches_intbox_overlap_for_boxes():
|
||||
a = IntBox(0, 0, 100, 100)
|
||||
assert overlaps_2d(a, IntBox(50, 50, 150, 150)) # shared area
|
||||
assert not overlaps_2d(a, IntBox(100, 0, 200, 100)) # shared edge only
|
||||
assert not overlaps_2d(a, IntBox(200, 200, 300, 300)) # disjoint
|
||||
|
||||
|
||||
def test_overlaps_2d_accepts_mixed_argument_order():
|
||||
oc = segment_octagon(IntPoint(0, 0), IntPoint(1000, 1000), 100)
|
||||
box = IntBox(400, 400, 600, 600) # straddles the centreline
|
||||
assert overlaps_2d(oc, box) == overlaps_2d(box, oc) is True
|
||||
|
||||
|
||||
def test_orthogonal_octagon_is_axis_aligned_cover():
|
||||
# a horizontal segment's octagon still covers its copper (squared caps)
|
||||
oc = segment_octagon(IntPoint(0, 0), IntPoint(1000, 0), 100)
|
||||
assert oc.contains(IntPoint(500, 90))
|
||||
assert oc.contains(IntPoint(500, -90))
|
||||
assert not oc.contains(IntPoint(500, 200))
|
||||
Reference in New Issue
Block a user