# -*- coding: utf-8 -*- u"""重新抓下板 PCB1 的 `pcb1-parts.json`(每个元件的**真实焊盘包围盒**,本地检查用)。 **从原理图导入变更之后必须跑一次**(skill §9m:包围盒不能在元件摆好之后重抓 —— 这里已经把焊盘位置反转回元件本地坐标系,所以摆好之后再抓也等价)。 用法: /c/Python314/python .claude/pcb1_parts_dump.py """ import io, json, os, sys HERE = os.path.dirname(os.path.abspath(__file__)); sys.path.insert(0, HERE) from eda import run PCB = '7e9415a16a909c51' OUT = os.path.join(HERE, 'pcb1-parts.json') JS = u""" await eda.dmt_EditorControl.openDocument('__PCB__'); await new Promise(r=>setTimeout(r,2000)); const parts = []; for (const c of await eda.pcb_PrimitiveComponent.getAll()) { const d = c.designator || c.name; const x = c.x, y = c.y, rot = ((c.rotation % 360) + 360) % 360; const lay = (c.layer !== undefined) ? c.layer : 1; const th = -rot * Math.PI / 180, ct = Math.cos(th), st = Math.sin(th); let ps = []; try { ps = await eda.pcb_PrimitiveComponent.getAllPinsByPrimitiveId(c.primitiveId); } catch(e) {} let x0=null,y0=null,x1=null,y1=null; const pads = []; for (const p of ps || []) { let w = 0, h = 0, isPoly = false, pcx = p.x, pcy = p.y; try { const s = p.pad; if (s && s[0] === 'POLYGON') { // ⚠️ POLYGON 盘的 s[1] 是**点数组**(板坐标 mil),不是宽度。 // 原来 `+s[1] || 0` 得到 NaN -> 0,尺寸就退回到 hole 那一位 —— // 于是 U1~U5 的散热盘(真身 2.03 x 4.32 mm)在 pcb1-parts.json 里 // 被记成一个 0.12 mm 的点。见 dump_pads.py 文件头第 3 条。 isPoly = true; const pts = (s[1] || []).filter(v => typeof v === 'number'); const xs = pts.filter((_, i) => i % 2 === 0), ys = pts.filter((_, i) => i % 2 === 1); const ax = Math.min.apply(null, xs), bx = Math.max.apply(null, xs); const ay = Math.min.apply(null, ys), by = Math.max.apply(null, ys); w = bx - ax; h = by - ay; pcx = (ax + bx) / 2; pcy = (ay + by) / 2; } else { w = +s[1] || 0; h = +s[2] || 0; } } catch(e) {} const rx = pcx - x, ry = pcy - y; const px = rx * ct - ry * st, py0 = rx * st + ry * ct; const py = (lay === 2) ? -py0 : py0; // ⚠️ `p.pad` 里的 w/h 是**未旋转**的,焊盘自己还有一个 rotation。 // QFN / SOIC 的侧边引脚就是把同一个长条焊盘转 90° 用的 —— // 不做这一步,U6(WQFN-16)的 bb 在 y 上会短 0.26 mm, // 而摆位检查、铺铜反焊盘全都吃这个 bb。 // 2026-09-06 实测:C607 贴到 U6 北排脚旁边,本地检查说净距 0.358(过), // DRC 实测 0.096(3.8 mil,规则要 7.9)。**几何检查全绿、板子是错的。** // 这一套 API 里 getAllPinsByPrimitiveId 给的 rotation 是**度** // (pcb_PrimitivePad.getAll() 给的却是弧度,见 pcb_fanout.py 文件头坑 1)。 // POLYGON 的点已经是板坐标,AABB 就是最终朝向,只要跟着**元件**的旋转转回本地。 let pr = (((+p.rotation || 0) - rot) % 360 + 360) % 360; if (!isPoly && (pr === 90 || pr === 270)) { const t = w; w = h; h = t; } if (isPoly && (rot === 90 || rot === 270)) { const t = w; w = h; h = t; } let hw = 0, hh = 0; try { // 单铜层焊盘(layer 1/2)上的 hole 是假的(POLYGON 盘的 0.6xmin 占位值), // 不许让它顶掉真实焊盘尺寸 —— 见 dump_pads.py 文件头第 3 条。 if (Array.isArray(p.hole) && p.layer !== 1 && p.layer !== 2) { if (String(p.hole[0]).toUpperCase() === 'ROUND') { hw = hh = +p.hole[1] || 0; } else { hw = +p.hole[1] || 0; hh = +p.hole[2] || hw; } } } catch(e) {} const ww = Math.max(w, hw), hhh = Math.max(h, hh); pads.push([p.padNumber, +px.toFixed(3), +py.toFixed(3), ww, hhh, p.net || null]); const a = px - ww/2, b = py - hhh/2, cc = px + ww/2, dd = py + hhh/2; x0 = (x0===null? a : Math.min(x0,a)); y0 = (y0===null? b : Math.min(y0,b)); x1 = (x1===null? cc: Math.max(x1,cc)); y1 = (y1===null? dd: Math.max(y1,dd)); } parts.push({d, x, y, rot, lay, fp: (c.footprint && c.footprint.name) || null, n: (ps||[]).length, bb: (x0===null ? null : [x0, y0, x1, y1]), pads}); } return parts; """.replace('__PCB__', PCB) r = run(JS, timeout=900) if not r.get('success'): sys.exit(json.dumps(r, ensure_ascii=False)[:600]) parts = r['result'] io.open(OUT, 'w', encoding='utf-8').write(json.dumps(parts, ensure_ascii=False)) print(u'pcb1-parts.json %d 个元件(bb / pads 都是**元件本地坐标**,单位 mil)' % len(parts)) nobb = [p['d'] for p in parts if not p['bb']] if nobb: print(u'⚠ 没有焊盘的元件 %d 个: %s' % (len(nobb), nobb[:12]))