tools/epro2/kicad: Phase-1 .kicad_pcb exporter — 6/6 boards open in KiCad 8

Phase-1 scope: produce a .kicad_pcb that kicad-cli loads cleanly and
that has the right geometry (nets, footprints, tracks, vias, board
outline) — not a 1:1 EDA round-trip. Skipped on purpose for Phase 2:
copper pours (POUR/POURED), manual FILL, teardrops, board-level
strings/images, ARC circle-center recovery.

What lands:
  - pcb_writer.write_pcb(): header/general, data-driven layer table
    (F.Cu = ord 0; B.Cu = ord 31; SIGNAL inner ids 15+ allocated to
    In1.Cu/In2.Cu/... in EPRO2-id sorted order so used inner layers
    stay contiguous), net-name → integer id map (id 0 reserved for the
    empty net per KiCad convention), LINE→segment / LINE→gr_line on
    Edge.Cuts, layer-11 POLY paths walked into Edge.Cuts gr_line chains
    (the actual board outline lives on POLY here, not LINE — without
    this stats showed edge=0), VIA→via.
  - footprint_writer.write_footprint_placement(): inline (footprint ...)
    blocks per PCB COMPONENT. EPRO2 RECT/ELLIPSE/OVAL/POLYGON pad
    shapes mapped to KiCad rect/circle/oval/custom; SMD vs THT detected
    by PAD.hole presence; SLOT holes use (drill oval w h). Pad nets
    resolved cross-doc via the existing PCB.PAD_NET → footprint.pad
    chain in ProjectRelations. layerId=2 component → (layer B.Cu) +
    text on B.SilkS so bottom-side parts render correctly.

Smoke test on ESP-VoCat (6 PCBs): all 6 pass `kicad-cli pcb export svg`
and render. DRC on smallest (MicBoard) reports 145 violations + 75
unconnected — most of the unconnected are GND nets that the EPRO2
source resolves through POUR copper, which Phase 2 will export.

CLI: `python -m tools.epro2.kicad <project> --all-pcb --out <dir>`
emits one .kicad_pcb per PCB doc.

52 → 65 unit tests pass. Float comparisons in tests use math.isclose
because the s-expr 6-decimal trim doesn't preserve strict equality
through `value * MIL_TO_MM` round-trips.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-29 00:18:32 +08:00
parent fc2a45f658
commit e61404478e
6 changed files with 1211 additions and 1 deletions

67
log.md
View File

@@ -4,6 +4,73 @@
---
## 2026-04-29 01:30 KiCad 导出 Phase 3 PCB6/6 .kicad_pcb 全部 kicad-cli 通过
**Claude 会话**
`ff5553f` 后续。Forge 投影最后一块——schematic 已经够用,做 `.kicad_pcb` 导出。一上来就铺了 Phase-1 最小可解析 scope要先有"能在 KiCad 8 里打开 + kicad-cli 处理"的底线),不追全保真。
### 做的
1. **`tools/epro2/kicad/pcb_writer.py`** — 单 PCB doc → `.kicad_pcb` 主入口
- 数据驱动的层映射 `_build_layer_map`:扫所有 primitive 的 `layerId`,把真正用到的 SIGNAL 内层id 15+)按 EPRO2-id 顺序铺成 `In1.Cu`/`In2.Cu`/...F.Cu 永远 ordinal 0、B.Cu 永远 ordinal 31KiCad 硬约定)。
- net 表:从 NET op 取名字,从 1 开始分配 id0 留给 KiCad 的 "no net")。
- 走 LINEcopper 层 → `(segment ...)`layer 11 OUTLINE → `(gr_line layer Edge.Cuts)`;走 VIA → `(via)`;走 layer-11 POLY → 把 path 拆 polyline 段 emit 成 Edge.Cuts 上的 gr_line 链(实测板子轮廓全在 POLY 不在 LINE少这步 edge=0
- ARC 现在按弦近似Phase 1 不解 EPRO2 ARC.path 的圆心/半径表示KiCad 收得下DRC 报 invalid_outline——下一轮改
2. **`tools/epro2/kicad/footprint_writer.py`** — inline `(footprint ...)`
- 走 FOOTPRINT.PAD`RECT/ELLIPSE/OVAL/POLYGON` → KiCad `rect/circle/oval/custom`hole 存在则 SMD→thru_hole + `(drill ...)`SLOT 走 `(drill oval w h)` 二参形式。
- layer 映射layerId 1 → `F.Cu+F.Mask+F.Paste`、2 → `B.Cu+B.Mask+B.Paste`、12 (MULTI/THT) → `*.Cu+*.Mask`
- net 解析借 `pcb_rel.pad_nets_by_pad[pad_id]`(之前 `ProjectRelations` 已经为 PCB→FOOTPRINT cross-doc 攒过的索引现在派上用场)。
- bottom-side COMPONENT (`layerId=2`) 整个 footprint 走 `(layer B.Cu)`Reference 标签同步落到 `B.SilkS`
3. **CLI 加 `--all-pcb`**`tools/epro2/kicad/__main__.py`):每个 PCB doc 一文件,文件名取 META.title。
### ESP-VoCat 实测6 个 PCB
| 板 | nets | footprints | segments | vias | edge_cuts |
|---|---:|---:|---:|---:|---:|
| BaseBoard | 38 | 59 | 391 | 126 | 4 |
| CoreBoard | 84 | 87 | 1131 | 262 | 4 |
| MicBoard | 13 | 17 | 88 | 47 | 16 |
| LCD-BD | 7 | 5 | 66 | 35 | 2 |
| Mainboard | 24 | 33 | 340 | 181 | 4 |
| Sub-board | 4 | 5 | 10 | 0 | 4 |
**6/6 都过 `kicad-cli pcb export svg`**——文件解析无 error、SVG 输出正常。footprint/track/via/edge cuts 全部可见。
`kicad-cli pcb drc` 对最小的 MicBoard 跑145 violations + 75 unconnected。分布
- 28 clearancetrace/pad 间距)+ 19 track_dangling + 39 via_dangling + 75 unconnected — 真连接问题,部分是 EPRO2 源里靠 POUR 解决但我们 Phase 1 没导
- 21 silk_overlap + 8×2 edge_clearance + 3 invalid_outline — 边角 + silk 美化
- 17 lib_footprint_issues — facere 库没注册cosmetic跟 schematic 一样)
### Phase 1 砍掉的 / 下一轮再做
- **POUR / POURED** — 铜皮覆铜,导了 75 unconnected 的大头会消掉GND 大面积在 POUR 里走,没导出来 trace 当然报 unconnected
- **FILL** — 元件下方手工 fill 块
- **ARC 圆心/半径解析** — 现在按弦近似invalid_outline 警告就这来的
- **TEARDROP** — pad/via 接 trace 处的圆角,纯美化
- **STRING / IMAGE** — 板上文字和 logo 图
### 决策Why
- **板子原点平移到 (100, 100) mm**EPRO2 板能在任意坐标含负KiCad 画布原点在 (0,0),不平移开 KiCad 时板子可能根本不在视野里。100mm 是经验值,留够 silkscreen 边距。
- **footprint inline 不走外部 .kicad_mod**:跟 schematic lib_symbol 同思路,自包含,能直接 `kicad-cli` 处理;缺点是同款 footprint 重复写多份,但 EPRO2 一个 SCH 内的 FOOTPRINT 数量级(几十)摊开来文件膨胀也不大。
- **ELLIPSE 当 circle 不当 oval**:实测 ELLIPSE 的 width/height 经常相等(=圆形 padKiCad 没有真椭圆 pad typecircle 取 max(w,h) 比 oval 更接近圆。
- **ARC 暂时按弦**:算 EPRO2 ARC 圆心要先反推三点定圆浮点精度敏感。Phase 2 单独处理。
### 测试
52 → 65 单测全过pcb_writer 8 个layer map / net id / segment / via / outline POLY / 零长 skip / 非 PCB doc reject+ footprint_writer 5 个SMD pad layers / 圆 vs slot drill / pad net 跨 doc 解析 / bottom-layer 翻转 / unresolved skip
### 下一步建议
- **POUR/POURED 导 zone**(中工作量):消掉大部分 75 unconnectedCoreBoard 这种 4-layer + 大量 GND/POWER 覆铜的板真实连通性会贴近完整。
- **schematic + PCB 同时跑** 一个 export 命令(`--all`),生成完整 KiCad project 目录。
- **Phase 4: KiCad project 文件 .kicad_pro**:让用户双击就能在 KiCad GUI 里打开 schematic + PCB 配对。
---
## 2026-04-29 01:00 科普文档:爬取 per-doc .epro2 vs 网页端 .epro2 ZIP 整包
**Claude 会话**