feat(gui,cli): 界面全面中文化并优化打包选项为紧凑网格布局
- gui.py: 全部用户可见文本直接写中文,选项区从 QFormLayout 改为 QGridLayout 两列布局,缩小窗口默认高度至720px,收紧控件间距 - cli.py: argparse 帮助文本和输出消息全部中文化 - docs: 新增 A1 SwapMod 换板 G-code 内容分析文档,按三类策略(基础/双轮抖板/安全换板)对比5个模板差异 - swap_gcode: 新增 LX_0.01.01 基础弹板模板
This commit is contained in:
+30
-30
@@ -17,12 +17,12 @@ from .paths import default_patch_config_path, default_swap_gcode_dir
|
|||||||
|
|
||||||
def parse_item(values: list[str]) -> PlateJob:
|
def parse_item(values: list[str]) -> PlateJob:
|
||||||
if len(values) != 2:
|
if len(values) != 2:
|
||||||
raise argparse.ArgumentTypeError("Each --item needs a path and a copy count.")
|
raise argparse.ArgumentTypeError("每个 --item 需要一个路径和一个份数。")
|
||||||
path = Path(values[0])
|
path = Path(values[0])
|
||||||
try:
|
try:
|
||||||
copies = int(values[1])
|
copies = int(values[1])
|
||||||
except ValueError as exc:
|
except ValueError as exc:
|
||||||
raise argparse.ArgumentTypeError(f"Invalid copy count: {values[1]}") from exc
|
raise argparse.ArgumentTypeError(f"无效的份数:{values[1]}") from exc
|
||||||
return PlateJob(path, copies)
|
return PlateJob(path, copies)
|
||||||
|
|
||||||
|
|
||||||
@@ -33,7 +33,7 @@ def build_command(args: argparse.Namespace) -> int:
|
|||||||
for input_path in args.inputs or []:
|
for input_path in args.inputs or []:
|
||||||
jobs.append(PlateJob(Path(input_path), args.copies))
|
jobs.append(PlateJob(Path(input_path), args.copies))
|
||||||
if not jobs:
|
if not jobs:
|
||||||
raise SystemExit("No input 3MF file was provided.")
|
raise SystemExit("未提供输入 3MF 文件。")
|
||||||
cool_bed_temp = None if args.no_bed_cooldown else args.cool_bed
|
cool_bed_temp = None if args.no_bed_cooldown else args.cool_bed
|
||||||
options = BuildOptions(
|
options = BuildOptions(
|
||||||
swap_gcode=args.swap_gcode,
|
swap_gcode=args.swap_gcode,
|
||||||
@@ -50,13 +50,13 @@ def build_command(args: argparse.Namespace) -> int:
|
|||||||
zip_compress_level=args.zip_level,
|
zip_compress_level=args.zip_level,
|
||||||
)
|
)
|
||||||
result = build_packed_3mf(jobs, options)
|
result = build_packed_3mf(jobs, options)
|
||||||
print(f"Output: {result.output_3mf}")
|
print(f"输出:{result.output_3mf}")
|
||||||
print(f"Plates: {result.plate_count}")
|
print(f"板数:{result.plate_count}")
|
||||||
print(f"G-code MD5: {result.gcode_md5}")
|
print(f"G-code MD5:{result.gcode_md5}")
|
||||||
if result.total_prediction_seconds is not None:
|
if result.total_prediction_seconds is not None:
|
||||||
print(f"Source print time: {int(result.total_prediction_seconds)} seconds")
|
print(f"源文件打印时间:{int(result.total_prediction_seconds)} 秒")
|
||||||
if result.total_weight_grams is not None:
|
if result.total_weight_grams is not None:
|
||||||
print(f"Source filament weight: {result.total_weight_grams:.2f} g")
|
print(f"源文件耗材重量:{result.total_weight_grams:.2f} g")
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
@@ -64,7 +64,7 @@ def list_swap_gcode_command(args: argparse.Namespace) -> int:
|
|||||||
directory = Path(args.swap_gcode_dir) if args.swap_gcode_dir else default_swap_gcode_dir()
|
directory = Path(args.swap_gcode_dir) if args.swap_gcode_dir else default_swap_gcode_dir()
|
||||||
files = list_swap_gcode_files(directory)
|
files = list_swap_gcode_files(directory)
|
||||||
if not files:
|
if not files:
|
||||||
print(f"No swap G-code files found in: {directory}")
|
print(f"在 {directory} 中未找到换料 G-code 文件")
|
||||||
return 0
|
return 0
|
||||||
for path in files:
|
for path in files:
|
||||||
print(path.name)
|
print(path.name)
|
||||||
@@ -74,32 +74,32 @@ def list_swap_gcode_command(args: argparse.Namespace) -> int:
|
|||||||
def create_parser() -> argparse.ArgumentParser:
|
def create_parser() -> argparse.ArgumentParser:
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
prog="a1-swap-mod-packer",
|
prog="a1-swap-mod-packer",
|
||||||
description=f"{APP_TITLE} - Pack repeated Bambu A1 SwapMod plates into one 3MF job.",
|
description=f"{APP_TITLE} - 将重复的 Bambu A1 SwapMod 面板打包为一个 3MF 作业。",
|
||||||
)
|
)
|
||||||
parser.add_argument("--version", action="version", version=f"{APP_NAME} {__version__}")
|
parser.add_argument("--version", action="version", version=f"{APP_NAME} {__version__}")
|
||||||
subparsers = parser.add_subparsers(dest="command", required=True)
|
subparsers = parser.add_subparsers(dest="command", required=True)
|
||||||
|
|
||||||
build = subparsers.add_parser("build", help="Build a packed 3MF file.")
|
build = subparsers.add_parser("build", help="构建一个打包的 3MF 文件。")
|
||||||
build.add_argument("inputs", nargs="*", help="Input 3MF files. Use --copies for the same copy count on all of them.")
|
build.add_argument("inputs", nargs="*", help="输入 3MF 文件。使用 --copies 为所有文件设置相同的份数。")
|
||||||
build.add_argument("--item", nargs=2, action="append", metavar=("PATH", "COPIES"), help="Add an input 3MF with its own copy count. Can be used multiple times.")
|
build.add_argument("--item", nargs=2, action="append", metavar=("PATH", "COPIES"), help="添加一个带有独立份数的输入 3MF 文件。可重复使用。")
|
||||||
build.add_argument("-o", "--output", required=True, help="Output 3MF path.")
|
build.add_argument("-o", "--output", required=True, help="输出 3MF 路径。")
|
||||||
build.add_argument("--swap-gcode", required=True, help="Swap G-code file name in swap_gcode, or an explicit file path.")
|
build.add_argument("--swap-gcode", required=True, help="换料 G-code 文件名,位于 swap_gcode 中,或显式文件路径。")
|
||||||
build.add_argument("--swap-gcode-dir", default=None, help=f"Template directory. Default: {default_swap_gcode_dir()}")
|
build.add_argument("--swap-gcode-dir", default=None, help=f"模板目录。默认值:{default_swap_gcode_dir()}")
|
||||||
build.add_argument("--copies", type=int, default=1, help="Copy count for positional input files.")
|
build.add_argument("--copies", type=int, default=1, help="位置参数输入文件的复制份数。")
|
||||||
build.add_argument("--cool-bed", type=int, default=45, help="Bed temperature to wait for before running the swap G-code.")
|
build.add_argument("--cool-bed", type=int, default=45, help="执行换料 G-code 前等待的床温。")
|
||||||
build.add_argument("--no-bed-cooldown", action="store_true", help="Do not insert M190 before the swap code.")
|
build.add_argument("--no-bed-cooldown", action="store_true", help="不在换料代码前插入 M190。")
|
||||||
build.add_argument("--wait", type=int, default=45, help="Seconds to wait after plate ejection.")
|
build.add_argument("--wait", type=int, default=45, help="板弹射后等待的秒数。")
|
||||||
build.add_argument("--show-plate-number", action="store_true", help="Add 100 hours per plate number to M73 R values.")
|
build.add_argument("--show-plate-number", action="store_true", help="每板号向 M73 R 值增加 100 小时。")
|
||||||
build.add_argument("--no-swap-after-final", action="store_true", help="Do not run the swap G-code after the last plate.")
|
build.add_argument("--no-swap-after-final", action="store_true", help="最后一块板后不执行换料 G-code。")
|
||||||
build.add_argument("--metadata-mode", choices=("source", "sum"), default="source", help="How to write slice_info prediction and weight.")
|
build.add_argument("--metadata-mode", choices=("source", "sum"), default="source", help="如何写入 slice_info 的 prediction 和 weight。")
|
||||||
build.add_argument("--line-ending", choices=("lf", "crlf"), default="crlf", help="Line ending for the generated G-code.")
|
build.add_argument("--line-ending", choices=("lf", "crlf"), default="crlf", help="生成 G-code 的换行符。")
|
||||||
build.add_argument("--zip-level", type=int, choices=range(1, 10), default=DEFAULT_ZIP_COMPRESS_LEVEL, metavar="1-9", help="zlib-ng Deflate compression level for the output 3MF. Default: 7.")
|
build.add_argument("--zip-level", type=int, choices=range(1, 10), default=DEFAULT_ZIP_COMPRESS_LEVEL, metavar="1-9", help="输出 3MF 的 zlib-ng Deflate 压缩级别。默认值:7。")
|
||||||
build.add_argument("--no-preview-label", action="store_true", help="Do not rewrite the preview image label/composite.")
|
build.add_argument("--no-preview-label", action="store_true", help="不重写预览图标签/合成图。")
|
||||||
build.add_argument("--no-gcode-patches", action="store_true", help=f"Do not apply editable patches from {default_patch_config_path()}.")
|
build.add_argument("--no-gcode-patches", action="store_true", help=f"不应用来自 {default_patch_config_path()} 的可编辑补丁。")
|
||||||
build.set_defaults(func=build_command)
|
build.set_defaults(func=build_command)
|
||||||
|
|
||||||
list_cmd = subparsers.add_parser("list-swap-gcode", help="List files from the swap_gcode directory.")
|
list_cmd = subparsers.add_parser("list-swap-gcode", help="列出 swap_gcode 目录中的文件。")
|
||||||
list_cmd.add_argument("--swap-gcode-dir", default=None, help=f"Template directory. Default: {default_swap_gcode_dir()}")
|
list_cmd.add_argument("--swap-gcode-dir", default=None, help=f"模板目录。默认值:{default_swap_gcode_dir()}")
|
||||||
list_cmd.set_defaults(func=list_swap_gcode_command)
|
list_cmd.set_defaults(func=list_swap_gcode_command)
|
||||||
return parser
|
return parser
|
||||||
|
|
||||||
@@ -110,7 +110,7 @@ def main(argv: list[str] | None = None) -> int:
|
|||||||
try:
|
try:
|
||||||
return int(args.func(args))
|
return int(args.func(args))
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
print(f"Error: {exc}", file=sys.stderr)
|
print(f"错误:{exc}", file=sys.stderr)
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+134
-104
@@ -23,6 +23,7 @@ try:
|
|||||||
QComboBox,
|
QComboBox,
|
||||||
QFileDialog,
|
QFileDialog,
|
||||||
QFormLayout,
|
QFormLayout,
|
||||||
|
QGridLayout,
|
||||||
QGraphicsOpacityEffect,
|
QGraphicsOpacityEffect,
|
||||||
QGroupBox,
|
QGroupBox,
|
||||||
QHBoxLayout,
|
QHBoxLayout,
|
||||||
@@ -269,7 +270,7 @@ class MainWindow(QMainWindow):
|
|||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.setWindowTitle(APP_TITLE)
|
self.setWindowTitle(APP_TITLE)
|
||||||
self.resize(960, 860)
|
self.resize(960, 720)
|
||||||
self.setAcceptDrops(True)
|
self.setAcceptDrops(True)
|
||||||
self._updating_table = False
|
self._updating_table = False
|
||||||
self._loading_settings = True
|
self._loading_settings = True
|
||||||
@@ -301,16 +302,17 @@ class MainWindow(QMainWindow):
|
|||||||
def build_ui(self) -> None:
|
def build_ui(self) -> None:
|
||||||
central = QWidget(self)
|
central = QWidget(self)
|
||||||
root = QVBoxLayout(central)
|
root = QVBoxLayout(central)
|
||||||
|
root.setSpacing(6)
|
||||||
self.root_layout = root
|
self.root_layout = root
|
||||||
|
|
||||||
file_group = QGroupBox("Input 3MF files")
|
file_group = QGroupBox("输入 3MF 文件")
|
||||||
self.file_group = file_group
|
self.file_group = file_group
|
||||||
file_layout = QVBoxLayout(file_group)
|
file_layout = QVBoxLayout(file_group)
|
||||||
file_body = QHBoxLayout()
|
file_body = QHBoxLayout()
|
||||||
table_layout = QVBoxLayout()
|
table_layout = QVBoxLayout()
|
||||||
self.table = DropTableWidget(self.add_paths, self.remove_selected)
|
self.table = DropTableWidget(self.add_paths, self.remove_selected)
|
||||||
self.table.setColumnCount(5)
|
self.table.setColumnCount(5)
|
||||||
self.table.setHorizontalHeaderLabels(["Order", "3MF file", "Copies", "Time", "Filament"])
|
self.table.setHorizontalHeaderLabels(["顺序", "3MF 文件", "份数", "时间", "耗材"])
|
||||||
self.table.horizontalHeader().setSectionResizeMode(ORDER_COLUMN, QHeaderView.ResizeToContents)
|
self.table.horizontalHeader().setSectionResizeMode(ORDER_COLUMN, QHeaderView.ResizeToContents)
|
||||||
self.table.horizontalHeader().setSectionResizeMode(FILE_COLUMN, QHeaderView.Stretch)
|
self.table.horizontalHeader().setSectionResizeMode(FILE_COLUMN, QHeaderView.Stretch)
|
||||||
self.table.horizontalHeader().setSectionResizeMode(COPIES_COLUMN, QHeaderView.ResizeToContents)
|
self.table.horizontalHeader().setSectionResizeMode(COPIES_COLUMN, QHeaderView.ResizeToContents)
|
||||||
@@ -319,21 +321,21 @@ class MainWindow(QMainWindow):
|
|||||||
self.table.setColumnWidth(ORDER_COLUMN, 104)
|
self.table.setColumnWidth(ORDER_COLUMN, 104)
|
||||||
self.table.verticalHeader().setDefaultSectionSize(38)
|
self.table.verticalHeader().setDefaultSectionSize(38)
|
||||||
self.table.verticalHeader().hide()
|
self.table.verticalHeader().hide()
|
||||||
self.table.setMinimumHeight(220)
|
self.table.setMinimumHeight(180)
|
||||||
self.table.setSelectionBehavior(QTableWidget.SelectRows)
|
self.table.setSelectionBehavior(QTableWidget.SelectRows)
|
||||||
self.table.setAlternatingRowColors(True)
|
self.table.setAlternatingRowColors(True)
|
||||||
self.table.setToolTip("Drag .3mf files or folders here. Folders add all top-level .3mf files.")
|
self.table.setToolTip("将 .3mf 文件或文件夹拖放到此处。文件夹将添加所有顶层 .3mf 文件。")
|
||||||
self.table.itemChanged.connect(self.on_table_item_changed)
|
self.table.itemChanged.connect(self.on_table_item_changed)
|
||||||
self.table.itemSelectionChanged.connect(self.update_thumbnail_preview)
|
self.table.itemSelectionChanged.connect(self.update_thumbnail_preview)
|
||||||
table_layout.addWidget(self.table)
|
table_layout.addWidget(self.table)
|
||||||
|
|
||||||
self.total_summary_label = QLabel("Total: 0 plates | Time: Unknown | Filament: Unknown")
|
self.total_summary_label = QLabel("合计:0 板 | 时间:未知 | 耗材:未知")
|
||||||
table_layout.addWidget(self.total_summary_label)
|
table_layout.addWidget(self.total_summary_label)
|
||||||
file_body.addLayout(table_layout, 1)
|
file_body.addLayout(table_layout, 1)
|
||||||
|
|
||||||
preview_group = QGroupBox("Selected thumbnail")
|
preview_group = QGroupBox("选中文件的缩略图")
|
||||||
preview_layout = QVBoxLayout(preview_group)
|
preview_layout = QVBoxLayout(preview_group)
|
||||||
self.thumbnail_label = QLabel("Select an input file")
|
self.thumbnail_label = QLabel("选择一个输入文件")
|
||||||
self.thumbnail_label.setAlignment(Qt.AlignCenter)
|
self.thumbnail_label.setAlignment(Qt.AlignCenter)
|
||||||
self.thumbnail_label.setFixedSize(280, 220)
|
self.thumbnail_label.setFixedSize(280, 220)
|
||||||
self.thumbnail_label.setStyleSheet(
|
self.thumbnail_label.setStyleSheet(
|
||||||
@@ -355,12 +357,12 @@ class MainWindow(QMainWindow):
|
|||||||
file_layout.addLayout(file_body)
|
file_layout.addLayout(file_body)
|
||||||
|
|
||||||
file_buttons = QHBoxLayout()
|
file_buttons = QHBoxLayout()
|
||||||
add_button = QPushButton("Add 3MF")
|
add_button = QPushButton("添加 3MF")
|
||||||
remove_button = QPushButton("Remove")
|
remove_button = QPushButton("移除")
|
||||||
remove_all_button = QPushButton("Remove All")
|
remove_all_button = QPushButton("全部移除")
|
||||||
apply_default_copies_button = QPushButton("Apply Default Copies to Selected")
|
apply_default_copies_button = QPushButton("应用默认份数至选中行")
|
||||||
self.build_button = QPushButton("Build 3MF")
|
self.build_button = QPushButton("构建 3MF")
|
||||||
self.build_button.setMinimumHeight(64)
|
self.build_button.setMinimumHeight(56)
|
||||||
self.build_button.setMinimumWidth(180)
|
self.build_button.setMinimumWidth(180)
|
||||||
build_font = self.build_button.font()
|
build_font = self.build_button.font()
|
||||||
build_font.setPointSize(build_font.pointSize() + 2)
|
build_font.setPointSize(build_font.pointSize() + 2)
|
||||||
@@ -374,22 +376,27 @@ class MainWindow(QMainWindow):
|
|||||||
self.build_button.clicked.connect(self.build_output)
|
self.build_button.clicked.connect(self.build_output)
|
||||||
|
|
||||||
for button in (add_button, remove_button, remove_all_button, apply_default_copies_button):
|
for button in (add_button, remove_button, remove_all_button, apply_default_copies_button):
|
||||||
button.setMinimumHeight(44)
|
button.setMinimumHeight(38)
|
||||||
file_buttons.addWidget(button)
|
file_buttons.addWidget(button)
|
||||||
file_buttons.addStretch(1)
|
file_buttons.addStretch(1)
|
||||||
file_buttons.addWidget(self.build_button)
|
file_buttons.addWidget(self.build_button)
|
||||||
file_layout.addLayout(file_buttons)
|
file_layout.addLayout(file_buttons)
|
||||||
root.addWidget(file_group, 1)
|
root.addWidget(file_group, 1)
|
||||||
|
|
||||||
options_group = QGroupBox("Packing options")
|
options_group = QGroupBox("打包选项")
|
||||||
options_layout = QFormLayout(options_group)
|
grid = QGridLayout(options_group)
|
||||||
|
grid.setColumnStretch(1, 1)
|
||||||
|
grid.setColumnStretch(3, 1)
|
||||||
|
grid.setHorizontalSpacing(20)
|
||||||
|
grid.setVerticalSpacing(6)
|
||||||
|
|
||||||
|
# ---- 控件创建 ----
|
||||||
self.swap_gcode_combo = QComboBox()
|
self.swap_gcode_combo = QComboBox()
|
||||||
self.swap_gcode_combo.setMinimumWidth(260)
|
self.swap_gcode_combo.setMinimumWidth(260)
|
||||||
self.swap_gcode_combo.setMaximumWidth(440)
|
self.swap_gcode_combo.setMaximumWidth(440)
|
||||||
swap_gcode_row = QHBoxLayout()
|
swap_gcode_row = QHBoxLayout()
|
||||||
refresh_button = QPushButton("Refresh")
|
refresh_button = QPushButton("刷新")
|
||||||
open_folder_button = QPushButton("Open Folder")
|
open_folder_button = QPushButton("打开文件夹")
|
||||||
refresh_button.setFixedWidth(refresh_button.sizeHint().width())
|
refresh_button.setFixedWidth(refresh_button.sizeHint().width())
|
||||||
open_folder_button.setFixedWidth(open_folder_button.sizeHint().width())
|
open_folder_button.setFixedWidth(open_folder_button.sizeHint().width())
|
||||||
refresh_button.clicked.connect(self.load_swap_gcode_to_combo)
|
refresh_button.clicked.connect(self.load_swap_gcode_to_combo)
|
||||||
@@ -397,15 +404,13 @@ class MainWindow(QMainWindow):
|
|||||||
swap_gcode_row.addWidget(self.swap_gcode_combo, 1)
|
swap_gcode_row.addWidget(self.swap_gcode_combo, 1)
|
||||||
swap_gcode_row.addWidget(refresh_button)
|
swap_gcode_row.addWidget(refresh_button)
|
||||||
swap_gcode_row.addWidget(open_folder_button)
|
swap_gcode_row.addWidget(open_folder_button)
|
||||||
options_layout.addRow("Swap G-code", swap_gcode_row)
|
|
||||||
|
|
||||||
self.default_copies_spin = QSpinBox()
|
self.default_copies_spin = QSpinBox()
|
||||||
self.default_copies_spin.setRange(1, 9999)
|
self.default_copies_spin.setRange(1, 9999)
|
||||||
self.default_copies_spin.setValue(1)
|
self.default_copies_spin.setValue(1)
|
||||||
self.default_copies_spin.setFixedWidth(96)
|
self.default_copies_spin.setFixedWidth(96)
|
||||||
options_layout.addRow("Default copies for new inputs", self.default_copies_spin)
|
|
||||||
|
|
||||||
self.bed_cooldown_check = QCheckBox("Wait for bed cooldown")
|
self.bed_cooldown_check = QCheckBox("等待热床降温")
|
||||||
self.bed_cooldown_check.setChecked(True)
|
self.bed_cooldown_check.setChecked(True)
|
||||||
self.cool_bed_spin = QSpinBox()
|
self.cool_bed_spin = QSpinBox()
|
||||||
self.cool_bed_spin.setRange(0, 120)
|
self.cool_bed_spin.setRange(0, 120)
|
||||||
@@ -415,94 +420,120 @@ class MainWindow(QMainWindow):
|
|||||||
bed_row.addWidget(self.cool_bed_spin)
|
bed_row.addWidget(self.cool_bed_spin)
|
||||||
bed_row.addWidget(QLabel("°C"))
|
bed_row.addWidget(QLabel("°C"))
|
||||||
bed_row.addStretch(1)
|
bed_row.addStretch(1)
|
||||||
options_layout.addRow("Bed cooldown", bed_row)
|
|
||||||
|
|
||||||
self.wait_spin = QSpinBox()
|
self.wait_spin = QSpinBox()
|
||||||
self.wait_spin.setRange(0, 3600)
|
self.wait_spin.setRange(0, 3600)
|
||||||
self.wait_spin.setValue(45)
|
self.wait_spin.setValue(45)
|
||||||
self.wait_spin.setSuffix(" s")
|
self.wait_spin.setSuffix(" 秒")
|
||||||
self.wait_spin.setFixedWidth(96)
|
self.wait_spin.setFixedWidth(96)
|
||||||
options_layout.addRow("Wait after ejection", self.wait_spin)
|
|
||||||
|
|
||||||
self.show_plate_number_check = QCheckBox("Show current plate in the hundreds digit of remaining time")
|
self.show_plate_number_check = QCheckBox("在剩余时间的百位显示当前板号")
|
||||||
self.show_plate_number_check.setChecked(True)
|
self.show_plate_number_check.setChecked(True)
|
||||||
options_layout.addRow("Remaining-time plate number", self.show_plate_number_check)
|
|
||||||
|
|
||||||
self.swap_final_check = QCheckBox("Run swap G-code after the last plate")
|
self.swap_final_check = QCheckBox("最后一块板后也执行换料 G-code")
|
||||||
self.swap_final_check.setChecked(True)
|
self.swap_final_check.setChecked(True)
|
||||||
options_layout.addRow("Final swap", self.swap_final_check)
|
|
||||||
|
|
||||||
self.patch_check = QCheckBox("Apply editable G-code patches")
|
self.patch_check = QCheckBox("应用可编辑的 G-code 补丁")
|
||||||
self.patch_check.setToolTip("Uses gcode_patches.ini")
|
self.patch_check.setToolTip("使用 gcode_patches.ini")
|
||||||
self.patch_check.setChecked(True)
|
self.patch_check.setChecked(True)
|
||||||
patch_row = QHBoxLayout()
|
patch_row = QHBoxLayout()
|
||||||
open_patch_button = QPushButton("Open Config")
|
open_patch_button = QPushButton("打开配置文件")
|
||||||
open_patch_button.clicked.connect(self.open_patch_config)
|
open_patch_button.clicked.connect(self.open_patch_config)
|
||||||
patch_row.addWidget(self.patch_check)
|
patch_row.addWidget(self.patch_check)
|
||||||
patch_row.addWidget(open_patch_button)
|
patch_row.addWidget(open_patch_button)
|
||||||
patch_row.addStretch(1)
|
patch_row.addStretch(1)
|
||||||
options_layout.addRow("G-code patches", patch_row)
|
|
||||||
|
|
||||||
self.metadata_combo = QComboBox()
|
self.metadata_combo = QComboBox()
|
||||||
self.metadata_combo.addItem("Keep source prediction and weight", "source")
|
self.metadata_combo.addItem("保留原始预测和重量", "source")
|
||||||
self.metadata_combo.addItem("Sum prediction and filament", "sum")
|
self.metadata_combo.addItem("累加预测和耗材用量", "sum")
|
||||||
self.metadata_combo.setFixedWidth(260)
|
self.metadata_combo.setFixedWidth(260)
|
||||||
options_layout.addRow("3MF metadata", self.metadata_combo)
|
|
||||||
|
|
||||||
self.zip_level_combo = QComboBox()
|
self.zip_level_combo = QComboBox()
|
||||||
for level in range(1, 10):
|
for level in range(1, 10):
|
||||||
self.zip_level_combo.addItem(f"Level {level}", level)
|
self.zip_level_combo.addItem(f"级别 {level}", level)
|
||||||
self.zip_level_combo.setCurrentIndex(DEFAULT_ZIP_COMPRESS_LEVEL - 1)
|
self.zip_level_combo.setCurrentIndex(DEFAULT_ZIP_COMPRESS_LEVEL - 1)
|
||||||
self.zip_level_combo.setFixedWidth(120)
|
self.zip_level_combo.setFixedWidth(120)
|
||||||
self.zip_level_combo.setToolTip("zlib-ng Deflate compression level for the output 3MF.")
|
self.zip_level_combo.setToolTip("输出 3MF 的 zlib-ng Deflate 压缩级别。")
|
||||||
options_layout.addRow("ZIP compression", self.zip_level_combo)
|
|
||||||
|
|
||||||
self.individual_batch_check = QCheckBox("Individual batch mode")
|
self.individual_batch_check = QCheckBox("独立批处理模式")
|
||||||
self.individual_batch_check.setToolTip(
|
self.individual_batch_check.setToolTip(
|
||||||
"Build each input row as a separate output file, using that row's copy count."
|
"将每行输入构建为独立的输出文件,使用该行的份数。"
|
||||||
)
|
)
|
||||||
options_layout.addRow("Batch mode", self.individual_batch_check)
|
|
||||||
|
|
||||||
self.clear_after_build_check = QCheckBox("Clear input list after successful build")
|
self.clear_after_build_check = QCheckBox("构建成功后清空输入列表")
|
||||||
self.clear_after_build_check.setChecked(False)
|
self.clear_after_build_check.setChecked(False)
|
||||||
self.skip_duplicates_check = QCheckBox("Skip duplicate file paths when adding inputs")
|
self.skip_duplicates_check = QCheckBox("添加输入时跳过重复文件路径")
|
||||||
self.skip_duplicates_check.setChecked(True)
|
self.skip_duplicates_check.setChecked(True)
|
||||||
input_handling_row = QHBoxLayout()
|
input_handling_row = QHBoxLayout()
|
||||||
input_handling_row.addWidget(self.skip_duplicates_check)
|
input_handling_row.addWidget(self.skip_duplicates_check)
|
||||||
input_handling_row.addWidget(self.clear_after_build_check)
|
input_handling_row.addWidget(self.clear_after_build_check)
|
||||||
input_handling_row.addStretch(1)
|
input_handling_row.addStretch(1)
|
||||||
options_layout.addRow("Input handling", input_handling_row)
|
|
||||||
|
# ---- 网格布局 ----
|
||||||
|
# [换料 G-code | 跨全行]
|
||||||
|
grid.addWidget(QLabel("换料 G-code"), 0, 0)
|
||||||
|
grid.addLayout(swap_gcode_row, 0, 1, 1, 3)
|
||||||
|
|
||||||
|
# 默认份数 | 热床降温
|
||||||
|
grid.addWidget(QLabel("新输入的默认份数"), 1, 0)
|
||||||
|
grid.addWidget(self.default_copies_spin, 1, 1)
|
||||||
|
grid.addWidget(QLabel("热床降温"), 1, 2)
|
||||||
|
grid.addLayout(bed_row, 1, 3)
|
||||||
|
|
||||||
|
# 弹射后等待 | 剩余时间板号
|
||||||
|
grid.addWidget(QLabel("弹射后等待时间"), 2, 0)
|
||||||
|
grid.addWidget(self.wait_spin, 2, 1)
|
||||||
|
grid.addWidget(QLabel("剩余时间板号"), 2, 2)
|
||||||
|
grid.addWidget(self.show_plate_number_check, 2, 3)
|
||||||
|
|
||||||
|
# 最后换料 | G-code 补丁
|
||||||
|
grid.addWidget(QLabel("最后换料"), 3, 0)
|
||||||
|
grid.addWidget(self.swap_final_check, 3, 1)
|
||||||
|
grid.addWidget(QLabel("G-code 补丁"), 3, 2)
|
||||||
|
grid.addLayout(patch_row, 3, 3)
|
||||||
|
|
||||||
|
# 3MF 元数据 | ZIP 压缩
|
||||||
|
grid.addWidget(QLabel("3MF 元数据"), 4, 0)
|
||||||
|
grid.addWidget(self.metadata_combo, 4, 1)
|
||||||
|
grid.addWidget(QLabel("ZIP 压缩"), 4, 2)
|
||||||
|
grid.addWidget(self.zip_level_combo, 4, 3)
|
||||||
|
|
||||||
|
# 批处理模式 | 输入处理
|
||||||
|
grid.addWidget(QLabel("批处理模式"), 5, 0)
|
||||||
|
grid.addWidget(self.individual_batch_check, 5, 1)
|
||||||
|
grid.addWidget(QLabel("输入处理"), 5, 2)
|
||||||
|
grid.addLayout(input_handling_row, 5, 3)
|
||||||
root.addWidget(options_group)
|
root.addWidget(options_group)
|
||||||
|
|
||||||
output_group = QGroupBox("Output")
|
output_group = QGroupBox("输出")
|
||||||
output_layout = QFormLayout(output_group)
|
output_layout = QFormLayout(output_group)
|
||||||
output_dir_row = QHBoxLayout()
|
output_dir_row = QHBoxLayout()
|
||||||
self.output_dir_edit = QLineEdit()
|
self.output_dir_edit = QLineEdit()
|
||||||
self.output_dir_edit.setPlaceholderText("Leave empty to write next to the input file")
|
self.output_dir_edit.setPlaceholderText("留空则写入输入文件所在目录")
|
||||||
self.output_dir_edit.setMinimumWidth(280)
|
self.output_dir_edit.setMinimumWidth(280)
|
||||||
self.output_dir_edit.setMaximumWidth(560)
|
self.output_dir_edit.setMaximumWidth(560)
|
||||||
browse_output_dir_button = QPushButton("Browse")
|
browse_output_dir_button = QPushButton("浏览")
|
||||||
browse_output_dir_button.setFixedWidth(browse_output_dir_button.sizeHint().width())
|
browse_output_dir_button.setFixedWidth(browse_output_dir_button.sizeHint().width())
|
||||||
browse_output_dir_button.clicked.connect(self.choose_output_dir)
|
browse_output_dir_button.clicked.connect(self.choose_output_dir)
|
||||||
output_dir_row.addWidget(self.output_dir_edit, 1)
|
output_dir_row.addWidget(self.output_dir_edit, 1)
|
||||||
output_dir_row.addWidget(browse_output_dir_button)
|
output_dir_row.addWidget(browse_output_dir_button)
|
||||||
output_layout.addRow("Output directory", output_dir_row)
|
output_layout.addRow("输出目录", output_dir_row)
|
||||||
|
|
||||||
self.output_name_edit = QLineEdit(DEFAULT_OUTPUT_PATTERN)
|
self.output_name_edit = QLineEdit(DEFAULT_OUTPUT_PATTERN)
|
||||||
self.output_name_edit.setPlaceholderText("Use tokens such as {source}, {sources}, {plates}, {copies}, {date}, {time}")
|
self.output_name_edit.setPlaceholderText("可使用 {source}、{sources}、{plates}、{copies}、{date}、{time} 等标记")
|
||||||
self.output_name_edit.setMinimumWidth(280)
|
self.output_name_edit.setMinimumWidth(280)
|
||||||
self.output_name_edit.setMaximumWidth(560)
|
self.output_name_edit.setMaximumWidth(560)
|
||||||
filename_rule_row = QHBoxLayout()
|
filename_rule_row = QHBoxLayout()
|
||||||
output_rule_help_button = QPushButton("?")
|
output_rule_help_button = QPushButton("?")
|
||||||
output_rule_help_button.setFixedWidth(34)
|
output_rule_help_button.setFixedWidth(34)
|
||||||
output_rule_help_button.setToolTip("Show filename token help")
|
output_rule_help_button.setToolTip("显示文件名标记帮助")
|
||||||
output_rule_help_button.clicked.connect(self.show_output_rule_help)
|
output_rule_help_button.clicked.connect(self.show_output_rule_help)
|
||||||
filename_rule_row.addWidget(self.output_name_edit, 1)
|
filename_rule_row.addWidget(self.output_name_edit, 1)
|
||||||
filename_rule_row.addWidget(output_rule_help_button)
|
filename_rule_row.addWidget(output_rule_help_button)
|
||||||
output_layout.addRow("Output filename rule", filename_rule_row)
|
output_layout.addRow("输出文件名规则", filename_rule_row)
|
||||||
|
|
||||||
self.output_preview_label = QLabel("-")
|
self.output_preview_label = QLabel("-")
|
||||||
output_layout.addRow("Preview", self.output_preview_label)
|
output_layout.addRow("预览", self.output_preview_label)
|
||||||
root.addWidget(output_group)
|
root.addWidget(output_group)
|
||||||
|
|
||||||
self.log = QTextEdit()
|
self.log = QTextEdit()
|
||||||
@@ -619,30 +650,29 @@ class MainWindow(QMainWindow):
|
|||||||
if state != 0:
|
if state != 0:
|
||||||
QMessageBox.information(
|
QMessageBox.information(
|
||||||
self,
|
self,
|
||||||
"Individual batch mode",
|
"独立批处理模式",
|
||||||
"Individual batch mode treats every input row as a separate build.\n\n"
|
"独立批处理模式将每一行输入作为独立的构建任务。\n\n"
|
||||||
"Example: if you add 20 single-plate 3MF files and set copies to 5, "
|
"示例:如果你添加了 20 个单板 3MF 文件并设置份数为 5,"
|
||||||
"Build 3MF will create 20 separate packed files. Each output contains "
|
"点击“构建 3MF”将创建 20 个独立的打包文件。"
|
||||||
"only that source file repeated 5 times.\n\n"
|
"每个输出文件仅包含该源文件重复 5 次的内容。\n\n"
|
||||||
"This is useful for quickly batch-converting many independent 3MF jobs "
|
"这个功能适用于快速将大量独立的 3MF 作业批量转换为多份数的 "
|
||||||
"into multi-copy SwapMod packs. It does not combine all input files into one 3MF.",
|
"SwapMod 包。它不会将所有输入文件合并到一个 3MF 中。",
|
||||||
)
|
)
|
||||||
|
|
||||||
def show_output_rule_help(self) -> None:
|
def show_output_rule_help(self) -> None:
|
||||||
QMessageBox.information(
|
QMessageBox.information(
|
||||||
self,
|
self,
|
||||||
"Output filename rule",
|
"输出文件名规则",
|
||||||
"Available filename tokens:\n\n"
|
"可用的文件名标记:\n\n"
|
||||||
"{source} - First input file stem, without .3mf\n"
|
"{source} — 第一个输入文件的文件名(不含 .3mf)\n"
|
||||||
"{sources} - Source name summary. For multiple different inputs, it becomes "
|
"{sources} — 源文件名称摘要。多个不同输入时,显示为 first_source_and_N_more\n"
|
||||||
"first_source_and_N_more\n"
|
"{plates} — 输出中的总板数\n"
|
||||||
"{plates} - Total plate count in the output\n"
|
"{copies} — 输出中使用的总份数\n"
|
||||||
"{copies} - Total copy count used for the output\n"
|
"{date} — 当前日期,格式 YYYYMMDD\n"
|
||||||
"{date} - Current date as YYYYMMDD\n"
|
"{time} — 当前时间,格式 HHMMSS\n\n"
|
||||||
"{time} - Current time as HHMMSS\n\n"
|
"默认规则:\n"
|
||||||
"Default rule:\n"
|
|
||||||
"{plates} Plates - {sources}.3mf\n\n"
|
"{plates} Plates - {sources}.3mf\n\n"
|
||||||
"In individual batch mode, these tokens are calculated separately for each input row.",
|
"在独立批处理模式下,这些标记会针对每行输入单独计算。",
|
||||||
)
|
)
|
||||||
|
|
||||||
def load_swap_gcode_to_combo(self) -> None:
|
def load_swap_gcode_to_combo(self) -> None:
|
||||||
@@ -661,7 +691,7 @@ class MainWindow(QMainWindow):
|
|||||||
if index >= 0:
|
if index >= 0:
|
||||||
self.swap_gcode_combo.setCurrentIndex(index)
|
self.swap_gcode_combo.setCurrentIndex(index)
|
||||||
if not files:
|
if not files:
|
||||||
self.log.append(f"No swap G-code files found in {default_swap_gcode_dir()}")
|
self.log.append(f"在 {default_swap_gcode_dir()} 中未找到换料 G-code 文件")
|
||||||
|
|
||||||
def open_path(self, path: Path) -> None:
|
def open_path(self, path: Path) -> None:
|
||||||
target = path if path.exists() else path.parent
|
target = path if path.exists() else path.parent
|
||||||
@@ -681,12 +711,12 @@ class MainWindow(QMainWindow):
|
|||||||
def open_patch_config(self) -> None:
|
def open_patch_config(self) -> None:
|
||||||
path = default_patch_config_path()
|
path = default_patch_config_path()
|
||||||
if not path.exists():
|
if not path.exists():
|
||||||
QMessageBox.information(self, APP_NAME, f"Patch config does not exist yet:\n{path}")
|
QMessageBox.information(self, APP_NAME, f"补丁配置文件不存在:\n{path}")
|
||||||
return
|
return
|
||||||
self.open_path(path)
|
self.open_path(path)
|
||||||
|
|
||||||
def add_files(self) -> None:
|
def add_files(self) -> None:
|
||||||
files, _ = QFileDialog.getOpenFileNames(self, "Add 3MF files", "", "3MF files (*.3mf);;All files (*)")
|
files, _ = QFileDialog.getOpenFileNames(self, "添加 3MF 文件", "", "3MF 文件 (*.3mf);;所有文件 (*)")
|
||||||
self.add_paths([Path(file_name) for file_name in files])
|
self.add_paths([Path(file_name) for file_name in files])
|
||||||
|
|
||||||
def create_order_widget(self, row: int) -> QWidget:
|
def create_order_widget(self, row: int) -> QWidget:
|
||||||
@@ -701,7 +731,7 @@ class MainWindow(QMainWindow):
|
|||||||
|
|
||||||
up_button = QToolButton()
|
up_button = QToolButton()
|
||||||
up_button.setIcon(self.style().standardIcon(QStyle.StandardPixmap.SP_ArrowUp))
|
up_button.setIcon(self.style().standardIcon(QStyle.StandardPixmap.SP_ArrowUp))
|
||||||
up_button.setToolTip("Move up")
|
up_button.setToolTip("上移")
|
||||||
up_button.setAutoRaise(True)
|
up_button.setAutoRaise(True)
|
||||||
up_button.setFixedSize(28, 28)
|
up_button.setFixedSize(28, 28)
|
||||||
up_button.setEnabled(row > 0)
|
up_button.setEnabled(row > 0)
|
||||||
@@ -709,7 +739,7 @@ class MainWindow(QMainWindow):
|
|||||||
|
|
||||||
down_button = QToolButton()
|
down_button = QToolButton()
|
||||||
down_button.setIcon(self.style().standardIcon(QStyle.StandardPixmap.SP_ArrowDown))
|
down_button.setIcon(self.style().standardIcon(QStyle.StandardPixmap.SP_ArrowDown))
|
||||||
down_button.setToolTip("Move down")
|
down_button.setToolTip("下移")
|
||||||
down_button.setAutoRaise(True)
|
down_button.setAutoRaise(True)
|
||||||
down_button.setFixedSize(28, 28)
|
down_button.setFixedSize(28, 28)
|
||||||
down_button.setEnabled(row < self.table.rowCount() - 1)
|
down_button.setEnabled(row < self.table.rowCount() - 1)
|
||||||
@@ -763,18 +793,18 @@ class MainWindow(QMainWindow):
|
|||||||
path = self.row_path(row) if row is not None else None
|
path = self.row_path(row) if row is not None else None
|
||||||
self.thumbnail_label.clear()
|
self.thumbnail_label.clear()
|
||||||
if path is None:
|
if path is None:
|
||||||
self.thumbnail_label.setText("Select an input file")
|
self.thumbnail_label.setText("选择一个输入文件")
|
||||||
self.thumbnail_name_label.setText("")
|
self.thumbnail_name_label.setText("")
|
||||||
return
|
return
|
||||||
self.thumbnail_name_label.setText(path.name)
|
self.thumbnail_name_label.setText(path.name)
|
||||||
try:
|
try:
|
||||||
pixmap = self.load_thumbnail_pixmap(path)
|
pixmap = self.load_thumbnail_pixmap(path)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
self.thumbnail_label.setText("Preview unavailable")
|
self.thumbnail_label.setText("预览不可用")
|
||||||
self.thumbnail_label.setToolTip(str(exc))
|
self.thumbnail_label.setToolTip(str(exc))
|
||||||
return
|
return
|
||||||
if pixmap is None:
|
if pixmap is None:
|
||||||
self.thumbnail_label.setText("No preview image")
|
self.thumbnail_label.setText("无预览图")
|
||||||
self.thumbnail_label.setToolTip("")
|
self.thumbnail_label.setToolTip("")
|
||||||
return
|
return
|
||||||
scaled = pixmap.scaled(self.thumbnail_label.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation)
|
scaled = pixmap.scaled(self.thumbnail_label.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation)
|
||||||
@@ -808,7 +838,7 @@ class MainWindow(QMainWindow):
|
|||||||
else:
|
else:
|
||||||
skipped += 1
|
skipped += 1
|
||||||
if added or skipped:
|
if added or skipped:
|
||||||
self.log.append(f"Added {added} input file(s). Skipped {skipped}.")
|
self.log.append(f"已添加 {added} 个输入文件。跳过 {skipped} 个。")
|
||||||
self.update_total_summary()
|
self.update_total_summary()
|
||||||
self.update_output_preview()
|
self.update_output_preview()
|
||||||
|
|
||||||
@@ -817,7 +847,7 @@ class MainWindow(QMainWindow):
|
|||||||
try:
|
try:
|
||||||
summary = read_3mf_summary(path)
|
summary = read_3mf_summary(path)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
self.log.append(f"Skipped {path}: {exc}")
|
self.log.append(f"已跳过 {path}:{exc}")
|
||||||
return False
|
return False
|
||||||
row = self.table.rowCount()
|
row = self.table.rowCount()
|
||||||
self._updating_table = True
|
self._updating_table = True
|
||||||
@@ -860,9 +890,9 @@ class MainWindow(QMainWindow):
|
|||||||
|
|
||||||
def base_summary_tooltip(self, summary: ThreeMfSummary) -> str:
|
def base_summary_tooltip(self, summary: ThreeMfSummary) -> str:
|
||||||
return (
|
return (
|
||||||
f"Base plates: {summary.plate_count}\n"
|
f"基础板数:{summary.plate_count}\n"
|
||||||
f"Base time: {format_duration(summary.prediction_seconds)}\n"
|
f"基础时间:{format_duration(summary.prediction_seconds)}\n"
|
||||||
f"Base filament: {format_filament(summary.weight_grams, summary.filament_used_m)}"
|
f"基础耗材:{format_filament(summary.weight_grams, summary.filament_used_m)}"
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_row_copies(self, row: int) -> int:
|
def get_row_copies(self, row: int) -> int:
|
||||||
@@ -1007,7 +1037,7 @@ class MainWindow(QMainWindow):
|
|||||||
first_path = self.row_path(0) if self.table.rowCount() > 0 else None
|
first_path = self.row_path(0) if self.table.rowCount() > 0 else None
|
||||||
if not start_dir and first_path is not None:
|
if not start_dir and first_path is not None:
|
||||||
start_dir = str(first_path.parent)
|
start_dir = str(first_path.parent)
|
||||||
directory = QFileDialog.getExistingDirectory(self, "Choose output directory", start_dir or "")
|
directory = QFileDialog.getExistingDirectory(self, "选择输出目录", start_dir or "")
|
||||||
if directory:
|
if directory:
|
||||||
self.output_dir_edit.setText(directory)
|
self.output_dir_edit.setText(directory)
|
||||||
|
|
||||||
@@ -1045,10 +1075,10 @@ class MainWindow(QMainWindow):
|
|||||||
def update_total_summary(self) -> None:
|
def update_total_summary(self) -> None:
|
||||||
summary = self.current_total_summary()
|
summary = self.current_total_summary()
|
||||||
self.total_summary_label.setText(
|
self.total_summary_label.setText(
|
||||||
"Total: "
|
"合计:"
|
||||||
f"{summary.plate_count} plate(s) | "
|
f"{summary.plate_count} 板 | "
|
||||||
f"Time: {format_duration(summary.prediction_seconds)} | "
|
f"时间:{format_duration(summary.prediction_seconds)} | "
|
||||||
f"Filament: {format_filament(summary.weight_grams, summary.filament_used_m)}"
|
f"耗材:{format_filament(summary.weight_grams, summary.filament_used_m)}"
|
||||||
)
|
)
|
||||||
|
|
||||||
def update_output_preview(self) -> None:
|
def update_output_preview(self) -> None:
|
||||||
@@ -1060,7 +1090,7 @@ class MainWindow(QMainWindow):
|
|||||||
if self.individual_batch_check.isChecked():
|
if self.individual_batch_check.isChecked():
|
||||||
first_path = resolve_output_path([jobs[0]], self.output_naming_options(), self.summary_for_path)
|
first_path = resolve_output_path([jobs[0]], self.output_naming_options(), self.summary_for_path)
|
||||||
self.output_preview_label.setText(
|
self.output_preview_label.setText(
|
||||||
f"{first_path} | {len(jobs)} output file(s)"
|
f"{first_path} | {len(jobs)} 个输出文件"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
output_path = resolve_output_path(jobs, self.output_naming_options(), self.summary_for_path)
|
output_path = resolve_output_path(jobs, self.output_naming_options(), self.summary_for_path)
|
||||||
@@ -1071,7 +1101,7 @@ class MainWindow(QMainWindow):
|
|||||||
def build_options_for_output(self, output_path: Path) -> BuildOptions:
|
def build_options_for_output(self, output_path: Path) -> BuildOptions:
|
||||||
swap_gcode_path = self.swap_gcode_combo.currentData()
|
swap_gcode_path = self.swap_gcode_combo.currentData()
|
||||||
if not swap_gcode_path:
|
if not swap_gcode_path:
|
||||||
raise ValueError("Please put at least one swap G-code file in the swap_gcode folder and select it.")
|
raise ValueError("请在 swap_gcode 文件夹中放入至少一个换料 G-code 文件并选择它。")
|
||||||
cool_bed_temp = self.cool_bed_spin.value() if self.bed_cooldown_check.isChecked() else None
|
cool_bed_temp = self.cool_bed_spin.value() if self.bed_cooldown_check.isChecked() else None
|
||||||
return BuildOptions(
|
return BuildOptions(
|
||||||
swap_gcode=Path(swap_gcode_path),
|
swap_gcode=Path(swap_gcode_path),
|
||||||
@@ -1086,11 +1116,11 @@ class MainWindow(QMainWindow):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def log_build_result(self, result: Any) -> None:
|
def log_build_result(self, result: Any) -> None:
|
||||||
self.log.append(f"Output: {result.output_3mf}")
|
self.log.append(f"输出:{result.output_3mf}")
|
||||||
self.log.append(f"Plates: {result.plate_count}")
|
self.log.append(f"板数:{result.plate_count}")
|
||||||
self.log.append(f"Estimated source time: {format_duration(result.total_prediction_seconds)}")
|
self.log.append(f"预估源文件时间:{format_duration(result.total_prediction_seconds)}")
|
||||||
self.log.append(f"Estimated source filament: {format_filament(result.total_weight_grams)}")
|
self.log.append(f"预估源文件耗材:{format_filament(result.total_weight_grams)}")
|
||||||
self.log.append(f"G-code MD5: {result.gcode_md5}")
|
self.log.append(f"G-code MD5:{result.gcode_md5}")
|
||||||
|
|
||||||
def show_success_toast(self, message: str) -> None:
|
def show_success_toast(self, message: str) -> None:
|
||||||
self.success_toast.show_message(message)
|
self.success_toast.show_message(message)
|
||||||
@@ -1098,7 +1128,7 @@ class MainWindow(QMainWindow):
|
|||||||
def build_output(self) -> None:
|
def build_output(self) -> None:
|
||||||
jobs = self.collect_jobs()
|
jobs = self.collect_jobs()
|
||||||
if not jobs:
|
if not jobs:
|
||||||
QMessageBox.warning(self, APP_NAME, "Please add at least one 3MF file.")
|
QMessageBox.warning(self, APP_NAME, "请至少添加一个 3MF 文件。")
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
self.save_current_settings()
|
self.save_current_settings()
|
||||||
@@ -1108,7 +1138,7 @@ class MainWindow(QMainWindow):
|
|||||||
self.build_combined_output(jobs)
|
self.build_combined_output(jobs)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
QMessageBox.critical(self, APP_NAME, str(exc))
|
QMessageBox.critical(self, APP_NAME, str(exc))
|
||||||
self.log.append(f"Error: {exc}")
|
self.log.append(f"错误:{exc}")
|
||||||
return
|
return
|
||||||
|
|
||||||
def build_combined_output(self, jobs: list[PlateJob]) -> None:
|
def build_combined_output(self, jobs: list[PlateJob]) -> None:
|
||||||
@@ -1116,7 +1146,7 @@ class MainWindow(QMainWindow):
|
|||||||
options = self.build_options_for_output(output_path)
|
options = self.build_options_for_output(output_path)
|
||||||
result = build_packed_3mf(jobs, options)
|
result = build_packed_3mf(jobs, options)
|
||||||
self.log_build_result(result)
|
self.log_build_result(result)
|
||||||
self.show_success_toast("The packed 3MF file was created.")
|
self.show_success_toast("已创建打包的 3MF 文件。")
|
||||||
if self.clear_after_build_check.isChecked():
|
if self.clear_after_build_check.isChecked():
|
||||||
self.remove_all()
|
self.remove_all()
|
||||||
|
|
||||||
@@ -1134,31 +1164,31 @@ class MainWindow(QMainWindow):
|
|||||||
options = self.build_options_for_output(output_path)
|
options = self.build_options_for_output(output_path)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
errors.append(f"{job.source_3mf.name}: {exc}")
|
errors.append(f"{job.source_3mf.name}: {exc}")
|
||||||
self.log.append(f"Error building {job.source_3mf}: {exc}")
|
self.log.append(f"构建 {job.source_3mf} 时出错:{exc}")
|
||||||
continue
|
continue
|
||||||
tasks.append(IndividualBuildTask(job, options))
|
tasks.append(IndividualBuildTask(job, options))
|
||||||
|
|
||||||
if tasks:
|
if tasks:
|
||||||
worker_count = individual_batch_worker_count(len(tasks))
|
worker_count = individual_batch_worker_count(len(tasks))
|
||||||
self.log.append(f"Building {len(tasks)} individual output(s) with {worker_count} worker(s).")
|
self.log.append(f"正在使用 {worker_count} 个工作线程构建 {len(tasks)} 个独立输出。")
|
||||||
batch_result = run_individual_batch_builds(tasks, max_workers=worker_count)
|
batch_result = run_individual_batch_builds(tasks, max_workers=worker_count)
|
||||||
for result in batch_result.results:
|
for result in batch_result.results:
|
||||||
self.log_build_result(result)
|
self.log_build_result(result)
|
||||||
success_count = len(batch_result.results)
|
success_count = len(batch_result.results)
|
||||||
for failure in batch_result.failures:
|
for failure in batch_result.failures:
|
||||||
errors.append(f"{failure.job.source_3mf.name}: {failure.error}")
|
errors.append(f"{failure.job.source_3mf.name}: {failure.error}")
|
||||||
self.log.append(f"Error building {failure.job.source_3mf}: {failure.error}")
|
self.log.append(f"构建 {failure.job.source_3mf} 时出错:{failure.error}")
|
||||||
|
|
||||||
if errors:
|
if errors:
|
||||||
QMessageBox.warning(
|
QMessageBox.warning(
|
||||||
self,
|
self,
|
||||||
APP_NAME,
|
APP_NAME,
|
||||||
f"Built {success_count} file(s), but {len(errors)} file(s) failed.\n\n"
|
f"已构建 {success_count} 个文件,但有 {len(errors)} 个文件失败。\n\n"
|
||||||
+ "\n".join(errors[:8])
|
+ "\n".join(errors[:8])
|
||||||
+ ("\n..." if len(errors) > 8 else ""),
|
+ ("\n..." if len(errors) > 8 else ""),
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
self.show_success_toast(f"Created {success_count} packed 3MF file(s).")
|
self.show_success_toast(f"已创建 {success_count} 个打包的 3MF 文件。")
|
||||||
if self.clear_after_build_check.isChecked():
|
if self.clear_after_build_check.isChecked():
|
||||||
self.remove_all()
|
self.remove_all()
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,138 @@
|
|||||||
|
# A1 SwapMod 换板 G-code 分析
|
||||||
|
|
||||||
|
**版本**: v0.5.0 | **日期**: 2026-07-27
|
||||||
|
|
||||||
|
## 文件清单
|
||||||
|
|
||||||
|
共 5 个模板,按内容复杂度分为三类:
|
||||||
|
|
||||||
|
| 类型 | 包含文件 | 主要特征 |
|
||||||
|
|------|----------|----------|
|
||||||
|
| 类型 A — 基础弹射 | `LX_0.01.01.gcode` | G380 S3/S2 交替下压,单轮弹射,无 M400/M17 |
|
||||||
|
| 类型 B — 双轮抖板 | `LX_0.07.22.gcode` | G380 S3/S2 交替下压,双轮 Shake,无 M400/M17 |
|
||||||
|
| 类型 C — 安全换板 | `LX_1.02.26.gcode`<br>`LX_1.02.26_slow.gcode`<br>`LX_1.02.26_slow_safeZ.gcode` | G1 Z- + G380 S2 下压,M400/M17 保护,双轮 Shake |
|
||||||
|
|
||||||
|
## 类型 A — 基础弹板(LX_0.01.01)
|
||||||
|
|
||||||
|
**行数**: 35 行 | **复杂度**: 最低
|
||||||
|
|
||||||
|
```
|
||||||
|
Z 轴下压(G380 S3/S2 交替)→ Y 归零 → 推到 Y266
|
||||||
|
→ Z 轴轻压(G380 S2 Z20)→ 关闭软限位(M211 X0 Y0 Z0)
|
||||||
|
→ Y266→Y0 高速前推 → Z 轴回压 → 单次弹射 Y-2 F5000
|
||||||
|
→ 归位 Y150
|
||||||
|
```
|
||||||
|
|
||||||
|
**特点**:
|
||||||
|
- `M211 X0 Y0 Z0` 关闭软限位,允许超行程运动(其他版本均无此行)
|
||||||
|
- 仅单轮弹射,无 Shake 抖板机制
|
||||||
|
- 使用相对坐标 `G91` 大量操作
|
||||||
|
- 无 `M400`/`M17` 保护
|
||||||
|
- 换板结束后 Z 轴停留在低位
|
||||||
|
|
||||||
|
## 类型 B — 双轮抖板(LX_0.07.22)
|
||||||
|
|
||||||
|
**行数**: 100 行 | **复杂度**: 中等
|
||||||
|
|
||||||
|
```
|
||||||
|
G28 Y → Y262 → Z 轴下压(G380 S3/S2 ×4轮)
|
||||||
|
→ G4 P2000 → Y15→Y150→Y40 → G380 S2 Y264
|
||||||
|
→ Z 轴上升(G1 Z-55)→ Y120 → G28 Y → Y-1
|
||||||
|
→ 抖板第1轮(6段: 1→1.5→2→2.5→3→3.5mm 往复)
|
||||||
|
→ Y235→Y110→Y200 → G380 S2 Y264 → G4 P1000
|
||||||
|
→ Y40 → Y-2 F5000
|
||||||
|
→ 抖板第2轮(6段: 1→1.5→2→2.5→3→3.5mm 往复)
|
||||||
|
→ Y120 归位
|
||||||
|
```
|
||||||
|
|
||||||
|
**特点**:
|
||||||
|
- 首次引入双轮抖板,且抖板偏移量最大(最大 3.5mm)
|
||||||
|
- G380 S3/S2 交替多轮下压确保板贴合
|
||||||
|
- 无 `M400`/`M17`
|
||||||
|
|
||||||
|
## 类型 C — 安全换板(LX_1.02.26 / slow / slow_safeZ)
|
||||||
|
|
||||||
|
**行数**: 115~128 行 | **复杂度**: 最高
|
||||||
|
|
||||||
|
### C1 — 标准版(LX_1.02.26)
|
||||||
|
|
||||||
|
```
|
||||||
|
G28 Y → Y262 F2000 → M400 → M17 Z0.4
|
||||||
|
→ Z 轴下压(G1 Z- + G380 S2 纯力控,4轮)
|
||||||
|
→ G4 P2000 → Y15→Y150→Y40 → G380 S2 Y264
|
||||||
|
→ Z 轴上升(G1 Z-70)→ Y200 → Y25 → Y-1 F20000 → G28 Y
|
||||||
|
→ 抖板第1轮(5段: 1→1.5→2→2.5→2.5mm 往复)
|
||||||
|
→ Y235→Y110→Y200 → G380 S2 Y264 → G4 P1000
|
||||||
|
→ Y25 → Y-2 F20000
|
||||||
|
→ 抖板第2轮(5段: 1→1.5→2→2.5→2.5mm 往复)
|
||||||
|
→ Y262→Y120 归位
|
||||||
|
```
|
||||||
|
|
||||||
|
### C2 — 慢速版(LX_1.02.26_slow)
|
||||||
|
|
||||||
|
与 C1 完全相同,仅 **一处差异**:
|
||||||
|
- 第2轮弹射 `Y-2 F20000` → `Y-2 F1000`
|
||||||
|
|
||||||
|
### C3 — 慢速安全版(LX_1.02.26_slow_safeZ)
|
||||||
|
|
||||||
|
与 C2 完全相同,尾部追加 **Z 轴安全回位**:
|
||||||
|
```gcode
|
||||||
|
G4 P3000 ; 驻留 3 秒
|
||||||
|
M400
|
||||||
|
G90
|
||||||
|
G1 Z30 F600 ; Z 轴升至 30mm
|
||||||
|
M400
|
||||||
|
```
|
||||||
|
> 由 QQ 群用户 **风信子号** 提供
|
||||||
|
|
||||||
|
**特点**:
|
||||||
|
- 引入 `M400`(同步等待)+ `M17 Z0.4`(降低 Z 电机电流),防止碰撞损伤
|
||||||
|
- Z 轴下压从 G380 S3/S2 交替改为 G1 Z- + G380 S2 纯力控
|
||||||
|
- 抖板偏移收窄到 2.5mm(原 3.5mm)
|
||||||
|
- Y262 归位提速 F1200→F2000
|
||||||
|
|
||||||
|
## 三类对比总表
|
||||||
|
|
||||||
|
| 特性 | 类型 A | 类型 B | 类型 C1 | 类型 C2 | 类型 C3 |
|
||||||
|
|------|--------|--------|---------|---------|---------|
|
||||||
|
| **M400/M17** | ❌ | ❌ | ✅ | ✅ | ✅ |
|
||||||
|
| **M211 关限位** | ✅ | ❌ | ❌ | ❌ | ❌ |
|
||||||
|
| **Z 下压方式** | G380 S3/S2 | G380 S3/S2 | G1 Z-+G380 S2 | G1 Z-+G380 S2 | G1 Z-+G380 S2 |
|
||||||
|
| **抖板轮数** | 0 | 2 | 2 | 2 | 2 |
|
||||||
|
| **抖板最大偏移** | — | 3.5mm | 2.5mm | 2.5mm | 2.5mm |
|
||||||
|
| **Z 轴上升量** | — | -55mm | -70mm | -70mm | -70mm |
|
||||||
|
| **第2轮弹射速度** | F5000 | F5000 | F20000 | F1000 | F1000 |
|
||||||
|
| **Safe Z 回位** | ❌ | ❌ | ❌ | ❌ | ✅ Z30 |
|
||||||
|
| **总行数** | 35 | 100 | 115 | 115 | 128 |
|
||||||
|
|
||||||
|
## 换板原理
|
||||||
|
|
||||||
|
A1 SwapMod 利用 **Y 轴运动 + 热床摩擦力** 来弹射和更换打印板:
|
||||||
|
|
||||||
|
- **弹射** — Y 轴推到后端 → Z 轴下压使板贴合热床 → Y 轴骤推到前端 → 惯性 + 力传感器校准 → Z 轴上升留出空间 → Y 轴高速反弹把旧板弹下
|
||||||
|
- **抖板** — 在板脱落位置做微小 Y 轴往复,通过振动确保板完全脱离
|
||||||
|
- **推板** — `G380 S2 Y264` 力控推到最前端,为下一块板留出插入空间
|
||||||
|
|
||||||
|
## 关键 G-code 指令
|
||||||
|
|
||||||
|
| 指令 | 含义 |
|
||||||
|
|------|------|
|
||||||
|
| `G28 Y` | Y 轴归零(寻找限位开关) |
|
||||||
|
| `G380 S2 <目标>` | **A1 专有** — 力传感器目标位移动。以预设力矩正向推进 |
|
||||||
|
| `G380 S3 <目标>` | **A1 专有** — 力传感器碰撞退出。同向移动直到力反馈下降 |
|
||||||
|
| `M400` | 等待所有运动队列清空 |
|
||||||
|
| `M17 Z0.4` | Z 轴电机电流降至 0.4A,降低运动阻力 |
|
||||||
|
| `M211 X0 Y0 Z0` | 关闭软限位,允许超行程运动 |
|
||||||
|
| `G4 Pxxx` | 驻留 xxx 毫秒 |
|
||||||
|
| `G91` / `G90` | 相对坐标 / 绝对坐标切换 |
|
||||||
|
|
||||||
|
## 建议
|
||||||
|
|
||||||
|
推荐保留两个模板作为不同使用场景:
|
||||||
|
|
||||||
|
| 场景 | 推荐模板 | 原因 |
|
||||||
|
|------|----------|------|
|
||||||
|
| **日常推荐** | C3 — 慢速安全版 | 弹射温和、Z 轴安全回位,最不容易出问题 |
|
||||||
|
| **追求速度** | C1 — 标准版 | 弹射速度快、节拍最短 |
|
||||||
|
| **实验/备选** | A — 基础弹板 | 极简实现,适合调试 |
|
||||||
|
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
;========开始换盘 =================
|
||||||
|
G91;
|
||||||
|
G380 S3 Z-20 F1200
|
||||||
|
G380 S2 Z75 F1200
|
||||||
|
G380 S3 Z-20 F1200
|
||||||
|
G380 S2 Z75 F1200
|
||||||
|
G380 S3 Z-20 F1200
|
||||||
|
G380 S2 Z75 F1200
|
||||||
|
G380 S3 Z-20 F1200
|
||||||
|
G1 Z5 F1200
|
||||||
|
G90;
|
||||||
|
G28 Y;
|
||||||
|
G90;
|
||||||
|
G1 Y266 F2000;
|
||||||
|
G4 P500
|
||||||
|
G91;
|
||||||
|
G380 S2 Z20 F1200
|
||||||
|
G90;
|
||||||
|
M211 X0 Y0 Z0 ;
|
||||||
|
G90;
|
||||||
|
G1 Y266 F2000;
|
||||||
|
G1 Y0 F1000
|
||||||
|
G91;
|
||||||
|
G380 S3 Z-20 F1200
|
||||||
|
G90;
|
||||||
|
G1 Y200 F2000
|
||||||
|
G1 Y50 F2000
|
||||||
|
G1 Y266 F2000
|
||||||
|
G1 Y43 F2000
|
||||||
|
G1 Y262 F2000
|
||||||
|
G1 Y-2 F5000
|
||||||
|
G1 Y150 F2000
|
||||||
|
G91;
|
||||||
|
;=======换板结束====================
|
||||||
Reference in New Issue
Block a user