main
noah Publish tmc2262 component page (1/5) a7e2d34 7d ago
"""Proof figure: the 3D model's terminals (taken from the STEP itself) against the manufacturer land pattern (taken
from the footprint), at true scale in millimetres, seen from the top.

    ../.venv-cq/bin/python tools/proof.py <slug> <footprint.kicad_mod> <model.step> <out.png>

Grey fill: every model face on the seating plane (z = 0) facing down, i.e. the solderable terminal faces.
Teal dashed: every copper pad of the footprint (KiCad Y down, flipped to the model's Y up). Thin grey: body outline.
A terminal must sit inside its land with the drawing's toe and heel; pad 1 and the pin-1 dot must be top left.
"""
import math, re, sys
import numpy as np
import cadquery as cq
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
from matplotlib.patches import Polygon, Rectangle, Ellipse, FancyBboxPatch

slug, mod, step, out = sys.argv[1:5]
shape = cq.importers.importStep(step)
terms = []
solids = sorted(shape.solids().vals(), key=lambda s: s.Volume())
for sol in solids[:-1]:                       # every solid but the largest (the mould body) is a terminal
    for f in sol.Faces():
        c = f.Center()
        if abs(c.z) > 1e-4 or f.geomType() != 'PLANE':
            continue
        try:
            n = f.normalAt()
        except Exception:
            continue
        if n.z > -0.9:
            continue
        vs, tris = f.tessellate(0.005)
        for a, b, cc in tris:
            terms.append([(vs[a].x, vs[a].y), (vs[b].x, vs[b].y), (vs[cc].x, vs[cc].y)])
bb = shape.val().BoundingBox()

t = open(mod).read()
pads = []
for m in re.finditer(r'\(pad "([^"]*)" (smd|thru_hole) (\w+) \(at ([-\d.]+) ([-\d.]+)\) \(size ([\d.]+) ([\d.]+)\)(.*?)\n  \)?', t, re.S):
    num, kind, shp, x, y, w, h, rest = m.group(1), m.group(2), m.group(3), *map(float, m.group(4, 5, 6, 7)), m.group(8)
    if num == '' or kind == 'thru_hole':
        continue
    poly = None
    if shp == 'custom':
        q = re.search(r'\(gr_poly \(pts (.*?)\) \(width', t[m.start():m.start() + 2000], re.S)
        poly = [(x + float(a), -(y + float(b))) for a, b in re.findall(r'\(xy ([-\d.]+) ([-\d.]+)\)', q.group(1))]
    pads.append((num, shp, x, -y, w, h, poly))

fig, ax = plt.subplots(figsize=(9, 9), dpi=140)
fig.patch.set_facecolor('#0d1117'); ax.set_facecolor('#0d1117')
ax.add_patch(Rectangle((bb.xmin, bb.ymin), bb.xlen, bb.ylen, fill=False, ec='#8b949e', lw=1.0))
from matplotlib.collections import PolyCollection
ax.add_collection(PolyCollection(terms, facecolors='#c9d1d9', edgecolors='#c9d1d9', linewidths=0.2, alpha=0.85))
for num, shp, x, y, w, h, poly in pads:
    kw = dict(fill=False, ec='#00d6cd', lw=1.3, ls=(0, (4, 2)))
    if poly:
        ax.add_patch(Polygon(poly, closed=True, **kw))
    elif shp == 'oval':
        r = min(w, h) / 2
        ax.add_patch(FancyBboxPatch((x - w / 2 + r, y - h / 2 + r), w - 2 * r, h - 2 * r, boxstyle='round,pad=%f' % r, **kw))
    else:
        ax.add_patch(Rectangle((x - w / 2, y - h / 2), w, h, **kw))
    if num in ('1', 'EP'):
        ax.annotate(num, (x, y), color='#f0b429', fontsize=11, ha='center', va='center', weight='bold')
lim = max(abs(v) for p in pads for v in (p[2] + p[4] / 2, p[3] + p[5] / 2)) + 0.6
ax.set_xlim(-lim, lim); ax.set_ylim(-lim, lim); ax.set_aspect('equal')
ax.tick_params(colors='#8b949e'); [s.set_color('#30363d') for s in ax.spines.values()]
ax.grid(color='#21262d', lw=0.5)
ax.set_xlabel('mm', color='#8b949e'); ax.set_ylabel('mm', color='#8b949e')
ax.set_title('%s: model terminals (grey, from the STEP) inside the manufacturer land pattern (teal dashed), top view'
             % slug.upper(), color='#e6edf3', fontsize=10)
fig.tight_layout(); fig.savefig(out, facecolor=fig.get_facecolor())
print('OK', out, len(solids) - 1, 'terminal solids,', len(pads), 'lands')