1124 lines
32 KiB
Markdown
1124 lines
32 KiB
Markdown
# 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 关键技术难点
|
||
|
||
#### 难点 1:PinMAP 布局分配算法
|
||
|
||
**问题**:给定 PinList(N 个 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+cols−1)
|
||
右边 (right) = rows − 2 个 Pin → Pin (rows+cols)..(rows+2×cols−3)
|
||
上边 (top) = cols − 1 个 Pin → Pin (rows+2×cols−2)..N
|
||
|
||
验证:rows + (cols−1) + (rows−2) + (cols−1) = 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 # PinName(A 列,可能为空)
|
||
|
||
|
||
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 时默认为 NC(warning)
|
||
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. 缺失 PinName(warning)
|
||
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 编码 Agent(1-2 个) |
|
||
| 风险等级 | 中低 |
|
||
| 第三方依赖 | 零新增 |
|
||
|
||
**结论**:
|
||
1. PinList → PinMAP 反向转换在技术上完全可行
|
||
2. 核心难点在于 OOXML 样式解析/写入,但可通过最小可用集逐步实现
|
||
3. 布局分配算法清晰,公式化计算,边界条件可控
|
||
4. 建议分 5 轮迭代开发,先完成核心转换逻辑,再增强模板支持
|
||
5. 与现有架构完全兼容,向后无破坏性变更
|
||
|
||
---
|
||
|
||
*文档结束 — 请审批后进入编码阶段*
|