diff --git a/.claude/r2/_base_silk_str.json b/.claude/r2/_base_silk_str.json new file mode 100644 index 0000000..8f3b33c --- /dev/null +++ b/.claude/r2/_base_silk_str.json @@ -0,0 +1,50 @@ +[ + { + "text": "PWR 20V", + "layer": 4, + "x": 12.5, + "y": 1.8, + "fs": 1.2, + "lw": 0.15 + }, + { + "text": "USB", + "layer": 4, + "x": 84.25, + "y": 1.8, + "fs": 1.2, + "lw": 0.15 + }, + { + "text": "NOT PCIe - PhoneRail blade only", + "layer": 3, + "x": 49.0, + "y": 79.48, + "fs": 1.2, + "lw": 0.15 + }, + { + "text": "NOT PCIe", + "layer": 3, + "x": 36.0, + "y": 18.52, + "fs": 1.2, + "lw": 0.15 + }, + { + "text": "NOT PCIe", + "layer": 3, + "x": 62.0, + "y": 38.84, + "fs": 1.2, + "lw": 0.15 + }, + { + "text": "NOT PCIe", + "layer": 3, + "x": 45.0, + "y": 59.16, + "fs": 1.2, + "lw": 0.15 + } +] \ No newline at end of file diff --git a/.claude/r2/silk_str.py b/.claude/r2/silk_str.py new file mode 100644 index 0000000..9ea7447 --- /dev/null +++ b/.claude/r2/silk_str.py @@ -0,0 +1,195 @@ +# -*- coding: utf-8 -*- +u"""底板的**自由丝印**(`pcb_PrimitiveString`):接口标注 + 插槽警告。 + + /c/Python314/python .claude/r2/silk_str.py # 抓板 + 找落点 + 报告(不下发) + /c/Python314/python .claude/r2/silk_str.py --push # 下发(幂等:先删同文案的,再建)+ 读回核对 + /c/Python314/python .claude/r2/silk_str.py --verify # 只读回核对 + +## 要标什么(方案 §5.2 / A1B-PD 的 note / PROGRESS 待办) + +| 文案 | 层 | 为什么 | +|---|---|---| +| `PWR 20V` | 4(底层,J1 在底面)| J1 与 J23 是**同一颗 USB-C 座**,插错要能看出来 | +| `USB` | 4 | 同上,J23 是 Hub 上行口 | +| `NOT PCIe - PhoneRail blade only` | 3(顶层,插槽在顶面)| 槽是 PCI-E x4 座,但针位是自定义的;插真 PCIe 卡会烧 | +| `NOT PCIe` ×3 | 3 | 五个槽每个都要邻着一条提示,完整句只放得下一条 | + +## 判据:丝印**只避** 焊盘 / 过孔 / 位号 / 安装孔 / 板框外 + +⛔ **压铜是常态**,不算冲突 —— 铜上有阻焊,油墨正常印在阻焊上。第一版把「压铜」也当冲突, + 于是六个候选位全军覆没,看着像「板上没地方放字」。 + +⚠ `alignMode` 要显式传 **5 = CENTER**(枚举 `EPCB_PrimitiveStringAlignMode`)。 + 不传的话默认不是居中,坐标含义和这里按中心算的框对不上,字会整体偏出去。 + (位号 `Attribute` 用的 3 是另一套口径,别照抄。) +""" +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)) +sys.path.insert(0, HERE) +from eda import run # noqa: E402 +import pcb_base as B # noqa: E402 +import fp_edge # noqa: E402 + +MIL = fp_edge.MIL +FS, LW = 1.2, 0.15 # 字高 / 线宽(嘉立创丝印下限 0.8 / 0.15) +CHAR_W, EDGE, CLR = 0.62, 0.5, 0.10 +ALIGN_CENTER = 5 +PLAN = os.path.join(HERE, '_base_silk_str.json') + +# (文案, 层, 目标 x, 目标 y, 允许偏移 dx, dy) +SILK = [ + (u'PWR 20V', 4, 12.5, 1.8, 4.0, 1.0), # J1(底面)南侧 + (u'USB', 4, 84.0, 1.8, 4.0, 1.0), # J23(底面)南侧 + (u'NOT PCIe - PhoneRail blade only', 3, 49.0, 79.48, 8.0, 3.0), # 槽4~槽5 之间 + (u'NOT PCIe', 3, 36.0, 18.52, 10.0, 3.0), # 槽1~槽2 + (u'NOT PCIe', 3, 62.0, 38.84, 10.0, 3.0), # 槽2~槽3 + (u'NOT PCIe', 3, 49.0, 59.16, 16.0, 4.0), # 槽3~槽4 +] + +READ = u""" +await eda.dmt_EditorControl.openDocument('%s'); +await new Promise(r=>setTimeout(r,2500)); +const M = 0.0254; +const V = (await eda.pcb_PrimitiveVia.getAll()||[]).map(v=>[v.x*M, v.y*M, v.diameter*M]); +const A = (await eda.pcb_PrimitiveAttribute.getAll()||[]).filter(a=>a.key==='Designator' && a.valueVisible) + .map(a=>({ref:a.value, lay:a.layer, x:a.x*M, y:a.y*M, rot:((a.rotation%%360)+360)%%360, fs:a.fontSize*M})); +const S = (await eda.pcb_PrimitiveString.getAll()||[]) + .map(s=>({lay:s.layer, x:s.x*M, y:s.y*M, t:(s.text!==undefined?s.text:s.string), id:s.primitiveId})); +return {vias:V, attrs:A, strs:S}; +""" % B.PCB + + +def tbox(t, x, y, fs=FS): + h = fs + w = CHAR_W * h * len(t) + return (x - w / 2. - CLR, y - h / 2. - CLR, x + w / 2. + CLR, y + h / 2. + CLR) + + +def _ov(p, q): + return not (p[2] <= q[0] or q[2] <= p[0] or p[3] <= q[1] or q[3] <= p[1]) + + +def clean(t, lay, x, y, pads, vias, attrs): + bb = tbox(t, x, y) + if bb[0] < EDGE or bb[1] < EDGE or bb[2] > B.BW - EDGE or bb[3] > B.BH - EDGE: + return False + want = (1, 12) if lay == 3 else (2, 12) + for p in pads: + if p[6] in want and _ov(bb, (p[2] - p[4] / 2., p[3] - p[5] / 2., + p[2] + p[4] / 2., p[3] + p[5] / 2.)): + return False + for (vx, vy, vd) in vias: + if _ov(bb, (vx - vd / 2., vy - vd / 2., vx + vd / 2., vy + vd / 2.)): + return False + for a in attrs: + if a['lay'] != lay: + continue + h = a['fs'] + w = CHAR_W * h * len(a['ref']) + if a['rot'] in (90, 270): + w, h = h, w + if _ov(bb, (a['x'] - w / 2., a['y'] - h / 2., a['x'] + w / 2., a['y'] + h / 2.)): + return False + r = B.HOLE_D / 2. + 0.6 + for (hx, hy) in B.MOUNT_HOLES.values(): + if _ov(bb, (hx - r, hy - r, hx + r, hy + r)): + return False + return True + + +def fit(t, lay, x0, y0, dx, dy, pads, vias, attrs): + r = 0.0 + while r <= max(dx, dy): + for sy in (0, 1, -1): + for sx in (0, 1, -1): + x = x0 + sx * r + y = y0 + sy * r * (dy / max(dx, 1e-9)) + if abs(x - x0) > dx or abs(y - y0) > dy: + continue + if clean(t, lay, x, y, pads, vias, attrs): + return round(x, 3), round(y, 3) + r += 0.25 + return None + + +def main(): + r = run(READ, timeout=600) + d = r.get('result') + assert d, json.dumps(r, ensure_ascii=False)[:400] + pads = json.load(io.open(os.path.join(HERE, '_base_pads.json'), encoding='utf-8')) + print(u'板上:过孔 %d / 可见位号 %d / 自由丝印 %d' + % (len(d['vias']), len(d['attrs']), len(d['strs']))) + plan, miss = [], [] + for (t, lay, x0, y0, dx, dy) in SILK: + g = fit(t, lay, x0, y0, dx, dy, pads, d['vias'], d['attrs']) + if g: + plan.append({'text': t, 'layer': lay, 'x': g[0], 'y': g[1], 'fs': FS, 'lw': LW}) + print(u' %-34s L%d (%7.3f, %7.3f) 偏离目标 %.2f' + % (t, lay, g[0], g[1], math.hypot(g[0] - x0, g[1] - y0))) + else: + miss.append(t) + print(u' %-34s L%d ⛔ 找不到干净落点' % (t, lay)) + io.open(PLAN, 'w', encoding='utf-8').write(json.dumps(plan, ensure_ascii=False, indent=1)) + if miss: + sys.exit(u'!! %d 条没落点,不下发' % len(miss)) + if '--verify' in sys.argv: + verify(plan) + return + if '--push' not in sys.argv: + print(u'(只算;加 --push 下发)') + return + # 幂等:先删掉板上文案相同的,再建 + kill = [s['id'] for s in d['strs'] if s['t'] in [p['text'] for p in plan]] + rows = [[p['layer'], p['x'] * MIL, p['y'] * MIL, p['text'], p['fs'] * MIL, p['lw'] * MIL] + for p in plan] + res = run(u""" +await eda.dmt_EditorControl.openDocument('%s'); +await new Promise(r=>setTimeout(r,2500)); +const KILL = %s, R = %s; +if (KILL.length) { try { await eda.pcb_PrimitiveString.delete(KILL); } catch (e) {} } +let n = 0; +for (const [lay, x, y, t, fs, lw] of R) { + const o = await eda.pcb_PrimitiveString.create(lay, x, y, t, undefined, fs, lw, %d, 0); + if (o) n++; +} +await eda.pcb_Document.save(); +return {deleted: KILL.length, made: n, + now: (await eda.pcb_PrimitiveString.getAll()||[]).length}; +""" % (B.PCB, json.dumps(kill), json.dumps(rows, ensure_ascii=False), ALIGN_CENTER), timeout=600) + print(u'下发:%s' % json.dumps(res.get('result'), ensure_ascii=False)) + verify(plan) + + +def verify(plan): + u"""读回逐条核对。 + + ⛔ **不能用文案当字典 key** —— `NOT PCIe` 有三条,同名会互相覆盖, + 于是「6 条都建上了」却报成「4/6 对上」。按**文案 + 坐标**配对,配上一条就消费掉一条。""" + back = run(READ, timeout=600).get('result') or {} + pool = list(back.get('strs', [])) + bad = [] + for q in plan: + hit = None + for k, s in enumerate(pool): + if s['t'] == q['text'] and abs(s['x'] - q['x']) <= 0.05 and abs(s['y'] - q['y']) <= 0.05 and s['lay'] == q['layer']: + hit = k + break + if hit is None: + bad.append(u'%s @ (%.3f, %.3f) L%d' % (q['text'], q['x'], q['y'], q['layer'])) + else: + pool.pop(hit) + print(u'读回核对:%d / %d 条对上;板上自由丝印共 %d 条%s' + % (len(plan) - len(bad), len(plan), len(back.get('strs', [])), + (u';没对上:%s' % u' / '.join(bad)) if bad else u'')) + return not bad + + +if __name__ == '__main__': + main() diff --git a/PROGRESS.md b/PROGRESS.md index 08e879d..aa5c3ad 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -87,6 +87,29 @@ 可行性先算过:现字高下 163 个里 161 个有解 ⇒ **不用缩字号**。 +### 自由丝印(`silk_str.py`,同日补) + +板上 `pcb_PrimitiveString` 原本 **0 条** —— 方案 §5.2 要求的插槽警告、J1/J23 的接口标注, +一直只写在原理图的说明文字里,**从没落到 PCB**。已补 6 条并读回核对: + +| 文案 | 层 | 坐标 | +|---|---|---| +| `PWR 20V` | 4(底面,J1)| (12.500, 1.800) | +| `USB` | 4(底面,J23)| (84.250, 1.800) | +| `NOT PCIe - PhoneRail blade only` | 3(顶面,插槽)| (49.000, 79.480) | +| `NOT PCIe` ×3 | 3 | (36.000, 18.520) / (62.000, 38.840) / (45.000, 59.160) | + +完整句只放得下一条,其余槽间放短标 ⇒ **五个槽每个都邻着一条**。字高 1.2 / 线宽 0.15。 + +**三条坑**: +1. ⛔ **丝印压铜不算冲突**(铜上有阻焊,油墨正常印在上面)。第一版把压铜也当冲突, + 六个候选位全军覆没,看着像「板上没地方放字」。真正要避的只有 + **焊盘 / 过孔 / 位号 / 安装孔 / 板框外**。 +2. ⚠ **`alignMode` 要显式传 5(CENTER)**,不传默认不是居中,字会整体偏出去。 + 位号 `Attribute` 用的 3 是另一套口径,别照抄。 +3. ⚠ **核对不能拿文案当字典 key**:`NOT PCIe` 有三条,同名互相覆盖 ⇒ + 「6 条都建上了」却报成「4/6 对上」。按**文案 + 坐标 + 层**配对,配上一条消费一条。 + ## ⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐ 2026-09-12(收尾):按用户最新布局**重布风扇窗口并已下发** —— 窝内只差 J27.4 用户:「将底板按照现在我的布局重新布线」→ 选「风扇窗口整体重布(拆掉那 43 段重来)」+「GND 回流孔一起补」。 diff --git a/刀片式方案规划-R2.md b/刀片式方案规划-R2.md index 0841311..31742c7 100644 --- a/刀片式方案规划-R2.md +++ b/刀片式方案规划-R2.md @@ -3326,6 +3326,32 @@ FAN_PWM_L (10.30, 83.90, 22.50, 87.30) 盖住 J27.4 与 Q6.3 工具:**`silk_base.py`**(`--push` 走 `pcb_PrimitiveAttribute.modify(id, {x, y, rotation})`, 下发后读回逐个核对),快照 `_base_silk.json`、方案 `_base_silk_plan.json`。 +### 39.5 自由丝印(`silk_str.py`,2026-09-12 补) + +板上原本 `pcb_PrimitiveString` 是 **0 条** —— 方案 §5.2 要求的插槽警告、以及 J1/J23 的接口标注 +一直只写在原理图的说明文字里,**没有落到 PCB**。已补 6 条: + +| 文案 | 层 | 坐标 | 为什么 | +|---|---|---|---| +| `PWR 20V` | 4 | (12.500, 1.800) | J1 与 J23 **同一颗 USB-C 座**,插错要能看出来 | +| `USB` | 4 | (84.250, 1.800) | J23 是 Hub 上行口 | +| `NOT PCIe - PhoneRail blade only` | 3 | (49.000, 79.480) | 槽是 PCI-E x4 座但针位自定义,插真卡会烧 | +| `NOT PCIe` ×3 | 3 | (36.000, 18.520) / (62.000, 38.840) / (45.000, 59.160) | 完整句只放得下一条,其余槽间放短标 ⇒ **五个槽每个都邻着一条** | + +字高 1.2 / 线宽 0.15(嘉立创丝印下限 0.8 / 0.15)。J1/J23 在底面 ⇒ 走底层丝印 layer 4; +插槽在顶面 ⇒ layer 3。 + +**两条判据上的坑**: + +1. ⛔ **丝印压铜不算冲突** —— 铜上有阻焊,油墨正常印在阻焊上。第一版把「压铜」也当冲突, + 于是六个候选位全军覆没,看着像「板上没地方放字」。真正要避的只有 + **焊盘 / 过孔 / 位号 / 安装孔 / 板框外**。 +2. ⚠ **`alignMode` 必须显式传 5(CENTER)**。不传的话默认不是居中,坐标含义和按中心算的 + 文字框对不上,字会整体偏出去。位号 `Attribute` 用的 3 是另一套口径,别照抄。 + +`silk_str.py` 自带落点搜索(0.25 mm 步长、限定偏移上限)+ 幂等下发(先删同文案再建)+ 读回核对, +落点存 `_base_silk_str.json`。 + ## 40. 刀片金手指两侧开台阶(2026-09-12 用户指出「插不进插槽」)+ 铺铜补齐 用户:「解决刀片板与插槽干涉的问题,完善刀片板铺铜」→「金手指两侧没有开槽,应该会插不进插槽」。