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:
12
.gitignore
vendored
12
.gitignore
vendored
@@ -16,6 +16,18 @@ build/
|
||||
.DS_Store
|
||||
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
|
||||
.vscode/
|
||||
.idea/
|
||||
|
||||
36
CHANGELOG.md
36
CHANGELOG.md
@@ -1,5 +1,41 @@
|
||||
# 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
|
||||
|
||||
### ✨ 功能新增
|
||||
|
||||
@@ -33,44 +33,44 @@ def wait_for_exit():
|
||||
|
||||
# ── Path helpers ────────────────────────────────────────────────────
|
||||
|
||||
def _find_balllist_template_path() -> str | None:
|
||||
"""查找根目录下的 BallList-Template.xlsx。
|
||||
def _find_pinlist_template_path() -> str | None:
|
||||
"""查找根目录下的 PinList-Template.xlsx。
|
||||
|
||||
MAP→List 输出使用 BallList 模板(而非旧 PinMAP 模板)。
|
||||
MAP→List 输出使用 PinList 模板。
|
||||
搜索顺序:
|
||||
1. 与 run.bat 同级的根目录
|
||||
2. 当前工作目录
|
||||
"""
|
||||
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, "BallList-Template.xlsx")
|
||||
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(), "BallList-Template.xlsx")
|
||||
cwd_template = os.path.join(os.getcwd(), "PinList-Template.xlsx")
|
||||
if os.path.exists(cwd_template):
|
||||
return cwd_template
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _find_ballmap_template_path() -> str | None:
|
||||
"""查找根目录下的 BallMAP-Template.xlsx。
|
||||
def _find_pinmap_template_path() -> str | None:
|
||||
"""查找根目录下的 PinMAP-Template.xlsx。
|
||||
|
||||
List→MAP 输出使用 BallMAP 模板(而非旧 PinMAP 模板)。
|
||||
List→MAP 输出使用 PinMAP 模板。
|
||||
搜索顺序:
|
||||
1. 与 run.bat 同级的根目录
|
||||
2. 当前工作目录
|
||||
"""
|
||||
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, "BallMAP-Template.xlsx")
|
||||
template_path = os.path.join(root_dir, "PinMAP-Template.xlsx")
|
||||
|
||||
if os.path.exists(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):
|
||||
return cwd_template
|
||||
|
||||
@@ -171,17 +171,17 @@ def run_map_to_list(filepath: str):
|
||||
data[f'A{row}'] = pin_name
|
||||
data[f'B{row}'] = str(pin_num)
|
||||
|
||||
# 尝试读取 BallList 模板样式(F009)
|
||||
template_path = _find_balllist_template_path()
|
||||
# 尝试读取 PinList 模板样式
|
||||
template_path = _find_pinlist_template_path()
|
||||
template_style = None
|
||||
if template_path:
|
||||
template_style = read_template_styles(template_path)
|
||||
if template_style:
|
||||
print(f"[INFO] 已加载 BallList 模板样式: {template_path}")
|
||||
print(f"[INFO] 已加载 PinList 模板样式: {template_path}")
|
||||
else:
|
||||
print("[WARN] BallList 模板文件存在但解析失败,使用默认样式")
|
||||
print("[WARN] PinList 模板文件存在但解析失败,使用默认样式")
|
||||
else:
|
||||
print("[INFO] 未检测到 BallList-Template.xlsx,使用默认样式")
|
||||
print("[INFO] 未检测到 PinList-Template.xlsx,使用默认样式")
|
||||
|
||||
if template_style is not None:
|
||||
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}")
|
||||
|
||||
try:
|
||||
# 尝试读取 BallMAP 模板样式(F010)
|
||||
template_path = _find_ballmap_template_path()
|
||||
# 尝试读取 PinMAP 模板样式
|
||||
template_path = _find_pinmap_template_path()
|
||||
template_style = None
|
||||
if template_path:
|
||||
template_style = read_template_styles(template_path)
|
||||
if template_style:
|
||||
print(f"[INFO] 已加载 BallMAP 模板样式: {template_path}")
|
||||
print(f"[INFO] 已加载 PinMAP 模板样式: {template_path}")
|
||||
else:
|
||||
print("[WARN] BallMAP 模板文件存在但解析失败,使用默认样式")
|
||||
print("[WARN] PinMAP 模板文件存在但解析失败,使用默认样式")
|
||||
else:
|
||||
print("[INFO] 未检测到 BallMAP-Template.xlsx,使用默认样式")
|
||||
print("[INFO] 未检测到 PinMAP-Template.xlsx,使用默认样式")
|
||||
|
||||
generate_pinmap(
|
||||
entries=entries,
|
||||
|
||||
@@ -56,12 +56,11 @@ def generate_pinmap(
|
||||
# 先写入 PinName 单元格
|
||||
for edge_name, edge in layout.items():
|
||||
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])
|
||||
data[name_ref] = pin_name if pin_name and pin_name.strip() else "NC"
|
||||
|
||||
# 再写入序号单元格(覆盖同位置的名字,确保序号优先)
|
||||
# v1.3: 角点单元格被两条边共享,需写入两个引脚序号
|
||||
# 再写入序号单元格(v1.5.4:无边角共享,每个序号独占一个单元格)
|
||||
cell_pins: dict[str, list[str]] = {}
|
||||
for edge_name, edge in layout.items():
|
||||
for (pin_num, pin_name), num_cell in zip(edge.pins, edge.cells):
|
||||
|
||||
@@ -12,7 +12,26 @@ Edge assignment (counter-clockwise, top-left = pin 1):
|
||||
|
||||
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
|
||||
@@ -86,28 +105,29 @@ def calculate_layout(
|
||||
|
||||
top_pins = entries[idx: idx + top_count]
|
||||
|
||||
# ── 计算单元格坐标 ────────────────────────────────────────────
|
||||
# ── 计算单元格坐标(v1.5.4:Number 外侧 + Name 里侧,无冲突)──
|
||||
#
|
||||
# 网格坐标体系(0-based):
|
||||
# 方形区域:行 [1..rows],列 [0..cols]
|
||||
# 左边: 序号在 (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] 逆序
|
||||
# 从网格边界往中心走,第一圈全是 Number,第二圈全是 Name
|
||||
#
|
||||
# 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 — 左上角
|
||||
|
||||
# 左边:从上到下
|
||||
left_cells = [(r, 0) for r in range(1, rows + 1)]
|
||||
# 左边:从上到下 (rows 个)
|
||||
left_cells = [(r, 0) for r in range(2, rows + 2)]
|
||||
|
||||
# 下边:从左到右
|
||||
bottom_cells = [(rows, c) for c in range(1, cols + 1)]
|
||||
# 下边:从左到右 (cols 个),Number 在最底行 rows+3
|
||||
bottom_cells = [(rows + 3, c) for c in range(1, cols + 1)]
|
||||
|
||||
# 右边:从下到上(逆序)
|
||||
right_cells = [(r, cols) for r in range(rows, 0, -1)]
|
||||
# 右边:从下到上 (rows 个),Number 在 cols+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)]
|
||||
|
||||
# ── 构建 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 单元格坐标。
|
||||
|
||||
v1.5.4: 第二圈 Name 紧挨第一圈 Number 内侧。
|
||||
上边角点会有例外(位于 row 1),以避免与左/右边 Name 冲突。
|
||||
|
||||
Parameters
|
||||
----------
|
||||
num_cell : tuple[int, int]
|
||||
序号单元格坐标 (row, col) 0-based
|
||||
edge_name : str
|
||||
"left" | "bottom" | "right" | "top"
|
||||
cols : int
|
||||
网格列数(上边检测角点例外时需要),默认 0
|
||||
|
||||
Returns
|
||||
-------
|
||||
@@ -142,12 +168,19 @@ def get_name_cell(num_cell: tuple[int, int], edge_name: str) -> tuple[int, int]:
|
||||
"""
|
||||
r, c = num_cell
|
||||
if edge_name == "left":
|
||||
return (r, c + 1) # Name 在序号右侧
|
||||
return (r, c + 1) # Name 在 Number 右侧 (col 1)
|
||||
elif edge_name == "bottom":
|
||||
return (r - 1, c) # Name 在序号上方
|
||||
return (r - 1, c) # Name 在 Number 上方 (row rows+2)
|
||||
elif edge_name == "right":
|
||||
return (r, c - 1) # Name 在序号左侧
|
||||
return (r, c - 1) # Name 在 Number 左侧 (col cols)
|
||||
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:
|
||||
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
|
||||
counter-clockwise order starting from the top-left corner.
|
||||
|
||||
v1.5.4: 支持 Number-外侧 + Name-里侧的双圈布局解析。
|
||||
|
||||
Usage
|
||||
-----
|
||||
>>> 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():
|
||||
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*
|
||||
# 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)
|
||||
# bottom : number at (max_row, c), name at (max_row-1, c)
|
||||
# right : number at (r, max_col), name at (r, max_col-1)
|
||||
# top : number at (min_row, 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] = {}
|
||||
|
||||
# left edge names
|
||||
# left edge names: adjacent inward (r, min_col+1)
|
||||
for r in range(min_row, max_row + 1):
|
||||
name = cells.get((r, min_col + 1), "")
|
||||
if name and 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):
|
||||
name = cells.get((max_row - 1, c), "")
|
||||
if name and 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):
|
||||
name = cells.get((r, max_col - 1), "")
|
||||
if name and 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):
|
||||
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 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) ──────
|
||||
# Each edge independently includes its endpoints (corners).
|
||||
|
||||
@@ -16,36 +16,41 @@ from pinlist_generator import generate_pinlist
|
||||
from utils import rc_to_cell_ref
|
||||
|
||||
|
||||
# ── 4x4 example from the task description ────────────────────────
|
||||
# 1-based Excel coords → 0-based (row, col):
|
||||
# A4:1 A5:2 B4:Pin1 B5:Pin2 → left edge
|
||||
# C7:3 D7:4 C6:Pin3 D6:Pin4 → bottom edge
|
||||
# F5:5 F4:6 E5:Pin5 E4:Pin6 → right edge
|
||||
# D2:7 C2:8 D3:Pin7 C3:Pin8 → top edge
|
||||
# A1: "QFP-44" → package info
|
||||
# ── 4x4 example (v1.5.4 layout) ───────────────────────────────
|
||||
# Layout: rows=4, cols=4, 16 pins
|
||||
# Left: Number A3..A6 (rows 2..5), Name B3..B6 (rows 2..5)
|
||||
# Bottom: Number B8..E8 (row 7), Name B7..E7 (row 6)
|
||||
# Right: Number F6..F3 (rows 5..2), Name E6..E3 (rows 5..2)
|
||||
# Top: Number E2..B2 (row 1), Name D3..C3 (row 2 interior)
|
||||
# + 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 = {
|
||||
(0, 0): "QFP-44",
|
||||
# left edge
|
||||
(3, 0): "1",
|
||||
(4, 0): "2",
|
||||
(3, 1): "Pin1",
|
||||
(4, 1): "Pin2",
|
||||
# bottom edge
|
||||
(6, 2): "3",
|
||||
(6, 3): "4",
|
||||
(5, 2): "Pin3",
|
||||
(5, 3): "Pin4",
|
||||
# right edge
|
||||
(4, 5): "5",
|
||||
(3, 5): "6",
|
||||
(4, 4): "Pin5",
|
||||
(3, 4): "Pin6",
|
||||
# top edge
|
||||
(1, 3): "7",
|
||||
(1, 2): "8",
|
||||
(2, 3): "Pin7",
|
||||
(2, 2): "Pin8",
|
||||
# left edge (rows 2..5, cols 0..1)
|
||||
(2, 0): "1", (2, 1): "Pin1",
|
||||
(3, 0): "2", (3, 1): "Pin2",
|
||||
(4, 0): "3", (4, 1): "Pin3",
|
||||
(5, 0): "4", (5, 1): "Pin4",
|
||||
# bottom edge (rows 6..7, cols 1..4)
|
||||
(6, 1): "Pin5", (6, 2): "Pin6", (6, 3): "Pin7", (6, 4): "Pin8",
|
||||
(7, 1): "5", (7, 2): "6", (7, 3): "7", (7, 4): "8",
|
||||
# right edge (rows 5..2, cols 4..5)
|
||||
(5, 4): "Pin9", (5, 5): "9",
|
||||
(4, 4): "Pin10", (4, 5): "10",
|
||||
(3, 4): "Pin11", (3, 5): "11",
|
||||
(2, 4): "Pin12", (2, 5): "12",
|
||||
# top edge (row 1 Number, row 2 interior Name + corner exceptions)
|
||||
# pin13: (1,4) Number, Name corner exception at (1,5)
|
||||
# pin14: (1,3) Number, Name at (2,3)
|
||||
# pin15: (1,2) Number, Name at (2,2)
|
||||
# pin16: (1,1) Number, Name corner exception at (1,0)
|
||||
(1, 4): "13", (1, 5): "Pin13",
|
||||
(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)
|
||||
|
||||
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)
|
||||
# → right(bot→top) → top(right→left)
|
||||
expected = [
|
||||
(1, "Pin1", "left"),
|
||||
(2, "Pin2", "left"),
|
||||
(3, "Pin3", "bottom"),
|
||||
(4, "Pin4", "bottom"),
|
||||
(5, "Pin5", "right"),
|
||||
(6, "Pin6", "right"),
|
||||
(7, "Pin7", "top"),
|
||||
(8, "Pin8", "top"),
|
||||
(3, "Pin3", "left"),
|
||||
(4, "Pin4", "left"),
|
||||
(5, "Pin5", "bottom"),
|
||||
(6, "Pin6", "bottom"),
|
||||
(7, "Pin7", "bottom"),
|
||||
(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):
|
||||
p = pm.pins[i]
|
||||
@@ -104,7 +117,7 @@ def test_missing_names_warning():
|
||||
|
||||
def test_duplicate_numbers():
|
||||
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)
|
||||
vr = validate_pinmap(pm)
|
||||
assert not vr.is_valid
|
||||
@@ -114,7 +127,7 @@ def test_duplicate_numbers():
|
||||
|
||||
def test_gap_in_numbers():
|
||||
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)
|
||||
vr = validate_pinmap(pm)
|
||||
assert not vr.is_valid
|
||||
@@ -224,24 +237,19 @@ def test_12pin_square():
|
||||
# ── F012: PinMAP 生成中上/下边 PinName 位置验证 ────────────
|
||||
|
||||
def test_f012_pinname_position():
|
||||
"""验证 PinList→PinMAP 时下/上边 PinName 位置正确。
|
||||
"""验证 PinList→PinMAP 时各边 PinName 位置正确(v1.5.4 布局)。
|
||||
|
||||
F012 要求:
|
||||
- 下边 Name 在 max_row-1(序号上方)
|
||||
- 上边 Name 在 min_row+1(序号下方)
|
||||
v1.5.4 布局:
|
||||
- 左边 Name 在 (2..rows+1, 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×5(20 Pin)PinList 数据
|
||||
2. 生成 PinMAP
|
||||
3. 检查输出 cell 位置
|
||||
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 个引脚) ──────────────────
|
||||
rows, cols = 5, 5
|
||||
@@ -251,89 +259,76 @@ def test_f012_pinname_position():
|
||||
]
|
||||
package_info = "QFN-20"
|
||||
|
||||
# ── 2. 生成 PinMAP(不使用模板,纯逻辑验证) ───────────────
|
||||
# ── 2. 生成 PinMAP ────────────────────────────────────────
|
||||
data = generate_pinmap(
|
||||
entries=entries,
|
||||
rows=rows,
|
||||
cols=cols,
|
||||
package_info=package_info,
|
||||
template_style=None,
|
||||
output_path=None, # 不写入文件
|
||||
output_path=None,
|
||||
)
|
||||
|
||||
# ── 3. 检查单元格位置 ───────────────────────────────────────
|
||||
# F012 验证:
|
||||
# 5×5 网格坐标(0-based):
|
||||
# min_row=1, max_row=5, min_col=0, max_col=5
|
||||
# 预期:
|
||||
# 左边: 序号 (r,0) Name (r,1) r ∈ [1,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] 逆序
|
||||
# ── 3. 检查单元格位置 (v1.5.4) ─────────────────────────────
|
||||
# 5×5: rows=5, cols=5, 20 pins
|
||||
# 左边: Number (2..6, 0), Name (2..6, 1)
|
||||
# 下边: Number (8, 1..5), Name (7, 1..5)
|
||||
# 右边: Number (6..2, 6), Name (6..2, 5)
|
||||
# 上边: Number (1, 5..1), Name interior (2, 4..2),
|
||||
# corner exceptions (1,0) and (1,6)
|
||||
|
||||
# ── 3a. 验证下边 Name 位置 ─────────────────────────────────
|
||||
# 下边序号在 (5, 1..5),Name 应在 (4, 1..5) = max_row-1
|
||||
# ── 3a. 验证下边 Name 位置 (rows+2=7, 1..cols) ──────────
|
||||
for c in range(1, cols + 1):
|
||||
num_cell = (rows, c) # (5, c)
|
||||
name_cell = (rows - 1, c) # (4, c) = max_row-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} 缺失"
|
||||
# Name 单元格应在 max_row-1
|
||||
num_ref = rc_to_cell_ref(rows + 3, c) # Number at row 8
|
||||
name_ref = rc_to_cell_ref(rows + 2, c) # Name at row 7
|
||||
assert num_ref in data, f"下边 Number {num_ref} 缺失"
|
||||
assert name_ref in data, (
|
||||
f"F012: 下边 Name 应在 {name_ref} (max_row-1), "
|
||||
f"但未找到。序号在 {num_ref}"
|
||||
f"下边 Name 应在 {name_ref} (rows+2), 但未找到。Number 在 {num_ref}"
|
||||
)
|
||||
|
||||
# ── 3b. 验证上边 Name 位置 ─────────────────────────────────
|
||||
# 上边序号在 (1, 5..1),Name 应在 (2, 5..1) = min_row+1
|
||||
for c in range(cols, 0, -1):
|
||||
num_cell = (1, c) # min_row=1
|
||||
name_cell = (2, c) # min_row+1=2
|
||||
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} 缺失"
|
||||
# Top pin layout: c=5 → (1,0)? No, top-right exception at (1,6)
|
||||
# c=4 → (2,4), c=3 → (2,3), c=2 → (2,2)
|
||||
# c=1 → (1,0) corner exception
|
||||
for idx, c in enumerate(range(cols, 0, -1)):
|
||||
num_ref = rc_to_cell_ref(1, c) # Number at row 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 name_ref in data, (
|
||||
f"F012: 上边 Name 应在 {name_ref} (min_row+1), "
|
||||
f"但未找到。序号在 {num_ref}"
|
||||
f"上边 Name 应在 {name_ref}, 但未找到。Number 在 {num_ref}"
|
||||
)
|
||||
|
||||
# ── 3c. 验证左边 Name 位置 ──────────────────────────────────
|
||||
for r in range(1, rows + 1):
|
||||
num_cell = (r, 0)
|
||||
name_cell = (r, 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])
|
||||
# ── 3c. 验证左边 Name 位置 (2..6, 1) ───────────────────
|
||||
for r in range(2, rows + 2):
|
||||
num_ref = rc_to_cell_ref(r, 0)
|
||||
name_ref = rc_to_cell_ref(r, 1)
|
||||
assert num_ref in data, f"左边 Number {num_ref} 缺失"
|
||||
assert name_ref in data, f"左边 Name {name_ref} 缺失"
|
||||
|
||||
assert num_ref in data, f"F012: 左边序号 {num_ref} 缺失"
|
||||
assert name_ref in data, f"F012: 左边 Name {name_ref} 缺失"
|
||||
|
||||
# ── 3d. 验证右边 Name 位置 ──────────────────────────────────
|
||||
for r in range(rows, 0, -1):
|
||||
num_cell = (r, cols)
|
||||
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} 缺失"
|
||||
# ── 3d. 验证右边 Name 位置 (6..2, 5) ────────────────────
|
||||
for r in range(rows + 1, 1, -1):
|
||||
num_ref = rc_to_cell_ref(r, cols + 1)
|
||||
name_ref = rc_to_cell_ref(r, cols)
|
||||
assert num_ref in data, f"右边 Number {num_ref} 缺失"
|
||||
assert name_ref in data, f"右边 Name {name_ref} 缺失"
|
||||
|
||||
# ── 4. 往返一致性验证(PinMAP → PinList)────────────────────
|
||||
from utils import cell_ref_to_rc
|
||||
|
||||
# 将 data dict 转换为 PinMAP 解析器可读的 {(row,col): value} 格式
|
||||
cell_data = {}
|
||||
for ref, value in data.items():
|
||||
cell_data[cell_ref_to_rc(ref)] = value
|
||||
|
||||
# 解析回 PinMAP
|
||||
pm = parse_pinmap(cell_data)
|
||||
assert len(pm.pins) == 20, f"往返: 预期 20 引脚,实际 {len(pm.pins)}"
|
||||
|
||||
# 验证引脚序号正确(20 个引脚全部恢复)
|
||||
actual_numbers = sorted([p.number for p in pm.pins])
|
||||
expected_numbers = list(range(1, 21))
|
||||
assert actual_numbers == expected_numbers, (
|
||||
@@ -342,7 +337,6 @@ def test_f012_pinname_position():
|
||||
f" 实际: {actual_numbers}"
|
||||
)
|
||||
|
||||
# 验证 20 个引脚全部恢复
|
||||
validation = validate_pinmap(pm)
|
||||
assert validation.is_valid, (
|
||||
f"往返验证失败: 错误={[e.message for e in validation.errors]}"
|
||||
@@ -353,7 +347,6 @@ def test_f012_pinname_position():
|
||||
f"往返 PinList: 预期 20 行,实际 {len(pinlist.rows)}"
|
||||
)
|
||||
|
||||
# 验证序号从 1 到 20
|
||||
for i, (name, num) in enumerate(pinlist.rows):
|
||||
expected_num = i + 1
|
||||
assert num == expected_num, (
|
||||
@@ -367,18 +360,18 @@ def test_f012_pinname_position():
|
||||
|
||||
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()
|
||||
result2 = _find_ballmap_template_path()
|
||||
result1 = _find_pinlist_template_path()
|
||||
result2 = _find_pinmap_template_path()
|
||||
|
||||
# 返回值要么是 str 要么是 None
|
||||
assert result1 is None or isinstance(result1, str)
|
||||
assert result2 is None or isinstance(result2, str)
|
||||
# 两者应该是不同路径
|
||||
if result1 and result2:
|
||||
assert "BallList" in result1
|
||||
assert "BallMAP" in result2
|
||||
assert "PinList" in result1
|
||||
assert "PinMAP" in result2
|
||||
assert result1 != result2
|
||||
|
||||
print("✓ test_template_path_generation passed")
|
||||
|
||||
19
README.md
19
README.md
@@ -48,6 +48,25 @@ pinmap-to-pinlist/
|
||||
- openpyxl(.xlsx 读写)
|
||||
- 自定义 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):
|
||||
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):
|
||||
filepath = os.path.join(fixture_dir, 'sample_4x4.xlsx')
|
||||
cells = read_xlsx_cells(filepath)
|
||||
pinmap = parse_pinmap(cells)
|
||||
validation = validate_pinmap(pinmap)
|
||||
pinlist = generate_pinlist(pinmap, validation)
|
||||
assert pinlist.package_info, "package_info 不应为空"
|
||||
assert len(pinlist.rows) > 0, "应有引脚数据"
|
||||
# 封装信息 (v1.5.4 布局)
|
||||
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]
|
||||
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)
|
||||
|
||||
@@ -570,19 +578,19 @@ def test_v15_styles(r: TestRunner):
|
||||
from xlsx_writer import write_xlsx_with_style
|
||||
|
||||
try:
|
||||
# ── TC-v1.5-001: MAP→List 加载 BallList 模板 ──
|
||||
# ── TC-v1.5-001: MAP→List 加载 PinList 模板 ──
|
||||
def _tc_v15_001(result):
|
||||
template_path = os.path.join(fixture_dir, 'BallList-Template.xlsx')
|
||||
assert os.path.exists(template_path), f"BallList 模板文件不存在: {template_path}"
|
||||
template_path = os.path.join(fixture_dir, 'PinList-Template.xlsx')
|
||||
assert os.path.exists(template_path), f"PinList 模板文件不存在: {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.borders) > 0, "应有边框定义"
|
||||
assert 0 in style.column_widths, "应有列宽定义"
|
||||
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 无模板降级 ──
|
||||
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)
|
||||
|
||||
# ── TC-v1.5-003: List→MAP 加载 BallMAP 模板 ──
|
||||
# ── TC-v1.5-003: List→MAP 加载 PinMAP 模板 ──
|
||||
def _tc_v15_003(result):
|
||||
template_path = os.path.join(fixture_dir, 'BallMAP-Template.xlsx')
|
||||
assert os.path.exists(template_path), f"BallMAP 模板文件不存在: {template_path}"
|
||||
template_path = os.path.join(fixture_dir, 'PinMAP-Template.xlsx')
|
||||
assert os.path.exists(template_path), f"PinMAP 模板文件不存在: {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.borders) > 0, "应有边框定义"
|
||||
assert 0 in style.row_heights, "应有行高定义"
|
||||
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 无模板降级 ──
|
||||
def _tc_v15_004(result):
|
||||
@@ -616,19 +624,19 @@ def test_v15_styles(r: TestRunner):
|
||||
|
||||
# ── TC-v1.5-005: 两个方向独立使用各自模板 ──
|
||||
def _tc_v15_005(result):
|
||||
bl_path = os.path.join(fixture_dir, 'BallList-Template.xlsx')
|
||||
bm_path = os.path.join(fixture_dir, 'BallMAP-Template.xlsx')
|
||||
bl_path = os.path.join(fixture_dir, 'PinList-Template.xlsx')
|
||||
bm_path = os.path.join(fixture_dir, 'PinMAP-Template.xlsx')
|
||||
|
||||
style_bl = read_template_styles(bl_path)
|
||||
style_bm = read_template_styles(bm_path)
|
||||
|
||||
assert style_bl is not None, "BallList 模板应成功加载"
|
||||
assert style_bm is not None, "BallMAP 模板应成功加载"
|
||||
assert style_bl is not None, "PinList 模板应成功加载"
|
||||
assert style_bm is not None, "PinMAP 模板应成功加载"
|
||||
|
||||
# BallList 有列宽,BallMAP 有行高
|
||||
assert 0 in style_bl.column_widths, "BallList 应有列宽"
|
||||
assert 0 in style_bm.row_heights, "BallMAP 应有行高"
|
||||
result.ok(f"两个模板独立: BL fonts={len(style_bl.fonts)}, BM fonts={len(style_bm.fonts)}")
|
||||
# PinList 有列宽,PinMAP 有行高
|
||||
assert 0 in style_bl.column_widths, "PinList 应有列宽"
|
||||
assert 0 in style_bm.row_heights, "PinMAP 应有行高"
|
||||
result.ok(f"两个模板独立: PL fonts={len(style_bl.fonts)}, PM fonts={len(style_bm.fonts)}")
|
||||
|
||||
r.run("TC-v1.5-005: 两个方向独立使用各自模板", _tc_v15_005)
|
||||
|
||||
@@ -645,7 +653,7 @@ def test_v15_styles(r: TestRunner):
|
||||
|
||||
# ── TC-v1.5-007: 模板字体应用到输出文件 ──
|
||||
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)
|
||||
|
||||
assert style is not None, "模板样式应成功读取"
|
||||
@@ -669,7 +677,7 @@ def test_v15_styles(r: TestRunner):
|
||||
|
||||
# ── TC-v1.5-008: 模板列宽应用到输出文件 ──
|
||||
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)
|
||||
assert style is not None, "模板样式应成功读取"
|
||||
|
||||
@@ -700,7 +708,7 @@ def test_v15_styles(r: TestRunner):
|
||||
|
||||
# ── TC-v1.5-009: 模板行高应用到输出文件 ──
|
||||
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)
|
||||
assert style is not None, "模板样式应成功读取"
|
||||
|
||||
@@ -732,19 +740,19 @@ def test_v15_styles(r: TestRunner):
|
||||
|
||||
# ── TC-v1.5-010: 两个方向使用不同模板各自的格式 ──
|
||||
def _tc_v15_010(result):
|
||||
bl_path = os.path.join(fixture_dir, 'BallList-Template.xlsx')
|
||||
bm_path = os.path.join(fixture_dir, 'BallMAP-Template.xlsx')
|
||||
bl_path = os.path.join(fixture_dir, 'PinList-Template.xlsx')
|
||||
bm_path = os.path.join(fixture_dir, 'PinMAP-Template.xlsx')
|
||||
|
||||
style_bl = read_template_styles(bl_path)
|
||||
style_bm = read_template_styles(bm_path)
|
||||
assert style_bl and style_bm, "两个模板都应该成功加载"
|
||||
|
||||
# MAP->List 方向:用 BallList 模板
|
||||
# MAP->List 方向:用 PinList 模板
|
||||
pinlist_data = {'A1': 'QFP-44', 'A2': 'Pin1', 'B2': '1'}
|
||||
pinlist_path = os.path.join(tmpdir, 'v15_010_pinlist.xlsx')
|
||||
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)]
|
||||
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)
|
||||
@@ -755,17 +763,17 @@ def test_v15_styles(r: TestRunner):
|
||||
with zipfile.ZipFile(pinmap_path, 'r') as zf:
|
||||
pm_styles = zf.read('xl/styles.xml').decode('utf-8')
|
||||
|
||||
assert '楷体' in pl_styles, "BallList 输出应包含楷体"
|
||||
assert '宋体' in pm_styles, "BallMAP 输出应包含宋体"
|
||||
assert '楷体' in pl_styles, "PinList 输出应包含楷体"
|
||||
assert '宋体' in pm_styles, "PinMAP 输出应包含宋体"
|
||||
|
||||
result.ok("两个方向输出字体不同: BL->楷体, BM->宋体")
|
||||
result.ok("两个方向输出字体不同: PinList->楷体, PinMAP->宋体")
|
||||
|
||||
r.run("TC-v1.5-010: 两个方向不同模板各自的格式", _tc_v15_010)
|
||||
|
||||
# ── TC-v1.5-011: 完整往返+模板隔离 ──
|
||||
# ── TC-v1.5-011: 完整往返+模板隔离 (4×4 网格) ──
|
||||
def _tc_v15_011(result):
|
||||
bl_path = os.path.join(fixture_dir, 'BallList-Template.xlsx')
|
||||
bm_path = os.path.join(fixture_dir, 'BallMAP-Template.xlsx')
|
||||
bl_path = os.path.join(fixture_dir, 'PinList-Template.xlsx')
|
||||
bm_path = os.path.join(fixture_dir, 'PinMAP-Template.xlsx')
|
||||
|
||||
style_bl = read_template_styles(bl_path)
|
||||
style_bm = read_template_styles(bm_path)
|
||||
@@ -786,7 +794,8 @@ def test_v15_styles(r: TestRunner):
|
||||
|
||||
pkg2, entries2 = parse_pinlist(pinlist_path)
|
||||
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_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')
|
||||
with zipfile.ZipFile(pinmap_path, 'r') as zf:
|
||||
pm_xml = zf.read('xl/styles.xml').decode('utf-8')
|
||||
assert '楷体' in pl_xml, "中间 PinList 应使用 BallList 的楷体"
|
||||
assert '宋体' in pm_xml, "最终 PinMAP 应使用 BallMAP 的宋体"
|
||||
assert '楷体' in pl_xml, "中间 PinList 应使用 PinList 模板的楷体"
|
||||
assert '宋体' in pm_xml, "最终 PinMAP 应使用 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)
|
||||
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 输出文件应存在"
|
||||
|
||||
result.ok("无模板完整流程正常")
|
||||
|
||||
@@ -1,51 +1,19 @@
|
||||
# PinMAP ↔ PinList 双向转换器 测试报告 (v1.5.0)
|
||||
# PinMAP ↔ PinList 双向转换器 测试报告
|
||||
|
||||
> **版本**: v1.5.0
|
||||
> **日期**: 2026-06-06
|
||||
> **测试类型**: 单元测试 + 集成测试 + 端到端测试
|
||||
> **日期**: 2026-06-09
|
||||
> **测试类型**: 集成测试 + 端到端测试
|
||||
> **测试环境**: 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 |
|
||||
| List->MAP 新增 | 17 | 17 | 0 |
|
||||
| v1.5 模板/样式集成 | 14 | 14 | 0 |
|
||||
| **总计** | **55** | **55** | **0** |
|
||||
| **总计** | **37** | **37** | **0** |
|
||||
|
||||
---
|
||||
|
||||
@@ -53,7 +21,7 @@ v1.5.0 引入三项核心变更:
|
||||
|
||||
### TC-MAP-001: 标准4x4 PinMAP转换
|
||||
- **结果**: ✅ 通过
|
||||
- **详情**: 封装=QFP12, Pin数=12, 序号递增
|
||||
- **详情**: 封装=QFP-16, Pin数=16, 序号 1-16, 引脚名=Pin1..Pin16
|
||||
|
||||
### TC-MAP-002: 长方形PinMAP转换
|
||||
- **结果**: ✅ 通过
|
||||
@@ -147,7 +115,7 @@ v1.5.0 引入三项核心变更:
|
||||
|
||||
## 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
|
||||
|
||||
@@ -155,7 +123,7 @@ v1.5.0 引入三项核心变更:
|
||||
- **结果**: ✅ 通过
|
||||
- **详情**: 无模板文件时优雅返回 None
|
||||
|
||||
### TC-v1.5-003: List->MAP 加载 BallMAP 模板
|
||||
### TC-v1.5-003: List->MAP 加载 PinMAP 模板
|
||||
- **结果**: ✅ 通过
|
||||
- **详情**: 模板加载成功: fonts=2, borders=2, row_height=25.0
|
||||
|
||||
@@ -165,7 +133,7 @@ v1.5.0 引入三项核心变更:
|
||||
|
||||
### TC-v1.5-005: 两个方向独立使用各自模板
|
||||
- **结果**: ✅ 通过
|
||||
- **详情**: 两个模板独立: BL fonts=2, BM fonts=2
|
||||
- **详情**: 两个模板独立: PL fonts=2, PM fonts=2
|
||||
|
||||
### TC-v1.5-006: 模板损坏优雅降级
|
||||
- **结果**: ✅ 通过
|
||||
@@ -185,11 +153,11 @@ v1.5.0 引入三项核心变更:
|
||||
|
||||
### TC-v1.5-010: 两个方向不同模板各自的格式
|
||||
- **结果**: ✅ 通过
|
||||
- **详情**: 两个方向输出字体不同: BL->楷体, BM->宋体
|
||||
- **详情**: 两个方向输出字体不同: PinList->楷体, PinMAP->宋体
|
||||
|
||||
### TC-v1.5-011: 完整往返+模板隔离
|
||||
- **结果**: ✅ 通过
|
||||
- **详情**: 往返成功: 12 pins, 楷体->PinList, 宋体->PinMAP
|
||||
- **详情**: 往返成功: 16 pins, 楷体->PinList, 宋体->PinMAP
|
||||
|
||||
### TC-v1.5-012: 无模板完整流程
|
||||
- **结果**: ✅ 通过
|
||||
|
||||
@@ -6,3 +6,5 @@
|
||||
| BUG-002 | 高 | 周长计算公式错误 | 输入 15×15 网格 + 60 Pin | 验证通过 `(rows+cols)*2=60` | 提示不匹配 `2*rows+2*cols-4=56` | 已修复 | F006 |
|
||||
| BUG-003 | 中 | 双向转换未读取模板样式 | 使用模板文件进行 MAP↔List 转换 | 读取并应用模板样式 | 使用默认样式 | 已修复 | F007 |
|
||||
| BUG-004 | 中 | 不支持循环处理流程 | 转换完成后继续操作 | 循环等待下一个文件,输入 Q 返回主菜单 | 处理完直接退出 | 已修复 | F008 |
|
||||
| BUG-005 | 高 | 模板文件名错误 | PinList↔PinMAP 转换时读取模板 | PinMAP 模板为 PinMAP-Template.xlsx,PinList 模板为 PinList-Template.xlsx | 引用的模板文件名错误 | 已修复 | - |
|
||||
| BUG-006 | 高 | PinList→PinMAP→PinList 双向转换数据错位 | 15×15 PinMap:PinList→PinMAP→PinList 双向转换 | 最终 PinList 与原始 PinList 一致 | 序号1从A4错位到A2,序号16从C20错位到B16,双向转换结果不一致 | 已修复 | - |
|
||||
|
||||
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 个测试通过*
|
||||
@@ -17,3 +17,8 @@
|
||||
| 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 |
|
||||
| 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 | - |
|
||||
|
||||
Reference in New Issue
Block a user