# A1 Swap Mod Packer 工作原理

**版本**: v0.6.0 | **日期**: 2026-07-27

## 总览

给一个源 `.3mf` 文件，设好份数和换盘 G-code，输出一个打包好的新 `.3mf`。

```mermaid
flowchart LR
    A["源 3MF × N 份"] --> B["① 解压读取"]
    B --> C["② 按份数展开"]
    C --> D["③ 预处理<br/>(换行/补丁/M73模板)"]
    D --> E["④ 逐份加工<br/>(M73偏移/插入换盘块)"]
    E --> F["⑤ 拼接总G-code"]
    F --> G["⑥ 写入新3MF<br/>(删除旧gcode/更新元数据/合成预览/压缩)"]
    G --> H["输出 3MF"]
```

---

## ① 解压读取

`.3mf` 本质是 ZIP 包。`load_plate_sources()` 打开它，提取三样东西：

| 文件名 | 内容 |
|--------|------|
| `Metadata/plate_N.gcode` | 面板 G-code 文本 |
| `Metadata/slice_info.config` | 打印时间（prediction）、重量（weight） |
| `Metadata/filament` 节点 | 耗材长度（used_m）、耗材重量（used_g） |

同时记下 G-code 成员的名称（比如 `Metadata/plate_1.gcode`），后面写回时要用。

---

## ② 按份数展开

`expand_jobs()` 遍历所有输入文件，每个文件按设定的份数重复展开。

例：`A.3mf` 里有 1 块板，设 5 份 → 展开成 5 个 `PlateSource`：
- 都指向 `A.3mf`
- 都引用 `Metadata/plate_1.gcode`
- 各自携带同样的预测时间、耗材信息

实际上 G-code 文本是**按需读取**的——相同 `(源文件, 面板名)` 组合只读取一次，后面的副本引用已有的 `PlateSource`。

---

## ③ 预处理

对每个唯一的 G-code 文本，只做一遍以下处理，结果按 `(源文件路径, 面板成员名)` 键缓存：

### 换行统一

```python
text.replace("\r\n", "\n").replace("\r", "\n")
```

### G-code 补丁（可选）

如果启用了"应用可编辑的 G-code 补丁"，按 `gcode_patches.ini` 的规则做查找替换：

```
[patch.a1_start_y]
flag = G0 X128 F30000     ← 从这行之后开始找
find = G0 Y254 F3000      ← 找到这行
replace = G0 Y250 F3000   ← 替换为这行
max_count = 1             ← 最多替换 1 次
```

### M73 模板拆解（可选）

如果启用了"剩余时间板号"，用正则把 G-code 中所有 `M73 P0 R10` 拆成三段：

```
前缀 "M73 P0 R"  +  数字 10  +  后缀 "\n"
```

存为 `M73OffsetTemplate`，后面逐份加工时直接拼接不用再扫一遍正则。

---

## ④ 逐份加工

对展开后的每一份，依次做两件事：

### M73 偏移

第 i 份的 `M73 R` 值加上 `i × 6000` 分钟（= 100 小时）：

```
第1份: M73 P0 R6010      (+6000)
第2份: M73 P0 R12010     (+12000)
第3份: M73 P0 R18010     (+18000)
```

打印机显示剩余时间时，**百位数字正好是板号**。

### 插入换盘块

在 `;=====printer finish  sound=========` 这一行**之前**插入换盘代码块：

```gcode
M190 S45          ; 等热床降温到 45°C（可选）
[你选的换盘G-code]  ; 弹射/换板动作
G4 P45000         ; 等 45 秒（可选）
```

最后一份：
- 如果关了"最后换盘" → 不插换盘块
- 如果关了"最后换盘"但前面都插了 → 最后一份正常结束

---

## ⑤ 拼接总 G-code

把所有份已经加工好的 G-code 尾行 `\n` 拼成一个大字符串。

按你选的换行符编码：
- `crlf`（默认）→ `\n` 全部替换为 `\r\n`
- `lf` → 保持 `\n`

---

## ⑥ 写入新 3MF

`write_output_3mf()` 是最后一步。它不从头创建 ZIP——而是**以第一个源 3MF 为模板**，部分覆盖：

### 删除旧的

- 所有 `Metadata/plate_N.gcode`
- 所有 `Metadata/plate_N.gcode.md5`

### 写入新的

- 把拼接好的总 G-code 写到原来的 `plate_1.gcode` 成员名（或其 MD5 对应的成员）
- 同时写入 `plate_1.gcode.md5`（总 G-code 的 MD5 哈希）

### 更新元数据（默认：累加模式）

默认累加所有重复份的预测和耗材用量，修改 `Metadata/slice_info.config`：

| 字段 | 修改方式 |
|------|----------|
| prediction | 所有重复份的总和 |
| weight | 所有重复份的总和 |
| filament.used_m | 所有重复份的总长度 |
| filament.used_g | 所有重复份的总重量 |

如果选了"保留原始预测和重量"，此文件原样保留。

### 合成预览图（可选）

如果启用了预览：

1. 从最多 9 个**不同输入文件**中各取一张 `plate_N.png`（优先取活动面板的预览图）
2. 按网格拼接（1/2/4/6/9 宫格）
3. 在左下角用绿色粗体字盖一个标签，如 `5 P`

合成后的 PNG 覆盖 ZIP 中原来的 `plate_1.png` 和 `plate_1_small.png`。

如果源文件中没有预览图 → 保持原样，仍然尝试盖标签。

### 压缩

用 **zlib-ng** 替换标准库的 `zipfile.zlib`，按你选的级别（1-9，默认 7）做 ZIP Deflate 压缩。

### 原子写入

整个写入过程分两步：
1. 先写到**临时文件**（`temp.N.3mf`）
2. 写入成功后 `shutil.move` 到目标路径

防止构建中断导致输出文件损坏。

---

## ⑦ 返回

```python
BuildResult(
    output_3mf=Path(...),              # 输出文件路径
    plate_count=42,                     # 总板数
    total_prediction_seconds=36000.0,   # 总预估打印时间
    total_weight_grams=150.0,           # 总耗材重量
    gcode_md5="a1b2c3..."              # 总 G-code MD5
)
```

---

## 使用方式

核心构建函数只有一个：`build_packed_3mf(jobs, options)`，GUI 在调用它之前做不同的准备工作。

```mermaid
flowchart TB
    subgraph GUI["GUI 入口: run_gui.py → gui.py"]
        G1["拖放/添加文件 → 表格"] --> G2["每行: 3MF路径 + 份数"]
        G2 --> G3["控件值 → BuildOptions"]
        G3 --> G4{"批处理模式?"}
        G4 -- 合并 --> BUILD["build_packed_3mf()"]
        G4 -- 独立 --> G5["逐行调 build_packed_3mf()<br/>ProcessPoolExecutor 并行"]
    end
```

### GUI

窗口操作，参数靠控件选、文件靠拖放。最终也是调 `build_packed_3mf()`，准备工作包括：

#### GUI 独有的准备阶段

1. **文件管理** — 表格里每行一个源文件 + 份数，可拖放、排序、移除
2. **元数据预读** — 添加文件时就解析 `slice_info.config`，表格直接显示时间和耗材
3. **缩略图预览** — 选中行 3MF → 右侧显示 `plate_N.png` 缩略图
4. **输出路径自动算** — 默认留空，自动写到第一个输入文件旁边。文件名按规则 `{plates} Plates - {sources}.3mf` 生成
5. **设置持久化** — 控件值自动存 `settings.json`，重启恢复

#### 合并模式（默认）

所有行合并调一次 `build_packed_3mf()`：

```
A.3mf 份数 3
B.3mf 份数 2   →  build_packed_3mf([A,A,A,B,B])  →  5 Plates - A_and_1_more.3mf
```

#### 独立批处理模式

每行独立构建，多输出并行：

```
A.3mf 份数 3  → build_packed_3mf([A]) →  3 Plates - A.3mf
B.3mf 份数 2  → build_packed_3mf([B]) →  2 Plates - B.3mf      并行（ProcessPoolExecutor，最多 8 个进程）
C.3mf 份数 5  → build_packed_3mf([C]) →  5 Plates - C.3mf
```

构建前：
- 每行自动算各自的输出路径（文件名规则 + 去重防覆盖）
- 弹出进度日志，记录每个文件的构建结果
- 有关闭全局预览标签的选项
