Files
A1SwapModPacker/docs/how_it_works.md
T
yw1573 b7a6f42189 docs: 新增 A1 Swap Mod Packer 工作原理文档
从输入到输出的完整处理流水线:解压读取→展开→预处理(缓存)→
逐份加工(M73偏移+插入换料块)→拼接→写入(元数据+预览+压缩+原子写入)
2026-07-28 09:20:18 +08:00

284 lines
8.5 KiB
Markdown
Raw 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.
# A1 Swap Mod Packer 工作原理
**版本**: v0.5.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 | 所有重复份的总重量 |
如果选了"保留原始预测和重量",此文件原样保留。
### 合成预览图(可选)
如果启用了预览(CLI 中默认开启):
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)`,CLI 和 GUI 会在调用它之前做不同的准备工作。
```mermaid
flowchart TB
subgraph CLI["CLI 入口: run_cli.py → cli.py"]
C1["argparse 解析参数"] --> C2["收集 --item / 位置参数"]
C2 --> C3["构造 BuildOptions"]
C3 --> BUILD["build_packed_3mf()"]
end
subgraph GUI["GUI 入口: run_gui.py → gui.py"]
G1["拖放/添加文件 → 表格"] --> G2["每行: 3MF路径 + 份数"]
G2 --> G3["控件值 → BuildOptions"]
G3 --> G4{"批处理模式?"}
G4 -- 合并 --> BUILD
G4 -- 独立 --> G5["逐行调 build_packed_3mf()<br/>ProcessPoolExecutor 并行"]
end
```
### CLI
一条命令搞定。指定输入文件、份数、输出路径和各项参数:
```bash
python -m a1_swap_mod_packer.cli build \
--item "A.3mf" 3 \
--swap-gcode "LX_1.02.26.gcode" \
--cool-bed 45 \
--wait 45 \
--show-plate-number \
-o "3 Plates - A.3mf"
```
CLI 做了这些:
1. `argparse` 解析参数(文件、份数、换料模板、冷却温度、等待秒数...)
2. 拼成 `PlateJob` 列表和 `BuildOptions`
3. 调一次 `build_packed_3mf()`(合并模式)
4. 把结果打印到 stdout
**所有输入合并成 1 个输出**。参数靠命令行传,没有交互、没有预览。
### GUI
窗口操作,参数靠控件选、文件靠拖放。最终也是调 `build_packed_3mf()`,但准备阶段和 CLI 不同:
#### GUI 独有的准备阶段
1. **文件管理** — 表格里每行一个源文件 + 份数,可拖放、排序、移除
2. **元数据预读** — 添加文件时就解析 `slice_info.config`,表格直接显示时间和耗材(CLI 不显示这个,只在构建后输出)
3. **缩略图预览** — 选中行 3MF → 右侧显示 `plate_N.png` 缩略图
4. **输出路径自动算** — 默认留空,自动写到第一个输入文件旁边。文件名按规则 `{plates} Plates - {sources}.3mf` 生成(CLI 必须手动用 `-o`
5. **设置持久化** — 控件值自动存 `settings.json`,重启恢复
#### 合并模式(默认)
和 CLI 一样,所有行合并调一次 `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
```
构建前:
- 每行自动算各自的输出路径(文件名规则 + 去重防覆盖)
- 弹出进度日志,记录每个文件的构建结果
- 有关闭全局预览标签的选项
#### GUI vs CLI 能力速查
| 能力 | CLI | GUI |
|------|:---:|:---:|
| 合并模式 | ✅ | ✅ |
| 独立批处理 | ❌ | ✅ |
| 拖放添加文件 | ❌ | ✅ |
| 表格编辑份数 | ❌ | ✅(每行 SpinBox |
| 元数据预显示 | ❌ | ✅(表格直接看时间/耗材) |
| 缩略图预览 | ❌ | ✅ |
| 输出路径自动生成 | ❌ | ✅(文件名规则) |
| 设置持久化 | ❌ | ✅(settings.json |
| 脚本/自动化友好 | ✅ | ❌ |