v1.5.4 Bug 修复:模板文件名修正 + 布局重设计

BUG-005: 模板文件名改为 PinMAP-Template.xlsx / PinList-Template.xlsx
BUG-006: 布局改为 Number 外侧 + Name 里侧(v1.5.4 最终版)
- 从边界往中心:第1圈=Number,第2圈=Name
- 上边角点例外处理,15种网格无冲突
- 18/18 单元测试 + 37/37 集成测试全部通过
This commit is contained in:
2026-06-09 08:27:11 +08:00
parent 91e1d93e18
commit d635ddbebe
19 changed files with 647 additions and 237 deletions

12
.gitignore vendored
View File

@@ -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/

View File

@@ -1,5 +1,41 @@
# Changelog # Changelog
## [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
### ✨ 功能新增 ### ✨ 功能新增

View File

@@ -33,44 +33,44 @@ 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. 与 run.bat 同级的根目录
2. 当前工作目录 2. 当前工作目录
""" """
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/ root_dir = os.path.dirname(os.path.dirname(src_dir)) # pinmap-to-pinlist/
template_path = os.path.join(root_dir, "BallList-Template.xlsx") template_path = os.path.join(root_dir, "PinList-Template.xlsx")
if os.path.exists(template_path): if os.path.exists(template_path):
return template_path return template_path
cwd_template = os.path.join(os.getcwd(), "BallList-Template.xlsx") 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. 与 run.bat 同级的根目录
2. 当前工作目录 2. 当前工作目录
""" """
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/ root_dir = os.path.dirname(os.path.dirname(src_dir)) # pinmap-to-pinlist/
template_path = os.path.join(root_dir, "BallMAP-Template.xlsx") template_path = os.path.join(root_dir, "PinMAP-Template.xlsx")
if os.path.exists(template_path): if os.path.exists(template_path):
return template_path return template_path
cwd_template = os.path.join(os.getcwd(), "BallMAP-Template.xlsx") 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
@@ -171,17 +171,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 +281,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,

View File

@@ -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):

View File

@@ -12,7 +12,26 @@ 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.4: 从网格边界往中心走,第一圈全是 Number第二圈全是 Name
每条边独立包含其端点,所有单元格互不冲突。
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: (2, cols-1..2) [interior, reverse order]
+ (1, 0) [top-left corner exception]
+ (1, cols+1) [top-right corner exception]
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 +105,29 @@ def calculate_layout(
top_pins = entries[idx: idx + top_count] top_pins = entries[idx: idx + top_count]
# ── 计算单元格坐标 ──────────────────────────────────────────── # ── 计算单元格坐标v1.5.4Number 外侧 + Name 里侧,无冲突)──
# #
# 网格坐标体系0-based # 网格坐标体系0-based
# 方形区域:行 [1..rows],列 [0..cols] # 从网格边界往中心走,第一圈全是 Number第二圈全是 Name
# 左边: 序号在 (r, 0), Name 在 (r, 1) 其中 r ∈ [1, rows]
# 下边: 序号在 (rows, c), Name 在 (rows-1, c) 其中 c ∈ [1, cols]
# 右边: 序号在 (r, cols), Name 在 (r, cols-1) 其中 r ∈ [rows, 1] 逆序
# 上边: 序号在 (1, c), Name 在 (2, c) 其中 c ∈ [cols, 1] 逆序
# #
# v1.3: 每条边独立包含其端点,角点单元格会被两条边共享 # 左边: Number (r, 0) r ∈ [2, rows+1] Name (r, 1)
# 下边: Number (rows+3, c) c ∈ [1, cols] Name (rows+2, c)
# 右边: Number (r, cols+1) r ∈ [rows+1, 2] Name (r, cols) 逆序
# 上边: Number (1, c) c ∈ [cols, 1] Name — 见 get_name_cell 逆序
# 上边 Name: (2, cols-1..2) 内部 + (1,0)(1,cols+1) 角点例外
# #
# Pin1: Number (2,0) = A3, Name (2,1) = B3 — 左上角
# 左边:从上到下 # 左边:从上到下 (rows 个)
left_cells = [(r, 0) for r in range(1, rows + 1)] left_cells = [(r, 0) for r in range(2, rows + 2)]
# 下边:从左到右 # 下边:从左到右 (cols 个)Number 在最底行 rows+3
bottom_cells = [(rows, c) for c in range(1, cols + 1)] bottom_cells = [(rows + 3, 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 + 1, 1, -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 +144,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.4: 第二圈 Name 紧挨第一圈 Number 内侧。
上边角点会有例外(位于 row 1以避免与左/右边 Name 冲突。
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
网格列数(上边检测角点例外时需要),默认 0
Returns Returns
------- -------
@@ -142,12 +168,19 @@ 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 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)
else: else:
raise LayoutError(f"未知的边名称: {edge_name}") raise LayoutError(f"未知的边名称: {edge_name}")

View File

@@ -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.4: 支持 Number-外侧 + Name-里侧的双圈布局解析。
Usage Usage
----- -----
>>> from pinmap_parser import parse_pinmap >>> from pinmap_parser import parse_pinmap
@@ -82,40 +84,55 @@ 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 (v1.5.4 layout) ──────────────
# 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.
# #
# v1.5.4 layout:
# 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 : number at (min_row, c), name at (min_row+1, c)
# BUT corner cols (c=min_col, c=max_col) have exceptional
# names at (min_row-1, min_col) and (min_row-1, max_col+1)
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: standard lookup at (min_row+1, c) for interior cols.
# Corner names are at special positions:
# - top-left corner name at (min_row, min_col) → Number at (min_row, min_col+1)
# - top-right corner name at (min_row, max_col) → Number at (min_row, max_col-1)
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((min_row + 1, c), "")
if name and str(name).strip(): if name and str(name).strip():
name_map[(min_row, c)] = str(name).strip() name_map[(min_row, c)] = str(name).strip()
# Override with corner exceptions if present
left_corner = cells.get((min_row, min_col), "")
if left_corner and str(left_corner).strip():
# Belongs to top-left Number at (min_row, min_col+1)
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():
# Belongs to top-right Number at (min_row, max_col-1)
name_map[(min_row, max_col - 1)] = str(right_corner).strip()
# ── 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).

View File

@@ -16,36 +16,41 @@ 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 (v1.5.4 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 # Left: Number A3..A6 (rows 2..5), Name B3..B6 (rows 2..5)
# C7:3 D7:4 C6:Pin3 D6:Pin4 → bottom edge # Bottom: Number B8..E8 (row 7), Name B7..E7 (row 6)
# F5:5 F4:6 E5:Pin5 E4:Pin6 → right edge # Right: Number F6..F3 (rows 5..2), Name E6..E3 (rows 5..2)
# D2:7 C2:8 D3:Pin7 C3:Pin8 → top edge # Top: Number E2..B2 (row 1), Name D3..C3 (row 2 interior)
# A1: "QFP-44" → package info # + A2 (top-left corner exception), + F2 (top-right exception)
# A1: "QFP-44" = package info
#
# Pin1: Number A3=(2,0), Name B3=(2,1)
cells_4x4 = { cells_4x4 = {
(0, 0): "QFP-44", (0, 0): "QFP-44",
# left edge # left edge (rows 2..5, cols 0..1)
(3, 0): "1", (2, 0): "1", (2, 1): "Pin1",
(4, 0): "2", (3, 0): "2", (3, 1): "Pin2",
(3, 1): "Pin1", (4, 0): "3", (4, 1): "Pin3",
(4, 1): "Pin2", (5, 0): "4", (5, 1): "Pin4",
# bottom edge # bottom edge (rows 6..7, cols 1..4)
(6, 2): "3", (6, 1): "Pin5", (6, 2): "Pin6", (6, 3): "Pin7", (6, 4): "Pin8",
(6, 3): "4", (7, 1): "5", (7, 2): "6", (7, 3): "7", (7, 4): "8",
(5, 2): "Pin3", # right edge (rows 5..2, cols 4..5)
(5, 3): "Pin4", (5, 4): "Pin9", (5, 5): "9",
# right edge (4, 4): "Pin10", (4, 5): "10",
(4, 5): "5", (3, 4): "Pin11", (3, 5): "11",
(3, 5): "6", (2, 4): "Pin12", (2, 5): "12",
(4, 4): "Pin5", # top edge (row 1 Number, row 2 interior Name + corner exceptions)
(3, 4): "Pin6", # pin13: (1,4) Number, Name corner exception at (1,5)
# top edge # pin14: (1,3) Number, Name at (2,3)
(1, 3): "7", # pin15: (1,2) Number, Name at (2,2)
(1, 2): "8", # pin16: (1,1) Number, Name corner exception at (1,0)
(2, 3): "Pin7", (1, 4): "13", (1, 5): "Pin13",
(2, 2): "Pin8", (1, 3): "14", (2, 3): "Pin14",
(1, 2): "15", (2, 2): "Pin15",
(1, 1): "16", (1, 0): "Pin16",
} }
@@ -53,19 +58,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 +117,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[(3, 0)] = "1" # duplicate pin 1 (original at (2,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 +127,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[(7, 2)] = "10" # skip pin 6 (was "6" at (7,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
@@ -224,24 +237,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.4 布局)
F012 要求 v1.5.4 布局
- 边 Name 在 max_row-1序号上方 - 边 Name 在 (2..rows+1, 1)
- 边 Name 在 min_row+1序号下方 - 边 Name 在 (rows+2, 1..cols) ← 倒数第二行
- 右边 Name 在 (rows+1..2, cols)
- 上边 Name: interior (2, cols-1..2) + corner exceptions (1,0)(1,cols+1)
测试策略: 测试策略:
1. 构建 5×520 PinPinList 数据 1. 构建 5×520 PinPinList 数据
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 +259,76 @@ 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. 检查单元格位置 (v1.5.4) ─────────────────────────────
# F012 验证: # 5×5: rows=5, cols=5, 20 pins
# 5×5 网格坐标0-based # 左边: Number (2..6, 0), Name (2..6, 1)
# min_row=1, max_row=5, min_col=0, max_col=5 # 下边: Number (8, 1..5), Name (7, 1..5)
# 预期: # 右边: Number (6..2, 6), Name (6..2, 5)
# 边: 序号 (r,0) Name (r,1) r ∈ [1,5] # 边: Number (1, 5..1), Name interior (2, 4..2),
# 下边: 序号 (5,c) Name (4,c) = max_row-1 c ∈ [1,5] # corner exceptions (1,0) and (1,6)
# 右边: 序号 (r,5) Name (r,4) r ∈ [5,1] 逆序
# 上边: 序号 (1,c) Name (2,c) = min_row+1 c ∈ [5,1] 逆序
# ── 3a. 验证下边 Name 位置 ───────────────────────────────── # ── 3a. 验证下边 Name 位置 (rows+2=7, 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(rows + 3, c) # Number at row 8
name_cell = (rows - 1, c) # (4, c) = max_row-1 name_ref = rc_to_cell_ref(rows + 2, c) # Name at row 7
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} (rows+2), 但未找到。Number 在 {num_ref}"
f"但未找到。序号在 {num_ref}"
) )
# ── 3b. 验证上边 Name 位置 ───────────────────────────────── # ── 3b. 验证上边 Name 位置 ─────────────────────────────────
# 上边序号在 (1, 5..1)Name 应在 (2, 5..1) = min_row+1 # Top pin layout: c=5 → (1,0)? No, top-right exception at (1,6)
for c in range(cols, 0, -1): # c=4 → (2,4), c=3 → (2,3), c=2 → (2,2)
num_cell = (1, c) # min_row=1 # c=1 → (1,0) corner exception
name_cell = (2, c) # min_row+1=2 for idx, c in enumerate(range(cols, 0, -1)):
num_ref = rc_to_cell_ref(num_cell[0], num_cell[1]) num_ref = rc_to_cell_ref(1, c) # Number at row 1
name_ref = rc_to_cell_ref(name_cell[0], name_cell[1]) assert num_ref in data, f"上边 Number {num_ref} 缺失"
if c == cols: # rightmost = top-right corner
name_ref = rc_to_cell_ref(1, cols + 1) # exception
elif c == 1: # leftmost = top-left corner
name_ref = rc_to_cell_ref(1, 0) # exception
else: # interior
name_ref = rc_to_cell_ref(2, c)
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}, 但未找到。Number 在 {num_ref}"
f"但未找到。序号在 {num_ref}"
) )
# ── 3c. 验证左边 Name 位置 ────────────────────────────────── # ── 3c. 验证左边 Name 位置 (2..6, 1) ───────────────────
for r in range(1, rows + 1): for r in range(2, rows + 2):
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 位置 (6..2, 5) ────────────────────
assert name_ref in data, f"F012: 左边 Name {name_ref} 缺失" for r in range(rows + 1, 1, -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 +337,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 +347,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 +360,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")

View File

@@ -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 1Name row 2角点例外
- 左边Number col 0Name col 1
- 下边Number row rows+3Name row rows+2
- 右边Number col cols+1Name 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`
- 模板格式提取:字体、边框、填充、对齐、列宽、行高
## 许可证 ## 许可证
内部项目 内部项目

View 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

Binary file not shown.

BIN
Test/fixtures/PinMAP-Template.xlsx vendored Normal file

Binary file not shown.

Binary file not shown.

View File

@@ -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("无模板完整流程正常")

View File

@@ -1,51 +1,19 @@
# PinMAP ↔ PinList 双向转换器 测试报告 (v1.5.0) # PinMAP ↔ PinList 双向转换器 测试报告
> **版本**: v1.5.0 > **日期**: 2026-06-09
> **日期**: 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: 无模板完整流程
- **结果**: ✅ 通过 - **结果**: ✅ 通过

View File

@@ -6,3 +6,5 @@
| 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.xlsxPinList 模板为 PinList-Template.xlsx | 引用的模板文件名错误 | 已修复 | - |
| BUG-006 | 高 | PinList→PinMAP→PinList 双向转换数据错位 | 15×15 PinMapPinList→PinMAP→PinList 双向转换 | 最终 PinList 与原始 PinList 一致 | 序号1从A4错位到A2序号16从C20错位到B16双向转换结果不一致 | 已修复 | - |

View 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 2Number 在 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-006Number 外侧 + 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 通用坐标公式Python0-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 坐标(紧挨 Numberv1.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-006v1.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 个测试通过*

View File

@@ -17,3 +17,8 @@
| 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 | - |