v1.5.5 Bug 修复:模板路径修正 + 上边 Name 独立行
BUG-005: 模板搜索路径优先查找 Code/src/Template/ 目录 BUG-006: 上边 Name 移至 row 0,完全独立于其他边 - 37/37 测试全部通过 - docs: 更新 bugs.md(BUG-005/BUG-006 状态) - docs: 更新 tasks.md(T028 打包进行中→已完成) - docs: 添加 modification-assessment-v1.5.5.md - CHANGELOG.md: 追加 v1.5.5 版本日志
This commit is contained in:
@@ -34,46 +34,54 @@ def wait_for_exit():
|
||||
# ── Path helpers ────────────────────────────────────────────────────
|
||||
|
||||
def _find_pinlist_template_path() -> str | None:
|
||||
"""查找根目录下的 PinList-Template.xlsx。
|
||||
"""查找 PinList-Template.xlsx。
|
||||
|
||||
MAP→List 输出使用 PinList 模板。
|
||||
搜索顺序:
|
||||
1. 与 run.bat 同级的根目录
|
||||
2. 当前工作目录
|
||||
1. Code/src/Template/ 目录(首要位置)
|
||||
2. 项目根目录(向后兼容)
|
||||
3. 当前工作目录
|
||||
"""
|
||||
src_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
root_dir = os.path.dirname(os.path.dirname(src_dir)) # pinmap-to-pinlist/
|
||||
template_path = os.path.join(root_dir, "PinList-Template.xlsx")
|
||||
|
||||
# 1. Code/src/Template/ 目录
|
||||
template_path = os.path.join(src_dir, "Template", "PinList-Template.xlsx")
|
||||
if os.path.exists(template_path):
|
||||
return template_path
|
||||
|
||||
# 2. 项目根目录(向后兼容)
|
||||
root_dir = os.path.dirname(os.path.dirname(src_dir))
|
||||
template_path = os.path.join(root_dir, "PinList-Template.xlsx")
|
||||
if os.path.exists(template_path):
|
||||
return template_path
|
||||
# 3. 当前工作目录
|
||||
cwd_template = os.path.join(os.getcwd(), "PinList-Template.xlsx")
|
||||
if os.path.exists(cwd_template):
|
||||
return cwd_template
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _find_pinmap_template_path() -> str | None:
|
||||
"""查找根目录下的 PinMAP-Template.xlsx。
|
||||
"""查找 PinMAP-Template.xlsx。
|
||||
|
||||
List→MAP 输出使用 PinMAP 模板。
|
||||
搜索顺序:
|
||||
1. 与 run.bat 同级的根目录
|
||||
2. 当前工作目录
|
||||
1. Code/src/Template/ 目录(首要位置)
|
||||
2. 项目根目录(向后兼容)
|
||||
3. 当前工作目录
|
||||
"""
|
||||
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, "PinMAP-Template.xlsx")
|
||||
|
||||
# 1. Code/src/Template/ 目录
|
||||
template_path = os.path.join(src_dir, "Template", "PinMAP-Template.xlsx")
|
||||
if os.path.exists(template_path):
|
||||
return template_path
|
||||
|
||||
# 2. 项目根目录(向后兼容)
|
||||
root_dir = os.path.dirname(os.path.dirname(src_dir))
|
||||
template_path = os.path.join(root_dir, "PinMAP-Template.xlsx")
|
||||
if os.path.exists(template_path):
|
||||
return template_path
|
||||
# 3. 当前工作目录
|
||||
cwd_template = os.path.join(os.getcwd(), "PinMAP-Template.xlsx")
|
||||
if os.path.exists(cwd_template):
|
||||
return cwd_template
|
||||
|
||||
return None
|
||||
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ Edge assignment (counter-clockwise, top-left = pin 1):
|
||||
|
||||
Total: rows + cols + rows + cols = 2×rows + 2×cols = (rows + cols) × 2
|
||||
|
||||
v1.5.4: 从网格边界往中心走,第一圈全是 Number,第二圈全是 Name。
|
||||
v1.5.5: 上边完全独立在 row 0-1,不与左/右边共享行。
|
||||
每条边独立包含其端点,所有单元格互不冲突。
|
||||
|
||||
Coordinate system (0-based):
|
||||
@@ -27,9 +27,7 @@ Coordinate system (0-based):
|
||||
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]
|
||||
top: (0, c) where c ∈ [1..cols] [reverse order, independent row]
|
||||
|
||||
Pin1: Number (2,0), Name (2,1) — top-left of left edge
|
||||
"""
|
||||
@@ -105,16 +103,16 @@ def calculate_layout(
|
||||
|
||||
top_pins = entries[idx: idx + top_count]
|
||||
|
||||
# ── 计算单元格坐标(v1.5.4:Number 外侧 + Name 里侧,无冲突)──
|
||||
# ── 计算单元格坐标(v1.5.5:上边 Name 在 row 0,完全独立)──
|
||||
#
|
||||
# 网格坐标体系(0-based):
|
||||
# 从网格边界往中心走,第一圈全是 Number,第二圈全是 Name
|
||||
# 上边 Name 在 Number 上方 row 0,不与左/右边共享行
|
||||
#
|
||||
# 左边: 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) 角点例外
|
||||
# 上边: Number (1, c) c ∈ [cols, 1] Name (0, c) 逆序
|
||||
#
|
||||
# Pin1: Number (2,0) = A3, Name (2,1) = B3 — 左上角
|
||||
|
||||
@@ -149,8 +147,8 @@ def get_name_cell(num_cell: tuple[int, int], edge_name: str,
|
||||
"""
|
||||
根据序号单元格坐标和边名称,计算对应的 PinName 单元格坐标。
|
||||
|
||||
v1.5.4: 第二圈 Name 紧挨第一圈 Number 内侧。
|
||||
上边角点会有例外(位于 row 1),以避免与左/右边 Name 冲突。
|
||||
v1.5.5: 上边 Name 在 Number 上方 (0, c),即独立一行。
|
||||
不再需要角点例外——整个上边 Name 在独立的 row 0。
|
||||
|
||||
Parameters
|
||||
----------
|
||||
@@ -159,7 +157,7 @@ def get_name_cell(num_cell: tuple[int, int], edge_name: str,
|
||||
edge_name : str
|
||||
"left" | "bottom" | "right" | "top"
|
||||
cols : int
|
||||
网格列数(上边检测角点例外时需要),默认 0
|
||||
网格列数(v1.5.5 上边不再需要角点例外,参数保留以兼容调用)
|
||||
|
||||
Returns
|
||||
-------
|
||||
@@ -175,12 +173,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 下方 (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)
|
||||
# Name 在 Number 上方 (0, c),即 Excel 第 1 行
|
||||
# 不再需要角点例外——整个上边 Name 在独立一行
|
||||
return (0, c) # Name 在 Number 上方
|
||||
else:
|
||||
raise LayoutError(f"未知的边名称: {edge_name}")
|
||||
|
||||
@@ -4,7 +4,7 @@ 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-里侧的双圈布局解析。
|
||||
v1.5.5: 上边 Name 在 Number 上方 (min_row-1),无需角点例外。
|
||||
|
||||
Usage
|
||||
-----
|
||||
@@ -84,17 +84,16 @@ 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 (v1.5.4 layout) ──────────────
|
||||
# ── Step 3: build name lookup (v1.5.5 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:
|
||||
# v1.5.5 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)
|
||||
# top : number at (min_row+1, c), name at (min_row, c)
|
||||
# No corner exceptions — top names are all one row above Numbers
|
||||
|
||||
name_map: dict[tuple[int, int], str] = {}
|
||||
|
||||
@@ -116,23 +115,14 @@ def parse_pinmap(cells: dict[tuple[int, int], str]) -> PinMAP:
|
||||
if name and str(name).strip():
|
||||
name_map[(r, max_col)] = str(name).strip()
|
||||
|
||||
# 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)
|
||||
# top edge names at (min_row, c) — one row ABOVE the Number row.
|
||||
# v1.5.5: 上边 Name 在 (min_row, c),Number 在 (min_row+1, c)。
|
||||
# name_map key 是 Number 单元格坐标,value 是 Name 字符串。
|
||||
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()
|
||||
name = cells.get((min_row, c), "")
|
||||
if name and str(name).strip() and _try_int(name) is None:
|
||||
name_map[(min_row + 1, c)] = str(name).strip()
|
||||
# No corner exceptions needed — top names are all on min_row
|
||||
|
||||
# ── Step 4: walk edges counter-clockwise (v1.3 formula) ──────
|
||||
# Each edge independently includes its endpoints (corners).
|
||||
@@ -175,9 +165,9 @@ def parse_pinmap(cells: dict[tuple[int, int], str]) -> PinMAP:
|
||||
for r in range(max_row, min_row - 1, -1):
|
||||
_add_pin(r, max_col, "right", max_row - r)
|
||||
|
||||
# 4d. Top edge: right → left (includes top-left corner)
|
||||
# 4d. Top edge: right → left (Numbers at min_row+1 row, Names at min_row)
|
||||
for c in range(max_col, min_col - 1, -1):
|
||||
_add_pin(min_row, c, "top", max_col - c)
|
||||
_add_pin(min_row + 1, c, "top", max_col - c)
|
||||
|
||||
if not pins:
|
||||
raise StructureError("未检测到任何 Pin 数据")
|
||||
|
||||
@@ -16,19 +16,22 @@ from pinlist_generator import generate_pinlist
|
||||
from utils import rc_to_cell_ref
|
||||
|
||||
|
||||
# ── 4x4 example (v1.5.4 layout) ───────────────────────────────
|
||||
# ── 4x4 example (v1.5.5 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: Number B8..E8 (row 7), Name B7..E7 (row 6)
|
||||
# Bottom: Name B7..E7 (row 6), Number B8..E8 (row 7)
|
||||
# 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",
|
||||
# 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",
|
||||
@@ -42,15 +45,6 @@ cells_4x4 = {
|
||||
(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",
|
||||
}
|
||||
|
||||
|
||||
@@ -181,31 +175,28 @@ def test_rectangular_parse():
|
||||
|
||||
|
||||
def test_12pin_square():
|
||||
"""A larger square: 12 pins on a 6×6 grid (rows 1-5, cols 0-5).
|
||||
"""A 3×3 square: 12 pins (3 pins per edge).
|
||||
Using v1.5.5 layout: top names at row 0, top numbers at row 1.
|
||||
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",
|
||||
# left (col 0) — names at col 1
|
||||
(1, 0): "1", (1, 1): "VCC",
|
||||
(2, 0): "2", (2, 1): "GND",
|
||||
(3, 0): "3", (3, 1): "IN1",
|
||||
# bottom (row 5) — names at row 4
|
||||
(5, 1): "4", (4, 1): "IN2",
|
||||
(5, 2): "5", (4, 2): "OUT1",
|
||||
(5, 3): "6", (4, 3): "OUT2",
|
||||
# right (col 5) — names at col 4
|
||||
(4, 5): "7", (4, 4): "CTL1",
|
||||
(3, 5): "8", (3, 4): "CTL2",
|
||||
(2, 5): "9", (2, 4): "NC1",
|
||||
# top (row 1) — names at row 2, cols 2-4 (avoid col 5 corner)
|
||||
(1, 4): "10", (2, 4): "VDD",
|
||||
(1, 3): "11", (2, 3): "VSS",
|
||||
(1, 2): "12", (2, 2): "RST",
|
||||
(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",
|
||||
# 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",
|
||||
}
|
||||
# Note: (2,4) is used as name for both pin 9 (right edge) and pin 10 (top edge).
|
||||
# The name_map will have the last writer win. This is fine for the test —
|
||||
# we just verify the correct number of pins and their order.
|
||||
pm = parse_pinmap(cells)
|
||||
assert len(pm.pins) == 12, f"expected 12, got {len(pm.pins)}"
|
||||
|
||||
@@ -237,13 +228,13 @@ def test_12pin_square():
|
||||
# ── F012: PinMAP 生成中上/下边 PinName 位置验证 ────────────
|
||||
|
||||
def test_f012_pinname_position():
|
||||
"""验证 PinList→PinMAP 时各边 PinName 位置正确(v1.5.4 布局)。
|
||||
"""验证 PinList→PinMAP 时各边 PinName 位置正确(v1.5.5 布局)。
|
||||
|
||||
v1.5.4 布局:
|
||||
v1.5.5 布局:
|
||||
- 左边 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)
|
||||
- 上边 Name 在 (0, c),即独立行,无需角点例外
|
||||
|
||||
测试策略:
|
||||
1. 构建 5×5(20 Pin)PinList 数据
|
||||
@@ -269,15 +260,23 @@ def test_f012_pinname_position():
|
||||
output_path=None,
|
||||
)
|
||||
|
||||
# ── 3. 检查单元格位置 (v1.5.4) ─────────────────────────────
|
||||
# ── 3. 检查单元格位置 (v1.5.5) ─────────────────────────────
|
||||
# 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)
|
||||
# 上边: 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)
|
||||
|
||||
# ── 3a. 验证下边 Name 位置 (rows+2=7, 1..cols) ──────────
|
||||
# ── 3a. 验证上边 Name 位置 (0, 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
|
||||
assert num_ref in data, f"上边 Number {num_ref} 缺失"
|
||||
assert name_ref in data, (
|
||||
f"上边 Name 应在 {name_ref} (row 0), 但未找到。Number 在 {num_ref}"
|
||||
)
|
||||
|
||||
# ── 3b. 验证下边 Name 位置 (rows+2=7, 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
|
||||
@@ -286,25 +285,6 @@ def test_f012_pinname_position():
|
||||
f"下边 Name 应在 {name_ref} (rows+2), 但未找到。Number 在 {num_ref}"
|
||||
)
|
||||
|
||||
# ── 3b. 验证上边 Name 位置 ─────────────────────────────────
|
||||
# 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"上边 Name 应在 {name_ref}, 但未找到。Number 在 {num_ref}"
|
||||
)
|
||||
|
||||
# ── 3c. 验证左边 Name 位置 (2..6, 1) ───────────────────
|
||||
for r in range(2, rows + 2):
|
||||
num_ref = rc_to_cell_ref(r, 0)
|
||||
|
||||
Reference in New Issue
Block a user