docs: 新增 A1 Swap Mod Packer 工作原理文档
从输入到输出的完整处理流水线:解压读取→展开→预处理(缓存)→ 逐份加工(M73偏移+插入换料块)→拼接→写入(元数据+预览+压缩+原子写入)
This commit is contained in:
@@ -0,0 +1,283 @@
|
||||
# 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) |
|
||||
| 脚本/自动化友好 | ✅ | ❌ |
|
||||
Reference in New Issue
Block a user