Solder coat only on the solder side of a THT joint
Build PCM package / build (push) Successful in 6s

The pad face is wetted where the joint is: the protrusion side,
opposite the component (already read from the owning footprint). The
component-side pad face stays bare - contact_solder_buildups and
tht_joint_buildups no longer coat both outer layers.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
janik
2026-07-16 19:05:06 +07:00
parent b2277f65bf
commit 38b6e34bfa
3 changed files with 50 additions and 36 deletions
+13 -10
View File
@@ -89,10 +89,11 @@ SWIG API. Requires KiCad **10.0.1+**.
**single-layer runs too** (drill mouths perforate a lone plane). **single-layer runs too** (drill mouths perforate a lone plane).
THT-pad copper and drills remain outside the model, but every THT-pad copper and drills remain outside the model, but every
**populated THT pad** of the net carries its full **soldered **populated THT pad** of the net carries its full **soldered
joint**: a solder-filled barrel (SAC305 core in parallel with the joint** on its SOLDER side (opposite the component; the
plating), the average-thickness solder coat over a pad-diameter disc component-side pad face stays bare): a solder-filled barrel (SAC305
on the outer layers, and the protruding-lead cone on the side core in parallel with the plating), the average-thickness solder
opposite its footprint (see barrel contacts below). Whether a hole coat over a pad-diameter disc, and the protruding-lead cone (see
barrel contacts below). Whether a hole
is a via or a THT pad, the owning footprint's side, and its **Do not is a via or a THT pad, the owning footprint's side, and its **Do not
populate** flag are all read from KiCad — DNP pads stay plating-only populate** flag are all read from KiCad — DNP pads stay plating-only
with no joint. At f > 0 the thickness scaling is applied with no joint. At f > 0 the thickness scaling is applied
@@ -130,12 +131,14 @@ SWIG API. Requires KiCad **10.0.1+**.
of the result (both contact models; verified against of the result (both contact models; verified against
R = ρ/(π·t)·acosh(d/2a) for two circular contacts on a sheet). A R = ρ/(π·t)·acosh(d/2a) for two circular contacts on a sheet). A
soldered **THT joint** additionally assumes the **hole is filled with soldered **THT joint** additionally assumes the **hole is filled with
solder** (core in parallel with the plating) and the **pad face solder** (core in parallel with the plating) and the **pad face on
carries an average-thickness solder coat** (`SOLDER_THICKNESS_UM`, the solder side carries an average-thickness solder coat**
50 µm) over the modeled copper under the pad shape. The **clipped (`SOLDER_THICKNESS_UM`, 50 µm) over the modeled copper under the pad
lead protrudes** `THT_LEAD_PROTRUSION_MM` (1.5 mm, 0 = off) on the shape — the solder side is the side opposite the component (taken
side opposite the component (taken from the owning footprint; from the owning footprint; assumed `B.Cu` if it cannot be found),
assumed `B.Cu` if it cannot be found) and a **solder cone** wraps and the component-side pad face stays bare. There the **clipped
lead protrudes** `THT_LEAD_PROTRUSION_MM` (1.5 mm, 0 = off)
and a **solder cone** wraps
it: full protrusion height at the drill wall, tapering linearly to it: full protrusion height at the drill wall, tapering linearly to
zero at the pad edge, applied as extra conduction-equivalent copper zero at the pad edge, applied as extra conduction-equivalent copper
per cell. The tall solder column at the wall pulls the joint per cell. The tall solder column at the wall pulls the joint
+29 -24
View File
@@ -105,8 +105,10 @@ class Electrode:
lead/wire soldered into the hole), so the contact cells are the lead/wire soldered into the hole), so the contact cells are the
copper ring at the drill wall, not the whole pad face. `solder` copper ring at the drill wall, not the whole pad face. `solder`
additionally models a soldered THT joint: the hole is filled with additionally models a soldered THT joint: the hole is filled with
solder and the pad face carries an average-thickness solder coat solder and the pad face on the SOLDER side (protrusion_side,
(Problem.solder_thickness_nm over `polygons`).""" opposite the component) carries an average-thickness solder coat
(Problem.solder_thickness_nm over `polygons`) plus the
protruding-lead cone."""
rect: Rect # bounding box (labels/summary) rect: Rect # bounding box (labels/summary)
contact: str = "all" contact: str = "all"
polygons: list[Polygon] | None = None polygons: list[Polygon] | None = None
@@ -217,21 +219,23 @@ class Problem:
def contact_solder_buildups(problem: Problem) -> list[str]: def contact_solder_buildups(problem: Problem) -> list[str]:
"""Soldered THT-joint contacts: the pad face is covered in solder of """Soldered THT-joint contacts: the pad face on the SOLDER side (the
average thickness solder_thickness_nm. Adds one SurfaceBuildup per protrusion side, opposite the component - the component-side face
outer layer for every `solder` electrode's pad shape (the buildup stays bare) is covered in solder of average thickness
machinery intersects with actual copper at raster time). Returns the solder_thickness_nm. Adds one SurfaceBuildup there for every
affected layer names. Called once when the problem is built.""" `solder` electrode's pad shape (the buildup machinery intersects
with actual copper at raster time). Returns the affected layer
names. Called once when the problem is built."""
included = {l.layer_name for l in problem.layers} included = {l.layer_name for l in problem.layers}
outer = [n for n in ("F.Cu", "B.Cu") if n in included]
touched = [] touched = []
for e in problem.electrodes1 + problem.electrodes2: for e in problem.electrodes1 + problem.electrodes2:
if not e.solder or not e.polygons: if not e.solder or not e.polygons \
or e.protrusion_side not in included:
continue continue
for name in outer: problem.buildups.append(
problem.buildups.append( SurfaceBuildup(layer_name=e.protrusion_side,
SurfaceBuildup(layer_name=name, polygons=list(e.polygons))) polygons=list(e.polygons)))
touched.append(name) touched.append(e.protrusion_side)
return sorted(set(touched)) return sorted(set(touched))
@@ -245,25 +249,26 @@ def _disc_polygon(x_nm: float, y_nm: float, r_nm: float,
def tht_joint_buildups(problem: Problem) -> list[str]: def tht_joint_buildups(problem: Problem) -> list[str]:
"""Solder coat of the net's populated STITCHING through-hole pads """Solder coat of the net's populated STITCHING through-hole pads
(ViaLink kind 'pad' with solder_filled): one pad-diameter disc per (ViaLink kind 'pad' with solder_filled): one pad-diameter disc on
outer layer - the exact pad shape is unknown for non-contact pads, the pad's SOLDER side (the protrusion side, opposite the component;
and the coat intersects the modeled copper at raster time anyway. the component-side face stays bare). The exact pad shape is unknown
Contact pads are skipped: contact_solder_buildups already coats for non-contact pads, and the coat intersects the modeled copper at
them with the exact pad shape. Returns the affected layer names.""" raster time anyway. Contact pads are skipped:
contact_solder_buildups already coats them with the exact pad
shape. Returns the affected layer names."""
included = {l.layer_name for l in problem.layers} included = {l.layer_name for l in problem.layers}
outer = [n for n in ("F.Cu", "B.Cu") if n in included]
contacts = {e.center for e in problem.electrodes1 + problem.electrodes2 contacts = {e.center for e in problem.electrodes1 + problem.electrodes2
if e.drill_nm > 0 and e.center is not None} if e.drill_nm > 0 and e.center is not None}
touched = [] touched = []
for v in problem.vias: for v in problem.vias:
if v.kind != "pad" or not v.solder_filled \ if v.kind != "pad" or not v.solder_filled \
or v.pad_nm <= v.drill_nm or (v.x, v.y) in contacts: or v.pad_nm <= v.drill_nm or (v.x, v.y) in contacts \
or v.protrusion_side not in included:
continue continue
disc = _disc_polygon(v.x, v.y, v.pad_nm / 2.0) disc = _disc_polygon(v.x, v.y, v.pad_nm / 2.0)
for name in outer: problem.buildups.append(
problem.buildups.append( SurfaceBuildup(layer_name=v.protrusion_side, polygons=[disc]))
SurfaceBuildup(layer_name=name, polygons=[disc])) touched.append(v.protrusion_side)
touched.append(name)
return sorted(set(touched)) return sorted(set(touched))
+8 -2
View File
@@ -125,15 +125,21 @@ def test_solder_filled_barrel_resistance():
def test_contact_solder_coat(): def test_contact_solder_coat():
"""A soldered THT contact adds an average-thickness solder buildup """A soldered THT contact adds an average-thickness solder buildup
over the pad face on the outer layers, lowering the spreading over the pad face on its SOLDER side only (opposite the component),
resistance vs the bare barrel contact.""" lowering the spreading resistance vs the bare barrel contact."""
def prob(): def prob():
p = make_problem([(PLATE20, [])], p = make_problem([(PLATE20, [])],
rect1_mm=(0, 0, 1, 20), rect2_mm=(19, 0, 20, 20)) 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, p.electrodes1 = [_barrel(10, 10, drill_mm=1.0, pad_mm=2.4,
solder=True, polygons=[_disc(10, 10, 1.2)])] solder=True, polygons=[_disc(10, 10, 1.2)])]
p.electrodes1[0].protrusion_side = "F.Cu"
return p return p
# solder side not among the included layers -> no coat there
q = prob()
q.electrodes1[0].protrusion_side = "B.Cu"
assert contact_solder_buildups(q) == []
p = prob() p = prob()
assert contact_solder_buildups(p) == ["F.Cu"] assert contact_solder_buildups(p) == ["F.Cu"]
assert len(p.buildups) == 1 and p.buildups[0].layer_name == "F.Cu" assert len(p.buildups) == 1 and p.buildups[0].layer_name == "F.Cu"