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,32 @@
|
||||
# 1:1 打印一张 A4 图(PNG,整页 210x297 mm)—— 不经过 PDF 阅读器,比例由程序定死。
|
||||
# powershell -ExecutionPolicy Bypass -File .claude\r2\print\print_1to1.ps1 -Image .claude\r2\print\blade_1to1.png [-Printer "EPSON3F4E59 (L4260 Series)"]
|
||||
# 做法:System.Drawing.Printing.PrintDocument,Graphics.PageUnit = 毫米,图画成 210x297 mm;
|
||||
# OriginAtMargins=false 时 Graphics 原点在「可打印区」左上角(硬边距处),所以再往回挪一个硬边距,让图的原点落在纸角上。
|
||||
# ⚠ 打印机驱动里如果开了「缩放以适合页面 / 无边距放大」,还是会被缩 —— 打出来量标尺为准。
|
||||
param([Parameter(Mandatory = $true)][string]$Image, [string]$Printer = '')
|
||||
Add-Type -AssemblyName System.Drawing
|
||||
$path = (Resolve-Path $Image).Path
|
||||
$img = [System.Drawing.Image]::FromFile($path)
|
||||
$pd = New-Object System.Drawing.Printing.PrintDocument
|
||||
if ($Printer) { $pd.PrinterSettings.PrinterName = $Printer }
|
||||
if (-not $pd.PrinterSettings.IsValid) { throw "打印机不可用: $($pd.PrinterSettings.PrinterName)" }
|
||||
$pd.DocumentName = 'PhoneRail blade 1to1'
|
||||
$a4 = $pd.PrinterSettings.PaperSizes | Where-Object { $_.Kind -eq [System.Drawing.Printing.PaperKind]::A4 } | Select-Object -First 1
|
||||
if ($a4) { $pd.DefaultPageSettings.PaperSize = $a4 }
|
||||
$pd.DefaultPageSettings.Landscape = $false
|
||||
$pd.OriginAtMargins = $false
|
||||
$script:hm = $null
|
||||
$pd.add_PrintPage({
|
||||
param($s, $e)
|
||||
$g = $e.Graphics
|
||||
$g.PageUnit = [System.Drawing.GraphicsUnit]::Millimeter
|
||||
$g.InterpolationMode = [System.Drawing.Drawing2D.InterpolationMode]::HighQualityBicubic
|
||||
$hx = $e.PageSettings.HardMarginX * 0.254 # 1/100 英寸 -> mm
|
||||
$hy = $e.PageSettings.HardMarginY * 0.254
|
||||
$script:hm = "{0:N2} / {1:N2} mm" -f $hx, $hy
|
||||
$g.DrawImage($img, [single](-$hx), [single](-$hy), [single]210, [single]297)
|
||||
$e.HasMorePages = $false
|
||||
})
|
||||
$pd.Print()
|
||||
$img.Dispose()
|
||||
"已提交打印:$($pd.PrinterSettings.PrinterName),纸 $($pd.DefaultPageSettings.PaperSize.PaperName),硬边距 $script:hm,图按 210x297 mm 画"
|
||||
二进制文件未显示。
@@ -0,0 +1,142 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
u"""焊接用**器件值打印稿**:底板 + 旧版刀片,1:1,同一张 A4 竖版(PDF + 600 dpi PNG)。
|
||||
|
||||
/c/Python314/python .claude/r2/print_value.py # 出 .claude/r2/print/value_1to1.pdf / .png
|
||||
powershell -ExecutionPolicy Bypass -File .claude/r2/print/print_1to1.ps1 -Image .claude/r2/print/value_1to1.png -Printer "EPSON3F4E59 (L4260 Series)"
|
||||
|
||||
- 两块板都画**背面(器件面)、左右镜像**:和把板子翻过来拿在手里看到的一致,字照常可读。
|
||||
- 器件值文字的位置 = `silk_value_plan.py` 排好并下发到 EDA 的位置(`_silk_value_plan.json`)。
|
||||
- 焊盘 / 器件 = `_silk_value_geom.json`(板上读回);红框 = 1 脚(二极管 / 有极性电容 / IC 的方向)。
|
||||
- 正面只有大件(插槽、插座、电解、侧键焊盘),列在页脚,不单独画。
|
||||
"""
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.stdout.reconfigure(encoding='utf-8')
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
sys.path.insert(0, HERE)
|
||||
import matplotlib # noqa: E402
|
||||
matplotlib.use('Agg')
|
||||
import matplotlib.pyplot as plt # noqa: E402
|
||||
from matplotlib.patches import Rectangle, Polygon # noqa: E402
|
||||
from silk_value_plan import pad_box, nums, M # noqa: E402
|
||||
|
||||
plt.rcParams['font.sans-serif'] = ['Microsoft YaHei', 'DejaVu Sans']
|
||||
plt.rcParams['axes.unicode_minus'] = False
|
||||
A4 = (210.0, 297.0)
|
||||
MM = 1 / 25.4
|
||||
OUT = os.path.join(HERE, 'print')
|
||||
geom = json.load(io.open(os.path.join(HERE, '_silk_value_geom.json'), encoding='utf-8'))
|
||||
extra = json.load(io.open(os.path.join(HERE, '_silk_value_extra.json'), encoding='utf-8'))
|
||||
plan = json.load(io.open(os.path.join(HERE, '_silk_value_plan.json'), encoding='utf-8'))
|
||||
restore = json.load(io.open(os.path.join(HERE, '_silk_value_restore.json'), encoding='utf-8'))
|
||||
dump = json.load(io.open(os.path.join(HERE, '_silk_value_dump.json'), encoding='utf-8'))
|
||||
NARROW = matplotlib.font_manager.FontProperties(fname=r'C:\Windows\Fonts\ARIALN.TTF')
|
||||
CAP = 0.716 # Arial 大写字高 / em
|
||||
|
||||
|
||||
def polar(c):
|
||||
r = c['ref']
|
||||
if r[0] in 'UQDY' or r.startswith('DZ') or r.startswith('J'):
|
||||
return True
|
||||
fp = c.get('fp') or ''
|
||||
return r.startswith('C') and ('CAP-' in fp or 'CASE' in fp)
|
||||
|
||||
|
||||
def draw(fig, name, uuid, x0, y0):
|
||||
g = geom[uuid]
|
||||
outline = [(x * M, y * M) for x, y in nums(g['polys'][0])]
|
||||
BW = max(p[0] for p in outline)
|
||||
BH = max(p[1] for p in outline)
|
||||
ax = fig.add_axes([x0 / A4[0], y0 / A4[1], (BW + 2) / A4[0], (BH + 2) / A4[1]])
|
||||
ax.set_xlim(BW + 1, -1) # 背面看:左右镜像
|
||||
ax.set_ylim(-1, BH + 1)
|
||||
ax.set_aspect('equal')
|
||||
ax.axis('off')
|
||||
ax.add_patch(Polygon(outline, closed=True, fill=False, ec='k', lw=0.6))
|
||||
for f in [f for f in extra[uuid]['F'] if f['layer'] == 12]:
|
||||
pl = json.loads(f['poly'])['polygon']
|
||||
q = [(x * M, y * M) for x, y in nums(pl)]
|
||||
if len(q) > 2:
|
||||
ax.add_patch(Polygon(q, closed=True, fc='0.8', ec='k', lw=0.3, hatch='///'))
|
||||
for p in g['pads']: # 安装孔等自由焊盘
|
||||
b = pad_box([p['x'], p['y'], p['rot'], p['layer'], p['pad']])
|
||||
ax.add_patch(Rectangle(b[:2], b[2] - b[0], b[3] - b[1], fc='0.35', ec='none'))
|
||||
for c in g['comps']:
|
||||
bot = c['layer'] == 2
|
||||
bs = []
|
||||
for p in c['pads']:
|
||||
if p['layer'] not in (2, 12):
|
||||
continue
|
||||
b = pad_box([p['x'], p['y'], p['rot'], p['layer'], p['pad']])
|
||||
bs.append(b)
|
||||
col = '#d9b64a' if bot else '#e6e6e6'
|
||||
ax.add_patch(Rectangle(b[:2], b[2] - b[0], b[3] - b[1], fc=col, ec='none'))
|
||||
if bot and p['n'] == '1' and polar(c) and len(c['pads']) >= 2:
|
||||
ax.add_patch(Rectangle(b[:2], b[2] - b[0], b[3] - b[1], fill=False, ec='#d00000', lw=0.45))
|
||||
if bot and bs:
|
||||
x1, y1 = min(b[0] for b in bs) - 0.15, min(b[1] for b in bs) - 0.15
|
||||
x2, y2 = max(b[2] for b in bs) + 0.15, max(b[3] for b in bs) + 0.15
|
||||
ax.add_patch(Rectangle((x1, y1), x2 - x1, y2 - y1, fill=False, ec='#1a3fa0', lw=0.25))
|
||||
texts = {s['id']: s['text'] for s in dump[name]['strings']}
|
||||
layer = {s['id']: s['layer'] for s in dump[name]['strings']}
|
||||
for sid, x, y, rot, fs in plan[name]['rows']:
|
||||
if layer[sid] != 4:
|
||||
continue # 正面的写在页脚
|
||||
fsm = fs * M
|
||||
ax.text(x * M, y * M, texts[sid], fontproperties=NARROW, fontsize=fsm / CAP * 72 / 25.4 * 0.98,
|
||||
ha='center', va='center', rotation=rot, color='k')
|
||||
return BW, BH
|
||||
|
||||
|
||||
def ruler(fig, x0, y0, length):
|
||||
ax = fig.add_axes([0, 0, 1, 1], facecolor='none')
|
||||
ax.set_xlim(0, A4[0])
|
||||
ax.set_ylim(0, A4[1])
|
||||
ax.axis('off')
|
||||
ax.plot([x0, x0 + length], [y0, y0], 'k', lw=0.6)
|
||||
for k in range(0, int(length) + 1):
|
||||
t = 2.0 if k % 10 == 0 else (1.2 if k % 5 == 0 else 0.6)
|
||||
ax.plot([x0 + k, x0 + k], [y0, y0 + t], 'k', lw=0.25)
|
||||
ax.text(x0 + length + 2, y0, u'%d mm(打出来量,应为 %.1f)' % (length, length), fontsize=6, va='center')
|
||||
|
||||
|
||||
def main():
|
||||
os.makedirs(OUT, exist_ok=True)
|
||||
fig = plt.figure(figsize=(A4[0] * MM, A4[1] * MM))
|
||||
txt = fig.add_axes([0, 0, 1, 1], facecolor='none')
|
||||
txt.set_xlim(0, A4[0])
|
||||
txt.set_ylim(0, A4[1])
|
||||
txt.axis('off')
|
||||
base, blade = restore['BASE']['pcb'], restore['BLADE']['pcb']
|
||||
txt.text(A4[0] / 2, 289, u'PhoneRail R2 焊接图 · 器件值 · 1:1(底板 BASE-PCB + 刀片 BLADE-PCB 旧版)',
|
||||
fontsize=9, ha='center', weight='bold')
|
||||
txt.text(A4[0] / 2, 284.5, u'两块都是【背面 / 器件面,左右镜像】= 把板子翻过来看到的样子。红框 = 封装 1 脚(IC 定位;二极管多为负极、电解/钽电容为正极)。打印「实际大小 100%」。',
|
||||
fontsize=6, ha='center')
|
||||
ya = 180.0
|
||||
txt.text(56, ya + 100.5, u'底板 98 × 98(背面)', fontsize=7, weight='bold')
|
||||
draw(fig, 'BASE', base, 55.0, ya - 1)
|
||||
yb = 97.0
|
||||
txt.text(60, yb + 69.5, u'刀片 89.5 × 67(背面,金手指朝下)', fontsize=7, weight='bold')
|
||||
draw(fig, 'BLADE', blade, 59.25, yb - 1)
|
||||
ruler(fig, 20, 86, 80)
|
||||
lines = [
|
||||
u'正面器件(不在图上):',
|
||||
u' 底板:C2 / C3 = 470uF 25V 电解(注意极性) J26 / J27 = 2510-4A 风扇座 J33 = 1×5P 2.54 排针 J34~J38 = PCIE-64P11L 插槽',
|
||||
u' 刀片:J1 = 金手指(不焊) K1~K4 = 侧键焊盘(不焊)',
|
||||
u'字号:大多 1.0 mm,挤的地方 0.85 / 0.7 mm。只写了器件值;看位号请开 EDA。',
|
||||
u'文件:.claude/r2/print/value_1to1.pdf(print_value.py 生成)',
|
||||
]
|
||||
for i, s in enumerate(lines):
|
||||
txt.text(20, 74 - i * 5, s, fontsize=6.2)
|
||||
pdf = os.path.join(OUT, 'value_1to1.pdf')
|
||||
png = os.path.join(OUT, 'value_1to1.png')
|
||||
fig.savefig(pdf)
|
||||
fig.savefig(png, dpi=600)
|
||||
print(u'已出 %s / %s' % (pdf, png))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,116 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
u"""焊接用:把底板 / 旧版刀片的丝印从「位号」临时换成「器件值」,焊完再换回来。
|
||||
|
||||
/c/Python314/python .claude/r2/silk_value.py --push # 隐藏位号 + 在原位建器件值文字,存还原表
|
||||
/c/Python314/python .claude/r2/silk_value.py --restore # 删掉建的文字 + 位号恢复可见
|
||||
|
||||
- 器件值 = 元件 `Value` 属性(阻容感);没有就用 `Device`(型号)去掉 `_Cxxxx` 后缀,个别手工缩写(LABEL_FIX)。
|
||||
- 位号没有「换成别的属性显示」的接口(PCB 属性不能新建),所以做法是:
|
||||
位号 `valueVisible=false` + 同层同位置同字号建一条**自由丝印** `pcb_PrimitiveString`。
|
||||
- 还原表 `_silk_value_restore.json`:建出来的文字 id + 原本可见的位号 id。⚠ 再下单打板前必须 --restore。
|
||||
"""
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
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
|
||||
|
||||
BOARDS = {'BASE': '7b9524b6aaf6f235', 'BLADE': '3b9c4e39c8d164db'} # 刀片用旧版 BLADE-PCB
|
||||
RESTORE = os.path.join(HERE, '_silk_value_restore.json')
|
||||
LABEL_FIX = {
|
||||
'6.3TPV2200M10X10.5 SMD': '2200uF 6.3V',
|
||||
'BM24-50DS/2-0.35V(51) HRS-PIN': 'BM24-50DS',
|
||||
'PCA9555PW,112': 'PCA9555PW',
|
||||
'NCP18XH103F03RB': 'NTC 10k',
|
||||
'MI8-KEY-PAD D1.5': 'KEY',
|
||||
'2.54mm 1*5P 直排针': '1x5P 2.54',
|
||||
}
|
||||
|
||||
|
||||
def label(value, device):
|
||||
if value:
|
||||
return value.replace(u'kΩ', 'k').replace(u'MΩ', 'M')
|
||||
d = device or ''
|
||||
if d in LABEL_FIX:
|
||||
return LABEL_FIX[d]
|
||||
return re.sub(r'_C\d+$', '', d)
|
||||
|
||||
|
||||
READ = u"""
|
||||
await eda.dmt_EditorControl.openDocument('%s');
|
||||
await new Promise(r=>setTimeout(r,3000));
|
||||
const C = await eda.pcb_PrimitiveComponent.getAll()||[];
|
||||
const cv = {}; C.forEach(c=>{const o=c.otherProperty||{}; cv[c.primitiveId]=[o.Value||null, o.Device||null];});
|
||||
const A = (await eda.pcb_PrimitiveAttribute.getAll()||[]).filter(a=>a.key==='Designator').map(a=>({id:a.primitiveId, pid:a.parentPrimitiveId, ref:a.value, layer:a.layer, x:a.x, y:a.y, rot:a.rotation, fs:a.fontSize, lw:a.lineWidth, align:a.alignMode, vis:a.valueVisible, mirror:a.mirror, font:a.fontFamily, reverse:a.reverse, expansion:a.expansion}));
|
||||
return {cv, attrs:A};
|
||||
"""
|
||||
|
||||
PUSH = u"""
|
||||
await eda.dmt_EditorControl.openDocument('%s');
|
||||
await new Promise(r=>setTimeout(r,3000));
|
||||
const J = %s;
|
||||
const made = [], hid = [], err = [];
|
||||
for (const t of J) {
|
||||
try {
|
||||
const s = await eda.pcb_PrimitiveString.create(t.layer, t.x, t.y, t.text, t.font, t.fs, t.lw, t.align, t.rot, t.reverse, t.expansion, t.mirror, false);
|
||||
if (s) made.push([t.ref, s.primitiveId]); else err.push(['create', t.ref]);
|
||||
await eda.pcb_PrimitiveAttribute.modify(t.id, {valueVisible: false});
|
||||
hid.push(t.id);
|
||||
} catch (e) { err.push([t.ref, String(e)]); }
|
||||
}
|
||||
await eda.pcb_Document.save();
|
||||
return {made, hid, err};
|
||||
"""
|
||||
|
||||
UNDO = u"""
|
||||
await eda.dmt_EditorControl.openDocument('%s');
|
||||
await new Promise(r=>setTimeout(r,3000));
|
||||
const R = %s;
|
||||
const ok = await eda.pcb_PrimitiveString.delete(R.made.map(m=>m[1]));
|
||||
let n = 0; for (const id of R.hid) { try { await eda.pcb_PrimitiveAttribute.modify(id, {valueVisible: true}); n++; } catch (e) {} }
|
||||
await eda.pcb_Document.save();
|
||||
return {deleted: ok, shown: n};
|
||||
"""
|
||||
|
||||
|
||||
def push():
|
||||
if os.path.exists(RESTORE):
|
||||
sys.exit(u'已有还原表 %s —— 先 --restore 再重推' % RESTORE)
|
||||
rec = {}
|
||||
for name, u in BOARDS.items():
|
||||
d = run(READ % u, timeout=300)['result']
|
||||
todo = []
|
||||
for a in d['attrs']:
|
||||
if not a['vis']:
|
||||
continue # 原本就隐藏的位号不动
|
||||
v, dev = d['cv'].get(a['pid'], [None, None])
|
||||
a['text'] = label(v, dev)
|
||||
todo.append(a)
|
||||
r = run(PUSH % (u, json.dumps(todo, ensure_ascii=False)), timeout=900)
|
||||
res = r.get('result')
|
||||
assert res, json.dumps(r, ensure_ascii=False)[:600]
|
||||
rec[name] = {'pcb': u, 'made': res['made'], 'hid': res['hid']}
|
||||
io.open(RESTORE, 'w', encoding='utf-8').write(json.dumps(rec, ensure_ascii=False, indent=1))
|
||||
print(u'%s:建 %d 条器件值、藏 %d 个位号,错误 %s' % (name, len(res['made']), len(res['hid']), res['err']))
|
||||
|
||||
|
||||
def restore():
|
||||
rec = json.load(io.open(RESTORE, encoding='utf-8'))
|
||||
for name, R in rec.items():
|
||||
r = run(UNDO % (R['pcb'], json.dumps(R, ensure_ascii=False)), timeout=900)
|
||||
print(name, r.get('result') or r)
|
||||
os.rename(RESTORE, RESTORE + '.done')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if '--push' in sys.argv:
|
||||
push()
|
||||
elif '--restore' in sys.argv:
|
||||
restore()
|
||||
else:
|
||||
print(__doc__)
|
||||
@@ -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()
|
||||
在新工单中引用
屏蔽一个用户