178 行
9.5 KiB
Python
178 行
9.5 KiB
Python
# -*- 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()
|