# -*- coding: utf-8 -*- u"""下板 PCB1 的**环境约束图**:把载板 PCB2 底面的东西折算到下板坐标系。 下板 160×125 居中在载板 220×135 下方 ⇒ **下板坐标 = 载板坐标 − (30, 5)** (事实来源 `pcb2-header-pads.json` 的 frame 字段 / 设计文档 §25.1)。 两板净空 **13 mm**(§22.3f:公头塑高 2.5 + 母座塑高 10.5)。 产出 `pcb1-env.json`: | 字段 | 含义 | |---|---| | `anchors` | J24 / J25 的 40 个焊盘在下板坐标下的目标位置(**对位的唯一依据**,别按引脚号推)| | `zones` | 一堆矩形,每个带 `hmax` = 下板在这块地方允许的最大元件高度 = 13 − h载板 − 1 | | `lanes` | 排线在载板背面占的车道(下板坐标)。排线要平贴着走,这里 `hmax` 取 11.0 | | `slots` | 载板的 10 条穿线槽投影(下板边缘 y<1.3 / y>123.9 两条带)| 用法: /c/Python314/python .claude/pcb1_env.py """ import io, json, os, sys HERE = os.path.dirname(os.path.abspath(__file__)); sys.path.insert(0, HERE) import pcb2_place as P2 from eda import run DX, DY = 30.0, 5.0 # 下板原点在载板坐标里的位置 GAP = 13.0 # 两板净空 MARGIN = 1.0 # 装配余量 BW, BH = 160.0, 125.0 LANE_H = 1.0 # 排线(0.2 厚)+ 走线余量,按 1 mm 算 def shift(x0, y0, x1, y1): return [round(x0 - DX, 2), round(y0 - DY, 2), round(x1 - DX, 2), round(y1 - DY, 2)] def clip(r): u"""裁到下板板框内;完全在框外返回 None。""" x0 = max(r[0], 0.0); y0 = max(r[1], 0.0) x1 = min(r[2], BW); y1 = min(r[3], BH) if x1 <= x0 or y1 <= y0: return None return [round(x0, 2), round(y0, 2), round(x1, 2), round(y1, 2)] def header_anchors(): u"""载板 J31/J32 的 80 个焊盘 -> 下板坐标 + **网络名**。 ⚠️ 对插的判据是**网络**,不是针号。两颗封装的焊盘编号同手性、母座又在载板底层, 所以下板 pin(2r−1) 正对载板 pin(2r)(设计文档 §26.5)。针号一定对不上,网络必须对上。 坐标来自 `pcb2-header-pads.json`(`pcb2_place --push` 导出),网络当场从载板读。 """ pads = json.load(io.open(os.path.join(HERE, 'pcb2-header-pads.json'), encoding='utf-8'))['pads'] nets = {} r = run(""" await eda.dmt_EditorControl.openDocument('f126ffb29c92f9b2'); await new Promise(r=>setTimeout(r,2000)); const out = {}; for (const c of await eda.pcb_PrimitiveComponent.getAll()) { const d = c.getState_Designator(); if (d !== 'J31' && d !== 'J32') continue; const ps = await eda.pcb_PrimitiveComponent.getAllPinsByPrimitiveId(c.getState_PrimitiveId()); out[d] = ps.map(p => [p.padNumber, p.net]); } return out; """, timeout=300) if not r.get('success'): raise RuntimeError(json.dumps(r, ensure_ascii=False)[:400]) for ref, lst in r['result'].items(): for pad, net in lst: nets[(ref, pad)] = net out = {} for q in pads: out.setdefault(q['ref'], {})[q['pad']] = [round(q['x'] - DX, 3), round(q['y'] - DY, 3), nets.get((q['ref'], q['pad']))] # J31 -> J24,J32 -> J25 return {'J24': out['J31'], 'J25': out['J32']} def build(): pos = P2.plan() bb = P2.load_bb() P2.CAN = {P2.desig(n)['c1'] for n in P2.SITES} zones = [] for d, (x, y, r, lay) in sorted(pos.items()): if lay != 2 and d not in P2.th_parts(): continue h = P2.part_height(d) b = P2.box(d, r, bb, 2) rect = clip(shift(x + b[0], y + b[1], x + b[2], y + b[3])) if not rect: continue zones.append({'ref': d, 'rect': rect, 'h': h, 'hmax': round(GAP - h - MARGIN, 2)}) lanes = [] for n in P2.SITES: for lname, ((u0, u1, v0, v1), owner) in P2.LANES.items(): ax, ay, _ = P2.to_board(n, u0, v0) bx, by, _ = P2.to_board(n, u1, v1) rect = clip(shift(min(ax, bx), min(ay, by), max(ax, bx), max(ay, by))) if rect: lanes.append({'ref': 'CH%d-%s-lane' % (n, lname), 'rect': rect, 'h': LANE_H, 'hmax': round(GAP - LANE_H - MARGIN, 2)}) slots = [] for name, x0, y0, x1, y1 in P2.slot_rects(): rect = clip(shift(x0, y0, x1, y1)) if rect: slots.append({'ref': name, 'rect': rect}) anchors = header_anchors() data = { 'frame': u'下板 PCB1 坐标系 mm,原点左下角,Y 向上。下板坐标 = 载板坐标 − (30,5)。' u'hmax = 该矩形内下板元件允许的最大高度 = 13(净空) − h(载板底面元件) − 1(余量)', 'gap': GAP, 'board': [BW, BH], 'anchors': anchors, 'zones': zones, 'lanes': lanes, 'slots': slots, } out = os.path.join(HERE, 'pcb1-env.json') json.dump(data, io.open(out, 'w', encoding='utf-8'), ensure_ascii=False, indent=1) return data, out def report(data, out): print(u'已写 %s' % out) print(u' 锚点 J24/J25 各 %d / %d 个焊盘(含载板侧网络名)' % (len(data['anchors']['J24']), len(data['anchors']['J25']))) print(u' 受限区 %d 块 / 排线车道 %d 条 / 槽投影 %d 条' % (len(data['zones']), len(data['lanes']), len(data['slots']))) tight = sorted([z for z in data['zones'] if z['hmax'] < 10.0], key=lambda z: z['hmax']) print(u'\n 下板高度受限的地方(hmax < 10 mm):') for z in tight: r = z['rect'] print(u' %-6s hmax=%5.2f x %6.2f~%6.2f y %6.2f~%6.2f' % (z['ref'], z['hmax'], r[0], r[2], r[1], r[3])) print(u'\n 排线车道(下板这些地方元件 ≤ %.1f mm):' % data['lanes'][0]['hmax'] if data['lanes'] else '') for z in data['lanes']: r = z['rect'] print(u' %-14s x %6.2f~%6.2f y %6.2f~%6.2f' % (z['ref'], r[0], r[2], r[1], r[3])) print(u'\n 槽在下板上的投影(排线从这里下穿,别放高件):') for z in data['slots']: r = z['rect'] print(u' %-12s x %6.2f~%6.2f y %6.2f~%6.2f' % (z['ref'], r[0], r[2], r[1], r[3])) if __name__ == '__main__': d, o = build() report(d, o) # ═════════════════ 高度地图(给人看的)═════════════════ def hmap(cell=2.0): u"""把 zones/lanes 栅格化成一张 hmax 地图,字符越大越矮。""" d = json.load(io.open(os.path.join(HERE, 'pcb1-env.json'), encoding='utf-8')) nx, ny = int(BW / cell), int(BH / cell) g = [[GAP - MARGIN] * nx for _ in range(ny)] # 12.0 = 载板那边是光板 for z in d['zones'] + d['lanes']: x0, y0, x1, y1 = z['rect'] for j in range(max(0, int(y0 / cell)), min(ny, int((y1 - 1e-6) / cell) + 1)): for i in range(max(0, int(x0 / cell)), min(nx, int((x1 - 1e-6) / cell) + 1)): g[j][i] = min(g[j][i], z['hmax']) def ch(h): if h >= 12.0: return '.' if h >= 11.0: return ',' if h >= 10.0: return ':' if h >= 8.0: return '=' if h >= 3.5: return '#' return '@' print(u'\n高度地图(cell=%.0f mm,左下角是原点;. ≥12 , ≥11 : ≥10 = ≥8 # ≥3.5 @ <3.5)' % cell) print(u' ' + ''.join(str(int(i * cell / 10) % 10) for i in range(nx))) for j in range(ny - 1, -1, -1): print(u'%4.0f ' % (j * cell) + ''.join(ch(v) for v in g[j])) return g