R2:底板 / 刀片布局布线脚本、快照、截图与规划文档更新(含外壳、打印件、布线归档)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
这个提交包含在:
@@ -0,0 +1,177 @@
|
||||
# -*- 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()
|
||||
在新工单中引用
屏蔽一个用户