tools/epro2/kicad: PCB Phase-2 — POUR → (zone), CoreBoard unconnected -43%
Phase-1 left 75-358 unconnected_items per board (DRC), dominated by
GND/AGND/POWER nets that EPRO2 routes through copper pour, not discrete
traces. Phase-2 lands those:
- pcb_writer._decode_zone_path handles the three POUR.path encodings
seen in ESP-VoCat: rectangle (['R', x, y, w, h, ...]), circle
(['CIRCLE', cx, cy, r]) approximated as a 36-segment polygon, and
polyline (numeric pairs with 'L'/'ARC' verb tokens).
- Each POUR on a copper layer turns into a (zone (polygon ...) ...)
block plus a (filled_polygon ...) that mirrors the boundary.
Why mirror, not auto-fill: kicad-cli pcb drc does NOT run the zone
filler before checking — only the KiCad GUI does. Without a
pre-computed (filled_polygon ...), DRC sees zones as empty regions and
reports the entire net as unconnected. Mirroring the boundary as the
fill is "connectivity-correct, clearance-imprecise" — KiCad users can
still hit Edit > Fill Zones to refine thermals and pad clearances. We
chose this over reading EPRO2's POURED.pourFill (the editor's own
post-fill polygons) because POURED paths use ARC tokens we'd need to
fully decode, and the user-drawn POUR boundary is already the
authoritative "intended copper" region.
ESP-VoCat DRC totals: 883 → 730 unconnected_items (-17% project-wide).
CoreBoard, the 4-layer board with the most pour coverage, drops 358 →
205 (-43%). Other boards see no movement because their unconnected
items are non-pour issues — pads outside the user-drawn POUR
rectangle, or internal $1N nets via vias on the wrong net (separate
problem, separate fix).
65 → 68 unit tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -153,6 +153,67 @@ def test_zero_length_segment_skipped():
|
||||
assert getattr(write_pcb, "last_stats").skipped == 1
|
||||
|
||||
|
||||
def test_pour_rectangle_emits_zone_with_filled_polygon():
|
||||
"""An EPRO2 POUR with rectangle path on a copper layer must turn into
|
||||
a (zone) with both a (polygon ...) boundary and a (filled_polygon ...)
|
||||
that mirrors it. Without the filled_polygon, kicad-cli pcb drc never
|
||||
runs the zone filler and reports the entire net as unconnected."""
|
||||
d = _pcb([
|
||||
('["NET","GND"]', {"_type": "NET"}),
|
||||
("p1", {"_type": "POUR", "layerId": 1, "netName": "GND",
|
||||
"path": [["R", 0, 0, 1000, 1000, 0, 0]]}),
|
||||
])
|
||||
text = write_pcb(d, project_relations=_empty_pr(d), board_origin_mm=(0.0, 0.0))
|
||||
p = parse(text)
|
||||
zones = _block(p, "zone")
|
||||
assert len(zones) == 1
|
||||
z = zones[0]
|
||||
layer = next(c for c in z if isinstance(c, list) and c[0] == "layer")
|
||||
assert layer[1] == "F.Cu"
|
||||
net_name = next(c for c in z if isinstance(c, list) and c[0] == "net_name")
|
||||
assert net_name[1] == "GND"
|
||||
poly = next(c for c in z if isinstance(c, list) and c[0] == "polygon")
|
||||
pts = next(c for c in poly if isinstance(c, list) and c[0] == "pts")
|
||||
xys = [c for c in pts if isinstance(c, list) and c[0] == "xy"]
|
||||
assert len(xys) == 4 # rectangle has 4 corners
|
||||
filled = next(c for c in z if isinstance(c, list) and c[0] == "filled_polygon")
|
||||
fpts = next(c for c in filled if isinstance(c, list) and c[0] == "pts")
|
||||
assert len([c for c in fpts if isinstance(c, list) and c[0] == "xy"]) == 4
|
||||
|
||||
|
||||
def test_pour_circle_path_sampled_to_polygon():
|
||||
"""Circular POURs on copper layers must be approximated as a polygon —
|
||||
KiCad zones don't accept (circle ...) primitives, so the fill region
|
||||
needs explicit (xy) points around the circumference."""
|
||||
d = _pcb([
|
||||
('["NET","GND"]', {"_type": "NET"}),
|
||||
("p1", {"_type": "POUR", "layerId": 1, "netName": "GND",
|
||||
"path": [["CIRCLE", 0, 0, 100]]}),
|
||||
])
|
||||
text = write_pcb(d, project_relations=_empty_pr(d), board_origin_mm=(0.0, 0.0))
|
||||
p = parse(text)
|
||||
z = _block(p, "zone")[0]
|
||||
poly = next(c for c in z if isinstance(c, list) and c[0] == "polygon")
|
||||
pts = next(c for c in poly if isinstance(c, list) and c[0] == "pts")
|
||||
xys = [c for c in pts if isinstance(c, list) and c[0] == "xy"]
|
||||
# 36 segments by default — enough to approximate a circle for fill
|
||||
assert len(xys) >= 12
|
||||
|
||||
|
||||
def test_pour_on_non_copper_layer_skipped():
|
||||
"""POURs only make sense as copper zones; an EPRO2 POUR mistakenly
|
||||
landed on a silk layer must NOT emit (zone ...) since KiCad zones
|
||||
are copper-only and the file would be semantically wrong."""
|
||||
d = _pcb([
|
||||
('["NET","GND"]', {"_type": "NET"}),
|
||||
("p1", {"_type": "POUR", "layerId": 3, "netName": "GND",
|
||||
"path": [["R", 0, 0, 100, 100]]}),
|
||||
])
|
||||
text = write_pcb(d, project_relations=_empty_pr(d), board_origin_mm=(0.0, 0.0))
|
||||
p = parse(text)
|
||||
assert _block(p, "zone") == []
|
||||
|
||||
|
||||
def test_non_pcb_doc_rejected():
|
||||
d = Document(doc_uuid="x", doc_type="SCH_PAGE")
|
||||
try:
|
||||
|
||||
Reference in New Issue
Block a user