main
Caleb Publish 0.1.1 4e55566 1d ago
#!/usr/bin/env python3
"""Adom-built 3D model of Samtec FTSH-107-01-L-DV-K-A (CC-BY-SA-4.0), cadquery.

Source: Samtec print FTSH-1XX-XX-XXX-DV-XXX-XXX, revision FQ
(https://suddendocs.samtec.com/prints/ftsh-1xx-xx-xxx-dv-xxx-xxx-mkt.pdf), sheet 1 Fig 1 + Table 1, sheet 2 Fig 2
(-K option) and Fig 3 (-A option, molded alignment pins). Printed dimensions are used as printed; features the print
does not dimension were measured off the vector drawing rendered at 1200 dpi (sheet 1 at 5:1 = 236.2 px/mm, sheet 2
at 3:1 = 141.7 px/mm, both calibrated on printed edges: 0.41 post, 1.27 pitch, 3.43 body, 2.51 body height).

Frame (Adom standard): Z-up, seating plane (bottom of the SMT tails) on z = 0, origin at the pad-pattern centre of the
page footprint, contact rows along Y (pin 1 at +Y, odd contacts at -X, even at +X, as the footprint: KiCad pad 1 at
(-2.035, -3.81) with KiCad y down). Usage: build_ftsh_107_k_a.py <out.step>
"""
import sys
import cadquery as cq

N = 7                     # positions per row (-107)
P = 1.27                  # pitch, both directions (print Fig 1: .050 [1.27])
ROW = 1.27                # row-to-row (print Fig 1 side view: .050 +/- .008 [1.27 +/- 0.20])
BODY_L = N * P            # body length, print Fig 1: No OF POS x .0500 [1.27] = 8.89
BODY_W = 3.43             # body width, print Fig 1 / Fig 2: .135 [3.43] REF
WEB_W = 2.92              # measured (sheet 1, 690 px / 236.2): web of the I-section end profile
FLANGE_T = 0.62           # measured (147 px / 236.2): top and bottom flange thickness
TAIL_T = 0.41             # tail / post section, print Fig 1: .016 [0.41] SQ REF
Z_BODY_BOT = TAIL_T       # body sits on the tails: 2.92 - 2.51 = 0.41
Z_BODY_TOP = 2.92         # print Fig 1: .115 [2.92] seating plane to body top
A_POST = 3.05             # Table 1, lead style -01: "A" .120 [3.05] above the body
TAIL_SPAN = 5.8           # print Fig 1: .23 [5.8] tail tip to tail tip
POST_TIP = 0.15           # chamfer on the post tip (drawn pointed; not dimensioned)
# -K key (sheet 2 Fig 2): FTSH-50-CN body, .200 [5.08] REF wide, .126 [3.20] REF above the body top,
# opening .085 [2.16] REF on the row-1 side; bars stand 0.83 mm proud of the body on both sides (measured,
# 117 px / 141.7 = 0.83), so the key is 5.08 x 5.08 in plan.
KEY_W = 5.08
KEY_H = 3.20
KEY_SLOT = 2.16
KEY_PROUD = 0.83
# -A molded alignment pins (sheet 2 Fig 3): dia .035 [0.89] REF, full R, .050 [1.27] REF below the body,
# .034 [0.86] (critical) below the seating plane, (No OF POS - 2) x .050 [1.27] = 6.35 apart on the centreline.
APIN_D = 0.89
APIN_TIP_Z = -0.86
APIN_PITCH = (N - 2) * P
# Edge notches between columns (sheet 1 Fig 1 top view; absent under the key): measured 0.39 wide at the edge,
# 0.20 at the bottom, 0.63 deep (sheet 2, 141.7 px/mm).
NOTCH_W0, NOTCH_W1, NOTCH_D = 0.39, 0.20, 0.63


def col_y(k):            # contact column k = 0..N-1 (contacts 2k+1, 2k+2); pin 1 at +Y
    return (N - 1) / 2 * P - k * P


def body():
    h_web = Z_BODY_TOP - Z_BODY_BOT - 2 * FLANGE_T
    bot = cq.Workplane('XY').box(BODY_W, BODY_L, FLANGE_T, centered=(True, True, False)).translate((0, 0, Z_BODY_BOT))
    web = cq.Workplane('XY').box(WEB_W, BODY_L, h_web, centered=(True, True, False)).translate((0, 0, Z_BODY_BOT + FLANGE_T))
    top = cq.Workplane('XY').box(BODY_W, BODY_L, FLANGE_T, centered=(True, True, False)).translate((0, 0, Z_BODY_TOP - FLANGE_T))
    b = bot.union(web).union(top)
    # edge notches between columns outside the key span
    for k in range(N - 1):
        y = (col_y(k) + col_y(k + 1)) / 2
        if abs(y) < KEY_W / 2:
            continue
        for sx in (-1, 1):
            x0 = sx * BODY_W / 2
            x1 = sx * (BODY_W / 2 - NOTCH_D)
            pts = [(x0 + sx * 0.01, y - NOTCH_W0 / 2), (x1, y - NOTCH_W1 / 2), (x1, y + NOTCH_W1 / 2), (x0 + sx * 0.01, y + NOTCH_W0 / 2)]
            cut = cq.Workplane('XY').polyline(pts).close().extrude(Z_BODY_TOP - Z_BODY_BOT + 0.02).translate((0, 0, Z_BODY_BOT - 0.01))
            b = b.cut(cut)
    return b


def key():
    x_in, x_out = BODY_W / 2, BODY_W / 2 + KEY_PROUD
    h = Z_BODY_TOP + KEY_H - Z_BODY_BOT
    t = x_out - x_in
    # row-2 side (+X): full bar
    k = cq.Workplane('XY').box(t, KEY_W, h, centered=(False, True, False)).translate((x_in, 0, Z_BODY_BOT))
    # row-1 side (-X): two blocks either side of the 2.16 opening
    blk = (KEY_W - KEY_SLOT) / 2
    for sy in (-1, 1):
        yc = sy * (KEY_SLOT / 2 + blk / 2)
        k = k.union(cq.Workplane('XY').box(t, blk, h, centered=(False, True, False)).translate((-x_out, yc, Z_BODY_BOT)))
    return k


def contacts():
    out = None
    for k in range(N):
        y = col_y(k)
        for sx in (-1, 1):                    # -1: odd contact (row 01), +1: even (row 02)
            x = sx * ROW / 2
            post = (cq.Workplane('XY').box(TAIL_T, TAIL_T, Z_BODY_TOP + A_POST - TAIL_T, centered=(True, True, False))
                    .edges('>Z').chamfer(POST_TIP).translate((x, y, TAIL_T)))
            x_in = x - sx * TAIL_T / 2
            x_out = sx * TAIL_SPAN / 2
            foot = cq.Workplane('XY').box(abs(x_out - x_in), TAIL_T, TAIL_T, centered=(False, True, False)).translate((min(x_in, x_out), y, 0))
            c = post.union(foot)
            out = c if out is None else out.union(c)
    return out


def align_pins():
    out = None
    r = APIN_D / 2
    for sy in (-1, 1):
        y = sy * APIN_PITCH / 2
        cyl = cq.Workplane('XY').circle(r).extrude(Z_BODY_BOT - (APIN_TIP_Z + r)).translate((0, y, APIN_TIP_Z + r))
        tip = cq.Workplane('XY').sphere(r).translate((0, y, APIN_TIP_Z + r))
        p = cyl.union(tip)
        out = p if out is None else out.union(p)
    return out


def build():
    housing = body().union(key()).union(align_pins())
    pins = contacts()
    asm = cq.Assembly(name='FTSH-107-01-L-DV-K-A')
    asm.add(housing, name='housing', color=cq.Color(0.10, 0.10, 0.11))
    asm.add(pins, name='contacts', color=cq.Color(0.83, 0.69, 0.22))
    return asm, housing, pins


if __name__ == '__main__':
    asm, housing, pins = build()
    asm.save(sys.argv[1], exportType='STEP')
    bb = housing.union(pins).val().BoundingBox()
    print(f'bbox x {bb.xmin:.3f}..{bb.xmax:.3f}  y {bb.ymin:.3f}..{bb.ymax:.3f}  z {bb.zmin:.3f}..{bb.zmax:.3f}')