Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| addad17e92 | |||
| c43faeb5a2 | |||
| d362b2280a | |||
| 53a25544fa | |||
| 40c339dd74 | |||
| 7c6a9f65ef | |||
| 703929eea5 | |||
| bcff3ae264 |
+117
-148
@@ -27,17 +27,17 @@ if sys.platform.startswith("win") and "QT_QPA_PLATFORM" not in os.environ:
|
||||
os.environ["QT_QPA_PLATFORM"] = "windows:fontengine=freetype"
|
||||
|
||||
try:
|
||||
from PySide6.QtCore import QEasingCurve, QEvent, QPropertyAnimation, Qt, QTimer, QUrl
|
||||
from PySide6.QtCore import QEvent, Qt, QUrl
|
||||
from PySide6.QtGui import QDragEnterEvent, QDragMoveEvent, QDropEvent, QFont, QIcon, QIntValidator, QKeyEvent, QPixmap
|
||||
from PySide6.QtWidgets import (
|
||||
QApplication,
|
||||
QAbstractItemView,
|
||||
QCheckBox,
|
||||
QComboBox,
|
||||
QDialog,
|
||||
QFileDialog,
|
||||
QFormLayout,
|
||||
QGridLayout,
|
||||
QGraphicsOpacityEffect,
|
||||
QGroupBox,
|
||||
QHBoxLayout,
|
||||
QHeaderView,
|
||||
@@ -132,103 +132,6 @@ def first_preview_image_member(member_names: list[str], gcode_member: str | None
|
||||
return sorted(candidates, key=preview_image_sort_key)[0]
|
||||
|
||||
|
||||
class SuccessToast(QLabel):
|
||||
"""构建成功时的浮动提示条组件。
|
||||
|
||||
提供淡入-停留-淡出动画效果,自动居中于父窗口底部。
|
||||
不会拦截鼠标事件,点击可穿透到下层控件。
|
||||
"""
|
||||
|
||||
FADE_MS = 1000 # 淡入/淡出动画时长(毫秒)
|
||||
HOLD_MS = 5000 # 停留显示时长(毫秒)
|
||||
|
||||
def __init__(self, parent: QWidget) -> None:
|
||||
"""初始化浮动提示条,创建透明度动画和停留计时器。"""
|
||||
super().__init__(parent)
|
||||
self.setAlignment(Qt.AlignCenter)
|
||||
self.setWordWrap(True)
|
||||
self.setAttribute(Qt.WA_TransparentForMouseEvents)
|
||||
self.setStyleSheet(
|
||||
"""
|
||||
QLabel {
|
||||
background: #f4fff6;
|
||||
color: #183f22;
|
||||
border: 3px solid #2e7d32;
|
||||
border-radius: 8px;
|
||||
padding: 14px 24px;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
}
|
||||
"""
|
||||
)
|
||||
self._opacity_effect = QGraphicsOpacityEffect(self)
|
||||
self.setGraphicsEffect(self._opacity_effect)
|
||||
# 淡入动画
|
||||
self._fade_in = QPropertyAnimation(self._opacity_effect, b"opacity", self)
|
||||
self._fade_in.setDuration(self.FADE_MS)
|
||||
self._fade_in.setEasingCurve(QEasingCurve.InOutQuad)
|
||||
# 淡出动画
|
||||
self._fade_out = QPropertyAnimation(self._opacity_effect, b"opacity", self)
|
||||
self._fade_out.setDuration(self.FADE_MS)
|
||||
self._fade_out.setEasingCurve(QEasingCurve.InOutQuad)
|
||||
self._fade_out.finished.connect(self.hide)
|
||||
# 停留计时器
|
||||
self._hold_timer = QTimer(self)
|
||||
self._hold_timer.setSingleShot(True)
|
||||
self._hold_timer.timeout.connect(self._start_fade_out)
|
||||
# 监听父窗口大小变化,自动重新定位
|
||||
parent.installEventFilter(self)
|
||||
self.hide()
|
||||
|
||||
def show_message(self, message: str) -> None:
|
||||
"""显示指定消息文本,启动淡入-停留-淡出动画序列。"""
|
||||
self._hold_timer.stop()
|
||||
self._fade_in.stop()
|
||||
self._fade_out.stop()
|
||||
self.setText(message)
|
||||
self._fit_to_parent()
|
||||
self._position()
|
||||
self._opacity_effect.setOpacity(0.0)
|
||||
self.show()
|
||||
self.raise_()
|
||||
self._fade_in.setStartValue(0.0)
|
||||
self._fade_in.setEndValue(1.0)
|
||||
self._fade_in.start()
|
||||
self._hold_timer.start(self.FADE_MS + self.HOLD_MS)
|
||||
|
||||
def eventFilter(self, watched: object, event: QEvent) -> bool:
|
||||
"""监听父窗口 Resize 事件,自动调整提示条尺寸和位置。"""
|
||||
if watched is self.parentWidget() and event.type() == QEvent.Resize and self.isVisible():
|
||||
self._fit_to_parent()
|
||||
self._position()
|
||||
return super().eventFilter(watched, event)
|
||||
|
||||
def _fit_to_parent(self) -> None:
|
||||
"""根据父窗口宽度自适应提示条的最大宽度。"""
|
||||
parent = self.parentWidget()
|
||||
if parent is None:
|
||||
return
|
||||
self.setMaximumWidth(max(360, min(720, parent.width() - 48)))
|
||||
self.adjustSize()
|
||||
|
||||
def _position(self) -> None:
|
||||
"""将提示条定位到父窗口底部居中位置。"""
|
||||
parent = self.parentWidget()
|
||||
if parent is None:
|
||||
return
|
||||
margin = 24
|
||||
x = max(margin, (parent.width() - self.width()) // 2)
|
||||
y = max(margin, parent.height() - self.height() - margin)
|
||||
self.move(x, y)
|
||||
|
||||
def _start_fade_out(self) -> None:
|
||||
"""启动淡出动画,从当前透明度渐变至完全透明。"""
|
||||
self._fade_out.stop()
|
||||
self._fade_out.setStartValue(self._opacity_effect.opacity())
|
||||
self._fade_out.setEndValue(0.0)
|
||||
self._fade_out.start()
|
||||
|
||||
|
||||
class DropTableWidget(QTableWidget):
|
||||
"""支持拖放 .3mf 文件的表格控件。
|
||||
|
||||
@@ -345,7 +248,6 @@ class MainWindow(QMainWindow):
|
||||
self._updating_table = False # 防重入标记:表格正在批量更新中
|
||||
self._loading_settings = True # 防重入标记:正在从设置恢复 UI 状态
|
||||
self._settings = self.load_settings()
|
||||
self._shared_growth_enabled = False # 窗口高度足够时,文件区和日志区共享扩展空间
|
||||
self.build_ui()
|
||||
self.load_swap_gcode_to_combo()
|
||||
self.restore_settings_to_ui()
|
||||
@@ -486,9 +388,11 @@ class MainWindow(QMainWindow):
|
||||
self.remove_button.clicked.connect(self.remove_selected)
|
||||
self.remove_all_button.clicked.connect(self.remove_all)
|
||||
self.apply_default_copies_button.clicked.connect(self.apply_default_copies_to_selected)
|
||||
self.repack_3mf_button = QPushButton(t("gui.options.repack_3mf"))
|
||||
self.repack_3mf_button.clicked.connect(self.repack_3mf_folder)
|
||||
self.build_button.clicked.connect(self.build_output)
|
||||
|
||||
for button in (self.add_button, self.remove_button, self.remove_all_button, self.apply_default_copies_button):
|
||||
for button in (self.add_button, self.remove_button, self.remove_all_button, self.apply_default_copies_button, self.repack_3mf_button):
|
||||
button.setMinimumHeight(32)
|
||||
file_buttons.addWidget(button)
|
||||
file_buttons.addStretch(1)
|
||||
@@ -512,10 +416,13 @@ class MainWindow(QMainWindow):
|
||||
self.swap_gcode_combo.setMinimumWidth(240)
|
||||
self.swap_gcode_combo.setMaximumWidth(440)
|
||||
self.refresh_button = QPushButton(t("gui.options.refresh"))
|
||||
self.view_gcode_button = QPushButton(t("gui.options.view_gcode"))
|
||||
self.open_folder_button = QPushButton(t("gui.options.open_folder"))
|
||||
self.refresh_button.setFixedWidth(self.refresh_button.sizeHint().width())
|
||||
self.view_gcode_button.setFixedWidth(self.view_gcode_button.sizeHint().width())
|
||||
self.open_folder_button.setFixedWidth(self.open_folder_button.sizeHint().width())
|
||||
self.refresh_button.clicked.connect(self.load_swap_gcode_to_combo)
|
||||
self.view_gcode_button.clicked.connect(self.view_swap_gcode)
|
||||
self.open_folder_button.clicked.connect(self.open_swap_gcode_folder)
|
||||
|
||||
# 默认份数
|
||||
@@ -619,6 +526,7 @@ class MainWindow(QMainWindow):
|
||||
grid.addWidget(self.swap_gcode_combo, 0, 1, 1, 2)
|
||||
swap_btn_row = QHBoxLayout()
|
||||
swap_btn_row.addWidget(self.refresh_button)
|
||||
swap_btn_row.addWidget(self.view_gcode_button)
|
||||
swap_btn_row.addWidget(self.open_folder_button)
|
||||
swap_btn_row.addStretch(1)
|
||||
grid.addLayout(swap_btn_row, 0, 3)
|
||||
@@ -690,36 +598,15 @@ class MainWindow(QMainWindow):
|
||||
|
||||
root.addWidget(self.output_group)
|
||||
|
||||
# =====================================================================
|
||||
# --- 日志区 ---
|
||||
# =====================================================================
|
||||
self.log = QTextEdit()
|
||||
self.log.setReadOnly(True)
|
||||
self.log.setMinimumHeight(80)
|
||||
root.addWidget(self.log, 0)
|
||||
|
||||
self.setCentralWidget(central)
|
||||
self.success_toast = SuccessToast(central)
|
||||
self.update_vertical_growth_policy()
|
||||
|
||||
# =========================================================================
|
||||
# 窗口事件
|
||||
# =========================================================================
|
||||
|
||||
def update_vertical_growth_policy(self) -> None:
|
||||
"""根据窗口高度动态调整文件区和日志区的垂直扩展比例。
|
||||
|
||||
窗口高度 >= 980px 时,文件区和日志区按 4:1 共享扩展空间;
|
||||
否则文件区独占扩展空间。
|
||||
"""
|
||||
if not hasattr(self, "root_layout") or not hasattr(self, "file_group") or not hasattr(self, "log"):
|
||||
return
|
||||
shared_growth = self.height() >= 980
|
||||
if shared_growth == self._shared_growth_enabled:
|
||||
return
|
||||
self._shared_growth_enabled = shared_growth
|
||||
self.root_layout.setStretchFactor(self.file_group, 4 if shared_growth else 1)
|
||||
self.root_layout.setStretchFactor(self.log, 1 if shared_growth else 0)
|
||||
"""已废弃——底部日志区已移除。"""
|
||||
pass
|
||||
|
||||
def resizeEvent(self, event: QEvent) -> None:
|
||||
"""窗口大小变化时,更新垂直扩展策略并刷新缩略图。"""
|
||||
@@ -869,8 +756,6 @@ class MainWindow(QMainWindow):
|
||||
index = self.swap_gcode_combo.findText(Path(str(current)).name)
|
||||
if index >= 0:
|
||||
self.swap_gcode_combo.setCurrentIndex(index)
|
||||
if not files:
|
||||
self.log.append(t("gui.log.no_swap_files", dir=str(default_swap_gcode_dir())))
|
||||
|
||||
def open_path(self, path: Path) -> None:
|
||||
"""跨平台打开文件或文件夹(Windows/Mac/Linux)。"""
|
||||
@@ -889,6 +774,35 @@ class MainWindow(QMainWindow):
|
||||
folder.mkdir(parents=True, exist_ok=True)
|
||||
self.open_path(folder)
|
||||
|
||||
def view_swap_gcode(self) -> None:
|
||||
"""在弹出的只读窗口中展示当前选中的换盘 G-code 内容。"""
|
||||
gcode_path = self.swap_gcode_combo.currentData()
|
||||
if not gcode_path:
|
||||
return
|
||||
try:
|
||||
text = Path(gcode_path).read_text(encoding="utf-8-sig", errors="replace")
|
||||
except OSError as exc:
|
||||
QMessageBox.warning(self, t("gui.dialog.warning.title"), str(exc))
|
||||
return
|
||||
|
||||
dialog = QDialog(self)
|
||||
dialog.setWindowTitle(t("gui.dialog.view_gcode.title") + " - " + Path(gcode_path).name)
|
||||
dialog.resize(700, 480)
|
||||
layout = QVBoxLayout(dialog)
|
||||
editor = QTextEdit()
|
||||
editor.setReadOnly(True)
|
||||
font = QFont("Consolas", 10)
|
||||
editor.setFont(font)
|
||||
editor.setText(text)
|
||||
layout.addWidget(editor)
|
||||
close_btn = QPushButton(t("gui.button.close"))
|
||||
close_btn.clicked.connect(dialog.close)
|
||||
btn_row = QHBoxLayout()
|
||||
btn_row.addStretch(1)
|
||||
btn_row.addWidget(close_btn)
|
||||
layout.addLayout(btn_row)
|
||||
dialog.exec()
|
||||
|
||||
def open_patch_config(self) -> None:
|
||||
"""打开 G-code 补丁配置文件。"""
|
||||
path = default_patch_config_path()
|
||||
@@ -897,6 +811,57 @@ class MainWindow(QMainWindow):
|
||||
return
|
||||
self.open_path(path)
|
||||
|
||||
def repack_3mf_folder(self) -> None:
|
||||
"""选择解压后的3MF文件夹,自动更新MD5并重新打包为.3mf"""
|
||||
import hashlib
|
||||
import re
|
||||
import os
|
||||
import zipfile
|
||||
|
||||
folder = QFileDialog.getExistingDirectory(self, t("gui.dialog.repack_select_folder"))
|
||||
if not folder:
|
||||
return
|
||||
|
||||
src_dir = Path(folder)
|
||||
ct_path = src_dir / "[Content_Types].xml"
|
||||
if not ct_path.exists():
|
||||
QMessageBox.warning(self, t("gui.dialog.warning.title"), t("gui.dialog.repack_no_content_types"))
|
||||
return
|
||||
|
||||
try:
|
||||
# 更新所有 plate_N.gcode.md5
|
||||
gcode_re = re.compile(r"^(.+[/\\])?Metadata[/\\]plate_(\d+)\.gcode$")
|
||||
metadata_dir = src_dir / "Metadata"
|
||||
if metadata_dir.is_dir():
|
||||
for gcode_file in sorted(metadata_dir.glob("plate_*.gcode")):
|
||||
if not gcode_re.match(str(gcode_file)):
|
||||
continue
|
||||
h = hashlib.md5()
|
||||
with open(gcode_file, "rb") as f:
|
||||
h.update(f.read())
|
||||
md5_path = Path(str(gcode_file) + ".md5")
|
||||
md5_path.write_text(h.hexdigest(), encoding="ascii")
|
||||
|
||||
# 打包:[Content_Types].xml 必须是第一个条目
|
||||
output = src_dir.parent / (src_dir.name.rstrip("/\\") + ".3mf")
|
||||
with zipfile.ZipFile(output, "w", zipfile.ZIP_DEFLATED) as z:
|
||||
z.write(ct_path, "[Content_Types].xml")
|
||||
for root, _dirs, files in os.walk(str(src_dir)):
|
||||
for fn in files:
|
||||
fp = Path(root) / fn
|
||||
arcname = fp.relative_to(src_dir).as_posix()
|
||||
if arcname == "[Content_Types].xml":
|
||||
continue
|
||||
z.write(str(fp), arcname)
|
||||
|
||||
QMessageBox.information(
|
||||
self,
|
||||
t("gui.dialog.repack_success.title"),
|
||||
t("gui.dialog.repack_success.text", path=str(output)),
|
||||
)
|
||||
except Exception as exc:
|
||||
QMessageBox.critical(self, t("gui.dialog.warning.title"), str(exc))
|
||||
|
||||
# =========================================================================
|
||||
# 文件输入管理(表格行操作)
|
||||
# =========================================================================
|
||||
@@ -1034,8 +999,6 @@ class MainWindow(QMainWindow):
|
||||
added += 1
|
||||
else:
|
||||
skipped += 1
|
||||
if added or skipped:
|
||||
self.log.append(t("gui.log.added_files", added=added, skipped=skipped))
|
||||
self.update_total_summary()
|
||||
self.update_output_preview()
|
||||
|
||||
@@ -1045,7 +1008,7 @@ class MainWindow(QMainWindow):
|
||||
try:
|
||||
summary = read_3mf_summary(path)
|
||||
except Exception as exc:
|
||||
self.log.append(t("gui.log.skipped_file", path=str(path), reason=str(exc)))
|
||||
QMessageBox.warning(self, t("gui.dialog.warning.title"), t("gui.log.skipped_file", path=path.name, reason=str(exc)))
|
||||
return False
|
||||
row = self.table.rowCount()
|
||||
self._updating_table = True
|
||||
@@ -1348,17 +1311,19 @@ class MainWindow(QMainWindow):
|
||||
zip_compress_level=int(self.zip_level_combo.currentData() or DEFAULT_ZIP_COMPRESS_LEVEL),
|
||||
)
|
||||
|
||||
def log_build_result(self, result: Any) -> None:
|
||||
"""将单次构建结果信息写入日志。"""
|
||||
self.log.append(t("gui.log.output", path=str(result.output_3mf)))
|
||||
self.log.append(t("gui.log.plates", count=result.plate_count))
|
||||
self.log.append(t("gui.log.time", time=format_duration(result.total_prediction_seconds)))
|
||||
self.log.append(t("gui.log.filament", filament=format_filament(result.total_weight_grams)))
|
||||
self.log.append(t("gui.log.md5", md5=result.gcode_md5))
|
||||
def _show_build_result_dialog(self, title: str, lines: list[str]) -> None:
|
||||
"""在弹出的对话框中展示构建结果信息。"""
|
||||
QMessageBox.information(self, title, "\n".join(lines))
|
||||
|
||||
def show_success_toast(self, message: str) -> None:
|
||||
"""显示构建成功的浮动提示条。"""
|
||||
self.success_toast.show_message(message)
|
||||
def log_build_result(self, result: Any) -> list[str]:
|
||||
"""返回单次构建结果的文本行列表。"""
|
||||
return [
|
||||
t("gui.log.output", path=str(result.output_3mf)),
|
||||
t("gui.log.plates", count=result.plate_count),
|
||||
t("gui.log.time", time=format_duration(result.total_prediction_seconds)),
|
||||
t("gui.log.filament", filament=format_filament(result.total_weight_grams)),
|
||||
t("gui.log.md5", md5=result.gcode_md5),
|
||||
]
|
||||
|
||||
def build_output(self) -> None:
|
||||
"""主构建入口:根据批处理模式分发到合并构建或独立构建。"""
|
||||
@@ -1374,7 +1339,6 @@ class MainWindow(QMainWindow):
|
||||
self.build_combined_output(jobs)
|
||||
except Exception as exc:
|
||||
QMessageBox.critical(self, t("gui.dialog.warning.title"), str(exc))
|
||||
self.log.append(t("gui.log.build_error", path=type(exc).__name__, error=str(exc)))
|
||||
return
|
||||
|
||||
def build_combined_output(self, jobs: list[PlateJob]) -> None:
|
||||
@@ -1382,15 +1346,13 @@ class MainWindow(QMainWindow):
|
||||
output_path = resolve_output_path(jobs, self.output_naming_options(), self.summary_for_path)
|
||||
options = self.build_options_for_output(output_path)
|
||||
result = build_packed_3mf(jobs, options)
|
||||
self.log_build_result(result)
|
||||
self.show_success_toast(t("gui.toast.combined_success"))
|
||||
self._show_build_result_dialog(t("gui.dialog.build_result.title"), self.log_build_result(result))
|
||||
if self.clear_after_build_check.isChecked():
|
||||
self.remove_all()
|
||||
|
||||
def build_individual_outputs(self, jobs: list[PlateJob]) -> None:
|
||||
"""独立批处理模式:每行输入构建一个独立的输出 3MF,支持并行处理。"""
|
||||
used_paths: set[Path] = set()
|
||||
success_count = 0
|
||||
errors: list[str] = []
|
||||
tasks: list[IndividualBuildTask] = []
|
||||
for job in jobs:
|
||||
@@ -1402,20 +1364,21 @@ class MainWindow(QMainWindow):
|
||||
options = self.build_options_for_output(output_path)
|
||||
except Exception as exc:
|
||||
errors.append(f"{job.source_3mf.name}: {exc}")
|
||||
self.log.append(t("gui.log.build_error", path=str(job.source_3mf), error=str(exc)))
|
||||
continue
|
||||
tasks.append(IndividualBuildTask(job, options))
|
||||
|
||||
all_lines: list[str] = []
|
||||
if tasks:
|
||||
worker_count = individual_batch_worker_count(len(tasks))
|
||||
self.log.append(t("gui.log.building_individual", count=len(tasks), workers=worker_count))
|
||||
all_lines.append(t("gui.log.building_individual", count=len(tasks), workers=worker_count))
|
||||
batch_result = run_individual_batch_builds(tasks, max_workers=worker_count)
|
||||
for result in batch_result.results:
|
||||
self.log_build_result(result)
|
||||
all_lines.extend(self.log_build_result(result))
|
||||
all_lines.append("")
|
||||
success_count = len(batch_result.results)
|
||||
for failure in batch_result.failures:
|
||||
all_lines.append(t("gui.log.build_error", path=str(failure.job.source_3mf), error=str(failure.error)))
|
||||
errors.append(f"{failure.job.source_3mf.name}: {failure.error}")
|
||||
self.log.append(t("gui.log.build_error", path=str(failure.job.source_3mf), error=str(failure.error)))
|
||||
|
||||
if errors:
|
||||
QMessageBox.warning(
|
||||
@@ -1426,7 +1389,10 @@ class MainWindow(QMainWindow):
|
||||
+ ("\n..." if len(errors) > 8 else ""),
|
||||
)
|
||||
return
|
||||
self.show_success_toast(t("gui.toast.batch_success", count=success_count))
|
||||
if all_lines:
|
||||
self._show_build_result_dialog(t("gui.dialog.build_result.title"), all_lines)
|
||||
if self.clear_after_build_check.isChecked():
|
||||
self.remove_all()
|
||||
if self.clear_after_build_check.isChecked():
|
||||
self.remove_all()
|
||||
|
||||
@@ -1484,15 +1450,18 @@ class MainWindow(QMainWindow):
|
||||
self.remove_button.setText(t("gui.button.remove"))
|
||||
self.remove_all_button.setText(t("gui.button.remove_all"))
|
||||
self.apply_default_copies_button.setText(t("gui.button.apply_copies"))
|
||||
self.repack_3mf_button.setText(t("gui.options.repack_3mf"))
|
||||
self.build_button.setText(t("gui.button.build"))
|
||||
|
||||
# 打包选项区
|
||||
self.options_group.setTitle(t("gui.options.group.title"))
|
||||
self.refresh_button.setText(t("gui.options.refresh"))
|
||||
self.open_folder_button.setText(t("gui.options.open_folder"))
|
||||
self.view_gcode_button.setText(t("gui.options.view_gcode"))
|
||||
# 重新计算按钮宽度以适应翻译后的文本长度
|
||||
self.refresh_button.setFixedWidth(self.refresh_button.sizeHint().width())
|
||||
self.open_folder_button.setFixedWidth(self.open_folder_button.sizeHint().width())
|
||||
self.view_gcode_button.setFixedWidth(self.view_gcode_button.sizeHint().width())
|
||||
self.bed_cooldown_check.setText(t("gui.options.bed_cooldown"))
|
||||
self.eject_wait_check.setText(t("gui.options.eject_wait"))
|
||||
self.show_plate_number_check.setText(t("gui.options.plate_number"))
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
{
|
||||
"_lang": "English",
|
||||
|
||||
"gui.file_group.title": "Input 3MF Files",
|
||||
"gui.file_group.table.header.order": "Order",
|
||||
"gui.file_group.table.header.file": "3MF File",
|
||||
@@ -15,18 +14,15 @@
|
||||
"gui.file_group.table.tooltip_base_filament": "Base filament: {filament}",
|
||||
"gui.file_group.total_summary": "Total: {plates} plate(s) | Time: {time} | Filament: {filament}",
|
||||
"gui.file_group.total_summary.empty": "Total: 0 plates | Time: 0s | Filament: 0.00 g",
|
||||
|
||||
"gui.button.add_3mf": "Add 3MF",
|
||||
"gui.button.remove": "Remove",
|
||||
"gui.button.remove_all": "Remove All",
|
||||
"gui.button.apply_copies": "Apply Default Copies to Selected",
|
||||
"gui.button.build": "Build 3MF",
|
||||
|
||||
"gui.preview.title": "Selected Thumbnail",
|
||||
"gui.preview.select_hint": "Select an input file",
|
||||
"gui.preview.no_image": "No preview image",
|
||||
"gui.preview.unavailable": "Preview unavailable",
|
||||
|
||||
"gui.options.group.title": "Packing Options",
|
||||
"gui.options.swap_gcode.label": "Swap G-code",
|
||||
"gui.options.refresh": "Refresh",
|
||||
@@ -60,7 +56,6 @@
|
||||
"gui.options.skip_duplicates": "Skip duplicate file paths when adding inputs",
|
||||
"gui.options.input_handling": "Input handling",
|
||||
"gui.options.language": "Language",
|
||||
|
||||
"gui.output.group.title": "Output",
|
||||
"gui.output.dir.label": "Output directory",
|
||||
"gui.output.dir.placeholder": "Leave empty to write next to the input file",
|
||||
@@ -71,7 +66,6 @@
|
||||
"gui.output.filename_rule.help.tooltip": "Show filename token help",
|
||||
"gui.output.preview": "Preview",
|
||||
"gui.output.batch_preview": "{first} | {count} output file(s)",
|
||||
|
||||
"gui.dialog.individual_batch.title": "Individual Batch Mode",
|
||||
"gui.dialog.individual_batch.text": "Individual batch mode treats every input row as a separate build.\n\nExample: if you add 20 single-plate 3MF files and set copies to 5, Build 3MF will create 20 separate packed files. Each output contains only that source file repeated 5 times.\n\nThis is useful for quickly batch-converting many independent 3MF jobs into multi-copy SwapMod packs. It does not combine all input files into one 3MF.",
|
||||
"gui.dialog.filename_help.title": "Output Filename Rule",
|
||||
@@ -85,10 +79,8 @@
|
||||
"gui.dialog.add_files_title": "Add 3MF files",
|
||||
"gui.dialog.file_filter.3mf": "3MF files (*.3mf)",
|
||||
"gui.dialog.file_filter.all": "All files (*)",
|
||||
|
||||
"gui.toast.combined_success": "The packed 3MF file was created.",
|
||||
"gui.toast.batch_success": "Created {count} packed 3MF file(s).",
|
||||
|
||||
"gui.log.added_files": "Added {added} input file(s). Skipped {skipped}.",
|
||||
"gui.log.skipped_file": "Skipped {path}: {reason}",
|
||||
"gui.log.no_swap_files": "No swap G-code files found in {dir}",
|
||||
@@ -99,6 +91,14 @@
|
||||
"gui.log.filament": "Estimated source filament: {filament}",
|
||||
"gui.log.md5": "G-code MD5: {md5}",
|
||||
"gui.log.build_error": "Error building {path}: {error}",
|
||||
|
||||
"format.unknown": "Unknown"
|
||||
}
|
||||
"format.unknown": "Unknown",
|
||||
"gui.button.close": "Close",
|
||||
"gui.dialog.view_gcode.title": "View Swap G-code",
|
||||
"gui.options.view_gcode": "View",
|
||||
"gui.dialog.build_result.title": "Build Result",
|
||||
"gui.options.repack_3mf": "Repack 3MF",
|
||||
"gui.dialog.repack_select_folder": "Select unpacked 3MF folder",
|
||||
"gui.dialog.repack_success.title": "Repack Complete",
|
||||
"gui.dialog.repack_success.text": "3MF has been repacked:\n{path}",
|
||||
"gui.dialog.repack_no_content_types": "[Content_Types].xml not found. Not a valid unpacked 3MF directory."
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
{
|
||||
"_lang": "简体中文",
|
||||
|
||||
"gui.file_group.title": "输入 3MF 文件",
|
||||
"gui.file_group.table.header.order": "顺序",
|
||||
"gui.file_group.table.header.file": "3MF 文件",
|
||||
@@ -15,22 +14,20 @@
|
||||
"gui.file_group.table.tooltip_base_filament": "基础耗材:{filament}",
|
||||
"gui.file_group.total_summary": "合计:{plates} 板 | 时间:{time} | 耗材:{filament}",
|
||||
"gui.file_group.total_summary.empty": "合计:0 板 | 时间:0s | 耗材:0.00 g",
|
||||
|
||||
"gui.button.add_3mf": "添加 3MF",
|
||||
"gui.button.remove": "移除",
|
||||
"gui.button.remove_all": "全部移除",
|
||||
"gui.button.apply_copies": "应用默认份数至选中行",
|
||||
"gui.button.build": "构建 3MF",
|
||||
|
||||
"gui.preview.title": "选中文件的缩略图",
|
||||
"gui.preview.select_hint": "选择一个输入文件",
|
||||
"gui.preview.no_image": "无预览图",
|
||||
"gui.preview.unavailable": "预览不可用",
|
||||
|
||||
"gui.options.group.title": "打包选项",
|
||||
"gui.options.swap_gcode.label": "换盘 G-code",
|
||||
"gui.options.refresh": "刷新",
|
||||
"gui.options.open_folder": "打开文件夹",
|
||||
"gui.options.view_gcode": "查看",
|
||||
"gui.options.default_copies": "新输入的默认份数",
|
||||
"gui.options.bed_cooldown": "等待热床降温",
|
||||
"gui.options.bed_cooldown.label": "热床降温",
|
||||
@@ -60,7 +57,6 @@
|
||||
"gui.options.skip_duplicates": "添加输入时跳过重复文件路径",
|
||||
"gui.options.input_handling": "输入处理",
|
||||
"gui.options.language": "语言",
|
||||
|
||||
"gui.output.group.title": "输出",
|
||||
"gui.output.dir.label": "输出目录",
|
||||
"gui.output.dir.placeholder": "留空则写入输入文件所在目录",
|
||||
@@ -71,7 +67,6 @@
|
||||
"gui.output.filename_rule.help.tooltip": "显示文件名标记帮助",
|
||||
"gui.output.preview": "预览",
|
||||
"gui.output.batch_preview": "{first} | {count} 个输出文件",
|
||||
|
||||
"gui.dialog.individual_batch.title": "独立批处理模式",
|
||||
"gui.dialog.individual_batch.text": "独立批处理模式将每一行输入作为独立的构建任务。\n\n示例:如果你添加了 20 个单板 3MF 文件并设置份数为 5,点击\"构建 3MF\"将创建 20 个独立的打包文件。每个输出文件仅包含该源文件重复 5 次的内容。\n\n这个功能适用于快速将大量独立的 3MF 作业批量转换为多份数的 SwapMod 包。它不会将所有输入文件合并到一个 3MF 中。",
|
||||
"gui.dialog.filename_help.title": "输出文件名规则",
|
||||
@@ -85,10 +80,8 @@
|
||||
"gui.dialog.add_files_title": "添加 3MF 文件",
|
||||
"gui.dialog.file_filter.3mf": "3MF 文件 (*.3mf)",
|
||||
"gui.dialog.file_filter.all": "所有文件 (*)",
|
||||
|
||||
"gui.toast.combined_success": "已创建打包的 3MF 文件。",
|
||||
"gui.toast.batch_success": "已创建 {count} 个打包的 3MF 文件。",
|
||||
|
||||
"gui.log.added_files": "已添加 {added} 个输入文件。跳过 {skipped} 个。",
|
||||
"gui.log.skipped_file": "已跳过 {path}:{reason}",
|
||||
"gui.log.no_swap_files": "在 {dir} 中未找到换盘 G-code 文件",
|
||||
@@ -99,6 +92,13 @@
|
||||
"gui.log.filament": "预估源文件耗材:{filament}",
|
||||
"gui.log.md5": "G-code MD5:{md5}",
|
||||
"gui.log.build_error": "构建 {path} 时出错:{error}",
|
||||
|
||||
"format.unknown": "未知"
|
||||
}
|
||||
"format.unknown": "未知",
|
||||
"gui.button.close": "关闭",
|
||||
"gui.dialog.view_gcode.title": "查看换盘 G-code",
|
||||
"gui.dialog.build_result.title": "构建结果",
|
||||
"gui.options.repack_3mf": "重新打包 3MF",
|
||||
"gui.dialog.repack_select_folder": "选择解压后的 3MF 文件夹",
|
||||
"gui.dialog.repack_success.title": "重新打包完成",
|
||||
"gui.dialog.repack_success.text": "已重新打包为 3MF 文件:\n{path}",
|
||||
"gui.dialog.repack_no_content_types": "未找到 [Content_Types].xml,不是有效的 3MF 解压目录。"
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
"""将解压后的 3MF 文件夹重新打包为 .3mf 文件的工具脚本。
|
||||
|
||||
用法:
|
||||
python repack_3mf.py "解压目录" "output.3mf"
|
||||
|
||||
会自动更新 Metadata/plate_N.gcode.md5 并确保 [Content_Types].xml 为 ZIP 第一条目。
|
||||
|
||||
如果省略输出路径,自动在解压目录同级生成同名 .3mf。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
GCODE_MEMBER_RE = re.compile(r"^Metadata[\\/]plate_(\d+)\.gcode$")
|
||||
|
||||
|
||||
def update_md5(src_dir: Path) -> None:
|
||||
"""更新所有 plate_N.gcode 对应的 .md5 文件。"""
|
||||
metadata = src_dir / "Metadata"
|
||||
if not metadata.is_dir():
|
||||
return
|
||||
for gcode_file in sorted(metadata.glob("plate_*.gcode")):
|
||||
if not GCODE_MEMBER_RE.match(str(gcode_file.relative_to(src_dir)).replace("\\", "/")):
|
||||
continue
|
||||
h = hashlib.md5()
|
||||
with open(gcode_file, "rb") as f:
|
||||
h.update(f.read())
|
||||
md5_path = Path(str(gcode_file) + ".md5")
|
||||
md5_path.write_text(h.hexdigest(), encoding="ascii")
|
||||
print(f" MD5 已更新: {md5_path.name} → {h.hexdigest()}")
|
||||
|
||||
|
||||
def repack(src_dir: Path, output: Path) -> None:
|
||||
"""将解压目录重新打包为 3MF。"""
|
||||
ct_path = src_dir / "[Content_Types].xml"
|
||||
if not ct_path.exists():
|
||||
raise FileNotFoundError(f"未找到 [Content_Types].xml,请确认 {src_dir} 是有效的 3MF 解压目录")
|
||||
|
||||
print(f"源目录: {src_dir}")
|
||||
update_md5(src_dir)
|
||||
|
||||
with zipfile.ZipFile(output, "w", zipfile.ZIP_DEFLATED) as z:
|
||||
z.write(ct_path, "[Content_Types].xml")
|
||||
|
||||
count = 1
|
||||
for root, _dirs, files in os.walk(str(src_dir)):
|
||||
for fn in files:
|
||||
fp = Path(root) / fn
|
||||
arcname = fp.relative_to(src_dir).as_posix()
|
||||
if arcname == "[Content_Types].xml":
|
||||
continue
|
||||
z.write(str(fp), arcname)
|
||||
count += 1
|
||||
|
||||
size_mb = output.stat().st_size / (1024 * 1024)
|
||||
print(f"输出文件: {output} ({size_mb:.1f} MB, {count} 条目)")
|
||||
print("打包完成。")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if len(sys.argv) < 2:
|
||||
print(__doc__)
|
||||
return 1
|
||||
|
||||
src = Path(sys.argv[1])
|
||||
if not src.is_dir():
|
||||
print(f"错误: 目录不存在 — {src}")
|
||||
return 1
|
||||
|
||||
if len(sys.argv) >= 3:
|
||||
output = Path(sys.argv[2])
|
||||
else:
|
||||
output = src.parent / (src.name.rstrip("/\\") + ".3mf")
|
||||
|
||||
try:
|
||||
repack(src, output)
|
||||
except Exception as exc:
|
||||
print(f"错误: {exc}")
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -1,132 +0,0 @@
|
||||
;==========================================================
|
||||
; A1 SwapMod 换板 G-code — 类型 B:双轮抖板
|
||||
; 特点:G380 S3/S2 交替下压、双轮 Shake 抖板(最大3.5mm偏移)、无 M400/M17
|
||||
;==========================================================
|
||||
|
||||
; 开始换板
|
||||
|
||||
; --- 阶段1:Y 轴归零并退到后端 ---
|
||||
G28 Y ; Y 轴归零(寻找限位开关)
|
||||
G1 Y262 F1200 ; 推到 Y262(后端位置,靠近 Z 轴立柱)
|
||||
|
||||
; --- 阶段2:Z 轴交替下压(G380 S3/S2 ×4轮),压紧打印板 ---
|
||||
G91 ; 切换相对坐标模式
|
||||
G380 S3 Z-25 F1200 ; 力传感器碰撞退出:Z 轴下降 25mm 直到碰到板
|
||||
G380 S2 Z85 F1200 ; 力传感器目标位移动:Z 轴上升 85mm 到预设力值
|
||||
G380 S3 Z-20 F1200 ; 第二轮下压碰撞检测
|
||||
G380 S2 Z85 F1200 ; 第二轮力控上升
|
||||
G380 S3 Z-20 F1200 ; 第三轮下压
|
||||
G380 S2 Z85 F1200 ; 第三轮上升
|
||||
G380 S3 Z-20 F1200 ; 第四轮下压(确保板完全贴合热床)
|
||||
G380 S2 Z85 F1200 ; 第四轮上升
|
||||
G1 Z-5 F1200 ; Z 轴下降 5mm 释放上升张力
|
||||
G90 ; 切回绝对坐标模式
|
||||
|
||||
; --- 阶段3:驻留稳定 ---
|
||||
G4 P2000 ; 驻留 2 秒,让板稳定贴合
|
||||
|
||||
; --- 阶段4:前推弹板 — 利用惯性将板从热床上推离 ---
|
||||
G1 Y15 F2000 ; 先推到前端附近 Y15
|
||||
G1 Y150 F1200 ; 后退一段
|
||||
G1 Y40 F1200 ; 再推向前端
|
||||
|
||||
; 以下注释掉的 Y260 是备选方案
|
||||
; G1 Y260 F3000
|
||||
|
||||
; 用触力传感器推到最前端 Y264(力控,不会硬撞)
|
||||
G380 S2 Y264 F2000 ; A1 专有:以预设力矩正向推进到 Y264
|
||||
|
||||
; --- 阶段5:Z 轴上撤退让,为弹射留出空间 ---
|
||||
G91 ; 切相对坐标
|
||||
; G380 S3 Z-55 F1200 ; 注释掉:原用触力退出方式
|
||||
G1 Z-55 F1200 ; Z 轴下降 55mm(使喷嘴远离板面)
|
||||
G90 ; 切回绝对坐标
|
||||
|
||||
; --- 阶段6:Y 轴快速移动 + 微调定位 ---
|
||||
G1 Y120 F4000 ; 快速移动到 Y120
|
||||
G28 Y ; Y 轴再次归零校准
|
||||
|
||||
; 使用 G1 而非 G380 是为了防止触力传感器干扰定位精度
|
||||
G1 Y-1 F1200 ; Y 轴后移 1mm(为抖板准备起始位置)
|
||||
; G380 S3 Y-1 F1200 ; 注释掉:原触力退出方式
|
||||
|
||||
G4 P500 ; 驻留 500ms
|
||||
|
||||
; ═══════════════════════════════════════════
|
||||
; 抖板 第1轮:微小 Y 轴往复振动,确保板完全脱离
|
||||
; 振幅逐步增大:1mm → 3.5mm
|
||||
; ═══════════════════════════════════════════
|
||||
; 开始 振动触发换板
|
||||
G1 Y1 F100 ; 前进 1mm(慢速)
|
||||
G1 Y0 F20000 ; 快速退回
|
||||
G4 P500 ; 驻留 500ms
|
||||
|
||||
G1 Y1.5 F100 ; 前进 1.5mm
|
||||
G1 Y0 F20000 ; 快速退回
|
||||
G4 P500
|
||||
|
||||
G1 Y2 F100 ; 前进 2mm
|
||||
G1 Y0 F20000 ; 快速退回
|
||||
G4 P500
|
||||
|
||||
G1 Y2.5 F100 ; 前进 2.5mm
|
||||
G1 Y0 F20000 ; 快速退回
|
||||
G4 P500
|
||||
|
||||
G1 Y3 F100 ; 前进 3mm
|
||||
G1 Y0 F10000 ; 退回(降速)
|
||||
G4 P500
|
||||
|
||||
G1 Y3.5 F100 ; 前进 3.5mm(最大振幅)
|
||||
G1 Y0 F5000 ; 退回(进一步降速)
|
||||
; 结束 振动触发换板
|
||||
|
||||
; --- 阶段7:推板前移 + 力控校准 ---
|
||||
G1 Y235 F1200 ; 推到 Y235
|
||||
G1 Y110 F5000 ; 后退
|
||||
G1 Y200 F1200 ; 再前推
|
||||
; G1 Y266 F1500 ; 注释掉:备选位置
|
||||
G380 S2 Y264 F2000 ; 力控推到最前端 Y264
|
||||
|
||||
; 将打印板缩回到最前端后,驻留 1 秒
|
||||
G4 P1000 ; 驻留 1 秒
|
||||
|
||||
; --- 阶段8:再次弹射 ---
|
||||
G1 Y40 F4000 ; 推到 Y40
|
||||
G1 Y-2 F5000 ; ⚡ 高速反向弹射 2mm
|
||||
|
||||
; ═══════════════════════════════════════════
|
||||
; 抖板 第2轮:再次振动,确保没有残留的板
|
||||
; 振幅:1mm → 3.5mm(与第1轮相同)
|
||||
; ═══════════════════════════════════════════
|
||||
; 开始 振动触发换板
|
||||
G1 Y1 F100
|
||||
G1 Y0 F20000
|
||||
G4 P500
|
||||
G1 Y1.5 F100
|
||||
G1 Y0 F20000
|
||||
G4 P500
|
||||
G1 Y2 F100
|
||||
G1 Y0 F20000
|
||||
G4 P500
|
||||
G1 Y2.5 F100
|
||||
G1 Y0 F20000
|
||||
G4 P500
|
||||
G1 Y3 F100
|
||||
G1 Y0 F10000
|
||||
G4 P500
|
||||
G1 Y3.5 F100
|
||||
G1 Y0 F5000
|
||||
|
||||
; 以下为注释掉的备选抖板参数
|
||||
; G1 Y2.5 F100
|
||||
; G1 Y0 F8000
|
||||
; G4 P1000
|
||||
; G1 Y3.5 F100
|
||||
; G1 Y0 F5000
|
||||
; 结束 振动触发换板
|
||||
|
||||
; --- 阶段9:归位到待机位置 ---
|
||||
G1 Y120 F2000 ; 回到 Y120 中间待机位置
|
||||
|
||||
; 结束换板
|
||||
@@ -1,140 +0,0 @@
|
||||
;==========================================================
|
||||
; A1 SwapMod 换板 G-code — 类型 C1:安全换板(标准版)
|
||||
; 特点:M400/M17 保护、G1 Z- + G380 S2 纯力控下压、双轮抖板(2.5mm偏移)
|
||||
;==========================================================
|
||||
|
||||
; 开始换板
|
||||
|
||||
; --- 阶段1:Y 轴归零并退到后端 ---
|
||||
G28 Y ; Y 轴归零(寻找限位开关)
|
||||
G1 Y262 F2000 ; 推到 Y262(后端位置,速度 F2000)
|
||||
|
||||
; --- 阶段2:运动保护 — 等所有运动停止 + 降低 Z 电机电流 ---
|
||||
M400 ; 等待所有运动队列清空
|
||||
M17 Z0.4 ; 将 Z 轴电机电流降至 0.4A,降低阻力,防止后续 Y 轴移动时 Z 轴硬怼损伤打印件
|
||||
|
||||
; --- 阶段3:Z 轴下压(G1 Z- + G380 S2 纯力控 ×4轮),压紧打印板 ---
|
||||
G91 ; 切换相对坐标模式
|
||||
G1 Z-35 F1200 ; Z 轴下降 35mm
|
||||
G380 S2 Z95 ; 力控上升 95mm 到预设力值
|
||||
|
||||
G1 Z-25 ; Z 轴再降 25mm
|
||||
G380 S2 Z90 ; 力控上升 90mm
|
||||
|
||||
G1 Z-25 ; 第三轮
|
||||
G380 S2 Z90
|
||||
|
||||
G1 Z-30 ; 第四轮
|
||||
G380 S2 Z95
|
||||
|
||||
G1 Z-5 ; Z 轴微降 5mm 释放张力
|
||||
G90 ; 切回绝对坐标
|
||||
|
||||
; --- 阶段4:驻留稳定 ---
|
||||
G4 P2000 ; 驻留 2 秒
|
||||
|
||||
; --- 阶段5:前推弹板 — 利用惯性将板推离 ---
|
||||
G1 Y15 F800 ; 缓慢推到前端 Y15
|
||||
G1 Y150 F1200 ; 后退到 Y150
|
||||
|
||||
; 此行控制卷料尾端的悬垂距离。值越小,悬垂越少。
|
||||
G1 Y40 F1000 ; 推到 Y40
|
||||
|
||||
; 平台通过力控确保即使没有板也能正常装入新板
|
||||
; G1 Y260 F3000 ; 注释掉:备选方案
|
||||
G380 S2 Y264 F2000 ; 力控推到最前端 Y264
|
||||
|
||||
; --- 阶段6:Z 轴上撤退让 ---
|
||||
G91 ; 切相对坐标
|
||||
G1 Z-70 F1200 ; Z 轴下降 70mm(使喷嘴远离板面,为弹射留空间)
|
||||
G90 ; 切回绝对坐标
|
||||
|
||||
; --- 阶段7:Y 轴高速移动 + 微调到弹射起始位 ---
|
||||
G1 Y200 F10000 ; 高速推到 Y200
|
||||
G1 Y25 F4000 ; 减速靠前
|
||||
|
||||
; 使用 G1 而非 G380,防止触力传感器干扰定位
|
||||
G1 Y-1 F20000 ; 高速后退 1mm(为抖板准备起始位)
|
||||
; G380 S3 Y-1 F1200 ; 注释掉:原触力退出方式
|
||||
|
||||
G4 P500 ; 驻留 500ms
|
||||
|
||||
; ═══════════════════════════════════════════
|
||||
; 抖板 第1轮:微小 Y 轴往复振动,确保板完全脱离
|
||||
; 振幅逐步增大:1mm → 2.5mm(比类型B更保守)
|
||||
; ═══════════════════════════════════════════
|
||||
|
||||
G28 Y ; 先 Y 轴归零校准
|
||||
|
||||
; 开始 振动触发换板
|
||||
G1 Y1 F100 ; 前进 1mm(慢速)
|
||||
G1 Y0 F20000 ; 快速退回
|
||||
G4 P500
|
||||
|
||||
G1 Y1.5 F100 ; 前进 1.5mm
|
||||
G1 Y0 F20000 ; 快速退回
|
||||
G4 P500
|
||||
|
||||
G1 Y2 F100 ; 前进 2mm
|
||||
G1 Y0 F20000 ; 快速退回
|
||||
G4 P500
|
||||
|
||||
G1 Y2.5 F100 ; 前进 2.5mm
|
||||
G1 Y0 F20000 ; 快速退回
|
||||
G4 P500
|
||||
|
||||
G1 Y2.5 F100 ; 再次 2.5mm(巩固)
|
||||
G1 Y0 F10000 ; 退回(降速)
|
||||
G4 P500
|
||||
; 注释掉的更大振幅
|
||||
; G1 Y3.5 F100
|
||||
; G1 Y0 F5000
|
||||
; 结束 振动触发换板
|
||||
|
||||
; --- 阶段8:推板前移 + 力控校准 ---
|
||||
G1 Y235 F1200 ; 推到 Y235
|
||||
G1 Y110 F5000 ; 后退
|
||||
G1 Y200 F1200 ; 再前推
|
||||
; G1 Y266 F1500 ; 注释掉:备选
|
||||
G380 S2 Y264 F2000 ; 力控推到最前端 Y264
|
||||
|
||||
; 将打印板缩回到最前端后,驻留 1 秒
|
||||
G4 P1000
|
||||
|
||||
; --- 阶段9:第2轮弹射 ---
|
||||
G1 Y25 F4000 ; 推到 Y25
|
||||
G1 Y-2 F20000 ; ⚡ 高速反向弹射 2mm(F20000,最快的弹射速度)
|
||||
|
||||
; G1 Y-2 F1000 ; 慢速备选
|
||||
|
||||
G4 P500 ; 驻留 500ms
|
||||
|
||||
; ═══════════════════════════════════════════
|
||||
; 抖板 第2轮:再次振动确保无残留
|
||||
; 振幅:1mm → 2.5mm
|
||||
; ═══════════════════════════════════════════
|
||||
; 开始 振动触发换板
|
||||
G1 Y1 F100
|
||||
G1 Y0 F20000
|
||||
G4 P500
|
||||
G1 Y1.5 F100
|
||||
G1 Y0 F20000
|
||||
G4 P500
|
||||
G1 Y2 F100
|
||||
G1 Y0 F20000
|
||||
G4 P500
|
||||
G1 Y2.5 F100
|
||||
G1 Y0 F20000
|
||||
G4 P500
|
||||
G1 Y2.5 F100
|
||||
G1 Y0 F10000
|
||||
G4 P500
|
||||
|
||||
; 结束 振动触发换板
|
||||
|
||||
; --- 阶段10:归位 ---
|
||||
; 推到前端
|
||||
G1 Y262 F1500 ; 推到 Y262
|
||||
G1 Y120 F2000 ; 回到 Y120 待机位置
|
||||
|
||||
; 结束换板
|
||||
@@ -1,133 +0,0 @@
|
||||
;==========================================================
|
||||
; A1 SwapMod 换板 G-code — 类型 C2:安全换板(慢速版)
|
||||
; 特点:与 C1 标准版完全相同,仅第2轮弹射速度从 F20000 降为 F1000
|
||||
; 更温和的弹射,减少对板和打印机的冲击
|
||||
;==========================================================
|
||||
|
||||
; 开始换板
|
||||
|
||||
; --- 阶段1:Y 轴归零并退到后端 ---
|
||||
G28 Y ; Y 轴归零(寻找限位开关)
|
||||
G1 Y262 F2000 ; 推到 Y262(后端位置)
|
||||
|
||||
; --- 阶段2:运动保护 ---
|
||||
M400 ; 等待所有运动队列清空
|
||||
M17 Z0.4 ; 将 Z 轴电机电流降至 0.4A,降低阻力,防止碰撞损伤打印件
|
||||
|
||||
; --- 阶段3:Z 轴下压(G1 Z- + G380 S2 纯力控 ×4轮)---
|
||||
G91 ; 切换相对坐标
|
||||
G1 Z-35 F1200 ; Z 轴下降 35mm
|
||||
G380 S2 Z95 ; 力控上升 95mm
|
||||
|
||||
G1 Z-25 ; 第二轮
|
||||
G380 S2 Z90
|
||||
|
||||
G1 Z-25 ; 第三轮
|
||||
G380 S2 Z90
|
||||
|
||||
G1 Z-30 ; 第四轮
|
||||
G380 S2 Z95
|
||||
|
||||
G1 Z-5 ; Z 轴微降释放张力
|
||||
G90 ; 切回绝对坐标
|
||||
|
||||
; --- 阶段4:驻留稳定 ---
|
||||
G4 P2000 ; 驻留 2 秒
|
||||
|
||||
; --- 阶段5:前推弹板 ---
|
||||
G1 Y15 F800 ; 缓慢推到前端 Y15
|
||||
G1 Y150 F1200 ; 后退到 Y150
|
||||
|
||||
; 此行控制卷料尾端的悬垂距离。值越小,悬垂越少。
|
||||
G1 Y40 F1000 ; 推到 Y40
|
||||
|
||||
; 平台通过力控确保即使没有板也能正常装入新板
|
||||
; G1 Y260 F3000
|
||||
G380 S2 Y264 F2000 ; 力控推到最前端 Y264
|
||||
|
||||
; --- 阶段6:Z 轴上撤退让 ---
|
||||
G91 ; 切相对坐标
|
||||
G1 Z-70 F1200 ; Z 轴下降 70mm(为弹射留空间)
|
||||
G90 ; 切回绝对坐标
|
||||
|
||||
; --- 阶段7:Y 轴高速移动 + 微调定位 ---
|
||||
G1 Y200 F10000 ; 高速推到 Y200
|
||||
G1 Y25 F4000 ; 减速靠前
|
||||
|
||||
; 使用 G1 而非 G380,防止触力传感器干扰定位
|
||||
G1 Y-1 F20000 ; 高速后退 1mm
|
||||
|
||||
G4 P500 ; 驻留 500ms
|
||||
|
||||
; --- 抖板校准归零 ---
|
||||
G28 Y ; Y 轴归零校准
|
||||
|
||||
; ═══════════════════════════════════════════
|
||||
; 抖板 第1轮:振动确保板完全脱离(1→2.5mm)
|
||||
; ═══════════════════════════════════════════
|
||||
; 开始 振动触发换板
|
||||
G1 Y1 F100
|
||||
G1 Y0 F20000
|
||||
G4 P500
|
||||
G1 Y1.5 F100
|
||||
G1 Y0 F20000
|
||||
G4 P500
|
||||
G1 Y2 F100
|
||||
G1 Y0 F20000
|
||||
G4 P500
|
||||
G1 Y2.5 F100
|
||||
G1 Y0 F20000
|
||||
G4 P500
|
||||
G1 Y2.5 F100
|
||||
G1 Y0 F10000
|
||||
G4 P500
|
||||
; G1 Y3.5 F100 ; 注释掉:更大的振幅(备选)
|
||||
; G1 Y0 F5000
|
||||
; 结束 振动触发换板
|
||||
|
||||
; --- 阶段8:推板 + 力控校准 ---
|
||||
G1 Y235 F1200
|
||||
G1 Y110 F5000
|
||||
G1 Y200 F1200
|
||||
; G1 Y266 F1500
|
||||
G380 S2 Y264 F2000 ; 力控推到最前端
|
||||
|
||||
; 驻留 1 秒
|
||||
G4 P1000
|
||||
|
||||
; --- 阶段9:第2轮弹射(🐌 慢速版关键差异)---
|
||||
G1 Y25 F4000 ; 推到 Y25
|
||||
; G1 Y-2 F20000 ; 注释掉:原标准版高速弹射
|
||||
G1 Y-2 F1000 ; 🐌 慢速弹射(F1000 vs 标准版 F20000)
|
||||
; 更温和,减少对板和结构的冲击
|
||||
|
||||
G4 P500
|
||||
|
||||
; ═══════════════════════════════════════════
|
||||
; 抖板 第2轮:再次振动(1→2.5mm)
|
||||
; ═══════════════════════════════════════════
|
||||
; 开始 振动触发换板
|
||||
G1 Y1 F100
|
||||
G1 Y0 F20000
|
||||
G4 P500
|
||||
G1 Y1.5 F100
|
||||
G1 Y0 F20000
|
||||
G4 P500
|
||||
G1 Y2 F100
|
||||
G1 Y0 F20000
|
||||
G4 P500
|
||||
G1 Y2.5 F100
|
||||
G1 Y0 F20000
|
||||
G4 P500
|
||||
G1 Y2.5 F100
|
||||
G1 Y0 F10000
|
||||
G4 P500
|
||||
|
||||
; 结束 振动触发换板
|
||||
|
||||
; --- 阶段10:归位 ---
|
||||
; 推到前端
|
||||
G1 Y262 F1500
|
||||
G1 Y120 F2000 ; 回到 Y120 待机位置
|
||||
|
||||
; 结束换板
|
||||
@@ -1,151 +0,0 @@
|
||||
;==========================================================
|
||||
; A1 SwapMod 换板 G-code — 类型 C3:安全换板(安全Z版)⭐ 推荐
|
||||
; 特点:基于 C2 慢速版,尾部追加 Z 轴安全回位(Z30),防止下一板打印时碰撞
|
||||
; 本变体由 QQ 群用户 风信子号 提供
|
||||
;==========================================================
|
||||
|
||||
;===================================
|
||||
; 此变体由 风信子号 友情提供
|
||||
;===================================
|
||||
|
||||
; 开始换板
|
||||
|
||||
; --- 阶段1:Y 轴归零并退到后端 ---
|
||||
G28 Y ; Y 轴归零(寻找限位开关)
|
||||
G1 Y262 F2000 ; 推到 Y262(后端位置)
|
||||
|
||||
; --- 阶段2:运动保护 ---
|
||||
M400 ; 等待所有运动队列清空
|
||||
M17 Z0.4 ; 将 Z 轴电机电流降至 0.4A,降低阻力,防止碰撞损伤打印件
|
||||
|
||||
; --- 阶段3:Z 轴下压(G1 Z- + G380 S2 纯力控 ×4轮)---
|
||||
G91 ; 切换相对坐标
|
||||
G1 Z-35 F1200 ; Z 轴下降 35mm
|
||||
G380 S2 Z95 ; 力控上升 95mm
|
||||
|
||||
G1 Z-25 ; 第二轮
|
||||
G380 S2 Z90
|
||||
|
||||
G1 Z-25 ; 第三轮
|
||||
G380 S2 Z90
|
||||
|
||||
G1 Z-30 ; 第四轮
|
||||
G380 S2 Z95
|
||||
|
||||
G1 Z-5 ; Z 轴微降释放张力
|
||||
G90 ; 切回绝对坐标
|
||||
|
||||
; --- 阶段4:驻留稳定 ---
|
||||
G4 P2000 ; 驻留 2 秒
|
||||
|
||||
; --- 阶段5:前推弹板 ---
|
||||
G1 Y15 F800 ; 缓慢推到前端 Y15
|
||||
G1 Y150 F1200 ; 后退到 Y150
|
||||
|
||||
; 此行控制卷料尾端的悬垂距离。值越小,悬垂越少。
|
||||
G1 Y40 F1000 ; 推到 Y40
|
||||
|
||||
; 平台通过力控确保即使没有板也能正常装入新板
|
||||
; G1 Y260 F3000
|
||||
G380 S2 Y264 F2000 ; 力控推到最前端 Y264
|
||||
|
||||
;=================================================
|
||||
; !!!!!! Z 轴下降,离开顶部
|
||||
;=================================================
|
||||
|
||||
; --- 阶段6:Z 轴上撤退让 ---
|
||||
G91 ; 切相对坐标
|
||||
G1 Z-70 F1200 ; Z 轴下降 70mm(为弹射留空间)
|
||||
G90 ; 切回绝对坐标
|
||||
|
||||
; --- 阶段7:Y 轴高速移动 + 微调定位 ---
|
||||
G1 Y200 F10000 ; 高速推到 Y200
|
||||
G1 Y25 F4000 ; 减速靠前
|
||||
|
||||
; 使用 G1 而非 G380,防止触力传感器干扰定位
|
||||
G1 Y-1 F20000 ; 高速后退 1mm
|
||||
|
||||
G4 P500 ; 驻留 500ms
|
||||
|
||||
; --- 抖板校准归零 ---
|
||||
G28 Y ; Y 轴归零校准
|
||||
|
||||
; ═══════════════════════════════════════════
|
||||
; 抖板 第1轮:振动确保板完全脱离(1→2.5mm)
|
||||
; ═══════════════════════════════════════════
|
||||
; 开始 振动触发换板
|
||||
G1 Y1 F100
|
||||
G1 Y0 F20000
|
||||
G4 P500
|
||||
G1 Y1.5 F100
|
||||
G1 Y0 F20000
|
||||
G4 P500
|
||||
G1 Y2 F100
|
||||
G1 Y0 F20000
|
||||
G4 P500
|
||||
G1 Y2.5 F100
|
||||
G1 Y0 F20000
|
||||
G4 P500
|
||||
G1 Y2.5 F100
|
||||
G1 Y0 F10000
|
||||
G4 P500
|
||||
; G1 Y3.5 F100 ; 注释掉:备选更大振幅
|
||||
; G1 Y0 F5000
|
||||
; 结束 振动触发换板
|
||||
|
||||
; --- 阶段8:推板 + 力控校准 ---
|
||||
G1 Y235 F1200
|
||||
G1 Y110 F5000
|
||||
G1 Y200 F1200
|
||||
; G1 Y266 F1500
|
||||
G380 S2 Y264 F2000 ; 力控推到最前端
|
||||
|
||||
; 驻留 1 秒
|
||||
G4 P1000
|
||||
|
||||
; --- 阶段9:第2轮弹射(慢速)---
|
||||
G1 Y25 F4000 ; 推到 Y25
|
||||
; G1 Y-2 F20000 ; 注释掉:标准版高速
|
||||
G1 Y-2 F1000 ; 🐌 慢速弹射
|
||||
|
||||
G4 P500
|
||||
|
||||
; ═══════════════════════════════════════════
|
||||
; 抖板 第2轮:再次振动(1→2.5mm)
|
||||
; ═══════════════════════════════════════════
|
||||
; 开始 振动触发换板
|
||||
G1 Y1 F100
|
||||
G1 Y0 F20000
|
||||
G4 P500
|
||||
G1 Y1.5 F100
|
||||
G1 Y0 F20000
|
||||
G4 P500
|
||||
G1 Y2 F100
|
||||
G1 Y0 F20000
|
||||
G4 P500
|
||||
G1 Y2.5 F100
|
||||
G1 Y0 F20000
|
||||
G4 P500
|
||||
G1 Y2.5 F100
|
||||
G1 Y0 F10000
|
||||
G4 P500
|
||||
|
||||
; 结束 振动触发换板
|
||||
|
||||
; --- 阶段10:归位 ---
|
||||
; 推到前端
|
||||
G1 Y262 F1500
|
||||
G1 Y120 F2000 ; 回到 Y120 待机位置
|
||||
|
||||
; 结束换板
|
||||
|
||||
; ═══════════════════════════════════════════
|
||||
; 🔐 安全 Z 轴回位(C3 独有)
|
||||
; 换板完成后 Z 轴在低位(约 Z-70),下一板开始时可能碰撞
|
||||
; 预先升到 Z30 确保安全间距
|
||||
; ═══════════════════════════════════════════
|
||||
G4 P3000 ; 驻留 3 秒,等待板完全稳定
|
||||
M400 ; 等待所有运动完成
|
||||
G90 ; 确保绝对坐标
|
||||
G1 Z30 F600 ; 🔐 Z 轴升至 30mm 安全高度
|
||||
M400 ; 再次确认到位
|
||||
Reference in New Issue
Block a user