# -*- coding: utf-8 -*- u"""刀片板 **1:1 打印稿**(A4 竖版 PDF + 600 dpi PNG),拿去和小米8 主板比孔位 / 侧键 / 排线槽。 /c/Python314/python .claude/r2/print_blade.py # 出 .claude/r2/print/blade_1to1.pdf / .png - 数据全用**板上读回**:线 / 孔 / 元件 = 最终整板快照 `_blade_board_rot180_final_0911.json`, 焊盘 = `_blade_pads.json`(下发后 --dump 的),板框 / 孔 / 槽 = `pcb_blade`(和板上折线同源)。 - 上半张 **A 面**(手机那一面,正看):板框 + 缺口、七个孔、两条槽、手机投影、K1~K4、金手指 B 排、A 面走线(淡)。 - 下半张 **B 面**(**从背面看、左右镜像**,和翻过来的实物一致):元件本体 + 位号、焊盘、B 面走线(淡)。 - 两根标尺(横 80 mm / 竖 50 mm):打出来量,不是 80.0 / 50.0 就是打印缩放了。 ⚠ 打印一律「实际大小 / 100% / 不缩放」。 """ import io import json import math import os import sys sys.stdout.reconfigure(encoding='utf-8') HERE = os.path.dirname(os.path.abspath(__file__)) sys.path.insert(0, os.path.dirname(HERE)) sys.path.insert(0, HERE) import matplotlib # noqa: E402 matplotlib.use('Agg') import matplotlib.pyplot as plt # noqa: E402 from matplotlib.patches import Rectangle, Circle, Polygon # noqa: E402 import pcb_blade as B # noqa: E402 import pcb_place as PL # noqa: E402 plt.rcParams['font.sans-serif'] = ['Microsoft YaHei', 'SimHei', 'DejaVu Sans'] plt.rcParams['axes.unicode_minus'] = False MIL = 1 / 0.0254 OUT = os.path.join(HERE, 'print') SNAP = os.path.join(HERE, '_blade_board_rot180_final_0911.json') A4 = (210.0, 297.0) # mm MM = 1 / 25.4 # mm → inch snap = json.load(io.open(SNAP, encoding='utf-8')) pads = json.load(io.open(os.path.join(HERE, '_blade_pads.json'), encoding='utf-8')) comps = {c[0]: (c[1] / MIL, c[2] / MIL, c[3], c[4]) for c in snap['comps']} sz = PL.sizes() def board_axes(fig, x0, y0, mirror=False): u"""在页面 (x0, y0)(mm,左下角)放一块 1:1 的板子坐标系:1 个数据单位 = 1 mm 纸。""" pad = 1.0 w, h = B.BW + 2 * pad, B.BH + 2 * pad ax = fig.add_axes([x0 / A4[0], y0 / A4[1], w / A4[0], h / A4[1]]) if mirror: ax.set_xlim(B.BW + pad, -pad) # 从背面看:左右镜像 else: ax.set_xlim(-pad, B.BW + pad) ax.set_ylim(-pad, B.BH + pad) ax.set_aspect('equal') ax.axis('off') return ax def outline(ax): pts = B.outline_polygon() ax.add_patch(Polygon(pts, closed=True, fill=False, ec='k', lw=0.6)) for (a, b, c, d) in B.mill_cuts(): ax.add_patch(Rectangle((a, b), c - a, d - b, fc='0.75', ec='k', lw=0.4, hatch='///')) for hn, (hx, hy) in B.phone_holes().items(): ax.add_patch(Circle((hx, hy), B.HOLE_D / 2, fc='k')) ax.add_patch(Circle((hx, hy), 1.6, fill=False, ec='k', lw=0.3, ls=':')) # M1.6 螺钉头 Ø3.2 ax.text(hx + 2.0, hy + 1.2, hn, fontsize=5) # 十字线,方便对孔 ax.plot([hx - 1.5, hx + 1.5], [hy, hy], 'w', lw=0.25) ax.plot([hx, hx], [hy - 1.5, hy + 1.5], 'w', lw=0.25) def tracks(ax, layer, col): for l in snap['lines']: if l[0] == layer: ax.plot([l[1] / MIL, l[3] / MIL], [l[2] / MIL, l[4] / MIL], color=col, lw=max(l[5] / MIL * 2.83, 0.2), alpha=0.35, solid_capstyle='round') for v in snap['vias']: ax.add_patch(Circle((v[0] / MIL, v[1] / MIL), (v[3] or 0.6 * MIL) / MIL / 2, fc='0.55', ec='none')) def pads_on(ax, layer): for p in pads: if p[6] != layer: continue ref, pin, x, y, w, h = p[:6] col = '#b8860b' if ref == 'J1' else ('#808080' if p[7] == 'GND' else '#2e7d32') ax.add_patch(Rectangle((x - w / 2, y - h / 2), w, h, fc=col, ec='none', alpha=0.85)) def bodies(ax, side): for ref, (x, y, rot, lay) in comps.items(): if ref == 'J1' or ref not in sz: continue if (lay == 2) != (side == 'B'): continue w, h = PL.wh(ref, int(rot) % 360, sz) ax.add_patch(Rectangle((x - w / 2, y - h / 2), w, h, fill=False, ec='#1a3fa0', lw=0.35)) ax.text(x, y, ref, fontsize=3.2 if w * h < 6 else 4.5, ha='center', va='center', color='#1a3fa0') def ruler(fig, x0, y0, length, vertical=False): ax = fig.add_axes([0, 0, 1, 1], facecolor='none') ax.set_xlim(0, A4[0]) ax.set_ylim(0, A4[1]) ax.axis('off') if vertical: ax.plot([x0, x0], [y0, y0 + length], 'k', lw=0.6) for k in range(0, int(length) + 1): t = 2.0 if k % 10 == 0 else (1.2 if k % 5 == 0 else 0.6) ax.plot([x0, x0 + t], [y0 + k, y0 + k], 'k', lw=0.25) ax.text(x0 + 3, y0 + length / 2, u'%d mm' % length, fontsize=6, rotation=90, va='center') else: ax.plot([x0, x0 + length], [y0, y0], 'k', lw=0.6) for k in range(0, int(length) + 1): t = 2.0 if k % 10 == 0 else (1.2 if k % 5 == 0 else 0.6) ax.plot([x0 + k, x0 + k], [y0, y0 + t], 'k', lw=0.25) ax.text(x0 + length / 2, y0 - 3.5, u'%d mm(打出来量这根,应为 %.1f)' % (length, length), fontsize=6, ha='center') return ax def main(): os.makedirs(OUT, exist_ok=True) fig = plt.figure(figsize=(A4[0] * MM, A4[1] * MM)) # 整页坐标轴放文字 txt = fig.add_axes([0, 0, 1, 1], facecolor='none') txt.set_xlim(0, A4[0]) txt.set_ylim(0, A4[1]) txt.axis('off') left = (A4[0] - B.BW) / 2.0 ya, yb = 196.0, 88.0 # 两块板的下沿(mm) txt.text(A4[0] / 2, 287, u'PhoneRail 刀片 90 × 68 · 1:1 打印稿(2026-09-11,主板转 180° 版)', fontsize=9, ha='center', weight='bold') txt.text(A4[0] / 2, 281, u'打印选「实际大小 / 100% / 不缩放」。先量下方标尺:横 80.0 mm、竖 50.0 mm,不对就是缩放了。', fontsize=6.5, ha='center') txt.text(left, ya + B.BH + 4, u'A 面(手机那一面,正看):板框 / 缺口 / 七个定位孔 Ø1.8(虚线圈 = 螺钉头 Ø3.2)/ 两条排线槽 / 手机投影(虚线)/ K1~K4', fontsize=5.5) txt.text(left, yb + B.BH + 4, u'B 面(器件面,从背面看、左右镜像 —— 和把板子翻过来看到的一致)', fontsize=5.5) # ── A 面 ── ax = board_axes(fig, left - 1.0, ya - 1.0) outline(ax) px0, py0 = B.PHONE_ORG ax.add_patch(Rectangle((px0, py0), B.PHONE_WH[0], B.PHONE_WH[1], fill=False, ec='#c62828', lw=0.5, ls='--')) ax.text(px0 + 1, py0 + B.PHONE_WH[1] - 3, u'小米8 主板投影(接口边朝上)', fontsize=5, color='#c62828') tracks(ax, 1, '#d32f2f') pads_on(ax, 1) for ref in ('K1', 'K2', 'K3', 'K4'): x, y = comps[ref][:2] ax.text(x + 1.3, y, ref, fontsize=4, color='#c62828', va='center') ax.text(45, -0.2, 'J1', fontsize=5, ha='center', va='top') # ── B 面(镜像)── bx = board_axes(fig, left - 1.0, yb - 1.0, mirror=True) outline(bx) tracks(bx, 2, '#1565c0') pads_on(bx, 2) bodies(bx, 'B') ruler(fig, left, 40.0, 80) ruler(fig, left - 8.0, 20.0, 50, vertical=True) txt.text(left + 20, 20, u'文件:.claude/r2/print/blade_1to1.pdf(print_blade.py 生成,数据 = 板上读回)', fontsize=5) pdf = os.path.join(OUT, 'blade_1to1.pdf') png = os.path.join(OUT, 'blade_1to1.png') fig.savefig(pdf) # ⚠ 不许 bbox_inches='tight':那会裁页面、改比例 fig.savefig(png, dpi=600) print(u'已出 %s / %s(A4 %.0f×%.0f mm,板子 1:1)' % (pdf, png, A4[0], A4[1])) if __name__ == '__main__': main()