Files
rpmroute/tests/board/test_search_tree.py
T
rsp2k 3e4f53a469 Keepout exits and rule areas
A route may not leave a cell inside a wire keepout as a wire, nor reach
a pin there by one: it changes layer instead, and the via goes in the
pad. A pin whose every stub enters a keepout is reached by a via only.

A structure keepout named rule_area... (mckicad names KiCad rule areas
so) is checked for overlap only, as KiCad's DRC does, with the free ends
of each wire tested for their round caps.
2026-09-28 10:08:02 -06:00

99 lines
4.1 KiB
Python

"""Tests for the exact ShapeSearchTree spatial index."""
from __future__ import annotations
from freeroute.board import ShapeSearchTree, TreeShape
from freeroute.geometry import IntBox
def shape(owner, net, layer, box, routed=False):
return TreeShape(owner, net, frozenset({layer}), box, routed=routed)
def test_insert_and_overlapping_exact():
tree = ShapeSearchTree(bucket_size=100)
tree.insert(shape(0, 1, 0, IntBox(0, 0, 50, 50)))
tree.insert(shape(1, 2, 0, IntBox(200, 200, 250, 250)))
# a query box overlapping only the first shape
hit = tree.overlapping(IntBox(40, 40, 60, 60))
assert [s.owner for s in hit] == [0]
# a query far from everything
assert tree.overlapping(IntBox(1000, 1000, 1010, 1010)) == []
def test_touching_boxes_do_not_overlap_but_do_intersect():
tree = ShapeSearchTree(bucket_size=100)
tree.insert(shape(0, 1, 0, IntBox(0, 0, 10, 10)))
# shares the edge x=10 -> intersection is 1-D (found by broad phase)
assert len(tree.overlapping(IntBox(10, 0, 20, 10))) == 1
def test_remove_owner():
tree = ShapeSearchTree(bucket_size=100)
tree.insert(shape(0, 1, 0, IntBox(0, 0, 50, 50)))
tree.insert(shape(0, 1, 0, IntBox(60, 0, 90, 50))) # same owner, 2 tiles
tree.insert(shape(1, 2, 0, IntBox(0, 0, 50, 50)))
tree.remove_owner(0)
assert all(s.owner == 1 for s in tree.all_shapes())
assert len(tree.all_shapes()) == 1
def test_clearance_conflict_respects_the_clearance_gap():
tree = ShapeSearchTree(bucket_size=1000)
tree.insert(shape(0, 1, 0, IntBox(0, 0, 100, 100), routed=True))
# a net-2 box exactly `clearance` (20) to the right -> allowed (touching)
ok = IntBox(120, 0, 200, 100)
assert not tree.clearance_conflict(ok, net_no=2, layer=0, clearance=20)
# one unit closer -> conflict
bad = IntBox(119, 0, 200, 100)
assert tree.clearance_conflict(bad, net_no=2, layer=0, clearance=20)
# same net never conflicts
assert not tree.clearance_conflict(bad, net_no=1, layer=0, clearance=20)
# different layer never conflicts
assert not tree.clearance_conflict(bad, net_no=2, layer=1, clearance=20)
def test_has_violation_ignores_static_pad_pairs():
tree = ShapeSearchTree(bucket_size=1000)
# two different-net *static* pads closer than clearance -> not a routing DRC
tree.insert(shape(0, 1, 0, IntBox(0, 0, 10, 10), routed=False))
tree.insert(shape(1, 2, 0, IntBox(11, 0, 20, 10), routed=False))
assert tree.has_violation(clearance=20) is None
# a routed trace of net 3 within clearance of pad net 1 -> violation
tree.insert(shape(2, 3, 0, IntBox(11, 0, 20, 10), routed=True))
assert tree.has_violation(clearance=20) is not None
def test_a_rule_area_conflicts_only_on_overlap():
tree = ShapeSearchTree()
tree.insert(TreeShape(1, -1, frozenset({0}), IntBox(100, 0, 200, 100), clearance=False))
near = IntBox(0, 0, 95, 100) # 5 short of the area, well inside clearance 50
over = IntBox(0, 0, 105, 100)
assert not tree.clearance_conflict(near, 7, 0, 50)
assert tree.clearance_conflict(over, 7, 0, 50)
def test_an_ordinary_keepout_still_needs_clearance():
tree = ShapeSearchTree()
tree.insert(TreeShape(1, -1, frozenset({0}), IntBox(100, 0, 200, 100)))
assert tree.clearance_conflict(IntBox(0, 0, 95, 100), 7, 0, 50)
def test_the_routed_board_check_treats_a_rule_area_the_same_way():
tree = ShapeSearchTree()
tree.insert(TreeShape(1, -1, frozenset({0}), IntBox(100, 0, 200, 100), clearance=False))
tree.insert(TreeShape(2, 7, frozenset({0}), IntBox(0, 0, 95, 100), routed=True))
assert tree.has_violation(50) is None
tree.insert(TreeShape(3, 7, frozenset({0}), IntBox(150, 0, 160, 100), routed=True))
assert tree.has_violation(50) is not None
def test_rule_area_overlap_sees_only_rule_areas():
tree = ShapeSearchTree()
tree.insert(TreeShape(1, -1, frozenset({0}), IntBox(100, 0, 200, 100), clearance=False))
tree.insert(TreeShape(2, -1, frozenset({0}), IntBox(300, 0, 400, 100)))
assert tree.rule_area_overlap(IntBox(90, 40, 110, 60), 0)
assert not tree.rule_area_overlap(IntBox(290, 40, 310, 60), 0)
assert not tree.rule_area_overlap(IntBox(90, 40, 110, 60), 1)