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:
2026-07-13 12:46:02 -06:00
parent 7f81c1f4ce
commit 8380318d3c
3 changed files with 194 additions and 0 deletions
+92
View File
@@ -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))