Files
pinmap-to-pinlist/Code/docs/architecture-design.md

1124 lines
32 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# PinMAP ↔ PinList 双向转换器 — 全局架构设计
> **版本**: v2.0
> **日期**: 2026-05-28
> **架构师**: 脚本架构师 (Script Architect)
> **状态**: 待审批
> **变更**: 新增 PinList → PinMAP 反向转换 + 模板格式支持
---
## 1. 项目概述
### 1.1 背景
将 Excel 格式的 **PinMAP** 文件(方形封装引脚布局图)与 **PinList** 格式(引脚序号列表)进行**双向自动转换**,消除手动抄录的低效与错误风险。
- **PinMAP → PinList**(已有):方形布局图 → 扁平引脚列表
- **PinList → PinMAP**(新增):扁平引脚列表 → 方形布局图
### 1.2 核心规则
- PinMAP 为方形/长方形结构,引脚沿四条边分布,左上角为 1 脚,**逆时针**排序
- 四条边遍历顺序左→下→右→上counter-clockwise
- 四个角点被相邻两边共享(不重复计数)
- 总 Pin 数 = 2 × width + 2 × height 4
### 1.3 约束
| 约束项 | 说明 |
|--------|------|
| 运行平台 | Windows |
| 技术栈 | Python 标准库,**零第三方依赖** |
| 输入格式 | `.xls`(必须支持)、`.xlsx`(优先支持) |
| 输出格式 | `.xlsx` 仅 |
| 交互方式 | 命令行 + 文件选择对话框 |
| 模板 | 可选,根目录 `PinMAP-Template.xlsx` |
---
## 2. 技术可行性评估
### 2.1 PinList → PinMAP 反向转换
**可行性:✅ 完全可行**
| 子任务 | 技术可行性 | 说明 |
|--------|-----------|------|
| PinList 读取与解析 | ✅ 低难度 | 复用现有 xls_reader/xlsx_reader解析 A/B 两列即可 |
| Pin 数据验证 | ✅ 低难度 | 连续性、唯一性检查逻辑与现有 validator 类似 |
| 尺寸输入 | ✅ 低难度 | 命令行 `input()` 交互,输入行数/列数 |
| PinMAP 布局计算 | ✅ 中难度 | 核心算法:根据 width/height 将 Pin 分配到四条边 |
| PinMAP 生成与输出 | ✅ 中难度 | 需要增强 xlsx_writer 支持模板样式 |
| 模板检测与读取 | ✅ 中难度 | 需新增 xlsx style 读取能力styles.xml 解析) |
### 2.2 关键技术难点
#### 难点 1PinMAP 布局分配算法
**问题**:给定 PinListN 个 Pin已按序号 1..N 排序)和尺寸 (rows, cols),如何将 Pin 分配到四条边?
**公式**(逆时针,左上角为 1 脚):
```
总 Pin 数 N = 2 × cols + 2 × rows 4
边分配(按遍历顺序):
左边 (left) = rows 个 Pin → Pin 1..rows
下边 (bottom) = cols 1 个 Pin → Pin (rows+1)..(rows+cols1)
右边 (right) = rows 2 个 Pin → Pin (rows+cols)..(rows+2×cols3)
上边 (top) = cols 1 个 Pin → Pin (rows+2×cols2)..N
验证rows + (cols1) + (rows2) + (cols1) = 2×rows + 2×cols 4 = N ✓
```
**示例**4×8 网格rows=4, cols=8, N=20
```
左边Pin 1-4 (4个)
下边Pin 5-12 (8个)
右边Pin 13-15 (3个)
上边Pin 16-20 (5个)
总计4+8+3+5 = 20 ✓
```
**非 4 倍数处理**:提示用户 "Pin 数量 {N} 不是 4 的倍数,四条边将不均匀分布",不影响转换。
#### 难点 2模板样式读取
**问题**:从零实现的 xlsx_reader 目前只读取单元格值不读取样式字体、边框、背景色等。PinMAP 输出需要参考模板的单元格样式。
**方案**
1. 新增 `template_reader.py` 模块,专门解析 `xl/styles.xml`
2. 提取关键样式信息:字体、边框、填充色、对齐方式
3.`pinmap_generator.py` 中应用模板样式到输出文件
4. 模板不存在时,使用默认样式(无边框、默认字体)
**OOXML styles.xml 结构**
```xml
<stylesSheet>
<fonts count="1">
<font><sz val="11"/><name val="Calibri"/></font>
</fonts>
<fills count="2">...</fills>
<borders count="1">...</borders>
<cellXfs count="1">
<xf numFmtId="0" fontId="0" fillId="0" borderId="0"/>
</cellXfs>
</stylesSheet>
```
**复杂度评估**:中等。需解析 XML 结构,提取 cellXfs单元格格式与 fonts/borders/fills 的关联关系。
#### 难点 3双向交互流程
**问题**main.py 需要支持两种转换方向,交互流程不同。
**方案**:启动时让用户选择方向,然后进入对应的流程。保持与现有 PinMAP→PinList 一致的交互风格Banner → 文件选择 → 处理 → 结果摘要)。
---
## 3. 模块划分
### 3.1 现有模块PinMAP → PinList 方向)
```
Code/src/
├── main.py # 入口:流程编排(需修改)
├── file_selector.py # 文件选择(需修改)
├── xls_reader.py # XLS 解析引擎(不变)
├── xlsx_reader.py # XLSX 解析引擎(不变)
├── pinmap_parser.py # PinMAP 结构解析(不变)
├── validator.py # 数据验证(需修改)
├── pinlist_generator.py # PinList 生成(不变)
├── xlsx_writer.py # XLSX 输出引擎(需修改)
├── models.py # 数据模型(需修改)
└── utils.py # 工具函数(不变)
```
### 3.2 新增模块PinList → PinMAP 方向)
| 模块 | 文件 | 职责 |
|------|------|------|
| **PinList 解析器** | `pinlist_parser.py` | 读取 PinList xlsx解析 A1 封装信息 + A/B 列 PinName/序号 |
| **PinList 验证器** | `pinlist_validator.py` | 验证 Pin 序号连续性、唯一性、与周长匹配 |
| **PinMAP 布局计算** | `pinmap_layout.py` | 根据尺寸将 Pin 分配到四条边,计算单元格坐标 |
| **PinMAP 生成器** | `pinmap_generator.py` | 组装 PinMAP 单元格数据,应用模板样式 |
| **模板读取器** | `template_reader.py` | 读取 PinMAP-Template.xlsx 的样式信息 |
### 3.3 修改后完整目录结构
```
pinmap-to-pinlist/
├── Code/
│ ├── src/
│ │ ├── main.py # ✏️ 修改:双向选择 + 双流程
│ │ ├── file_selector.py # ✏️ 修改:支持两种文件类型过滤
│ │ ├── models.py # ✏️ 修改:新增 PinList 输入模型
│ │ ├── validator.py # ✏️ 修改:新增 PinList 验证函数
│ │ ├── xlsx_writer.py # ✏️ 修改:支持样式写入
│ │ │
│ │ ├── xls_reader.py # (不变)
│ │ ├── xlsx_reader.py # (不变)
│ │ ├── pinmap_parser.py # (不变)
│ │ ├── pinlist_generator.py # (不变)
│ │ ├── utils.py # (不变)
│ │ │
│ │ ├── pinlist_parser.py # 🆕 PinList 读取与解析
│ │ ├── pinlist_validator.py # 🆕 PinList 数据验证
│ │ ├── pinmap_layout.py # 🆕 PinMAP 布局计算
│ │ ├── pinmap_generator.py # 🆕 PinMAP 生成与输出
│ │ └── template_reader.py # 🆕 模板样式读取
│ └── docs/
│ ├── architecture-design.md # ✏️ 更新
│ └── modification-assessment.md
├── Test/
│ └── fixtures/
│ ├── sample_4x4.xlsx # 现有 PinMAP 测试
│ ├── sample_rect.xlsx
│ ├── sample_pinlist.xlsx # 🆕 PinList 测试
│ └── PinMAP-Template.xlsx # 🆕 模板文件(可选)
├── run.bat # (不变)
└── Releases/
```
---
## 4. 模块详细设计
### 4.1 新增模块:`pinlist_parser.py`
**职责**:读取 PinList 格式的 xlsx 文件,解析封装信息和引脚数据。
```python
"""PinList parser — reads a flat pin list from an Excel file."""
from dataclasses import dataclass
@dataclass
class PinListEntry:
"""A single pin entry from the PinList."""
number: int # Pin 序号B 列)
name: str # PinNameA 列,可能为空)
class PinListParser:
"""Parse a PinList Excel file."""
def __init__(self, filepath: str):
self._filepath = filepath
def parse(self) -> tuple[str, list[PinListEntry]]:
"""
解析 PinList 文件。
Returns
-------
(package_info, entries)
package_info: A1 单元格的封装信息
entries: 按 Pin 序号排序的引脚列表
Raises
------
StructureError
A1 为空、A/B 列无数据、序号非整数等
"""
# 1. 读取文件(复用 xlsx_reader / xls_reader
# 2. 提取 A1 封装信息
# 3. 解析 A 列 (PinName) 和 B 列 (Pin序号)
# 4. 按 Pin 序号排序
# 5. 返回 (package_info, entries)
```
**解析规则**
- A1 单元格 = 封装信息(与 PinMAP 方向一致)
- A 列(从 A2 开始)= PinName
- B 列(从 B2 开始)= Pin 序号
- 读取到第一个空行为止
- Pin 序号可能不是按顺序排列的(需要在 validator 中检查)
### 4.2 新增模块:`pinlist_validator.py`
**职责**:验证 PinList 数据的完整性和正确性。
```python
"""PinList validator — checks pin data integrity."""
from models import ValidationResult, ValidationError
def validate_pinlist(
entries: list[PinListEntry],
rows: int,
cols: int,
) -> ValidationResult:
"""
验证 PinList 数据。
检查项:
1. Pin 序号从 1 开始连续无缺失
2. Pin 序号无重复
3. Pin 总数 = 2×rows + 2×cols 4周长匹配
4. Pin 缺少 PinName 时默认为 NCwarning
5. Pin 数量不是 4 的倍数时提示info
Parameters
----------
entries : list[PinListEntry]
已按序号排序的引脚列表
rows : int
用户输入的 PinMAP 行数
cols : int
用户输入的 PinMAP 列数
Returns
-------
ValidationResult
"""
```
**验证逻辑**
```python
# 1. 连续性检查
numbers = sorted(e.number for e in entries)
expected = list(range(1, len(numbers) + 1))
if numbers != expected:
missing = set(expected) - set(numbers)
errors.append(f"Pin序号不连续缺失: {sorted(missing)}")
# 2. 唯一性检查
if len(numbers) != len(set(numbers)):
errors.append("Pin序号存在重复")
# 3. 周长匹配
expected_total = 2 * rows + 2 * cols - 4
if len(entries) != expected_total:
errors.append(
f"Pin数量 ({len(entries)}) 与网格周长 ({expected_total}) 不匹配"
)
# 4. 缺失 PinNamewarning
missing_names = [e for e in entries if not e.name or not e.name.strip()]
if missing_names:
warnings.append(f"{len(missing_names)} 个引脚缺少 PinName将默认为 NC")
# 5. 非 4 倍数提示info
if len(entries) % 4 != 0:
infos.append(f"Pin数量 ({len(entries)}) 不是 4 的倍数,四条边将不均匀分布")
```
### 4.3 新增模块:`pinmap_layout.py`
**职责**:根据 PinMAP 尺寸和 PinList 数据,计算每条边的 Pin 分布和单元格坐标。
```python
"""PinMAP layout calculator — distributes pins to four edges."""
from dataclasses import dataclass
from typing import NamedTuple
class CellRef(NamedTuple):
"""Excel cell reference (row, col), 0-based."""
row: int
col: int
@dataclass
class EdgePins:
"""Pins assigned to one edge of the PinMAP."""
edge: str # "left" | "bottom" | "right" | "top"
pins: list[tuple[int, str]] # [(number, name), ...]
cells: list[CellRef] # 对应的单元格坐标
def calculate_layout(
entries: list[PinListEntry],
rows: int,
cols: int,
) -> dict[str, EdgePins]:
"""
计算 PinMAP 布局。
逆时针分配(左上角为 1 脚):
左边 → 下边 → 右边 → 上边
Parameters
----------
entries : list[PinListEntry]
已按序号排序的引脚列表
rows : int
PinMAP 行数
cols : int
PinMAP 列数
Returns
-------
dict[str, EdgePins]
{"left": ..., "bottom": ..., "right": ..., "top": ...}
Raises
------
StructureError
尺寸无效rows < 2 或 cols < 2
"""
```
**布局算法**
```python
def calculate_layout(entries, rows, cols):
"""
网格坐标体系0-based
假设方形区域从 (1, 0) 开始Excel 的 A2
方形区域:行 [1..rows],列 [0..cols-1]
四条边的单元格坐标:
左边 (left): 序号在 (r, 0), Name 在 (r, 1) 其中 r ∈ [1, rows]
下边 (bottom): 序号在 (rows, c), Name 在 (rows-1, c) 其中 c ∈ [1, cols-1]
右边 (right): 序号在 (r, cols-1), Name 在 (r, cols-2) 其中 r ∈ [rows-1, 2]
上边 (top): 序号在 (1, c), Name 在 (2, c) 其中 c ∈ [cols-2, 1]
Pin 分配(按逆时针遍历顺序):
左边: Pin 1..rows → rows 个
下边: Pin (rows+1)..(rows+cols-1) → cols-1 个
右边: Pin (rows+cols)..(rows+2*cols-3) → rows-2 个
上边: Pin (rows+2*cols-2)..N → cols-1 个
"""
```
**示例**4×8 网格
```
左边 (4个): Pin 1-4
序号: (1,0)=1, (2,0)=2, (3,0)=3, (4,0)=4
Name: (1,1), (2,1), (3,1), (4,1)
下边 (8个): Pin 5-12
序号: (4,1)=5, (4,2)=6, ..., (4,8)=12
Name: (3,1), (3,2), ..., (3,8)
右边 (3个): Pin 13-15
序号: (3,8)=13, (2,8)=14, (1,8)=15
Name: (3,7), (2,7), (1,7)
上边 (5个): Pin 16-20
序号: (1,7)=16, (1,6)=17, ..., (1,3)=20
Name: (2,7), (2,6), ..., (2,3)
```
### 4.4 新增模块:`pinmap_generator.py`
**职责**:根据布局计算结果,生成 PinMAP 的单元格数据字典,并写入 xlsx 文件。
```python
"""PinMAP generator — builds PinMAP cell data and writes to xlsx."""
from pinmap_layout import calculate_layout, EdgePins
from template_reader import TemplateStyle
from xlsx_writer import write_xlsx_with_style
def generate_pinmap(
entries: list[PinListEntry],
rows: int,
cols: int,
package_info: str,
template_style: TemplateStyle | None = None,
output_path: str | None = None,
) -> dict[str, str]:
"""
生成 PinMAP 布局并写入文件。
Parameters
----------
entries : list[PinListEntry]
PinList 数据
rows : int
PinMAP 行数
cols : int
PinMAP 列数
package_info : str
封装信息(写入 A1
template_style : TemplateStyle | None
模板样式(可选)
output_path : str | None
输出文件路径
Returns
-------
dict[str, str]
单元格数据字典 {"A1": "封装", "A2": "1", "B2": "Pin1", ...}
"""
# 1. 计算布局
layout = calculate_layout(entries, rows, cols)
# 2. 构建单元格数据
data = {"A1": package_info}
for edge_name, edge in layout.items():
for (pin_num, pin_name), cell in zip(edge.pins, edge.cells):
data[f"{cell_ref(cell)}"] = str(pin_num)
name_cell = get_name_cell(cell, edge_name)
data[f"{cell_ref(name_cell)}"] = pin_name or "NC"
# 3. 写入文件(应用模板样式)
if output_path:
write_xlsx_with_style(data, output_path, template_style)
return data
```
### 4.5 新增模块:`template_reader.py`
**职责**:读取 PinMAP-Template.xlsx 的样式信息。
```python
"""Template reader — extracts cell styles from a template xlsx file."""
from dataclasses import dataclass, field
from typing import Optional
@dataclass
class FontStyle:
"""Font style."""
name: str = "Calibri"
size: float = 11.0
bold: bool = False
italic: bool = False
color: str = "000000"
@dataclass
class BorderStyle:
"""Border style for a cell."""
top: str = "none" # none | thin | medium | thick
bottom: str = "none"
left: str = "none"
right: str = "none"
color: str = "000000"
@dataclass
class FillStyle:
"""Cell fill style."""
pattern_type: str = "none" # none | solid | gray125
fg_color: str = ""
@dataclass
class AlignmentStyle:
"""Cell alignment."""
horizontal: str = "center" # left | center | right
vertical: str = "center"
@dataclass
class TemplateStyle:
"""Complete style extracted from template."""
fonts: list[FontStyle] = field(default_factory=list)
borders: list[BorderStyle] = field(default_factory=list)
fills: list[FillStyle] = field(default_factory=list)
cell_xfs: list[dict] = field(default_factory=list) # xf index → style mapping
column_widths: dict[int, float] = field(default_factory=dict)
row_heights: dict[int, float] = field(default_factory=dict)
class TemplateReader:
"""Read styles from a template xlsx file."""
def __init__(self, filepath: str):
self._filepath = filepath
def read_styles(self) -> TemplateStyle:
"""
读取模板文件的样式信息。
解析 xl/styles.xml 中的:
- fonts, fills, borders, cellXfs
- 列宽和行高(从 sheetData 读取)
Returns
-------
TemplateStyle
"""
```
**实现要点**
- 复用 `xlsx_reader.py` 的 ZIP 解压和 XML 解析能力
- 新增解析 `xl/styles.xml` 的逻辑
- 提取 `cellXfs` 中每个 xf 索引对应的 fontId/borderId/fillId
- 列宽从 `<cols>` 元素读取,行高从 `<row>` 元素的 `ht` 属性读取
### 4.6 修改模块:`main.py`
**修改内容**:支持双向转换,启动时选择方向。
```python
def main():
show_banner()
# ── 选择转换方向 ──────────────────────────────────────────
direction = select_direction() # "map_to_list" or "list_to_map"
if direction == "map_to_list":
run_map_to_list()
else:
run_list_to_map()
def select_direction() -> str:
"""让用户选择转换方向。"""
print("请选择转换方向:")
print(" 1. PinMAP → PinList (方形布局图转引脚列表)")
print(" 2. PinList → PinMAP (引脚列表转方形布局图)")
while True:
choice = input("请输入选项 (1/2): ").strip()
if choice == "1":
return "map_to_list"
elif choice == "2":
return "list_to_map"
print("无效选项,请重新输入")
def run_map_to_list():
"""PinMAP → PinList 流程(现有逻辑,增加模板格式参考)"""
# 1. 文件选择PinMAP 文件)
# 2. 读取 Excel
# 3. 解析 PinMAP
# 4. 验证
# 5. 生成 PinList
# 6. 写入 xlsx如有模板则参考格式
# 7. 结果摘要
def run_list_to_map():
"""PinList → PinMAP 流程(新增)"""
# 1. 文件选择PinList 文件)
# 2. 解析 PinList
# 3. 输入尺寸(行数、列数)
# 4. 验证 PinList 数据
# 5. 计算布局
# 6. 生成 PinMAP
# 7. 写入 xlsx应用模板样式
# 8. 结果摘要
```
### 4.7 修改模块:`file_selector.py`
**修改内容**:支持两种文件类型的过滤。
```python
def select_file(direction: str) -> Optional[str]:
"""
文件选择流程。
Parameters
----------
direction : str
"map_to_list" → 选择 PinMAP 文件
"list_to_map" → 选择 PinList 文件
"""
if direction == "map_to_list":
title = "选择 PinMAP 文件"
else:
title = "选择 PinList 文件"
# 复用现有逻辑,仅修改提示文本和对话框标题
```
### 4.8 修改模块:`models.py`
**修改内容**:新增 PinList 输入相关的数据模型。
```python
# 新增数据模型
@dataclass
class PinListEntry:
"""PinList 中的单个引脚条目。"""
number: int
name: str
@dataclass
class PinMAPLayout:
"""计算出的 PinMAP 布局。"""
package_info: str
rows: int
cols: int
edges: dict[str, EdgePins] # left/bottom/right/top
cells: dict[str, str] # 单元格数据 {"A2": "1", "B2": "Pin1", ...}
# 新增异常
class LayoutError(PinMapError):
"""布局计算错误尺寸无效、Pin 数量不匹配等)。"""
```
### 4.9 修改模块:`xlsx_writer.py`
**修改内容**:支持样式写入(用于 PinMAP 输出)。
```python
def write_xlsx_with_style(
data: dict[str, str],
output_path: str,
style: TemplateStyle | None = None,
):
"""
写入 xlsx 文件,可选应用模板样式。
Parameters
----------
data : dict[str, str]
单元格数据
output_path : str
输出路径
style : TemplateStyle | None
模板样式(可选)
"""
# 现有 write_xlsx() 保持不变(用于 PinList 输出)
# 新增 write_xlsx_with_style() 用于 PinMAP 输出(需要样式)
```
**样式写入实现**
- 在 OOXML 中生成 `xl/styles.xml`
- 在 sheet1.xml 的 `<c>` 元素中添加 `s` 属性style index
- 如果模板不存在,使用最小样式集(无边框、默认字体)
### 4.10 修改模块:`validator.py`
**修改内容**:新增 `validate_pinlist()` 函数,与现有 `validate_pinmap()` 并存。
```python
# 新增函数(不修改现有 validate_pinmap
def validate_pinlist(entries, rows, cols) -> ValidationResult:
"""验证 PinList 数据(独立函数,不修改现有 validate_pinmap"""
```
**建议**:将 `validate_pinlist()` 放在独立的 `pinlist_validator.py` 中,保持单一职责。
---
## 5. 数据流设计
### 5.1 PinMAP → PinList 流程(现有 + 模板增强)
```
PinMAP 文件 (.xls/.xlsx)
xls_reader / xlsx_reader
│ → dict[(row,col), str]
pinmap_parser.parse_pinmap()
│ → PinMAP
validator.validate_pinmap()
│ → ValidationResult
pinlist_generator.generate_pinlist()
│ → PinList
xlsx_writer.write_xlsx() ← 如有模板,参考格式
PinList 文件 (.xlsx)
```
### 5.2 PinList → PinMAP 流程(新增)
```
PinList 文件 (.xls/.xlsx)
pinlist_parser.parse()
│ → (package_info, entries)
用户输入:行数、列数
pinlist_validator.validate_pinlist()
│ → ValidationResult
├─ 缺失 PinName → 提示确认,默认 NC
├─ 非 4 倍数 → 提示
└─ 错误 → 终止
pinmap_layout.calculate_layout()
│ → dict[str, EdgePins]
pinmap_generator.generate_pinmap()
│ → cells dict + 写入文件
└─ 检测 PinMAP-Template.xlsx
├─ 存在 → template_reader.read_styles() → 应用样式
└─ 不存在 → 默认样式
PinMAP 文件 (.xlsx)
```
---
## 6. PinList 文件格式规范
### 6.1 格式定义
| 单元格 | 内容 | 说明 |
|--------|------|------|
| A1 | 封装信息 | 如 "QFN-20"、"BGA-256" |
| A2 | PinName1 | 引脚名称 |
| B2 | 1 | Pin 序号 |
| A3 | PinName2 | 引脚名称 |
| B3 | 2 | Pin 序号 |
| ... | ... | ... |
| An | PinNameN | 引脚名称 |
| Bn | N | Pin 序号 |
### 6.2 示例
```
A1: "QFN-20"
A2: "VCC" B2: "1"
A3: "GND" B3: "2"
A4: "IN1" B4: "3"
A5: "IN2" B5: "4"
A6: "OUT1" B6: "5"
...
A21: "RST" B21: "20"
```
### 6.3 约束
- A1 不能为空(封装信息)
- B 列必须为整数Pin 序号)
- A 列可为空(缺失时默认 NC
- Pin 序号从 1 开始连续
- 读取到第一个空行A 列和 B 列都为空)为止
---
## 7. 模板格式规范
### 7.1 模板文件
- **文件名**`PinMAP-Template.xlsx`
- **位置**:程序根目录(与 `run.bat` 同级)
- **作用**:提供输出 PinMAP 文件的样式参考
### 7.2 模板内容
模板文件应包含:
- 正确的方形布局结构(用于参考单元格位置)
- 单元格样式:字体、字号、边框、填充色、对齐方式
- 列宽和行高设置
### 7.3 模板使用方式
- 模板**仅用于样式参考**,不用于结构参考
- 程序从模板提取:字体、边框、填充、对齐、列宽、行高
- 实际引脚数据由 PinList 决定
- 模板不存在时使用默认样式无边框、Calibri 11号、居中对齐
---
## 8. 交互流程设计
### 8.1 启动 Banner
```
============================================================
PinMAP ↔ PinList 双向转换器
自动在方形布局图与引脚列表之间转换
支持.xls和.xlsx格式输出.xlsx格式
============================================================
请选择转换方向:
1. PinMAP → PinList (方形布局图转引脚列表)
2. PinList → PinMAP (引脚列表转方形布局图)
请输入选项 (1/2):
```
### 8.2 PinList → PinMAP 完整交互流程
```
[INFO] 正在读取 PinList 文件: C:\test\pinlist.xlsx
[INFO] 文件读取完成,共 20 个引脚
[INFO] 封装信息: QFN-20
请输入 PinMAP 尺寸:
行数: 4
列数: 8
[INFO] 正在验证数据...
[WARN] 检测到 2 个引脚缺少 PinName将默认为 NC
[INFO] Pin数量 (20) 是 4 的倍数,四条边均匀分布
[INFO] 验证通过
[INFO] 正在计算 PinMAP 布局...
[INFO] 布局计算完成: 4×8 网格左边4个 + 下边8个 + 右边3个 + 上边5个
[INFO] 检测到模板文件: PinMAP-Template.xlsx
[INFO] 正在生成 PinMAP...
[INFO] 正在写入输出文件: C:\test\pinlist_PinMAP.xlsx
[SUCCESS] 转换完成!
输出文件: C:\test\pinlist_PinMAP.xlsx
封装信息: QFN-20
网格尺寸: 4×8
Pin数量: 20
模板: 已应用
```
### 8.3 错误处理交互
```
[ERROR] Pin序号不连续缺失: [3, 7, 15]
转换终止,请修正 PinList 文件后重试。
按 Enter 键退出...
```
```
[ERROR] Pin数量 (18) 与网格周长 (20) 不匹配
网格 4×8 需要 20 个引脚,但 PinList 只有 18 个
转换终止,请检查尺寸或 PinList 数据。
按 Enter 键退出...
```
---
## 9. 工作量评估
### 9.1 开发任务拆分
| 任务编号 | 任务 | 文件 | 工作量 | 依赖 |
|---------|------|------|--------|------|
| **T1** | PinList 解析器 | `pinlist_parser.py` | 1h | 无 |
| **T2** | PinList 验证器 | `pinlist_validator.py` | 1h | T1 |
| **T3** | PinMAP 布局计算 | `pinmap_layout.py` | 2h | T1, T2 |
| **T4** | PinMAP 生成器 | `pinmap_generator.py` | 1.5h | T3 |
| **T5** | 模板读取器 | `template_reader.py` | 2.5h | 无 |
| **T6** | xlsx_writer 样式支持 | `xlsx_writer.py` | 2h | T5 |
| **T7** | main.py 双向改造 | `main.py` | 1.5h | T1-T6 |
| **T8** | file_selector 修改 | `file_selector.py` | 0.5h | T7 |
| **T9** | models.py 扩展 | `models.py` | 0.5h | T1 |
| **T10** | 测试用例 | `test_pinlist.py` | 2h | T1-T9 |
| **T11** | 测试数据文件 | `Test/fixtures/` | 1h | T10 |
**总计预估**:约 15.5 小时(约 2 个工作日)
### 9.2 复杂度分级
| 复杂度 | 任务 | 原因 |
|--------|------|------|
| **低** | T1, T2, T8, T9 | 逻辑简单,复用现有代码 |
| **中** | T3, T4, T7 | 核心业务逻辑,需仔细处理边界条件 |
| **高** | T5, T6, T10 | OOXML 样式解析/写入复杂,测试覆盖全面 |
### 9.3 推荐开发顺序
```
第1轮并行
T1: PinList 解析器
T5: 模板读取器(可独立开发)
第2轮依赖 T1
T2: PinList 验证器
T3: PinMAP 布局计算
第3轮依赖 T2, T3
T4: PinMAP 生成器
T6: xlsx_writer 样式支持
第4轮集成
T7: main.py 双向改造
T8: file_selector 修改
T9: models.py 扩展
第5轮测试
T10: 测试用例
T11: 测试数据文件
```
---
## 10. 风险评估
| 风险 | 影响 | 概率 | 缓解措施 |
|------|------|------|---------|
| OOXML styles.xml 解析复杂度高 | 高 | 中 | 先实现最小可用样式集(字体+边框),逐步完善 |
| Pin 数量与周长不匹配的用户输入 | 中 | 高 | 在 validator 中严格检查,给出明确的错误提示 |
| 模板文件存在但格式异常 | 中 | 低 | 优雅降级:模板解析失败时回退到默认样式 |
| 非 4 倍数 Pin 数量的边界处理 | 低 | 中 | 明确文档化分配公式,测试各种尺寸组合 |
| 双向交互流程的用户体验 | 中 | 低 | 保持与现有流程一致的交互风格 |
---
## 11. 与现有架构的兼容性
### 11.1 向后兼容
- 现有 PinMAP → PinList 流程**完全不受影响**
- 新增模块与现有模块**松耦合**,通过接口交互
- 模板功能为**可选增强**,不影响无模板场景
### 11.2 代码复用
| 现有模块 | 复用方式 |
|---------|---------|
| `xls_reader.py` | PinList 解析直接复用 |
| `xlsx_reader.py` | PinList 解析 + 模板读取复用 ZIP/XML 解析 |
| `models.py` | 扩展新增数据模型 |
| `utils.py` | 坐标转换工具直接复用 |
| `xlsx_writer.py` | 扩展新增样式写入能力 |
### 11.3 零第三方依赖保持
所有新增功能仍使用 Python 标准库实现:
- Excel 读写:`zipfile` + `xml.etree.ElementTree` + `struct`
- 文件选择:`tkinter.filedialog`
- 样式解析:`xml.etree.ElementTree`
---
## 12. 测试策略
### 12.1 单元测试
```python
# test_pinlist.py
def test_pinlist_parse_basic():
"""基本 PinList 解析"""
def test_pinlist_parse_unsorted():
"""PinList 序号非顺序排列"""
def test_pinlist_parse_missing_names():
"""缺失 PinName 的处理"""
def test_pinlist_validate_continuous():
"""连续性验证"""
def test_pinlist_validate_duplicate():
"""重复序号验证"""
def test_pinlist_validate_perimeter_match():
"""周长匹配验证"""
def test_layout_4x4():
"""4×4 网格布局计算"""
def test_layout_4x8():
"""4×8 长方形布局计算"""
def test_layout_non_multiple_of_4():
"""非 4 倍数 Pin 数量布局"""
def test_template_read():
"""模板样式读取"""
def test_pinmap_generate():
"""PinMAP 生成与写入"""
```
### 12.2 集成测试
| 测试场景 | 输入 | 预期 |
|---------|------|------|
| 正向转换 | sample_4x4.xlsx | 生成 PinList与手动结果一致 |
| 反向转换 | 生成的 PinList | 还原为原始 PinMAP |
| 往返转换 | PinMAP → PinList → PinMAP | 与原始 PinMAP 一致 |
| 模板应用 | PinList + Template | 输出文件样式与模板一致 |
| 无模板 | PinList无模板 | 输出文件使用默认样式 |
| 错误输入 | 缺失序号的 PinList | 报错并终止 |
| 尺寸不匹配 | PinList + 错误尺寸 | 报错并终止 |
---
## 13. 接口契约
### 13.1 新增模块接口
```python
# pinlist_parser.py
def parse_pinlist(filepath: str) -> tuple[str, list[PinListEntry]]:
"""
输入: PinList 文件路径
输出: (封装信息, 按序号排序的引脚列表)
"""
# pinlist_validator.py
def validate_pinlist(
entries: list[PinListEntry],
rows: int,
cols: int,
) -> ValidationResult:
"""
输入: 引脚列表 + 尺寸
输出: ValidationResult
"""
# pinmap_layout.py
def calculate_layout(
entries: list[PinListEntry],
rows: int,
cols: int,
) -> dict[str, EdgePins]:
"""
输入: 引脚列表 + 尺寸
输出: 四条边的引脚分配
"""
# pinmap_generator.py
def generate_pinmap(
entries: list[PinListEntry],
rows: int,
cols: int,
package_info: str,
template_style: TemplateStyle | None = None,
output_path: str | None = None,
) -> dict[str, str]:
"""
输入: 引脚数据 + 尺寸 + 封装 + 可选模板样式
输出: 单元格数据字典
"""
# template_reader.py
def read_template_styles(filepath: str) -> TemplateStyle | None:
"""
输入: 模板文件路径
输出: 模板样式(不存在或解析失败时返回 None
"""
```
---
## 14. 总结
| 项目 | 内容 |
|------|------|
| 新增模块数 | 5 个 |
| 修改模块数 | 5 个 |
| 不变模块数 | 5 个 |
| 技术难度 | 中(核心难点在 OOXML 样式处理) |
| 预估工作量 | ~15.5 小时(约 2 个工作日) |
| 推荐 Agent | Python 编码 Agent1-2 个) |
| 风险等级 | 中低 |
| 第三方依赖 | 零新增 |
**结论**
1. PinList → PinMAP 反向转换在技术上完全可行
2. 核心难点在于 OOXML 样式解析/写入,但可通过最小可用集逐步实现
3. 布局分配算法清晰,公式化计算,边界条件可控
4. 建议分 5 轮迭代开发,先完成核心转换逻辑,再增强模板支持
5. 与现有架构完全兼容,向后无破坏性变更
---
*文档结束 — 请审批后进入编码阶段*