# -*- coding: utf-8 -*- u"""R2 底板 PCB(M8):板框 + 分区 + 插槽阵列 + 安装孔。 /c/Python314/python .claude/r2/pcb_base.py # 只算 + 自检 /c/Python314/python .claude/r2/pcb_base.py --outline # 画/重画板框(幂等) /c/Python314/python .claude/r2/pcb_base.py --holes # 打安装孔 /c/Python314/python .claude/r2/pcb_base.py --verify # 读回校验 与 `pcb_blade.py` 是同一套接口(`PCB` / `BW` / `BH` / `ZONES` / `mill_cuts()` …), 下游的 `pcb_place` / `router` / `pcb_pour` 用 `--base` 切到这一份。 ## 坐标系 **板左下角 = (0, 0),x 向右、y 向上**(EasyEDA PCB 里 y 向上为正,方案 §22.5)。 x = 槽距方向(5 槽并排),y = 槽长方向 / 板深。方案 §6.5 那张分区图可以直接抄。 ## ⛔ 板框必须是**折线**不是导线 `pcb_PrimitiveLine` 在嘉立创里是**导线(铜)**。M4b~M6 的刀片板框就是这么画的: 渲染成品红、DRC 干净、四道几何校验全过,而客户端根本不认它是板框 (自动布线一路弹「请先绘制板框!」)。见方案 §27.3。 ⇒ 板框只用 `pcb_PrimitivePolyline`。底板文档里现成那 4 条 `Line`(建工程时的默认框) 属于同一个坑,`push_outline()` 会把它们删掉。 ## 刀片投影是硬碰撞,不是净距问题 插槽把刀片底边抬到底板上方 ~11 mm ⇒ **投影带内元件 ≤ 8 mm**(方案 §6.6)。 两颗 470 µF 直立电解、DC-005 座、TH 排针 / 风扇座都只能放到投影带**外面** (`y < PROJ[0]` 或 `y > PROJ[1]`)。`zone_of()` / `height_ok()` 就是干这个的。 """ import io import json import math import os import sys sys.stdout.reconfigure(encoding='utf-8') HERE = os.path.dirname(os.path.abspath(__file__)) # ⚠ 顺序不能反:`.claude` 下有同名的 R1 脚本,先插 .claude 会 import 到 R1 那一份且不报错。 sys.path.insert(0, os.path.dirname(HERE)) sys.path.insert(0, HERE) from eda import run # noqa: E402 import fp_edge # noqa: E402 PCB = '7b9524b6aaf6f235' # BASE-PCB(r2-uuids.json) MIL = fp_edge.MIL # ── 板框(mm,出处 方案 §3.1)───────────────────────────────────────── BW, BH = 110.0, 125.0 CORNER_R = 5.0 # 底板四角 R5(刀片是 R2) CORNER_SEG = 10 OUTLINE_W = 0.15 # ── 插槽阵列(方案 §6.3;槽距 20.32 = T1 定稿)────────────────────────── SLOT_PITCH = 20.32 SLOT_N = 5 SLOT_CX = BW / 2.0 # 阵列中心 = 板中心 = Hub 的 x SLOT_CY = 64.5 # 槽体中心 y ⇒ 槽体 45.0~84.0(39 mm) # ⚠ 39 是方案 §3.1 抄的「槽体长度」,**不是封装占的地方**。`PCIE-64P11L` 的焊盘外框 # 实测 **42.0 × 9.2**(含两端 3.0×5.0 的屏蔽脚 65),转 90° 之后沿 y 占 42、沿 x 占 9.2。 # 分区就按实测走,别按 39 —— 差的那 3 mm 正好会让槽体啃进 hub / ctrl 两带。 SLOT_BODY_L = 42.0 # 槽体沿 y(= 转 90° 后的封装长边,实测) SLOT_BODY_W = 9.2 # 槽体沿 x(实测) # 焊盘两列:中心 x = 槽心 ± 3.6,盘 2.0(x) × 0.7(y),即 x 占 ±2.6~±4.6。 # ⇒ 相邻槽之间的**布线走廊净宽 = 20.32 − 9.2 = 11.12**,两端各剩 9.76。 PAD_COL_DX = 3.6 PAD_L, PAD_W = 2.0, 0.7 # 刀片投影:刀片 98 宽沿 y 铺开,中心对齐槽体中心 BLADE_W = 98.0 PROJ = (round(SLOT_CY - BLADE_W / 2, 2), round(SLOT_CY + BLADE_W / 2, 2)) # 15.5 ~ 113.5 H_PROJ = 8.0 # 投影带内元件高度上限(方案 §6.6,已留 3 mm 余量) # 刀片 x(刀片自身坐标)→ 底板 y 的映射:blade_x = 0 落在投影带下沿。 # ⇒ 刀片的**功率列**(blade x 72~98,最吵最热)落在 y 87.5~113.5,正对风墙; # 手机主板(blade x 3~70)落在 y 18.5~85.6。 def blade_x_to_base_y(bx): return round(PROJ[0] + bx, 3) def slot_x(i): u"""槽 i(1~5)的中心 x。""" return round(SLOT_CX + (i - (SLOT_N + 1) / 2.0) * SLOT_PITCH, 3) def slot_xs(): return [slot_x(i) for i in range(1, SLOT_N + 1)] # ── 分区(方案 §6.5)────────────────────────────────────────────────── # ⚠ 'slots' 带内除了 J34~J38 什么都不放:那 39×110 是 D± 走廊 + 槽体本身。 ZONES = { 'input': (0.0, 0.0, BW, 15.5), # 投影带外:DC 座 / F1 / Q1 / C2,C3 直立电解 / 泄放 'hub': (0.0, 15.5, BW, 43.5), # U32 居中 + Y1 + U31 + 5 个 D± 退路座 'slots': (0.0, 43.5, BW, 85.5), # 5 个插槽(42 mm)+ 走廊(L2 一刀不许割) 'ctrl': (0.0, 85.5, BW, 113.5), # ESP32 / mux / DAC / 3V3 / 5V(全部 ≤8 mm) 'fan': (0.0, 113.5, BW, BH), # 投影带外:风扇座 / J33 应急口(可高) } # ── 12V 母线(9 A):**梳子形**,一条脊 + 五根齿 ──────────────────────── # ⛔ **不能靠布线器的 1.5 mm 线送 9 A**。布线器把 `+12V` 当一个网布成一棵树, # 树干那一段要背 ~7.5 A 而只有 1.5 mm(1 oz 外层 ΔT20K 只带 3.2 A), # DRC / 连通性 / 几何四道判据一条都不会响。⇒ 母线做成铺铜,布线器的线与它并联。 # # ⛔ **为什么不是「槽阵南沿一条横杠」**(第一版就是这么画的): # 横杠沿 x 送电,而五对 D± 要从 U32 往北**竖着穿过它** —— 两层板上一根竖线 # 就把横杠**整个切断**,五处切口把它剁成五段,每段还得各自找电。 # 横杠上的「铜宽够不够」根本不是问题所在,问题是它压根不连通, # **而铺铜管理器、DRC、断面校核三个都只会告诉你「有铜」。** # # ⇒ 改成梳子: # · `SPINE` 横脊走**输入带最南那 5 mm**(y 0.8~5.8),从 `BUS_IN` 一路往西 # 钻到 `J1` 的桶身底下。那一条上只有 `J1` 的三只脚(全在 y≥8.2)和板边, # 南北穿过它的东西一个都没有 —— 它南边就是板边。 # ⛔ **横脊原来放在 hub 带(y 16.2~24.2),那是错的**:hub 带一共只有 28 mm, # 横脊一占,`U32` 北边的 D± 扇出带就只剩 8.6 mm,而 # **10 根 1.2 mm 的线要从 0.5 mm 间距的 QFN 脚上散开到 1.41 mm 间距, # 光斜着散开就要 4.1 mm**,再加三条车道 8.1 —— 塞不下。 # 实测现象:内侧那三对(`USB2`/`USB3`/`USB4`)一段都布不出来。 # 挪到输入带之后 hub 带整条 28 mm 归 `U32`,扇出带 14.6 mm,宽裕。 # · 五根**支路**从脊往北接各槽的 +12V 针(y 48~50)。它们只有 **1.5 A**, # 交给布线器画 1.5 mm 的线就够(1 oz 外层 ΔT20K 带 3.2 A),不必铺铜。 # # ⛔ **齿曾经也是铺铜,画完才发现走不了**:槽的第 65 脚(屏蔽脚,3.0×5.0,接 GND) # 正好坐在两列焊盘之间那条 5.2 mm 无盘带的**正中**(转 90° 后 y 43.5~46.5), # 把齿从中间堵死 —— 断面校核量出来是 **0.00 mm**。 # 这是「按封装的焊盘外框想当然」的典型:外框里看着是空的,实际有一只大脚。 # # ⚠ `BUS_IN` 西端必须**盖住 `Rs6` 的 +12V 焊盘**(x 63.2~65.2): # 不盖住的话,9 A 要靠布线器那根 1.5 mm 的线从分流电阻爬到母线上。 # ⛔ 脊只能 **5 mm 高**,不是最初画的 8 —— hub 带一共只有 28 mm,要塞下 # 「脊 + U32(9.75) + 南边逃逸 + 北边 D± 扇出」。第一版 8 mm 的脊把 U32 逼到 y=37, # 它北沿离插槽焊盘只剩 **1.6 mm**,五对 D± 根本扇不开(布线器实测 USB2 布了 0 段)。 # 载流上 5 mm 也够:脊上最大电流不是 9 A 而是 ~6 A(进料点在东段 x63~91, # 往西只带 3 个槽 + 两颗 Buck),1 oz 外层 6 A ΔT20K 要 5.2 mm。 SPINE = (8.0, 0.8, 91.0, 5.8) # 西端 8.0:让开 H1 螺钉头(0.8~7.2) BUS_IN = (63.0, 0.8, 91.0, 16.0) # 输入带里的 9 A 母线(15.2 mm 高) def bus_rects(): return [BUS_IN, SPINE] # ── D± 扇出带:`U32` 北沿到槽体南沿之间那条,**一件都不许放** ───────────── # 五对 D± 全部在这里分东西两路(USB1/USB2 往东、USB4/USB5 往西、USB3 直上)。 # 带子 = `U32` 北沿(28.9) 到槽体南沿(43.5) = **14.6 mm**: # 先要 4.1 mm 让 10 根线从 0.5 mm 的脚距斜着散到 1.41 mm 的线距,再要三条 2.7 的车道。 # ⛔ 不设这条禁区的实测后果:排布器把 `U32` 的去耦和退路座填满整条带, # 布线器报「`USB2` 布了 0 段」,而摆位自检 / 朝向断言 / DRC **一条都不响**。 FANOUT = (2.0, 28.9, 108.0, 43.5) # ── ESP32 天线缺口:西板边挖掉一块,天线整段悬空 ───────────────────────── # 2026-09-09 用户拍板。**原来的做法是错的**:`U20` 摆在板中央 (17, 96, rot0)、 # 天线朝北埋在板里,净空只有 20 × 7.5 一小块 ⇒ 天线尖**正前方 1.0 mm 就是 # 重新铺起来的整块地平面**(再往北 3 mm 是风扇带的 RT601/RT602), # 东西两侧净空也只比模组宽 0.5 mm。等于给天线加了块反射板。 # 而设计文档 §PCB 要点写的是「天线区域下方禁止铺铜、禁止走线,**模组放板边**」—— # 后半条从来没做到,前半条也只做了「下方那一小块」。 # ⇒ `U20` 转 **rot=90(天线朝西)**挪到西板边,天线那 7 mm 下方的**板材整块挖掉**。 # # 尺寸依据(全部从 `_base_pads.json` 量出来,不是抄封装库): # 模组本体 18.0 × 25.5;焊盘外框 19.0 × 19.0(三面出脚,每边比本体宽 0.5) # ⇒ 天线段 = 25.5 − (19.0 − 0.5×2) = **7.5 mm**(本体 7.0 + 0.5 余量)。 # 缺口比模组两侧各宽 1.0 mm(y 85~107),深 7.5;`U20` 焊盘西沿 8.5, # 离缺口边 1.0 mm ⇒ 焊盘还在板上、天线整段在板外,只有天线根部 1 mm 压着无铜的 FR4。 # ⚠ 内角是铣刀铣出来的,实际会带 Ø2 铣刀的圆角,不必在板框里画圆角。 ANT_NOTCH = (0.0, 85.0, 7.5, 107.0) def notch_rect(): u"""天线缺口(板材挖掉的那一块)。板框在这里内凹,不走 `mill_cuts()`。""" return ANT_NOTCH def keepout_rects(): u"""摆位禁区(不是铺铜禁区):12V 母线 + D± 扇出带 + 天线缺口(那儿没板材)。""" return bus_rects() + [FANOUT, ANT_NOTCH] # ── 安装孔(M3 尼龙立柱,NPTH)──────────────────────────────────────── # **2026-09-09 用户拍板:只留四角 4 个**(原来还在东西两侧中点加了 H5/H6 当插拔力的支点, # 用户决定省掉两颗立柱)。⇒ 5 片刀片一起插拔时板中部无支撑,装配时**从槽阵下方托住再插**。 HOLE_D = 3.2 # M3 过孔 HOLE_KEEP = 3.2 # 螺钉头 Ø6.0 /2 + 余量 HOLE_INSET = 4.0 # 孔心离板边 MOUNT_HOLES = { 'H1': (HOLE_INSET, HOLE_INSET), 'H2': (BW - HOLE_INSET, HOLE_INSET), 'H3': (HOLE_INSET, BH - HOLE_INSET), 'H4': (BW - HOLE_INSET, BH - HOLE_INSET), } # ── 下游脚本要的空壳(底板没有排线槽 / 手机定位孔 / 金手指缺口)────────── FLEX_W = {} SLOT_Y = None PHONE_ORG = None PHONE_WH = None def phone_holes(): u"""下游(`router` / `pcb_place`)拿这个当「两层都不许压的孔」表。 刀片上是手机主板定位孔;底板上没有手机,但四个 M3 安装孔一样不许压 —— 它们是 `pcb_PrimitivePad` 不是元件引脚,**不在 `_base_pads.json` 里**, 不从这儿喂进去布线器就会直接从孔上压过去(而 DRC 只报 NPTH 到铜,很晚才看见)。 """ return dict(MOUNT_HOLES) def mill_cuts(): u"""板上铣穿的内槽:底板一个都没有。""" return [] def notch_break(): return None def corner(cx, cy, a0, a1): pts = [] for k in range(CORNER_SEG + 1): a = math.radians(a0 + (a1 - a0) * k / CORNER_SEG) pts.append((cx + CORNER_R * math.cos(a), cy + CORNER_R * math.sin(a))) return pts def outline_pts(): u"""板框点列(逆时针,从底边左端 (r,0) 起,闭合回 (r,0))。 ⚠ 相邻重合点必须去掉:零长度线段会被编辑器静默吃掉,读回条数对不上(刀片踩过)。 """ r = CORNER_R pts = [(r, 0.0), (BW - r, 0.0)] pts += corner(BW - r, r, 270, 360) pts += [(BW, BH - r)] pts += corner(BW - r, BH - r, 0, 90) pts += [(r, BH)] pts += corner(r, BH - r, 90, 180) # ESP32 天线缺口:西边从北往南走,走到 y=107 拐进板里 7.5,到 y=85 再拐回来 pts += [(0.0, ANT_NOTCH[3]), (ANT_NOTCH[2], ANT_NOTCH[3]), (ANT_NOTCH[2], ANT_NOTCH[1]), (0.0, ANT_NOTCH[1])] pts += [(0.0, r)] pts += corner(r, r, 180, 270) out = [pts[0]] for q in pts[1:]: if abs(q[0] - out[-1][0]) > 1e-9 or abs(q[1] - out[-1][1]) > 1e-9: out.append(q) assert abs(out[0][0] - out[-1][0]) < 1e-9 and abs(out[0][1] - out[-1][1]) < 1e-9, \ u'板框点列没有闭合' return out def outline_polygon(): return outline_pts() def zone_of(y): for k, z in ZONES.items(): if z[1] <= y < z[3]: return k return None def height_ok(y, h): u"""y 处允许 h mm 高的件吗(刀片投影是硬碰撞)。""" return not (PROJ[0] <= y <= PROJ[1] and h > H_PROJ) def selfcheck(): bad = [] xs = slot_xs() if abs(xs[0] - 14.36) > 0.01 or abs(xs[-1] - 95.64) > 0.01: bad.append(u'槽阵 x 与方案 §6.3 对不上: %s' % xs) # 槽体不许出板边 if xs[0] - SLOT_BODY_W / 2 < 2.0 or xs[-1] + SLOT_BODY_W / 2 > BW - 2.0: bad.append(u'槽体贴板边太近: %.2f / %.2f' % (xs[0] - SLOT_BODY_W / 2, xs[-1] + SLOT_BODY_W / 2)) # 槽体必须整个落在 'slots' 带里 z = ZONES['slots'] if abs((SLOT_CY - SLOT_BODY_L / 2) - z[1]) > 0.01 or abs((SLOT_CY + SLOT_BODY_L / 2) - z[3]) > 0.01: bad.append(u'槽体 y %.1f~%.1f 与 slots 带 %.1f~%.1f 对不上' % (SLOT_CY - SLOT_BODY_L / 2, SLOT_CY + SLOT_BODY_L / 2, z[1], z[3])) # 分区必须首尾相接、铺满板 ks = sorted(ZONES, key=lambda k: ZONES[k][1]) for a, b in zip(ks, ks[1:]): if abs(ZONES[a][3] - ZONES[b][1]) > 1e-6: bad.append(u'分区 %s 顶 %.1f != %s 底 %.1f' % (a, ZONES[a][3], b, ZONES[b][1])) if abs(ZONES[ks[0]][1]) > 1e-6 or abs(ZONES[ks[-1]][3] - BH) > 1e-6: bad.append(u'分区没铺满 0~%.1f' % BH) # 投影带边界必须正好是 input 顶 / fan 底 —— 高件区就是投影带外那两条 if abs(PROJ[0] - ZONES['input'][3]) > 1e-6 or abs(PROJ[1] - ZONES['fan'][1]) > 1e-6: bad.append(u'刀片投影 %s 与 input/fan 带边界对不上' % (PROJ,)) # 安装孔不许压在槽体上,也不许出板 for n, (hx, hy) in MOUNT_HOLES.items(): if not (HOLE_KEEP < hx < BW - HOLE_KEEP and HOLE_KEEP < hy < BH - HOLE_KEEP): bad.append(u'安装孔 %s 出板或贴边: (%.1f, %.1f)' % (n, hx, hy)) for sx in slot_xs(): if abs(hx - sx) < SLOT_BODY_W / 2 + HOLE_KEEP and \ ZONES['slots'][1] - HOLE_KEEP < hy < ZONES['slots'][3] + HOLE_KEEP: bad.append(u'安装孔 %s 压到槽 x=%.2f' % (n, sx)) # 四角圆角不许吃掉安装孔 for n, (hx, hy) in MOUNT_HOLES.items(): for cx, cy in ((CORNER_R, CORNER_R), (BW - CORNER_R, CORNER_R), (CORNER_R, BH - CORNER_R), (BW - CORNER_R, BH - CORNER_R)): out_x = hx < cx if cx < BW / 2 else hx > cx out_y = hy < cy if cy < BH / 2 else hy > cy if out_x and out_y and math.hypot(hx - cx, hy - cy) > CORNER_R - HOLE_KEEP: bad.append(u'安装孔 %s 被 R%.0f 圆角切到' % (n, CORNER_R)) # 天线缺口:那儿没板材,孔和螺钉头都不许沾(H1/H3 在 x=4,只差 y 就撞上) for n, (hx, hy) in MOUNT_HOLES.items(): if (ANT_NOTCH[0] - HOLE_KEEP < hx < ANT_NOTCH[2] + HOLE_KEEP and ANT_NOTCH[1] - HOLE_KEEP < hy < ANT_NOTCH[3] + HOLE_KEEP): bad.append(u'安装孔 %s 落进 ESP32 天线缺口 (%.1f, %.1f)' % (n, hx, hy)) assert not bad, u'底板自检不过:\n ' + u'\n '.join(bad) return True # ── 下发 ────────────────────────────────────────────────────────────── def call(js, timeout=180): r = run(js, timeout=timeout) if not r.get('success'): raise RuntimeError(json.dumps(r, ensure_ascii=False)[:800]) return r['result'] def mil(v): return round(v * MIL, 4) def polygon_src(pts): a = [mil(pts[0][0]), mil(pts[0][1])] for q in pts[1:]: a += ['L', mil(q[0]), mil(q[1])] if pts[0] != pts[-1]: a += ['L', mil(pts[0][0]), mil(pts[0][1])] return a def push_outline(): u"""板框 = 一条闭合折线;顺带删掉建工程时留下的 4 条层 11 **导线**(§27.3 的坑)。""" src = polygon_src(outline_polygon()) js = ("await eda.dmt_EditorControl.openDocument('%s');" % PCB + "await new Promise(r=>setTimeout(r,2500));" + "const oldL = (await eda.pcb_PrimitiveLine.getAll()||[]).filter(l=>l.layer===11)" ".map(l=>l.primitiveId);" + "if (oldL.length) await eda.pcb_PrimitiveLine.delete(oldL);" + "const oldP = (await eda.pcb_PrimitivePolyline.getAll()||[])" ".filter(p=>p.getState_Layer()===11).map(p=>p.getState_PrimitiveId());" + "if (oldP.length) await eda.pcb_PrimitivePolyline.delete(oldP);" + "const poly = await eda.pcb_MathPolygon.createPolygon(%s);" % json.dumps(src) + "const r = await eda.pcb_PrimitivePolyline.create('', 11, poly, %s);" % mil(OUTLINE_W) + "await eda.pcb_Document.save();" + "return {delLine: oldL.length, delPoly: oldP.length, made: !!r," " now: (await eda.pcb_PrimitivePolyline.getAll()||[])" ".filter(p=>p.getState_Layer()===11).length," " line11: (await eda.pcb_PrimitiveLine.getAll()||[]).filter(l=>l.layer===11).length};") r = call(js, timeout=300) print(u'板框折线: %s(删旧导线 %d / 旧折线 %d);层 11 现有折线 %d / 导线 %d' % (u'建好' if r['made'] else u'失败', r['delLine'], r['delPoly'], r['now'], r['line11'])) if not r['made'] or r['now'] != 1: raise SystemExit(u'!! 板框没画成') return r PUSH_HOLES_JS = u""" await eda.dmt_EditorControl.openDocument('%s'); await new Promise(r=>setTimeout(r,2500)); const H = %s, D = %s; for (const p of await eda.pcb_PrimitivePad.getAll()) { const num = p.getState_PadNumber(); if (typeof num === 'string' && /^H[1-9]$/.test(num)) await eda.pcb_PrimitivePad.delete(p.getState_PrimitiveId()); } let k = 0; for (const [name, x, y] of H) { // layer 12 = 多层;末位 metallization=false ⇒ NPTH。参数位序照抄 pcb_blade.push_holes await eda.pcb_PrimitivePad.create(12, name, x, y, 0, ['ELLIPSE', D, D], '', ['ROUND', D], 0, 0, 0, false); k++; } await eda.pcb_Document.save(); return {made: k, now: (await eda.pcb_PrimitivePad.getAll()) .filter(p => /^H[1-9]$/.test(String(p.getState_PadNumber()))).length}; """ def push_holes(): arr = [[k, mil(v[0]), mil(v[1])] for k, v in sorted(MOUNT_HOLES.items())] r = call(PUSH_HOLES_JS % (PCB, json.dumps(arr), mil(HOLE_D)), timeout=300) print(u'安装孔: 下发 %d / 读回 %d,NPTH Φ%.1f(M3 尼龙立柱)' % (r['made'], r['now'], HOLE_D)) for k, v in sorted(MOUNT_HOLES.items()): print(u' %-3s (%6.2f, %6.2f)' % (k, v[0], v[1])) return r def verify(): r = call("await eda.dmt_EditorControl.openDocument('%s');" "await new Promise(r=>setTimeout(r,2000));" "const ps = (await eda.pcb_PrimitivePolyline.getAll()||[])" ".filter(p=>p.getState_Layer()===11);" "const line11 = (await eda.pcb_PrimitiveLine.getAll()||[])" ".filter(l=>l.layer===11).length;" "const holes = (await eda.pcb_PrimitivePad.getAll())" ".filter(p=>/^H[1-9]$/.test(String(p.getState_PadNumber())))" ".map(p=>[String(p.getState_PadNumber()), p.getState_X(), p.getState_Y()]);" "return {n: ps.length, poly: ps.length ? ps[0].getState_Polygon() : null," " line11, holes, comps: (await eda.pcb_PrimitiveComponent.getAll()).length};" % PCB, timeout=180) bad = [] if r['n'] != 1: bad.append(u'层 11 上有 %d 条折线(应为 1)' % r['n']) if r['line11']: bad.append(u'层 11 上还有 %d 条**导线**(板框画成铜的老坑,§27.3)' % r['line11']) pts = [] src = r['poly'] if isinstance(src, dict): src = src.get('pointArr') or src.get('points') or src.get('polygon') or src if isinstance(src, list): i = 0 while i < len(src): if src[i] in ('L', 'l'): i += 1 continue if isinstance(src[i], (int, float)) and i + 1 < len(src) and isinstance(src[i + 1], (int, float)): pts.append((src[i] / MIL, src[i + 1] / MIL)) i += 2 else: i += 1 if pts: xs = [p[0] for p in pts] ys = [p[1] for p in pts] if abs(min(xs)) > 0.01 or abs(max(xs) - BW) > 0.01 or \ abs(min(ys)) > 0.01 or abs(max(ys) - BH) > 0.01: bad.append(u'外接矩形 %.2f~%.2f x %.2f~%.2f != 0~%.0f x 0~%.0f' % (min(xs), max(xs), min(ys), max(ys), BW, BH)) if abs(pts[0][0] - pts[-1][0]) > 0.01 or abs(pts[0][1] - pts[-1][1]) > 0.01: bad.append(u'折线首尾不重合: %s vs %s' % (pts[0], pts[-1])) if abs(len(pts) - len(outline_polygon())) > 1: bad.append(u'点数 %d != %d' % (len(pts), len(outline_polygon()))) print(u'读回板框折线 1 条 / %d 点,外接 %.2f~%.2f x %.2f~%.2f;层 11 遗留导线 %d' % (len(pts), min(xs), max(xs), min(ys), max(ys), r['line11'])) else: bad.append(u'读不回折线点列(原始值 %s)' % str(r['poly'])[:120]) got = {h[0]: (round(h[1] / MIL, 3), round(h[2] / MIL, 3)) for h in r['holes']} for n, (hx, hy) in sorted(MOUNT_HOLES.items()): if n not in got: bad.append(u'安装孔 %s 没打' % n) elif abs(got[n][0] - hx) > 0.02 or abs(got[n][1] - hy) > 0.02: bad.append(u'安装孔 %s 位置 %s != (%.1f, %.1f)' % (n, got[n], hx, hy)) print(u'安装孔读回 %d 个;板上元件 %d 件' % (len(got), r['comps'])) if bad: print(u'[X] 校验不过:') for b in bad: print(u' ' + b) else: print(u'[OK] 板框是一条闭合折线、外接矩形对上、安装孔齐') return bad if __name__ == '__main__': selfcheck() print(u'底板 %.0f x %.0f,四角 R%.0f;PCB=%s' % (BW, BH, CORNER_R, PCB)) print(u'槽阵 5 槽 @%.2f: %s' % (SLOT_PITCH, u' / '.join('%.2f' % x for x in slot_xs()))) print(u'槽体 y %.1f~%.1f;刀片投影 y %.1f~%.1f(带内元件 ≤ %.0f mm)' % (SLOT_CY - SLOT_BODY_L / 2, SLOT_CY + SLOT_BODY_L / 2, PROJ[0], PROJ[1], H_PROJ)) for k in sorted(ZONES, key=lambda k: ZONES[k][1]): z = ZONES[k] print(u' %-7s y %6.2f ~ %6.2f (%5.1f mm)%s' % (k, z[1], z[3], z[3] - z[1], u' ← 投影外,可放高件' if k in ('input', 'fan') else u'')) if '--outline' in sys.argv: push_outline() verify() elif '--holes' in sys.argv: push_holes() verify() elif '--verify' in sys.argv: verify()