diff --git a/README.md b/README.md index 3d2712d..bbdce16 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/fill_resistance/geometry.py b/fill_resistance/geometry.py index dfd0882..2ee94bc 100644 --- a/fill_resistance/geometry.py +++ b/fill_resistance/geometry.py @@ -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) + problem.buildups.append( + 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) + problem.buildups.append( + SurfaceBuildup(layer_name=v.protrusion_side, polygons=[disc])) + touched.append(v.protrusion_side) return sorted(set(touched)) diff --git a/tests/test_barrel_contacts.py b/tests/test_barrel_contacts.py index 42ee813..7a84eae 100644 --- a/tests/test_barrel_contacts.py +++ b/tests/test_barrel_contacts.py @@ -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"