Full solder joint at every populated THT pad, read from KiCad
Build PCM package / build (push) Successful in 8s

No size-threshold guessing: whether a hole is a via or a THT pad
already comes from KiCad (board.get_vias vs drilled board.get_pads).
Every populated THT pad of the net now carries the complete joint the
contacts got: solder-filled barrel, average-thickness coat over a
pad-diameter disc on the outer layers, and the protruding-lead cone on
the side opposite its owning footprint. The footprint side and the
Do-not-populate flag are read from KiCad (footprint pads store absolute
positions, so owner lookup is an exact (x, y, number) map); DNP pads
stay plating-only with no joint. Contact pads are deduplicated by
barrel center so their cone/coat is never applied twice.

Barrels are now gathered in single-layer runs too: via rings and drill
mouths perforate a lone plane, THT joints stiffen it locally.

ViaLink gains solder_filled + protrusion_side (legacy dumps load with
the old every-THT-pad-filled semantics).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
janik
2026-07-16 19:00:09 +07:00
parent 9e307f2818
commit b2277f65bf
7 changed files with 236 additions and 60 deletions
+68 -1
View File
@@ -9,7 +9,8 @@ import pytest
from fill_resistance import raster, solver
from fill_resistance.geometry import (Electrode, Polygon, ViaLink,
contact_solder_buildups, load_problem,
save_problem)
problem_from_json, problem_to_json,
save_problem, tht_joint_buildups)
from tests.util import NM, make_problem, rect_mm, ring_mm
PLATE20 = [(0, 0), (20, 0), (20, 20), (0, 20)]
@@ -195,6 +196,72 @@ def test_lead_fillet_lowers_resistance(monkeypatch):
assert r_ada.R_ohm == pytest.approx(r_cone.R_ohm, rel=2e-3)
def _pad_link(populated=True):
return ViaLink(x=10 * NM, y=10 * NM, drill_nm=1_000_000, z_top_nm=-1,
z_bot_nm=1, kind="pad", pad_nm=2_400_000,
solder_filled=populated,
protrusion_side="F.Cu" if populated else None)
def test_stitching_pad_joint():
"""A populated THT pad on the net (not a contact) gets the full
joint: coat discs on the outer layers and a cone on its protrusion
side; a DNP pad gets neither."""
def prob(populated=True):
p = make_problem([(PLATE20, [])],
rect1_mm=(0, 0, 1, 20), rect2_mm=(19, 0, 20, 20))
p.vias = [_pad_link(populated)]
return p
p = prob()
assert tht_joint_buildups(p) == ["F.Cu"]
assert len(p.buildups) == 1
r_joint, stack = _solve(p, 0.1)
assert stack.thick_scale is not None and stack.thick_scale.max() > 3.0
assert stack.buildup is not None and stack.buildup.any()
q = prob(populated=False)
assert tht_joint_buildups(q) == []
r_bare, s2 = _solve(q, 0.1)
assert s2.thick_scale is None and s2.buildup is None
assert r_joint.R_ohm < r_bare.R_ohm
def test_cone_not_doubled_at_contact():
"""A contact THT pad also appears in the net's pad list (ViaLink):
the cone and coat must be applied once, not squared/stacked."""
p = make_problem([(PLATE20, [])],
rect1_mm=(0, 0, 1, 20), rect2_mm=(19, 0, 20, 20))
p.electrodes1 = [_barrel(10, 10, drill_mm=1.0, pad_mm=2.4, solder=True,
polygons=[_disc(10, 10, 1.2)])]
p.electrodes1[0].protrusion_side = "F.Cu"
p.vias = [_pad_link()]
assert contact_solder_buildups(p) == ["F.Cu"]
assert tht_joint_buildups(p) == [] # contact center is skipped
stack = raster.rasterize_stack(p, 0.1 * NM)
wall = 1.0 + p.tht_protrusion_nm \
* (p.rho_ohm_m / p.solder_rho_ohm_m) / p.layers[0].thickness_nm
assert stack.thick_scale.max() == pytest.approx(wall, rel=1e-12)
def test_vialink_solder_json():
p = make_problem([(PLATE20, [])],
rect1_mm=(0, 0, 1, 20), rect2_mm=(19, 0, 20, 20))
p.vias = [_pad_link()]
d = problem_to_json(p)
q = problem_from_json(d)
assert q.vias[0].solder_filled is True
assert q.vias[0].protrusion_side == "F.Cu"
# legacy dumps without the flag: THT pads counted as solder-filled,
# vias as plating-only
del d["vias"][0]["solder_filled"], d["vias"][0]["protrusion_side"]
q = problem_from_json(d)
assert q.vias[0].solder_filled is True
assert q.vias[0].protrusion_side is None
d["vias"][0]["kind"] = "via"
assert problem_from_json(d).vias[0].solder_filled is False
def test_barrel_electrode_json_roundtrip(tmp_path):
p = make_problem([(PLATE20, [])],
rect1_mm=(0, 0, 1, 20), rect2_mm=(19, 0, 20, 20))