main
John Lauer Component record for ltc2945cms-pbf: files batch 3 16c3b96 23h ago
#!/usr/bin/env python3
"""Overlay a 3D model of the MS12 (12-lead MSOP) package on Analog Devices' package drawing (LTC DWG 05-08-1668 Rev A,
LTC2945 datasheet Rev. D p.32, rendered at 400 dpi).

The drawing says "DRAWING NOT TO SCALE", so each view is calibrated on its own printed reference lines and the scale
per axis is reported:
  top view : x from the 4.039 mm body-length extension lines, y from the 3.00 mm body-width extension lines;
  side view: x shares the top view's columns; z from the seating plane (z = 0) and the 0.86 REF body-top line
             (z = 0.1016 + 0.86 = 0.9616 mm nominal).
The pixel positions of those lines were found by dark-pixel projections of the crops (rows/columns with long runs).
The model is rotated +90 deg about Z so that its pin 1 lands bottom-left like the drawing's top view.
Model faces are drawn at 45 % opacity (red body, blue leads, green pin-1 dimple); the residuals at printed features
NOT used for calibration (lead tips, standoff) are written on the image.

usage: overlay_ms12.py page32_400dpi.png model.step out.png out.json   (run with the CadQuery python)"""
import sys, os, json
import numpy as np
from PIL import Image, ImageDraw, ImageFont
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from ortho import load_tris

page, step, out, outj = sys.argv[1:5]
im = Image.open(page).convert("RGB")
TOP = (2050, 1400, 2600, 2200)
SIDE = (2050, 2200, 2600, 2550)
top_x0, top_x1 = 55.0, 368.0           # 4.039 mm body length extension lines (crop columns)
top_y0, top_y1 = 333.5, 584.5          # 3.00 mm body width extension lines (crop rows)
tip_y0, tip_y1 = 251.0, 669.5          # 4.90 mm tip-to-tip extension lines (not used for calibration)
side_seat, side_top = 208.0, 92.5      # seating plane, 0.86 REF top line (side crop rows)
side_body_bot = 189.5                  # 0.86 REF lower line (body underside, not used for calibration)
sx = (top_x1 - top_x0) / 4.039; cx = (top_x0 + top_x1) / 2
sy = (top_y1 - top_y0) / 3.00; cy = (top_y0 + top_y1) / 2
sz = (side_seat - side_top) / (0.1016 + 0.86)

tris = load_tris(step)
R = np.array([[0, -1, 0], [1, 0, 0], [0, 0, 1]], float)

def draw_view(crop, project, view_dir):
    base = crop.convert("RGBA")
    lay = Image.new("RGBA", base.size, (0, 0, 0, 0)); d = ImageDraw.Draw(lay)
    items = []
    for p, col in tris:
        q = p @ R.T
        n = np.cross(q[1] - q[0], q[2] - q[0]); nn = np.linalg.norm(n)
        if nn == 0 or n @ view_dir / nn <= 1e-6: continue
        items.append(((q @ view_dir).mean(), [project(v) for v in q], col))
    items.sort(key=lambda t: t[0])
    for _, xy, col in items:
        lum = sum(col) / 3 if col is not None else 0.0
        c = (230, 60, 40, 115) if lum < 0.15 else ((40, 110, 230, 115) if lum > 0.5 else (20, 170, 60, 200))
        d.polygon(xy, fill=c)
    return Image.alpha_composite(base, lay)

top_img = draw_view(im.crop(TOP), lambda v: (cx + v[0] * sx, cy - v[1] * sy), np.array([0, 0, 1.0]))
side_img = draw_view(im.crop(SIDE), lambda v: (cx + v[0] * sx, side_seat - v[2] * sz), np.array([0, -1.0, 0]))
allq = np.concatenate([t[0] for t in tris]) @ R.T
standoff_x = ((side_seat - side_body_bot) / sz) / 0.1016
res = {
    "scale_px_per_mm": {"top_x": round(sx, 2), "top_y": round(sy, 2), "side_z": round(sz, 2)},
    "drawing_note": "DRAWING NOT TO SCALE (note 2 on the drawing): x and y scales differ by %.1f %%, and the side view's standoff is drawn about %.1fx its printed value." % (abs(sy / sx - 1) * 100, standoff_x),
    "lead_tips_drawn_mm": round((tip_y1 - tip_y0) / sy, 3),
    "lead_tips_model_mm": round(float(allq[:, 1].max() - allq[:, 1].min()), 3),
    "lead_tips_printed_mm": "4.90 +-0.152",
    "calibrated_on": ["4.039 body length (x)", "3.00 body width (y)", "seating plane and 0.86 REF top line (z)"],
}
F = "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf"
f = ImageFont.truetype(F, 18); fb = ImageFont.truetype(F, 22)
W = 1150; H = 70 + top_img.height + 20 + side_img.height + 210
sheet = Image.new("RGBA", (W, H), (255, 255, 255, 255)); dr = ImageDraw.Draw(sheet)
dr.text((20, 15), "LTC2945CMS#PBF 3D model over ADI drawing 05-08-1668 Rev A (datasheet Rev. D p.32)", fill=(0, 0, 0), font=fb)
dr.text((20, 42), "red: model body, blue: model leads, green: model pin-1 dimple; 45 % opacity; model rotated +90 deg about Z", fill=(60, 60, 60), font=f)
sheet.alpha_composite(top_img, (20, 70))
sheet.alpha_composite(side_img, (20, 90 + top_img.height))
y = 100 + top_img.height + side_img.height
for l in [
    "Top view calibrated on 4.039 (x, %.1f px/mm) and 3.00 (y, %.1f px/mm) extension lines." % (sx, sy),
    "Side view: x as top view, z on seating plane and 0.86 REF top (%.1f px/mm)." % sz,
    "Lead tips: model %.3f mm; printed 4.90 +-0.152; drawn %.3f mm at the y scale." % (res["lead_tips_model_mm"], res["lead_tips_drawn_mm"]),
    "Drawing note 2: NOT TO SCALE; standoff drawn about %.1fx its printed 0.1016 mm." % standoff_x,
    "Pin 1: drawing dot and pin 1 bottom-left; model pin-1 dimple bottom-left.",
]:
    dr.text((20, y), l, fill=(0, 0, 0), font=f); y += 30
sheet.convert("RGB").save(out)
json.dump(res, open(outj, "w"), indent=1)
print(json.dumps(res))