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).
THT-pad copper and drills remain outside the model, but every
**populated THT pad** of the net carries its full **soldered
joint**: a solder-filled barrel (SAC305 core in parallel with the
plating), the average-thickness solder coat over a pad-diameter disc
on the outer layers, and the protruding-lead cone on the side
opposite its footprint (see barrel contacts below). Whether a hole
joint** on its SOLDER side (opposite the component; the
component-side pad face stays bare): a solder-filled barrel (SAC305
core in parallel with the plating), the average-thickness solder
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
populate** flag are all read from KiCad — DNP pads stay plating-only
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
R = ρ/(π·t)·acosh(d/2a) for two circular contacts on a sheet). A
soldered **THT joint** additionally assumes the **hole is filled with
solder** (core in parallel with the plating) and the **pad face
carries an average-thickness solder coat** (`SOLDER_THICKNESS_UM`,
50 µm) over the modeled copper under the pad shape. The **clipped
lead protrudes** `THT_LEAD_PROTRUSION_MM` (1.5 mm, 0 = off) on the
side opposite the component (taken from the owning footprint;
assumed `B.Cu` if it cannot be found) and a **solder cone** wraps
solder** (core in parallel with the plating) and the **pad face on
the solder side carries an average-thickness solder coat**
(`SOLDER_THICKNESS_UM`, 50 µm) over the modeled copper under the pad
shape — the solder side is the side opposite the component (taken
from the owning footprint; assumed `B.Cu` if it cannot be found),
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
zero at the pad edge, applied as extra conduction-equivalent copper
per cell. The tall solder column at the wall pulls the joint
+27 -22
View File
@@ -105,8 +105,10 @@ class Electrode:
lead/wire soldered into the hole), so the contact cells are the
copper ring at the drill wall, not the whole pad face. `solder`
additionally models a soldered THT joint: the hole is filled with
solder and the pad face carries an average-thickness solder coat
(Problem.solder_thickness_nm over `polygons`)."""
solder and the pad face on the SOLDER side (protrusion_side,
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)
contact: str = "all"
polygons: list[Polygon] | None = None
@@ -217,21 +219,23 @@ class Problem:
def contact_solder_buildups(problem: Problem) -> list[str]:
"""Soldered THT-joint contacts: the pad face is covered in solder of
average thickness solder_thickness_nm. Adds one SurfaceBuildup per
outer layer for every `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."""
"""Soldered THT-joint contacts: the pad face on the SOLDER side (the
protrusion side, opposite the component - the component-side face
stays bare) is covered in solder of average thickness
solder_thickness_nm. Adds one SurfaceBuildup there for every
`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}
outer = [n for n in ("F.Cu", "B.Cu") if n in included]
touched = []
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
for name in outer:
problem.buildups.append(
SurfaceBuildup(layer_name=name, polygons=list(e.polygons)))
touched.append(name)
SurfaceBuildup(layer_name=e.protrusion_side,
polygons=list(e.polygons)))
touched.append(e.protrusion_side)
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]:
"""Solder coat of the net's populated STITCHING through-hole pads
(ViaLink kind 'pad' with solder_filled): one pad-diameter disc per
outer layer - the exact pad shape is unknown for non-contact pads,
and the coat intersects the modeled copper at raster time anyway.
Contact pads are skipped: contact_solder_buildups already coats
them with the exact pad shape. Returns the affected layer names."""
(ViaLink kind 'pad' with solder_filled): one pad-diameter disc on
the pad's SOLDER side (the protrusion side, opposite the component;
the component-side face stays bare). The exact pad shape is unknown
for non-contact pads, and the coat intersects the modeled copper at
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}
outer = [n for n in ("F.Cu", "B.Cu") if n in included]
contacts = {e.center for e in problem.electrodes1 + problem.electrodes2
if e.drill_nm > 0 and e.center is not None}
touched = []
for v in problem.vias:
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
disc = _disc_polygon(v.x, v.y, v.pad_nm / 2.0)
for name in outer:
problem.buildups.append(
SurfaceBuildup(layer_name=name, polygons=[disc]))
touched.append(name)
SurfaceBuildup(layer_name=v.protrusion_side, polygons=[disc]))
touched.append(v.protrusion_side)
return sorted(set(touched))
+8 -2
View File
@@ -125,15 +125,21 @@ def test_solder_filled_barrel_resistance():
def test_contact_solder_coat():
"""A soldered THT contact adds an average-thickness solder buildup
over the pad face on the outer layers, lowering the spreading
resistance vs the bare barrel contact."""
over the pad face on its SOLDER side only (opposite the component),
lowering the spreading resistance vs the bare barrel contact."""
def prob():
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"
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()
assert contact_solder_buildups(p) == ["F.Cu"]
assert len(p.buildups) == 1 and p.buildups[0].layer_name == "F.Cu"