v1.3.0: 修复pinmap_layout周长公式,新增PinList→PinMAP反向转换完整支持
This commit is contained in:
140
Code/src/main.py
140
Code/src/main.py
@@ -33,6 +33,29 @@ def wait_for_exit():
|
||||
|
||||
# ── Path helpers ────────────────────────────────────────────────────
|
||||
|
||||
def _find_template_path() -> str | None:
|
||||
"""查找根目录下的 PinMAP-Template.xlsx。
|
||||
|
||||
搜索顺序:
|
||||
1. 与 run.bat 同级的根目录
|
||||
2. 当前工作目录
|
||||
"""
|
||||
# 从 Code/src 回退到根目录
|
||||
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")
|
||||
|
||||
if os.path.exists(template_path):
|
||||
return template_path
|
||||
|
||||
# 回退到当前工作目录
|
||||
cwd_template = os.path.join(os.getcwd(), "PinMAP-Template.xlsx")
|
||||
if os.path.exists(cwd_template):
|
||||
return cwd_template
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _build_output_path_map_to_list(input_path: str) -> str:
|
||||
"""Generate output path: {original_filename}_PinList.xlsx"""
|
||||
base, _ = os.path.splitext(input_path)
|
||||
@@ -55,7 +78,8 @@ def run_map_to_list(filepath: str):
|
||||
from pinmap_parser import parse_pinmap
|
||||
from validator import validate_pinmap
|
||||
from pinlist_generator import generate_pinlist
|
||||
from xlsx_writer import write_xlsx
|
||||
from xlsx_writer import write_xlsx, write_xlsx_with_style
|
||||
from template_reader import read_template_styles
|
||||
from models import FileFormatError, StructureError
|
||||
|
||||
# ── 1. File selection ───────────────────────────────────────────
|
||||
@@ -126,7 +150,22 @@ def run_map_to_list(filepath: str):
|
||||
data[f'A{row}'] = pin_name
|
||||
data[f'B{row}'] = str(pin_num)
|
||||
|
||||
write_xlsx(data, output_path)
|
||||
# 尝试读取模板样式(F007)
|
||||
template_path = _find_template_path()
|
||||
template_style = None
|
||||
if template_path:
|
||||
template_style = read_template_styles(template_path)
|
||||
if template_style:
|
||||
print(f"[INFO] 已加载模板样式: {template_path}")
|
||||
else:
|
||||
print("[WARN] 模板文件存在但解析失败,使用默认样式")
|
||||
else:
|
||||
print("[INFO] 未检测到模板文件,使用默认样式")
|
||||
|
||||
if template_style is not None:
|
||||
write_xlsx_with_style(data, output_path, template_style)
|
||||
else:
|
||||
write_xlsx(data, output_path)
|
||||
except Exception as e:
|
||||
print(f"[FATAL] 输出失败: {e}")
|
||||
wait_for_exit()
|
||||
@@ -139,7 +178,7 @@ def run_map_to_list(filepath: str):
|
||||
print(f" 封装信息: {pinlist.package_info}")
|
||||
print(f" Pin数量: {len(pinlist.rows)}")
|
||||
|
||||
wait_for_exit()
|
||||
# F008: 不再退出,返回主循环
|
||||
|
||||
|
||||
# ── Direction 2: List → MAP ────────────────────────────────────────
|
||||
@@ -221,8 +260,17 @@ def run_list_to_map(filepath: str):
|
||||
print(f"[INFO] 正在生成 PinMAP 并写入: {output_path}")
|
||||
|
||||
try:
|
||||
# 尝试读取模板样式(优雅降级)
|
||||
template_style = read_template_styles(filepath)
|
||||
# 尝试读取模板样式(F007 — 从根目录读取而非输入文件路径)
|
||||
template_path = _find_template_path()
|
||||
template_style = None
|
||||
if template_path:
|
||||
template_style = read_template_styles(template_path)
|
||||
if template_style:
|
||||
print(f"[INFO] 已加载模板样式: {template_path}")
|
||||
else:
|
||||
print("[WARN] 模板文件存在但解析失败,使用默认样式")
|
||||
else:
|
||||
print("[INFO] 未检测到模板文件,使用默认样式")
|
||||
|
||||
generate_pinmap(
|
||||
entries=entries,
|
||||
@@ -249,7 +297,7 @@ def run_list_to_map(filepath: str):
|
||||
print(f" PinMAP 尺寸: {rows}×{cols}")
|
||||
print(f" Pin数量: {len(entries)}")
|
||||
|
||||
wait_for_exit()
|
||||
# F008: 不再退出,返回主循环
|
||||
|
||||
|
||||
# ── Main entry ──────────────────────────────────────────────────────
|
||||
@@ -257,41 +305,59 @@ def run_list_to_map(filepath: str):
|
||||
def main():
|
||||
show_banner()
|
||||
|
||||
# ── Direction selection ─────────────────────────────────────────
|
||||
if len(sys.argv) > 1:
|
||||
# Legacy mode: direct file argument → MAP→List
|
||||
direction = 1
|
||||
filepath = sys.argv[1]
|
||||
else:
|
||||
print("请选择转换方向:")
|
||||
print(" 1 — PinMAP → PinList")
|
||||
print(" 2 — PinList → PinMAP")
|
||||
print()
|
||||
# F008: 循环处理流程
|
||||
while True:
|
||||
# ── Direction selection ─────────────────────────────────────
|
||||
if len(sys.argv) > 1:
|
||||
# Legacy mode: direct file argument → MAP→List
|
||||
direction = 1
|
||||
filepath = sys.argv[1]
|
||||
sys.argv = [sys.argv[0]] # 清除 argv,下次循环进入交互模式
|
||||
else:
|
||||
print("请选择转换方向:")
|
||||
print(" 1 — PinMAP → PinList")
|
||||
print(" 2 — PinList → PinMAP")
|
||||
print(" Q — 退出程序")
|
||||
print()
|
||||
|
||||
while True:
|
||||
choice = input("请输入选项 (1/2): ").strip()
|
||||
if choice in ('1', '2'):
|
||||
direction = int(choice)
|
||||
break
|
||||
print("[ERROR] 无效选项,请输入 1 或 2")
|
||||
choice = input("请输入选项 (1/2/Q): ").strip().upper()
|
||||
if choice == 'Q':
|
||||
print("感谢使用,再见!")
|
||||
return
|
||||
elif choice == '1':
|
||||
direction = 1
|
||||
elif choice == '2':
|
||||
direction = 2
|
||||
else:
|
||||
print("[ERROR] 无效选项,请输入 1、2 或 Q")
|
||||
continue
|
||||
|
||||
filepath = None # will be selected inside the flow
|
||||
filepath = None
|
||||
|
||||
# ── Dispatch ────────────────────────────────────────────────────
|
||||
if direction == 1:
|
||||
# ── Dispatch ────────────────────────────────────────────────
|
||||
if direction == 1:
|
||||
print()
|
||||
print("─" * 40)
|
||||
print(" 方向: PinMAP → PinList")
|
||||
print("─" * 40)
|
||||
print()
|
||||
run_map_to_list(filepath)
|
||||
else:
|
||||
print()
|
||||
print("─" * 40)
|
||||
print(" 方向: PinList → PinMAP")
|
||||
print("─" * 40)
|
||||
print()
|
||||
run_list_to_map(filepath)
|
||||
|
||||
# ── 处理完成后循环 ──────────────────────────────────────────
|
||||
print()
|
||||
print("─" * 40)
|
||||
print(" 方向: PinMAP → PinList")
|
||||
print("─" * 40)
|
||||
print()
|
||||
run_map_to_list(filepath)
|
||||
else:
|
||||
print()
|
||||
print("─" * 40)
|
||||
print(" 方向: PinList → PinMAP")
|
||||
print("─" * 40)
|
||||
print()
|
||||
run_list_to_map(filepath)
|
||||
print("=" * 40)
|
||||
next_choice = input("输入 Q 退出,或按 Enter 返回主菜单继续转换: ").strip().upper()
|
||||
if next_choice == 'Q':
|
||||
print("感谢使用,再见!")
|
||||
return
|
||||
# 否则继续 while 循环,回到主菜单
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
Validates a PinList for:
|
||||
1. Pin numbers starting from 1 with no gaps
|
||||
2. No duplicate pin numbers
|
||||
3. Total pin count matches grid perimeter (2×rows + 2×cols − 4)
|
||||
3. Total pin count matches grid perimeter (rows + cols) × 2
|
||||
4. Missing PinName defaults to NC (warning)
|
||||
5. Pin count not a multiple of 4 (info)
|
||||
"""
|
||||
@@ -22,7 +22,7 @@ def validate_pinlist(
|
||||
检查项:
|
||||
1. Pin 序号从 1 开始连续无缺失
|
||||
2. Pin 序号无重复
|
||||
3. Pin 总数 = 2×rows + 2×cols − 4(周长匹配)
|
||||
3. Pin 总数 = (rows + cols) × 2(周长匹配)
|
||||
4. Pin 缺少 PinName 时默认为 NC(warning)
|
||||
5. Pin 数量不是 4 的倍数时提示(info)
|
||||
|
||||
@@ -68,7 +68,8 @@ def validate_pinlist(
|
||||
))
|
||||
|
||||
# ── 3. 周长匹配 ──────────────────────────────────────────────
|
||||
expected_total = 2 * rows + 2 * cols - 4
|
||||
# 周长公式:(rows + cols) * 2
|
||||
expected_total = (rows + cols) * 2
|
||||
actual_total = len(entries)
|
||||
if actual_total != expected_total:
|
||||
errors.append(ValidationError(
|
||||
|
||||
@@ -61,10 +61,17 @@ def generate_pinmap(
|
||||
data[name_ref] = pin_name if pin_name and pin_name.strip() else "NC"
|
||||
|
||||
# 再写入序号单元格(覆盖同位置的名字,确保序号优先)
|
||||
# v1.3: 角点单元格被两条边共享,需写入两个引脚序号
|
||||
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):
|
||||
num_ref = rc_to_cell_ref(num_cell[0], num_cell[1])
|
||||
data[num_ref] = str(pin_num)
|
||||
if num_ref not in cell_pins:
|
||||
cell_pins[num_ref] = []
|
||||
cell_pins[num_ref].append(str(pin_num))
|
||||
|
||||
for num_ref, pins in cell_pins.items():
|
||||
data[num_ref] = "/".join(pins)
|
||||
|
||||
# 3. 写入文件(应用模板样式)
|
||||
if output_path:
|
||||
|
||||
@@ -6,11 +6,13 @@ from the top-left corner (pin 1).
|
||||
|
||||
Edge assignment (counter-clockwise, top-left = pin 1):
|
||||
left → rows pins
|
||||
bottom → cols-1 pins
|
||||
right → rows-2 pins
|
||||
top → cols-1 pins
|
||||
bottom → cols pins
|
||||
right → rows pins
|
||||
top → cols pins
|
||||
|
||||
Total: rows + (cols-1) + (rows-2) + (cols-1) = 2×rows + 2×cols − 4
|
||||
Total: rows + cols + rows + cols = 2×rows + 2×cols = (rows + cols) × 2
|
||||
|
||||
v1.3: 每条边独立包含其端点,角点单元格会被两条边共享。
|
||||
"""
|
||||
|
||||
from models import PinListEntry, EdgePins, LayoutError
|
||||
@@ -27,6 +29,12 @@ def calculate_layout(
|
||||
逆时针分配(左上角为 1 脚):
|
||||
左边 → 下边 → 右边 → 上边
|
||||
|
||||
角点分配策略(v1.3:每条边独立包含其端点):
|
||||
- 左边: 包含左下角
|
||||
- 下边: 包含右下角
|
||||
- 右边: 包含右上角
|
||||
- 上边: 包含左上角
|
||||
|
||||
Parameters
|
||||
----------
|
||||
entries : list[PinListEntry]
|
||||
@@ -52,13 +60,13 @@ def calculate_layout(
|
||||
if cols < 2:
|
||||
raise LayoutError(f"列数无效: {cols},至少需要 2 列")
|
||||
|
||||
# ── 边分配计数 ────────────────────────────────────────────────
|
||||
# ── 边分配计数(v1.3:每条边独立包含其端点)─────────────────
|
||||
left_count = rows
|
||||
bottom_count = cols - 1
|
||||
right_count = rows - 2
|
||||
top_count = cols - 1
|
||||
bottom_count = cols
|
||||
right_count = rows
|
||||
top_count = cols
|
||||
|
||||
total = left_count + bottom_count + right_count + top_count
|
||||
total = (rows + cols) * 2
|
||||
if len(entries) != total:
|
||||
raise LayoutError(
|
||||
f"Pin数量 ({len(entries)}) 与网格周长 ({total}) 不匹配"
|
||||
@@ -83,22 +91,24 @@ def calculate_layout(
|
||||
# 网格坐标体系(0-based):
|
||||
# 方形区域:行 [1..rows],列 [0..cols]
|
||||
# 左边: 序号在 (r, 0), Name 在 (r, 1) 其中 r ∈ [1, rows]
|
||||
# 下边: 序号在 (rows, c), Name 在 (rows-1, c) 其中 c ∈ [1, cols-1]
|
||||
# 右边: 序号在 (r, cols), Name 在 (r, cols-1) 其中 r ∈ [rows-1, 2] 逆序
|
||||
# 上边: 序号在 (1, c), Name 在 (2, c) 其中 c ∈ [cols-1, 2] 逆序
|
||||
# 下边: 序号在 (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] 逆序
|
||||
#
|
||||
# v1.3: 每条边独立包含其端点,角点单元格会被两条边共享
|
||||
#
|
||||
|
||||
# 左边:从上到下
|
||||
left_cells = [(r, 0) for r in range(1, rows + 1)]
|
||||
|
||||
# 下边:从左到右
|
||||
bottom_cells = [(rows, c) for c in range(1, cols)]
|
||||
bottom_cells = [(rows, c) for c in range(1, cols + 1)]
|
||||
|
||||
# 右边:从下到上(逆序)
|
||||
right_cells = [(r, cols) for r in range(rows - 1, 1, -1)]
|
||||
right_cells = [(r, cols) for r in range(rows, 0, -1)]
|
||||
|
||||
# 上边:从右到左(逆序)
|
||||
top_cells = [(1, c) for c in range(cols - 1, 0, -1)]
|
||||
top_cells = [(1, c) for c in range(cols, 0, -1)]
|
||||
|
||||
# ── 构建 EdgePins ─────────────────────────────────────────────
|
||||
def _make_edge(edge_name: str, pin_list: list[PinListEntry],
|
||||
|
||||
@@ -117,41 +117,49 @@ def parse_pinmap(cells: dict[tuple[int, int], str]) -> PinMAP:
|
||||
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.
|
||||
# ── Step 4: walk edges counter-clockwise (v1.3 formula) ──────
|
||||
# Each edge independently includes its endpoints (corners).
|
||||
# Corner cells are read by two edges — this is expected per
|
||||
# v1.3: total = (rows + cols) × 2.
|
||||
pins: list[Pin] = []
|
||||
|
||||
seen_cells: set[tuple[int, int]] = set()
|
||||
|
||||
def _add_pin(r: int, c: int, edge: str, pos: int) -> None:
|
||||
# Skip if this cell was already processed (corner visited by two edges)
|
||||
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,
|
||||
))
|
||||
seen_cells.add((r, c))
|
||||
raw = cells.get((r, c), "")
|
||||
if not raw:
|
||||
return
|
||||
# Handle "6/7" format from corner cells
|
||||
parts = str(raw).strip().split("/")
|
||||
for part in parts:
|
||||
num = _try_int(part)
|
||||
if num is None:
|
||||
continue
|
||||
pins.append(Pin(
|
||||
number=num,
|
||||
name=name_map.get((r, c), ""),
|
||||
edge=edge,
|
||||
position_on_edge=pos,
|
||||
))
|
||||
|
||||
# 4a. Left edge: top → bottom
|
||||
# 4a. Left edge: top → bottom (includes bottom-left corner)
|
||||
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):
|
||||
# 4b. Bottom edge: left → right (includes bottom-right corner)
|
||||
for c in range(min_col, 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):
|
||||
# 4c. Right edge: bottom → top (includes top-right corner)
|
||||
for r in range(max_row, 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):
|
||||
# 4d. Top edge: right → left (includes top-left corner)
|
||||
for c in range(max_col, min_col - 1, -1):
|
||||
_add_pin(min_row, c, "top", max_col - c)
|
||||
|
||||
if not pins:
|
||||
|
||||
@@ -115,7 +115,7 @@ def validate_pinlist_for_map(
|
||||
1. **Continuity** — pin numbers must start from 1 with no gaps.
|
||||
2. **Uniqueness** — no duplicate pin numbers.
|
||||
3. **Perimeter match** — total pin count must equal
|
||||
2×rows + 2×cols − 4 (the grid perimeter).
|
||||
(rows + cols) × 2 (the grid perimeter).
|
||||
4. **Non-multiple-of-4** — if pin count is not a multiple of 4,
|
||||
a warning is issued (but conversion is not blocked).
|
||||
|
||||
@@ -158,7 +158,8 @@ def validate_pinlist_for_map(
|
||||
))
|
||||
|
||||
# ── 3. Perimeter match ───────────────────────────────────────
|
||||
expected_total = 2 * rows + 2 * cols - 4
|
||||
# 周长公式:(rows + cols) * 2
|
||||
expected_total = (rows + cols) * 2
|
||||
actual_total = len(entries)
|
||||
if actual_total != expected_total:
|
||||
result.errors.append(ValidationError(
|
||||
|
||||
Reference in New Issue
Block a user