v1.1.0: 增加交互提示、路径输入、窗口属性配置

- main.py: 增加show_banner()启动说明、各阶段[INFO]日志、结果摘要、任意键退出
- file_selector.py: 重写为路径输入→验证→空输入弹窗回退→不存在循环重试
- run.bat: 新建启动脚本(chcp 65001, mode con cols=80 lines=20, color 0B, title固定署名, pause)
- Code/docs/modification-assessment.md: 修改需求评估文档
This commit is contained in:
2026-05-25 17:29:19 +08:00
parent 5fbc215e59
commit 836ad20515
35 changed files with 4105 additions and 25 deletions

View File

@@ -0,0 +1,167 @@
"""PinMAP structure parser.
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.
Usage
-----
>>> from pinmap_parser import parse_pinmap
>>> pinmap = parse_pinmap(cells)
"""
from models import Pin, PinMAP, StructureError
def _try_int(value: str) -> int | None:
"""Try to parse a cell value as an integer pin number.
Returns the int or None if the value is not a valid pin number.
"""
if not value or not str(value).strip():
return None
try:
return int(float(str(value).strip()))
except (ValueError, TypeError):
return None
def parse_pinmap(cells: dict[tuple[int, int], str]) -> PinMAP:
"""Parse a PinMAP from a cell dictionary and return a PinMAP object.
Algorithm
---------
1. Scan all non-empty cells to determine the rectangular boundary
[min_row..max_row] × [min_col..max_col].
2. Read A1 (0,0) as the package-info string.
3. For each of the four edges, collect pin numbers from the boundary
cell and pin names from the adjacent inner cell.
4. Walk the edges counter-clockwise (left → bottom → right → top),
deduplicating corner pins by number.
Parameters
----------
cells : dict mapping (row, col) → cell text (0-based).
Returns
-------
PinMAP
Raises
------
StructureError
If the cell map is empty, the boundary is too small, A1 is
missing, or no pins are detected.
"""
if not cells:
raise StructureError("文件为空,无单元格数据")
# ── Step 1: determine rectangular boundary ───────────────────
# Exclude (0,0) — it holds the package-info label, not PinMAP data.
pin_cells = {
rc: v for rc, v in cells.items()
if rc != (0, 0) and v and str(v).strip()
}
if not pin_cells:
raise StructureError("未检测到任何 Pin 数据")
rows = {r for r, _ in pin_cells}
cols = {c for _, c in pin_cells}
min_row, max_row = min(rows), max(rows)
min_col, max_col = min(cols), max(cols)
width = max_col - min_col + 1
height = max_row - min_row + 1
if width < 2 or height < 2:
raise StructureError(
f"方形区域太小: {width}x{height},至少需要 2x2"
)
# ── Step 2: package info from A1 ─────────────────────────────
package_info = cells.get((0, 0), "")
if not package_info or not str(package_info).strip():
raise StructureError("A1 单元格为空,缺少封装信息")
# ── Step 3: build name lookup ────────────────────────────────
# For each edge, pin names live in the cell *adjacent inward*
# from the boundary cell that holds the pin number.
#
# 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)
name_map: dict[tuple[int, int], str] = {}
# left edge names
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
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
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
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()
# ── Step 4: walk edges counter-clockwise ─────────────────────
# Deduplicate by *cell position* (corners are shared cells),
# NOT by pin number — duplicate numbers are a data error for
# the validator to catch.
pins: list[Pin] = []
seen_cells: set[tuple[int, int]] = set()
def _add_pin(r: int, c: int, edge: str, pos: int) -> None:
if (r, c) in seen_cells:
return # corner cell already processed
seen_cells.add((r, c))
num = _try_int(cells.get((r, c), ""))
if num is None:
return
pins.append(Pin(
number=num,
name=name_map.get((r, c), ""),
edge=edge,
position_on_edge=pos,
))
# 4a. Left edge: top → bottom
for r in range(min_row, max_row + 1):
_add_pin(r, min_col, "left", r - min_row)
# 4b. Bottom edge: left → right (skip min_col corner already done)
for c in range(min_col + 1, max_col + 1):
_add_pin(max_row, c, "bottom", c - min_col)
# 4c. Right edge: bottom → top (skip max_row corner already done)
for r in range(max_row - 1, min_row - 1, -1):
_add_pin(r, max_col, "right", max_row - r)
# 4d. Top edge: right → left (skip max_col corner already done)
for c in range(max_col - 1, min_col - 1, -1):
_add_pin(min_row, c, "top", max_col - c)
if not pins:
raise StructureError("未检测到任何 Pin 数据")
return PinMAP(
package_info=str(package_info).strip(),
pins=pins,
width=width,
height=height,
grid_origin=(min_row, min_col),
raw_cells=cells,
)