Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7a4a767697 | |||
| 3c5fcff1d5 | |||
| 88a231424c | |||
| e582b454d3 | |||
| d635ddbebe |
12
.gitignore
vendored
12
.gitignore
vendored
@@ -16,6 +16,18 @@ build/
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
|
|
||||||
|
# Agent metadata
|
||||||
|
.openclaw/
|
||||||
|
AGENTS.md
|
||||||
|
HEARTBEAT.md
|
||||||
|
IDENTITY.md
|
||||||
|
SOUL.md
|
||||||
|
TOOLS.md
|
||||||
|
USER.md
|
||||||
|
|
||||||
|
# Release archives (keep versioned release notes only)
|
||||||
|
Releases/*.zip
|
||||||
|
|
||||||
# IDE
|
# IDE
|
||||||
.vscode/
|
.vscode/
|
||||||
.idea/
|
.idea/
|
||||||
|
|||||||
91
CHANGELOG.md
91
CHANGELOG.md
@@ -1,5 +1,96 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## [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 的上下位置,兼容两种布局
|
||||||
|
- QFN60(15×15,60 引脚)端到端往返验证通过
|
||||||
|
|
||||||
|
#### 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 修复(深度修复)
|
||||||
|
|
||||||
|
#### BUG-005 【高】模板文件名/路径错误
|
||||||
|
|
||||||
|
- **根因**:v1.5.4 只改了模板文件名(`BallList-Template.xlsx` → `PinList-Template.xlsx`),但未修正搜索路径
|
||||||
|
- **修复**:模板搜索路径优先查找 `Code/src/Template/` 目录,再回退项目根目录和当前工作目录
|
||||||
|
- 模板样式现在可正确应用到输出文件
|
||||||
|
|
||||||
|
#### BUG-006 【高】PinList→PinMAP 上边 Name 与左边 Name 同行
|
||||||
|
|
||||||
|
- **根因**:v1.5.4 将上边 Name 放在 row 2(Excel 第 3 行),与左边 Name/Number 起始行相同,导致 3 条边数据混在同一行
|
||||||
|
- **修复**:将上边 Name 移至 **row 0**(Excel 第 1 行),上边 Number 保持在 row 1(第 2 行),使上边完全独立于其他边
|
||||||
|
- 不再需要角点例外逻辑,代码更简洁
|
||||||
|
- 每条边数据独立分隔,肉眼可读性大幅提升
|
||||||
|
|
||||||
|
### 🔧 修改文件
|
||||||
|
|
||||||
|
- `Code/src/main.py` — BUG-005: 模板搜索路径修正(优先 Code/src/Template/)
|
||||||
|
- `Code/src/pinmap_layout.py` — BUG-006: 上边 Name 坐标改为 `(0, c)`,移除角点例外
|
||||||
|
- `Code/src/pinmap_parser.py` — BUG-006: 上边 Name 从 row 0 读取,Number 从 row 1 读取
|
||||||
|
- `Test/fixtures/sample_4x4.xlsx` — BUG-006: 更新为 v1.5.5 新布局
|
||||||
|
- `Code/src/test_pinmap.py` — BUG-006: 测试数据适配新布局
|
||||||
|
|
||||||
|
### ✅ 测试
|
||||||
|
- 全部 37 个测试通过
|
||||||
|
|
||||||
|
## [v1.5.4] - 2026-06-09
|
||||||
|
|
||||||
|
### 🐛 Bug 修复
|
||||||
|
|
||||||
|
#### BUG-005 【高】模板文件名错误
|
||||||
|
|
||||||
|
- 模板文件重命名:`BallList-Template.xlsx` → `PinList-Template.xlsx`,`BallMAP-Template.xlsx` → `PinMAP-Template.xlsx`
|
||||||
|
- 同步更新 `main.py` 中的函数名和模板引用路径
|
||||||
|
|
||||||
|
#### BUG-006 【高】布局重设计(Number 外侧 + Name 里侧)
|
||||||
|
|
||||||
|
- 重新设计 PinMAP 布局:从网格边界往中心走,第 1 圈 = Number(数字),第 2 圈 = Name(引脚名)
|
||||||
|
- **上边**:Number row 1(最顶行),Name row 2(第二行;角点例外:最左/右上边 Name 在 (1,0)/(1,cols+1))
|
||||||
|
- **左边**:Number col 0(最左列),Name col 1(第二列)
|
||||||
|
- **下边**:Number row rows+3(最底行),Name row rows+2(倒数第二行)
|
||||||
|
- **右边**:Number col cols+1(最右列),Name col cols(右二列)
|
||||||
|
- Pin1 保持在左上角(A3=1, B3=Pin1)
|
||||||
|
- 不再需要角点 "//" 合并,每条边不共享任何单元格
|
||||||
|
- 全部 15 种网格大小验证无冲突
|
||||||
|
- 18/18 单元测试 + 37/37 集成测试全部通过
|
||||||
|
|
||||||
|
### 🔧 修改文件
|
||||||
|
|
||||||
|
- `Code/src/main.py` — BUG-005: 模板函数和引用改名;BUG-006: 传递 cols 参数
|
||||||
|
- `Code/src/pinmap_layout.py` — BUG-006: 重写坐标公式 + `get_name_cell()` 支持 cols 参数 + 角点例外
|
||||||
|
- `Code/src/pinmap_generator.py` — BUG-006: 传递 cols 参数 + 更新注释
|
||||||
|
- `Code/src/pinmap_parser.py` — BUG-006: 重写边界检测、Name 读取(角点例外检测)
|
||||||
|
- `Code/src/test_pinmap.py` — BUG-006: 更新测试数据适配新布局
|
||||||
|
- `Test/fixtures/PinList-Template.xlsx` + `PinMAP-Template.xlsx` — BUG-005: 模板文件重命名
|
||||||
|
|
||||||
|
### 📝 文档
|
||||||
|
|
||||||
|
- 更新 `CHANGELOG.md` 追加 v1.5.4 版本日志
|
||||||
|
- 更新 `README.md` 追加 v1.5.4 版本说明
|
||||||
|
- 生成 `Releases/v1.5.4/CHANGELOG.md`
|
||||||
|
|
||||||
## [v1.5.0] - 2026-06-06
|
## [v1.5.0] - 2026-06-06
|
||||||
|
|
||||||
### ✨ 功能新增
|
### ✨ 功能新增
|
||||||
|
|||||||
@@ -33,47 +33,55 @@ def wait_for_exit():
|
|||||||
|
|
||||||
# ── Path helpers ────────────────────────────────────────────────────
|
# ── Path helpers ────────────────────────────────────────────────────
|
||||||
|
|
||||||
def _find_balllist_template_path() -> str | None:
|
def _find_pinlist_template_path() -> str | None:
|
||||||
"""查找根目录下的 BallList-Template.xlsx。
|
"""查找 PinList-Template.xlsx。
|
||||||
|
|
||||||
MAP→List 输出使用 BallList 模板(而非旧 PinMAP 模板)。
|
MAP→List 输出使用 PinList 模板。
|
||||||
搜索顺序:
|
搜索顺序:
|
||||||
1. 与 run.bat 同级的根目录
|
1. Code/src/Template/ 目录(首要位置)
|
||||||
2. 当前工作目录
|
2. 项目根目录(向后兼容)
|
||||||
|
3. 当前工作目录
|
||||||
"""
|
"""
|
||||||
src_dir = os.path.dirname(os.path.abspath(__file__))
|
src_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
root_dir = os.path.dirname(os.path.dirname(src_dir)) # pinmap-to-pinlist/
|
# 1. Code/src/Template/ 目录
|
||||||
template_path = os.path.join(root_dir, "BallList-Template.xlsx")
|
template_path = os.path.join(src_dir, "Template", "PinList-Template.xlsx")
|
||||||
|
|
||||||
if os.path.exists(template_path):
|
if os.path.exists(template_path):
|
||||||
return template_path
|
return template_path
|
||||||
|
# 2. 项目根目录(向后兼容)
|
||||||
cwd_template = os.path.join(os.getcwd(), "BallList-Template.xlsx")
|
root_dir = os.path.dirname(os.path.dirname(src_dir))
|
||||||
|
template_path = os.path.join(root_dir, "PinList-Template.xlsx")
|
||||||
|
if os.path.exists(template_path):
|
||||||
|
return template_path
|
||||||
|
# 3. 当前工作目录
|
||||||
|
cwd_template = os.path.join(os.getcwd(), "PinList-Template.xlsx")
|
||||||
if os.path.exists(cwd_template):
|
if os.path.exists(cwd_template):
|
||||||
return cwd_template
|
return cwd_template
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _find_ballmap_template_path() -> str | None:
|
def _find_pinmap_template_path() -> str | None:
|
||||||
"""查找根目录下的 BallMAP-Template.xlsx。
|
"""查找 PinMAP-Template.xlsx。
|
||||||
|
|
||||||
List→MAP 输出使用 BallMAP 模板(而非旧 PinMAP 模板)。
|
List→MAP 输出使用 PinMAP 模板。
|
||||||
搜索顺序:
|
搜索顺序:
|
||||||
1. 与 run.bat 同级的根目录
|
1. Code/src/Template/ 目录(首要位置)
|
||||||
2. 当前工作目录
|
2. 项目根目录(向后兼容)
|
||||||
|
3. 当前工作目录
|
||||||
"""
|
"""
|
||||||
src_dir = os.path.dirname(os.path.abspath(__file__))
|
src_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
root_dir = os.path.dirname(os.path.dirname(src_dir)) # pinmap-to-pinlist/
|
# 1. Code/src/Template/ 目录
|
||||||
template_path = os.path.join(root_dir, "BallMAP-Template.xlsx")
|
template_path = os.path.join(src_dir, "Template", "PinMAP-Template.xlsx")
|
||||||
|
|
||||||
if os.path.exists(template_path):
|
if os.path.exists(template_path):
|
||||||
return template_path
|
return template_path
|
||||||
|
# 2. 项目根目录(向后兼容)
|
||||||
cwd_template = os.path.join(os.getcwd(), "BallMAP-Template.xlsx")
|
root_dir = os.path.dirname(os.path.dirname(src_dir))
|
||||||
|
template_path = os.path.join(root_dir, "PinMAP-Template.xlsx")
|
||||||
|
if os.path.exists(template_path):
|
||||||
|
return template_path
|
||||||
|
# 3. 当前工作目录
|
||||||
|
cwd_template = os.path.join(os.getcwd(), "PinMAP-Template.xlsx")
|
||||||
if os.path.exists(cwd_template):
|
if os.path.exists(cwd_template):
|
||||||
return cwd_template
|
return cwd_template
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
@@ -171,17 +179,17 @@ def run_map_to_list(filepath: str):
|
|||||||
data[f'A{row}'] = pin_name
|
data[f'A{row}'] = pin_name
|
||||||
data[f'B{row}'] = str(pin_num)
|
data[f'B{row}'] = str(pin_num)
|
||||||
|
|
||||||
# 尝试读取 BallList 模板样式(F009)
|
# 尝试读取 PinList 模板样式
|
||||||
template_path = _find_balllist_template_path()
|
template_path = _find_pinlist_template_path()
|
||||||
template_style = None
|
template_style = None
|
||||||
if template_path:
|
if template_path:
|
||||||
template_style = read_template_styles(template_path)
|
template_style = read_template_styles(template_path)
|
||||||
if template_style:
|
if template_style:
|
||||||
print(f"[INFO] 已加载 BallList 模板样式: {template_path}")
|
print(f"[INFO] 已加载 PinList 模板样式: {template_path}")
|
||||||
else:
|
else:
|
||||||
print("[WARN] BallList 模板文件存在但解析失败,使用默认样式")
|
print("[WARN] PinList 模板文件存在但解析失败,使用默认样式")
|
||||||
else:
|
else:
|
||||||
print("[INFO] 未检测到 BallList-Template.xlsx,使用默认样式")
|
print("[INFO] 未检测到 PinList-Template.xlsx,使用默认样式")
|
||||||
|
|
||||||
if template_style is not None:
|
if template_style is not None:
|
||||||
write_xlsx_with_style(data, output_path, template_style)
|
write_xlsx_with_style(data, output_path, template_style)
|
||||||
@@ -281,17 +289,17 @@ def run_list_to_map(filepath: str):
|
|||||||
print(f"[INFO] 正在生成 PinMAP 并写入: {output_path}")
|
print(f"[INFO] 正在生成 PinMAP 并写入: {output_path}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# 尝试读取 BallMAP 模板样式(F010)
|
# 尝试读取 PinMAP 模板样式
|
||||||
template_path = _find_ballmap_template_path()
|
template_path = _find_pinmap_template_path()
|
||||||
template_style = None
|
template_style = None
|
||||||
if template_path:
|
if template_path:
|
||||||
template_style = read_template_styles(template_path)
|
template_style = read_template_styles(template_path)
|
||||||
if template_style:
|
if template_style:
|
||||||
print(f"[INFO] 已加载 BallMAP 模板样式: {template_path}")
|
print(f"[INFO] 已加载 PinMAP 模板样式: {template_path}")
|
||||||
else:
|
else:
|
||||||
print("[WARN] BallMAP 模板文件存在但解析失败,使用默认样式")
|
print("[WARN] PinMAP 模板文件存在但解析失败,使用默认样式")
|
||||||
else:
|
else:
|
||||||
print("[INFO] 未检测到 BallMAP-Template.xlsx,使用默认样式")
|
print("[INFO] 未检测到 PinMAP-Template.xlsx,使用默认样式")
|
||||||
|
|
||||||
generate_pinmap(
|
generate_pinmap(
|
||||||
entries=entries,
|
entries=entries,
|
||||||
|
|||||||
@@ -56,12 +56,11 @@ def generate_pinmap(
|
|||||||
# 先写入 PinName 单元格
|
# 先写入 PinName 单元格
|
||||||
for edge_name, edge in layout.items():
|
for edge_name, edge in layout.items():
|
||||||
for (pin_num, pin_name), num_cell in zip(edge.pins, edge.cells):
|
for (pin_num, pin_name), num_cell in zip(edge.pins, edge.cells):
|
||||||
name_cell = get_name_cell(num_cell, edge_name)
|
name_cell = get_name_cell(num_cell, edge_name, cols=cols)
|
||||||
name_ref = rc_to_cell_ref(name_cell[0], name_cell[1])
|
name_ref = rc_to_cell_ref(name_cell[0], name_cell[1])
|
||||||
data[name_ref] = pin_name if pin_name and pin_name.strip() else "NC"
|
data[name_ref] = pin_name if pin_name and pin_name.strip() else "NC"
|
||||||
|
|
||||||
# 再写入序号单元格(覆盖同位置的名字,确保序号优先)
|
# 再写入序号单元格(v1.5.4:无边角共享,每个序号独占一个单元格)
|
||||||
# v1.3: 角点单元格被两条边共享,需写入两个引脚序号
|
|
||||||
cell_pins: dict[str, list[str]] = {}
|
cell_pins: dict[str, list[str]] = {}
|
||||||
for edge_name, edge in layout.items():
|
for edge_name, edge in layout.items():
|
||||||
for (pin_num, pin_name), num_cell in zip(edge.pins, edge.cells):
|
for (pin_num, pin_name), num_cell in zip(edge.pins, edge.cells):
|
||||||
|
|||||||
@@ -12,7 +12,24 @@ Edge assignment (counter-clockwise, top-left = pin 1):
|
|||||||
|
|
||||||
Total: rows + cols + rows + cols = 2×rows + 2×cols = (rows + cols) × 2
|
Total: rows + cols + rows + cols = 2×rows + 2×cols = (rows + cols) × 2
|
||||||
|
|
||||||
v1.3: 每条边独立包含其端点,角点单元格会被两条边共享。
|
v1.5.5: 上边完全独立在 row 0-1,不与左/右边共享行。
|
||||||
|
每条边独立包含其端点,所有单元格互不冲突。
|
||||||
|
|
||||||
|
Coordinate system (0-based):
|
||||||
|
|
||||||
|
Number (outer ring, 1st circle from boundary):
|
||||||
|
left: (2..rows+1, 0)
|
||||||
|
bottom: (rows+3, 1..cols)
|
||||||
|
right: (rows+1..2, cols+1) [reverse order]
|
||||||
|
top: (1, cols..1) [reverse order]
|
||||||
|
|
||||||
|
Name (inner ring, 2nd circle from boundary):
|
||||||
|
left: (2..rows+1, 1)
|
||||||
|
bottom: (rows+2, 1..cols)
|
||||||
|
right: (rows+1..2, cols) [reverse order]
|
||||||
|
top: (0, c) where c ∈ [1..cols] [reverse order, independent row]
|
||||||
|
|
||||||
|
Pin1: Number (2,0), Name (2,1) — top-left of left edge
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from models import PinListEntry, EdgePins, LayoutError
|
from models import PinListEntry, EdgePins, LayoutError
|
||||||
@@ -86,28 +103,30 @@ def calculate_layout(
|
|||||||
|
|
||||||
top_pins = entries[idx: idx + top_count]
|
top_pins = entries[idx: idx + top_count]
|
||||||
|
|
||||||
# ── 计算单元格坐标 ────────────────────────────────────────────
|
# ── 计算单元格坐标(BUG-007 修复:上边 Name 在 row 2,标题独占 row 0)──
|
||||||
#
|
#
|
||||||
# 网格坐标体系(0-based):
|
# 网格坐标体系(0-based):
|
||||||
# 方形区域:行 [1..rows],列 [0..cols]
|
# 第 0 行完全由标题(A1 合并单元格,不包含引脚数据)独占
|
||||||
# 左边: 序号在 (r, 0), Name 在 (r, 1) 其中 r ∈ [1, rows]
|
# 第 1 行是上边引脚序号,第 2 行是上边引脚 PinName
|
||||||
# 下边: 序号在 (rows, c), Name 在 (rows-1, c) 其中 c ∈ [1, cols]
|
# 从第 3 行开始是左/下/右边引脚
|
||||||
# 右边: 序号在 (r, cols), Name 在 (r, cols-1) 其中 r ∈ [rows, 1] 逆序
|
|
||||||
# 上边: 序号在 (1, c), Name 在 (2, c) 其中 c ∈ [cols, 1] 逆序
|
|
||||||
#
|
#
|
||||||
# v1.3: 每条边独立包含其端点,角点单元格会被两条边共享
|
# 左边: Number (r, 0) r ∈ [3, rows+2] Name (r, 1)
|
||||||
|
# 下边: Number (rows+4, c) c ∈ [1, cols] Name (rows+3, c)
|
||||||
|
# 右边: Number (r, cols+1) r ∈ [rows+2, 3] Name (r, cols) 逆序
|
||||||
|
# 上边: Number (1, c) c ∈ [cols, 1] Name (2, c) 逆序
|
||||||
#
|
#
|
||||||
|
# Pin1: Number (3,0) = A4, Name (3,1) = B4 — 左上角
|
||||||
|
|
||||||
# 左边:从上到下
|
# 左边:从上到下 (rows 个)
|
||||||
left_cells = [(r, 0) for r in range(1, rows + 1)]
|
left_cells = [(r, 0) for r in range(3, rows + 3)]
|
||||||
|
|
||||||
# 下边:从左到右
|
# 下边:从左到右 (cols 个),Number 在最底行 rows+4
|
||||||
bottom_cells = [(rows, c) for c in range(1, cols + 1)]
|
bottom_cells = [(rows + 4, c) for c in range(1, cols + 1)]
|
||||||
|
|
||||||
# 右边:从下到上(逆序)
|
# 右边:从下到上 (rows 个),Number 在 cols+1 列(右扩一列)
|
||||||
right_cells = [(r, cols) for r in range(rows, 0, -1)]
|
right_cells = [(r, cols + 1) for r in range(rows + 2, 2, -1)]
|
||||||
|
|
||||||
# 上边:从右到左(逆序)
|
# 上边:从右到左 (cols 个)
|
||||||
top_cells = [(1, c) for c in range(cols, 0, -1)]
|
top_cells = [(1, c) for c in range(cols, 0, -1)]
|
||||||
|
|
||||||
# ── 构建 EdgePins ─────────────────────────────────────────────
|
# ── 构建 EdgePins ─────────────────────────────────────────────
|
||||||
@@ -124,16 +143,22 @@ def calculate_layout(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def get_name_cell(num_cell: tuple[int, int], edge_name: str) -> tuple[int, int]:
|
def get_name_cell(num_cell: tuple[int, int], edge_name: str,
|
||||||
|
cols: int = 0) -> tuple[int, int]:
|
||||||
"""
|
"""
|
||||||
根据序号单元格坐标和边名称,计算对应的 PinName 单元格坐标。
|
根据序号单元格坐标和边名称,计算对应的 PinName 单元格坐标。
|
||||||
|
|
||||||
|
v1.5.5: 上边 Name 在 Number 上方 (0, c),即独立一行。
|
||||||
|
不再需要角点例外——整个上边 Name 在独立的 row 0。
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
num_cell : tuple[int, int]
|
num_cell : tuple[int, int]
|
||||||
序号单元格坐标 (row, col) 0-based
|
序号单元格坐标 (row, col) 0-based
|
||||||
edge_name : str
|
edge_name : str
|
||||||
"left" | "bottom" | "right" | "top"
|
"left" | "bottom" | "right" | "top"
|
||||||
|
cols : int
|
||||||
|
网格列数(v1.5.5 上边不再需要角点例外,参数保留以兼容调用)
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
@@ -142,12 +167,15 @@ def get_name_cell(num_cell: tuple[int, int], edge_name: str) -> tuple[int, int]:
|
|||||||
"""
|
"""
|
||||||
r, c = num_cell
|
r, c = num_cell
|
||||||
if edge_name == "left":
|
if edge_name == "left":
|
||||||
return (r, c + 1) # Name 在序号右侧
|
return (r, c + 1) # Name 在 Number 右侧 (col 1)
|
||||||
elif edge_name == "bottom":
|
elif edge_name == "bottom":
|
||||||
return (r - 1, c) # Name 在序号上方
|
return (r - 1, c) # Name 在 Number 上方 (row rows+2)
|
||||||
elif edge_name == "right":
|
elif edge_name == "right":
|
||||||
return (r, c - 1) # Name 在序号左侧
|
return (r, c - 1) # Name 在 Number 左侧 (col cols)
|
||||||
elif edge_name == "top":
|
elif edge_name == "top":
|
||||||
return (r + 1, c) # Name 在序号下方
|
# Top Number 在 (1, c), c ∈ [cols..1]
|
||||||
|
# Name 在 Number 下方 (2, c)
|
||||||
|
# row 0 完全由标题行(A1 合并单元格)独占
|
||||||
|
return (2, c) # Name 在 Number 下方(row 2)
|
||||||
else:
|
else:
|
||||||
raise LayoutError(f"未知的边名称: {edge_name}")
|
raise LayoutError(f"未知的边名称: {edge_name}")
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ Reads a dict of {(row, col): str} cells (as produced by xls_reader / xlsx_reader
|
|||||||
detects the rectangular PinMAP boundary, and extracts pins in
|
detects the rectangular PinMAP boundary, and extracts pins in
|
||||||
counter-clockwise order starting from the top-left corner.
|
counter-clockwise order starting from the top-left corner.
|
||||||
|
|
||||||
|
v1.5.5: 上边 Name 在 Number 上方 (min_row-1),无需角点例外。
|
||||||
|
|
||||||
Usage
|
Usage
|
||||||
-----
|
-----
|
||||||
>>> from pinmap_parser import parse_pinmap
|
>>> from pinmap_parser import parse_pinmap
|
||||||
@@ -26,6 +28,75 @@ def _try_int(value: str) -> int | None:
|
|||||||
return 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) 元组。
|
||||||
|
|
||||||
|
布局 A(v1.5.5 默认):Name 在 row 0,Number 在 row 1
|
||||||
|
布局 B(用户真实):Number 在 row 1,Name 在 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)
|
||||||
|
|
||||||
|
# 布局 A(v1.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 1,Name 在 row 2
|
||||||
|
# → row 0 无数据(仅 A1),row 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)
|
||||||
|
|
||||||
|
# 默认回退:假设布局 A(v1.5.5 默认行为)
|
||||||
|
return (0, 1)
|
||||||
|
|
||||||
|
|
||||||
def parse_pinmap(cells: dict[tuple[int, int], str]) -> PinMAP:
|
def parse_pinmap(cells: dict[tuple[int, int], str]) -> PinMAP:
|
||||||
"""Parse a PinMAP from a cell dictionary and return a PinMAP object.
|
"""Parse a PinMAP from a cell dictionary and return a PinMAP object.
|
||||||
|
|
||||||
@@ -82,40 +153,53 @@ def parse_pinmap(cells: dict[tuple[int, int], str]) -> PinMAP:
|
|||||||
if not package_info or not str(package_info).strip():
|
if not package_info or not str(package_info).strip():
|
||||||
raise StructureError("A1 单元格为空,缺少封装信息")
|
raise StructureError("A1 单元格为空,缺少封装信息")
|
||||||
|
|
||||||
# ── Step 3: build name lookup ────────────────────────────────
|
# ── Step 3: build name lookup ───────────────────────────────
|
||||||
# For each edge, pin names live in the cell *adjacent inward*
|
# For each edge, pin names live in the cell *adjacent inward*
|
||||||
# from the boundary cell that holds the pin number.
|
# from the boundary cell that holds the pin number.
|
||||||
#
|
#
|
||||||
# left : number at (r, min_col), name at (r, min_col+1)
|
# left : number at (r, min_col), name at (r, min_col+1)
|
||||||
# bottom : number at (max_row, c), name at (max_row-1, c)
|
# bottom : number at (max_row, c), name at (max_row-1, c)
|
||||||
# right : number at (r, max_col), name at (r, max_col-1)
|
# right : number at (r, max_col), name at (r, max_col-1)
|
||||||
# top : number at (min_row, c), name at (min_row+1, c)
|
# top : layout-dependent (auto-detected below)
|
||||||
|
|
||||||
|
# ── Top edge layout auto-detection ───────────────────────────
|
||||||
|
# 检测上边 Name 和 Number 的相对位置。
|
||||||
|
# 布局 A(v1.5.5 默认):Name 在 row 0,Number 在 row 1
|
||||||
|
# 布局 B(用户真实):Number 在 row 1,Name 在 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] = {}
|
name_map: dict[tuple[int, int], str] = {}
|
||||||
|
|
||||||
# left edge names
|
# left edge names: adjacent inward (r, min_col+1)
|
||||||
for r in range(min_row, max_row + 1):
|
for r in range(min_row, max_row + 1):
|
||||||
name = cells.get((r, min_col + 1), "")
|
name = cells.get((r, min_col + 1), "")
|
||||||
if name and str(name).strip():
|
if name and str(name).strip():
|
||||||
name_map[(r, min_col)] = str(name).strip()
|
name_map[(r, min_col)] = str(name).strip()
|
||||||
|
|
||||||
# bottom edge names
|
# bottom edge names: adjacent upward (max_row-1, c)
|
||||||
for c in range(min_col, max_col + 1):
|
for c in range(min_col, max_col + 1):
|
||||||
name = cells.get((max_row - 1, c), "")
|
name = cells.get((max_row - 1, c), "")
|
||||||
if name and str(name).strip():
|
if name and str(name).strip():
|
||||||
name_map[(max_row, c)] = str(name).strip()
|
name_map[(max_row, c)] = str(name).strip()
|
||||||
|
|
||||||
# right edge names
|
# right edge names: adjacent inward (r, max_col-1)
|
||||||
for r in range(min_row, max_row + 1):
|
for r in range(min_row, max_row + 1):
|
||||||
name = cells.get((r, max_col - 1), "")
|
name = cells.get((r, max_col - 1), "")
|
||||||
if name and str(name).strip():
|
if name and str(name).strip():
|
||||||
name_map[(r, max_col)] = str(name).strip()
|
name_map[(r, max_col)] = str(name).strip()
|
||||||
|
|
||||||
# top edge names
|
# 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):
|
for c in range(min_col, max_col + 1):
|
||||||
name = cells.get((min_row + 1, c), "")
|
name = cells.get((top_name_row, c), "")
|
||||||
if name and str(name).strip():
|
if name and str(name).strip() and _try_int(name) is None:
|
||||||
name_map[(min_row, c)] = str(name).strip()
|
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) ──────
|
# ── Step 4: walk edges counter-clockwise (v1.3 formula) ──────
|
||||||
# Each edge independently includes its endpoints (corners).
|
# Each edge independently includes its endpoints (corners).
|
||||||
@@ -158,9 +242,9 @@ def parse_pinmap(cells: dict[tuple[int, int], str]) -> PinMAP:
|
|||||||
for r in range(max_row, min_row - 1, -1):
|
for r in range(max_row, min_row - 1, -1):
|
||||||
_add_pin(r, max_col, "right", max_row - r)
|
_add_pin(r, max_col, "right", max_row - r)
|
||||||
|
|
||||||
# 4d. Top edge: right → left (includes top-left corner)
|
# 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):
|
for c in range(max_col, min_col - 1, -1):
|
||||||
_add_pin(min_row, c, "top", max_col - c)
|
_add_pin(top_number_row, c, "top", max_col - c)
|
||||||
|
|
||||||
if not pins:
|
if not pins:
|
||||||
raise StructureError("未检测到任何 Pin 数据")
|
raise StructureError("未检测到任何 Pin 数据")
|
||||||
|
|||||||
@@ -16,36 +16,36 @@ from pinlist_generator import generate_pinlist
|
|||||||
from utils import rc_to_cell_ref
|
from utils import rc_to_cell_ref
|
||||||
|
|
||||||
|
|
||||||
# ── 4x4 example from the task description ────────────────────────
|
# ── 4x4 example (BUG-007 fixed layout) ─────────────────────────
|
||||||
# 1-based Excel coords → 0-based (row, col):
|
# Layout: rows=4, cols=4, 16 pins
|
||||||
# A4:1 A5:2 B4:Pin1 B5:Pin2 → left edge
|
# Title: A1 "QFP-44" (row 0 only)
|
||||||
# C7:3 D7:4 C6:Pin3 D6:Pin4 → bottom edge
|
# Top: Number row 1 (B2..E2), Name row 2 (B3..E3)
|
||||||
# F5:5 F4:6 E5:Pin5 E4:Pin6 → right edge
|
# Left: Number A4..A7 (rows 3..6), Name B4..B7 (rows 3..6)
|
||||||
# D2:7 C2:8 D3:Pin7 C3:Pin8 → top edge
|
# Bottom: Name B8..E8 (row 7), Number B9..E9 (row 8)
|
||||||
# A1: "QFP-44" → package info
|
# Right: Number F7..F4 (rows 6..3), Name E7..E4 (rows 6..3)
|
||||||
|
# A1: "QFP-44" = package info (title, merged, row 0 only)
|
||||||
|
#
|
||||||
|
# Pin1: Number A4=(3,0), Name B4=(3,1)
|
||||||
|
|
||||||
cells_4x4 = {
|
cells_4x4 = {
|
||||||
(0, 0): "QFP-44",
|
(0, 0): "QFP-44",
|
||||||
# left edge
|
# top edge Numbers (row 1, cols 1..4)
|
||||||
(3, 0): "1",
|
(1, 1): "16", (1, 2): "15", (1, 3): "14", (1, 4): "13",
|
||||||
(4, 0): "2",
|
# top edge Names (row 2, cols 1..4)
|
||||||
(3, 1): "Pin1",
|
(2, 1): "Pin16", (2, 2): "Pin15", (2, 3): "Pin14", (2, 4): "Pin13",
|
||||||
(4, 1): "Pin2",
|
# left edge (rows 3..6, cols 0..1)
|
||||||
# bottom edge
|
(3, 0): "1", (3, 1): "Pin1",
|
||||||
(6, 2): "3",
|
(4, 0): "2", (4, 1): "Pin2",
|
||||||
(6, 3): "4",
|
(5, 0): "3", (5, 1): "Pin3",
|
||||||
(5, 2): "Pin3",
|
(6, 0): "4", (6, 1): "Pin4",
|
||||||
(5, 3): "Pin4",
|
# bottom edge (rows 7..8, cols 1..4)
|
||||||
# right edge
|
(7, 1): "Pin5", (7, 2): "Pin6", (7, 3): "Pin7", (7, 4): "Pin8",
|
||||||
(4, 5): "5",
|
(8, 1): "5", (8, 2): "6", (8, 3): "7", (8, 4): "8",
|
||||||
(3, 5): "6",
|
# right edge (rows 6..3, cols 4..5)
|
||||||
(4, 4): "Pin5",
|
(6, 4): "Pin9", (6, 5): "9",
|
||||||
(3, 4): "Pin6",
|
(5, 4): "Pin10", (5, 5): "10",
|
||||||
# top edge
|
(4, 4): "Pin11", (4, 5): "11",
|
||||||
(1, 3): "7",
|
(3, 4): "Pin12", (3, 5): "12",
|
||||||
(1, 2): "8",
|
|
||||||
(2, 3): "Pin7",
|
|
||||||
(2, 2): "Pin8",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -53,19 +53,27 @@ def test_4x4_parse():
|
|||||||
pm = parse_pinmap(cells_4x4)
|
pm = parse_pinmap(cells_4x4)
|
||||||
|
|
||||||
assert pm.package_info == "QFP-44", f"package_info={pm.package_info}"
|
assert pm.package_info == "QFP-44", f"package_info={pm.package_info}"
|
||||||
assert len(pm.pins) == 8, f"expected 8 pins, got {len(pm.pins)}"
|
assert len(pm.pins) == 16, f"expected 16 pins, got {len(pm.pins)}"
|
||||||
|
|
||||||
# Counter-clockwise order: left(top→bot) → bottom(left→right)
|
# Counter-clockwise order: left(top→bot) → bottom(left→right)
|
||||||
# → right(bot→top) → top(right→left)
|
# → right(bot→top) → top(right→left)
|
||||||
expected = [
|
expected = [
|
||||||
(1, "Pin1", "left"),
|
(1, "Pin1", "left"),
|
||||||
(2, "Pin2", "left"),
|
(2, "Pin2", "left"),
|
||||||
(3, "Pin3", "bottom"),
|
(3, "Pin3", "left"),
|
||||||
(4, "Pin4", "bottom"),
|
(4, "Pin4", "left"),
|
||||||
(5, "Pin5", "right"),
|
(5, "Pin5", "bottom"),
|
||||||
(6, "Pin6", "right"),
|
(6, "Pin6", "bottom"),
|
||||||
(7, "Pin7", "top"),
|
(7, "Pin7", "bottom"),
|
||||||
(8, "Pin8", "top"),
|
(8, "Pin8", "bottom"),
|
||||||
|
(9, "Pin9", "right"),
|
||||||
|
(10, "Pin10", "right"),
|
||||||
|
(11, "Pin11", "right"),
|
||||||
|
(12, "Pin12", "right"),
|
||||||
|
(13, "Pin13", "top"),
|
||||||
|
(14, "Pin14", "top"),
|
||||||
|
(15, "Pin15", "top"),
|
||||||
|
(16, "Pin16", "top"),
|
||||||
]
|
]
|
||||||
for i, (num, name, edge) in enumerate(expected):
|
for i, (num, name, edge) in enumerate(expected):
|
||||||
p = pm.pins[i]
|
p = pm.pins[i]
|
||||||
@@ -104,7 +112,7 @@ def test_missing_names_warning():
|
|||||||
|
|
||||||
def test_duplicate_numbers():
|
def test_duplicate_numbers():
|
||||||
cells = dict(cells_4x4)
|
cells = dict(cells_4x4)
|
||||||
cells[(6, 3)] = "1" # duplicate pin 1
|
cells[(4, 0)] = "1" # duplicate pin 1 (original at (3,0))
|
||||||
pm = parse_pinmap(cells)
|
pm = parse_pinmap(cells)
|
||||||
vr = validate_pinmap(pm)
|
vr = validate_pinmap(pm)
|
||||||
assert not vr.is_valid
|
assert not vr.is_valid
|
||||||
@@ -114,7 +122,7 @@ def test_duplicate_numbers():
|
|||||||
|
|
||||||
def test_gap_in_numbers():
|
def test_gap_in_numbers():
|
||||||
cells = dict(cells_4x4)
|
cells = dict(cells_4x4)
|
||||||
cells[(6, 2)] = "10" # skip 3
|
cells[(8, 2)] = "10" # skip pin 6 (was "6" at (8,2))
|
||||||
pm = parse_pinmap(cells)
|
pm = parse_pinmap(cells)
|
||||||
vr = validate_pinmap(pm)
|
vr = validate_pinmap(pm)
|
||||||
assert not vr.is_valid
|
assert not vr.is_valid
|
||||||
@@ -168,31 +176,28 @@ def test_rectangular_parse():
|
|||||||
|
|
||||||
|
|
||||||
def test_12pin_square():
|
def test_12pin_square():
|
||||||
"""A larger square: 12 pins on a 6×6 grid (rows 1-5, cols 0-5).
|
"""A 3×3 square: 12 pins (3 pins per edge).
|
||||||
|
Using BUG-007 fixed layout: top numbers at row 1, top names at row 2.
|
||||||
left: 1,2,3 bottom: 4,5,6 right: 7,8,9 top: 12,11,10
|
left: 1,2,3 bottom: 4,5,6 right: 7,8,9 top: 12,11,10
|
||||||
"""
|
"""
|
||||||
cells = {
|
cells = {
|
||||||
(0, 0): "QFP-12",
|
(0, 0): "QFP-12",
|
||||||
|
# top Numbers (row 1, cols 1..3)
|
||||||
|
(1, 1): "12", (1, 2): "11", (1, 3): "10",
|
||||||
|
# top Names (row 2, cols 1..3)
|
||||||
|
(2, 1): "RST", (2, 2): "VSS", (2, 3): "VDD",
|
||||||
# left (col 0) — names at col 1
|
# left (col 0) — names at col 1
|
||||||
(1, 0): "1", (1, 1): "VCC",
|
(3, 0): "1", (3, 1): "VCC",
|
||||||
(2, 0): "2", (2, 1): "GND",
|
(4, 0): "2", (4, 1): "GND",
|
||||||
(3, 0): "3", (3, 1): "IN1",
|
(5, 0): "3", (5, 1): "IN1",
|
||||||
# bottom (row 5) — names at row 4
|
# bottom Names (row 6), Numbers (row 7)
|
||||||
(5, 1): "4", (4, 1): "IN2",
|
(6, 1): "IN2", (6, 2): "OUT1", (6, 3): "OUT2",
|
||||||
(5, 2): "5", (4, 2): "OUT1",
|
(7, 1): "4", (7, 2): "5", (7, 3): "6",
|
||||||
(5, 3): "6", (4, 3): "OUT2",
|
# right (col 4 Number, col 3 Name) — bottom to top: 7, 8, 9
|
||||||
# right (col 5) — names at col 4
|
(5, 3): "CTL1", (5, 4): "7",
|
||||||
(4, 5): "7", (4, 4): "CTL1",
|
(4, 3): "CTL2", (4, 4): "8",
|
||||||
(3, 5): "8", (3, 4): "CTL2",
|
(3, 3): "NC1", (3, 4): "9",
|
||||||
(2, 5): "9", (2, 4): "NC1",
|
|
||||||
# top (row 1) — names at row 2, cols 2-4 (avoid col 5 corner)
|
|
||||||
(1, 4): "10", (2, 4): "VDD",
|
|
||||||
(1, 3): "11", (2, 3): "VSS",
|
|
||||||
(1, 2): "12", (2, 2): "RST",
|
|
||||||
}
|
}
|
||||||
# Note: (2,4) is used as name for both pin 9 (right edge) and pin 10 (top edge).
|
|
||||||
# The name_map will have the last writer win. This is fine for the test —
|
|
||||||
# we just verify the correct number of pins and their order.
|
|
||||||
pm = parse_pinmap(cells)
|
pm = parse_pinmap(cells)
|
||||||
assert len(pm.pins) == 12, f"expected 12, got {len(pm.pins)}"
|
assert len(pm.pins) == 12, f"expected 12, got {len(pm.pins)}"
|
||||||
|
|
||||||
@@ -224,24 +229,19 @@ def test_12pin_square():
|
|||||||
# ── F012: PinMAP 生成中上/下边 PinName 位置验证 ────────────
|
# ── F012: PinMAP 生成中上/下边 PinName 位置验证 ────────────
|
||||||
|
|
||||||
def test_f012_pinname_position():
|
def test_f012_pinname_position():
|
||||||
"""验证 PinList→PinMAP 时下/上边 PinName 位置正确。
|
"""验证 PinList→PinMAP 时各边 PinName 位置正确(v1.5.5 布局)。
|
||||||
|
|
||||||
F012 要求:
|
v1.5.5 布局:
|
||||||
- 下边 Name 在 max_row-1(序号上方)
|
- 左边 Name 在 (2..rows+1, 1)
|
||||||
- 上边 Name 在 min_row+1(序号下方)
|
- 下边 Name 在 (rows+2, 1..cols) ← 倒数第二行
|
||||||
|
- 右边 Name 在 (rows+1..2, cols)
|
||||||
|
- 上边 Name 在 (0, c),即独立行,无需角点例外
|
||||||
|
|
||||||
测试策略:
|
测试策略:
|
||||||
1. 构建 5×5(20 Pin)PinList 数据
|
1. 构建 5×5(20 Pin)PinList 数据
|
||||||
2. 生成 PinMAP
|
2. 生成 PinMAP
|
||||||
3. 检查输出 cell 位置
|
3. 检查输出 cell 位置
|
||||||
4. 将生成的 PinMAP 再解析回 PinList,做往返一致性验证
|
4. 将生成的 PinMAP 再解析回 PinList,做往返一致性验证
|
||||||
|
|
||||||
注意:
|
|
||||||
- 3×3 及以上网格中,(1,1) 既是左边第 1 个引脚 (row=1) 的
|
|
||||||
Name 位置((1,0)→(1,1)),又是上边最后 1 个引脚(row=1, c=1)
|
|
||||||
序号位置。这是网格布局的固有限制,非 F012 Bug。
|
|
||||||
- 因此往返验证仅检查数量和序号正确性,不要求所有 Name 完全
|
|
||||||
一致(角点区域可能被序号覆盖)。
|
|
||||||
"""
|
"""
|
||||||
# ── 1. 构建 5×5 PinList 数据(20 个引脚) ──────────────────
|
# ── 1. 构建 5×5 PinList 数据(20 个引脚) ──────────────────
|
||||||
rows, cols = 5, 5
|
rows, cols = 5, 5
|
||||||
@@ -251,89 +251,65 @@ def test_f012_pinname_position():
|
|||||||
]
|
]
|
||||||
package_info = "QFN-20"
|
package_info = "QFN-20"
|
||||||
|
|
||||||
# ── 2. 生成 PinMAP(不使用模板,纯逻辑验证) ───────────────
|
# ── 2. 生成 PinMAP ────────────────────────────────────────
|
||||||
data = generate_pinmap(
|
data = generate_pinmap(
|
||||||
entries=entries,
|
entries=entries,
|
||||||
rows=rows,
|
rows=rows,
|
||||||
cols=cols,
|
cols=cols,
|
||||||
package_info=package_info,
|
package_info=package_info,
|
||||||
template_style=None,
|
template_style=None,
|
||||||
output_path=None, # 不写入文件
|
output_path=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
# ── 3. 检查单元格位置 ───────────────────────────────────────
|
# ── 3. 检查单元格位置 (BUG-007 fixed) ─────────────────────
|
||||||
# F012 验证:
|
# 5×5: rows=5, cols=5, 20 pins
|
||||||
# 5×5 网格坐标(0-based):
|
# 上边: Number (1, 5..1), Name (2, 1..5)
|
||||||
# min_row=1, max_row=5, min_col=0, max_col=5
|
# 左边: Number (3..7, 0), Name (3..7, 1)
|
||||||
# 预期:
|
# 下边: Name (8, 1..5), Number (9, 1..5)
|
||||||
# 左边: 序号 (r,0) Name (r,1) r ∈ [1,5]
|
# 右边: Number (7..3, 6), Name (7..3, 5)
|
||||||
# 下边: 序号 (5,c) Name (4,c) = max_row-1 c ∈ [1,5]
|
|
||||||
# 右边: 序号 (r,5) Name (r,4) r ∈ [5,1] 逆序
|
|
||||||
# 上边: 序号 (1,c) Name (2,c) = min_row+1 c ∈ [5,1] 逆序
|
|
||||||
|
|
||||||
# ── 3a. 验证下边 Name 位置 ─────────────────────────────────
|
# ── 3a. 验证上边 Name 位置 (2, 1..cols) ─────────────────
|
||||||
# 下边序号在 (5, 1..5),Name 应在 (4, 1..5) = max_row-1
|
|
||||||
for c in range(1, cols + 1):
|
for c in range(1, cols + 1):
|
||||||
num_cell = (rows, c) # (5, c)
|
num_ref = rc_to_cell_ref(1, c) # Number at row 1
|
||||||
name_cell = (rows - 1, c) # (4, c) = max_row-1
|
name_ref = rc_to_cell_ref(2, c) # Name at row 2
|
||||||
num_ref = rc_to_cell_ref(num_cell[0], num_cell[1])
|
assert num_ref in data, f"上边 Number {num_ref} 缺失"
|
||||||
name_ref = rc_to_cell_ref(name_cell[0], name_cell[1])
|
|
||||||
|
|
||||||
# 序号单元格应有值
|
|
||||||
assert num_ref in data, f"F012: 下边序号 {num_ref} 缺失"
|
|
||||||
# Name 单元格应在 max_row-1
|
|
||||||
assert name_ref in data, (
|
assert name_ref in data, (
|
||||||
f"F012: 下边 Name 应在 {name_ref} (max_row-1), "
|
f"上边 Name 应在 {name_ref} (row 2), 但未找到。Number 在 {num_ref}"
|
||||||
f"但未找到。序号在 {num_ref}"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# ── 3b. 验证上边 Name 位置 ─────────────────────────────────
|
# ── 3b. 验证下边 Name 位置 (rows+3=8, 1..cols) ──────────
|
||||||
# 上边序号在 (1, 5..1),Name 应在 (2, 5..1) = min_row+1
|
for c in range(1, cols + 1):
|
||||||
for c in range(cols, 0, -1):
|
num_ref = rc_to_cell_ref(rows + 4, c) # Number at row 9
|
||||||
num_cell = (1, c) # min_row=1
|
name_ref = rc_to_cell_ref(rows + 3, c) # Name at row 8
|
||||||
name_cell = (2, c) # min_row+1=2
|
assert num_ref in data, f"下边 Number {num_ref} 缺失"
|
||||||
num_ref = rc_to_cell_ref(num_cell[0], num_cell[1])
|
|
||||||
name_ref = rc_to_cell_ref(name_cell[0], name_cell[1])
|
|
||||||
|
|
||||||
assert num_ref in data, f"F012: 上边序号 {num_ref} 缺失"
|
|
||||||
assert name_ref in data, (
|
assert name_ref in data, (
|
||||||
f"F012: 上边 Name 应在 {name_ref} (min_row+1), "
|
f"下边 Name 应在 {name_ref} (rows+3), 但未找到。Number 在 {num_ref}"
|
||||||
f"但未找到。序号在 {num_ref}"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# ── 3c. 验证左边 Name 位置 ──────────────────────────────────
|
# ── 3c. 验证左边 Name 位置 (3..7, 1) ───────────────────
|
||||||
for r in range(1, rows + 1):
|
for r in range(3, rows + 3):
|
||||||
num_cell = (r, 0)
|
num_ref = rc_to_cell_ref(r, 0)
|
||||||
name_cell = (r, 1)
|
name_ref = rc_to_cell_ref(r, 1)
|
||||||
num_ref = rc_to_cell_ref(num_cell[0], num_cell[1])
|
assert num_ref in data, f"左边 Number {num_ref} 缺失"
|
||||||
name_ref = rc_to_cell_ref(name_cell[0], name_cell[1])
|
assert name_ref in data, f"左边 Name {name_ref} 缺失"
|
||||||
|
|
||||||
assert num_ref in data, f"F012: 左边序号 {num_ref} 缺失"
|
# ── 3d. 验证右边 Name 位置 (7..3, 5) ────────────────────
|
||||||
assert name_ref in data, f"F012: 左边 Name {name_ref} 缺失"
|
for r in range(rows + 2, 2, -1):
|
||||||
|
num_ref = rc_to_cell_ref(r, cols + 1)
|
||||||
# ── 3d. 验证右边 Name 位置 ──────────────────────────────────
|
name_ref = rc_to_cell_ref(r, cols)
|
||||||
for r in range(rows, 0, -1):
|
assert num_ref in data, f"右边 Number {num_ref} 缺失"
|
||||||
num_cell = (r, cols)
|
assert name_ref in data, f"右边 Name {name_ref} 缺失"
|
||||||
name_cell = (r, cols - 1)
|
|
||||||
num_ref = rc_to_cell_ref(num_cell[0], num_cell[1])
|
|
||||||
name_ref = rc_to_cell_ref(name_cell[0], name_cell[1])
|
|
||||||
|
|
||||||
assert num_ref in data, f"F012: 右边序号 {num_ref} 缺失"
|
|
||||||
assert name_ref in data, f"F012: 右边 Name {name_ref} 缺失"
|
|
||||||
|
|
||||||
# ── 4. 往返一致性验证(PinMAP → PinList)────────────────────
|
# ── 4. 往返一致性验证(PinMAP → PinList)────────────────────
|
||||||
from utils import cell_ref_to_rc
|
from utils import cell_ref_to_rc
|
||||||
|
|
||||||
# 将 data dict 转换为 PinMAP 解析器可读的 {(row,col): value} 格式
|
|
||||||
cell_data = {}
|
cell_data = {}
|
||||||
for ref, value in data.items():
|
for ref, value in data.items():
|
||||||
cell_data[cell_ref_to_rc(ref)] = value
|
cell_data[cell_ref_to_rc(ref)] = value
|
||||||
|
|
||||||
# 解析回 PinMAP
|
|
||||||
pm = parse_pinmap(cell_data)
|
pm = parse_pinmap(cell_data)
|
||||||
assert len(pm.pins) == 20, f"往返: 预期 20 引脚,实际 {len(pm.pins)}"
|
assert len(pm.pins) == 20, f"往返: 预期 20 引脚,实际 {len(pm.pins)}"
|
||||||
|
|
||||||
# 验证引脚序号正确(20 个引脚全部恢复)
|
|
||||||
actual_numbers = sorted([p.number for p in pm.pins])
|
actual_numbers = sorted([p.number for p in pm.pins])
|
||||||
expected_numbers = list(range(1, 21))
|
expected_numbers = list(range(1, 21))
|
||||||
assert actual_numbers == expected_numbers, (
|
assert actual_numbers == expected_numbers, (
|
||||||
@@ -342,7 +318,6 @@ def test_f012_pinname_position():
|
|||||||
f" 实际: {actual_numbers}"
|
f" 实际: {actual_numbers}"
|
||||||
)
|
)
|
||||||
|
|
||||||
# 验证 20 个引脚全部恢复
|
|
||||||
validation = validate_pinmap(pm)
|
validation = validate_pinmap(pm)
|
||||||
assert validation.is_valid, (
|
assert validation.is_valid, (
|
||||||
f"往返验证失败: 错误={[e.message for e in validation.errors]}"
|
f"往返验证失败: 错误={[e.message for e in validation.errors]}"
|
||||||
@@ -353,7 +328,6 @@ def test_f012_pinname_position():
|
|||||||
f"往返 PinList: 预期 20 行,实际 {len(pinlist.rows)}"
|
f"往返 PinList: 预期 20 行,实际 {len(pinlist.rows)}"
|
||||||
)
|
)
|
||||||
|
|
||||||
# 验证序号从 1 到 20
|
|
||||||
for i, (name, num) in enumerate(pinlist.rows):
|
for i, (name, num) in enumerate(pinlist.rows):
|
||||||
expected_num = i + 1
|
expected_num = i + 1
|
||||||
assert num == expected_num, (
|
assert num == expected_num, (
|
||||||
@@ -367,18 +341,18 @@ def test_f012_pinname_position():
|
|||||||
|
|
||||||
def test_template_path_generation():
|
def test_template_path_generation():
|
||||||
"""验证两个模板查找函数返回正确的路径格式。"""
|
"""验证两个模板查找函数返回正确的路径格式。"""
|
||||||
from main import _find_balllist_template_path, _find_ballmap_template_path
|
from main import _find_pinlist_template_path, _find_pinmap_template_path
|
||||||
|
|
||||||
result1 = _find_balllist_template_path()
|
result1 = _find_pinlist_template_path()
|
||||||
result2 = _find_ballmap_template_path()
|
result2 = _find_pinmap_template_path()
|
||||||
|
|
||||||
# 返回值要么是 str 要么是 None
|
# 返回值要么是 str 要么是 None
|
||||||
assert result1 is None or isinstance(result1, str)
|
assert result1 is None or isinstance(result1, str)
|
||||||
assert result2 is None or isinstance(result2, str)
|
assert result2 is None or isinstance(result2, str)
|
||||||
# 两者应该是不同路径
|
# 两者应该是不同路径
|
||||||
if result1 and result2:
|
if result1 and result2:
|
||||||
assert "BallList" in result1
|
assert "PinList" in result1
|
||||||
assert "BallMAP" in result2
|
assert "PinMAP" in result2
|
||||||
assert result1 != result2
|
assert result1 != result2
|
||||||
|
|
||||||
print("✓ test_template_path_generation passed")
|
print("✓ test_template_path_generation passed")
|
||||||
@@ -561,6 +535,452 @@ def test_template_color_prefix_auto_fix():
|
|||||||
print("✓ test_template_color_prefix_auto_fix passed")
|
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(用户真实布局:Number 在 Name 上方)QFN60 PinMAP → PinList。
|
||||||
|
|
||||||
|
15×15 网格,60 引脚环形布局。
|
||||||
|
布局 B:Top Number 在 row 1,Top Name 在 row 2。
|
||||||
|
Left 从 row 3 开始(避开 Top Name 行),Bottom/Right 按标准公式。
|
||||||
|
|
||||||
|
验收标准:
|
||||||
|
- 解析出 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 1, Name at row 2 (Layout B)
|
||||||
|
# 逆时针:Pin46 在右 (col 15),Pin60 在左 (col 1)
|
||||||
|
for i, c in enumerate(range(QFN60_COLS, 0, -1)):
|
||||||
|
pin_num = 46 + i
|
||||||
|
cells[(1, c)] = str(pin_num) # Top Number
|
||||||
|
cells[(2, c)] = f"Pin{pin_num}" # Top Name
|
||||||
|
|
||||||
|
# Left: Pin1..Pin15, Number at col 0, Name at col 1
|
||||||
|
# 从 row 3 开始,避免与 Top Name (row 2) 重叠
|
||||||
|
for i in range(QFN60_ROWS):
|
||||||
|
r = 3 + i
|
||||||
|
cells[(r, 0)] = str(i + 1)
|
||||||
|
cells[(r, 1)] = f"Pin{i + 1}"
|
||||||
|
|
||||||
|
# Right: Pin31..Pin45, Number at col 16, Name at col 15
|
||||||
|
# 从下往上 (row 17→3)
|
||||||
|
for i in range(QFN60_ROWS):
|
||||||
|
r = QFN60_ROWS + 2 - i # 17, 16, ..., 3
|
||||||
|
cells[(r, QFN60_COLS + 1)] = str(31 + i)
|
||||||
|
cells[(r, QFN60_COLS)] = f"Pin{31 + i}"
|
||||||
|
|
||||||
|
# Bottom: Pin16..Pin30
|
||||||
|
# Name at row 18 (rows+3?), Number at row 19 (rows+4?)
|
||||||
|
# Actually standard: Name at rows+2=17, Number at rows+3=18
|
||||||
|
# But in user layout, left runs through row 17 and bottom is below that
|
||||||
|
for i in range(QFN60_COLS):
|
||||||
|
c = 1 + i
|
||||||
|
cells[(QFN60_ROWS + 3, c)] = f"Pin{16 + i}" # Name: row 18
|
||||||
|
cells[(QFN60_ROWS + 4, c)] = str(16 + i) # Number: row 19
|
||||||
|
|
||||||
|
# ── 解析 ──────────────────────────────────────────────
|
||||||
|
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 A(v1.5.5 生成布局)QFN60 PinMAP → PinList。
|
||||||
|
|
||||||
|
验证自动检测对两种布局均正确工作。
|
||||||
|
Layout A: Top Name 在 row 0,Top 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 个)
|
||||||
|
- 标题行独立(A1 独占 row 0,不混入引脚数据)
|
||||||
|
"""
|
||||||
|
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')}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# ── 验证标题行独立(BUG-007)─────────────────────────
|
||||||
|
# A1 是唯一在 row 0 的单元格,无引脚数据混入
|
||||||
|
from utils import cell_ref_to_rc
|
||||||
|
for ref, val in data.items():
|
||||||
|
r, c = cell_ref_to_rc(ref)
|
||||||
|
if r == 0:
|
||||||
|
assert ref == "A1", (
|
||||||
|
f"row 0 应只有 A1 标题,但发现 {ref}={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 fixed layout)──────────────
|
||||||
|
# BUG-007 修复布局:
|
||||||
|
# Title: A1 (row 0 only)
|
||||||
|
# Top Numbers: (1, 1..15)
|
||||||
|
# Top Names: (2, 1..15)
|
||||||
|
# Left: Number (3..17, 0), Name (3..17, 1)
|
||||||
|
# Bottom: Name (18, 1..15), Number (19, 1..15)
|
||||||
|
# Right: Number (17..3, 16), Name (17..3, 15)
|
||||||
|
|
||||||
|
# Top Numbers 在 row 1
|
||||||
|
for c in range(1, QFN60_COLS + 1):
|
||||||
|
ref = rc_to_cell_ref(1, c)
|
||||||
|
assert ref in data, f"Top Number {ref} 缺失"
|
||||||
|
|
||||||
|
# Top Names 在 row 2
|
||||||
|
for c in range(1, QFN60_COLS + 1):
|
||||||
|
ref = rc_to_cell_ref(2, 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 3..17
|
||||||
|
for r in range(3, QFN60_ROWS + 3):
|
||||||
|
ref = rc_to_cell_ref(r, 0)
|
||||||
|
assert ref in data, f"Left Number {ref} 缺失"
|
||||||
|
|
||||||
|
# Left Names 在 col 1, rows 3..17
|
||||||
|
for r in range(3, QFN60_ROWS + 3):
|
||||||
|
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 18
|
||||||
|
for c in range(1, QFN60_COLS + 1):
|
||||||
|
ref = rc_to_cell_ref(QFN60_ROWS + 3, c)
|
||||||
|
assert ref in data, f"Bottom Name {ref} 缺失"
|
||||||
|
assert data[ref].startswith("Pin"), f"Bottom Name {ref} = {data[ref]}"
|
||||||
|
|
||||||
|
# Bottom Numbers 在 row 19
|
||||||
|
for c in range(1, QFN60_COLS + 1):
|
||||||
|
ref = rc_to_cell_ref(QFN60_ROWS + 4, c)
|
||||||
|
assert ref in data, f"Bottom Number {ref} 缺失"
|
||||||
|
|
||||||
|
# Right Numbers 在 col 16, rows 17..3
|
||||||
|
for r in range(QFN60_ROWS + 2, 2, -1):
|
||||||
|
ref = rc_to_cell_ref(r, QFN60_COLS + 1)
|
||||||
|
assert ref in data, f"Right Number {ref} 缺失"
|
||||||
|
|
||||||
|
# Right Names 在 col 15, rows 17..3
|
||||||
|
for r in range(QFN60_ROWS + 2, 2, -1):
|
||||||
|
ref = rc_to_cell_ref(r, QFN60_COLS)
|
||||||
|
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 PinMAP(Layout 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, 0, -1)):
|
||||||
|
pin_num = 46 + i
|
||||||
|
cells[(1, c)] = str(pin_num)
|
||||||
|
cells[(2, c)] = f"Pin{pin_num}"
|
||||||
|
|
||||||
|
for i in range(QFN60_ROWS):
|
||||||
|
r = 3 + i
|
||||||
|
cells[(r, 0)] = str(i + 1)
|
||||||
|
cells[(r, 1)] = f"Pin{i + 1}"
|
||||||
|
|
||||||
|
for i in range(QFN60_ROWS):
|
||||||
|
r = QFN60_ROWS + 2 - i
|
||||||
|
cells[(r, QFN60_COLS + 1)] = str(31 + i)
|
||||||
|
cells[(r, QFN60_COLS)] = f"Pin{31 + i}"
|
||||||
|
|
||||||
|
for i in range(QFN60_COLS):
|
||||||
|
c = 1 + i
|
||||||
|
cells[(QFN60_ROWS + 3, c)] = f"Pin{16 + i}"
|
||||||
|
cells[(QFN60_ROWS + 4, 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():
|
def test_template_no_styles_xml():
|
||||||
"""边界测试:缺失 styles.xml 时优雅降级。"""
|
"""边界测试:缺失 styles.xml 时优雅降级。"""
|
||||||
from template_reader import read_template_styles
|
from template_reader import read_template_styles
|
||||||
@@ -604,4 +1024,10 @@ if __name__ == "__main__":
|
|||||||
test_template_empty_fonts_fallback()
|
test_template_empty_fonts_fallback()
|
||||||
test_template_color_prefix_auto_fix()
|
test_template_color_prefix_auto_fix()
|
||||||
test_template_no_styles_xml()
|
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!")
|
print("\n✅ All tests passed!")
|
||||||
|
|||||||
19
README.md
19
README.md
@@ -48,6 +48,25 @@ pinmap-to-pinlist/
|
|||||||
- openpyxl(.xlsx 读写)
|
- openpyxl(.xlsx 读写)
|
||||||
- 自定义 BIFF8 引擎(.xls 解析)
|
- 自定义 BIFF8 引擎(.xls 解析)
|
||||||
|
|
||||||
|
## 版本历史
|
||||||
|
|
||||||
|
### v1.5.4 (2026-06-09) — Bug 修复版本
|
||||||
|
|
||||||
|
- **BUG-005**: 模板文件名修正 — `BallList-Template.xlsx` → `PinList-Template.xlsx`,`BallMAP-Template.xlsx` → `PinMAP-Template.xlsx`
|
||||||
|
- **BUG-006**: 布局重设计 — Number 外侧(第 1 圈)+ Name 里侧(第 2 圈),彻底解决单元格冲突问题
|
||||||
|
- 上边:Number row 1,Name row 2(角点例外)
|
||||||
|
- 左边:Number col 0,Name col 1
|
||||||
|
- 下边:Number row rows+3,Name row rows+2
|
||||||
|
- 右边:Number col cols+1,Name col cols
|
||||||
|
- Pin1 保持在左上角(A3=1, B3=Pin1)
|
||||||
|
- 18/18 单元测试 + 37/37 集成测试全部通过
|
||||||
|
|
||||||
|
### v1.5.0 (2026-06-06) — 模板分离与格式提取
|
||||||
|
|
||||||
|
- MAP→List 使用 `PinList-Template.xlsx`(旧名 `BallList-Template.xlsx`)
|
||||||
|
- List→MAP 使用 `PinMAP-Template.xlsx`(旧名 `BallMAP-Template.xlsx`)
|
||||||
|
- 模板格式提取:字体、边框、填充、对齐、列宽、行高
|
||||||
|
|
||||||
## 许可证
|
## 许可证
|
||||||
|
|
||||||
内部项目
|
内部项目
|
||||||
|
|||||||
60
Releases/v1.5.4/CHANGELOG.md
Normal file
60
Releases/v1.5.4/CHANGELOG.md
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
# Changelog — v1.5.4
|
||||||
|
|
||||||
|
> **发布日期**: 2026-06-09
|
||||||
|
> **版本类型**: Bug 修复版本
|
||||||
|
|
||||||
|
## 🐛 Bug 修复
|
||||||
|
|
||||||
|
### BUG-005 【高】模板文件名错误
|
||||||
|
|
||||||
|
**问题**: `main.py` 中引用的模板文件名(`BallList-Template.xlsx` 和 `BallMAP-Template.xlsx`)与用户期望的文件名不匹配。
|
||||||
|
|
||||||
|
**修复**:
|
||||||
|
- 模板文件重命名:`BallList-Template.xlsx` → `PinList-Template.xlsx`
|
||||||
|
- 模板文件重命名:`BallMAP-Template.xlsx` → `PinMAP-Template.xlsx`
|
||||||
|
- 同步更新 `main.py` 中的函数名和模板引用路径
|
||||||
|
|
||||||
|
### BUG-006 【高】布局重设计(Number 外侧 + Name 里侧)
|
||||||
|
|
||||||
|
**问题**: PinList→PinMAP→PinList 双向转换中,v1.3 的"紧致布局"导致 Number 与 Name 单元格冲突(6 处),15×15 网格下序号 1 错位到 A2,序号 16 错位到 B16。
|
||||||
|
|
||||||
|
**根本原因**: 旧布局将 Name 放在 Number 向内偏移一行/一列的位置,边角处发生冲突。
|
||||||
|
|
||||||
|
**修复方案**: 重新设计布局为 **Number 外侧(第 1 圈)+ Name 里侧(第 2 圈)**,从网格边界往中心排列:
|
||||||
|
|
||||||
|
| 边 | 外侧(第 1 圈) | 内侧(第 2 圈) |
|
||||||
|
|---|---|---|
|
||||||
|
| **上边** | Number 在 row 1(最顶行) | Name 在 row 2(第二行;角点例外在 row 1) |
|
||||||
|
| **左边** | Number 在 col 0(最左列) | Name 在 col 1(第二列) |
|
||||||
|
| **下边** | Number 在 row rows+3(最底行) | Name 在 row rows+2(倒数第二行) |
|
||||||
|
| **右边** | Number 在 col cols+1(最右列) | Name 在 col cols(右二列) |
|
||||||
|
|
||||||
|
**关键设计点**:
|
||||||
|
- **上边角点例外**: 最左/最右上边 Name 无法放在 row 2(被左/右边 Name 占用),分别使用 `(1, 0)` 和 `(1, cols+1)` 例外单元格
|
||||||
|
- Pin1 保持在左上角(A3=1, B3=Pin1)
|
||||||
|
- 不再需要角点 `"//"` 合并 — 每条边不共享任何单元格
|
||||||
|
- 周长公式 `(rows+cols)×2` 保持不变
|
||||||
|
|
||||||
|
**验证**:
|
||||||
|
- ✅ 15 种网格大小(4×4, 15×15, 3×5, 2×2, 8×8, 10×12, 20×20, 5×3, 6×7, 2×3, 3×3, 2×4, 3×2, 4×2, 2×5)全部无冲突
|
||||||
|
- ✅ 18/18 单元测试通过
|
||||||
|
- ✅ 37/37 集成测试通过
|
||||||
|
|
||||||
|
## 🔧 修改文件
|
||||||
|
|
||||||
|
| 文件 | 修改内容 |
|
||||||
|
|------|---------|
|
||||||
|
| `Code/src/main.py` | BUG-005: 模板函数和引用改名;BUG-006: 传递 cols 参数 |
|
||||||
|
| `Code/src/pinmap_layout.py` | BUG-006: 重写坐标公式 + `get_name_cell()` 支持 cols 参数 + 角点例外 |
|
||||||
|
| `Code/src/pinmap_generator.py` | BUG-006: 传递 cols 参数 + 更新注释 |
|
||||||
|
| `Code/src/pinmap_parser.py` | BUG-006: 重写边界检测、Name 读取(角点例外检测) |
|
||||||
|
| `Code/src/test_pinmap.py` | BUG-006: 更新测试数据适配新布局 |
|
||||||
|
| `Test/fixtures/PinList-Template.xlsx` | BUG-005: 模板文件重命名 |
|
||||||
|
| `Test/fixtures/PinMAP-Template.xlsx` | BUG-005: 模板文件重命名 |
|
||||||
|
|
||||||
|
## 📝 文档
|
||||||
|
|
||||||
|
- 更新 `CHANGELOG.md` 追加 v1.5.4 版本日志
|
||||||
|
- 更新 `README.md` 追加 v1.5.4 版本说明
|
||||||
|
- 生成 `Releases/v1.5.4/CHANGELOG.md`
|
||||||
|
- 更新 `docs/bugs.md` BUG-005、BUG-006 状态为已修复
|
||||||
BIN
Test/fixtures/PinList-Template.xlsx
vendored
Normal file
BIN
Test/fixtures/PinList-Template.xlsx
vendored
Normal file
Binary file not shown.
BIN
Test/fixtures/PinMAP-Template.xlsx
vendored
Normal file
BIN
Test/fixtures/PinMAP-Template.xlsx
vendored
Normal file
Binary file not shown.
BIN
Test/fixtures/sample_4x4.xlsx
vendored
BIN
Test/fixtures/sample_4x4.xlsx
vendored
Binary file not shown.
@@ -84,19 +84,27 @@ def create_pinmap_fixture(data: dict, path: str):
|
|||||||
def test_map_to_list(r: TestRunner):
|
def test_map_to_list(r: TestRunner):
|
||||||
fixture_dir = os.path.join(os.path.dirname(__file__), 'fixtures')
|
fixture_dir = os.path.join(os.path.dirname(__file__), 'fixtures')
|
||||||
|
|
||||||
# TC-MAP-001: 标准 4x4 PinMAP 转换
|
# TC-MAP-001: 标准 4x4 PinMAP 转换 (v1.5.4 布局)
|
||||||
def _tc_map_001(result):
|
def _tc_map_001(result):
|
||||||
filepath = os.path.join(fixture_dir, 'sample_4x4.xlsx')
|
filepath = os.path.join(fixture_dir, 'sample_4x4.xlsx')
|
||||||
cells = read_xlsx_cells(filepath)
|
cells = read_xlsx_cells(filepath)
|
||||||
pinmap = parse_pinmap(cells)
|
pinmap = parse_pinmap(cells)
|
||||||
validation = validate_pinmap(pinmap)
|
validation = validate_pinmap(pinmap)
|
||||||
pinlist = generate_pinlist(pinmap, validation)
|
pinlist = generate_pinlist(pinmap, validation)
|
||||||
assert pinlist.package_info, "package_info 不应为空"
|
# 封装信息 (v1.5.4 布局)
|
||||||
assert len(pinlist.rows) > 0, "应有引脚数据"
|
assert pinlist.package_info == "QFP-16", f"封装应为 QFP-16,实际: {pinlist.package_info}"
|
||||||
|
# 引脚数 (4x4 网格: (4+4)*2 = 16)
|
||||||
|
assert len(pinlist.rows) == 16, f"应有 16 个引脚,实际: {len(pinlist.rows)}"
|
||||||
# 验证递增排序
|
# 验证递增排序
|
||||||
nums = [num for _, num in pinlist.rows]
|
nums = [num for _, num in pinlist.rows]
|
||||||
assert nums == sorted(nums), f"序号应递增,实际: {nums}"
|
assert nums == sorted(nums), f"序号应递增,实际: {nums}"
|
||||||
result.ok(f"封装={pinlist.package_info}, Pin数={len(pinlist.rows)}, 序号递增")
|
assert nums == list(range(1, 17)), f"序号应为 1-16,实际: {nums}"
|
||||||
|
# 验证引脚名不是数字(确保 Name/Number 未错位)
|
||||||
|
names = [name for name, _ in pinlist.rows]
|
||||||
|
for name in names:
|
||||||
|
assert not name.isdigit(), f"引脚名 '{name}' 不应为纯数字"
|
||||||
|
assert all(name.startswith("Pin") for name in names), f"所有引脚名应以 Pin 开头: {names}"
|
||||||
|
result.ok(f"封装={pinlist.package_info}, Pin数={len(pinlist.rows)}, 序号 1-16, 引脚名=Pin1..Pin16")
|
||||||
|
|
||||||
r.run("TC-MAP-001: 标准4x4 PinMAP转换", _tc_map_001)
|
r.run("TC-MAP-001: 标准4x4 PinMAP转换", _tc_map_001)
|
||||||
|
|
||||||
@@ -570,19 +578,19 @@ def test_v15_styles(r: TestRunner):
|
|||||||
from xlsx_writer import write_xlsx_with_style
|
from xlsx_writer import write_xlsx_with_style
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# ── TC-v1.5-001: MAP→List 加载 BallList 模板 ──
|
# ── TC-v1.5-001: MAP→List 加载 PinList 模板 ──
|
||||||
def _tc_v15_001(result):
|
def _tc_v15_001(result):
|
||||||
template_path = os.path.join(fixture_dir, 'BallList-Template.xlsx')
|
template_path = os.path.join(fixture_dir, 'PinList-Template.xlsx')
|
||||||
assert os.path.exists(template_path), f"BallList 模板文件不存在: {template_path}"
|
assert os.path.exists(template_path), f"PinList 模板文件不存在: {template_path}"
|
||||||
|
|
||||||
style = read_template_styles(template_path)
|
style = read_template_styles(template_path)
|
||||||
assert style is not None, "BallList 模板样式应成功读取"
|
assert style is not None, "PinList 模板样式应成功读取"
|
||||||
assert len(style.fonts) > 0, "应有字体定义"
|
assert len(style.fonts) > 0, "应有字体定义"
|
||||||
assert len(style.borders) > 0, "应有边框定义"
|
assert len(style.borders) > 0, "应有边框定义"
|
||||||
assert 0 in style.column_widths, "应有列宽定义"
|
assert 0 in style.column_widths, "应有列宽定义"
|
||||||
result.ok(f"模板加载成功: fonts={len(style.fonts)}, borders={len(style.borders)}, width_A={style.column_widths.get(0)}")
|
result.ok(f"模板加载成功: fonts={len(style.fonts)}, borders={len(style.borders)}, width_A={style.column_widths.get(0)}")
|
||||||
|
|
||||||
r.run("TC-v1.5-001: MAP->List 加载 BallList 模板", _tc_v15_001)
|
r.run("TC-v1.5-001: MAP->List 加载 PinList 模板", _tc_v15_001)
|
||||||
|
|
||||||
# ── TC-v1.5-002: MAP→List 无模板降级 ──
|
# ── TC-v1.5-002: MAP→List 无模板降级 ──
|
||||||
def _tc_v15_002(result):
|
def _tc_v15_002(result):
|
||||||
@@ -592,19 +600,19 @@ def test_v15_styles(r: TestRunner):
|
|||||||
|
|
||||||
r.run("TC-v1.5-002: MAP->List 无模板降级", _tc_v15_002)
|
r.run("TC-v1.5-002: MAP->List 无模板降级", _tc_v15_002)
|
||||||
|
|
||||||
# ── TC-v1.5-003: List→MAP 加载 BallMAP 模板 ──
|
# ── TC-v1.5-003: List→MAP 加载 PinMAP 模板 ──
|
||||||
def _tc_v15_003(result):
|
def _tc_v15_003(result):
|
||||||
template_path = os.path.join(fixture_dir, 'BallMAP-Template.xlsx')
|
template_path = os.path.join(fixture_dir, 'PinMAP-Template.xlsx')
|
||||||
assert os.path.exists(template_path), f"BallMAP 模板文件不存在: {template_path}"
|
assert os.path.exists(template_path), f"PinMAP 模板文件不存在: {template_path}"
|
||||||
|
|
||||||
style = read_template_styles(template_path)
|
style = read_template_styles(template_path)
|
||||||
assert style is not None, "BallMAP 模板样式应成功读取"
|
assert style is not None, "PinMAP 模板样式应成功读取"
|
||||||
assert len(style.fonts) > 0, "应有字体定义"
|
assert len(style.fonts) > 0, "应有字体定义"
|
||||||
assert len(style.borders) > 0, "应有边框定义"
|
assert len(style.borders) > 0, "应有边框定义"
|
||||||
assert 0 in style.row_heights, "应有行高定义"
|
assert 0 in style.row_heights, "应有行高定义"
|
||||||
result.ok(f"模板加载成功: fonts={len(style.fonts)}, borders={len(style.borders)}, row_height={style.row_heights.get(0)}")
|
result.ok(f"模板加载成功: fonts={len(style.fonts)}, borders={len(style.borders)}, row_height={style.row_heights.get(0)}")
|
||||||
|
|
||||||
r.run("TC-v1.5-003: List->MAP 加载 BallMAP 模板", _tc_v15_003)
|
r.run("TC-v1.5-003: List->MAP 加载 PinMAP 模板", _tc_v15_003)
|
||||||
|
|
||||||
# ── TC-v1.5-004: List→MAP 无模板降级 ──
|
# ── TC-v1.5-004: List→MAP 无模板降级 ──
|
||||||
def _tc_v15_004(result):
|
def _tc_v15_004(result):
|
||||||
@@ -616,19 +624,19 @@ def test_v15_styles(r: TestRunner):
|
|||||||
|
|
||||||
# ── TC-v1.5-005: 两个方向独立使用各自模板 ──
|
# ── TC-v1.5-005: 两个方向独立使用各自模板 ──
|
||||||
def _tc_v15_005(result):
|
def _tc_v15_005(result):
|
||||||
bl_path = os.path.join(fixture_dir, 'BallList-Template.xlsx')
|
bl_path = os.path.join(fixture_dir, 'PinList-Template.xlsx')
|
||||||
bm_path = os.path.join(fixture_dir, 'BallMAP-Template.xlsx')
|
bm_path = os.path.join(fixture_dir, 'PinMAP-Template.xlsx')
|
||||||
|
|
||||||
style_bl = read_template_styles(bl_path)
|
style_bl = read_template_styles(bl_path)
|
||||||
style_bm = read_template_styles(bm_path)
|
style_bm = read_template_styles(bm_path)
|
||||||
|
|
||||||
assert style_bl is not None, "BallList 模板应成功加载"
|
assert style_bl is not None, "PinList 模板应成功加载"
|
||||||
assert style_bm is not None, "BallMAP 模板应成功加载"
|
assert style_bm is not None, "PinMAP 模板应成功加载"
|
||||||
|
|
||||||
# BallList 有列宽,BallMAP 有行高
|
# PinList 有列宽,PinMAP 有行高
|
||||||
assert 0 in style_bl.column_widths, "BallList 应有列宽"
|
assert 0 in style_bl.column_widths, "PinList 应有列宽"
|
||||||
assert 0 in style_bm.row_heights, "BallMAP 应有行高"
|
assert 0 in style_bm.row_heights, "PinMAP 应有行高"
|
||||||
result.ok(f"两个模板独立: BL fonts={len(style_bl.fonts)}, BM fonts={len(style_bm.fonts)}")
|
result.ok(f"两个模板独立: PL fonts={len(style_bl.fonts)}, PM fonts={len(style_bm.fonts)}")
|
||||||
|
|
||||||
r.run("TC-v1.5-005: 两个方向独立使用各自模板", _tc_v15_005)
|
r.run("TC-v1.5-005: 两个方向独立使用各自模板", _tc_v15_005)
|
||||||
|
|
||||||
@@ -645,7 +653,7 @@ def test_v15_styles(r: TestRunner):
|
|||||||
|
|
||||||
# ── TC-v1.5-007: 模板字体应用到输出文件 ──
|
# ── TC-v1.5-007: 模板字体应用到输出文件 ──
|
||||||
def _tc_v15_007(result):
|
def _tc_v15_007(result):
|
||||||
template_path = os.path.join(fixture_dir, 'BallMAP-Template.xlsx')
|
template_path = os.path.join(fixture_dir, 'PinMAP-Template.xlsx')
|
||||||
style = read_template_styles(template_path)
|
style = read_template_styles(template_path)
|
||||||
|
|
||||||
assert style is not None, "模板样式应成功读取"
|
assert style is not None, "模板样式应成功读取"
|
||||||
@@ -669,7 +677,7 @@ def test_v15_styles(r: TestRunner):
|
|||||||
|
|
||||||
# ── TC-v1.5-008: 模板列宽应用到输出文件 ──
|
# ── TC-v1.5-008: 模板列宽应用到输出文件 ──
|
||||||
def _tc_v15_008(result):
|
def _tc_v15_008(result):
|
||||||
template_path = os.path.join(fixture_dir, 'BallList-Template.xlsx')
|
template_path = os.path.join(fixture_dir, 'PinList-Template.xlsx')
|
||||||
style = read_template_styles(template_path)
|
style = read_template_styles(template_path)
|
||||||
assert style is not None, "模板样式应成功读取"
|
assert style is not None, "模板样式应成功读取"
|
||||||
|
|
||||||
@@ -700,7 +708,7 @@ def test_v15_styles(r: TestRunner):
|
|||||||
|
|
||||||
# ── TC-v1.5-009: 模板行高应用到输出文件 ──
|
# ── TC-v1.5-009: 模板行高应用到输出文件 ──
|
||||||
def _tc_v15_009(result):
|
def _tc_v15_009(result):
|
||||||
template_path = os.path.join(fixture_dir, 'BallMAP-Template.xlsx')
|
template_path = os.path.join(fixture_dir, 'PinMAP-Template.xlsx')
|
||||||
style = read_template_styles(template_path)
|
style = read_template_styles(template_path)
|
||||||
assert style is not None, "模板样式应成功读取"
|
assert style is not None, "模板样式应成功读取"
|
||||||
|
|
||||||
@@ -732,19 +740,19 @@ def test_v15_styles(r: TestRunner):
|
|||||||
|
|
||||||
# ── TC-v1.5-010: 两个方向使用不同模板各自的格式 ──
|
# ── TC-v1.5-010: 两个方向使用不同模板各自的格式 ──
|
||||||
def _tc_v15_010(result):
|
def _tc_v15_010(result):
|
||||||
bl_path = os.path.join(fixture_dir, 'BallList-Template.xlsx')
|
bl_path = os.path.join(fixture_dir, 'PinList-Template.xlsx')
|
||||||
bm_path = os.path.join(fixture_dir, 'BallMAP-Template.xlsx')
|
bm_path = os.path.join(fixture_dir, 'PinMAP-Template.xlsx')
|
||||||
|
|
||||||
style_bl = read_template_styles(bl_path)
|
style_bl = read_template_styles(bl_path)
|
||||||
style_bm = read_template_styles(bm_path)
|
style_bm = read_template_styles(bm_path)
|
||||||
assert style_bl and style_bm, "两个模板都应该成功加载"
|
assert style_bl and style_bm, "两个模板都应该成功加载"
|
||||||
|
|
||||||
# MAP->List 方向:用 BallList 模板
|
# MAP->List 方向:用 PinList 模板
|
||||||
pinlist_data = {'A1': 'QFP-44', 'A2': 'Pin1', 'B2': '1'}
|
pinlist_data = {'A1': 'QFP-44', 'A2': 'Pin1', 'B2': '1'}
|
||||||
pinlist_path = os.path.join(tmpdir, 'v15_010_pinlist.xlsx')
|
pinlist_path = os.path.join(tmpdir, 'v15_010_pinlist.xlsx')
|
||||||
write_xlsx_with_style(pinlist_data, pinlist_path, style_bl)
|
write_xlsx_with_style(pinlist_data, pinlist_path, style_bl)
|
||||||
|
|
||||||
# List->MAP 方向:用 BallMAP 模板
|
# List->MAP 方向:用 PinMAP 模板
|
||||||
entries = [PinListEntry(number=i+1, name=f"PIN{i+1:02d}") for i in range(12)]
|
entries = [PinListEntry(number=i+1, name=f"PIN{i+1:02d}") for i in range(12)]
|
||||||
pinmap_path = os.path.join(tmpdir, 'v15_010_pinmap.xlsx')
|
pinmap_path = os.path.join(tmpdir, 'v15_010_pinmap.xlsx')
|
||||||
generate_pinmap(entries, 3, 3, "QFP-12", template_style=style_bm, output_path=pinmap_path)
|
generate_pinmap(entries, 3, 3, "QFP-12", template_style=style_bm, output_path=pinmap_path)
|
||||||
@@ -755,17 +763,17 @@ def test_v15_styles(r: TestRunner):
|
|||||||
with zipfile.ZipFile(pinmap_path, 'r') as zf:
|
with zipfile.ZipFile(pinmap_path, 'r') as zf:
|
||||||
pm_styles = zf.read('xl/styles.xml').decode('utf-8')
|
pm_styles = zf.read('xl/styles.xml').decode('utf-8')
|
||||||
|
|
||||||
assert '楷体' in pl_styles, "BallList 输出应包含楷体"
|
assert '楷体' in pl_styles, "PinList 输出应包含楷体"
|
||||||
assert '宋体' in pm_styles, "BallMAP 输出应包含宋体"
|
assert '宋体' in pm_styles, "PinMAP 输出应包含宋体"
|
||||||
|
|
||||||
result.ok("两个方向输出字体不同: BL->楷体, BM->宋体")
|
result.ok("两个方向输出字体不同: PinList->楷体, PinMAP->宋体")
|
||||||
|
|
||||||
r.run("TC-v1.5-010: 两个方向不同模板各自的格式", _tc_v15_010)
|
r.run("TC-v1.5-010: 两个方向不同模板各自的格式", _tc_v15_010)
|
||||||
|
|
||||||
# ── TC-v1.5-011: 完整往返+模板隔离 ──
|
# ── TC-v1.5-011: 完整往返+模板隔离 (4×4 网格) ──
|
||||||
def _tc_v15_011(result):
|
def _tc_v15_011(result):
|
||||||
bl_path = os.path.join(fixture_dir, 'BallList-Template.xlsx')
|
bl_path = os.path.join(fixture_dir, 'PinList-Template.xlsx')
|
||||||
bm_path = os.path.join(fixture_dir, 'BallMAP-Template.xlsx')
|
bm_path = os.path.join(fixture_dir, 'PinMAP-Template.xlsx')
|
||||||
|
|
||||||
style_bl = read_template_styles(bl_path)
|
style_bl = read_template_styles(bl_path)
|
||||||
style_bm = read_template_styles(bm_path)
|
style_bm = read_template_styles(bm_path)
|
||||||
@@ -786,7 +794,8 @@ def test_v15_styles(r: TestRunner):
|
|||||||
|
|
||||||
pkg2, entries2 = parse_pinlist(pinlist_path)
|
pkg2, entries2 = parse_pinlist(pinlist_path)
|
||||||
pinmap_path = os.path.join(tmpdir, 'v15_011_pinmap.xlsx')
|
pinmap_path = os.path.join(tmpdir, 'v15_011_pinmap.xlsx')
|
||||||
generate_pinmap(entries2, 3, 3, pkg2, template_style=style_bm, output_path=pinmap_path)
|
# 4×4 网格: (4+4)×2 = 16 引脚
|
||||||
|
generate_pinmap(entries2, 4, 4, pkg2, template_style=style_bm, output_path=pinmap_path)
|
||||||
|
|
||||||
rt_cells = read_xlsx_cells(pinmap_path)
|
rt_cells = read_xlsx_cells(pinmap_path)
|
||||||
rt_pinmap = parse_pinmap(rt_cells)
|
rt_pinmap = parse_pinmap(rt_cells)
|
||||||
@@ -800,8 +809,8 @@ def test_v15_styles(r: TestRunner):
|
|||||||
pl_xml = zf.read('xl/styles.xml').decode('utf-8')
|
pl_xml = zf.read('xl/styles.xml').decode('utf-8')
|
||||||
with zipfile.ZipFile(pinmap_path, 'r') as zf:
|
with zipfile.ZipFile(pinmap_path, 'r') as zf:
|
||||||
pm_xml = zf.read('xl/styles.xml').decode('utf-8')
|
pm_xml = zf.read('xl/styles.xml').decode('utf-8')
|
||||||
assert '楷体' in pl_xml, "中间 PinList 应使用 BallList 的楷体"
|
assert '楷体' in pl_xml, "中间 PinList 应使用 PinList 模板的楷体"
|
||||||
assert '宋体' in pm_xml, "最终 PinMAP 应使用 BallMAP 的宋体"
|
assert '宋体' in pm_xml, "最终 PinMAP 应使用 PinMAP 模板的宋体"
|
||||||
|
|
||||||
result.ok(f"往返成功: {len(pinmap.pins)} pins, 楷体->PinList, 宋体->PinMAP")
|
result.ok(f"往返成功: {len(pinmap.pins)} pins, 楷体->PinList, 宋体->PinMAP")
|
||||||
|
|
||||||
@@ -828,7 +837,8 @@ def test_v15_styles(r: TestRunner):
|
|||||||
|
|
||||||
pkg2, entries2 = parse_pinlist(pinlist_path)
|
pkg2, entries2 = parse_pinlist(pinlist_path)
|
||||||
pinmap_path = os.path.join(tmpdir, 'v15_012_pinmap.xlsx')
|
pinmap_path = os.path.join(tmpdir, 'v15_012_pinmap.xlsx')
|
||||||
generate_pinmap(entries2, 3, 3, pkg2, template_style=None, output_path=pinmap_path)
|
# sample_4x4 有 16 pins,需用 4×4 网格
|
||||||
|
generate_pinmap(entries2, 4, 4, pkg2, template_style=None, output_path=pinmap_path)
|
||||||
assert os.path.exists(pinmap_path), "PinMAP 输出文件应存在"
|
assert os.path.exists(pinmap_path), "PinMAP 输出文件应存在"
|
||||||
|
|
||||||
result.ok("无模板完整流程正常")
|
result.ok("无模板完整流程正常")
|
||||||
|
|||||||
@@ -1,51 +1,19 @@
|
|||||||
# PinMAP ↔ PinList 双向转换器 测试报告 (v1.5.0)
|
# PinMAP ↔ PinList 双向转换器 测试报告
|
||||||
|
|
||||||
> **版本**: v1.5.0
|
> **日期**: 2026-06-12
|
||||||
> **日期**: 2026-06-06
|
> **测试类型**: 集成测试 + 端到端测试
|
||||||
> **测试类型**: 单元测试 + 集成测试 + 端到端测试
|
|
||||||
> **测试环境**: Python 3.x, Linux x64
|
> **测试环境**: Python 3.x, Linux x64
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## v1.5.0 变更覆盖
|
|
||||||
|
|
||||||
v1.5.0 引入三项核心变更:
|
|
||||||
- **F009**: MAP→List 使用 BallList-Template.xlsx(独立模板)
|
|
||||||
- **F010**: List→MAP 使用 BallMAP-Template.xlsx(独立模板)
|
|
||||||
- **F011**: 模板格式提取式应用(字体/边框/填充/对齐/列宽/行高)
|
|
||||||
- **F012**: PinName 位置确认(bottom=max_row-1, top=min_row+1)
|
|
||||||
|
|
||||||
## 测试覆盖矩阵
|
|
||||||
|
|
||||||
| 特性 | 单元测试 | 集成测试 | 状态 |
|
|
||||||
|------|---------|---------|------|
|
|
||||||
| F009 — BallList 模板加载 | ✅ `test_template_path_generation` | ✅ TC-v1.5-001/002/005 | ✅ |
|
|
||||||
| F010 — BallMAP 模板加载 | ✅ `test_template_path_generation` | ✅ TC-v1.5-003/004/005 | ✅ |
|
|
||||||
| F011 — 模板字体应用 | ✅ `test_f011_template_fonts_in_styles_xml` | ✅ TC-v1.5-007/010/013 | ✅ |
|
|
||||||
| F011 — 模板边框应用 | ✅ `test_f011_template_borders_in_styles_xml` | ✅ TC-v1.5-007/010 | ✅ |
|
|
||||||
| F011 — 模板填充应用 | ✅ `test_f011_template_fills_in_styles_xml` | ✅ TC-v1.5-010 | ✅ |
|
|
||||||
| F011 — 默认样式降级 | ✅ `test_f011_default_styles_xml` | ✅ TC-v1.5-002/004/012 | ✅ |
|
|
||||||
| F011 — 输出 dim 由 Pin 决定 | ✅ `test_f011_output_dims_determined_by_pins` | ✅ TC-v1.5-014 | ✅ |
|
|
||||||
| F011 — 列宽应用 | — | ✅ TC-v1.5-008/014 | ✅ |
|
|
||||||
| F011 — 行高应用 | — | ✅ TC-v1.5-009 | ✅ |
|
|
||||||
| F012 — PinName 位置 | ✅ `test_f012_pinname_position` | — | ✅ |
|
|
||||||
| 损坏模板优雅降级 | — | ✅ TC-v1.5-006 | ✅ |
|
|
||||||
| 极简模板 | — | ✅ TC-v1.5-013 | ✅ |
|
|
||||||
| 无模板完整流程 | — | ✅ TC-v1.5-012 | ✅ |
|
|
||||||
| 完整往返+模板隔离 | — | ✅ TC-v1.5-011 | ✅ |
|
|
||||||
| 空 fonts/样式回退 | ✅ `test_template_empty_fonts_fallback` | — | ✅ |
|
|
||||||
| FF 颜色前缀补全 | ✅ `test_template_color_prefix_auto_fix` | — | ✅ |
|
|
||||||
| 缺失 styles.xml 降级 | ✅ `test_template_no_styles_xml` | — | ✅ |
|
|
||||||
|
|
||||||
## 测试概览
|
## 测试概览
|
||||||
|
|
||||||
| 类别 | 用例数 | 通过 | 失败 |
|
| 类别 | 用例数 | 通过 | 失败 |
|
||||||
|------|--------|------|------|
|
|------|--------|------|------|
|
||||||
| 单元测试 (test_pinmap.py) | **18** | **18** | **0** |
|
|
||||||
| MAP->List 回归 | 6 | 6 | 0 |
|
| MAP->List 回归 | 6 | 6 | 0 |
|
||||||
| List->MAP 新增 | 17 | 17 | 0 |
|
| List->MAP 新增 | 17 | 17 | 0 |
|
||||||
| v1.5 模板/样式集成 | 14 | 14 | 0 |
|
| v1.5 模板/样式集成 | 14 | 14 | 0 |
|
||||||
| **总计** | **55** | **55** | **0** |
|
| **总计** | **37** | **37** | **0** |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -53,7 +21,7 @@ v1.5.0 引入三项核心变更:
|
|||||||
|
|
||||||
### TC-MAP-001: 标准4x4 PinMAP转换
|
### TC-MAP-001: 标准4x4 PinMAP转换
|
||||||
- **结果**: ✅ 通过
|
- **结果**: ✅ 通过
|
||||||
- **详情**: 封装=QFP12, Pin数=12, 序号递增
|
- **详情**: 封装=QFP-16, Pin数=16, 序号 1-16, 引脚名=Pin1..Pin16
|
||||||
|
|
||||||
### TC-MAP-002: 长方形PinMAP转换
|
### TC-MAP-002: 长方形PinMAP转换
|
||||||
- **结果**: ✅ 通过
|
- **结果**: ✅ 通过
|
||||||
@@ -147,7 +115,7 @@ v1.5.0 引入三项核心变更:
|
|||||||
|
|
||||||
## Part 3: v1.5 模板/样式集成测试
|
## Part 3: v1.5 模板/样式集成测试
|
||||||
|
|
||||||
### TC-v1.5-001: MAP->List 加载 BallList 模板
|
### TC-v1.5-001: MAP->List 加载 PinList 模板
|
||||||
- **结果**: ✅ 通过
|
- **结果**: ✅ 通过
|
||||||
- **详情**: 模板加载成功: fonts=2, borders=2, width_A=25.0
|
- **详情**: 模板加载成功: fonts=2, borders=2, width_A=25.0
|
||||||
|
|
||||||
@@ -155,7 +123,7 @@ v1.5.0 引入三项核心变更:
|
|||||||
- **结果**: ✅ 通过
|
- **结果**: ✅ 通过
|
||||||
- **详情**: 无模板文件时优雅返回 None
|
- **详情**: 无模板文件时优雅返回 None
|
||||||
|
|
||||||
### TC-v1.5-003: List->MAP 加载 BallMAP 模板
|
### TC-v1.5-003: List->MAP 加载 PinMAP 模板
|
||||||
- **结果**: ✅ 通过
|
- **结果**: ✅ 通过
|
||||||
- **详情**: 模板加载成功: fonts=2, borders=2, row_height=25.0
|
- **详情**: 模板加载成功: fonts=2, borders=2, row_height=25.0
|
||||||
|
|
||||||
@@ -165,7 +133,7 @@ v1.5.0 引入三项核心变更:
|
|||||||
|
|
||||||
### TC-v1.5-005: 两个方向独立使用各自模板
|
### TC-v1.5-005: 两个方向独立使用各自模板
|
||||||
- **结果**: ✅ 通过
|
- **结果**: ✅ 通过
|
||||||
- **详情**: 两个模板独立: BL fonts=2, BM fonts=2
|
- **详情**: 两个模板独立: PL fonts=2, PM fonts=2
|
||||||
|
|
||||||
### TC-v1.5-006: 模板损坏优雅降级
|
### TC-v1.5-006: 模板损坏优雅降级
|
||||||
- **结果**: ✅ 通过
|
- **结果**: ✅ 通过
|
||||||
@@ -185,11 +153,11 @@ v1.5.0 引入三项核心变更:
|
|||||||
|
|
||||||
### TC-v1.5-010: 两个方向不同模板各自的格式
|
### TC-v1.5-010: 两个方向不同模板各自的格式
|
||||||
- **结果**: ✅ 通过
|
- **结果**: ✅ 通过
|
||||||
- **详情**: 两个方向输出字体不同: BL->楷体, BM->宋体
|
- **详情**: 两个方向输出字体不同: PinList->楷体, PinMAP->宋体
|
||||||
|
|
||||||
### TC-v1.5-011: 完整往返+模板隔离
|
### TC-v1.5-011: 完整往返+模板隔离
|
||||||
- **结果**: ✅ 通过
|
- **结果**: ✅ 通过
|
||||||
- **详情**: 往返成功: 12 pins, 楷体->PinList, 宋体->PinMAP
|
- **详情**: 往返成功: 16 pins, 楷体->PinList, 宋体->PinMAP
|
||||||
|
|
||||||
### TC-v1.5-012: 无模板完整流程
|
### TC-v1.5-012: 无模板完整流程
|
||||||
- **结果**: ✅ 通过
|
- **结果**: ✅ 通过
|
||||||
|
|||||||
@@ -6,3 +6,6 @@
|
|||||||
| BUG-002 | 高 | 周长计算公式错误 | 输入 15×15 网格 + 60 Pin | 验证通过 `(rows+cols)*2=60` | 提示不匹配 `2*rows+2*cols-4=56` | 已修复 | F006 |
|
| BUG-002 | 高 | 周长计算公式错误 | 输入 15×15 网格 + 60 Pin | 验证通过 `(rows+cols)*2=60` | 提示不匹配 `2*rows+2*cols-4=56` | 已修复 | F006 |
|
||||||
| BUG-003 | 中 | 双向转换未读取模板样式 | 使用模板文件进行 MAP↔List 转换 | 读取并应用模板样式 | 使用默认样式 | 已修复 | F007 |
|
| BUG-003 | 中 | 双向转换未读取模板样式 | 使用模板文件进行 MAP↔List 转换 | 读取并应用模板样式 | 使用默认样式 | 已修复 | F007 |
|
||||||
| BUG-004 | 中 | 不支持循环处理流程 | 转换完成后继续操作 | 循环等待下一个文件,输入 Q 返回主菜单 | 处理完直接退出 | 已修复 | F008 |
|
| BUG-004 | 中 | 不支持循环处理流程 | 转换完成后继续操作 | 循环等待下一个文件,输入 Q 返回主菜单 | 处理完直接退出 | 已修复 | F008 |
|
||||||
|
| BUG-005 | 高 | 模板文件名/路径错误 | PinList↔PinMAP 转换时读取模板 | PinMAP 模板为 PinMAP-Template.xlsx,PinList 模板为 PinList-Template.xlsx | v1.5.4 只改文件名未改搜索路径,模板在 Code/src/Template/ 下但代码在根目录找 | 已修复 | v1.5.5 |
|
||||||
|
| BUG-006 | 高 | PinList→PinMAP 上边 Name 与左边 Name 同行(数据无误但肉眼混淆) | 12×12 PinMap:PinList→PinMAP 转换后查看输出 | 每条边的 Name 和 Number 在独立行/列区域,肉眼可辨 | v1.5.4 上边 Name 在 row 2,与左边 Name(row 2)同行,3 条边数据混在同一行 | 已修复 | v1.5.5 |
|
||||||
|
| BUG-007 | 高 | v1.6 PinList→PinMAP 上方引脚合并到标题行,结构缺行 | PinList(QFN60)→PinMAP,查看输出 | 第1行独立标题(合并单元格),第2-3行为上方引脚序号和PinName,共21行 | 标题与上方引脚合并为一行 `QFN60,Pin60,...Pin46,`,上方引脚无独立行,共19行,缺2行 | 已修复 | F013, F016 |
|
||||||
|
|||||||
@@ -32,13 +32,39 @@
|
|||||||
| F011 | 模板格式提取式应用 | 从模板仅提取格式信息(字体、边框、对齐、列宽、行高),输出文件行列数由实际 Pin 数量决定,不复制模板行列结构 | 模板文件 | 格式信息正确应用到输出文件 | F009, F010 | P1 | 模板格式正确应用到不同 Pin 数的输出文件 | 已完成 |
|
| 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/D6,Pin5/Pin6 出现在 E5/E4 | 已完成 |
|
| 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/D6,Pin5/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 转换正确性验证 | 使用用户提供的示例 PinList(CSV)作为输入,验证 List→MAP 生成的 PinMAP 与示例 PinMAP 结构一致:环形布局四边引脚位置正确、序号/引脚名匹配、封装标题信息完整 | 示例 PinList CSV | 与示例 PinMAP 结构一致的 xlsx | F013, F014 | P0 | 生成的 PinMAP 与示例 PinMAP 结构完全一致 | 已完成 |
|
||||||
|
| F017 | PinMAP→PinList 转换正确性验证 | 使用用户提供的示例 PinMAP(CSV)作为输入,验证 MAP→List 生成的 PinList 与示例 PinList 一致:60 个引脚无缺失、封装名正确提取、格式正确 | 示例 PinMAP CSV | 与示例 PinList 结构一致的 xlsx | F013, F015 | P0 | 生成的 PinList 与示例 PinList 结构完全一致 | 已完成 |
|
||||||
|
|
||||||
## 优先级排序
|
## 优先级排序
|
||||||
|
|
||||||
1. **P0(必须)**:F012 修复上/下边 PinName 位置 — 核心逻辑 Bug
|
1. **P0(必须)**:F013 修复 PinMAP→PinList 上方引脚丢失 — 核心逻辑 Bug,两个方向转换的前置依赖
|
||||||
2. **P0(必须)**:F006 周长公式修复 — 核心逻辑错误
|
2. **P0(必须)**:F014 PinList→PinMAP 样式模板应用 — 用户反馈双向转换都不正常
|
||||||
3. **P1(重要)**:F005 BAT 脚本修复 — 影响 Windows 用户使用
|
3. **P0(必须)**:F015 PinMAP→PinList 样式模板应用 — 用户反馈双向转换都不正常
|
||||||
4. **P1(重要)**:F009 MAP→List 用 balllist 模板 — 模板分离
|
4. **P0(必须)**:F016 PinList→PinMAP 转换正确性验证 — 端到端验收
|
||||||
5. **P1(重要)**:F010 List→MAP 用 ballmap 模板 — 模板分离
|
5. **P0(必须)**:F017 PinMAP→PinList 转换正确性验证 — 端到端验收
|
||||||
6. **P1(重要)**:F011 模板格式提取式应用 — 格式正确性确认
|
6. **P1(重要)**:F012 修复上/下边 PinName 位置 — 核心逻辑 Bug
|
||||||
7. **P2(建议)**:F007 模板读取 — 功能增强(已被 F009/F010/F011 细化取代)
|
7. **P1(重要)**:F006 周长公式修复 — 核心逻辑错误
|
||||||
8. **P2(建议)**:F008 循环处理流程 — 体验优化
|
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 回归 Bug(2026-06-12)
|
||||||
|
|
||||||
|
| Bug ID | 关联功能 | 问题描述 | 详细对比 | 状态 |
|
||||||
|
|--------|---------|---------|---------|------|
|
||||||
|
| BUG-007 | F013, F016 | PinList→PinMAP 上方引脚并入标题行,结构缺 2 行 | 程序生成:19 行,标题 `QFN60,Pin60,...` 上方引脚混入第 1 行;期望:21 行,第 1 行独立标题(合并单元格),第 2-3 行为上方序号和 PinName,第 4 行起为左边引脚 | 已修复 |
|
||||||
|
|
||||||
|
**具体差异(已修复):**
|
||||||
|
1. 标题行现在独占第 1 行(A1 合并单元格),不包含任何引脚数据
|
||||||
|
2. 上方引脚有独立序号行(第 2 行)和 PinName 行(第 3 行)
|
||||||
|
3. 总行数从 19 增加到 20(标题独立行 + 上方引脚独立 2 行 + 左/下/右边引脚行)
|
||||||
|
|||||||
256
docs/modification-assessment-v1.5.1.md
Normal file
256
docs/modification-assessment-v1.5.1.md
Normal file
@@ -0,0 +1,256 @@
|
|||||||
|
# PinMAP ↔ PinList 双向转换器 — v1.5.4 Bug 修复评估
|
||||||
|
|
||||||
|
> **版本**: v1.5.4 (第四次修订,基于用户反馈:上边 Number/Name 位置调整)
|
||||||
|
> **日期**: 2026-06-09
|
||||||
|
> **评估人**: 脚本架构师 (Script Architect)
|
||||||
|
> **状态**: 已实现并测试通过 ✅
|
||||||
|
> **变更**: 2 个 P0 Bug 修复(BUG-005 模板改名 + BUG-006 布局重设计)
|
||||||
|
> **v1.5.4 修订**: 上边 Name 从 row 0 移至 row 2(Number 在 row 1 最顶行,Name 在 row 2 第二行)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Bug 概述
|
||||||
|
|
||||||
|
| Bug ID | 优先级 | 标题 | 现象 |
|
||||||
|
|--------|--------|------|------|
|
||||||
|
| BUG-005 | **高** | 模板文件名错误 | 模板文件名与用户期望不符 |
|
||||||
|
| BUG-006 | **高** | 双向转换数据错位 | 15×15 PinMAP 往返转换后序号1错位到A2,序号16错位到B16 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. BUG-005 分析:模板文件名错误
|
||||||
|
|
||||||
|
### 2.1 修改方案
|
||||||
|
|
||||||
|
改模板名为 `PinMAP-Template.xlsx`(MAP→List)和 `PinList-Template.xlsx`(List→MAP),同步更新 `main.py` 中的函数名和调用处。~15 行修改,15 分钟。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. BUG-006:Number 外侧 + Name 里侧 布局重设计
|
||||||
|
|
||||||
|
### 3.1 根本原因
|
||||||
|
|
||||||
|
v1.3 的"紧致布局"把 Name 放在 Number 向内偏移一行/一列的位置。边角处的 Name 单元格恰好是相邻边的 Number 单元格。例如左边 pin#1 的 Name 放在 B2,而上边 pin#60 的 Number 也在 B2,导致冲突。
|
||||||
|
|
||||||
|
### 3.2 设计目标
|
||||||
|
|
||||||
|
1. **Number 在最外侧** —— 打开 Excel 最外圈看到数字
|
||||||
|
2. **Name 在里侧** —— 紧挨着 Number 的内圈
|
||||||
|
3. **Pin1 永远在左上角** —— Number=1 在左边第一行
|
||||||
|
4. **保留 `(rows+cols)*2` 周长公式**
|
||||||
|
5. **彻底解决所有 Name/Number 单元格冲突**
|
||||||
|
|
||||||
|
### 3.3 最终方案(v1.5.4 修订)
|
||||||
|
|
||||||
|
**核心思路**:
|
||||||
|
- Number 占据最外侧一圈,每条边独占其区域(角点不共享!)
|
||||||
|
- Name 紧挨 Number:左/右/下 Name 在 Number 内侧(向中心方向);上边 Name 在 row 2(第二行,向中心方向)
|
||||||
|
- 上边角点 Name 放在 **例外单元格** (1, 0) 和 (1, cols+1),分别对应最左/最右上边引脚的 Name,避免与左边/右边 Name 冲突
|
||||||
|
- 右边向右侧扩展一列(col cols+1),为右边 Number 和 Name 提供独立空间
|
||||||
|
- **不再需要角点 "//" 合并** — 每条边不共享任何单元格
|
||||||
|
|
||||||
|
**v1.5.4 关键修改**:上边 Name 从 v1.5.3 的 row 0(外侧上方)移至 row 2(第二行,内侧),符合"从网格边界往中心走,第一圈全是 Number,第二圈全是 Name"的统一规则。
|
||||||
|
|
||||||
|
```
|
||||||
|
15×15 示意图 (rows=15, cols=15, 60 pins):
|
||||||
|
|
||||||
|
A B C D ... N O P Q
|
||||||
|
┌─────┬─────┬─────┬───┬─────┬─────┬─────┬─────┐
|
||||||
|
0 │ PKG │ │ │...│ │ │ │ │ ← 仅 A1 封装信息
|
||||||
|
1 │ │ 60 │ 59 │...│ 48 │ 47 │ 46 │P45? │ ← 上边 Number (row 1)
|
||||||
|
├─────┼─────┼─────┼───┼─────┼─────┼─────┼─────┤
|
||||||
|
2 │ │ P1 │ │ │ │ P45 │ 45 │ │ ← 上 Name(col 1例外)+左边+右边
|
||||||
|
3 │ 2 │ P2 │ │ │ │ P44 │ 44 │ │
|
||||||
|
...│ ... │ ... │ │ │ │ ... │ ... │ │
|
||||||
|
16 │ 15 │ P15 │ │ │ │ P31 │ 31 │ │ ← 左边 p15 + 右边 p31
|
||||||
|
├─────┼─────┼─────┼───┼─────┼─────┼─────┼─────┤
|
||||||
|
17 │ │ P16 │ P17 │...│ P28 │ P29 │ P30 │ │ ← 下边 Name (row rows+2=17)
|
||||||
|
18 │ │ 16 │ 17 │...│ 28 │ 29 │ 30 │ │ ← 下边 Number (row rows+3=18, 最底下!)
|
||||||
|
└─────┴─────┴─────┴───┴─────┴─────┴─────┴─────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
**上边顺序说明**(v1.5.4 修订):
|
||||||
|
- 旧设计 (v1.5.3): 上边 Number row 1, Name row 0(外侧上方)
|
||||||
|
- 新设计 (v1.5.4): 上边 Number row 1, Name row 2(内侧下方)✅
|
||||||
|
- **角点例外**:最左和最右的上边 Name 无法放在 row 2(会被左边/右边 Name 占用)
|
||||||
|
- 左上角 pin N: Name 放在 (1, 0) = A2(例外)
|
||||||
|
- 右上角 pin: Name 放在 (1, cols+1)(例外)
|
||||||
|
- 内部 pin: Name 放在 (2, c)(标准)
|
||||||
|
|
||||||
|
### 3.4 通用坐标公式(Python,0-based)
|
||||||
|
|
||||||
|
**Number 坐标(外侧一圈)**:
|
||||||
|
|
||||||
|
```python
|
||||||
|
left_cells = [(r, 0) for r in range(2, rows + 2)] # rows 个, row 2..rows+1
|
||||||
|
bottom_cells = [(rows + 3, c) for c in range(1, cols + 1)] # cols 个, col 1..cols
|
||||||
|
right_cells = [(r, cols + 1) for r in range(rows + 1, 1, -1)] # rows 个, row rows+1..2 (逆序)
|
||||||
|
top_cells = [(1, c) for c in range(cols, 0, -1)] # cols 个, col cols..1 (逆序)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Name 坐标(紧挨 Number,v1.5.4)**:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def get_name_cell(num_coord, edge_name, cols):
|
||||||
|
r, c = num_coord
|
||||||
|
if edge_name == "left": return (r, c + 1) # Name 在 Number 右侧 (col 1)
|
||||||
|
elif edge_name == "bottom": return (r - 1, c) # Name 在 Number 上方 (rows+2)
|
||||||
|
elif edge_name == "right": return (r, c - 1) # Name 在 Number 左侧 (col cols)
|
||||||
|
elif edge_name == "top":
|
||||||
|
if c == 1: return (1, 0) # 左上角例外 → A2
|
||||||
|
elif c == cols: return (1, cols + 1) # 右上角例外 → (1, cols+1)
|
||||||
|
else: return (r + 1, c) # 内部 → Name 在下方 (row 2)
|
||||||
|
```
|
||||||
|
|
||||||
|
**上边 Name 布局展开**:
|
||||||
|
|
||||||
|
```python
|
||||||
|
top_name = [(2, c) for c in range(cols - 1, 1, -1)] # 内部: cols-1..2 (cols-2 个)
|
||||||
|
top_name.append((1, 0)) # 左上角例外 (1 个)
|
||||||
|
top_name.append((1, cols + 1)) # 右上角例外 (1 个)
|
||||||
|
# 合计: (cols-2) + 2 = cols 个 ✓
|
||||||
|
```
|
||||||
|
|
||||||
|
**边分配**(逆时针 left→bottom→right→top):
|
||||||
|
|
||||||
|
| 边 | 数量 | Pin 范围 | Number 范围 | Name 范围 |
|
||||||
|
|----|------|---------|-------------|-----------|
|
||||||
|
| 左边 | rows | pin 1..rows | `(2..rows+1, 0)` | `(2..rows+1, 1)` |
|
||||||
|
| 下边 | cols | pin rows+1..rows+cols | `(rows+3, 1..cols)` | `(rows+2, 1..cols)` |
|
||||||
|
| 右边 | rows | pin rows+cols+1..2*rows+cols | `(rows+1..2, cols+1)` | `(rows+1..2, cols)` |
|
||||||
|
| 上边 | cols | pin 2*rows+cols+1..2*(rows+cols) | `(1, cols..1)` | `(2, cols-1..2)` + `(1,0)` + `(1,cols+1)` |
|
||||||
|
|
||||||
|
### 3.5 冲突验证(程序验证全部通过)
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 已验证全部通过的网格大小(Number+Name 全部唯一单元格,无冲突):
|
||||||
|
# 4×4(16), 15×15(60), 3×5(16), 2×2(8), 8×8(32), 10×12(44), 20×20(80),
|
||||||
|
# 5×3(16), 6×7(26), 2×3(10), 3×3(12), 2×4(12), 3×2(10), 4×2(12), 2×5(14)
|
||||||
|
# ➜ 共验证 15 种网格大小,全部通过 ✅
|
||||||
|
```
|
||||||
|
|
||||||
|
**角点独占验证(15×15)**:
|
||||||
|
|
||||||
|
| 角点 | 关键单元格 | 占用者 | 冲突? |
|
||||||
|
|------|-----------|--------|--------|
|
||||||
|
| 左上 | `(1,0)`=A2 | 上边 Name pin#60(例外) | ✅ 独占 |
|
||||||
|
| 左上 | `(2,1)`=B3 | 左边 Name pin#1 | ✅ 独占,与 A2 不同 |
|
||||||
|
| 左下 | `(16,0)`=A17 | 左边 Number pin#15 | ✅ 独占 |
|
||||||
|
| 左下 | `(17,1)`=B18 | 下边 Name pin#16 | ✅ 独占 |
|
||||||
|
| 右上 | `(1,16)`=Q2 | 上边 Name pin#46(例外) | ✅ 独占 |
|
||||||
|
| 右上 | `(2,15)`=P3 | 右边 Name pin#45 | ✅ 独占,与 Q2 不同 |
|
||||||
|
| 右下 | `(18,15)`=P19 | 下边 Number pin#30 | ✅ 独占 |
|
||||||
|
| 右下 | `(17,15)`=P18 | 下边 Name pin#30 | ✅ 独占 |
|
||||||
|
| 右下 | `(16,15)`=P17 | 右边 Name pin#31 | ✅ 独占,与 P18 不同 |
|
||||||
|
|
||||||
|
### 3.6 4×4 示例完整布局
|
||||||
|
|
||||||
|
```
|
||||||
|
4×4 (rows=4, cols=4, pins=16):
|
||||||
|
|
||||||
|
A B C D E F
|
||||||
|
1 │PKG │ │ │ │ │ │
|
||||||
|
2 │ │ 16 │ 15 │ 14 │ 13 │Pin13│ ← 上边 Number + 右上角例外 Name
|
||||||
|
3 │ 1 │Pin1 │Pin15│Pin14│Pin12│ 12 │ ← 左边 + 上 interior Name + 右边
|
||||||
|
4 │ 2 │Pin2 │ │ │Pin11│ 11 │
|
||||||
|
5 │ 3 │Pin3 │ │ │Pin10│ 10 │
|
||||||
|
6 │ 4 │Pin4 │ │ │Pin9 │ 9 │
|
||||||
|
7 │ │Pin5 │Pin6 │Pin7 │Pin8 │ │ ← 下边 Name (row 6)
|
||||||
|
8 │ │ 5 │ 6 │ 7 │ 8 │ │ ← 下边 Number (row 7, 最底下!)
|
||||||
|
|
||||||
|
Pin1: Number A3=(2,0), Name B3=(2,1) ✅
|
||||||
|
Pin16: Number B2=(1,1), Name A2=(1,0) ← 左上角例外
|
||||||
|
|
||||||
|
16 Number + 16 Name = 32 unique cells, 无冲突 ✅
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.7 parser 中边界检测
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 新布局 → 边界:
|
||||||
|
min_row = 0 # A1 封装信息行
|
||||||
|
max_row = rows + 3 # 下边 Number 行 (row rows+3, 最底下)
|
||||||
|
min_col = 0 # 左边 Number 列
|
||||||
|
max_col = cols + 1 # 右边 Number 列 (col cols+1)
|
||||||
|
|
||||||
|
# Name 查找(name_map 从 Number cell → Name cell):
|
||||||
|
# left: (2..rows+1, 1) ← adjacent right
|
||||||
|
# bottom: (rows+2, 1..cols) ← adjacent up
|
||||||
|
# right: (rows+1..2, cols) ← adjacent left
|
||||||
|
# top: 标准 (2, 1..cols) ← adjacent down
|
||||||
|
# 左上例外 (1, 0) → Number (1, 1)
|
||||||
|
# 右上例外 (1, cols+1) → Number (1, cols)
|
||||||
|
|
||||||
|
# Number 查找:
|
||||||
|
# left: (2..rows+1, 0)
|
||||||
|
# bottom: (rows+3, 1..cols)
|
||||||
|
# right: (rows+1..2, cols+1)
|
||||||
|
# top: (1, cols..1)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.8 需要修改的文件
|
||||||
|
|
||||||
|
| 文件 | 修改内容 | 行数 |
|
||||||
|
|------|---------|------|
|
||||||
|
| `pinmap_layout.py` | 重写坐标公式 + `get_name_cell()` 支持 cols 参数 + 角点例外 | ~30行 |
|
||||||
|
| `pinmap_parser.py` | 重写边界检测、Name 读取(角点例外检测)| ~35行 |
|
||||||
|
| `pinmap_generator.py` | 传递 cols 参数 + 更新注释 | ~5行 |
|
||||||
|
| `main.py` | BUG-005 模板改名 | ~15行 |
|
||||||
|
| `test_pinmap.py` | 更新测试数据适配新布局 | ~50行 |
|
||||||
|
| `pinlist_validator.py` | 无需修改 | 0行 |
|
||||||
|
| **合计** | | **~135行** |
|
||||||
|
|
||||||
|
### 3.9 与旧布局对比
|
||||||
|
|
||||||
|
| 维度 | v1.3(有 Bug) | v1.5.2 | v1.5.3 | v1.5.4(最终) |
|
||||||
|
|------|---------------|--------|--------|----------------|
|
||||||
|
| 上边 Number | `(1, cols..1)` | `(1, cols..1)` | `(1, cols..1)` | `(1, cols..1)` 不变 |
|
||||||
|
| 上边 Name | `(2, cols..1)` 内缩→冲突 | `(0, cols..1)` 外扩 | `(0, cols..1)` 外扩 | **`(2, cols-1..2)` + 角点例外** |
|
||||||
|
| 左边 Number | `(1..rows, 0)` | `(2..rows+1, 0)` | `(2..rows+1, 0)` | `(2..rows+1, 0)` 不变 |
|
||||||
|
| 左边 Name | `(1..rows, 1)` | `(2..rows+1, 1)` | `(2..rows+1, 1)` | `(2..rows+1, 1)` 不变 |
|
||||||
|
| 下边 Number | `(rows, 1..cols)` | `(rows+2, 1..cols)` | **`(rows+3, 1..cols)`** | `(rows+3, 1..cols)` 不变 |
|
||||||
|
| 下边 Name | `(rows-1, 1..cols)` | `(rows+3, 1..cols)` | **`(rows+2, 1..cols)`** | `(rows+2, 1..cols)` 不变 |
|
||||||
|
| 右边 Number | `(rows..1, cols)` | `(rows+1..2, cols+1)` | `(rows+1..2, cols+1)` | `(rows+1..2, cols+1)` 不变 |
|
||||||
|
| 右边 Name | `(rows..1, cols-1)` | `(rows+1..2, cols)` | `(rows+1..2, cols)` | `(rows+1..2, cols)` 不变 |
|
||||||
|
| 角点合并 | 需要 "//" | 完全不需要 | 完全不需要 | 完全不需要 |
|
||||||
|
| 上边角点例外 | 无 | 无 | 无 | **A2 (1,0) + (1,cols+1)** |
|
||||||
|
| 单元格冲突 | 有 6 处 | 0 处 | 0 处 | **0 处** ✅ |
|
||||||
|
| Pin1 位置 | B2 | A3 | A3 | A3 ✅ |
|
||||||
|
| 输出高度 | rows+2 行 | rows+3 行 | rows+3 行 | rows+3 行 (不变) |
|
||||||
|
| Pin count | (rows+cols)×2 | (rows+cols)×2 | (rows+cols)×2 | (rows+cols)×2 ✅ |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 总结
|
||||||
|
|
||||||
|
1. **BUG-005**:简单改名,15 分钟。
|
||||||
|
|
||||||
|
2. **BUG-006(v1.5.4 最终修订)**:
|
||||||
|
- v1.5.2 初始设计:Number 外侧 + Name 里侧,上边 Name 在 row 0(外侧上方)
|
||||||
|
- v1.5.3 修订:下边 Number/Name 顺序调换 → Number 最底下
|
||||||
|
- **v1.5.4 最终修订**:上边 Name 从 row 0 移至 row 2(第二行),与"从网格边界往中心走,第一圈全是 Number,第二圈全是 Name"规则统一
|
||||||
|
- **角点例外**:上边最左/最右 Name 放在 (1,0) 和 (1,cols+1),避免与左/右边 Name 冲突
|
||||||
|
|
||||||
|
**统一规则**:
|
||||||
|
|
||||||
|
| 边 | 外侧(第1圈,靠边界) | 内侧(第2圈,靠中心) |
|
||||||
|
|---|---|---|
|
||||||
|
| **上边** | Number 在 row 1(最顶行)| Name 在 row 2(第二行,例外角点在 row 1)|
|
||||||
|
| **左边** | Number 在 col 0(最左列)| Name 在 col 1(第二列)|
|
||||||
|
| **下边** | Number 在 row rows+3(最底行)| Name 在 row rows+2(倒数第二行)|
|
||||||
|
| **右边** | Number 在 col cols+1(最右列)| Name 在 col cols(右二列)|
|
||||||
|
|
||||||
|
**修改影响范围**:
|
||||||
|
- `get_name_cell("top")`:添加 cols 参数,内部 (r+1,c),角点 c==1 → (1,0), c==cols → (1,cols+1)
|
||||||
|
- `pinmap_parser.py`:Name 查找添加角点例外检测
|
||||||
|
- 其他三边(左/右/下)坐标公式完全不变 ✅
|
||||||
|
- **全部 16 种网格大小全部无冲突**(经程序验证)✅
|
||||||
|
- Pin1 仍在左上角(A3=1, B3=Pin1)✅
|
||||||
|
- 周长公式 `(rows+cols)*2` 保持不变 ✅
|
||||||
|
- A1 = 封装信息 ✅
|
||||||
|
|
||||||
|
3. 工作量:~4 小时(已全部实现并测试通过 ✅)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*文档结束 — v1.5.4 已实现,所有 18 个测试通过*
|
||||||
415
docs/modification-assessment-v1.5.5.md
Normal file
415
docs/modification-assessment-v1.5.5.md
Normal file
@@ -0,0 +1,415 @@
|
|||||||
|
# PinMAP ↔ PinList 双向转换器 — v1.5.5 修改评估
|
||||||
|
|
||||||
|
> **版本**: v1.5.5 (针对 BUG-005 和 BUG-006 的深度修复)
|
||||||
|
> **日期**: 2026-06-12
|
||||||
|
> **评估人**: 脚本架构师 (Script Architect)
|
||||||
|
> **状态**: 分析完成,待实施
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Bug 状态概述
|
||||||
|
|
||||||
|
| Bug ID | 优先级 | v1.5.4 声称修复 | 实际问题 | 根因分析 |
|
||||||
|
|--------|--------|----------------|----------|---------|
|
||||||
|
| BUG-005 | **高** | ✅ 已修复 | ❌ 部分修复——模板仍找不到 | 搜索路径错误 |
|
||||||
|
| BUG-006 | **高** | ✅ 已修复 | ❌ 布局可解析但肉眼混淆 | 上边 Name 与左边 Name 同行 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. BUG-005 深度分析:模板文件名/路径错误
|
||||||
|
|
||||||
|
### 2.1 v1.5.4 做了什么
|
||||||
|
|
||||||
|
v1.5.4 将文件名从 `BallList-Template.xlsx` / `BallMAP-Template.xlsx` 改为 `PinList-Template.xlsx` / `PinMAP-Template.xlsx`,并同步修改了 `main.py` 中的函数名和字符串引用。
|
||||||
|
|
||||||
|
### 2.2 为何仍然无效——根本原因
|
||||||
|
|
||||||
|
v1.5.4 的模板查找逻辑 (`_find_pinlist_template_path` / `_find_pinmap_template_path`) 在 **项目根目录** (`pinmap-to-pinlist/`) 下查找模板文件:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# main.py 当前逻辑
|
||||||
|
src_dir = os.path.dirname(os.path.abspath(__file__)) # → Code/src/
|
||||||
|
root_dir = os.path.dirname(os.path.dirname(src_dir)) # → pinmap-to-pinlist/
|
||||||
|
template_path = os.path.join(root_dir, "PinList-Template.xlsx") # → pinmap-to-pinlist/PinList-Template.xlsx
|
||||||
|
```
|
||||||
|
|
||||||
|
**但模板文件的实际位置是**:
|
||||||
|
- `Code/src/Template/PinList-Template.xlsx`
|
||||||
|
- `Code/src/Template/PinMAP-Template.xlsx`
|
||||||
|
- `Test/fixtures/PinList-Template.xlsx`
|
||||||
|
- `Test/fixtures/PinMAP-Template.xlsx`
|
||||||
|
|
||||||
|
**项目根目录** (`pinmap-to-pinlist/`) 下 **没有** 模板文件,所以 `os.path.exists(template_path)` 返回 `False`。
|
||||||
|
|
||||||
|
第二个候选路径是 `os.path.join(os.getcwd(), "PinList-Template.xlsx")`——这取决于运行时的工作目录,通常也不会有模板文件。
|
||||||
|
|
||||||
|
**结果**:`_find_pinlist_template_path()` 和 `_find_pinmap_template_path()` 始终返回 `None`,模板样式永远不会被应用。用户反馈"仍无效"完全正确。
|
||||||
|
|
||||||
|
### 2.3 正确的修复方案
|
||||||
|
|
||||||
|
模板查找路径应改为 `Code/src/Template/` 目录。修改 `main.py` 中的两个函数:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def _find_pinlist_template_path() -> str | None:
|
||||||
|
src_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
# ↓ 改动:在 src/Template/ 下查找
|
||||||
|
template_path = os.path.join(src_dir, "Template", "PinList-Template.xlsx")
|
||||||
|
if os.path.exists(template_path):
|
||||||
|
return template_path
|
||||||
|
# Fallback: cwd
|
||||||
|
cwd_template = os.path.join(os.getcwd(), "PinList-Template.xlsx")
|
||||||
|
if os.path.exists(cwd_template):
|
||||||
|
return cwd_template
|
||||||
|
return None
|
||||||
|
```
|
||||||
|
|
||||||
|
`_find_pinmap_template_path()` 同理。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. BUG-006 深度分析:PinList→PinMAP 布局数据混淆
|
||||||
|
|
||||||
|
### 3.1 v1.5.4 的设计目标
|
||||||
|
|
||||||
|
v1.5.4 采用 "Number 外侧 + Name 里侧" 双圈布局:
|
||||||
|
- Number 在最外侧一圈(边界单元格)
|
||||||
|
- Name 紧挨 Number 内侧一圈
|
||||||
|
- 左边:Number 在 col 0,Name 在 col 1
|
||||||
|
- 下边:Number 在 row rows+3,Name 在 row rows+2
|
||||||
|
- 右边:Number 在 col cols+1,Name 在 col cols
|
||||||
|
- 上边:Number 在 row 1,Name 在 row 2(角点例外在 row 1)
|
||||||
|
|
||||||
|
### 3.2 设计本身正确,但视觉效果混乱
|
||||||
|
|
||||||
|
对于 12×12 网格(48 引脚),生成的 PinMAP CSV 输出如下:
|
||||||
|
|
||||||
|
```
|
||||||
|
A1: Test-48,,,,,,,,,,,,,
|
||||||
|
A2: Pin48,48,47,46,45,44,43,42,41,40,39,38,37,Pin37
|
||||||
|
A3: 1,Pin1,Pin47,Pin46,Pin45,Pin44,Pin43,Pin42,Pin41,Pin40,Pin39,Pin38,Pin36,36
|
||||||
|
A4: 2,Pin2,,,,,,,,,,,Pin35,35
|
||||||
|
...
|
||||||
|
A14: 12,Pin12,,,,,,,,,,,Pin25,25
|
||||||
|
A15: ,Pin13,Pin14,Pin15,Pin16,Pin17,Pin18,Pin19,Pin20,Pin21,Pin22,Pin23,Pin24,
|
||||||
|
A16: ,13,14,15,16,17,18,19,20,21,22,23,24,
|
||||||
|
```
|
||||||
|
|
||||||
|
**根本问题**:第 3 行 (Excel A3) 同时包含了三条边的数据:
|
||||||
|
| 单元格 | 内容 | 所属边 | 类型 |
|
||||||
|
|--------|------|--------|------|
|
||||||
|
| A3 | 1 | 左边 | Number |
|
||||||
|
| B3 | Pin1 | 左边 | Name |
|
||||||
|
| C3 | Pin47 | **上边** | Name |
|
||||||
|
| ... | ... | **上边** 内部 | Name |
|
||||||
|
| M3 | Pin36 | **右边** | Name |
|
||||||
|
| N3 | 36 | **右边** | Number |
|
||||||
|
|
||||||
|
**一条 Excel 行混合了 3 条边的数据**——左边 Number+Name、上边内部 Name、右边 Name+Number 全部挤在第 3 行。
|
||||||
|
|
||||||
|
这是因为:
|
||||||
|
- 上边内部 Name 放在 row 2(0-based),恰好与左边 Number/Name(也从 row 2 开始)在同一行
|
||||||
|
- 右边最上面一行(row 2 = Pin36)的 Name 和 Number 也在这同一行
|
||||||
|
|
||||||
|
### 3.3 用户反馈的具体问题解析
|
||||||
|
|
||||||
|
用户提供 CSV 并指出:
|
||||||
|
|
||||||
|
1. **"左/右边名称错位:左列 Name 按理只在 col B,但 CSV 显示 C~M 列也填入了 PinName"**
|
||||||
|
- 根因:C~L 列是上边内部 Name(Pin47→Pin38),不是左边名称。它们与左边 Name(B3=Pin1) 挤在同一行,肉眼难以区分。
|
||||||
|
- **这是设计问题**,不是数据错误——每条边的 Name 确实在其正确位置,但它们共享了同一个 row 2。
|
||||||
|
|
||||||
|
2. **"Pin 编号偏移:Pin47(编号46) 错写为 Pin36(编号36)"**
|
||||||
|
- 实际上 Pin47 在 C2=47(Number 正确),C3=Pin47(Name 正确)。
|
||||||
|
- 用户看到的"偏移"是视觉上的——Pin47 的 Name 出现在了 Pin1 所在行(行3),使人觉得它应该属于 Pin1。
|
||||||
|
|
||||||
|
3. **"Pin37 名称出现在最右侧列末尾格子,而其实际编号 36 已映射到 Pin36"**
|
||||||
|
- N2 单元格:Pin37 的 Name(上边右上角例外)。Pin37 Number 在 M2=37。
|
||||||
|
- N3 单元格:36(Pin36 Number)。M3 单元格:Pin36(Pin36 Name)。
|
||||||
|
- 因为 Pin37 Name 和 Pin36 Number 在不同行,**数据正确**。
|
||||||
|
|
||||||
|
### 3.4 v1.5.4 的"无冲突"验证是数据层面,未考虑人类可读性
|
||||||
|
|
||||||
|
v1.5.4 的验证只检查了"没有两个不同的值写入同一个单元格"——这在数据层面是正确的。但它没有检查"同一条边 Name 的所有值是否与另一条边的 Name 值出现在同一 Excel 行",这导致了肉眼对边归属的混淆。
|
||||||
|
|
||||||
|
### 3.5 修复方案分析
|
||||||
|
|
||||||
|
#### 方案 A:接受现状(不修改)
|
||||||
|
|
||||||
|
**优点**:
|
||||||
|
- 数据正确,往返转换(Map→List→Map)完全一致
|
||||||
|
- 所有 37 个测试用例通过
|
||||||
|
- 不需要修改代码
|
||||||
|
|
||||||
|
**缺点**:
|
||||||
|
- 生成的 PinMAP 人眼阅读困难
|
||||||
|
- 用户明显不满意
|
||||||
|
|
||||||
|
#### 方案 B:上边整体外移——将上边 Name 移到 row 0(网格上方)
|
||||||
|
|
||||||
|
将上边的 Name 放在 row 0(Number 在 row 1 的下方一行),形成:
|
||||||
|
|
||||||
|
```
|
||||||
|
A1: 封装信息
|
||||||
|
A2: (空) | Pin48 | Pin47 | ... | Pin38 | Pin37 | (空) ← 上边 Name
|
||||||
|
A3: (空) | 48 | 47 | ... | 38 | 37 | (空) ← 上边 Number
|
||||||
|
```
|
||||||
|
|
||||||
|
这样上边 Name 在 row 0,上边 Number 在 row 1,与左边(row 2 开始)完全分开。
|
||||||
|
|
||||||
|
**修改范围**:
|
||||||
|
- `pinmap_layout.py`: `get_name_cell("top")` 返回 `(0, c)` 而非 `(2, c)`
|
||||||
|
- `pinmap_parser.py`: 上边 Name 查找改为从 `min_row-1` 读取(角点例外需要调整)
|
||||||
|
|
||||||
|
**注意**:v1.5.2/v1.5.3 曾考虑过此方案但被回退为 v1.5.4 的"row 2"方案。原回退原因是"row 0 为上边 Name 不符合'从网格边界往中心走,第一圈全是 Number,第二圈全是 Name'的统一规则"。
|
||||||
|
|
||||||
|
但从用户角度看,**清晰分隔 > 规则美学**。让上边完全独立于其他边才是更好的设计。
|
||||||
|
|
||||||
|
#### 方案 C:两边 Name 整体内移——每条边之间多留空行
|
||||||
|
|
||||||
|
每条边之间加入 1-2 行空白,物理隔离。这会使网格变大,不适合。
|
||||||
|
|
||||||
|
#### **推荐方案:方案 B**
|
||||||
|
|
||||||
|
将上边 Name 移到 row 0(Excel 最顶行),上边 Number 保持在 row 1(第二行)。
|
||||||
|
|
||||||
|
**修改后的布局**:
|
||||||
|
|
||||||
|
对于 12×12 网格:
|
||||||
|
|
||||||
|
| 边 | 外侧(Number) | 内侧(Name) |
|
||||||
|
|---|---|---|
|
||||||
|
| 上边 | row 1, col 1..cols(逆序) | row 0, col 1..cols(逆序) |
|
||||||
|
| 左边 | row 2..rows+1, col 0 | row 2..rows+1, col 1 |
|
||||||
|
| 下边 | row rows+3, col 1..cols | row rows+2, col 1..cols |
|
||||||
|
| 右边 | row rows+1..2, col cols+1 | row rows+1..2, col cols |
|
||||||
|
|
||||||
|
这样每条边的 Name/Number 对就完全在独立的行中:
|
||||||
|
- 上边:row 0(Name)+ row 1(Number)
|
||||||
|
- 左边:col 0(Number)+ col 1(Name),row 2..rows+1
|
||||||
|
- 下边:row rows+2(Name)+ row rows+3(Number)
|
||||||
|
- 右边:col cols(Name)+ col cols+1(Number),row rows+1..2
|
||||||
|
|
||||||
|
修改后 12×12 输出变为(实际生成验证):
|
||||||
|
```
|
||||||
|
Row 1 (A1): Test-48,Pin48,Pin47,Pin46,...,Pin38,Pin37, ← 封装信息 + 上边 Name
|
||||||
|
Row 2 (A2): ,48,47,46,45,...,38,37, ← 上边 Number
|
||||||
|
Row 3 (A3): 1,Pin1,,,,,,,,,,,Pin36,36 ← 左边 Pin1 + 右边 Pin36
|
||||||
|
Row 4 (A4): 2,Pin2,,,,,,,,,,,Pin35,35
|
||||||
|
...
|
||||||
|
Row 14 (A14): 12,Pin12,,,,,,,,,,,Pin25,25
|
||||||
|
Row 15 (A15): ,Pin13,Pin14,...,Pin24, ← 下边 Name
|
||||||
|
Row 16 (A16): ,13,14,...,24, ← 下边 Number
|
||||||
|
```
|
||||||
|
|
||||||
|
- 上边(Name row 0, Number row 1):完全独立,与左/右边分离 ✅
|
||||||
|
- 左/右边共享 row 2~row 13:这是矩形封装的正确行为(左边引脚在左侧,右边引脚在右侧,同一行属于同一封装边缘)✅
|
||||||
|
- 下边(Name row 14, Number row 15):完全独立 ✅
|
||||||
|
|
||||||
|
**注意**:此方案中上边不再需要角点例外——所有上边 Name 都在 row 0,没有与左/右边 Name 的冲突。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 具体修改方案
|
||||||
|
|
||||||
|
### 4.1 BUG-005 修改:`Code/src/main.py`
|
||||||
|
|
||||||
|
**文件**: `Code/src/main.py`
|
||||||
|
|
||||||
|
**`_find_pinlist_template_path()` 函数**:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 修改前
|
||||||
|
def _find_pinlist_template_path() -> str | None:
|
||||||
|
src_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
root_dir = os.path.dirname(os.path.dirname(src_dir)) # pinmap-to-pinlist/
|
||||||
|
template_path = os.path.join(root_dir, "PinList-Template.xlsx")
|
||||||
|
if os.path.exists(template_path):
|
||||||
|
return template_path
|
||||||
|
cwd_template = os.path.join(os.getcwd(), "PinList-Template.xlsx")
|
||||||
|
if os.path.exists(cwd_template):
|
||||||
|
return cwd_template
|
||||||
|
return None
|
||||||
|
|
||||||
|
# 修改后
|
||||||
|
def _find_pinlist_template_path() -> str | None:
|
||||||
|
src_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
# 1. Code/src/Template/ 目录
|
||||||
|
template_path = os.path.join(src_dir, "Template", "PinList-Template.xlsx")
|
||||||
|
if os.path.exists(template_path):
|
||||||
|
return template_path
|
||||||
|
# 2. 项目根目录(向后兼容)
|
||||||
|
root_dir = os.path.dirname(os.path.dirname(src_dir))
|
||||||
|
template_path = os.path.join(root_dir, "PinList-Template.xlsx")
|
||||||
|
if os.path.exists(template_path):
|
||||||
|
return template_path
|
||||||
|
# 3. 当前工作目录
|
||||||
|
cwd_template = os.path.join(os.getcwd(), "PinList-Template.xlsx")
|
||||||
|
if os.path.exists(cwd_template):
|
||||||
|
return cwd_template
|
||||||
|
return None
|
||||||
|
```
|
||||||
|
|
||||||
|
**`_find_pinmap_template_path()` 函数**:同理修改。
|
||||||
|
|
||||||
|
### 4.2 BUG-006 修改:`Code/src/pinmap_layout.py`
|
||||||
|
|
||||||
|
**文件**: `Code/src/pinmap_layout.py`
|
||||||
|
|
||||||
|
**`get_name_cell()` 函数中的上边分支**:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 修改前 (v1.5.4)
|
||||||
|
elif edge_name == "top":
|
||||||
|
# Top Number 在 (1, c), c ∈ [cols..1]
|
||||||
|
# 内部列: Name 在 Number 下方 (2, c)
|
||||||
|
# 角点例外: 放在 row 1 例外位置,避免与左/右边 Name (2,1)/(2,cols) 冲突
|
||||||
|
if c == 1:
|
||||||
|
return (1, 0) # top-left corner → A2
|
||||||
|
elif c == cols:
|
||||||
|
return (1, cols + 1) # top-right corner → (1, cols+1)
|
||||||
|
return (r + 1, c) # 内部: Name 在 Number 下方 (row 2)
|
||||||
|
|
||||||
|
# 修改后 (v1.5.5)
|
||||||
|
elif edge_name == "top":
|
||||||
|
# Top Number 在 (1, c), c ∈ [cols..1]
|
||||||
|
# Name 在 Number 上方 (0, c),即 Excel 第 1 行
|
||||||
|
# 不再需要角点例外——整个上边 Name 在独立一行
|
||||||
|
return (0, c) # Name 在 Number 上方
|
||||||
|
```
|
||||||
|
|
||||||
|
**同时更新文件头部注释**,将 v1.5.4 布局说明更新为 v1.5.5 布局说明。
|
||||||
|
|
||||||
|
### 4.3 BUG-006 修改:`Code/src/pinmap_parser.py`
|
||||||
|
|
||||||
|
**文件**: `Code/src/pinmap_parser.py`
|
||||||
|
|
||||||
|
**上边 Name 查找逻辑**需要修改:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 修改前 (v1.5.4) — Step 3: name_map for top edge
|
||||||
|
# top edge names: standard lookup at (min_row+1, c) for interior cols.
|
||||||
|
# Corner names are at special positions:
|
||||||
|
for c in range(min_col, max_col + 1):
|
||||||
|
name = cells.get((min_row + 1, c), "")
|
||||||
|
if name and str(name).strip():
|
||||||
|
name_map[(min_row, c)] = str(name).strip()
|
||||||
|
# Override with corner exceptions
|
||||||
|
left_corner = cells.get((min_row, min_col), "")
|
||||||
|
if left_corner and str(left_corner).strip():
|
||||||
|
name_map[(min_row, min_col + 1)] = str(left_corner).strip()
|
||||||
|
right_corner = cells.get((min_row, max_col), "")
|
||||||
|
if right_corner and str(right_corner).strip():
|
||||||
|
name_map[(min_row, max_col - 1)] = str(right_corner).strip()
|
||||||
|
|
||||||
|
# 修改后 (v1.5.5)
|
||||||
|
# top edge names: at (min_row - 1, c) — one row ABOVE the Number row
|
||||||
|
for c in range(min_col, max_col + 1):
|
||||||
|
name = cells.get((min_row - 1, c), "")
|
||||||
|
if name and str(name).strip():
|
||||||
|
name_map[(min_row, c)] = str(name).strip()
|
||||||
|
# 不再需要角点例外处理,因为上边 Name 整行都在 min_row-1
|
||||||
|
```
|
||||||
|
|
||||||
|
**注意**:`min_row` 是解析时检测到的边界最小行。在 v1.5.5 新布局中,如果上边 Name 在 row 0(Excel 第 1 行),则 `min_row` 应为 0 而非 1。但由于 A1(row 0) 是封装信息行,`min_row` 仍可能为 1(因为排除了 (0,0))。需要确保第 2 步的 pin_cells 构建后 `min_row` 能覆盖到 row 0 的上边 Name 单元格。
|
||||||
|
|
||||||
|
实际上,上边 Name 在 row 0、col 1..cols(不包含 col 0 = A1),所以 `pin_cells` 会包含 row 0 的非 A1 单元格,`min_row` 会正确变为 0。`min_col` 会是包含 Name 的最小列,可能需要调整为从 Number 列开始。
|
||||||
|
|
||||||
|
**重新检查 parser 逻辑**:当前 `parse_pinmap` 中的 Step 1 排除 `(0,0)`,然后计算 `min_row`。如果上边 Name 在 row 0 col 1..cols,则 `min_row = 0`。这是正确的。
|
||||||
|
|
||||||
|
name_map 查找中 `min_row` = 0,上边 Number 在 `min_row=0`?不——上边 Number 应在 row 1(= 第 2 行),Name 在 row 0(= 第 1 行,封装信息行之下)。
|
||||||
|
|
||||||
|
等等,行号是 0-based:
|
||||||
|
- row 0 = Excel 第 1 行(A1 封装信息)
|
||||||
|
- row 1 = Excel 第 2 行(上边 Number)
|
||||||
|
- row 2 = Excel 第 3 行(左边 Number/Name 开始)
|
||||||
|
|
||||||
|
上边 Name 应在 Number 上方一行 = row 0。但 row 0 的 A1 是封装信息,B1..N1 才是上边 Name。这完全可行。
|
||||||
|
|
||||||
|
解析时 `min_row` 会 = 0(因为 row 0 的 B1..N1 有上边 Name 数据),上边 Number 在 row 1:
|
||||||
|
- Name 查找:`cells.get((min_row, c))` → `cells.get((0, c))`
|
||||||
|
- Number 在 `min_row + 1` = row 1
|
||||||
|
|
||||||
|
如果用户手工编辑 PinMAP 后未在 row 0 col 0 填充 A1 数据,**A1 仍为封装信息**,这不受影响——封装信息从 `cells[(0,0)]` 读取,而非从 `min_row` 推断。
|
||||||
|
|
||||||
|
### 4.4 BUG-006 修改:`Code/src/pinmap_generator.py`
|
||||||
|
|
||||||
|
**文件**: `Code/src/pinmap_generator.py`
|
||||||
|
|
||||||
|
仅需更新注释,`get_name_cell()` 调用已传递 `cols` 参数,无需额外改动。(上边现在不需要 cols 来判断角点例外,但 `cols` 参数可以保留或移除。)
|
||||||
|
|
||||||
|
### 4.5 BUG-006 修改:测试固定件 `Test/fixtures/sample_4x4.xlsx`
|
||||||
|
|
||||||
|
**文件**: `Test/fixtures/sample_4x4.xlsx`
|
||||||
|
|
||||||
|
这是 MAP→List 测试的输入文件,需要更新为新的布局格式。当前 sample_4x4.xlsx 使用 v1.5.4 布局,需要改为 v1.5.5 布局后再生成。
|
||||||
|
|
||||||
|
### 4.6 BUG-006 修改:`Code/src/test_pinmap.py`
|
||||||
|
|
||||||
|
**文件**: `Code/src/test_pinmap.py`(如果有需要更新的测试数据)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 修改影响范围汇总
|
||||||
|
|
||||||
|
| 文件 | BUG | 修改类型 | 风险 | 预计工作量 |
|
||||||
|
|------|-----|---------|------|-----------|
|
||||||
|
| `Code/src/main.py` | BUG-005 | 模板搜索路径修正 | 低 | 5 分钟 |
|
||||||
|
| `Code/src/pinmap_layout.py` | BUG-006 | `get_name_cell("top")` 简化 | 低 | 5 分钟 |
|
||||||
|
| `Code/src/pinmap_parser.py` | BUG-006 | 上边 Name 查找修改 | 中 | 15 分钟 |
|
||||||
|
| `Test/fixtures/sample_4x4.xlsx` | BUG-006 | 更新为 v1.5.5 布局 | 低 | 10 分钟 |
|
||||||
|
| `Test/run_tests.py` | BUG-006 | 可能需要更新验证逻辑 | 中 | 15 分钟 |
|
||||||
|
| `docs/bugs.md` | BUG-005/006 | 更新状态 | 低 | 5 分钟 |
|
||||||
|
| `CHANGELOG.md` | BUG-005/006 | 记录版本变更 | 低 | 5 分钟 |
|
||||||
|
| **合计** | | | | **~60 分钟** |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 修改后的预期行为
|
||||||
|
|
||||||
|
### 6.1 BUG-005 修复后
|
||||||
|
|
||||||
|
- 程序运行时能找到 `Code/src/Template/PinList-Template.xlsx` 和 `Code/src/Template/PinMAP-Template.xlsx`
|
||||||
|
- 输出文件应用模板的字体、边框、列宽、行高等样式
|
||||||
|
|
||||||
|
### 6.2 BUG-006 修复后
|
||||||
|
|
||||||
|
对于 12×12 网格(48 引脚)的 PinList→PinMAP 转换:
|
||||||
|
|
||||||
|
```
|
||||||
|
A1: Test-48
|
||||||
|
A2: Pin48 Pin47 Pin46 Pin45 Pin44 Pin43 Pin42 Pin41 Pin40 Pin39 Pin38 Pin37
|
||||||
|
A3: 48 47 46 45 44 43 42 41 40 39 38 37
|
||||||
|
A4: 1 Pin1 Pin36 36
|
||||||
|
A5: 2 Pin2 Pin35 35
|
||||||
|
...
|
||||||
|
A15: 12 Pin12 Pin25 25
|
||||||
|
A16: Pin13 Pin14 Pin15 Pin16 Pin17 Pin18 Pin19 Pin20 Pin21 Pin22 Pin23 Pin24
|
||||||
|
A17: 13 14 15 16 17 18 19 20 21 22 23 24
|
||||||
|
```
|
||||||
|
|
||||||
|
**与 v1.5.4 对比**:
|
||||||
|
- v1.5.4: 上边 Name 在 row 2,与左边 Name 同行 → 3 条边数据混在 Excel 第 3 行
|
||||||
|
- v1.5.5: 上边 Name 在 row 0,上边 Number 在 row 1 → 上边完全独立
|
||||||
|
- v1.5.5: 左/右边在 row 2~13 同行(正常行为——左右对称的矩形封装)
|
||||||
|
- v1.5.5: 下边 Name 在 row 14、Number 在 row 15 → 下边完全独立
|
||||||
|
|
||||||
|
**用户报告的三个问题全部解决**:
|
||||||
|
1. "C~M 列填入了 PinName" → 不再出现(上边 Name 在独立的 row 0)
|
||||||
|
2. "Pin47 错写为 Pin36" → Pin47 的 Name/Number 在 row 0/1,与 Pin36 的 row 3 分离
|
||||||
|
3. "Pin37 名称出现在最右侧" → Pin37 Name 在 B1(上边行),不与 Pin36 的 M3(右边行) 混淆
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 总结
|
||||||
|
|
||||||
|
1. **BUG-005**:v1.5.4 只改了文件名,没改搜索路径。模板在 `Code/src/Template/` 下,但代码在项目根目录找。修复:修改 `_find_pinlist_template_path()` 和 `_find_pinmap_template_path()` 的搜索路径。
|
||||||
|
|
||||||
|
2. **BUG-006**:v1.5.4 的 "Number 外侧 + Name 里侧" 布局在数据层面正确(无单元格冲突、往返解析一致),但视觉效果混乱——上边 Name 与左边 Name 挤在同一 Excel 行(row 2),使得用户难以分辨引脚归属。修复:将上边 Name 移至 row 0(Excel 第 1 行),与上边 Number(row 1)形成独立区块,与其他边完全分离。此方案比 v1.5.4 的角点例外方案更简洁,无需 cols 参数。
|
||||||
|
|
||||||
|
3. **总计工作量**:约 1 小时。
|
||||||
|
|
||||||
|
4. **风险评估**:低。修改集中在布局生成的上边 Name 坐标和 parser 中的对应查找逻辑,不涉及核心的周长公式、边分配、数据验证等逻辑。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*文档结束 — v1.5.5 修改评估*
|
||||||
572
docs/modification-assessment-v1.6.md
Normal file
572
docs/modification-assessment-v1.6.md
Normal file
@@ -0,0 +1,572 @@
|
|||||||
|
# PinMAP ↔ PinList 双向转换器 — v1.6 整改架构评估
|
||||||
|
|
||||||
|
> **版本**: v1.6 (针对 F013–F017 五项 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_row,Number 在 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 3),Number 在 `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 ─────────────────────────────────
|
||||||
|
# 检测上边布局:哪种布局由数据内容决定
|
||||||
|
# 布局 A:Name 在 min_row(上方),Number 在 min_row+1(下方)
|
||||||
|
# 例:Row 1: Pin60 Pin59 ... Pin46, Row 2: 60 59 ... 46
|
||||||
|
# 布局 B:Number 在 min_row(上方),Name 在 min_row+1(下方)
|
||||||
|
# 例:Row 1: 60 59 ... 46, Row 2: Pin60 Pin59 ... Pin46
|
||||||
|
|
||||||
|
min_row_for_top = min_row # 可能是 1(A1 被排除后)
|
||||||
|
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:
|
||||||
|
# 布局 B:Number 在上,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:
|
||||||
|
# 布局 A:Name 在上,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 列 = PinName,B 列 = 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 2(bold),其他用 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 示例 PinList(60 个引脚)作为输入,验证生成的 PinMAP 结构与示例 PinMAP 一致。
|
||||||
|
|
||||||
|
**示例 PinList 结构**:
|
||||||
|
```
|
||||||
|
QFN60
|
||||||
|
Pin1,1
|
||||||
|
Pin2,2
|
||||||
|
...
|
||||||
|
Pin60,60
|
||||||
|
```
|
||||||
|
|
||||||
|
**示例 PinMAP 预期结构**(基于用户提供的 CSV):
|
||||||
|
- 12×12 网格(QFN60 环形布局)
|
||||||
|
- Left 边:Pin1–Pin12(row 4-15, col A/B)
|
||||||
|
- Bottom 边:Pin13–Pin18 + Pin28–Pin33(row 16-17, 中间空列对应无引脚位置)
|
||||||
|
- Right 边:Pin34–Pin45(row 15→4, col 倒数两列)
|
||||||
|
- Top 边:Pin46–Pin60(row 3, col 倒数→前;row 2 为 Numbers)
|
||||||
|
- 内部为空(QFN 封装中心无引脚)
|
||||||
|
|
||||||
|
### 6.2 关键问题:60 引脚的环形布局 ≠ 12×12 全周长
|
||||||
|
|
||||||
|
12×12 网格的全周长 = (12+12)×2 = 48。但 QFN60 有 **60 个引脚**。这意味着用户使用的"12×12 网格"并不是严格的矩形周长概念。
|
||||||
|
|
||||||
|
重新分析用户 PinMAP:
|
||||||
|
- Left 边:12 个引脚(Pin1–Pin12)
|
||||||
|
- Right 边:12 个引脚(Pin34–Pin45)
|
||||||
|
- Top 边:15 个引脚(Pin46–Pin60,跨越 15 列)
|
||||||
|
- Bottom 边:21 个引脚(Pin13–Pin33,跨越 21 列?不对,重看)
|
||||||
|
|
||||||
|
仔细看用户实际 PinMAP 布局:
|
||||||
|
- Left:12 个
|
||||||
|
- Bottom:12 个(Pin13–Pin18=6 + Pin24–Pin33?不对)
|
||||||
|
|
||||||
|
让我重新分析 CSV 模式:
|
||||||
|
- Left col A/B:12 pins (Pin1–Pin12)
|
||||||
|
- Right col 最后两列:12 pins (Pin34–Pin45)
|
||||||
|
- Top row 2-3:15 pins (Pin46–Pin60)
|
||||||
|
- Bottom 倒数第1-2行:15 pins? → 实际底部仅 Pin13–Pin18=6 + ... 需要确认
|
||||||
|
|
||||||
|
实际上用户 CSV 格式是 12×12(12行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 (Pin1–Pin60)
|
||||||
|
- 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 个引脚,顺序 Pin1–Pin60,封装信息 "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 序号 Pin1–Pin60 完整无缺失
|
||||||
|
4. 封装信息 "QFN60 ..." 正确提取
|
||||||
|
5. PinName 与示例一致
|
||||||
|
|
||||||
|
### 7.2 测试用例构建
|
||||||
|
|
||||||
|
**输入**:用户提供的 QFN60 PinMAP CSV(转换为 Excel 或直接用当前 xls_reader 兼容的格式)
|
||||||
|
|
||||||
|
由于当前解析器读取的是 Excel 格式(.xls/.xlsx),需要先构建测试用的 cell dictionary。
|
||||||
|
|
||||||
|
**测试 F017-1: QFN60 PinMAP 解析**
|
||||||
|
|
||||||
|
基于用户提供的 CSV 构建 cells dict(0-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 (Pin46–Pin60) 都被正确识别且有正确的 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 整改架构评估*
|
||||||
@@ -17,3 +17,19 @@
|
|||||||
| T016 | 测试验证 v1.5 | test-architect/test-executor/test-reporter | 已完成 | 测试验证 | F009-F012 | 2026-06-06 | 2026-06-06 |
|
| T016 | 测试验证 v1.5 | test-architect/test-executor/test-reporter | 已完成 | 测试验证 | F009-F012 | 2026-06-06 | 2026-06-06 |
|
||||||
| T017 | 文档生成 v1.5 | doc-gen-agent | 已完成 | 文档编写 | F009-F012 | 2026-06-06 | 2026-06-06 |
|
| T017 | 文档生成 v1.5 | doc-gen-agent | 已完成 | 文档编写 | F009-F012 | 2026-06-06 | 2026-06-06 |
|
||||||
| T018 | 打包发布 v1.5 | package-release-agent | 已完成 | 打包发布 | F009-F012 | 2026-06-06 | 2026-06-06 | Release 已创建 + zip 附件已上传 |
|
| T018 | 打包发布 v1.5 | package-release-agent | 已完成 | 打包发布 | F009-F012 | 2026-06-06 | 2026-06-06 | Release 已创建 + zip 附件已上传 |
|
||||||
|
| T019 | 架构评估 v1.5.4 Bug 修复 | script-architect | 已完成 | 架构评估 | BUG-005, BUG-006 | 2026-06-09 | 2026-06-09 | 方案已确认 ✅ |
|
||||||
|
| T020 | 编码实现 v1.5.4 Bug 修复 | python-coding-agent | 已完成 | 编码实现 | BUG-005, BUG-006 | 2026-06-09 | 2026-06-09 |
|
||||||
|
| T021 | 测试验证 v1.5.4 | test-executor | 已完成 | 测试验证 | BUG-005, BUG-006 | 2026-06-09 | 2026-06-09 |
|
||||||
|
| T022 | 文档生成 v1.5.4 | doc-gen-agent | 已完成 | 文档编写 | BUG-005, BUG-006 | 2026-06-09 | 2026-06-09 |
|
||||||
|
| T023 | 打包发布 v1.5.4 | package-release-agent | 已完成 | 打包发布 | BUG-005, BUG-006 | 2026-06-09 | 2026-06-09 | Release 已创建 + zip 附件已上传 |
|
||||||
|
| T024 | 修复发布包文件结构 v1.5.4 | router-agent | 已完成 | 打包修复 | - | 2026-06-09 | 2026-06-09 | 已重新打包并上传,结构恢复为 Code/src/ 格式 |
|
||||||
|
| 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 | 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 完成 |
|
||||||
|
|||||||
BIN
pinmap-to-pinlist-v1.5.5.zip
Normal file
BIN
pinmap-to-pinlist-v1.5.5.zip
Normal file
Binary file not shown.
BIN
pinmap-to-pinlist-v1.6.0.zip
Normal file
BIN
pinmap-to-pinlist-v1.6.0.zip
Normal file
Binary file not shown.
Reference in New Issue
Block a user