R2 焊接:丝印临时换成器件值 + 摆放优化 + 1:1 器件值打印稿
2026-09-25 为焊接底板与旧版刀片(BLADE-PCB 3b9c4e39c8d164db): - silk_value.py:隐藏位号、在原位建自由丝印写器件值(Value,没有就用型号), --restore 按 _silk_value_restore.json 删字并恢复位号。⚠ 再出 Gerber / 下单前必须先 --restore。 - silk_value_plan.py:器件值摆放优化(不压焊盘 / 不重叠 / 不出框 / 避开开槽与金手指区; 软代价:压过孔、压他件本体、离本件远、缩字号)。EDA 默认字体实测字宽 ≈0.78×字高。 - print_value.py:两块板背面镜像 1:1 画在同一张 A4,红框标 1 脚; print/print_1to1.ps1 按 210×297 mm 直送打印机(不经 PDF 阅读器缩放)。 - 数据:_silk_value_geom/_extra(板上读回几何)、_dump/_plan(排位结果)。 Co-Authored-By: Claude Opus 5.5 <[email protected]>
这个提交包含在:
@@ -0,0 +1,305 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
u"""器件值丝印(silk_value.py 建的那批自由文字)的摆放优化。
|
||||
|
||||
/c/Python314/python .claude/r2/silk_value_plan.py # 读板 + 算 + 出预览 PNG
|
||||
/c/Python314/python .claude/r2/silk_value_plan.py --push # 再下发(pcb_PrimitiveString.modify)并存盘
|
||||
|
||||
硬约束:不压同面焊盘 / 通孔焊盘 / 安装孔,文字互不重叠,不出板框(留 EDGE),不进开槽、金手指区。
|
||||
软代价:压过孔、压别的器件本体、离自己器件远、缩字号、离别的器件比离自己还近。
|
||||
器件本体 = 该器件同面焊盘外包框(没有丝印框数据,焊盘框是能拿到的最好近似)。
|
||||
"""
|
||||
import io
|
||||
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))
|
||||
from eda import run # noqa: E402
|
||||
|
||||
M = 0.0254 # mil → mm
|
||||
RESTORE = os.path.join(HERE, '_silk_value_restore.json')
|
||||
PLAN = os.path.join(HERE, '_silk_value_plan.json')
|
||||
DUMP = os.path.join(HERE, '_silk_value_dump.json')
|
||||
CHAR_W, EDGE, GAP = 0.78, 0.35, 0.15
|
||||
FS = [(1.0, 0.0), (0.85, 1.5), (0.7, 4.0)] # (字高 mm, 代价)
|
||||
FINGER_Y = {'BLADE': 10.6} # 刀片金手指区 y < 10.6 不放字
|
||||
|
||||
READ = u"""
|
||||
await eda.dmt_EditorControl.openDocument('%s');
|
||||
await new Promise(r=>setTimeout(r,3000));
|
||||
const C = await eda.pcb_PrimitiveComponent.getAll()||[];
|
||||
const comps=[];
|
||||
for (const c of C) {
|
||||
const pins = await eda.pcb_PrimitiveComponent.getAllPinsByPrimitiveId(c.primitiveId)||[];
|
||||
comps.push({ref:c.designator, x:c.x, y:c.y, layer:c.layer, pads: pins.map(p=>[p.x,p.y,p.rotation,p.layer,p.pad])});
|
||||
}
|
||||
const P = (await eda.pcb_PrimitivePad.getAll()||[]).map(p=>[p.x,p.y,p.rotation,p.layer,p.pad]);
|
||||
const V = (await eda.pcb_PrimitiveVia.getAll()||[]).map(v=>[v.x,v.y,v.diameter]);
|
||||
const PL = (await eda.pcb_PrimitivePolyline.getAll()||[]).filter(l=>l.layer===11).map(l=>l.polygon.polygon);
|
||||
const F = (await eda.pcb_PrimitiveFill.getAll()||[]).filter(f=>f.layer===12).map(f=>(f.complexPolygon||f.polygon).polygon);
|
||||
const S = (await eda.pcb_PrimitiveString.getAll()||[]).map(s=>({id:s.primitiveId, layer:s.layer, text:s.text, x:s.x, y:s.y, rot:s.rotation, fs:s.fontSize, lw:s.lineWidth, align:s.alignMode}));
|
||||
return {comps, pads:P, vias:V, outline:PL, cuts:F, strings:S};
|
||||
"""
|
||||
|
||||
|
||||
def nums(poly):
|
||||
v = [t for t in poly if not isinstance(t, str)]
|
||||
return list(zip(v[0::2], v[1::2]))
|
||||
|
||||
|
||||
def pad_box(p):
|
||||
x, y, rot, lay, shp = p
|
||||
w, h = (shp[1], shp[2]) if shp and shp[0] in ('RECT', 'OVAL', 'ELLIPSE') else (None, None)
|
||||
if w is None: # POLYGON 等:取点集外包
|
||||
pts = nums(shp[1]) if shp and len(shp) > 1 and isinstance(shp[1], list) else []
|
||||
if pts:
|
||||
xs, ys = [q[0] for q in pts], [q[1] for q in pts]
|
||||
w, h = max(xs) - min(xs), max(ys) - min(ys)
|
||||
else:
|
||||
w = h = 40
|
||||
r = int(round((rot or 0) / 90.0)) % 2
|
||||
if abs((rot or 0) / 90.0 - round((rot or 0) / 90.0)) > 0.05:
|
||||
w = h = max(w, h)
|
||||
if r:
|
||||
w, h = h, w
|
||||
return (x * M - w * M / 2, y * M - h * M / 2, x * M + w * M / 2, y * M + h * M / 2, lay)
|
||||
|
||||
|
||||
def ov(a, b, g=0.0):
|
||||
return not (a[2] + g <= b[0] or b[2] + g <= a[0] or a[3] + g <= b[1] or b[3] + g <= a[1])
|
||||
|
||||
|
||||
def oarea(a, b):
|
||||
return max(0, min(a[2], b[2]) - max(a[0], b[0])) * max(0, min(a[3], b[3]) - max(a[1], b[1]))
|
||||
|
||||
|
||||
def inpoly(x, y, pts):
|
||||
c = False
|
||||
for i in range(len(pts)):
|
||||
x1, y1 = pts[i]
|
||||
x2, y2 = pts[i - 1]
|
||||
if (y1 > y) != (y2 > y) and x < (x2 - x1) * (y - y1) / (y2 - y1) + x1:
|
||||
c = not c
|
||||
return c
|
||||
|
||||
|
||||
def seg_dist(px, py, a, b):
|
||||
(x1, y1), (x2, y2) = a, b
|
||||
dx, dy = x2 - x1, y2 - y1
|
||||
L = dx * dx + dy * dy
|
||||
t = 0 if L == 0 else max(0, min(1, ((px - x1) * dx + (py - y1) * dy) / L))
|
||||
return math.hypot(px - x1 - t * dx, py - y1 - t * dy)
|
||||
|
||||
|
||||
def tsize(text, fs, rot):
|
||||
w, h = CHAR_W * fs * len(text) + 0.15, fs + 0.15
|
||||
return (h, w) if rot % 180 else (w, h)
|
||||
|
||||
|
||||
def box_dist(a, b):
|
||||
dx = max(b[0] - a[2], a[0] - b[2], 0)
|
||||
dy = max(b[1] - a[3], a[1] - b[3], 0)
|
||||
return math.hypot(dx, dy)
|
||||
|
||||
|
||||
def plan_board(name, d, mine):
|
||||
outline = [(x * M, y * M) for x, y in nums(d['outline'][0])]
|
||||
cuts = []
|
||||
for f in d['cuts']:
|
||||
q = nums(f)
|
||||
if q:
|
||||
cuts.append((min(a for a, _ in q) * M, min(b for _, b in q) * M, max(a for a, _ in q) * M, max(b for _, b in q) * M))
|
||||
fy = FINGER_Y.get(name)
|
||||
silk_side = {3: (1, 12), 4: (2, 12)}
|
||||
padsB = {3: [], 4: []}
|
||||
bodies = {}
|
||||
for c in d['comps']:
|
||||
bs = [pad_box(p) for p in c['pads']]
|
||||
for b in bs:
|
||||
for sl, ok in silk_side.items():
|
||||
if b[4] in ok:
|
||||
padsB[sl].append(b[:4])
|
||||
side = [b for b in bs if b[4] in (c['layer'], 12)] or bs
|
||||
if side:
|
||||
bodies[c['ref']] = ((min(b[0] for b in side) - 0.15, min(b[1] for b in side) - 0.15,
|
||||
max(b[2] for b in side) + 0.15, max(b[3] for b in side) + 0.15), 3 if c['layer'] == 1 else 4)
|
||||
for p in d['pads']:
|
||||
b = pad_box(p)
|
||||
for sl in (3, 4):
|
||||
padsB[sl].append(b[:4])
|
||||
vias = [(x * M - dd * M / 2, y * M - dd * M / 2, x * M + dd * M / 2, y * M + dd * M / 2) for x, y, dd in d['vias']]
|
||||
fixed = {3: [], 4: []}
|
||||
for s in d['strings']:
|
||||
if s['id'] in mine:
|
||||
continue
|
||||
w, h = tsize(s['text'], (s['fs'] or 40) * M, int(round(s['rot'] or 0)))
|
||||
fixed[s['layer']].append((s['x'] * M - w / 2, s['y'] * M - h / 2, s['x'] * M + w / 2, s['y'] * M + h / 2))
|
||||
|
||||
def legal_static(bb, sl):
|
||||
for (x, y) in ((bb[0], bb[1]), (bb[2], bb[1]), (bb[0], bb[3]), (bb[2], bb[3])):
|
||||
if not inpoly(x, y, outline):
|
||||
return False
|
||||
cx, cy = (bb[0] + bb[2]) / 2, (bb[1] + bb[3]) / 2
|
||||
r = min(seg_dist(x, y, outline[i], outline[i - 1]) for (x, y) in
|
||||
((bb[0], bb[1]), (bb[2], bb[1]), (bb[0], bb[3]), (bb[2], bb[3]), (cx, bb[1]), (cx, bb[3]), (bb[0], cy), (bb[2], cy))
|
||||
for i in range(len(outline)))
|
||||
if r < EDGE:
|
||||
return False
|
||||
if fy and bb[1] < fy:
|
||||
return False
|
||||
if any(ov(bb, c, 0.3) for c in cuts):
|
||||
return False
|
||||
if any(ov(bb, p, GAP) for p in padsB[sl]):
|
||||
return False
|
||||
if any(ov(bb, f, GAP) for f in fixed[sl]):
|
||||
return False
|
||||
return True
|
||||
|
||||
texts = [s for s in d['strings'] if s['id'] in mine]
|
||||
cands = {}
|
||||
for s in texts:
|
||||
ref = mine[s['id']]
|
||||
body, sl = bodies.get(ref, (None, s['layer']))
|
||||
if body is None:
|
||||
continue
|
||||
sl = s['layer']
|
||||
bw, bh = body[2] - body[0], body[3] - body[1]
|
||||
bcx, bcy = (body[0] + body[2]) / 2, (body[1] + body[3]) / 2
|
||||
L = []
|
||||
for fs, fc in FS:
|
||||
for rot in (0, 90):
|
||||
w, h = tsize(s['text'], fs, rot)
|
||||
pos = []
|
||||
for r in (0.0, 0.3, 0.7, 1.2, 1.8, 2.6, 3.5, 4.6, 5.8):
|
||||
g = GAP + 0.05 + r
|
||||
for k in (-1, -0.5, 0, 0.5, 1):
|
||||
sx = bcx + k * max(0, (bw - w) / 2 + w * 0.4)
|
||||
sy = bcy + k * max(0, (bh - h) / 2 + h * 0.4)
|
||||
pos += [(sx, body[3] + g + h / 2, r), (sx, body[1] - g - h / 2, r),
|
||||
(body[2] + g + w / 2, sy, r), (body[0] - g - w / 2, sy, r)]
|
||||
for dx, dy in ((1, 1), (1, -1), (-1, 1), (-1, -1)):
|
||||
pos.append((bcx + dx * (bw / 2 + g + w / 2), bcy + dy * (bh / 2 + g + h / 2), r + 0.4))
|
||||
inside = bw * bh > 2.5 * w * h and w < bw - 0.2 and h < bh - 0.2
|
||||
if inside:
|
||||
for ky in (0, 0.25, -0.25):
|
||||
pos.append((bcx, bcy + ky * (bh - h), -1))
|
||||
for x, y, r in pos:
|
||||
bb = (x - w / 2, y - h / 2, x + w / 2, y + h / 2)
|
||||
if not legal_static(bb, sl):
|
||||
continue
|
||||
cost = fc + (0.3 if rot == 90 else 0) + (r * 1.2 if r >= 0 else 1.0)
|
||||
cost += 3.0 * sum(1 for v in vias if ov(bb, v))
|
||||
near_own = box_dist(bb, body)
|
||||
for oref, (ob, osl) in bodies.items():
|
||||
if oref == ref or osl != sl:
|
||||
continue
|
||||
a = oarea(bb, ob)
|
||||
if a > 0:
|
||||
cost += 4.0 + 6.0 * a / (w * h)
|
||||
elif box_dist(bb, ob) + 0.05 < near_own:
|
||||
cost += 2.5
|
||||
L.append((cost, bb, x, y, rot, fs))
|
||||
L.sort(key=lambda t: t[0])
|
||||
cands[s['id']] = L[:400]
|
||||
|
||||
# 贪心:候选少的先排;然后逐个拿掉重排若干轮
|
||||
order = sorted(cands, key=lambda i: len(cands[i]))
|
||||
chosen = {}
|
||||
|
||||
def pick(i):
|
||||
for c in cands[i]:
|
||||
bb = c[1]
|
||||
if any(j != i and ov(bb, chosen[j][1], GAP) for j in chosen):
|
||||
continue
|
||||
return c
|
||||
return None
|
||||
|
||||
for i in order:
|
||||
c = pick(i)
|
||||
if c:
|
||||
chosen[i] = c
|
||||
for _ in range(4):
|
||||
changed = 0
|
||||
for i in order:
|
||||
old = chosen.pop(i, None)
|
||||
c = pick(i)
|
||||
if c:
|
||||
chosen[i] = c
|
||||
if old is None or c[2:] != old[2:]:
|
||||
changed += 1
|
||||
elif old:
|
||||
chosen[i] = old
|
||||
if not changed:
|
||||
break
|
||||
fail = [mine[i] for i in cands if i not in chosen]
|
||||
return chosen, fail, {'outline': outline, 'cuts': cuts, 'pads': padsB, 'bodies': bodies, 'vias': vias, 'fixed': fixed}
|
||||
|
||||
|
||||
def preview(name, chosen, texts, geo, path):
|
||||
import matplotlib
|
||||
matplotlib.use('Agg')
|
||||
import matplotlib.pyplot as plt
|
||||
from matplotlib.patches import Rectangle, Polygon
|
||||
fig, axs = plt.subplots(1, 2, figsize=(22, 11))
|
||||
for ax, sl in zip(axs, (4, 3)):
|
||||
ax.add_patch(Polygon(geo['outline'], fill=False, ec='k'))
|
||||
for c in geo['cuts']:
|
||||
ax.add_patch(Rectangle(c[:2], c[2] - c[0], c[3] - c[1], fc='0.7'))
|
||||
for p in geo['pads'][sl]:
|
||||
ax.add_patch(Rectangle(p[:2], p[2] - p[0], p[3] - p[1], fc='#c9a227', ec='none'))
|
||||
for ref, (b, s) in geo['bodies'].items():
|
||||
if s == sl:
|
||||
ax.add_patch(Rectangle(b[:2], b[2] - b[0], b[3] - b[1], fill=False, ec='#1a3fa0', lw=0.4))
|
||||
for i, c in chosen.items():
|
||||
if texts[i]['layer'] != sl:
|
||||
continue
|
||||
bb = c[1]
|
||||
ax.add_patch(Rectangle(bb[:2], bb[2] - bb[0], bb[3] - bb[1], fill=False, ec='r', lw=0.4))
|
||||
ax.text(c[2], c[3], texts[i]['text'], fontsize=c[5] * 4.2, ha='center', va='center',
|
||||
rotation=c[4], color='r')
|
||||
ax.set_aspect('equal')
|
||||
ax.autoscale_view()
|
||||
ax.set_title('%s %s' % (name, 'BOTTOM' if sl == 4 else 'TOP'))
|
||||
fig.savefig(path, dpi=130, bbox_inches='tight')
|
||||
|
||||
|
||||
def main():
|
||||
rec = json.load(io.open(RESTORE, encoding='utf-8'))
|
||||
dump = {}
|
||||
plans = {}
|
||||
for name, R in rec.items():
|
||||
r = run(READ % R['pcb'], timeout=900)
|
||||
d = r.get('result')
|
||||
assert d, json.dumps(r, ensure_ascii=False)[:500]
|
||||
dump[name] = d
|
||||
mine = {sid: ref for ref, sid in R['made']}
|
||||
chosen, fail, geo = plan_board(name, d, mine)
|
||||
texts = {s['id']: s for s in d['strings']}
|
||||
fsz = {}
|
||||
for c in chosen.values():
|
||||
fsz[c[5]] = fsz.get(c[5], 0) + 1
|
||||
print(u'%s:%d 条,排上 %d,无解 %d %s;字号分布 %s' % (name, len(mine), len(chosen), len(fail), fail, fsz))
|
||||
plans[name] = {'pcb': R['pcb'], 'rows': [[i, c[2] / M, c[3] / M, c[4], c[5] / M] for i, c in chosen.items()]}
|
||||
preview(name, chosen, texts, geo, os.path.join(HERE, '_silk_value_%s.png' % name.lower()))
|
||||
io.open(DUMP, 'w', encoding='utf-8').write(json.dumps(dump, ensure_ascii=False))
|
||||
io.open(PLAN, 'w', encoding='utf-8').write(json.dumps(plans, ensure_ascii=False))
|
||||
if '--push' not in sys.argv:
|
||||
print(u'(只算;预览 _silk_value_base.png / _silk_value_blade.png;加 --push 下发)')
|
||||
return
|
||||
for name, P in plans.items():
|
||||
js = u"""
|
||||
await eda.dmt_EditorControl.openDocument('%s');
|
||||
await new Promise(r=>setTimeout(r,3000));
|
||||
const R = %s; let n=0; const err=[];
|
||||
for (const [id,x,y,rot,fs] of R) { try { const s = await eda.pcb_PrimitiveString.modify(id, {x:x, y:y, rotation:rot, fontSize:fs, alignMode:3}); if (s) n++; else err.push(id); } catch(e) { err.push(String(e)); } }
|
||||
await eda.pcb_Document.save();
|
||||
return {n, err};
|
||||
""" % (P['pcb'], json.dumps(P['rows']))
|
||||
print(name, run(js, timeout=900).get('result'))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
在新工单中引用
屏蔽一个用户