From d635ddbebeb7def60f398e78cfda00f42e4fd355 Mon Sep 17 00:00:00 2001 From: Agent Date: Tue, 9 Jun 2026 08:27:11 +0800 Subject: [PATCH] =?UTF-8?q?v1.5.4=20Bug=20=E4=BF=AE=E5=A4=8D=EF=BC=9A?= =?UTF-8?q?=E6=A8=A1=E6=9D=BF=E6=96=87=E4=BB=B6=E5=90=8D=E4=BF=AE=E6=AD=A3?= =?UTF-8?q?=20+=20=E5=B8=83=E5=B1=80=E9=87=8D=E8=AE=BE=E8=AE=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BUG-005: 模板文件名改为 PinMAP-Template.xlsx / PinList-Template.xlsx BUG-006: 布局改为 Number 外侧 + Name 里侧(v1.5.4 最终版) - 从边界往中心:第1圈=Number,第2圈=Name - 上边角点例外处理,15种网格无冲突 - 18/18 单元测试 + 37/37 集成测试全部通过 --- .gitignore | 12 + CHANGELOG.md | 36 +++ .../src/Template/PinList-Template.xlsx | Bin .../src/Template/PinMAP-Template.xlsx | Bin Code/src/main.py | 40 +-- Code/src/pinmap_generator.py | 5 +- Code/src/pinmap_layout.py | 73 +++-- Code/src/pinmap_parser.py | 27 +- Code/src/test_pinmap.py | 209 +++++++------- README.md | 19 ++ Releases/v1.5.4/CHANGELOG.md | 60 ++++ Test/fixtures/PinList-Template.xlsx | Bin 0 -> 2542 bytes Test/fixtures/PinMAP-Template.xlsx | Bin 0 -> 2538 bytes Test/fixtures/sample_4x4.xlsx | Bin 2076 -> 2180 bytes Test/run_tests.py | 88 +++--- Test/test_report.md | 52 +--- docs/bugs.md | 2 + docs/modification-assessment-v1.5.1.md | 256 ++++++++++++++++++ docs/tasks.md | 5 + 19 files changed, 647 insertions(+), 237 deletions(-) rename Test/fixtures/BallList-Template.xlsx => Code/src/Template/PinList-Template.xlsx (100%) rename Test/fixtures/BallMAP-Template.xlsx => Code/src/Template/PinMAP-Template.xlsx (100%) create mode 100644 Releases/v1.5.4/CHANGELOG.md create mode 100644 Test/fixtures/PinList-Template.xlsx create mode 100644 Test/fixtures/PinMAP-Template.xlsx create mode 100644 docs/modification-assessment-v1.5.1.md diff --git a/.gitignore b/.gitignore index c35f0db..c9e13a7 100644 --- a/.gitignore +++ b/.gitignore @@ -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/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 57e1b14..cb7afa3 100644 --- a/CHANGELOG.md +++ b/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 ### ✨ 功能新增 diff --git a/Test/fixtures/BallList-Template.xlsx b/Code/src/Template/PinList-Template.xlsx similarity index 100% rename from Test/fixtures/BallList-Template.xlsx rename to Code/src/Template/PinList-Template.xlsx diff --git a/Test/fixtures/BallMAP-Template.xlsx b/Code/src/Template/PinMAP-Template.xlsx similarity index 100% rename from Test/fixtures/BallMAP-Template.xlsx rename to Code/src/Template/PinMAP-Template.xlsx diff --git a/Code/src/main.py b/Code/src/main.py index d83c97a..4e7296f 100644 --- a/Code/src/main.py +++ b/Code/src/main.py @@ -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, diff --git a/Code/src/pinmap_generator.py b/Code/src/pinmap_generator.py index 5de0b76..49959b6 100644 --- a/Code/src/pinmap_generator.py +++ b/Code/src/pinmap_generator.py @@ -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): diff --git a/Code/src/pinmap_layout.py b/Code/src/pinmap_layout.py index c71cc74..bd5b87f 100644 --- a/Code/src/pinmap_layout.py +++ b/Code/src/pinmap_layout.py @@ -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}") diff --git a/Code/src/pinmap_parser.py b/Code/src/pinmap_parser.py index d61ef62..af8d726 100644 --- a/Code/src/pinmap_parser.py +++ b/Code/src/pinmap_parser.py @@ -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). diff --git a/Code/src/test_pinmap.py b/Code/src/test_pinmap.py index dc7f480..13f7940 100644 --- a/Code/src/test_pinmap.py +++ b/Code/src/test_pinmap.py @@ -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") diff --git a/README.md b/README.md index f3fd988..a993dec 100644 --- a/README.md +++ b/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`) +- 模板格式提取:字体、边框、填充、对齐、列宽、行高 + ## 许可证 内部项目 diff --git a/Releases/v1.5.4/CHANGELOG.md b/Releases/v1.5.4/CHANGELOG.md new file mode 100644 index 0000000..a8dd8ca --- /dev/null +++ b/Releases/v1.5.4/CHANGELOG.md @@ -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 状态为已修复 diff --git a/Test/fixtures/PinList-Template.xlsx b/Test/fixtures/PinList-Template.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..e4d0fa47a669a50385cbfbd97e73ca6bb84f461e GIT binary patch literal 2542 zcmZ`*2Q=Gz8&1R)qX=4R*4~7m1XXD@)E=qYjTkXXs$x~OxIUw?I^5c`MQgjUs)+fK zXzQY?Evf^p*650I|LUCUYrcDvbMia+|IhQD^E}V{yzkEj4Ph1pfk5n_Mr{9CR7XgMdH+z*ASVkYEBfnBd_+48?}KsYM3iYwgk2OqzUM>&kiV9IBBT{VP2W2W+kS zBs5wY@n#9*uoNx$@6Ji zk=(kGHN@mT;dR^Q6={BDZ)=^9@Qg{jtblQF3g5VJ8OM;R8nr3wz>`Bx3hEV)(RUK4 zZ`Y4}5syK7*R)Ysb#)ut-Aq5b@t>Jr@uj|B2;k8?Qz|KZHn?*e2LHx>z+6AHN++{F zv3s$YByM>7lWMGr@+p)|bV80qH>~6t=9|!??>bkT^**ehn54l`#XJEE|2b}B$)0zn zZ>2`0BGhy1YZ)h`@Rf!y?!(&R&LvoYRB_zc9WmbgSXtu^>#D4yskl{*n{i3bVP7HZ z7j1sg(k*#ItqIUj1<;cVI6cC!_;7W#-QRTT1D*s8KGu)|yla3f7r z2MTSXtXyHwpJ#ZBiK$or+V9ij?h2-%lp#uvnP^l?_H}2ee=)x>>0mNZ4CP}ZY!1a#Yw9E_K^yPMI$ne#A z?vW*@`05!km&P+&$(8p+VFeaOzVNw$8~0Nc(33uVb$;8@h}o11cuBX|(7^2CS;O)tn! zRVW^@sXh{k6}S4Ft*cQ6cm6W~;Yny)IH;&>geEB3lQH=$`IBHx%Fu_@`CgPTyE{j2 zNA|;52?N9P7q6Xs2b0z#=gchg`6fpASpJA9;X0clB^*wtvfLn!hp8iZS!EmtP1L%W z>4cdH_n8Y=I_Oun(t_2R6n_@2lK0MRCp9~&{J%a>*1Wr{<_}Sbq=((> zb8|~Vf?TZ0_Rw>lQb}&D{u2Jj6t!0d`tF)AiGGxRcRH}8!1tAs9B29C0mIT9IIlsm z(Y#5N>mk9z$ zZJ9wJ?mbDu2}C?L{O62tKm#i9&3+L)Mn_pFlnc&WR)e}B5`$r`Ei8`)PiB)|9WpYb z(i}tVqcSq^l^jy#67%UO>%2-t$PxL*W*vNa42{PQc`#ky-sTTGv;4IIVjQs-W5Q7m z=W!TDD1F34WHLV!*`z5XrinBFftVj zpPK9JAo6X)U{>-S>JzQvomRk3fnjo*4nO!bNa}|%f`}FEfAw- z-AD6ZB2Vo=8?1lr?KKtu>s5e(oPe3TivUP+xSv-T_MAN-%s&|S)6bw0O&%c5EVRzb z-#Q)(I`AE{n7V1206AXkFQaq%jvI5Uyrl-AzUxKn=qDK4W?zb#G}m$iQ{s~{T^rn7 zgi-D1A{WEvxRY0fo)3l+61{|SJyKLRq!&yBgQL<4M&QzmuL~;P%HFrHB;KaAK9e~m zwo!36!r;3}Q64y40f3bUkXn&FX1jR}_rqcd;Q&eC(A@3TM7!C5UP-~ORr^7*N4l?Qc~v=f zg+n(&H6`er?UlB&XuAK?3qb-?>1)I;W1fc5@un(gq#sV7gax0R9-eqAuGjIP6IlaF zR0vXa#lez8V4gBU_|R=__$l)oDE=j_Z>m<4$FVdGik?*bT`u}fdK$-f2v5FqgSdGG zrAeWnKJ#gUi)3FwP|m0<$vK+Gwc&Yg{DAG^Q^~cIw6VkBgG8}a)5-cdlftm~OUlP_ z$kWzG{CAFQs1yrxU`Ej&MOp^yea3|kV%0M1E<)CBMf8we`e&ZVgt7Zu>6_HK3qrq4 zFRAod5SL9?jo#+69W)wDA5cG!kbSh%Gfb}wo3|ZHwo19S?gjNwO`RKg!DnoP1~Uml z{{3D6sQL2~56JtEw*Ki_0p5Z@ OY{0h%kXVR)_upT~_4m2} literal 0 HcmV?d00001 diff --git a/Test/fixtures/PinMAP-Template.xlsx b/Test/fixtures/PinMAP-Template.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..0b87a2c34c3743afda0f927f89486a0567f73d29 GIT binary patch literal 2538 zcmZ`*2{csw8y<$i*q1@ps7OO*>`8-ceGM6seP_nlWyux^k^0CIqGStMzls#18Ecws z5o+ur3R5P+DE}+x{QH{Ickem(ckaFCdCz&?=Y8Jy#h}4V0w55G6;zAuv`3A|8i+E0 zKzd*hh#&ZBr|0h%jP(n4vIq~r200uJ^Yy7di#BCM^0ZLlxsGfKVTjJ9R$`Z#NxLMX zzSc+2X;Hv^r+K`?>hJw5XENp^aZ1&K$35N(^5;x>qxjGE_E#1pJH6S)hHE#>{H+wm zsTNihp4cu#HG8}y%?EciRrL=_88J`y8emA`84$Y1)~$QAuP&W;NW}86V(B2dJhpFv zdhnaXMU-n*V+TY{t+vTQcin-{c4i6Jw>;~`g|sc+FJ$l6`~#-4$;xYREa1LsTIUtQ zTw$t&)}1d3QS$K9h6f^JZ%PtiMPoLbg0Ht!;~#6RP>n|VRSXNcyk@WIV~kmIZQGZs zgi8aQw>R!V!TIkIIFFUpxt&WeFR8-l-f}VSyr@Ik&rI*9AHVKFLEMT?unOD&Q$sPo z<(M$Ohw?x^*@4q35bG19cy#xf-1nR-7QqAgoL7eT%Z`mhCbYU5TXUjx6uR;; z5hix9iHQ_fF)_u^Ny;khjgt4e{s$u^##I$?f}z>*Tt`B6!%_<2Vrpq-E(g?lK<}JP z2kbEiayD@wLXdZsnL}!n|AQl^20B08fL~p_!6reY@vdFD%-aK#9&axM*Ly0;7P&Rzv$Fy~aw^~-*o7cb>KVL{}Ig{vc7z&rN? zeAL$zNnuzCll2TW#61BMiun?v?8^ZH7Xuj%=CF zGKf)WZ=70$NozFRB!A+;UAf?9{QY7PyM2dsH|L+qR;TrNwIMiSC{fU2+RVK;q~RUM z&VrHZNRSKsJl<6lP3tE23Dm1Tcqkshm)>8-T|VC2ml09&%8dh(TLCd38mcD{3DoK7 zkAed1ki@eb?#@yP4o^HKJx|IhFLkw7=rD?YmR>yLU4Iw%{)h~;WUx!C_@)ZCX5on$ zod~-Tbw+bhNyfU-qeYXhs7JEtj2&93!W%~( zqOO!4vFLxk<)gURCZXIQ+dVh6yXF4d#OwvY1Q|Q=SKBU_5doz7X;OK3`Izapy zCJ=~oFC{_2;Xc@)UHKnd5L1Q_9L-di*@_R&5+08sXX;e6=NQc7dJd|YyILiAsMbCw zr0q;`Z-{PiFV(jR_7I><4Wr>n`T5Ir9m`UX5Q_OaYsrM8Z(U%&^QS_^HCofwCc0Y5 z2JfpAP=ac#@~`wTD0K^(9d70+pZQsjw8TjeBhZ3SZRLWNvi7RYPMq z<|AsuU5eHUa%%Jnszk=XnOnA2{217#?l_sy=x2Sa9Qt6O=f#sg$>-6V2d(26GERT5 z84796$s*uzPhZv2;5FUuW}oynY!txfkhap<1@p4CzTu%_NQUV9_4{EOOG4~dl^PsOL@4U;8^knVtM)gYZ~va1g)|IFP#~ zoS8;G0~k4h@=YL(k`oDvFFiOY6CVZ&i@Cq{?wU+_dOwjon-!-P!uPIZc`_d*8%T4B zM4DgC3x%$RKw|CZKJ}_X9i8s_N$9k@+xtdPP7!4dMn7h1ZG9HBD>^7VRTQgM;u0rZ znN^kKt(`jCO=R)r%Pu5pFe$A)bB~mNv?NizD7KKv^JqoB;jfcSnukZ{Zbz|c+?Y=` z%6MgQUVPh(=>qeB40Ff$d;8|D_shy5Jwv4F<1eSZuB~X0mS2>8X2c)+XgqWOkfpY< z`@*r7r;JW_rl|Y!^QSMx(XbCT`ih^m7TZseND`!}Wh{4+>_3WMC?jh^7&HT;0QkRm z0w8HWuNa^-{dvFjahQSyJ@vpH+52PP5zkqUpdUPX)9!%e}e}RR7G#Vgc?} LAjN#FyKnyk`7|2r literal 0 HcmV?d00001 diff --git a/Test/fixtures/sample_4x4.xlsx b/Test/fixtures/sample_4x4.xlsx index afd2793956004bec037caedd0aae9326d22af5cf..2c460ac45590791125239f7df86cee6e168afd47 100644 GIT binary patch delta 822 zcmbOu&?3ki;LXe;!oa}5!NB5tawD$;BQubm9Kd)BOzSee2GN_PnOPY5fQ%TM<%#+4 zfXYjlC+o6E*H3cHJFFn!`rVQ>X@0^ZGhZ!*-D`qZ+A?g)>`7s}-n{zTH`Tjay5I9Q znfE(>Fnqq=r0cvv@tKsR_e_IWUKPo|OnZ9!b!zpLrtanU`e!yxz2ErsW-d>$V5#=T zsm_|0HwWF}Rjj=CR5M`hp68Z7gQkD>{b<5||FeLT=i*!DXB}1RXm@zFt}UJ2OgH z#k`lC!M>}8k%7U6ZL&VAeEpQWeN6@et?$2U&bqgLse=8iCc#Ga8?_8dp(nizbuZrC z9$!*#S_F~kG|g)gy^NbZN<)bC!+yBNPkGMsPk+gFJ?Zv%I9*IzCD zEKr*MusTj<*Dh0=>O>#Y+~sHQvTXh};nUifa8B|5rR|GWwEv3h-TF5FOkdnjf7`m7 z_g}AllH;-S@yuOm^MtPZbpBoM(j&Fz+wv^=70mJ{|Id&ys&g{3K5|4QSV2u`rjViH z$&il7v#xy0CNe8bFG=dS;Cs_iWx;k$fyWo?8!xV(R+4aprMPkW{ z=jVB~N?4|w%#q@4oMZP}M#HbV1rAcX8M~)P` zJ?^|{(^j-g-1^7`_GpD?CmR&hW^JD3vsTSv-=eurE_OSMI~FeA>*C{K9m-^L!0V6w zpPS|Z-n@)VBFylVJh_j}4xEC|vzdWu4t6Ur?aJoFl*+;LXe;!oa}5!4SZ6a3ik+BQubm9Kd)BOzSee2GN_PnOPY5fQ*>OpF-p= z0F@^(P1a?RuJ1qSci4cZ?R|CEiSlXNnhgUc-7S+7vT?XF%QWcmlv{Hjyyc8}@E}&{ z`RDwXzwdrD_J7vJz1&IeRxUH!N`2==-R;}2U%t!8r4TQ&RprY9O-cgN_Q=RVfx?s9VS+gWM(#Xc;{GW4}O+t;yf4*C`G zv7ApjS!VeIl|6IL}8i(*UR=_^4fIO8fKKxh~e#iecXtVfx(((vM;NA{bcXFLk0p4-{%~f zRa+>;VQ=YZazS_t`vxIq!6RZ6rWv1u%s=I9(R$!*88KbC(D>VvKCenKv5fEo7k{7bDh`(qRlj>9 zW(uErb+Yx#!x{W{V%JPM>-?(zoQ?M+$@eL%7s?o_&Ds|HtBy16>b?fwUALYdkay=# z*513vKXKc$V>vst|D1W~T|3`g(;&W~IVk3|%ZF^uiodHrJUCKzZM$*G^2{aw4c7}g zD~Y;oYDw8PBXZISy1Ep!4& zh`JT+VpTTYrMl}(%?j1`+n+?tzoa{3;Y6*d&nj%j#a3!j#*33#+~50u_+K7h2MkX} zCJ| 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("无模板完整流程正常") diff --git a/Test/test_report.md b/Test/test_report.md index 08f3f4e..f10a413 100644 --- a/Test/test_report.md +++ b/Test/test_report.md @@ -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: 无模板完整流程 - **结果**: ✅ 通过 diff --git a/docs/bugs.md b/docs/bugs.md index 94a5ea5..3027518 100644 --- a/docs/bugs.md +++ b/docs/bugs.md @@ -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,双向转换结果不一致 | 已修复 | - | diff --git a/docs/modification-assessment-v1.5.1.md b/docs/modification-assessment-v1.5.1.md new file mode 100644 index 0000000..bfb4590 --- /dev/null +++ b/docs/modification-assessment-v1.5.1.md @@ -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 个测试通过* diff --git a/docs/tasks.md b/docs/tasks.md index af3b464..20b0f20 100644 --- a/docs/tasks.md +++ b/docs/tasks.md @@ -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 | - |