# -*- coding: utf-8 -*- u"""`BLADE-PCB_1`(用户 2026-09-14 另存的新布局副本)按现有摆位收板框。 /c/Python314/python .claude/r2/outline_blade1.py # 只算:包络 / 余量 / 与现板框比对 /c/Python314/python .claude/r2/outline_blade1.py --push # 下发 + 读回核对 只改外接尺寸 `BW` / `BH`,形状一律复用 `pcb_blade.outline_polygon()`: 舌头、台阶、防呆缺口、金手指位置(`J1_X` = 45.0 常数)、四角 R2 都不动, 器件 / 孔 / 排线槽也一个不动 —— 所以左边沿和底边(金手指)保持不变,只收**东边**和**北边**。 ⛔ 这里用运行时覆盖 `B.BW / B.BH`,**不改 `pcb_blade.py` 里的常量**: 那两个数描述的是已出 Gerber 的 `BLADE-PCB`(89.5×67),两块板现在尺寸不同。 判据(mm): - 东边 = 器件包围盒(`getPrimitivesBBox`,含丝印外形)最东 + `EDGE_E`,向上取到 0.5 格; - 北边 = max(器件最北 + `EDGE_N`, 排线槽顶 + `pcb_blade.SLOT_RIB`),向上取到 0.5 格; - 东边还不得小于手机主板投影右沿 `PHONE_X1`(主板要整块落在刀片上)。 """ 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) from eda import run # noqa: E402 import pcb_blade as B # noqa: E402 PCB1 = 'c575a2e854057614' # BLADE-PCB_1(未挂到 Board,工程里单独一份 PCB) MIL = B.MIL EDGE_E = 1.0 # 器件外形到东板边 EDGE_N = 1.0 # 器件外形到北板边 JS_READ = u""" await eda.dmt_EditorControl.openDocument('%s'); await new Promise(r=>setTimeout(r,2500)); const cur = await eda.dmt_SelectControl.getCurrentDocumentInfo(); if (cur.uuid !== '%s') return {err: 'wrong doc ' + cur.uuid}; const comps = []; for (const c of await eda.pcb_PrimitiveComponent.getAll()) comps.push([c.getState_Designator(), await eda.pcb_Primitive.getPrimitivesBBox([c.getState_PrimitiveId()])]); const fills = []; for (const f of await eda.pcb_PrimitiveFill.getAll()) if (f.getState_Layer() === 12) fills.push(await eda.pcb_Primitive.getPrimitivesBBox([f.getState_PrimitiveId()])); const holes = (await eda.pcb_PrimitivePad.getAll()).filter(p => p.getState_Layer() === 12 && /^H\\d$/.test(p.getState_PadNumber())) .map(p => [p.getState_PadNumber(), p.getState_X(), p.getState_Y()]); const polys = (await eda.pcb_PrimitivePolyline.getAll()).filter(p => p.getState_Layer() === 11) .map(p => p.getState_Polygon().getSource()); const jc = (await eda.pcb_PrimitiveComponent.getAll()).find(c => c.getState_Designator() === 'J1'); const j1 = [jc.getState_X(), jc.getState_Y(), jc.getState_Rotation()]; return {comps, fills, holes, polys, j1}; """ def read(): r = run(JS_READ % (PCB1, PCB1), timeout=300) if not r.get('success') or r['result'].get('err'): raise SystemExit(json.dumps(r, ensure_ascii=False)[:600]) return r['result'] def mm(b): return [b[k] / MIL for k in ('minX', 'minY', 'maxX', 'maxY')] def plan(st): comps = [(d, mm(b)) for d, b in st['comps']] e = max(comps, key=lambda t: t[1][2]) n = max(comps, key=lambda t: t[1][3]) slot_top = max(mm(f)[3] for f in st['fills']) bw = math.ceil(max(e[1][2] + EDGE_E, B.PHONE_X1) * 2) / 2.0 bh = math.ceil(max(n[1][3] + EDGE_N, slot_top + B.SLOT_RIB) * 2) / 2.0 return bw, bh, e, n, slot_top, comps def src_pts(src): u"""⚠ 读回的源数组是 `x y L x y x y ...` —— 一个 `L` 后面可以跟多对点,不是每对都带 `L`。""" nums = [v for v in src if v != 'L'] assert all(not isinstance(v, str) for v in nums), u'折线里有非直线段: %s' % set( v for v in src if isinstance(v, str)) return [(nums[k], nums[k + 1]) for k in range(0, len(nums), 2)] TONGUE_HALF = round((B.TONGUE_X1 - B.TONGUE_X0) / 2.0, 4) # 17.25,舌头跟着金手指走 assert abs(B.J1_X - B.TONGUE_X0 - TONGUE_HALF) < 1e-9, u'pcb_blade 里舌头不是以 J1_X 为中心的' def set_shape(bw, bh, j1x): u"""运行时覆盖 pcb_blade 的外形参数。舌头 / 防呆缺口都由 J1_X 推,**只有这一处改**。""" B.BW, B.BH, B.J1_X = bw, bh, j1x B.TONGUE_X0, B.TONGUE_X1 = round(j1x - TONGUE_HALF, 4), round(j1x + TONGUE_HALF, 4) def same_shape(src, bw, bh, j1x): u"""板上折线与 `outline_polygon()`(给定 BW/BH/J1_X)比,返回最大偏差 mm。""" set_shape(bw, bh, j1x) want = src_pts(B.polygon_src(B.outline_polygon())) got = src_pts(src) if abs(len(want) - len(got)) > 1: # 闭合点可能被客户端吃掉一个 return None # ⚠ 客户端存盘后会换起点 / 换走向,不能按下标逐点比 ⇒ 双向「最近顶点」距离取最大 d = lambda p, qs: min(math.hypot(p[0] - q[0], p[1] - q[1]) for q in qs) return max(max(d(p, got) for p in want), max(d(q, want) for q in got)) / MIL def main(): st = read() bw, bh, e, n, slot_top, comps = plan(st) j1 = st['j1'] old_j1x = round(j1[0] / MIL, 3) pts = src_pts(st['polys'][0]) if len(st['polys']) == 1 else [] old_bw = round(max(p[0] for p in pts) / MIL, 3) old_bh = round(max(p[1] for p in pts) / MIL, 3) print(u'BLADE-PCB_1:%d 件 / 挖槽 %d / 定位孔 %d / 板框折线 %d' % (len(comps), len(st['fills']), len(st['holes']), len(st['polys']))) assert len(st['polys']) == 1, u'层 11 上折线不是 1 条' dev = same_shape(st['polys'][0], old_bw, old_bh, old_j1x) print(u'现板框 vs 脚本 %.2f×%.2f(J1_X %.2f):%s' % (old_bw, old_bh, old_j1x, u'点数不同' if dev is None else u'最大偏差 %.4f mm' % dev)) assert dev is not None and dev < 0.01, u'现板框不是脚本画的那个形状,别直接覆盖' # ⛔ 2026-09-14 用户要求「金手指移到正中」:J1(连同舌头 / 缺口)挪到板宽中线。 # 只动 J1 一个件(此时板上没布线,飞线跟着走);别的器件、孔、槽一概不动。 # ⚠ 东边界 `plan()` 按含 J1 的包络算的;J1 西移后不会变成最东件,结果不受影响。 j1x = bw / 2.0 if '--keep-j1' not in sys.argv else old_j1x print(u'器件最东 %s %.2f / 最北 %s %.2f;排线槽顶 %.2f;手机右沿 %.2f' % (e[0], e[1][2], n[0], n[1][3], slot_top, B.PHONE_X1)) print(u'⇒ 板框 %.1f × %.1f,金手指中心 %.2f → %.2f(板中 %.2f)' % (bw, bh, old_j1x, j1x, bw / 2)) set_shape(bw, bh, j1x) B.selfcheck() # 舌头上方 / 两侧不许压件:J1 以外的件底边都得在台阶之上 low = [d for d, (x0, y0, x1, y1) in comps if d != 'J1' and y0 < B.STEP_H + 1.0 and x1 > B.TONGUE_X0 - 1.0 and x0 < B.TONGUE_X1 + 1.0] assert not low, u'舌头附近有件: %s' % low out = [d for d, (x0, y0, x1, y1) in comps if d != 'J1' and (x1 > bw - 0.3 or y1 > bh - 0.3)] assert not out, u'有器件贴边/越框: %s' % out for h, x, y in st['holes']: x, y = x / MIL, y / MIL dd = min(x, bw - x, bh - y) assert dd - B.HOLE_D / 2 > 1.0, u'孔 %s 离板边太近 %.2f' % (h, dd) if '--push' not in sys.argv: print(u'(未加 --push,没有下发)') return src = B.polygon_src(B.outline_polygon()) js = ("const cur = await eda.dmt_SelectControl.getCurrentDocumentInfo();" "if (cur.uuid !== '%s') return {err: 'wrong doc ' + cur.uuid};" % PCB1 + "const oldP = (await eda.pcb_PrimitivePolyline.getAll()).filter(p=>p.getState_Layer()===11).map(p=>p.getState_PrimitiveId());" "if (oldP.length !== 1) return {err: 'outline count ' + oldP.length};" "const j = (await eda.pcb_PrimitiveComponent.getAll()).find(c=>c.getState_Designator()==='J1');" "if (!j) return {err: 'no J1'};" "if (Math.abs(j.getState_X() - %s) > 1e-3) {" % round(j1x * MIL, 4) + " const a = j.toAsync(); a.setState_X(%s); await a.done(); }" % round(j1x * MIL, 4) + "const poly = await eda.pcb_MathPolygon.createPolygon(%s);" % json.dumps(src) + "const r = await eda.pcb_PrimitivePolyline.create('', 11, poly, %s);" % round(B.OUTLINE_W * MIL, 4) + "if (!r) return {err: 'create failed'};" "await eda.pcb_PrimitivePolyline.delete(oldP);" "const saved = await eda.pcb_Document.save();" "const now = (await eda.pcb_PrimitivePolyline.getAll()).filter(p=>p.getState_Layer()===11);" "const j2 = (await eda.pcb_PrimitiveComponent.getAll()).find(c=>c.getState_Designator()==='J1');" "return {saved, n: now.length, src: now.length ? now[0].getState_Polygon().getSource() : null," " j1: [j2.getState_X(), j2.getState_Y(), j2.getState_Rotation(), j2.getState_Layer()]};") r = run(js, timeout=300) res = r.get('result') or {} if not r.get('success') or res.get('err'): raise SystemExit(u'!! 下发失败: ' + json.dumps(r, ensure_ascii=False)[:600]) dev = same_shape(res['src'], bw, bh, j1x) if res['n'] == 1 else None print(u'下发:存盘 %s / 层 11 折线 %d 条 / 读回与计划最大偏差 %s / J1 读回 (%.3f, %.3f) rot %s 层 %s' % (res['saved'], res['n'], u'点数不同' if dev is None else u'%.4f mm' % dev, res['j1'][0] / MIL, res['j1'][1] / MIL, res['j1'][2], res['j1'][3])) if res['n'] != 1 or dev is None or dev > 0.01 or abs(res['j1'][0] / MIL - j1x) > 0.001: raise SystemExit(u'!! 读回对不上') if __name__ == '__main__': main()