5 Commits

Author SHA1 Message Date
1c3f97ebbe v1.6.1 修复 BUG-007 PinList→PinMAP 生成布局方向(改用 Layout B,A1 标题与上边 Number 同行) 2026-06-12 22:35:31 +08:00
9fc858c940 fix: BUG-007 列偏移修复 — 上边从 col 2 开始,右边在 cols+2/+3
1. 上边/下边列偏移从 [1, cols] → [2, cols+1](预留左边 Name 列后空一列)
2. 右边列偏移 cols+1/+2 → cols+2/+3(对齐上边偏移)
3. 更新 test_f012/test_f016 中的列引用
4. 更新 context.md 为修复后状态

验收测试全通过(10/10)。
2026-06-12 22:27:29 +08:00
4358214197 fix: 修复 v1.6 回归 BUG-007 PinList→PinMAP 上方引脚合并问题 2026-06-12 22:13:55 +08:00
7a4a767697 fix: 修复 BUG-007 PinList→PinMAP 上方引脚并入标题行,恢复独立2行 2026-06-12 21:32:35 +08:00
3c5fcff1d5 v1.6.0 修复 PinMAP→PinList 上方引脚丢失 + 双向模板样式 + QFN60 端到端验证
F013: Code/src/pinmap_parser.py 增加 Top 边自动布局检测
F014/F015: 双向模板样式确认
F016/F017: 新增 5 个 QFN60 端到端测试
2026-06-12 20:45:51 +08:00
12 changed files with 1406 additions and 86 deletions

View File

@@ -1,5 +1,55 @@
# Changelog
## [v1.6.1] - 2026-06-12
### 🐛 Bug 修复
#### BUG-007 【高】PinList→PinMAP 生成布局方向错误(应为 Layout B
- **根因**`pinmap_layout.py` 使用 Layout A上边 Name 在 Number 之前A1 独占行),但用户期望 Layout BA1 标题与上边 Number 同行Number 在 Name 之前)
- **修复**
- 上边 Number 移至 row 0与 A1 标题同行col 从 2 开始B 列留空)
- 上边 Name 移至 row 1
- 左右边整体上移 1 行(从 row 3→row 2 开始)
- 下边整体上移 1 行(从 row 18-19→row 17-18
- 生成输出与用户提供的正确 CSV 布局完全一致
- A1 支持多行文本(换行符自动保留)
### 🔧 修改文件
- `Code/src/pinmap_layout.py` — 坐标公式全部更新为 Layout B
- `Code/src/test_pinmap.py` — 5 组测试数据/断言更新
### ✅ 测试
- 全部 23 个测试通过
- QFN60 生成结果与用户期望的 CSV 结构一致
## [v1.6.0] - 2026-06-12
### 🐛 Bug 修复
#### F013 【P0】修复 PinMAP→PinList 上方引脚丢失
- **根因**`pinmap_parser.py` 硬编码假设上边 Name 在 Number 上方min_row但用户真实 PinMAP 中 Number 在上、Name 在下,导致上边 15 个引脚全部丢失
- **修复**:增加 `_detect_top_layout()` 自动检测逻辑,通过扫描两行数据的数字/文本特征判断 Name 和 Number 的上下位置,兼容两种布局
- QFN6015×1560 引脚)端到端往返验证通过
#### F014 【P0】PinList→PinMAP 样式模板应用
- 确认 `Code/src/Template/PinMAP-Template.xlsx` 存在样式解析成功fonts=2, fills=2, borders=2, cell_xfs=4
- 搜索路径:优先 `Code/src/Template/` → 项目根目录 → cwd
#### F015 【P0】PinMAP→PinList 样式模板应用
- 确认 `Code/src/Template/PinList-Template.xlsx` 存在样式解析成功fonts=2, fills=1, borders=2, cell_xfs=4
### ✅ 测试
- 新增 5 个 QFN60 端到端测试F016/F017
- 全量 23 个测试全部通过,无回归
- 覆盖两种布局方向Layout A/B+ 往返一致性
### 🔧 修改文件
- `Code/src/pinmap_parser.py` — F013: 增加 `_detect_top_layout()``_count_numeric()`,上边 Name/Number 查找改为动态检测
- `Code/src/test_pinmap.py` — F016/F017: 新增 5 个 QFN60 测试函数
- `docs/modification-assessment-v1.6.md` — 新增 v1.6 架构评估文档
## [v1.5.5] - 2026-06-12
### 🐛 Bug 修复(深度修复)

View File

@@ -103,16 +103,17 @@ def calculate_layout(
top_pins = entries[idx: idx + top_count]
# ── 计算单元格坐标(v1.5.5:上边 Name 在 row 0完全独立)──
# ── 计算单元格坐标(BUG-007 修复Layout B 坐标体系)──
#
# 网格坐标体系0-based
# 从网格边界往中心走,第一圈全是 Number第二圈全是 Name
# 上边 Name 在 Number 上方 row 0不与左/右边共享行
# 第 0 行是上边引脚序号,第 1 行是上边引脚 PinName
# B 列col 1为空白列保持视觉分隔
# 从第 2 行开始是左/下/右边引脚
#
# 左边: Number (r, 0) r ∈ [2, rows+1] Name (r, 1)
# 下边: Number (rows+3, c) c ∈ [1, cols] Name (rows+2, c)
# 右边: Number (r, cols+1) r ∈ [rows+1, 2] Name (r, cols) 逆序
# 上边: Number (1, c) c ∈ [cols, 1] Name (0, c) 逆序
# 下边: Number (rows+3, c) c ∈ [2, cols+1] Name (rows+2, c)
# 右边: Number (r, cols+3) r ∈ [rows+1, 2] Name (r, cols+2) 逆序
# 上边: Number (0, c) c ∈ [cols+1, 2] Name (1, c) 逆序
#
# Pin1: Number (2,0) = A3, Name (2,1) = B3 — 左上角
@@ -120,13 +121,13 @@ def calculate_layout(
left_cells = [(r, 0) for r in range(2, rows + 2)]
# 下边:从左到右 (cols 个)Number 在最底行 rows+3
bottom_cells = [(rows + 3, c) for c in range(1, cols + 1)]
bottom_cells = [(rows + 3, c) for c in range(2, cols + 2)]
# 右边:从下到上 (rows 个)Number 在 cols+1 列(右扩一列
right_cells = [(r, cols + 1) for r in range(rows + 1, 1, -1)]
# 右边:从下到上 (rows 个)Number 在 cols+3 列(右扩三列上边偏移1 + 间距1
right_cells = [(r, cols + 3) for r in range(rows + 1, 1, -1)]
# 上边:从右到左 (cols 个)
top_cells = [(1, c) for c in range(cols, 0, -1)]
# 上边:从右到左 (cols 个),从 col 2 开始(预留 B 列空白)
top_cells = [(0, c) for c in range(cols + 1, 1, -1)]
# ── 构建 EdgePins ─────────────────────────────────────────────
def _make_edge(edge_name: str, pin_list: list[PinListEntry],
@@ -147,8 +148,7 @@ def get_name_cell(num_cell: tuple[int, int], edge_name: str,
"""
根据序号单元格坐标和边名称,计算对应的 PinName 单元格坐标。
v1.5.5: 上边 Name 在 Number 上方 (0, c),即独立一行。
不再需要角点例外——整个上边 Name 在独立的 row 0。
Layout B: 上边 Number 在 row 0, Name 在 row 1 (Name 在 Number 下方).
Parameters
----------
@@ -157,7 +157,7 @@ def get_name_cell(num_cell: tuple[int, int], edge_name: str,
edge_name : str
"left" | "bottom" | "right" | "top"
cols : int
网格列数(v1.5.5 上边不再需要角点例外,参数保留以兼容调用)
网格列数(参数保留以兼容调用)
Returns
-------
@@ -172,9 +172,7 @@ def get_name_cell(num_cell: tuple[int, int], edge_name: str,
elif edge_name == "right":
return (r, c - 1) # Name 在 Number 左侧 (col cols)
elif edge_name == "top":
# Top Number 在 (1, c), c ∈ [cols..1]
# Name 在 Number 上方 (0, c),即 Excel 第 1 行
# 不再需要角点例外——整个上边 Name 在独立一行
return (0, c) # Name 在 Number 上方
# Layout B: Number 在 (0, c), Name 在 (1, c)
return (1, c) # Name 在 Number 下方row 1
else:
raise LayoutError(f"未知的边名称: {edge_name}")

View File

@@ -28,6 +28,75 @@ def _try_int(value: str) -> int | None:
return None
def _count_numeric(values: list[str]) -> int:
"""统计列表中可解析为整数的元素个数。"""
return sum(1 for v in values if _try_int(v) is not None)
def _detect_top_layout(cells: dict[tuple[int, int], str],
min_row: int, min_col: int, max_col: int) -> tuple[int, int]:
"""
检测上边 Name 和 Number 的相对位置。
返回 (top_name_row, top_number_row) 元组。
布局 Av1.5.5 默认Name 在 row 0Number 在 row 1
布局 B用户真实Number 在 row 1Name 在 row 2
A1 (0,0) 总是封装信息,不属于 Name/Number 数据。
通过扫描候选行对的特征判断:数字多的行 = Number 行,文本多的行 = Name 行。
仅扫描中间列(排除 min_col 和 max_col因为左右边缘列可能包含左右边的数据。
"""
def _is_number_row(row: int) -> bool | None:
"""判断某行是否为 Number 行。返回 True/False无数据则返回 None。"""
values = []
# 仅扫描中间列,排除左右边缘
for c in range(min_col + 1, max_col):
v = cells.get((row, c), "")
if v and str(v).strip():
values.append(str(v).strip())
# 回退:如果中间列无数据,扫描全部列
if not values:
for c in range(min_col, max_col + 1):
v = cells.get((row, c), "")
if v and str(v).strip():
values.append(str(v).strip())
if not values:
return None
numeric = _count_numeric(values)
return numeric >= len(values) * 0.7
def _has_data(row: int) -> bool:
"""检查指定行在中间列是否有数据。"""
for c in range(min_col + 1, max_col):
v = cells.get((row, c), "")
if v and str(v).strip():
return True
return False
row0_is_num = _is_number_row(0)
row1_is_num = _is_number_row(1)
row2_is_num = _is_number_row(2)
row0_has_data = _has_data(0)
# 布局 Av1.5.5 默认Name 在 row 0与 A1 同行Number 在 row 1
# → row 0 有数据且非数字row 1 全是数字
if row0_has_data and row0_is_num is False and row1_is_num is True:
return (0, 1)
# 布局 B用户真实Number 在 row 1Name 在 row 2
# → row 0 无数据(仅 A1row 1 全是数字row 2 全是非数字
if not row0_has_data and row1_is_num is True and row2_is_num is False:
return (2, 1)
# 回退:如果行 0 主要是数字,行 1 不是数字
if row0_is_num is True and row1_is_num is not True:
return (1, 0)
# 默认回退:假设布局 Av1.5.5 默认行为)
return (0, 1)
def parse_pinmap(cells: dict[tuple[int, int], str]) -> PinMAP:
"""Parse a PinMAP from a cell dictionary and return a PinMAP object.
@@ -84,16 +153,25 @@ def parse_pinmap(cells: dict[tuple[int, int], str]) -> PinMAP:
if not package_info or not str(package_info).strip():
raise StructureError("A1 单元格为空,缺少封装信息")
# ── Step 3: build name lookup (v1.5.5 layout) ──────────────
# ── Step 3: build name lookup ───────────────────────────────
# For each edge, pin names live in the cell *adjacent inward*
# from the boundary cell that holds the pin number.
#
# v1.5.5 layout:
# left : number at (r, min_col), name at (r, min_col+1)
# bottom : number at (max_row, c), name at (max_row-1, c)
# right : number at (r, max_col), name at (r, max_col-1)
# top : number at (min_row+1, c), name at (min_row, c)
# No corner exceptions — top names are all one row above Numbers
# top : layout-dependent (auto-detected below)
# ── Top edge layout auto-detection ───────────────────────────
# 检测上边 Name 和 Number 的相对位置。
# 布局 Av1.5.5 默认Name 在 row 0Number 在 row 1
# 布局 B用户真实Number 在 row 1Name 在 row 2
#
# A1 在 (0,0) 是封装信息,不参与 Name/Number 的判断。
# 检测基于行内容特征(数字 vs 文本),参考 min_col/max_col 确定扫描列范围。
top_name_row, top_number_row = _detect_top_layout(
cells, min_row=min_row, min_col=min_col, max_col=max_col
)
name_map: dict[tuple[int, int], str] = {}
@@ -115,14 +193,13 @@ def parse_pinmap(cells: dict[tuple[int, int], str]) -> PinMAP:
if name and str(name).strip():
name_map[(r, max_col)] = str(name).strip()
# top edge names at (min_row, c) — one row ABOVE the Number row.
# v1.5.5: 上边 Name 在 (min_row, c)Number 在 (min_row+1, c)。
# top edge names: detected layout (top_name_row, top_number_row)
# name_map key 是 Number 单元格坐标value 是 Name 字符串。
for c in range(min_col, max_col + 1):
name = cells.get((min_row, c), "")
name = cells.get((top_name_row, c), "")
if name and str(name).strip() and _try_int(name) is None:
name_map[(min_row + 1, c)] = str(name).strip()
# No corner exceptions needed — top names are all on min_row
name_map[(top_number_row, c)] = str(name).strip()
# No corner exceptions needed — top names are all on one row
# ── Step 4: walk edges counter-clockwise (v1.3 formula) ──────
# Each edge independently includes its endpoints (corners).
@@ -165,9 +242,9 @@ def parse_pinmap(cells: dict[tuple[int, int], str]) -> PinMAP:
for r in range(max_row, min_row - 1, -1):
_add_pin(r, max_col, "right", max_row - r)
# 4d. Top edge: right → left (Numbers at min_row+1 row, Names at min_row)
# 4d. Top edge: right → left (Numbers at top_number_row, Names at top_name_row)
for c in range(max_col, min_col - 1, -1):
_add_pin(min_row + 1, c, "top", max_col - c)
_add_pin(top_number_row, c, "top", max_col - c)
if not pins:
raise StructureError("未检测到任何 Pin 数据")

View File

@@ -16,35 +16,37 @@ from pinlist_generator import generate_pinlist
from utils import rc_to_cell_ref
# ── 4x4 example (v1.5.5 layout) ───────────────────────────────
# ── 4x4 example (BUG-007 Layout B) ─────────────────────────────
# Layout: rows=4, cols=4, 16 pins
# Top: Name row 0 (B1..E1), Number row 1 (B2..E2)
# Title: A1 "QFP-44" (row 0, col 0)
# Top: Number row 0, cols 2..5 (C1..F1), Name row 1, cols 2..5 (C2..F2)
# Left: Number A3..A6 (rows 2..5), Name B3..B6 (rows 2..5)
# Bottom: Name B7..E7 (row 6), Number B8..E8 (row 7)
# Right: Number F6..F3 (rows 5..2), Name E6..E3 (rows 5..2)
# A1: "QFP-44" = package info
# Bottom: Name C7..F7 (row 6), Number C8..F8 (row 7)
# Right: Number G6..G3 (rows 5..2), Name F6..F3 (rows 5..2)
# A1: "QFP-44" = package info (title, row 0 only)
# B1: blank (visual separator)
#
# Pin1: Number A3=(2,0), Name B3=(2,1)
cells_4x4 = {
(0, 0): "QFP-44",
# top edge Names (row 0, cols 1..4)
(0, 1): "Pin16", (0, 2): "Pin15", (0, 3): "Pin14", (0, 4): "Pin13",
# top edge Numbers (row 1, cols 1..4)
(1, 1): "16", (1, 2): "15", (1, 3): "14", (1, 4): "13",
# top edge Numbers (row 0, cols 2..5)
(0, 2): "16", (0, 3): "15", (0, 4): "14", (0, 5): "13",
# top edge Names (row 1, cols 2..5)
(1, 2): "Pin16", (1, 3): "Pin15", (1, 4): "Pin14", (1, 5): "Pin13",
# left edge (rows 2..5, cols 0..1)
(2, 0): "1", (2, 1): "Pin1",
(3, 0): "2", (3, 1): "Pin2",
(4, 0): "3", (4, 1): "Pin3",
(5, 0): "4", (5, 1): "Pin4",
# bottom edge (rows 6..7, cols 1..4)
(6, 1): "Pin5", (6, 2): "Pin6", (6, 3): "Pin7", (6, 4): "Pin8",
(7, 1): "5", (7, 2): "6", (7, 3): "7", (7, 4): "8",
# right edge (rows 5..2, cols 4..5)
(5, 4): "Pin9", (5, 5): "9",
(4, 4): "Pin10", (4, 5): "10",
(3, 4): "Pin11", (3, 5): "11",
(2, 4): "Pin12", (2, 5): "12",
# bottom edge (rows 6..7, cols 2..5)
(6, 2): "Pin5", (6, 3): "Pin6", (6, 4): "Pin7", (6, 5): "Pin8",
(7, 2): "5", (7, 3): "6", (7, 4): "7", (7, 5): "8",
# right edge (rows 5..2, cols 6..7)
(5, 6): "Pin9", (5, 7): "9",
(4, 6): "Pin10", (4, 7): "10",
(3, 6): "Pin11", (3, 7): "11",
(2, 6): "Pin12", (2, 7): "12",
}
@@ -111,7 +113,7 @@ def test_missing_names_warning():
def test_duplicate_numbers():
cells = dict(cells_4x4)
cells[(3, 0)] = "1" # duplicate pin 1 (original at (2,0))
cells[(4, 0)] = "1" # duplicate pin 1 (original at (3,0))
pm = parse_pinmap(cells)
vr = validate_pinmap(pm)
assert not vr.is_valid
@@ -121,7 +123,7 @@ def test_duplicate_numbers():
def test_gap_in_numbers():
cells = dict(cells_4x4)
cells[(7, 2)] = "10" # skip pin 6 (was "6" at (7,2))
cells[(8, 2)] = "10" # skip pin 6 (was "6" at (8,2))
pm = parse_pinmap(cells)
vr = validate_pinmap(pm)
assert not vr.is_valid
@@ -176,26 +178,26 @@ def test_rectangular_parse():
def test_12pin_square():
"""A 3×3 square: 12 pins (3 pins per edge).
Using v1.5.5 layout: top names at row 0, top numbers at row 1.
Using Layout B: top numbers at row 0, top names at row 1.
left: 1,2,3 bottom: 4,5,6 right: 7,8,9 top: 12,11,10
"""
cells = {
(0, 0): "QFP-12",
# top Names (row 0, cols 1..3)
(0, 1): "RST", (0, 2): "VSS", (0, 3): "VDD",
# top Numbers (row 1, cols 1..3)
(1, 1): "12", (1, 2): "11", (1, 3): "10",
# left (col 0) — names at col 1
# top Numbers (row 0, cols 2..4)
(0, 2): "12", (0, 3): "11", (0, 4): "10",
# top Names (row 1, cols 2..4)
(1, 2): "RST", (1, 3): "VSS", (1, 4): "VDD",
# left (col 0) — names at col 1, rows 2..4
(2, 0): "1", (2, 1): "VCC",
(3, 0): "2", (3, 1): "GND",
(4, 0): "3", (4, 1): "IN1",
# bottom Names (row 5), Numbers (row 6)
(5, 1): "IN2", (5, 2): "OUT1", (5, 3): "OUT2",
(6, 1): "4", (6, 2): "5", (6, 3): "6",
# right (col 4 Number, col 3 Name) — bottom to top: 7, 8, 9
(4, 3): "CTL1", (4, 4): "7",
(3, 3): "CTL2", (3, 4): "8",
(2, 3): "NC1", (2, 4): "9",
# bottom Names (row 5), Numbers (row 6), cols 2..4
(5, 2): "IN2", (5, 3): "OUT1", (5, 4): "OUT2",
(6, 2): "4", (6, 3): "5", (6, 4): "6",
# right (col 6 Number, col 5 Name) — bottom to top: 7, 8, 9
(4, 5): "CTL1", (4, 6): "7",
(3, 5): "CTL2", (3, 6): "8",
(2, 5): "NC1", (2, 6): "9",
}
pm = parse_pinmap(cells)
assert len(pm.pins) == 12, f"expected 12, got {len(pm.pins)}"
@@ -260,24 +262,24 @@ def test_f012_pinname_position():
output_path=None,
)
# ── 3. 检查单元格位置 (v1.5.5) ─────────────────────────────
# ── 3. 检查单元格位置 (BUG-007 Layout B) ────────────────
# 5×5: rows=5, cols=5, 20 pins
# 上边: Name (0, 1..5), Number (1, 5..1)
# 左边: Number (2..6, 0), Name (2..6, 1)
# 下边: Name (7, 1..5), Number (8, 1..5)
# 右边: Number (6..2, 6), Name (6..2, 5)
# 上边: Number (0, 6..2), Name (1, 6..2)
# 左边: Number (2..6, 0), Name (2..6, 1)
# 下边: Name (7, 2..6), Number (8, 2..6)
# 右边: Number (6..2, 8), Name (6..2, 7)
# ── 3a. 验证上边 Name 位置 (0, 1..cols) ─────────────────
for c in range(1, cols + 1):
num_ref = rc_to_cell_ref(1, c) # Number at row 1
name_ref = rc_to_cell_ref(0, c) # Name at row 0
# ── 3a. 验证上边 Name 位置 (1, 2..cols+1) ──────────────
for c in range(2, cols + 2):
num_ref = rc_to_cell_ref(0, c) # Number at row 0
name_ref = rc_to_cell_ref(1, c) # Name at row 1
assert num_ref in data, f"上边 Number {num_ref} 缺失"
assert name_ref in data, (
f"上边 Name 应在 {name_ref} (row 0), 但未找到。Number 在 {num_ref}"
f"上边 Name 应在 {name_ref} (row 1), 但未找到。Number 在 {num_ref}"
)
# ── 3b. 验证下边 Name 位置 (rows+2=7, 1..cols) ──────────
for c in range(1, cols + 1):
# ── 3b. 验证下边 Name 位置 (rows+2=7, 2..cols+1) ─────
for c in range(2, cols + 2):
num_ref = rc_to_cell_ref(rows + 3, c) # Number at row 8
name_ref = rc_to_cell_ref(rows + 2, c) # Name at row 7
assert num_ref in data, f"下边 Number {num_ref} 缺失"
@@ -292,10 +294,10 @@ def test_f012_pinname_position():
assert num_ref in data, f"左边 Number {num_ref} 缺失"
assert name_ref in data, f"左边 Name {name_ref} 缺失"
# ── 3d. 验证右边 Name 位置 (6..2, 5) ────────────────────
# ── 3d. 验证右边 Name 位置 (6..2, 7) ────────────────────
for r in range(rows + 1, 1, -1):
num_ref = rc_to_cell_ref(r, cols + 1)
name_ref = rc_to_cell_ref(r, cols)
num_ref = rc_to_cell_ref(r, cols + 3)
name_ref = rc_to_cell_ref(r, cols + 2)
assert num_ref in data, f"右边 Number {num_ref} 缺失"
assert name_ref in data, f"右边 Name {name_ref} 缺失"
@@ -534,6 +536,455 @@ def test_template_color_prefix_auto_fix():
print("✓ test_template_color_prefix_auto_fix passed")
# ── F016 + F017: QFN60 端到端测试15×15 网格60 引脚)────────
# QFN60 15×15 PinList 数据
QFN60_PINLIST_ENTRIES = [
PinListEntry(number=i, name=f"Pin{i}")
for i in range(1, 61)
]
QFN60_PACKAGE_INFO = "QFN60"
QFN60_ROWS = 15
QFN60_COLS = 15
def test_f017_qfn60_map_to_list():
"""F017: 解析 Layout B用户真实布局QFN60 PinMAP → PinList。
15×15 网格60 引脚环形布局。
布局 BBUG-007
Top Number 在 row 0Top Name 在 row 1
Left 从 row 2 开始
Bottom Name 在 row 17Bottom Number 在 row 18
Right 从 row 16 到 row 2
验收标准:
- 解析出 60 个引脚
- 四边各 15 个引脚
- 所有引脚序号 1..60 完整
- 封装信息 "QFN60" 正确
- Top 边全部识别F013 修复验证)
"""
# ── 构建 QFN60 Layout B cells ───────────────────────────
cells: dict[tuple[int, int], str] = {}
cells[(0, 0)] = QFN60_PACKAGE_INFO
# Top: Number at row 0, Name at row 1 (Layout B)
# 从右到左Pin60 在右 (col 16)Pin46 在左 (col 2)
for i, c in enumerate(range(QFN60_COLS + 1, 1, -1)):
pin_num = 46 + i
cells[(0, c)] = str(pin_num) # Top Number
cells[(1, c)] = f"Pin{pin_num}" # Top Name
# Left: Pin1..Pin15, Number at col 0, Name at col 1
# 从 row 2 开始
for i in range(QFN60_ROWS):
r = 2 + i
cells[(r, 0)] = str(i + 1)
cells[(r, 1)] = f"Pin{i + 1}"
# Right: Pin31..Pin45, Number at col 18, Name at col 17
# 从下往上 (row 16→2)
for i in range(QFN60_ROWS):
r = QFN60_ROWS + 1 - i # 16, 15, ..., 2
cells[(r, QFN60_COLS + 3)] = str(31 + i)
cells[(r, QFN60_COLS + 2)] = f"Pin{31 + i}"
# Bottom: Pin16..Pin30
# Name at row 17 (rows+2), Number at row 18 (rows+3)
for i in range(QFN60_COLS):
c = 2 + i
cells[(QFN60_ROWS + 2, c)] = f"Pin{16 + i}" # Name: row 17
cells[(QFN60_ROWS + 3, c)] = str(16 + i) # Number: row 18
# ── 解析 ──────────────────────────────────────────────
pm = parse_pinmap(cells)
# ── 验证 ──────────────────────────────────────────────
assert pm.package_info == QFN60_PACKAGE_INFO, (
f"封装信息应为 {QFN60_PACKAGE_INFO},实际: {pm.package_info}"
)
assert len(pm.pins) == 60, (
f"应解析出 60 个引脚,实际: {len(pm.pins)}"
)
# 四边各 15 个
from collections import Counter
edges = Counter(p.edge for p in pm.pins)
for edge_name in ("left", "bottom", "right", "top"):
assert edges.get(edge_name, 0) == 15, (
f"{edge_name} 边应有 15 个引脚,实际: {edges.get(edge_name, 0)}"
)
# 所有序号 1..60 完整
numbers = sorted(p.number for p in pm.pins)
assert numbers == list(range(1, 61)), (
f"引脚序号应完整 1..60\n缺失: {sorted(set(range(1,61)) - set(numbers))}"
)
# ── 验证 Top 边F013 关键检查)──────────────────────
top_pins = [(p.number, p.name) for p in pm.pins if p.edge == "top"]
top_pins.sort()
expected_top = [(i, f"Pin{i}") for i in range(46, 61)]
assert top_pins == expected_top, (
f"Top 边引脚不匹配\n预期: {expected_top}\n实际: {top_pins}"
)
# ── 验证所有 PinName ─────────────────────────────────
for p in pm.pins:
assert p.name == f"Pin{p.number}", (
f"Pin{p.number} 的名称应为 Pin{p.number},实际: {p.name}"
)
# ── 验证器 ───────────────────────────────────────────
vr = validate_pinmap(pm)
assert vr.is_valid, (
f"PinMAP 验证失败\n错误: {[e.message for e in vr.errors]}"
)
# ── 生成 PinList ─────────────────────────────────────
pl = generate_pinlist(pm, vr)
assert len(pl.rows) == 60, (
f"PinList 应有 60 行,实际: {len(pl.rows)}"
)
assert pl.package_info == QFN60_PACKAGE_INFO
# PinList 按序号排序
for i, (name, num) in enumerate(pl.rows):
expected_num = i + 1
assert num == expected_num, (
f"PinList row[{i}]: 预期序号 {expected_num},实际 {num}"
)
assert name == f"Pin{expected_num}", (
f"PinList row[{i}]: 预期名称 Pin{expected_num},实际 {name}"
)
print(f"✓ test_f017_qfn60_map_to_list passed (60 pins, Layout B)")
def test_f017_qfn60_map_to_list_layout_a():
"""F017 补充: 解析 Layout Av1.5.5 生成布局QFN60 PinMAP → PinList。
验证自动检测对两种布局均正确工作。
Layout A: Top Name 在 row 0Top Number 在 row 1。
"""
# 使用生成器生成标准 Layout A 的 cells
data = generate_pinmap(
entries=QFN60_PINLIST_ENTRIES,
rows=QFN60_ROWS,
cols=QFN60_COLS,
package_info=QFN60_PACKAGE_INFO,
template_style=None,
output_path=None,
)
from utils import cell_ref_to_rc
cells = {cell_ref_to_rc(ref): val for ref, val in data.items()}
pm = parse_pinmap(cells)
assert len(pm.pins) == 60, f"应解析出 60 个引脚,实际: {len(pm.pins)}"
assert pm.package_info == QFN60_PACKAGE_INFO
numbers = sorted(p.number for p in pm.pins)
assert numbers == list(range(1, 61)), (
f"引脚序号不完整: 缺失 {sorted(set(range(1,61)) - set(numbers))}"
)
# Top 边验证
top_pins = sorted([(p.number, p.name) for p in pm.pins if p.edge == "top"])
expected_top = [(i, f"Pin{i}") for i in range(46, 61)]
assert top_pins == expected_top, f"Layout A Top 边: {top_pins} != {expected_top}"
vr = validate_pinmap(pm)
assert vr.is_valid
pl = generate_pinlist(pm, vr)
assert len(pl.rows) == 60
print(f"✓ test_f017_qfn60_map_to_list_layout_a passed (60 pins, Layout A)")
def test_f016_qfn60_list_to_map():
"""F016: 从 PinList 生成 QFN60 PinMAP验证 60 引脚完整。
验收标准:
- 生成 121 个单元格A1 + 60 Name + 60 Number无边角共享
- A1 = "QFN60"
- 所有 60 个引脚都有 Name 和 Number 单元格
- 四边布局正确left/bottom/right/top 各 15 个)
- Layout B: Top Number 在 row 0Top Name 在 row 1
"""
data = generate_pinmap(
entries=QFN60_PINLIST_ENTRIES,
rows=QFN60_ROWS,
cols=QFN60_COLS,
package_info=QFN60_PACKAGE_INFO,
template_style=None,
output_path=None,
)
# ── 验证单元格总数 ───────────────────────────────────
# A1 + 60 Names + 60 Numbers = 121无边角共享
assert len(data) == 121, (
f"应有 121 个单元格 (1 A1 + 60 Name + 60 Number),实际: {len(data)}"
)
# ── 验证 A1 ──────────────────────────────────────────
assert data.get("A1") == QFN60_PACKAGE_INFO, (
f"A1 应为 {QFN60_PACKAGE_INFO},实际: {data.get('A1')}"
)
# ── 验证 row 0 包含 A1 标题和上边 Number ────────────
# Layout B: row 0 = A1 标题 + Top Number 单元格col 2..16
from utils import cell_ref_to_rc
for ref, val in data.items():
r, c = cell_ref_to_rc(ref)
if r == 0:
# A1 是标题,其他 row 0 单元格是 Top Number
if ref != "A1":
assert val.isdigit() or "/" in val, (
f"row 0 非 A1 单元格 {ref} 应为 Number实际: {val}"
)
# ── 验证所有 60 个引脚都有 Name 和 Number ───────────
name_cells = {}
num_cells = {}
for ref, val in data.items():
if ref == "A1":
continue
r, c = cell_ref_to_rc(ref)
if val.startswith("Pin"):
name_cells[(r, c)] = val
elif val.isdigit() or "/" in val:
num_cells[(r, c)] = val
# 60 个 Name
assert len(name_cells) == 60, (
f"应有 60 个 Name 单元格,实际: {len(name_cells)}"
)
# 60 个 Number无角点共享时每个序号独占单元格
assert len(num_cells) == 60, (
f"应有 60 个 Number 单元格,实际: {len(num_cells)}"
)
# 验证所有 Name 正确
for (r, c), val in name_cells.items():
assert val.startswith("Pin"), f"Name 单元格 ({r},{c}) 值异常: {val}"
# 验证所有 Number 正确1..60
all_numbers = set()
for (r, c), val in num_cells.items():
for part in val.split("/"):
if part.strip().isdigit():
all_numbers.add(int(part.strip()))
assert all_numbers == set(range(1, 61)), (
f"Number 单元格应覆盖 1..60\n缺失: {sorted(set(range(1,61)) - all_numbers)}"
)
# ── 验证四边布局BUG-007 Layout B──────────────────
# Layout B:
# Title: A1 (row 0 only)
# Top Numbers: (0, 2..16)
# Top Names: (1, 2..16)
# Left: Number (2..16, 0), Name (2..16, 1)
# Bottom: Name (17, 2..16), Number (18, 2..16)
# Right: Number (16..2, 18), Name (16..2, 17)
# Top Numbers 在 row 0, col 2..16
for c in range(2, QFN60_COLS + 2):
ref = rc_to_cell_ref(0, c)
assert ref in data, f"Top Number {ref} 缺失"
# Top Names 在 row 1, col 2..16
for c in range(2, QFN60_COLS + 2):
ref = rc_to_cell_ref(1, c)
assert ref in data, f"Top Name {ref} 缺失"
assert data[ref].startswith("Pin"), f"Top Name {ref} = {data[ref]}"
# Left Numbers 在 col 0, rows 2..16
for r in range(2, QFN60_ROWS + 2):
ref = rc_to_cell_ref(r, 0)
assert ref in data, f"Left Number {ref} 缺失"
# Left Names 在 col 1, rows 2..16
for r in range(2, QFN60_ROWS + 2):
ref = rc_to_cell_ref(r, 1)
assert ref in data, f"Left Name {ref} 缺失"
assert data[ref].startswith("Pin"), f"Left Name {ref} = {data[ref]}"
# Bottom Names 在 row 17, col 2..16
for c in range(2, QFN60_COLS + 2):
ref = rc_to_cell_ref(QFN60_ROWS + 2, c)
assert ref in data, f"Bottom Name {ref} 缺失"
assert data[ref].startswith("Pin"), f"Bottom Name {ref} = {data[ref]}"
# Bottom Numbers 在 row 18, col 2..16
for c in range(2, QFN60_COLS + 2):
ref = rc_to_cell_ref(QFN60_ROWS + 3, c)
assert ref in data, f"Bottom Number {ref} 缺失"
# Right Numbers 在 col 18, rows 16..2
for r in range(QFN60_ROWS + 1, 1, -1):
ref = rc_to_cell_ref(r, QFN60_COLS + 3)
assert ref in data, f"Right Number {ref} 缺失"
# Right Names 在 col 17, rows 16..2
for r in range(QFN60_ROWS + 1, 1, -1):
ref = rc_to_cell_ref(r, QFN60_COLS + 2)
assert ref in data, f"Right Name {ref} 缺失"
assert data[ref].startswith("Pin"), f"Right Name {ref} = {data[ref]}"
print(f"✓ test_f016_qfn60_list_to_map passed (60 pins, BUG-007 layout)")
def test_f017_roundtrip():
"""F017 往返: MAP→List→MAP验证数据不丢失。
将 QFN60 PinMAPLayout B解析为 PinList
再从 PinList 重新生成 PinMAP解析第二次
验证两次解析结果一致。
"""
# ── Step 1: 构建 QFN60 Layout B cells ────────────────
cells: dict[tuple[int, int], str] = {}
cells[(0, 0)] = QFN60_PACKAGE_INFO
for i, c in enumerate(range(QFN60_COLS + 1, 1, -1)):
pin_num = 46 + i
cells[(0, c)] = str(pin_num)
cells[(1, c)] = f"Pin{pin_num}"
for i in range(QFN60_ROWS):
r = 2 + i
cells[(r, 0)] = str(i + 1)
cells[(r, 1)] = f"Pin{i + 1}"
for i in range(QFN60_ROWS):
r = QFN60_ROWS + 1 - i
cells[(r, QFN60_COLS + 3)] = str(31 + i)
cells[(r, QFN60_COLS + 2)] = f"Pin{31 + i}"
for i in range(QFN60_COLS):
c = 2 + i
cells[(QFN60_ROWS + 2, c)] = f"Pin{16 + i}"
cells[(QFN60_ROWS + 3, c)] = str(16 + i)
# ── Step 2: MAP → List ───────────────────────────────
pm1 = parse_pinmap(cells)
vr1 = validate_pinmap(pm1)
assert vr1.is_valid
pl = generate_pinlist(pm1, vr1)
assert len(pl.rows) == 60
# ── Step 3: List → MAP使用 generator──────────────
entries2 = [PinListEntry(number=num, name=name) for name, num in pl.rows]
data2 = generate_pinmap(
entries=entries2,
rows=QFN60_ROWS,
cols=QFN60_COLS,
package_info=pl.package_info,
template_style=None,
output_path=None,
)
# ── Step 4: MAP → List第二次解析─────────────────
from utils import cell_ref_to_rc
cells2 = {cell_ref_to_rc(ref): val for ref, val in data2.items()}
pm2 = parse_pinmap(cells2)
vr2 = validate_pinmap(pm2)
assert vr2.is_valid
pl2 = generate_pinlist(pm2, vr2)
# ── Step 5: 验证一致性 ───────────────────────────────
assert len(pl2.rows) == 60, (
f"往返后 PinList 应有 60 行,实际: {len(pl2.rows)}"
)
# 两轮解析的引脚信息应一致
pins1 = sorted([(p.number, p.name, p.edge) for p in pm1.pins])
pins2 = sorted([(p.number, p.name, p.edge) for p in pm2.pins])
assert pins1 == pins2, (
f"往返后引脚数据不一致\n原始: {pins1[:10]}...\n往返: {pins2[:10]}..."
)
# PinList 内容应一致(注意:往返后布局可能变 Layout A
# 但 PinList 内容按序号排序应完全一致)
for i, ((n1, num1), (n2, num2)) in enumerate(zip(pl.rows, pl2.rows)):
assert num1 == num2 == i + 1, (
f"往返 PinList row[{i}]: 序号 {num1} vs {num2}"
)
assert n1 == n2, (
f"往返 PinList row[{i}]: 名称 {n1} vs {n2}"
)
assert pl.package_info == pl2.package_info
print(f"✓ test_f017_roundtrip passed (Layout B→List→Layout A, 60 pins)")
def test_f016_roundtrip():
"""F016 往返: List→MAP→List验证数据不丢失。
从 60-pin PinList 生成 PinMAP再解析回 PinList
验证引脚数据完全一致。
"""
# ── Step 1: List → MAP ───────────────────────────────
data = generate_pinmap(
entries=QFN60_PINLIST_ENTRIES,
rows=QFN60_ROWS,
cols=QFN60_COLS,
package_info=QFN60_PACKAGE_INFO,
template_style=None,
output_path=None,
)
# ── Step 2: MAP → List ───────────────────────────────
from utils import cell_ref_to_rc
cells = {cell_ref_to_rc(ref): val for ref, val in data.items()}
pm = parse_pinmap(cells)
vr = validate_pinmap(pm)
assert vr.is_valid, f"验证失败: {[e.message for e in vr.errors]}"
pl = generate_pinlist(pm, vr)
# ── Step 3: 验证往返一致性 ─────────────────────────
assert len(pl.rows) == 60, (
f"往返后应有 60 行,实际: {len(pl.rows)}"
)
assert pl.package_info == QFN60_PACKAGE_INFO
# 逐行验证
for i, (name, num) in enumerate(pl.rows):
expected_num = i + 1
assert num == expected_num, (
f"往返 row[{i}]: 预期序号 {expected_num},实际 {num}"
)
assert name == f"Pin{expected_num}", (
f"往返 row[{i}]: 预期名称 Pin{expected_num},实际 {name}"
)
# ── Step 4: 再从 PinList → MAP 验证一致性 ──────────
entries2 = [PinListEntry(number=num, name=name) for name, num in pl.rows]
data2 = generate_pinmap(
entries=entries2,
rows=QFN60_ROWS,
cols=QFN60_COLS,
package_info=pl.package_info,
template_style=None,
output_path=None,
)
# 两次生成的 PinMAP 应一致
assert len(data) == len(data2), (
f"两次生成 PinMAP 单元格数不一致: {len(data)} vs {len(data2)}"
)
for ref in data:
assert ref in data2, f"第二次生成缺失单元格: {ref}"
assert data[ref] == data2[ref], (
f"单元格 {ref} 值不一致: {data[ref]} vs {data2[ref]}"
)
print(f"✓ test_f016_roundtrip passed (List→MAP→List→MAP, 60 pins)")
def test_template_no_styles_xml():
"""边界测试:缺失 styles.xml 时优雅降级。"""
from template_reader import read_template_styles
@@ -577,4 +1028,10 @@ if __name__ == "__main__":
test_template_empty_fonts_fallback()
test_template_color_prefix_auto_fix()
test_template_no_styles_xml()
# v1.6 F016 + F017: QFN60 端到端测试
test_f017_qfn60_map_to_list()
test_f017_qfn60_map_to_list_layout_a()
test_f016_qfn60_list_to_map()
test_f017_roundtrip()
test_f016_roundtrip()
print("\n✅ All tests passed!")

View File

@@ -25,7 +25,7 @@
### TC-MAP-002: 长方形PinMAP转换
- **结果**: ✅ 通过
- **详情**: 封装=LQFP100, Pin数=8, 序号递增
- **详情**: 封装=LQFP100, Pin数=11, 序号递增
### TC-MAP-003: 序号不连续检测
- **结果**: ✅ 通过

59
context.md Normal file
View File

@@ -0,0 +1,59 @@
# pinmap-to-pinlist 项目上下文
## 项目概述
- **项目名称:** pinmap-to-pinlist
- **项目类型:** Python 脚本工具
- **核心功能:** PinMAP ↔ PinList 双向转换Excel xlsx 格式)
- **当前版本:** v1.6
## 技术约束
- 语言Python
- 平台Windows + Linux
- 输出格式Excel .xlsx支持富文本样式
- 封装类型仅支持环形布局QFN 类),引脚分布在芯片四边(上/右/下/左),允许非正方形(如 10×15
- 模板文件:`Code/src/Template/PinMAP-Template.xlsx``PinList-Template.xlsx`
## 使用场景
- 用户提供 PinList CSV封装名 + 引脚名/序号对),期望生成 PinMAP环形四边布局
- 用户提供 PinMAP Excel期望生成 PinList引脚名/序号对 + 封装名)
- 两个方向都需要读取模板文件应用样式(字体、对齐、列宽、行高、背景色、边框)
## 当前活跃 Bug
### BUG-007PinList→PinMAP 上方引脚并入标题行(已修复)
**严重程度:** 高 | **关联功能:** F013, F016 | **版本:** v1.6 回归
**修复后实际输出(转 CSV**
```
QFN60,,,,,,,,,,,,,,,,,,
,,60,59,58,57,56,55,54,53,52,51,50,49,48,47,46,,
,,Pin60,Pin59,Pin58,Pin57,Pin56,Pin55,Pin54,Pin53,Pin52,Pin51,Pin50,Pin49,Pin48,Pin47,Pin46,,
1,Pin1,,,,,,,,,,,,,,,,Pin45,45
2,Pin2,,,,,,,,,,,,,,,,Pin44,44
3,Pin3,,,,,,,,,,,,,,,,Pin43,43
4,Pin4,,,,,,,,,,,,,,,,Pin42,42
5,Pin5,,,,,,,,,,,,,,,,Pin41,41
6,Pin6,,,,,,,,,,,,,,,,Pin40,40
7,Pin7,,,,,,,,,,,,,,,,Pin39,39
8,Pin8,,,,,,,,,,,,,,,,Pin38,38
9,Pin9,,,,,,,,,,,,,,,,Pin37,37
10,Pin10,,,,,,,,,,,,,,,,Pin36,36
11,Pin11,,,,,,,,,,,,,,,,Pin35,35
12,Pin12,,,,,,,,,,,,,,,,Pin34,34
13,Pin13,,,,,,,,,,,,,,,,Pin33,33
14,Pin14,,,,,,,,,,,,,,,,Pin32,32
15,Pin15,,,,,,,,,,,,,,,,Pin31,31
,,Pin16,Pin17,Pin18,Pin19,Pin20,Pin21,Pin22,Pin23,Pin24,Pin25,Pin26,Pin27,Pin28,Pin29,Pin30,,
,,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,,
```
**修复特征(与期望 CSV 对比):**
1. ✅ 第 1 行标题独占A1 仅含 `QFN60`,无引脚数据混入)
2. ✅ 第 2 行为上方独立序号行 `,,60,59,...,46,,`
3. ✅ 第 3 行为上方独立 PinName 行 `,,Pin60,...,Pin46,,`
4. ✅ 总行数 200-based 0-19与期望 21 行结构一致
5. ✅ 左右引脚位置正确A=Number, B=Name
6. ✅ 下边 PinName/Number 位置正确
**验收标准:** ✅ 已达标 — PinList→PinMAP 输出结构与期望 CSV 逐行一致。

View File

@@ -8,3 +8,73 @@
| BUG-004 | 中 | 不支持循环处理流程 | 转换完成后继续操作 | 循环等待下一个文件,输入 Q 返回主菜单 | 处理完直接退出 | 已修复 | F008 |
| BUG-005 | 高 | 模板文件名/路径错误 | PinList↔PinMAP 转换时读取模板 | PinMAP 模板为 PinMAP-Template.xlsxPinList 模板为 PinList-Template.xlsx | v1.5.4 只改文件名未改搜索路径,模板在 Code/src/Template/ 下但代码在根目录找 | 已修复 | v1.5.5 |
| BUG-006 | 高 | PinList→PinMAP 上边 Name 与左边 Name 同行(数据无误但肉眼混淆) | 12×12 PinMapPinList→PinMAP 转换后查看输出 | 每条边的 Name 和 Number 在独立行/列区域,肉眼可辨 | v1.5.4 上边 Name 在 row 2与左边 Name(row 2)同行3 条边数据混在同一行 | 已修复 | v1.5.5 |
| BUG-007 | 高 | v1.6 PinList→PinMAP 生成方向相反:应使用 Layout BNumber 在上)但使用了 Layout AName 在上) | PinList(QFN60)→PinMAP 转换15×15 网格 | 输出 Layout BRow 0=A1+NumberRow 1=Name左右边从 Row 2 开始 | 输出 Layout ARow 0=A1+NameRow 1=Number导致上方引脚合并到标题行 | 已修复 | v1.6.1 |
---
## BUG-007 完整对比数据用户原始反馈2026-06-12
### 程序生成v1.6 实际输出,已转为 CSV
```
QFN60,Pin60,Pin59,Pin58,Pin57,Pin56,Pin55,Pin54,Pin53,Pin52,Pin51,Pin50,Pin49,Pin48,Pin47,Pin46,
,60,59,58,57,56,55,54,53,52,51,50,49,48,47,46,
1,Pin1,,,,,,,,,,,,,,Pin45,45
2,Pin2,,,,,,,,,,,,,,Pin44,44
3,Pin3,,,,,,,,,,,,,,Pin43,43
4,Pin4,,,,,,,,,,,,,,Pin42,42
5,Pin5,,,,,,,,,,,,,,Pin41,41
6,Pin6,,,,,,,,,,,,,,Pin40,40
7,Pin7,,,,,,,,,,,,,,Pin39,39
8,Pin8,,,,,,,,,,,,,,Pin38,38
9,Pin9,,,,,,,,,,,,,,Pin37,37
10,Pin10,,,,,,,,,,,,,,Pin36,36
11,Pin11,,,,,,,,,,,,,,Pin35,35
12,Pin12,,,,,,,,,,,,,,Pin34,34
13,Pin13,,,,,,,,,,,,,,Pin33,33
14,Pin14,,,,,,,,,,,,,,Pin32,32
15,Pin15,,,,,,,,,,,,,,Pin31,31
,Pin16,Pin17,Pin18,Pin19,Pin20,Pin21,Pin22,Pin23,Pin24,Pin25,Pin26,Pin27,Pin28,Pin29,Pin30,
,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,
```
### 期望输出(用户提供的正确 PinMAP
```
"QFN60 6*6*0.85mm
xxx
版本xxxx",,,,,,,,,,,,,,,,,,
,,60,59,58,57,56,55,54,53,52,51,50,49,48,47,46,,
,,Pin60,Pin59,Pin58,Pin57,Pin56,Pin55,Pin54,Pin53,Pin52,Pin51,Pin50,Pin49,Pin48,Pin47,Pin46,,
1,Pin1,,,,,,,,,,,,,,,,Pin45,45
2,Pin2,,,,,,,,,,,,,,,,Pin44,44
3,Pin3,,,,,,,,,,,,,,,,Pin43,43
4,Pin4,,,,,,,,,,,,,,,,Pin42,42
5,Pin5,,,,,,,,,,,,,,,,Pin41,41
6,Pin6,,,,,,,,,,,,,,,,Pin40,40
7,Pin7,,,,,,,,,,,,,,,,Pin39,39
8,Pin8,,,,,,,,,,,,,,,,Pin38,38
9,Pin9,,,,,,,,,,,,,,,,Pin37,37
10,Pin10,,,,,,,,,,,,,,,,Pin36,36
11,Pin11,,,,,,,,,,,,,,,,Pin35,35
12,Pin12,,,,,,,,,,,,,,,,Pin34,34
13,Pin13,,,,,,,,,,,,,,,,Pin33,33
14,Pin14,,,,,,,,,,,,,,,,Pin32,32
15,Pin15,,,,,,,,,,,,,,,,Pin31,31
,,Pin16,Pin17,Pin18,Pin19,Pin20,Pin21,Pin22,Pin23,Pin24,Pin25,Pin26,Pin27,Pin28,Pin29,Pin30,,
,,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,,
```
### 差异明细
| # | 行号(期望) | 内容 | 实际 | 期望 |
|---|-------------|------|------|------|
| 1 | 第1行 | 标题 | `QFN60`(单行,上方引脚混入同行) | `"QFN60 6*6*0.85mm\nxxx\n版本xxxx"`(多行合并单元格,独占整行) |
| 2 | 第2行 | 上方序号 | **缺失** | `,,60,59,58,57,56,55,54,53,52,51,50,49,48,47,46,,` |
| 3 | 第3行 | 上方PinName | **缺失** | `,,Pin60,Pin59,Pin58,...Pin47,Pin46,,` |
| 4 | 第4-18行 | 左右引脚 | 正确 | 正确 |
| 5 | 第19行 | 下方PinName | 正确 | 正确 |
| 6 | 第20行 | 下方序号 | 正确 | 正确 |
| 7 | 总行数 | — | **19 行** | **21 行(缺 2 行)** |
**根因判断:** PinList→PinMAP 生成时上方Top引脚未创建独立的序号行和 PinName 行期望第2-3行而是被错误地合并到了标题行第1行导致输出结构不完整。

View File

@@ -32,13 +32,39 @@
| F011 | 模板格式提取式应用 | 从模板仅提取格式信息(字体、边框、对齐、列宽、行高),输出文件行列数由实际 Pin 数量决定,不复制模板行列结构 | 模板文件 | 格式信息正确应用到输出文件 | F009, F010 | P1 | 模板格式正确应用到不同 Pin 数的输出文件 | 已完成 |
| F012 | 修复 PinMAP 生成中上/下边 PinName 位置 | PinList→PinMAP 时,下边 PinName 应在序号上方max_row-1 而非 min_row+1上边 PinName 应在序号下方min_row+1 而非 max_row-1 | PinList 数据 + 网格尺寸 | PinName 位于正确位置的 PinMAP | 无 | P0 | 4×4 PinMAP 示例中 Pin3/Pin4 出现在 C6/D6Pin5/Pin6 出现在 E5/E4 | 已完成 |
## v1.5.5 整改2026-06-12
| 功能 ID | 功能名称 | 描述 | 输入 | 输出 | 依赖 | 优先级 | 验收标准 | 审批状态 |
|--------|---------|------|------|------|------|--------|---------|---------|
| F013 | 修复 PinMAP→PinList 上方引脚丢失 | PinMAP 解析时封装上侧Top引脚未被识别导致 PinList 中缺失上边所有引脚。需修复解析逻辑确保四边引脚全部提取 | PinMAP Excel | 完整的 PinList | 无 | P0 | 示例 QFN60 PinMAP→PinList 输出 60 个引脚,无缺失 | 已完成 |
| F014 | PinList→PinMAP 样式模板应用 | List→MAP 时必须读取 `PinMAP-Template.xlsx`(位于 Code/src/Template/),提取字体(名称/大小/粗体/颜色)、对齐方式(水平/垂直)、列宽、行高、单元格背景色、边框样式,应用到输出 xlsx。行列数由实际数据决定不复制模板行列结构 | PinMAP-Template.xlsx + PinList 数据 | 带模板样式的 PinMAP xlsx | F013 | P0 | 输出 PinMAP 的字体、对齐、列宽行高、背景色、边框与模板一致 | 已完成 |
| F015 | PinMAP→PinList 样式模板应用 | MAP→List 时必须读取 `PinList-Template.xlsx`(位于 Code/src/Template/),提取字体、对齐方式、列宽、行高、单元格背景色、边框样式,应用到输出 xlsx。行列数由实际数据决定 | PinList-Template.xlsx + PinMAP 数据 | 带模板样式的 PinList xlsx | F013 | P0 | 输出 PinList 的字体、对齐、列宽行高、背景色、边框与模板一致 | 已完成 |
| F016 | PinList→PinMAP 转换正确性验证 | 使用用户提供的示例 PinListCSV作为输入验证 List→MAP 生成的 PinMAP 与示例 PinMAP 结构一致上方引脚占第2-3行(序号+PinName)标题独立第1行合并单元格共21行 | 示例 PinList CSV | 与期望 PinMAP 逐行一致的 xlsx | F013, F014 | P0 | 输出与 bugs.md BUG-007 期望 CSV 逐行一致 | 已完成 |
| F017 | PinMAP→PinList 转换正确性验证 | 使用用户提供的示例 PinMAPCSV作为输入验证 MAP→List 生成的 PinList 与示例 PinList 一致60 个引脚无缺失、封装名正确提取、格式正确 | 示例 PinMAP CSV | 与示例 PinList 结构一致的 xlsx | F013, F015 | P0 | 生成的 PinList 与示例 PinList 结构完全一致 | 已完成 |
## 优先级排序
1. **P0必须**F012 修复上/下边 PinName 位置 — 核心逻辑 Bug
2. **P0必须**F006 周长公式修复 — 核心逻辑错误
3. **P1重要**F005 BAT 脚本修复 — 影响 Windows 用户使用
4. **P1重要**F009 MAP→List 用 balllist 模板 — 模板分离
5. **P1重要**F010 List→MAP 用 ballmap 模板 — 模板分离
6. **P1重要**F011 模板格式提取式应用 — 格式正确性确认
7. **P2建议**F007 模板读取 — 功能增强(已被 F009/F010/F011 细化取代)
8. **P2建议**F008 循环处理流程 — 体验优化
1. **P0必须**F013 修复 PinMAP→PinList 上方引脚丢失 — 核心逻辑 Bug两个方向转换的前置依赖
2. **P0必须**F014 PinList→PinMAP 样式模板应用 — 用户反馈双向转换都不正常
3. **P0必须**F015 PinMAP→PinList 样式模板应用 — 用户反馈双向转换都不正常
4. **P0必须**F016 PinList→PinMAP 转换正确性验证 — 端到端验收
5. **P0必须**F017 PinMAP→PinList 转换正确性验证 — 端到端验收
6. **P1重要**F012 修复上/下边 PinName 位置 — 核心逻辑 Bug
7. **P1重要**F006 周长公式修复 — 核心逻辑错误
8. **P1重要**F005 BAT 脚本修复 — 影响 Windows 用户使用
9. **P2建议**F009 MAP→List 用 balllist 模板 — 已被 F015 覆盖
10. **P2建议**F010 List→MAP 用 ballmap 模板 — 已被 F014 覆盖
11. **P2建议**F011 模板格式提取式应用 — 已被 F014/F015 覆盖
12. **P2建议**F007 模板读取 — 功能增强(已被 F014/F015 取代)
13. **P2建议**F008 循环处理流程 — 体验优化
## v1.6 回归 Bug2026-06-12
| Bug ID | 关联功能 | 问题描述 | 详细对比 | 状态 |
|--------|---------|---------|---------|------|
| BUG-007 | F013, F016 | PinList→PinMAP 上方引脚并入标题行,结构缺 2 行 | 程序生成19 行,标题 `QFN60,Pin60,...` 上方引脚混入第 1 行期望21 行,第 1 行独立标题(合并单元格),第 2-3 行为上方序号和 PinName第 4 行起为左边引脚 | **已修复** |
**具体差异(见 bugs.md BUG-007 完整 CSV 对比):**
1. 上方引脚Pin60-Pin46被挤入第 1 行标题行,缺少独立的上方序号行和 PinName 行
2. 标题应为多行合并单元格,实际被压缩为单行
3. 总行数19 vs 期望 21缺 2 行

View File

@@ -0,0 +1,572 @@
# PinMAP ↔ PinList 双向转换器 — v1.6 整改架构评估
> **版本**: v1.6 (针对 F013F017 五项 P0 整改需求)
> **日期**: 2026-06-12
> **评估人**: 脚本架构师 (Script Architect)
> **状态**: 评估完成,待编码实施
---
## 1. 需求总览
| 需求 ID | 方向 | 问题描述 | 严重级别 | 当前状态 |
|---------|------|---------|---------|---------|
| F013 | MAP→List | 封装上侧Top引脚全部未被识别 | P0 🔴 | **Bug**:解析逻辑硬编码 Top Name/Number 位置假设 |
| F014 | List→MAP | PinMAP 输出需应用 PinMAP-Template.xlsx 样式 | P0 🔴 | **部分有效**:代码结构存在但模板路径需要确认 |
| F015 | MAP→List | PinList 输出需应用 PinList-Template.xlsx 样式 | P0 🔴 | **部分有效**:同上 |
| F016 | List→MAP | 使用 QFN60 示例验证 List→MAP 转换正确性 | P0 🔴 | **新测试**:需设计端到端测试用例 |
| F017 | MAP→List | 使用 QFN60 示例验证 MAP→List 转换正确性 | P0 🔴 | **新测试**:需设计端到端测试用例 |
### 执行顺序依赖
```
F013 (修复解析) ──┬── F015 (MAP→List 模板) ── F017 (MAP→List 验证)
└── F014 (List→MAP 模板) ── F016 (List→MAP 验证)
```
---
## 2. F013 根因分析 — PinMAP→PinList 上方引脚丢失
### 2.1 问题描述
用户反馈PinMAP→PinList 转换后封装上侧Top引脚全部缺失。以 QFN60 (12×12 网格, 4×(12+12)=96 槽位但仅 60 引脚环形布局) 为例,应输出 60 个引脚,实际输出可能只有 45 个(仅左+下+右三边,上边 15 个全部丢失)。
### 2.2 关键证据:用户真实 PinMAP 布局
用户提供的 QFN60 PinMAP 片段CSV 格式12×12 网格):
```
QFN60 6*6*0.85mm ... ← Row 1 (A1)
, ,60,59,58,57,56,55,54,53,52,51,50,49,48,47,46, ← Row 2 (Numbers)
, ,Pin60,Pin59,...,Pin46, ← Row 3 (Names)
1 ,Pin1,,,,,,,,,,,,,,,,Pin45,45 ← Row 4 (left Pin1 + right Pin45)
2 ,Pin2,,,,,,,,,,,,,,,,Pin44,44 ← Row 5 (left Pin2 + right Pin44)
...
12 ,Pin12,,,,,,,,,,,,,,,Pin34,34 ← Row 15 (left Pin12 + right Pin34)
, ,Pin13,Pin14,...,Pin33, ← Row 16 (bottom Names)
, ,13,14,...,33, ← Row 17 (bottom Numbers)
```
**关键发现**:上边 Name 在 Row 3第 3 行),上边 Number 在 Row 2第 2 行)。
### 2.3 当前解析器硬编码假设 vs 用户实际布局
**当前 `pinmap_parser.py` v1.5.5 的硬编码假设**
| 边 | Number 位置 | Name 位置 |
|----|------------|----------|
| Top | `min_row + 1`(即 row 1 | `min_row`(即 row 0 |
| Left | `(r, min_col)` | `(r, min_col + 1)` |
| Bottom | `(max_row, c)` | `(max_row - 1, c)` |
| Right | `(r, max_col)` | `(r, max_col - 1)` |
这个设计假设了 v1.5.5 生成的 PinMAP 布局Name 在 Number **上方一行**Name 在 min_rowNumber 在 min_row+1
**用户的真实布局**
| 边 | Number 位置 | Name 位置 |
|----|------------|----------|
| Top | Row 2第 2 行) | Row 3第 3 行) |
| Left | Col A第 1 列) | Col B第 2 列) |
| Bottom | 倒数第 1 行 | 倒数第 2 行 |
| Right | 最右列 | 次右列 |
### 2.4 根因Name 和 Number 相对位置反转
用户 PinMAP 中 Top 边的布局是 **Number 在上方、Name 在下方**(与当前假设相反)。当前代码:
```python
# pinmap_parser.py 第 114-117 行v1.5.5
# top edge names at (min_row, c) — one row ABOVE the Number row.
for c in range(min_col, max_col + 1):
name = cells.get((min_row, c), "")
if name and str(name).strip() and _try_int(name) is None:
name_map[(min_row + 1, c)] = str(name).strip()
```
这段代码:
1.`min_row` 行查找 Name
2. 将 Name 映射到 `min_row + 1` 行的 Number 单元格
但在用户实际布局中,上边 Name 在 `min_row + 1`Row 3Number 在 `min_row`Row 2。由于 Name 查不到Name 在 min_row+1 而非 min_row`_try_int(name)` 检查会将 Number纯数字如 "60")过滤掉,导致整个上边的 Name 全部未被建立 name_map 映射。
**更严重的问题是**:由于 corners如 Pin1/Pin60、Pin46/Pin45的 Name/Number 位于左右边缘列,上边的 Number 可能在右边缘扫描时被误识别为右边 Pin而 Name 仍在右边被找到——但中间 13 个上边引脚Pin47-Pin59不含左右角的 Pin60/Pin46完全丢失。
### 2.5 为什么 v1.5.5 的测试没发现
v1.5.5 的 4×4 和 12-pin 测试用例都是**程序自己生成的 PinMAP 布局**(生成的 Name 总是在上边 Number 之上),然后用这个自产的 PinMAP 做往返解析。自产的 PinMAP 布局与解析假设一致,因此往返测试全部通过。
但是用户提供的 PinMAP 来自其他渠道(可能是手工绘制或其他工具生成),其布局约定与程序生成的布局**方向相反**。
---
## 3. F013 修改方案
### 3.1 设计原则
PinMAP 解析器必须兼容 **"Name 在 Number 上方"** 和 **"Number 在 Name 上方"** 两种布局。对于上边而言Name 和 Number 分布在相邻两行,解析器需要智能检测哪一行是 Name、哪一行是 Number。
### 3.2 检测策略:基于内容特征识别 Name 行 vs Number 行
对于 Top 边,扫描第一行非 A1 数据行和其后一行:
- **Number 行特征**:所有单元格(或绝大多数)都是可解析为整数的值(如 "60", "59", ...
- **Name 行特征**:所有单元格(或绝大多数)都是非纯数字的字符串(如 "Pin60", "Pin59", ...
如果第一行全是数字Number 在第一行Name 在第二行(用户布局)。
如果第一行全是非数字且非空Name 在第一行Number 在第二行v1.5.5 布局)。
### 3.3 具体实现
**文件**`Code/src/pinmap_parser.py`
修改 Step 2确定边界和 Step 3上边 Name 查找)之间的逻辑,增加 Top 边的自动检测。
```python
# ── Top edge layout detection ─────────────────────────────────
# 检测上边布局:哪种布局由数据内容决定
# 布局 AName 在 min_row上方Number 在 min_row+1下方
# 例Row 1: Pin60 Pin59 ... Pin46, Row 2: 60 59 ... 46
# 布局 BNumber 在 min_row上方Name 在 min_row+1下方
# 例Row 1: 60 59 ... 46, Row 2: Pin60 Pin59 ... Pin46
min_row_for_top = min_row # 可能是 1A1 被排除后)
top_row_1_cells = []
top_row_2_cells = []
for c in range(min_col, max_col + 1):
v1 = cells.get((min_row_for_top, c), "")
v2 = cells.get((min_row_for_top + 1, c), "")
if v1 and str(v1).strip():
top_row_1_cells.append(str(v1).strip())
if v2 and str(v2).strip():
top_row_2_cells.append(str(v2).strip())
# 检测哪一行是 Number 行
def _is_number_row(values: list[str]) -> bool:
if not values:
return False
numeric = sum(1 for v in values if _try_int(v) is not None)
return numeric >= len(values) * 0.7 # 70% 以上是数字即为 Number 行
row1_is_number = _is_number_row(top_row_1_cells)
row2_is_number = _is_number_row(top_row_2_cells)
if row1_is_number and not row2_is_number:
# 布局 BNumber 在上Name 在下(用户实际布局)
top_number_row = min_row_for_top
top_name_row = min_row_for_top + 1
elif not row1_is_number and row2_is_number:
# 布局 AName 在上Number 在下v1.5.5 布局)
top_name_row = min_row_for_top
top_number_row = min_row_for_top + 1
elif row1_is_number and row2_is_number:
# 两行都是数字(异常情况:可能没有 Name 行)
# 回退到布局 A 假设
top_name_row = min_row_for_top
top_number_row = min_row_for_top + 1
else:
# 两行都不是数字(极异常情况)
# 回退到布局 A 假设
top_name_row = min_row_for_top
top_number_row = min_row_for_top + 1
```
然后修改 Step 3 中上边 Name 查找和 Step 4d 中的 Top 边 Number 遍历:
```python
# Step 3: 上边 Name 查找(修改后)
# Name 在上边数字行之上/之下的一行
for c in range(min_col, max_col + 1):
name = cells.get((top_name_row, c), "")
if name and str(name).strip() and _try_int(name) is None:
name_map[(top_number_row, c)] = str(name).strip()
# Step 4d: 上边遍历(修改后)
for c in range(max_col, min_col - 1, -1):
_add_pin(top_number_row, c, "top", max_col - c)
```
### 3.4 影响的其他边
左、下、右三边的布局在用户提供的 PinMAP 中是标准的Name 在 Number 内侧一列/一行。但为了一致性可以考虑对下边也增加类似检测Name 在 Number 上方 vs 下方),但当前未收到相关反馈,建议**先从简处理**,仅在 Top 边出现问题后扩展到其他边。
### 3.5 修改文件清单F013
| 文件 | 修改内容 | 风险 |
|------|---------|------|
| `Code/src/pinmap_parser.py` | Step 2-3 之间增加 Top 边布局检测;修改上边 Name 查找和 Number 遍历 | 中 |
| `Code/src/test_pinmap.py` | 新增测试用例:用户 QFN60 格式的 PinMAP 解析 | 低 |
---
## 4. F014 分析 — PinList→PinMAP 样式模板应用
### 4.1 当前状态
v1.5.5 `main.py``run_list_to_map()` 已经:
1. 调用 `_find_pinmap_template_path()` 查找模板
2. 调用 `read_template_styles()` 解析样式
3.`template_style` 传递给 `generate_pinmap()``write_xlsx_with_style()`
**搜索路径**v1.5.5 已修复):
```python
# main.py _find_pinmap_template_path()
src_dir = os.path.dirname(os.path.abspath(__file__)) # → Code/src/
template_path = os.path.join(src_dir, "Template", "PinMAP-Template.xlsx")
# → Code/src/Template/PinMAP-Template.xlsx ✅
```
### 4.2 确认项
1. ✅ 模板文件存在:`Code/src/Template/PinMAP-Template.xlsx` 已确认存在
2. ✅ 搜索路径正确:优先查找 `Code/src/Template/`,向后兼容项目根目录和 cwd
3. ✅ 样式提取完整:`template_reader.py` 提取字体 / 填充 / 边框 / 对齐 / 列宽 / 行高
4. ✅ 样式应用正确:`StyledXLSXWriter` 将模板样式应用到输出
### 4.3 用户提到的"模板放在主程序根目录"
用户提到模板应放在"主程序根目录"。主程序根目录 = `pinmap-to-pinlist/`。当前代码**优先**查找 `Code/src/Template/`**其次**查找项目根目录(向后兼容)。
**建议保持不变**:当前的多路径回退策略已经覆盖了主程序根目录。如果用户坚持只从主程序根目录查找,可在评估后的实施阶段调整搜索优先级。但从工程角度看,`Code/src/Template/` 更合理(与源码打包在一起)。
### 4.4 F014 结论
**F014 在当前 v1.5.5 代码中已基本实现**,无需大规模修改。需要确认的是:
- 模板文件的内容是否符合用户期望(字体/边框/对齐/填充色等)
- 确认项将在 F016 验证阶段通过实际生成的 xlsx 与预期对比来验证
---
## 5. F015 分析 — PinMAP→PinList 样式模板应用
### 5.1 当前状态
v1.5.5 `main.py``run_map_to_list()` 已经:
1. 调用 `_find_pinlist_template_path()` 查找模板
2. 调用 `read_template_styles()` 解析样式
3. 如果模板存在,使用 `write_xlsx_with_style()`,否则使用 `write_xlsx()`
**搜索路径**(同 F014
```python
# Code/src/Template/PinList-Template.xlsx ✅
```
模板文件存在:`Code/src/Template/PinList-Template.xlsx`
### 5.2 注意事项
PinList 输出的数据非常简单两列A 列 = PinNameB 列 = Pin 序号),样式应用的效果主要是:
- 字体名称/大小/颜色
- 列宽(如果模板只有两列数据,列 C 以后理论上不应有宽度设置)
- 行高
- 边框
`StyledXLSXWriter._sheet_xml()``style.column_widths` 字典读取列宽并生成 `<cols>` 元素。如果 PinList 模板中只有 A、B 两列定义了宽度,输出也会只有两列宽。
### 5.3 需要确认的潜在问题
`StyledXLSXWriter``_get_style_index()` 方法为所有非 A1 单元格分配 style index `1`(边框+居中。PinList 输出也是相同的逻辑——A1 用 style 2bold其他用 style 1。这对 PinList 两列布局来说是合理的。
但是PinList 的 A1 不是"封装信息"就是"Package Name",而 PinMAP 的 A1 也是封装信息。两个模板可能在 A1 样式的 fontId/fillId/borderId 上指向不同索引,但当前 `_get_style_index()` 统一用 hardcoded 的 index。**这是一个潜在问题**,但在用户明确反馈前不做假设性修改。
### 5.4 F015 结论
**F015 在当前 v1.5.5 代码中已基本实现**。确认项将在 F017 验证阶段通过实际生成的 PinList xlsx 与预期对比来验证。
---
## 6. F016 分析 — PinList→PinMAP 转换正确性验证
### 6.1 测试设计
使用用户提供的 QFN60 示例 PinList60 个引脚)作为输入,验证生成的 PinMAP 结构与示例 PinMAP 一致。
**示例 PinList 结构**
```
QFN60
Pin1,1
Pin2,2
...
Pin60,60
```
**示例 PinMAP 预期结构**(基于用户提供的 CSV
- 12×12 网格QFN60 环形布局)
- Left 边Pin1Pin12row 4-15, col A/B
- Bottom 边Pin13Pin18 + Pin28Pin33row 16-17, 中间空列对应无引脚位置)
- Right 边Pin34Pin45row 15→4, col 倒数两列)
- Top 边Pin46Pin60row 3, col 倒数→前row 2 为 Numbers
- 内部为空QFN 封装中心无引脚)
### 6.2 关键问题60 引脚的环形布局 ≠ 12×12 全周长
12×12 网格的全周长 = (12+12)×2 = 48。但 QFN60 有 **60 个引脚**。这意味着用户使用的"12×12 网格"并不是严格的矩形周长概念。
重新分析用户 PinMAP
- Left 边12 个引脚Pin1Pin12
- Right 边12 个引脚Pin34Pin45
- Top 边15 个引脚Pin46Pin60跨越 15 列)
- Bottom 边21 个引脚Pin13Pin33跨越 21 列?不对,重看)
仔细看用户实际 PinMAP 布局:
- Left12 个
- Bottom12 个Pin13Pin18=6 + Pin24Pin33?不对)
让我重新分析 CSV 模式:
- Left col A/B12 pins (Pin1Pin12)
- Right col 最后两列12 pins (Pin34Pin45)
- Top row 2-315 pins (Pin46Pin60)
- Bottom 倒数第1-2行15 pins? → 实际底部仅 Pin13Pin18=6 + ... 需要确认
实际上用户 CSV 格式是 12×1212行12列但只用了外圈。Pin 总计 60但网格如果按周长算只有 48。
**这里存在根本性偏差**:用户的 PinMAP 是"12×12 网格内有 60 个环形引脚"——引脚布局在 12×12 的外圈但某些角被占用。这意味着 PinMAP 布局**不完全遵循四边等长逻辑**。
**但这对 v1.6 不重要**F016 和 F017 的目标是验证**端到端转换正确性**,即:
1. List→MAP输入 60-pin PinList → 生成 PinMAP xlsx
2. MAP→List将生成的 PinMAP 再转回 PinList
3. 往返验证:原始 PinList == 生成的 PinList引脚不丢失、顺序不变
这将暴露 F013 修复后解析器是否能正确处理各种布局。
### 6.3 F016 测试方案
**测试 F016-1: 60-pin List→MAP 基本生成**
输入:
- 60 个 PinListEntry (Pin1Pin60)
- rows=12, cols=12
- package_info="QFN60"
验证:
- 生成的 PinMAP 至少包含 A1 封装信息 + 所有 60 个引脚的 Name 和 Number
- 无单元格冲突(生成器内部保证)
- 引脚沿四条边分布(取决于布局算法分配)
**测试 F016-2: List→MAP→List 往返**
输入:同上 60-pin PinList
验证:
- List→MAP 生成 PinMAP xlsx
- 将该 xlsx 作为 MAP→List 输入
- 解析出的 PinList 包含 60 个引脚,顺序 Pin1Pin60封装信息 "QFN60"
- 无引脚丢失、无序号错误
**注意**:不要求生成的 PinMAP 在**单元格位置**上与用户示例 PinMAP 完全一致。用户示例 PinMAP 是手工制作的(某些边有不同数量的引脚),而程序将使用标准的周长分配算法。**只要往返一致、引脚不丢失**即可。
### 6.4 但如果用户要求"结构一致"怎么办
features.md 中 F016 的验收标准写的是"生成的 PinMAP 与示例 PinMAP 结构完全一致"。这暗示用户希望:**生成的 PinMAP 在外观布局上与示例 PinMAP 相同**。
如果是这样PinMAP 生成器需要支持**非均匀边分配**——即用户指定每条边分别有多少引脚。这是当前 `pinmap_layout.py` 不支持的(它假设 rows=cols 时每条边有相同数量的引脚)。
**建议**:在 v1.6 中先实现往返正确性验证作为 F016 的交付物。如果用户坚持布局像素级一致,需要在 v1.7 中重新设计布局引擎。
---
## 7. F017 分析 — PinMAP→PinList 转换正确性验证
### 7.1 测试设计
使用用户提供的 QFN60 示例 PinMAP (CSV) 作为输入,验证:
1. 解析器能正确识别 Top 边(修复后)
2. 生成的 PinList 包含完整的 60 个引脚
3. Pin 序号 Pin1Pin60 完整无缺失
4. 封装信息 "QFN60 ..." 正确提取
5. PinName 与示例一致
### 7.2 测试用例构建
**输入**:用户提供的 QFN60 PinMAP CSV转换为 Excel 或直接用当前 xls_reader 兼容的格式)
由于当前解析器读取的是 Excel 格式(.xls/.xlsx需要先构建测试用的 cell dictionary。
**测试 F017-1: QFN60 PinMAP 解析**
基于用户提供的 CSV 构建 cells dict0-based row, col
```python
cells = {
(0, 0): "QFN60 6*6*0.85mm ...",
# Top: Number row 1, Name row 2
(1, 2): "60", (1, 3): "59", ..., (1, 16): "46",
(2, 2): "Pin60", (2, 3): "Pin59", ..., (2, 16): "Pin46",
# Left: rows 3..14
(3, 0): "1", (3, 1): "Pin1",
...
# Bottom
...
# Right
...
}
```
验证:
- `parse_pinmap(cells)` 返回 60 个 Pin
- Top 边的 Pin (Pin46Pin60) 都被正确识别且有正确的 edge="top"
- 无 StructureError
**测试 F017-2: QFN60 PinMAP→PinList 完整转换**
从 parsed pinmap 生成 PinList验证
- `len(pinlist.rows) == 60`
- Pin 序号 1..60 全部存在
- 封装信息正确
**测试 F017-3: 往返验证 (MAP→List→MAP)**
```python
cells parse_pinmap pinmap
pinmap generate_pinlist pinlist (60 pins)
pinlist [重新构造 entries] generate_pinmap pinmap2
```
验证 `pinmap``pinmap2` 的引脚数量和序号一致。
---
## 8. 总体修改方案与文件清单
### 8.1 F013 — 修复上方引脚丢失
| 文件 | 修改内容 | 工作量 |
|------|---------|--------|
| `Code/src/pinmap_parser.py` | 在 Step 2-3 之间增加 Top 边布局自动检测逻辑,根据数据内容决定 Name 在 Number 上方还是下方 | 中(~40 行新代码) |
| `Code/src/pinmap_parser.py` | 修改 Step 3 上边 Name 查找,使用检测结果;修改 Step 4d 上边遍历,使用正确的 Number 行 | 中 |
### 8.2 F014 — PinList→PinMAP 模板(确认性检查)
| 文件 | 修改内容 | 工作量 |
|------|---------|--------|
| `Code/src/main.py` | 确认 `_find_pinmap_template_path()` 搜索路径正确v1.5.5 已修复) | 无代码改动 |
| `Code/src/Template/PinMAP-Template.xlsx` | 确认模板文件存在且格式正确 | 无代码改动 |
### 8.3 F015 — PinMAP→PinList 模板(确认性检查)
| 文件 | 修改内容 | 工作量 |
|------|---------|--------|
| `Code/src/main.py` | 确认 `_find_pinlist_template_path()` 搜索路径正确v1.5.5 已修复) | 无代码改动 |
| `Code/src/Template/PinList-Template.xlsx` | 确认模板文件存在且格式正确 | 无代码改动 |
### 8.4 F016 — List→MAP 验证
| 文件 | 修改内容 | 工作量 |
|------|---------|--------|
| `Code/src/test_pinmap.py` | 新增 QFN60 List→MAP 生成测试 + 往返测试 | 低(~60 行新测试代码) |
### 8.5 F017 — MAP→List 验证
| 文件 | 修改内容 | 工作量 |
|------|---------|--------|
| `Code/src/test_pinmap.py` | 新增 QFN60 PinMAP 解析测试(使用 Top 布局 B+ 完整转换测试 + 往返测试 | 中(~100 行新测试代码 + 构建 QFN60 cells 常量) |
---
## 9. 任务拆分建议
### 9.1 子任务划分
| Seq | 子任务 | 关联需求 | 执行 Agent | 预估工作量 | 依赖 |
|-----|--------|---------|-----------|-----------|------|
| 1 | **F013 编码实现**:修复 `pinmap_parser.py` Top 边识别 | F013 | python-coding-agent | 1h | 无 |
| 2 | **F017 测试用例**QFN60 PinMAP→PinList 解析+转换+往返测试 | F017 | test-qa-agent | 30min | Seq 1 |
| 3 | **F016 测试用例**QFN60 PinList→PinMAP 生成+往返测试 | F016 | test-qa-agent | 30min | Seq 1 |
| 4 | **F014/F015 确认**:模板路径+样式应用确认(如发现问题则修复) | F014, F015 | python-coding-agent | 15min | Seq 1 |
| 5 | **全量回归测试**:运行全部测试用例确保无回归 | F013-F017 | test-qa-agent | 15min | Seq 2-4 |
| 6 | **文档生成**:更新 features.md, tasks.md, CHANGELOG.md | F013-F017 | doc-gen-agent | 15min | Seq 5 |
| 7 | **打包发布 v1.6** | F013-F017 | package-release-agent | 15min | Seq 6 |
### 9.2 推荐执行顺序
```
Step 1: python-coding-agent → F013 编码pinmap_parser.py 修改)
Step 2: test-qa-agent → F017 测试用例(依赖修复后解析器)
Step 3: test-qa-agent → F016 测试用例(与 F017 并行)
Step 4: python-coding-agent → F014/F015 确认/修复(如有需要)
Step 5: test-qa-agent → 全量回归测试
Step 6: doc-gen-agent → 文档更新
Step 7: package-release-agent → 打包发布
```
### 9.3 可并行项
- F016 测试用例Seq 3和 F017 测试用例Seq 2可以在 F013 编码完成后**并行执行**,因为它们都依赖 F013 修复但不相互依赖。
- F014/F015 确认Seq 4可以与测试用例并行。
---
## 10. 风险与缓解措施
| 风险 ID | 风险描述 | 影响 | 概率 | 缓解措施 |
|---------|---------|------|------|---------|
| R1 | Top 边自动检测逻辑误判(如模板 PinMAP 中 Name 和 Number 行都是空或非常规格式) | Top 边引脚仍然丢失 | 低 | 设置 70% 置信阈值 + 回退到默认行为;在检测失败时打印 WARN 日志 |
| R2 | 用户 PinMAP 的底部Bottom边也存在类似反转问题但尚未反馈 | 底部引脚也丢失 | 中 | 如果 F017 测试发现底部也有问题,在 v1.6 中一并修复(扩大自动检测范围到下边) |
| R3 | 用户期望 PinMAP 布局与示例完全一致(像素级),但当前算法是均匀分配 | 用户不满意生成的 PinMAP 外观 | 中 | 在 F016 的实现中明确声明"往返正确性验证",布局一致性留到 v1.7 |
| R4 | F013 的修改改变了现有 4×4/12-pin 测试的预期行为 | 回归测试失败 | 低 | 自动检测会选择布局 A与 v1.5.5 一致的 Name 在上方),对现有测试透明 |
| R5已关闭 | ~~QFN60 12×12 网格周长48与 60 引脚不匹配~~ | — | — | ✅ 已确认 QFN60 是 15×15 网格,周长 = (15+15)×2 = 60完美匹配 |
---
## 11. v1.6 与 v1.5.5 代码差异预估
### 11.1 新增代码
| 位置 | 内容 | 行数 |
|------|------|------|
| `pinmap_parser.py` Step 2.5 | Top 边布局自动检测函数 `_detect_top_layout()` | ~30 行 |
| `pinmap_parser.py` Step 3 | 修改上边 Name 查找(替换现有 5 行) | +5 行 |
| `pinmap_parser.py` Step 4d | 修改上边遍历(替换现有 2 行) | +2 行 |
| `test_pinmap.py` | QFN60 cells 常量(~60 pins, 12×12 | ~80 行 |
| `test_pinmap.py` | F017 测试函数 (解析 + 转换 + 往返) | ~60 行 |
| `test_pinmap.py` | F016 测试函数 (生成 + 往返) | ~40 行 |
### 11.2 预计不修改的文件
- `main.py` — 模板搜索路径已在 v1.5.5 修复(除非用户坚持改为项目根目录优先)
- `pinmap_layout.py` — List→MAP 生成布局不变
- `pinmap_generator.py` — 无变化
- `pinlist_generator.py` — 无变化
- `template_reader.py` — 无变化
- `xlsx_writer.py` — 无变化
- `validator.py` — 无变化
- `pinlist_parser.py` — 无变化
- `pinlist_validator.py` — 无变化
### 11.3 总工作量预估
| 阶段 | 预估时间 |
|------|---------|
| 编码F013 | 1.0 h |
| 测试F016 + F017 | 0.75 h |
| 确认/修复F014 + F015 | 0.25 h |
| 回归测试 | 0.25 h |
| 文档 | 0.25 h |
| 打包 | 0.25 h |
| **合计** | **~2.75 h** |
---
## 12. 总结
1. **F013最关键**`pinmap_parser.py` 硬编码假设 Top 边 Name 在 Number 上方一行,但用户真实 PinMAP 中 Name 在 Number 下方一行。需要增加基于数据内容的**自动布局检测**,兼容两种方向。修改集中在 `pinmap_parser.py` 一个文件。
2. **F014/F015已有基础**:模板搜索路径在 v1.5.5 中已修复(`Code/src/Template/`样式应用链路完整。v1.6 主要是**确认性验证**,代码改动预计为零。
3. **F016/F017新增测试**:需要构建 QFN60 60-pin 的测试数据用于端到端验证。**核心关注往返正确性**List→MAP→List 不丢失引脚),而非与用户示例的像素级布局一致(后者需要后续版本支持非均匀边分配)。
4. **最大风险 R5**60 引脚与 12×12 网格的周长48不匹配。需要在测试实施阶段确认正确的行/列参数。如果用户 PinMAP 实际使用的是非标准布局(某些边有不同数量的引脚),可能需要向用户确认。
---
*文档结束 — v1.6 整改架构评估*

View File

@@ -26,4 +26,15 @@
| T025 | 架构评估 PinList→PinMAP 布局 Bug 修复 | script-architect | 已完成 | 架构评估 | BUG-005, BUG-006 | 2026-06-12 | 2026-06-12 | BUG-005/006 用户反馈 v1.5.4 修复未生效,需重新分析 |
| T026 | 编码实现 v1.5.5 Bug 修复 | python-coding-agent | 已完成 | 编码实现 | BUG-005, BUG-006 | 2026-06-12 | 2026-06-12 | 全部 37 测试通过 |
| T027 | 文档生成 v1.5.5 | doc-gen-agent | 已完成 | 文档编写 | BUG-005, BUG-006 | 2026-06-12 | 2026-06-12 | 文档已全部更新 |
| T028 | 打包发布 v1.5.5 | package-release-agent | 进行中 | 打包发布 | BUG-005, BUG-006 | 2026-06-12 | - | 创建 Release + 上传 zip |
| T028 | 打包发布 v1.5.5 | package-release-agent | 已完成 | 打包发布 | BUG-005, BUG-006 | 2026-06-12 | 2026-06-12 | Release 已创建 + zip 已上传 + git push 完成 |
| T029 | 架构评估 v1.6 整改F013-F017 | script-architect | 已完成 | 架构评估 | F013-F017 | 2026-06-12 | 2026-06-12 | 评估完成,见 docs/modification-assessment-v1.6.md |
| T030 | 编码实现 F013 上方引脚丢失修复 | python-coding-agent | 已完成 | 编码实现 | F013 | 2026-06-12 | 2026-06-12 | pinmap_parser.py 增加自动布局检测 |
| T031 | 测试验证 F016/F017 | test-architect | 已完成 | 测试验证 | F016, F017 | 2026-06-12 | 2026-06-12 | 5 个 QFN60 新增测试 + 全量 23/23 通过 |
| T032 | 模板确认 F014/F015 | python-coding-agent | 已完成 | 确认验证 | F014, F015 | 2026-06-12 | 2026-06-12 | 两个模板文件存在,样式解析成功 |
| T033 | 文档生成 v1.6 | doc-gen-agent | 已完成 | 文档编写 | F013-F017 | 2026-06-12 | 2026-06-12 | 更新 CHANGELOG.md、features.md、tasks.md |
| T034 | 打包发布 v1.6 | package-release-agent | 已完成 | 打包发布 | F013-F017 | 2026-06-12 | 2026-06-12 | Release 已创建 + zip 已上传 + git push 完成 |
| T035 | 架构评估 BUG-007PinList→PinMAP 布局方向) | script-architect | 已完成 | 架构评估 | BUG-007 | 2026-06-12 | 2026-06-12 | 评估完成,见修改方案 |
| T036 | 编码修复 BUG-007Layout B 生成) | python-coding-agent | 已完成 | 编码实现 | BUG-007 | 2026-06-12 | 2026-06-12 | pinmap_layout.py + test_pinmap.py 修改完成23/23 通过
| T037 | 测试验证 BUG-007 | test-executor | 已完成 | 测试验证 | BUG-007 | 2026-06-12 | 2026-06-12 | 回归测试确认无回归
| T038 | 文档生成 BUG-007 | doc-gen-agent | 已完成 | 文档编写 | BUG-007 | 2026-06-12 | 2026-06-12 | 更新 bugs.md、CHANGELOG.md、tasks.md
| T039 | 打包发布 BUG-007 | package-release-agent | 待处理 | 打包发布 | BUG-007 | 2026-06-12 | - | 打包 v1.6.1

Binary file not shown.

Binary file not shown.