fix: 修复 BUG-007 PinList→PinMAP 上方引脚并入标题行,恢复独立2行

This commit is contained in:
2026-06-12 21:32:35 +08:00
parent 3c5fcff1d5
commit 7a4a767697
7 changed files with 120 additions and 95 deletions

View File

@@ -103,27 +103,28 @@ def calculate_layout(
top_pins = entries[idx: idx + top_count]
# ── 计算单元格坐标(v1.5.5:上边 Name 在 row 0完全独立)──
# ── 计算单元格坐标(BUG-007 修复:上边 Name 在 row 2标题独占 row 0)──
#
# 网格坐标体系0-based
# 从网格边界往中心走,第一圈全是 Number第二圈全是 Name
# 上边 Name 在 Number 上方 row 0不与左/右边共享行
# 第 0 行完全由标题A1 合并单元格,不包含引脚数据)独占
# 第 1 行是上边引脚序号,第 2 行是上边引脚 PinName
# 从第 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 (0, c) 逆序
# 左边: Number (r, 0) r ∈ [3, rows+2] Name (r, 1)
# 下边: Number (rows+4, c) c ∈ [1, cols] Name (rows+3, c)
# 右边: Number (r, cols+1) r ∈ [rows+2, 3] Name (r, cols) 逆序
# 上边: Number (1, c) c ∈ [cols, 1] Name (2, c) 逆序
#
# Pin1: Number (2,0) = A3, Name (2,1) = B3 — 左上角
# Pin1: Number (3,0) = A4, Name (3,1) = B4 — 左上角
# 左边:从上到下 (rows 个)
left_cells = [(r, 0) for r in range(2, rows + 2)]
left_cells = [(r, 0) for r in range(3, rows + 3)]
# 下边:从左到右 (cols 个)Number 在最底行 rows+3
bottom_cells = [(rows + 3, c) for c in range(1, cols + 1)]
# 下边:从左到右 (cols 个)Number 在最底行 rows+4
bottom_cells = [(rows + 4, c) for c in range(1, cols + 1)]
# 右边:从下到上 (rows 个)Number 在 cols+1 列(右扩一列)
right_cells = [(r, cols + 1) for r in range(rows + 1, 1, -1)]
right_cells = [(r, cols + 1) for r in range(rows + 2, 2, -1)]
# 上边:从右到左 (cols 个)
top_cells = [(1, c) for c in range(cols, 0, -1)]
@@ -173,8 +174,8 @@ def get_name_cell(num_cell: tuple[int, int], edge_name: str,
return (r, c - 1) # Name 在 Number 左侧 (col cols)
elif edge_name == "top":
# Top Number 在 (1, c), c ∈ [cols..1]
# Name 在 Number 方 (0, c),即 Excel 第 1 行
# 不再需要角点例外——整个上边 Name 在独立一行
return (0, c) # Name 在 Number 上方
# Name 在 Number 方 (2, c)
# row 0 完全由标题行A1 合并单元格)独占
return (2, c) # Name 在 Number 下方row 2
else:
raise LayoutError(f"未知的边名称: {edge_name}")

View File

@@ -16,35 +16,36 @@ from pinlist_generator import generate_pinlist
from utils import rc_to_cell_ref
# ── 4x4 example (v1.5.5 layout) ───────────────────────────────
# ── 4x4 example (BUG-007 fixed layout) ─────────────────────────
# Layout: rows=4, cols=4, 16 pins
# Top: Name row 0 (B1..E1), Number row 1 (B2..E2)
# Left: Number A3..A6 (rows 2..5), Name B3..B6 (rows 2..5)
# Bottom: Name B7..E7 (row 6), Number B8..E8 (row 7)
# Right: Number F6..F3 (rows 5..2), Name E6..E3 (rows 5..2)
# A1: "QFP-44" = package info
# Title: A1 "QFP-44" (row 0 only)
# Top: Number row 1 (B2..E2), Name row 2 (B3..E3)
# Left: Number A4..A7 (rows 3..6), Name B4..B7 (rows 3..6)
# Bottom: Name B8..E8 (row 7), Number B9..E9 (row 8)
# Right: Number F7..F4 (rows 6..3), Name E7..E4 (rows 6..3)
# A1: "QFP-44" = package info (title, merged, row 0 only)
#
# Pin1: Number A3=(2,0), Name B3=(2,1)
# Pin1: Number A4=(3,0), Name B4=(3,1)
cells_4x4 = {
(0, 0): "QFP-44",
# top edge Names (row 0, cols 1..4)
(0, 1): "Pin16", (0, 2): "Pin15", (0, 3): "Pin14", (0, 4): "Pin13",
# top edge Numbers (row 1, cols 1..4)
(1, 1): "16", (1, 2): "15", (1, 3): "14", (1, 4): "13",
# 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 Names (row 2, cols 1..4)
(2, 1): "Pin16", (2, 2): "Pin15", (2, 3): "Pin14", (2, 4): "Pin13",
# left edge (rows 3..6, cols 0..1)
(3, 0): "1", (3, 1): "Pin1",
(4, 0): "2", (4, 1): "Pin2",
(5, 0): "3", (5, 1): "Pin3",
(6, 0): "4", (6, 1): "Pin4",
# bottom edge (rows 7..8, cols 1..4)
(7, 1): "Pin5", (7, 2): "Pin6", (7, 3): "Pin7", (7, 4): "Pin8",
(8, 1): "5", (8, 2): "6", (8, 3): "7", (8, 4): "8",
# right edge (rows 6..3, cols 4..5)
(6, 4): "Pin9", (6, 5): "9",
(5, 4): "Pin10", (5, 5): "10",
(4, 4): "Pin11", (4, 5): "11",
(3, 4): "Pin12", (3, 5): "12",
}
@@ -111,7 +112,7 @@ def test_missing_names_warning():
def test_duplicate_numbers():
cells = dict(cells_4x4)
cells[(3, 0)] = "1" # duplicate pin 1 (original at (2,0))
cells[(4, 0)] = "1" # duplicate pin 1 (original at (3,0))
pm = parse_pinmap(cells)
vr = validate_pinmap(pm)
assert not vr.is_valid
@@ -121,7 +122,7 @@ def test_duplicate_numbers():
def test_gap_in_numbers():
cells = dict(cells_4x4)
cells[(7, 2)] = "10" # skip pin 6 (was "6" at (7,2))
cells[(8, 2)] = "10" # skip pin 6 (was "6" at (8,2))
pm = parse_pinmap(cells)
vr = validate_pinmap(pm)
assert not vr.is_valid
@@ -176,26 +177,26 @@ def test_rectangular_parse():
def test_12pin_square():
"""A 3×3 square: 12 pins (3 pins per edge).
Using v1.5.5 layout: top names at row 0, top numbers at row 1.
Using BUG-007 fixed layout: top numbers at row 1, top names at row 2.
left: 1,2,3 bottom: 4,5,6 right: 7,8,9 top: 12,11,10
"""
cells = {
(0, 0): "QFP-12",
# top Names (row 0, cols 1..3)
(0, 1): "RST", (0, 2): "VSS", (0, 3): "VDD",
# top Numbers (row 1, cols 1..3)
(1, 1): "12", (1, 2): "11", (1, 3): "10",
# top Names (row 2, cols 1..3)
(2, 1): "RST", (2, 2): "VSS", (2, 3): "VDD",
# left (col 0) — names at col 1
(2, 0): "1", (2, 1): "VCC",
(3, 0): "2", (3, 1): "GND",
(4, 0): "3", (4, 1): "IN1",
# bottom Names (row 5), Numbers (row 6)
(5, 1): "IN2", (5, 2): "OUT1", (5, 3): "OUT2",
(6, 1): "4", (6, 2): "5", (6, 3): "6",
(3, 0): "1", (3, 1): "VCC",
(4, 0): "2", (4, 1): "GND",
(5, 0): "3", (5, 1): "IN1",
# bottom Names (row 6), Numbers (row 7)
(6, 1): "IN2", (6, 2): "OUT1", (6, 3): "OUT2",
(7, 1): "4", (7, 2): "5", (7, 3): "6",
# right (col 4 Number, col 3 Name) — bottom to top: 7, 8, 9
(4, 3): "CTL1", (4, 4): "7",
(3, 3): "CTL2", (3, 4): "8",
(2, 3): "NC1", (2, 4): "9",
(5, 3): "CTL1", (5, 4): "7",
(4, 3): "CTL2", (4, 4): "8",
(3, 3): "NC1", (3, 4): "9",
}
pm = parse_pinmap(cells)
assert len(pm.pins) == 12, f"expected 12, got {len(pm.pins)}"
@@ -260,40 +261,40 @@ def test_f012_pinname_position():
output_path=None,
)
# ── 3. 检查单元格位置 (v1.5.5) ─────────────────────────────
# ── 3. 检查单元格位置 (BUG-007 fixed) ─────────────────────
# 5×5: rows=5, cols=5, 20 pins
# 上边: Name (0, 1..5), Number (1, 5..1)
# 左边: Number (2..6, 0), Name (2..6, 1)
# 下边: Name (7, 1..5), Number (8, 1..5)
# 右边: Number (6..2, 6), Name (6..2, 5)
# 上边: Number (1, 5..1), Name (2, 1..5)
# 左边: Number (3..7, 0), Name (3..7, 1)
# 下边: Name (8, 1..5), Number (9, 1..5)
# 右边: Number (7..3, 6), Name (7..3, 5)
# ── 3a. 验证上边 Name 位置 (0, 1..cols) ─────────────────
# ── 3a. 验证上边 Name 位置 (2, 1..cols) ─────────────────
for c in range(1, cols + 1):
num_ref = rc_to_cell_ref(1, c) # Number at row 1
name_ref = rc_to_cell_ref(0, c) # Name at row 0
name_ref = rc_to_cell_ref(2, c) # Name at row 2
assert num_ref in data, f"上边 Number {num_ref} 缺失"
assert name_ref in data, (
f"上边 Name 应在 {name_ref} (row 0), 但未找到。Number 在 {num_ref}"
f"上边 Name 应在 {name_ref} (row 2), 但未找到。Number 在 {num_ref}"
)
# ── 3b. 验证下边 Name 位置 (rows+2=7, 1..cols) ──────────
# ── 3b. 验证下边 Name 位置 (rows+3=8, 1..cols) ──────────
for c in range(1, cols + 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
num_ref = rc_to_cell_ref(rows + 4, c) # Number at row 9
name_ref = rc_to_cell_ref(rows + 3, c) # Name at row 8
assert num_ref in data, f"下边 Number {num_ref} 缺失"
assert name_ref in data, (
f"下边 Name 应在 {name_ref} (rows+2), 但未找到。Number 在 {num_ref}"
f"下边 Name 应在 {name_ref} (rows+3), 但未找到。Number 在 {num_ref}"
)
# ── 3c. 验证左边 Name 位置 (2..6, 1) ───────────────────
for r in range(2, rows + 2):
# ── 3c. 验证左边 Name 位置 (3..7, 1) ───────────────────
for r in range(3, rows + 3):
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} 缺失"
# ── 3d. 验证右边 Name 位置 (6..2, 5) ────────────────────
for r in range(rows + 1, 1, -1):
# ── 3d. 验证右边 Name 位置 (7..3, 5) ────────────────────
for r in range(rows + 2, 2, -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} 缺失"
@@ -709,6 +710,7 @@ def test_f016_qfn60_list_to_map():
- A1 = "QFN60"
- 所有 60 个引脚都有 Name 和 Number 单元格
- 四边布局正确left/bottom/right/top 各 15 个)
- 标题行独立A1 独占 row 0不混入引脚数据
"""
data = generate_pinmap(
entries=QFN60_PINLIST_ENTRIES,
@@ -730,9 +732,17 @@ def test_f016_qfn60_list_to_map():
f"A1 应为 {QFN60_PACKAGE_INFO},实际: {data.get('A1')}"
)
# ── 验证所有 60 个引脚都有 Name 和 Number ───────────
# ── 验证标题行独立BUG-007─────────────────────────
# A1 是唯一在 row 0 的单元格,无引脚数据混入
from utils import cell_ref_to_rc
for ref, val in data.items():
r, c = cell_ref_to_rc(ref)
if r == 0:
assert ref == "A1", (
f"row 0 应只有 A1 标题,但发现 {ref}={val}"
)
# ── 验证所有 60 个引脚都有 Name 和 Number ───────────
name_cells = {}
num_cells = {}
for ref, val in data.items():
@@ -767,58 +777,60 @@ def test_f016_qfn60_list_to_map():
f"Number 单元格应覆盖 1..60\n缺失: {sorted(set(range(1,61)) - all_numbers)}"
)
# ── 验证四边布局 ───────────────────────────────────
# v1.5.5 Layout A:
# Top: Name (0, 1..15), Number (1, 15..1)
# Left: Number (2..16, 0), Name (2..16, 1)
# Bottom: Name (17, 1..15), Number (18, 1..15)
# Right: Number (16..2, 16), Name (16..2, 15)
# Top Names 在 row 0
for c in range(1, QFN60_COLS + 1):
ref = rc_to_cell_ref(0, c)
assert ref in data, f"Top Name {ref} 缺失"
assert data[ref].startswith("Pin"), f"Top Name {ref} = {data[ref]}"
# ── 验证四边布局BUG-007 fixed layout──────────────
# BUG-007 修复布局:
# Title: A1 (row 0 only)
# Top Numbers: (1, 1..15)
# Top Names: (2, 1..15)
# Left: Number (3..17, 0), Name (3..17, 1)
# Bottom: Name (18, 1..15), Number (19, 1..15)
# Right: Number (17..3, 16), Name (17..3, 15)
# Top Numbers 在 row 1
for c in range(1, QFN60_COLS + 1):
ref = rc_to_cell_ref(1, c)
assert ref in data, f"Top Number {ref} 缺失"
# Left Numbers 在 col 0, rows 2..16
for r in range(2, QFN60_ROWS + 2):
# Top Names 在 row 2
for c in range(1, QFN60_COLS + 1):
ref = rc_to_cell_ref(2, c)
assert ref in data, f"Top Name {ref} 缺失"
assert data[ref].startswith("Pin"), f"Top Name {ref} = {data[ref]}"
# Left Numbers 在 col 0, rows 3..17
for r in range(3, QFN60_ROWS + 3):
ref = rc_to_cell_ref(r, 0)
assert ref in data, f"Left Number {ref} 缺失"
# Left Names 在 col 1, rows 2..16
for r in range(2, QFN60_ROWS + 2):
# Left Names 在 col 1, rows 3..17
for r in range(3, QFN60_ROWS + 3):
ref = rc_to_cell_ref(r, 1)
assert ref in data, f"Left Name {ref} 缺失"
assert data[ref].startswith("Pin"), f"Left Name {ref} = {data[ref]}"
# Bottom Names 在 row 17
# Bottom Names 在 row 18
for c in range(1, QFN60_COLS + 1):
ref = rc_to_cell_ref(QFN60_ROWS + 2, c)
ref = rc_to_cell_ref(QFN60_ROWS + 3, c)
assert ref in data, f"Bottom Name {ref} 缺失"
assert data[ref].startswith("Pin"), f"Bottom Name {ref} = {data[ref]}"
# Bottom Numbers 在 row 18
# Bottom Numbers 在 row 19
for c in range(1, QFN60_COLS + 1):
ref = rc_to_cell_ref(QFN60_ROWS + 3, c)
ref = rc_to_cell_ref(QFN60_ROWS + 4, c)
assert ref in data, f"Bottom Number {ref} 缺失"
# Right Numbers 在 col 16, rows 16..2
for r in range(QFN60_ROWS + 1, 1, -1):
# Right Numbers 在 col 16, rows 17..3
for r in range(QFN60_ROWS + 2, 2, -1):
ref = rc_to_cell_ref(r, QFN60_COLS + 1)
assert ref in data, f"Right Number {ref} 缺失"
# Right Names 在 col 15, rows 16..2
for r in range(QFN60_ROWS + 1, 1, -1):
# Right Names 在 col 15, rows 17..3
for r in range(QFN60_ROWS + 2, 2, -1):
ref = rc_to_cell_ref(r, QFN60_COLS)
assert ref in data, f"Right Name {ref} 缺失"
assert data[ref].startswith("Pin"), f"Right Name {ref} = {data[ref]}"
print(f"✓ test_f016_qfn60_list_to_map passed (60 pins, Layout A)")
print(f"✓ test_f016_qfn60_list_to_map passed (60 pins, BUG-007 layout)")
def test_f017_roundtrip():