文件

178 行
9.5 KiB
Python

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
# -*- 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()