Merge branch 'main' into fix/plugin-install-state

This commit is contained in:
Ian Chua
2026-07-23 12:24:19 +08:00
committed by GitHub
55 changed files with 5519 additions and 1439 deletions
+25 -7
View File
@@ -10,7 +10,6 @@
OrcaSlicer: an open source Next-Gen Slicing Software for Precision 3D Prints. OrcaSlicer: an open source Next-Gen Slicing Software for Precision 3D Prints.
Optimize your prints with ultra-fast slicing, intelligent support generation, and seamless printer compatibility—engineered for perfection. Optimize your prints with ultra-fast slicing, intelligent support generation, and seamless printer compatibility—engineered for perfection.
<h3>
# Official links and community # Official links and community
@@ -90,11 +89,22 @@ Visit our GitHub Releases page for the latest stable version of OrcaSlicer, reco
🌙 **[Download the Latest Nightly Build](https://github.com/OrcaSlicer/OrcaSlicer/releases/tag/nightly-builds)** 🌙 **[Download the Latest Nightly Build](https://github.com/OrcaSlicer/OrcaSlicer/releases/tag/nightly-builds)**
Explore the latest developments in OrcaSlicer with our nightly builds. Feedback on these versions is highly appreciated. Explore the latest developments in OrcaSlicer with our nightly builds. Feedback on these versions is highly appreciated.
### Belt Printer Builds
The [nightly release](https://github.com/OrcaSlicer/OrcaSlicer/releases/tag/nightly-builds) ships **two parallel builds**: the standard build and a belt-printer build. Both are attached to the same release — tell them apart by the filename suffix:
- **Standard** — no suffix (e.g. `OrcaSlicer_Windows_Installer_x64_nightly.exe`)
- **Belt** — `_belt` suffix (e.g. `OrcaSlicer_Windows_Installer_x64_nightly_belt.exe`)
The `_belt` builds add **experimental support for belt / conveyor (infinite-Z) printers**, where the model is sliced against a tilted belt surface instead of a flat horizontal bed. They include ready-to-use belt printer profiles, the full belt slicing pipeline (mesh rotation and G-code transforms), belt-aware support generation, and a tilted-bed preview.
> ⚠️ Belt printer support is under active development and is **not yet merged into `main`** — it currently ships only in these parallel `_belt` builds, produced from the [`belt-printer`](https://github.com/OrcaSlicer/OrcaSlicer/tree/belt-printer) branch. See tracking PR [#14394](https://github.com/OrcaSlicer/OrcaSlicer/pull/14394) and the original documentation in [#12998](https://github.com/OrcaSlicer/OrcaSlicer/pull/12998).
# How to install # How to install
## Windows ## Windows
Download the **Windows Installer exe** for your preferred version from the [releases page](https://github.com/OrcaSlicer/OrcaSlicer/releases). Download the **Windows Installer exe** for your preferred version from the [releases page](https://github.com/OrcaSlicer/OrcaSlicer/releases). Both `x64` and `arm64` installers are published — pick the one matching your CPU.
- *For convenience there is also a portable build available.* - *For convenience there is also a portable build available.*
<details> <details>
@@ -109,7 +119,7 @@ Download the **Windows Installer exe** for your preferred version from the [rel
- This file may already be available on your computer if you've installed visual studio. Check the following location: `%VCINSTALLDIR%Redist\MSVC\v142` - This file may already be available on your computer if you've installed visual studio. Check the following location: `%VCINSTALLDIR%Redist\MSVC\v142`
</details> </details>
Windows Package Manager ### Windows Package Manager
```shell ```shell
winget install --id=SoftFever.OrcaSlicer -e winget install --id=SoftFever.OrcaSlicer -e
@@ -117,7 +127,7 @@ winget install --id=SoftFever.OrcaSlicer -e
## Mac ## Mac
1. Download the DMG for your computer: `arm64` version for Apple Silicon and `x86_64` for Intel CPU. 1. Download the universal DMG, which runs on both Apple Silicon and Intel Macs.
2. Drag OrcaSlicer.app to Application folder. 2. Drag OrcaSlicer.app to Application folder.
3. *If you want to run a build from a PR, you also need to follow the instructions below:* 3. *If you want to run a build from a PR, you also need to follow the instructions below:*
@@ -142,6 +152,14 @@ winget install --id=SoftFever.OrcaSlicer -e
![mac_security_setting](./SoftFever_doc/mac_security_setting.png) ![mac_security_setting](./SoftFever_doc/mac_security_setting.png)
</details> </details>
### Homebrew Cask
```shell
brew install --cask orcaslicer
```
The [Homebrew cask](https://formulae.brew.sh/cask/orcaslicer) installs the official macOS DMG from [GitHub Releases](https://github.com/OrcaSlicer/OrcaSlicer/releases).
## Linux ## Linux
### Flathub (Recommended) ### Flathub (Recommended)
@@ -190,8 +208,8 @@ resolution: 0.1
# Supports # Supports
**OrcaSlicer** is an open-source project and I'm deeply grateful to all my sponsors and backers. **OrcaSlicer** is an open-source project, and we're deeply grateful to all our sponsors and backers.
Their generous support enables me to purchase filaments and other essential 3D printing materials for the project. Their generous support helps fund filaments and other essential 3D printing materials for the project.
Thank you! :) Thank you! :)
## Sponsors ## Sponsors
@@ -215,7 +233,7 @@ Thank you! :)
**Ko-fi supporters** ☕: [Backers list](https://github.com/user-attachments/files/16147016/Supporters_638561417699952499.csv) **Ko-fi supporters** ☕: [Backers list](https://github.com/user-attachments/files/16147016/Supporters_638561417699952499.csv)
## Support me ## Support the project
<a href="https://github.com/sponsors/SoftFever"><img src="https://img.shields.io/badge/GitHub%20Sponsors-30363D?style=flat&logo=GitHub-Sponsors&logoColor=EA4AAA" height="50"></a> <a href="https://github.com/sponsors/SoftFever"><img src="https://img.shields.io/badge/GitHub%20Sponsors-30363D?style=flat&logo=GitHub-Sponsors&logoColor=EA4AAA" height="50"></a>
<a href="https://ko-fi.com/G2G5IP3CP"><img src="https://img.shields.io/badge/Support_me_on_Ko--fi-FF5E5B?style=flat&logo=ko-fi&logoColor=white" height="50"></a> <a href="https://ko-fi.com/G2G5IP3CP"><img src="https://img.shields.io/badge/Support_me_on_Ko--fi-FF5E5B?style=flat&logo=ko-fi&logoColor=white" height="50"></a>
File diff suppressed because it is too large Load Diff
+171 -159
View File
@@ -18,22 +18,22 @@ msgstr ""
"X-Generator: Poedit 3.9\n" "X-Generator: Poedit 3.9\n"
msgid "Main Extruder" msgid "Main Extruder"
msgstr "" msgstr "主挤出机"
msgid "Main extruder" msgid "Main extruder"
msgstr "" msgstr "主挤出机"
msgid "main extruder" msgid "main extruder"
msgstr "" msgstr "主挤出机"
msgid "Auxiliary Extruder" msgid "Auxiliary Extruder"
msgstr "" msgstr "辅助挤出机"
msgid "Auxiliary extruder" msgid "Auxiliary extruder"
msgstr "" msgstr "辅助挤出机"
msgid "auxiliary extruder" msgid "auxiliary extruder"
msgstr "" msgstr "辅助挤出机"
msgid "Left Extruder" msgid "Left Extruder"
msgstr "左挤出机" msgstr "左挤出机"
@@ -54,22 +54,22 @@ msgid "right extruder"
msgstr "右侧挤出机" msgstr "右侧挤出机"
msgid "Main Nozzle" msgid "Main Nozzle"
msgstr "" msgstr "主喷嘴"
msgid "Main nozzle" msgid "Main nozzle"
msgstr "" msgstr "主喷嘴"
msgid "main nozzle" msgid "main nozzle"
msgstr "" msgstr "主喷嘴"
msgid "Auxiliary Nozzle" msgid "Auxiliary Nozzle"
msgstr "" msgstr "辅助喷嘴"
msgid "Auxiliary nozzle" msgid "Auxiliary nozzle"
msgstr "" msgstr "辅助喷嘴"
msgid "auxiliary nozzle" msgid "auxiliary nozzle"
msgstr "" msgstr "辅助喷嘴"
msgid "Left Nozzle" msgid "Left Nozzle"
msgstr "左喷嘴" msgstr "左喷嘴"
@@ -90,52 +90,52 @@ msgid "right nozzle"
msgstr "右喷嘴" msgstr "右喷嘴"
msgid "Main Hotend" msgid "Main Hotend"
msgstr "" msgstr "主热端"
msgid "Main hotend" msgid "Main hotend"
msgstr "" msgstr "主热端"
msgid "main hotend" msgid "main hotend"
msgstr "" msgstr "主热端"
msgid "Auxiliary Hotend" msgid "Auxiliary Hotend"
msgstr "" msgstr "辅助热端"
msgid "Auxiliary hotend" msgid "Auxiliary hotend"
msgstr "" msgstr "辅助热端"
msgid "auxiliary hotend" msgid "auxiliary hotend"
msgstr "" msgstr "辅助热端"
msgid "Left Hotend" msgid "Left Hotend"
msgstr "" msgstr "左热端"
msgid "Left hotend" msgid "Left hotend"
msgstr "" msgstr "左热端"
msgid "left hotend" msgid "left hotend"
msgstr "" msgstr "左热端"
msgid "Right Hotend" msgid "Right Hotend"
msgstr "" msgstr "右热端"
msgid "Right hotend" msgid "Right hotend"
msgstr "" msgstr "右热端"
msgid "right hotend" msgid "right hotend"
msgstr "" msgstr "右热端"
msgid "main" msgid "main"
msgstr "" msgstr ""
msgid "auxiliary" msgid "auxiliary"
msgstr "" msgstr "辅助"
msgid "Main" msgid "Main"
msgstr "" msgstr ""
msgid "Auxiliary" msgid "Auxiliary"
msgstr "" msgstr "辅助"
msgid "left" msgid "left"
msgstr "左" msgstr "左"
@@ -159,22 +159,22 @@ msgid "AMS does not support 'Bambu Lab PET-CF'."
msgstr "AMS不支持Bambu Lab PET-CF耗材。" msgstr "AMS不支持Bambu Lab PET-CF耗材。"
msgid "The current filament doesn't support the E3D high-flow nozzle and can't be used." msgid "The current filament doesn't support the E3D high-flow nozzle and can't be used."
msgstr "" msgstr "当前的耗材不支持 E3D 高流量喷嘴,因此无法使用。"
msgid "The current filament doesn't support the TPU high-flow nozzle and can't be used." msgid "The current filament doesn't support the TPU high-flow nozzle and can't be used."
msgstr "" msgstr "当前的耗材不支持 TPU 高流量喷嘴,因此无法使用。"
msgid "Auto dynamic flow calibration is not supported for TPU filament." msgid "Auto dynamic flow calibration is not supported for TPU filament."
msgstr "" msgstr "自动流量校准不支持 TPU 耗材"
msgid "Bambu TPU 85A is not supported for printing with 0.4 mm Standard or High Flow nozzles." msgid "Bambu TPU 85A is not supported for printing with 0.4 mm Standard or High Flow nozzles."
msgstr "" msgstr "Bambu TPU 85A 不支持使用 0.4 毫米标准或高流动性喷嘴进行打印。"
msgid "How to feed TPU filament." msgid "How to feed TPU filament."
msgstr "" msgstr "如何装入 TPU 耗材。"
msgid "How to feed TPU filament on X2D." msgid "How to feed TPU filament on X2D."
msgstr "" msgstr "如何为 X2D 装载 TPU 耗材。"
msgid "Please cold pull before printing TPU to avoid clogging. You may use cold pull maintenance on the printer." msgid "Please cold pull before printing TPU to avoid clogging. You may use cold pull maintenance on the printer."
msgstr "打印TPU前请先执行冷拉以避免堵塞。您可以使用打印机上的冷拉维护功能。" msgstr "打印TPU前请先执行冷拉以避免堵塞。您可以使用打印机上的冷拉维护功能。"
@@ -189,7 +189,7 @@ msgid "The rough surface of PLA Glow can accelerate wear on the AMS system, part
msgstr "泛光PLA的粗糙表面可能会加速AMS的磨损,尤其是AMS Lite的内部组件。" msgstr "泛光PLA的粗糙表面可能会加速AMS的磨损,尤其是AMS Lite的内部组件。"
msgid "PLA Glow may wear the AMS first stage feeder. Use an external spool instead." msgid "PLA Glow may wear the AMS first stage feeder. Use an external spool instead."
msgstr "" msgstr "PLA Glow 可能导致 AMS 一级进料器过度磨损,建议改用外置料盘。"
msgid "CF/GF filaments are hard and brittle, it's easy to break or get stuck in AMS, please use with caution." msgid "CF/GF filaments are hard and brittle, it's easy to break or get stuck in AMS, please use with caution."
msgstr "CF/GF耗材丝又硬又脆,在AMS中很容易断裂或卡住,请谨慎使用。" msgstr "CF/GF耗材丝又硬又脆,在AMS中很容易断裂或卡住,请谨慎使用。"
@@ -201,23 +201,23 @@ msgid "PPA-CF is brittle and could break in bended PTFE tube above Toolhead."
msgstr "PPA-CF材质较脆,用在工具头上方的弯曲PTFE管中可能会断裂。" msgstr "PPA-CF材质较脆,用在工具头上方的弯曲PTFE管中可能会断裂。"
msgid "Default settings may affect print quality. Adjust as needed for best results." msgid "Default settings may affect print quality. Adjust as needed for best results."
msgstr "" msgstr "默认参数可能影响打印质量,请根据需要进行调整,以获得最佳效果。"
#, c-format, boost-format #, c-format, boost-format
msgid "%s has a risk of nozzle clogging when using 0.4mm high-flow nozzles. Use with caution." msgid "%s has a risk of nozzle clogging when using 0.4mm high-flow nozzles. Use with caution."
msgstr "" msgstr "%s 在使用 0.4 mm 高流量喷嘴时存在堵头风险,谨慎使用。"
#, c-format, boost-format #, c-format, boost-format
msgid "%s filaments are hard and brittle and could break in AMS, and there is also a risk of nozzle clogging when using 0.4mm high-flow nozzles. Use with caution." msgid "%s filaments are hard and brittle and could break in AMS, and there is also a risk of nozzle clogging when using 0.4mm high-flow nozzles. Use with caution."
msgstr "" msgstr "%s 耗材质地较硬且较脆,在 AMS 中使用时可能发生断裂,使用 0.4 mm 高流量喷嘴时还存在堵头风险,谨慎使用。"
#, c-format, boost-format #, c-format, boost-format
msgid "%s has a risk of nozzle clogging when using 0.4, 0.6, 0.8mm high-flow nozzles. Use with caution." msgid "%s has a risk of nozzle clogging when using 0.4, 0.6, 0.8mm high-flow nozzles. Use with caution."
msgstr "" msgstr "%s 在使用 0.4 / 0.6 / 0.8 mm 高流量喷嘴时存在堵头风险,谨慎使用。"
#, c-format, boost-format #, c-format, boost-format
msgid "%s may fail to load or unload due to the Filament Track Switch. If you wish to continue." msgid "%s may fail to load or unload due to the Filament Track Switch. If you wish to continue."
msgstr "" msgstr "%s 可能因耗材变轨器而无法装入或退料。如果要继续。"
#, c-format, boost-format #, c-format, boost-format
msgid "%s is not supported by %s extruder." msgid "%s is not supported by %s extruder."
@@ -225,19 +225,19 @@ msgstr "%s 不被 %s 挤出机支持。"
#, c-format, boost-format #, c-format, boost-format
msgid "There may be critical print quality issues when printing '%s' with %s Bowden extruder. Use with caution!" msgid "There may be critical print quality issues when printing '%s' with %s Bowden extruder. Use with caution!"
msgstr "" msgstr "'%s' 在 %s 远程挤出机上打印时,可能会出现严重的打印质量问题。谨慎使用!"
#, c-format, boost-format #, c-format, boost-format
msgid "There may be critical print quality issues when printing '%s' with %s extruder. Use with caution!" msgid "There may be critical print quality issues when printing '%s' with %s extruder. Use with caution!"
msgstr "" msgstr "'%s' 在 %s 挤出机上打印时,可能会出现严重的打印质量问题。谨慎使用!"
#, c-format, boost-format #, c-format, boost-format
msgid "There may be print quality issues when printing '%s' with %s Bowden extruder. Use with caution." msgid "There may be print quality issues when printing '%s' with %s Bowden extruder. Use with caution."
msgstr "" msgstr "'%s' 在 %s 远程挤出机上打印时,可能会出现打印质量问题。谨慎使用。"
#, c-format, boost-format #, c-format, boost-format
msgid "There may be print quality issues when printing '%s' with %s extruder. Use with caution." msgid "There may be print quality issues when printing '%s' with %s extruder. Use with caution."
msgstr "" msgstr "'%s' 在 %s 挤出机上打印时,可能会出现打印质量问题。谨慎使用。"
msgid "High Flow" msgid "High Flow"
msgstr "高流量" msgstr "高流量"
@@ -282,7 +282,7 @@ msgid "During the hotend upgrade, the toolhead will move. Don't reach into the c
msgstr "在热端升级过程中,工具头会移动。请勿将手伸入机箱内。" msgstr "在热端升级过程中,工具头会移动。请勿将手伸入机箱内。"
msgid "Update" msgid "Update"
msgstr "" msgstr "更新"
msgid "Current AMS humidity" msgid "Current AMS humidity"
msgstr "当前AMS湿度" msgstr "当前AMS湿度"
@@ -345,7 +345,7 @@ msgid "Please wait"
msgstr "请稍候" msgstr "请稍候"
msgid "Reading" msgid "Reading"
msgstr "" msgstr "读取中"
msgid "Running..." msgid "Running..."
msgstr "运行中..." msgstr "运行中..."
@@ -369,7 +369,7 @@ msgid "Refresh"
msgstr "刷新" msgstr "刷新"
msgid "Refreshing" msgid "Refreshing"
msgstr "清爽" msgstr "刷新中"
msgid "Hotend status abnormal, unavailable at present. Please upgrade the firmware and try again." msgid "Hotend status abnormal, unavailable at present. Please upgrade the firmware and try again."
msgstr "热端状态异常,目前不可用。请升级固件后重试。" msgstr "热端状态异常,目前不可用。请升级固件后重试。"
@@ -645,7 +645,7 @@ msgstr "错误:请先关闭所有工具栏菜单"
msgctxt "inches" msgctxt "inches"
msgid "in" msgid "in"
msgstr "" msgstr "in"
msgid "mm" msgid "mm"
msgstr "mm" msgstr "mm"
@@ -718,13 +718,13 @@ msgid "Part"
msgstr "零件" msgstr "零件"
msgid "Relative" msgid "Relative"
msgstr "" msgstr "相对"
msgid "Coordinate system used for transform actions." msgid "Coordinate system used for transform actions."
msgstr "" msgstr "用于变换操作的坐标系"
msgid "Absolute" msgid "Absolute"
msgstr "" msgstr "绝对"
msgid "Reset current rotation to the value when open the rotation tool." msgid "Reset current rotation to the value when open the rotation tool."
msgstr "重置当前旋转为打开旋转工具时的值" msgstr "重置当前旋转为打开旋转工具时的值"
@@ -2191,31 +2191,39 @@ msgid "The version of Orca Slicer is too low and needs to be updated to the late
msgstr "此逆戟鲸切片器的版本过低,需更新至最新版本方可正常使用" msgstr "此逆戟鲸切片器的版本过低,需更新至最新版本方可正常使用"
msgid "Cloud sync conflict:" msgid "Cloud sync conflict:"
msgstr "" msgstr "云同步冲突:"
#, c-format, boost-format #, c-format, boost-format
msgid "Cloud sync conflict for preset \"%s\":" msgid "Cloud sync conflict for preset \"%s\":"
msgstr "" msgstr "预设 \"%s\" 云同步冲突:"
msgid "" msgid ""
"This preset has a newer version in OrcaCloud.\n" "This preset has a newer version in OrcaCloud.\n"
"Pull downloads the cloud copy. Force push overwrites it with your local preset." "Pull downloads the cloud copy. Force push overwrites it with your local preset."
msgstr "" msgstr ""
"该预设在 OrcaCloud 中有更新版本。\n"
"拉取会下载云端副本。强制推送会用您的本地预设覆盖该副本"
msgid "" msgid ""
"A preset with this name already exists in OrcaCloud.\n" "A preset with this name already exists in OrcaCloud.\n"
"Pull downloads the cloud copy. Force push overwrites it with your local preset." "Pull downloads the cloud copy. Force push overwrites it with your local preset."
msgstr "" msgstr ""
"OrcaCloud 中已经存在一个同名的预设。\n"
"拉取会下载云端副本。强制推送会用您的本地预设覆盖该副本。"
msgid "" msgid ""
"A preset with the same name was previously deleted from the cloud.\n" "A preset with the same name was previously deleted from the cloud.\n"
"Delete will delete your local preset. Force push overwrites it with your local preset." "Delete will delete your local preset. Force push overwrites it with your local preset."
msgstr "" msgstr ""
"之前已从云端删除了一个同名的预设。\n"
"删除将删除您的本地预设。强制推送会用您的本地预设覆盖该预设。"
msgid "" msgid ""
"There was an unexpected or unidentified preset conflict.\n" "There was an unexpected or unidentified preset conflict.\n"
"Pull downloads the cloud copy. Force push overwrites it with your local preset." "Pull downloads the cloud copy. Force push overwrites it with your local preset."
msgstr "" msgstr ""
"发生了一个意料之外或无法识别的预设冲突。\n"
"拉取会下载云端副本。强制推送会用您的本地预设覆盖该副本。"
msgid "" msgid ""
"Force push will overwrite the cloud copy with your local preset changes.\n" "Force push will overwrite the cloud copy with your local preset changes.\n"
@@ -2229,21 +2237,23 @@ msgid ""
"Force push will overwrite the cloud copy of preset \"%s\" with your local changes.\n" "Force push will overwrite the cloud copy of preset \"%s\" with your local changes.\n"
"Do you want to continue?" "Do you want to continue?"
msgstr "" msgstr ""
"强制推送将用您的本地更改覆盖预设 \"%s\" 的云端副本。\n"
"你希望继续吗?"
msgid "Resolve cloud sync conflict" msgid "Resolve cloud sync conflict"
msgstr "解决云同步冲突" msgstr "解决云同步冲突"
msgid "Syncing your account…" msgid "Syncing your account…"
msgstr "" msgstr "同步账号中..."
msgid "Migrating presets…" msgid "Migrating presets…"
msgstr "" msgstr "迁移预设中..."
msgid "Fetching plugins…" msgid "Fetching plugins…"
msgstr "" msgstr "获取插件中..."
msgid "Loading user presets…" msgid "Loading user presets…"
msgstr "" msgstr "加载用户预设中..."
msgid "Retrieving printer information, please try again later." msgid "Retrieving printer information, please try again later."
msgstr "正在获取打印机信息,请稍后重试。" msgstr "正在获取打印机信息,请稍后重试。"
@@ -2345,7 +2355,7 @@ msgid "Loading user preset"
msgstr "正在加载用户预设" msgstr "正在加载用户预设"
msgid "There is an update available. Open the preset bundle dialog to update it." msgid "There is an update available. Open the preset bundle dialog to update it."
msgstr "" msgstr "有更新可用。打开预设包对话框进行更新。"
#, c-format, boost-format #, c-format, boost-format
msgid "%s has been removed." msgid "%s has been removed."
@@ -2362,7 +2372,7 @@ msgstr "语言"
#, c-format, boost-format #, c-format, boost-format
msgid "Switching Orca Slicer to language %s failed." msgid "Switching Orca Slicer to language %s failed."
msgstr "" msgstr "切换 Orca Slicer 语言到 %s 失败。"
msgid "" msgid ""
"\n" "\n"
@@ -2370,25 +2380,27 @@ msgid ""
msgstr "" msgstr ""
msgid "Orca Slicer - Switching language failed" msgid "Orca Slicer - Switching language failed"
msgstr "" msgstr "Orca Slicer - 切换语言失败"
msgid "*" msgid "*"
msgstr "*" msgstr "*"
msgid "Plugins" msgid "Plugins"
msgstr "" msgstr "插件"
#, c-format, boost-format #, c-format, boost-format
msgid "" msgid ""
"Failed to open the Plugins dialog:\n" "Failed to open the Plugins dialog:\n"
"%s" "%s"
msgstr "" msgstr ""
"无法打开插件对话框:\n"
"%s"
msgid "Failed to open the Plugins dialog (unknown error)." msgid "Failed to open the Plugins dialog (unknown error)."
msgstr "" msgstr "无法打开插件对话框(未知错误)。"
msgid "Plugin Terminal" msgid "Plugin Terminal"
msgstr "" msgstr "插件终端"
msgid "Changing application language" msgid "Changing application language"
msgstr "正在为应用程序切换语言" msgstr "正在为应用程序切换语言"
@@ -2575,7 +2587,7 @@ msgid "OrcaSliced Combo"
msgstr "OrcaSliced 复合模型" msgstr "OrcaSliced 复合模型"
msgid "Orca Badge" msgid "Orca Badge"
msgstr "" msgstr "Orca 徽章"
msgid "Orca Tolerance Test" msgid "Orca Tolerance Test"
msgstr "Orca误差测试" msgstr "Orca误差测试"
@@ -2794,13 +2806,13 @@ msgid "Edit text"
msgstr "编辑文字" msgstr "编辑文字"
msgid "Ability to change text, font, size, ..." msgid "Ability to change text, font, size, ..."
msgstr "能够更改文本、字体、大小。。。" msgstr "能够更改文本、字体、大小 ..."
msgid "Edit SVG" msgid "Edit SVG"
msgstr "编辑SVG" msgstr "编辑SVG"
msgid "Change SVG source file, projection, size, ..." msgid "Change SVG source file, projection, size, ..."
msgstr "改变SVG源文件投影大小..." msgstr "改变SVG源文件投影大小 ..."
msgid "Invalidate cut info" msgid "Invalidate cut info"
msgstr "解除切割关系" msgstr "解除切割关系"
@@ -3021,43 +3033,43 @@ msgid "Click the icon to shift this object to the bed"
msgstr "点击这个图标可将对象移动到热床上" msgstr "点击这个图标可将对象移动到热床上"
msgid "Rename Object" msgid "Rename Object"
msgstr "" msgstr "重命名对象"
msgid "Rename Part" msgid "Rename Part"
msgstr "" msgstr "重命名零件"
msgid "Paste settings" msgid "Paste settings"
msgstr "" msgstr "粘贴设置"
msgid "Shift objects to bed" msgid "Shift objects to bed"
msgstr "" msgstr ""
msgid "Object order changed" msgid "Object order changed"
msgstr "" msgstr "对象顺序已更改"
msgid "Layer setting added" msgid "Layer setting added"
msgstr "" msgstr "层设置已添加"
msgid "Part setting added" msgid "Part setting added"
msgstr "" msgstr "零件设置已添加"
msgid "Object setting added" msgid "Object setting added"
msgstr "" msgstr "对象设置已添加"
msgid "Height range settings added" msgid "Height range settings added"
msgstr "" msgstr "高度范围设置已添加"
msgid "Part settings added" msgid "Part settings added"
msgstr "" msgstr "零件设置已添加"
msgid "Object settings added" msgid "Object settings added"
msgstr "" msgstr "对象设置已添加"
msgid "Load Part" msgid "Load Part"
msgstr "" msgstr "加载零件"
msgid "Load Modifier" msgid "Load Modifier"
msgstr "" msgstr "加载修改器"
msgid "Loading file" msgid "Loading file"
msgstr "载入文件中" msgstr "载入文件中"
@@ -3084,7 +3096,7 @@ msgid "Remove paint-on fuzzy skin"
msgstr "移除手绘绒毛表面" msgstr "移除手绘绒毛表面"
msgid "Delete Settings" msgid "Delete Settings"
msgstr "" msgstr "删除设置"
msgid "Remove height range" msgid "Remove height range"
msgstr "移除高度范围" msgstr "移除高度范围"
@@ -3119,7 +3131,7 @@ msgid "Deleting the last solid part is not allowed."
msgstr "不允许删除对象的最后一个实体零件。" msgstr "不允许删除对象的最后一个实体零件。"
msgid "Delete part" msgid "Delete part"
msgstr "" msgstr "删除零件"
msgid "The target object contains only one part and can not be split." msgid "The target object contains only one part and can not be split."
msgstr "目标对象只包含一个零件,无法分割" msgstr "目标对象只包含一个零件,无法分割"
@@ -3131,10 +3143,10 @@ msgid "Assembly"
msgstr "组合体" msgstr "组合体"
msgid "Merge parts to an object" msgid "Merge parts to an object"
msgstr "" msgstr "合并零件到对象"
msgid "Add layers" msgid "Add layers"
msgstr "" msgstr "添加层"
msgid "Cut Connectors information" msgid "Cut Connectors information"
msgstr "切割连接件信息" msgstr "切割连接件信息"
@@ -3170,7 +3182,7 @@ msgid "Settings for height range"
msgstr "高度范围设置" msgstr "高度范围设置"
msgid "Delete selected" msgid "Delete selected"
msgstr "" msgstr "删除选中"
msgid "Layer" msgid "Layer"
msgstr "层" msgstr "层"
@@ -3195,7 +3207,7 @@ msgid "Choose part type"
msgstr "选择零件类型" msgstr "选择零件类型"
msgid "Instances to Separated Objects" msgid "Instances to Separated Objects"
msgstr "" msgstr "实例拆分为对象"
msgid "Enter new name" msgid "Enter new name"
msgstr "输入新名称" msgstr "输入新名称"
@@ -3223,7 +3235,7 @@ msgid "\"%s\" part's mesh contains errors. Please repair it first."
msgstr "\"%s\" 零件的网格包含错误,请先修复。" msgstr "\"%s\" 零件的网格包含错误,请先修复。"
msgid "Change Filaments" msgid "Change Filaments"
msgstr "" msgstr "更改耗材"
msgid "Additional process preset" msgid "Additional process preset"
msgstr "附加工艺预设" msgstr "附加工艺预设"
@@ -3445,7 +3457,7 @@ msgid "Filament type is unknown which is required to perform this action. Please
msgstr "因耗材类型未知,所以需要执行此操作。请指定目标耗材的信息。" msgstr "因耗材类型未知,所以需要执行此操作。请指定目标耗材的信息。"
msgid "AMS has not been initialized. Please initialize it before use." msgid "AMS has not been initialized. Please initialize it before use."
msgstr "" msgstr "AMS 尚未初始化。请在使用前进行初始化。"
msgid "Changing fan speed during printing may affect print quality, please choose carefully." msgid "Changing fan speed during printing may affect print quality, please choose carefully."
msgstr "在打印过程中更改风扇速度可能影响打印质量,请谨慎选择。" msgstr "在打印过程中更改风扇速度可能影响打印质量,请谨慎选择。"
@@ -3570,22 +3582,22 @@ msgid "Check filament location"
msgstr "检查耗材丝位置" msgstr "检查耗材丝位置"
msgid "Switch" msgid "Switch"
msgstr "" msgstr "切换"
msgid "hotend" msgid "hotend"
msgstr "" msgstr "热端"
msgid "Wait for AMS cooling" msgid "Wait for AMS cooling"
msgstr "" msgstr "等待 AMS 冷却"
msgid "Switch current filament at Filament Track Switch" msgid "Switch current filament at Filament Track Switch"
msgstr "" msgstr "切换变轨器的当前耗材"
msgid "Pull back current filament at Filament Track Switch" msgid "Pull back current filament at Filament Track Switch"
msgstr "" msgstr "退回变轨器的当前耗材"
msgid "Switch track at Filament Track Switch" msgid "Switch track at Filament Track Switch"
msgstr "" msgstr "变轨器开关切换"
msgid "The maximum temperature cannot exceed " msgid "The maximum temperature cannot exceed "
msgstr "最高温度不可超过 " msgstr "最高温度不可超过 "
@@ -3640,7 +3652,7 @@ msgid "Launch troubleshoot center"
msgstr "启动故障排查中心" msgstr "启动故障排查中心"
msgid "Set nozzle count" msgid "Set nozzle count"
msgstr "" msgstr "设置喷嘴数量"
msgid "Please set nozzle count" msgid "Please set nozzle count"
msgstr "请设置喷嘴数量" msgstr "请设置喷嘴数量"
@@ -3693,7 +3705,7 @@ msgid "Ignore"
msgstr "忽略" msgstr "忽略"
msgid "Done." msgid "Done."
msgstr "" msgstr "完成"
msgid "" msgid ""
"All the selected objects are on a locked plate.\n" "All the selected objects are on a locked plate.\n"
@@ -4126,7 +4138,7 @@ msgid "Bed temperature"
msgstr "热床温度" msgstr "热床温度"
msgid "mm³" msgid "mm³"
msgstr "立方毫米" msgstr "mm³"
# TODO: Review, changed by lang refactor. PR 14254 # TODO: Review, changed by lang refactor. PR 14254
msgid "Start" msgid "Start"
@@ -7390,7 +7402,7 @@ msgid "Bottom"
msgstr "底部" msgstr "底部"
msgid "Plugin Selection" msgid "Plugin Selection"
msgstr "" msgstr "插件选择"
msgid "" msgid ""
"No plugins capabilities available for this type.\n" "No plugins capabilities available for this type.\n"
@@ -7401,13 +7413,13 @@ msgid "There is stringing-prone filament in the current print job. Enabling nozz
msgstr "" msgstr ""
msgid "Enable Nozzle Clumping Detection" msgid "Enable Nozzle Clumping Detection"
msgstr "" msgstr "裹头检测"
msgid "When enabled, the printer will automatically capture photos of printed parts and upload them to the cloud. Would you like to enable this option?" msgid "When enabled, the printer will automatically capture photos of printed parts and upload them to the cloud. Would you like to enable this option?"
msgstr "" msgstr "启用此功能后,打印机将自动拍摄打印件的照片并将其上传至云端。是否要启用此选项?"
msgid "Confirm Enable Print Status Snapshot" msgid "Confirm Enable Print Status Snapshot"
msgstr "" msgstr "确认启用打印状态快照"
msgid "Enable detection of build plate position" msgid "Enable detection of build plate position"
msgstr "启用打印板位置检测" msgstr "启用打印板位置检测"
@@ -7423,19 +7435,19 @@ msgid "Identifies the type and position of the build plate on the heatbed. Pausi
msgstr "识别打印平台板在热床上的类型和位置。如果检测到不匹配,则暂停打印。" msgstr "识别打印平台板在热床上的类型和位置。如果检测到不匹配,则暂停打印。"
msgid "Purifies the chamber air as the print finishes, based on the selected mode." msgid "Purifies the chamber air as the print finishes, based on the selected mode."
msgstr "" msgstr "每次打印结束时,根据所选方式净化打印机的腔体空气。"
msgid "Purifies the chamber air through internal circulation as each print finishes." msgid "Purifies the chamber air through internal circulation as each print finishes."
msgstr "" msgstr "每次打印结束时,通过内循环净化打印机的腔体空气。"
msgid "Automatically match the corresponding switch strategy for leak-prone filaments (disable blob detection) and regular filaments (enable blob detection)." msgid "Automatically match the corresponding switch strategy for leak-prone filaments (disable blob detection) and regular filaments (enable blob detection)."
msgstr "" msgstr "对易拉丝耗材(关闭裹头检测)与常规耗材(开启裹头检测)自动匹配相应的开关策略。"
msgid "Detect whether the nozzle is wrapped by filament or other foreign matter." msgid "Detect whether the nozzle is wrapped by filament or other foreign matter."
msgstr "" msgstr "检测喷嘴是否被耗材或其他异物包裹住。"
msgid "After disabling, nozzle wrapping cannot be detected, which may lead to print failure or nozzle damage." msgid "After disabling, nozzle wrapping cannot be detected, which may lead to print failure or nozzle damage."
msgstr "" msgstr "关闭后无法发现喷嘴包裹,可能导致打印失败或喷嘴损坏。"
msgid "AI Detections" msgid "AI Detections"
msgstr "人工智能检测" msgstr "人工智能检测"
@@ -7481,7 +7493,7 @@ msgid "Store Sent Files on External Storage"
msgstr "将发送的文件存储在外部存储器上" msgstr "将发送的文件存储在外部存储器上"
msgid "Save the printing files sent from the slicer and other apps on External Storage" msgid "Save the printing files sent from the slicer and other apps on External Storage"
msgstr "" msgstr "将切片软件和其他应用程序发送的打印文件保存到外部存储中"
msgid "Allow Prompt Sound" msgid "Allow Prompt Sound"
msgstr "允许提示音" msgstr "允许提示音"
@@ -7494,31 +7506,31 @@ msgid "Check if the nozzle is clumping by filament or other foreign objects."
msgstr "检查喷嘴是否被耗材丝或其他异物裹住" msgstr "检查喷嘴是否被耗材丝或其他异物裹住"
msgid "Purify Air at Print End" msgid "Purify Air at Print End"
msgstr "" msgstr "打印结束时净化空气"
msgid "Internal Circulation" msgid "Internal Circulation"
msgstr "" msgstr "内循环"
msgid "Alignment Detection" msgid "Alignment Detection"
msgstr "" msgstr "偏移检测"
msgid "Pauses printing when build plate misalignment is detected." msgid "Pauses printing when build plate misalignment is detected."
msgstr "" msgstr "若检测到打印板偏移,则暂停打印。"
msgid "Foreign Object Detection" msgid "Foreign Object Detection"
msgstr "" msgstr "异物检测"
msgid "Checks for any objects on the build plate at the start of a print to avoid collisions." msgid "Checks for any objects on the build plate at the start of a print to avoid collisions."
msgstr "" msgstr "打印开始时,检测打印板上是否有异物,避免打印过程中出现碰撞。"
msgid "Printed Part Displacement Detection" msgid "Printed Part Displacement Detection"
msgstr "" msgstr "打印件位移检测"
msgid "Monitors the printed part during printing and alerts immediately if it shifts or collapses." msgid "Monitors the printed part during printing and alerts immediately if it shifts or collapses."
msgstr "" msgstr "检测打印中的打印件状态,若发生移动或倒塌则立即提醒。"
msgid "Checks if the nozzle is clumping by filament or other foreign objects." msgid "Checks if the nozzle is clumping by filament or other foreign objects."
msgstr "" msgstr "检查喷嘴是否被耗材或其他异物堵塞。"
msgid "On" msgid "On"
msgstr "开" msgstr "开"
@@ -7533,10 +7545,10 @@ msgid "Pause printing"
msgstr "暂停打印" msgstr "暂停打印"
msgid "Print Status Snapshot" msgid "Print Status Snapshot"
msgstr "" msgstr "打印状态快照"
msgid "Automatically capture and upload print photos, showing defects during printing and the final result for remote viewing." msgid "Automatically capture and upload print photos, showing defects during printing and the final result for remote viewing."
msgstr "" msgstr "自动拍摄打印件照片并上传云端,包括打印过程中检测到的缺陷和任务结束时的打印件状态,便于远程查看。"
msgctxt "Nozzle Type" msgctxt "Nozzle Type"
msgid "Type" msgid "Type"
@@ -7560,7 +7572,7 @@ msgid "High flow"
msgstr "高流量" msgstr "高流量"
msgid "TPU High flow" msgid "TPU High flow"
msgstr "" msgstr "TPU 高流量"
msgid "No wiki link available for this printer." msgid "No wiki link available for this printer."
msgstr "没有适用于此打印机的 wiki 链接。" msgstr "没有适用于此打印机的 wiki 链接。"
@@ -7679,7 +7691,7 @@ msgid "Not installed"
msgstr "未安装" msgstr "未安装"
msgid "The software does not support using different diameter of nozzles for one print. If the left and right nozzles are inconsistent, we can only proceed with single-head printing. Please confirm which nozzle you would like to use for this project." msgid "The software does not support using different diameter of nozzles for one print. If the left and right nozzles are inconsistent, we can only proceed with single-head printing. Please confirm which nozzle you would like to use for this project."
msgstr "该软件不支持在一次打印中使用不同直径的喷嘴。如果左右喷不一致,就只能进行单头打印。请确认您想在该项目中使用哪种喷嘴。" msgstr "该软件不支持在一次打印中使用不同直径的喷嘴。如果左右喷不一致,就只能进行单头打印。请确认您想在该项目中使用哪种喷嘴。"
msgid "Switch diameter" msgid "Switch diameter"
msgstr "开关直径" msgstr "开关直径"
@@ -8002,19 +8014,19 @@ msgid ""
msgstr "您正尝试删除切割对象的一部分,这将破坏切割对应关系,删除之后,将无法再保证模型的一致性。" msgstr "您正尝试删除切割对象的一部分,这将破坏切割对应关系,删除之后,将无法再保证模型的一致性。"
msgid "Delete Object" msgid "Delete Object"
msgstr "" msgstr "删除对象"
msgid "Delete All Objects" msgid "Delete All Objects"
msgstr "" msgstr "删除所有对象"
msgid "Reset Project" msgid "Reset Project"
msgstr "" msgstr "重置项目"
msgid "The selected object couldn't be split." msgid "The selected object couldn't be split."
msgstr "选中的模型不可分裂。" msgstr "选中的模型不可分裂。"
msgid "Split to Objects" msgid "Split to Objects"
msgstr "" msgstr "拆分为对象"
msgid "Disable Auto-Drop to preserve Z positioning?\n" msgid "Disable Auto-Drop to preserve Z positioning?\n"
msgstr "禁用自动落板以保留 Z 轴定位?\n" msgstr "禁用自动落板以保留 Z 轴定位?\n"
@@ -8043,7 +8055,7 @@ msgid "File for the replacement wasn't selected"
msgstr "未选择替换文件" msgstr "未选择替换文件"
msgid "Replace with 3D file" msgid "Replace with 3D file"
msgstr "" msgstr "替换为 3D 文件"
msgid "Select folder to replace from" msgid "Select folder to replace from"
msgstr "选择要替换的文件夹" msgstr "选择要替换的文件夹"
@@ -8092,7 +8104,7 @@ msgid "Error during reload"
msgstr "重新加载时发生错误" msgstr "重新加载时发生错误"
msgid "Reload all" msgid "Reload all"
msgstr "" msgstr "重新加载全部"
msgid "There are warnings after slicing models:" msgid "There are warnings after slicing models:"
msgstr "模型切片警告:" msgstr "模型切片警告:"
@@ -9231,7 +9243,7 @@ msgid "Edit preset"
msgstr "编辑预设" msgstr "编辑预设"
msgid "Change extruder color" msgid "Change extruder color"
msgstr "" msgstr "更改挤出机颜色"
msgid "Unspecified" msgid "Unspecified"
msgstr "未指定" msgstr "未指定"
@@ -9457,10 +9469,10 @@ msgid "Your filament grouping method in the sliced file is not optimal."
msgstr "您在切片文件中的耗材丝分组方法不是最佳的。" msgstr "您在切片文件中的耗材丝分组方法不是最佳的。"
msgid "To ensure print quality, the drying temperature will be lowered during printing." msgid "To ensure print quality, the drying temperature will be lowered during printing."
msgstr "" msgstr "为保证打印质量,打印过程中会适当降低烘干温度。"
msgid "Select timelapse storage location" msgid "Select timelapse storage location"
msgstr "" msgstr "选择延时摄影存储位置"
msgid "Auto Bed Leveling" msgid "Auto Bed Leveling"
msgstr "自动床调平" msgstr "自动床调平"
@@ -9493,7 +9505,7 @@ msgstr ""
"*自动模式:打印前检查校准情况。如果不需要,请跳过。" "*自动模式:打印前检查校准情况。如果不需要,请跳过。"
msgid "Shared PA Profile" msgid "Shared PA Profile"
msgstr "" msgstr "共享 PA 预设"
msgid "Nozzles and filaments of the same type share the same PA profile." msgid "Nozzles and filaments of the same type share the same PA profile."
msgstr "" msgstr ""
@@ -9511,39 +9523,39 @@ msgid "Extra info"
msgstr "额外信息" msgstr "额外信息"
msgid "The Filament Track Switch installed on the printer does not match the slicing file. Please re-slice to avoid print quality issues." msgid "The Filament Track Switch installed on the printer does not match the slicing file. Please re-slice to avoid print quality issues."
msgstr "" msgstr "打印机上安装的耗材变轨器与切片文件不匹配。请重新切片以避免打印质量问题。"
msgid "This print requires a Filament Track Switch. Please install it first." msgid "This print requires a Filament Track Switch. Please install it first."
msgstr "" msgstr "此打印件需要耗材变轨器。请先安装它。"
msgid "The Filament Track Switch has not been setup. Please setup it first." msgid "The Filament Track Switch has not been setup. Please setup it first."
msgstr "" msgstr "变轨器尚未初始化,请先进行初始化。"
#, c-format, boost-format #, c-format, boost-format
msgid "Failed to send nozzle auto-mapping request to printer { code: %d }. Please try to refresh the printer information. If it still does not recover, you can try to rebind the printer and check the network connection." msgid "Failed to send nozzle auto-mapping request to printer { code: %d }. Please try to refresh the printer information. If it still does not recover, you can try to rebind the printer and check the network connection."
msgstr "" msgstr "向打印机发送喷嘴自动映射请求失败 {错误代码:%d}。请尝试刷新打印机信息。若问题仍未解决,可尝试重新绑定打印机并检查网络连接。"
msgid "The printer is calculating nozzle mapping." msgid "The printer is calculating nozzle mapping."
msgstr "" msgstr "打印机正在计算喷嘴映射。"
msgid "Please wait a moment..." msgid "Please wait a moment..."
msgstr "" msgstr "请稍等片刻..."
#, c-format, boost-format #, c-format, boost-format
msgid "Failed to receive nozzle auto-mapping table from printer { msg: %s }. Please refresh the printer information." msgid "Failed to receive nozzle auto-mapping table from printer { msg: %s }. Please refresh the printer information."
msgstr "" msgstr "未能从打印机接收喷嘴自动映射表 {消息:%s}。请刷新打印机信息。"
#, c-format, boost-format #, c-format, boost-format
msgid "The printer failed to build the nozzle auto-mapping table { code: %d }. Please refresh nozzle information." msgid "The printer failed to build the nozzle auto-mapping table { code: %d }. Please refresh nozzle information."
msgstr "" msgstr "打印机未能构建喷嘴自动映射表 { code: %d }。请刷新喷嘴信息。"
#, c-format, boost-format #, c-format, boost-format
msgid "The current nozzle mapping may produce an extra %0.2f g of waste." msgid "The current nozzle mapping may produce an extra %0.2f g of waste."
msgstr "" msgstr "当前的喷嘴映射可能会产生额外 %0.2f 克的废料。"
#, c-format, boost-format #, c-format, boost-format
msgid "Recommended filament arrangement saves %s->" msgid "Recommended filament arrangement saves %s->"
msgstr "" msgstr "如果按照推荐摆料可省 %s->"
#, c-format, boost-format #, c-format, boost-format
msgid "Filament %s does not match the filament in AMS slot %s. Please update the printer firmware to support AMS slot assignment." msgid "Filament %s does not match the filament in AMS slot %s. Please update the printer firmware to support AMS slot assignment."
@@ -9599,31 +9611,31 @@ msgstr "内部"
#, c-format, boost-format #, c-format, boost-format
msgid "%s space less than 20MB. Timelapse may not save properly. You can turn it off or" msgid "%s space less than 20MB. Timelapse may not save properly. You can turn it off or"
msgstr "" msgstr "%s空间不足 20MB,延时摄影可能无法正常存储,可选择关闭,或者"
msgid "Clean up files" msgid "Clean up files"
msgstr "" msgstr "清理文件"
msgid "Low internal storage. This timelapse will overwrite the oldest video files." msgid "Low internal storage. This timelapse will overwrite the oldest video files."
msgstr "" msgstr "机内存储空间不足,本次延时摄影将覆盖最早的视频文件。"
msgid "Low external storage. This timelapse will overwrite the oldest video files." msgid "Low external storage. This timelapse will overwrite the oldest video files."
msgstr "" msgstr "外部存储空间不足,本次延时摄影将覆盖最早的视频文件。"
msgid "Insufficient external storage for time-lapse photography. Connect to computer to delete files, or use a larger memory card." msgid "Insufficient external storage for time-lapse photography. Connect to computer to delete files, or use a larger memory card."
msgstr "" msgstr "外部存储空间不足,无法进行延时摄影,请将存储设备连接至电脑,手动删除不需要的文件,或更换大容量内存卡。"
msgid "Storage Space Not Enough" msgid "Storage Space Not Enough"
msgstr "" msgstr "存储空间不足"
msgid "Confirm & Print" msgid "Confirm & Print"
msgstr "" msgstr "好的,发起打印"
msgid "Cancel Timelapse & Print" msgid "Cancel Timelapse & Print"
msgstr "" msgstr "取消延时摄影,继续打印"
msgid "Clean Up" msgid "Clean Up"
msgstr "" msgstr "清理"
msgid "Preparing print job" msgid "Preparing print job"
msgstr "正在准备打印任务" msgstr "正在准备打印任务"
@@ -9640,32 +9652,32 @@ msgstr "喷嘴"
#, c-format, boost-format #, c-format, boost-format
msgid "Refreshing information of hotends(%d/%d)." msgid "Refreshing information of hotends(%d/%d)."
msgstr "" msgstr "正在刷新喷嘴信息(%d/%d)。"
msgid "There are not enough available hotends currently." msgid "There are not enough available hotends currently."
msgstr "" msgstr "目前可用的热端数量不足。"
msgid "Please complete the hotend rack setup and try again." msgid "Please complete the hotend rack setup and try again."
msgstr "" msgstr "请先进行热端挂架初始化后重试。"
msgid "Please refresh the nozzle information and try again." msgid "Please refresh the nozzle information and try again."
msgstr "" msgstr "请刷新喷嘴信息后重试。"
msgid "Please re-slice to avoid filament waste." msgid "Please re-slice to avoid filament waste."
msgstr "" msgstr "请重新切片以避免耗材浪费。"
msgid "The reported hotend information may be unreliable." msgid "The reported hotend information may be unreliable."
msgstr "" msgstr "报告的热端信息可能不可靠。"
#, c-format, boost-format #, c-format, boost-format
msgid "The printer has no nozzle matching the slicing file (%s)." msgid "The printer has no nozzle matching the slicing file (%s)."
msgstr "" msgstr "打印机没有与切片文件匹配的喷嘴(%s"
msgid "Please install a matching nozzle in the hotend rack, or set the corresponding printer preset while slicing." msgid "Please install a matching nozzle in the hotend rack, or set the corresponding printer preset while slicing."
msgstr "" msgstr "请在热端架上安装匹配的喷嘴,或在切片时设置相应的打印机预设。"
msgid "The toolhead and hotend rack are full. Please remove at least one hotend before printing." msgid "The toolhead and hotend rack are full. Please remove at least one hotend before printing."
msgstr "" msgstr "工具头和热端挂架已满,请卸下至少一个热端后再打印。"
#, c-format, boost-format #, c-format, boost-format
msgid "The nozzle flow setting of %s(%s) doesn't match with the slicing file(%s). Please make sure the nozzle installed matches with settings in printer, then set the corresponding printer preset while slicing." msgid "The nozzle flow setting of %s(%s) doesn't match with the slicing file(%s). Please make sure the nozzle installed matches with settings in printer, then set the corresponding printer preset while slicing."
@@ -11082,7 +11094,7 @@ msgid "Ext spool"
msgstr "外置耗材盘" msgstr "外置耗材盘"
msgid "Please check whether the nozzle type of the device is the same as the preset nozzle type." msgid "Please check whether the nozzle type of the device is the same as the preset nozzle type."
msgstr "请检查设备的喷类型是否与预设的喷类型相同。" msgstr "请检查设备的喷类型是否与预设的喷类型相同。"
msgid "Storage is not available or is in read-only mode." msgid "Storage is not available or is in read-only mode."
msgstr "存储不可用或处于只读模式。" msgstr "存储不可用或处于只读模式。"
@@ -20925,7 +20937,7 @@ msgstr ""
#~ msgstr "选择安装到正确喷嘴的耗材" #~ msgstr "选择安装到正确喷嘴的耗材"
#~ msgid "Select Filament && Hotends" #~ msgid "Select Filament && Hotends"
#~ msgstr "选择材料预设和喷" #~ msgstr "选择材料预设和喷"
#, c-format, boost-format #, c-format, boost-format
#~ msgid "Note: the filament type(%s) does not match with the filament type(%s) in the slicing file. If you want to use this slot, you can install %s instead of %s and change slot information on the 'Device' page." #~ msgid "Note: the filament type(%s) does not match with the filament type(%s) in the slicing file. If you want to use this slot, you can install %s instead of %s and change slot information on the 'Device' page."
@@ -21449,7 +21461,7 @@ msgstr ""
#~ msgstr "删除一个耳状Brim" #~ msgstr "删除一个耳状Brim"
#~ msgid "Adjust head diameter" #~ msgid "Adjust head diameter"
#~ msgstr "调整喷直径" #~ msgstr "调整喷直径"
#~ msgid "Adjust section view" #~ msgid "Adjust section view"
#~ msgstr "调整剖面视图" #~ msgstr "调整剖面视图"
File diff suppressed because it is too large Load Diff
+19 -2
View File
@@ -24,6 +24,8 @@
#include <iostream> #include <iostream>
#include <math.h> #include <math.h>
#include <csignal> #include <csignal>
#include <atomic>
#include <new>
#if defined(__linux__) || defined(__LINUX__) #if defined(__linux__) || defined(__LINUX__)
#include <condition_variable> #include <condition_variable>
@@ -7608,6 +7610,9 @@ LONG WINAPI VectoredExceptionHandler(PEXCEPTION_POINTERS pExceptionInfo)
}*/ }*/
#if defined(_MSC_VER) || defined(__MINGW32__) #if defined(_MSC_VER) || defined(__MINGW32__)
// Guards against a failed allocation inside the dump re-entering the new-handler.
static std::atomic<bool> g_dump_in_progress{false};
extern "C" { extern "C" {
__declspec(dllexport) int __stdcall orcaslicer_main(int argc, wchar_t **argv) __declspec(dllexport) int __stdcall orcaslicer_main(int argc, wchar_t **argv)
{ {
@@ -7626,9 +7631,21 @@ extern "C" {
//AddVectoredExceptionHandler(1, CBaseException::UnhandledExceptionFilter); //AddVectoredExceptionHandler(1, CBaseException::UnhandledExceptionFilter);
SET_DEFULTER_HANDLER(); SET_DEFULTER_HANDLER();
#endif #endif
// Dump before unwinding, while the stack still names what asked for the memory. Throwing
// std::bad_alloc is standard-permitted here and is what reaches generic_exception_handle().
std::set_new_handler([]() { std::set_new_handler([]() {
int *a = nullptr; if (!g_dump_in_progress.exchange(true)) {
*a = 0; try {
// A null EXCEPTION_POINTERS walks the calling thread as it stands.
CBaseException base(GetCurrentProcess(), GetCurrentProcessId(), NULL, nullptr);
base.ShowCallstack();
} catch (...) {
// A failed dump must not displace the std::bad_alloc owed to the caller.
}
// ObjParser recovers from std::bad_alloc, so let a later one dump again.
g_dump_in_progress = false;
}
throw std::bad_alloc();
}); });
// Call the UTF8 main. // Call the UTF8 main.
return CLI().run(argc, argv_ptrs.data()); return CLI().run(argc, argv_ptrs.data());
+7 -1
View File
@@ -57,6 +57,9 @@ double calculate_infill_rotation_angle(const PrintObject* object,
if (template_string.empty()) { if (template_string.empty()) {
return Geometry::deg2rad(fixed_infill_angle); return Geometry::deg2rad(fixed_infill_angle);
} }
// Convert the id to an index. Layer::id() counts the raft layers, object->layers() does not.
const size_t first_object_layer_id = object->get_layer(0)->id();
layer_id = layer_id > first_object_layer_id ? layer_id - first_object_layer_id : 0;
double angle = 0.0; double angle = 0.0;
ConfigOptionFloats rotate_angles; ConfigOptionFloats rotate_angles;
const std::string search_string = "/NnZz$LlUuQq~^|#"; const std::string search_string = "/NnZz$LlUuQq~^|#";
@@ -75,6 +78,8 @@ double calculate_infill_rotation_angle(const PrintObject* object,
double angle_start = 0; double angle_start = 0;
double limit_fill_z = object->get_layer(0)->bottom_z(); double limit_fill_z = object->get_layer(0)->bottom_z();
double start_fill_z = limit_fill_z; double start_fill_z = limit_fill_z;
// The raft height, or 0 without a raft.
const double print_z_offset = object->slicing_parameters().object_print_z_min;
bool _noop = false; bool _noop = false;
auto fill_form = std::string::npos; auto fill_form = std::string::npos;
bool _absolute = false; bool _absolute = false;
@@ -84,7 +89,8 @@ double calculate_infill_rotation_angle(const PrintObject* object,
for (int i = 0; i <= layer_id; i++) { for (int i = 0; i <= layer_id; i++) {
double fill_z = object->get_layer(i)->bottom_z(); double fill_z = object->get_layer(i)->bottom_z();
if (limit_fill_z < object->get_layer(i)->slice_z) { // slice_z is measured from the bottom of the model, limit_fill_z from the build plate.
if (limit_fill_z < object->get_layer(i)->slice_z + print_z_offset) {
if (repeats) { // if repeats >0 then restore parameters for new iteration if (repeats) { // if repeats >0 then restore parameters for new iteration
limit_fill_z += limit_fill_z - start_fill_z; limit_fill_z += limit_fill_z - start_fill_z;
start_fill_z = fill_z; start_fill_z = fill_z;
+41
View File
@@ -730,6 +730,42 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
return temp_set_by_gcode; return temp_set_by_gcode;
} }
struct CustomGCodeMotionStateChanges
{
bool acceleration = false;
bool jerk = false;
};
static bool custom_gcode_line_has_xy_parameter(const std::string &raw)
{
const size_t comment_pos = raw.find(';');
const std::string_view code(raw.data(), comment_pos == std::string::npos ? raw.size() : comment_pos);
return code.find_first_of("XxYy") != std::string_view::npos;
}
static CustomGCodeMotionStateChanges custom_gcode_motion_state_changes(const std::string &gcode)
{
CustomGCodeMotionStateChanges changes;
GCodeReader parser;
parser.parse_buffer(gcode, [&changes](GCodeReader &parser, const GCodeReader::GCodeLine &line) {
const std::string_view cmd = line.cmd();
if (boost::iequals(cmd, "M204") || boost::iequals(cmd, "M201") ||
boost::iequals(cmd, "M202"))
changes.acceleration = true;
else if ((boost::iequals(cmd, "M205") || boost::iequals(cmd, "M207") || boost::iequals(cmd, "M566")) &&
custom_gcode_line_has_xy_parameter(line.raw()))
changes.jerk = true;
else if (boost::iequals(cmd, "SET_VELOCITY_LIMIT")) {
changes.acceleration |= boost::icontains(line.raw(), "ACCEL=");
changes.jerk |= boost::icontains(line.raw(), "SQUARE_CORNER_VELOCITY=");
}
if (changes.acceleration && changes.jerk)
parser.quit_parsing();
});
return changes;
}
// BBS // BBS
// start_pos refers to the last position before the wipe_tower. // start_pos refers to the last position before the wipe_tower.
// end_pos refers to the wipe tower's start_pos. // end_pos refers to the wipe tower's start_pos.
@@ -4335,6 +4371,11 @@ PlaceholderParserIntegration &ppi = m_placeholder_parser_integration;
ppi.update_from_gcodewriter(m_writer); ppi.update_from_gcodewriter(m_writer);
std::string output = ppi.parser.process(templ, current_filament_id, config_override, &ppi.output_config, &ppi.context); std::string output = ppi.parser.process(templ, current_filament_id, config_override, &ppi.output_config, &ppi.context);
ppi.validate_output_vector_variables(); ppi.validate_output_vector_variables();
const CustomGCodeMotionStateChanges motion_state_changes = custom_gcode_motion_state_changes(output);
if (motion_state_changes.acceleration)
m_writer.invalidate_acceleration();
if (motion_state_changes.jerk)
m_writer.invalidate_jerk();
if (const std::vector<double> &pos = ppi.opt_position->values; ppi.position != pos) { if (const std::vector<double> &pos = ppi.opt_position->values; ppi.position != pos) {
// Update G-code writer. // Update G-code writer.
+15
View File
@@ -614,6 +614,8 @@ void GCodeProcessor::TimeMachine::calculate_time(GCodeProcessorResult& result, P
float leftover = 0.0f; float leftover = 0.0f;
for (size_t i = additional_buffer_idx; i < additional_buffer.size(); ++i) for (size_t i = additional_buffer_idx; i < additional_buffer.size(); ++i)
leftover += additional_buffer[i].second; leftover += additional_buffer[i].second;
BOOST_LOG_TRIVIAL(debug) << "calculate_time(is_final): leftover=" << leftover
<< "s from " << (additional_buffer.size() - additional_buffer_idx) << " items";
time += double(leftover); time += double(leftover);
gcode_time.cache += leftover; gcode_time.cache += leftover;
} else { } else {
@@ -3492,6 +3494,7 @@ void GCodeProcessor::reset()
m_extruder_blocks.clear(); m_extruder_blocks.clear();
m_machine_start_gcode_end_line_id = (unsigned int) (-1); m_machine_start_gcode_end_line_id = (unsigned int) (-1);
m_machine_end_gcode_start_line_id = (unsigned int) (-1); m_machine_end_gcode_start_line_id = (unsigned int) (-1);
m_skip_end_gcode_delays = false;
m_remaining_volume = std::vector<float>(MAXIMUM_EXTRUDER_NUMBER, 0.f); m_remaining_volume = std::vector<float>(MAXIMUM_EXTRUDER_NUMBER, 0.f);
m_line_id = 0; m_line_id = 0;
@@ -4219,6 +4222,14 @@ void GCodeProcessor::process_tags(const std::string_view comment, bool producers
return; return;
} }
// End gcode marker: skip post-print M400 S/P dwells after this point so the M73 estimate reports
// print-completion time, not post-print filtration/cooldown. BBS drops the same remainder in
// calculate_time(is_final).
if (comment == Machine_End_GCode_Start_Tag) {
m_skip_end_gcode_delays = true;
return;
}
// Orca: Integrate filament consumption for purging performed to an external device and controlled via macros // Orca: Integrate filament consumption for purging performed to an external device and controlled via macros
// (eg. Happy Hare) in the filament consumption stats. // (eg. Happy Hare) in the filament consumption stats.
if (boost::starts_with(comment, GCodeProcessor::External_Purge_Tag)) { if (boost::starts_with(comment, GCodeProcessor::External_Purge_Tag)) {
@@ -6401,6 +6412,10 @@ void GCodeProcessor::process_M400(const GCodeReader::GCodeLine& line)
float value_p = 0.0; float value_p = 0.0;
if (line.has_value('S', value_s) || line.has_value('P', value_p)) { if (line.has_value('S', value_s) || line.has_value('P', value_p)) {
value_s += value_p * 0.001; value_s += value_p * 0.001;
// Skip post-print end-gcode dwells so they don't inflate the M73 estimate (see
// m_skip_end_gcode_delays). Only omits dwell time — no state is updated here.
if (m_skip_end_gcode_delays)
return;
simulate_st_synchronize(value_s); simulate_st_synchronize(value_s);
} }
} }
+4
View File
@@ -1109,6 +1109,10 @@ class Print;
std::vector<ExtruderPreHeating::ExtruderUsageBlcok> m_extruder_blocks; std::vector<ExtruderPreHeating::ExtruderUsageBlcok> m_extruder_blocks;
unsigned int m_machine_start_gcode_end_line_id{ (unsigned int) (-1) }; unsigned int m_machine_start_gcode_end_line_id{ (unsigned int) (-1) };
unsigned int m_machine_end_gcode_start_line_id{ (unsigned int) (-1) }; unsigned int m_machine_end_gcode_start_line_id{ (unsigned int) (-1) };
// Set when the MACHINE_END_GCODE_START tag is seen during the streaming parse; tells
// process_M400 to skip post-print end-gcode dwells (air purification, timelapse, sound)
// so they don't inflate the M73 estimate. BBS excludes them in calculate_time(is_final).
bool m_skip_end_gcode_delays{ false };
// Tracks, during the stream, which filament sits in each physical nozzle and which nozzle each // Tracks, during the stream, which filament sits in each physical nozzle and which nozzle each
// extruder currently carries. Written by both branches of the two-arg process_filament_change // extruder currently carries. Written by both branches of the two-arg process_filament_change
// (the fallback branch does occupancy bookkeeping only); read by the richer change-time model // (the fallback branch does occupancy bookkeeping only); read by the richer change-time model
+10
View File
@@ -1091,6 +1091,16 @@ namespace Slic3r
if (layer + 1 < layer_filaments.size()) next_lf = layer_filaments[layer + 1]; if (layer + 1 < layer_filaments.size()) next_lf = layer_filaments[layer + 1];
std::vector<unsigned int> filament_used_next_layer = collect_filaments_in_groups<unsigned int>(filament_sets, next_lf); std::vector<unsigned int> filament_used_next_layer = collect_filaments_in_groups<unsigned int>(filament_sets, next_lf);
// Enable inter-layer forecast: when choosing filament ordering for current layer,
// also consider next layer's filament set to minimize inter-layer transition flush.
// solve_extruder_order_with_forcast() tries all permutations of curr+next layer
// and picks the ordering that minimizes total flush across both layers.
// This avoids expensive inter-layer transitions (e.g. ending layer with F2 when
// next layer starts with F3, costing flush[F2→F3], instead of ending with F3
// which gives flush[F3→F3]=0). Limited to ≤5 filaments due to O(N!×M!) complexity.
// The per-nozzle base reorder does not use the inter-layer forecast. This function drives
// BBL multi-extruder grouping cost and H2C ordering, so keeping it false avoids perturbing
// existing H2D/H2C output.
bool use_forcast = false; bool use_forcast = false;
float tmp_cost = 0; float tmp_cost = 0;
std::vector<unsigned int> sequence; std::vector<unsigned int> sequence;
+196 -18
View File
@@ -1609,6 +1609,64 @@ void WipeTower::set_extruder(size_t idx, const PrintConfig& config)
if (max_vol_speed!= 0.f) if (max_vol_speed!= 0.f)
m_filpar[idx].max_e_speed = (max_vol_speed / filament_area()); m_filpar[idx].max_e_speed = (max_vol_speed / filament_area());
// Vortek H2C: carousel-specific ramming, precool, and reverse travel parameters
{
// Ramming speed: .first = extruder change, .second = nozzle change (carousel)
// Use the dedicated ramming volumetric speed, falling back to max_vol_speed only when
// the setting is nil/-1.
float ramming_vol_speed = float(config.filament_ramming_volumetric_speed.get_at(idx));
if (config.filament_ramming_volumetric_speed.is_nil(idx) || is_approx(config.filament_ramming_volumetric_speed.get_at(idx), -1.))
ramming_vol_speed = max_vol_speed;
m_filpar[idx].max_e_ramming_speed.first = (ramming_vol_speed / filament_area());
float ramming_vol_speed_nc = float(config.filament_ramming_volumetric_speed_nc.get_at(idx));
if (config.filament_ramming_volumetric_speed_nc.is_nil(idx) || is_approx(config.filament_ramming_volumetric_speed_nc.get_at(idx), -1.))
ramming_vol_speed_nc = max_vol_speed;
m_filpar[idx].max_e_ramming_speed.second = (ramming_vol_speed_nc / filament_area());
}
{
// Precool target temp: .first = extruder change, .second = nozzle change (carousel)
// Precool is only active when enable_pre_heating is on; otherwise no precool temp/timing is
// applied and the downstream precool_t stays 0, matching printers with pre-heating disabled.
m_filpar[idx].precool_target_temp = {0, 0};
if (config.enable_pre_heating.value) {
if (!config.filament_pre_cooling_temperature.is_nil(idx) && config.filament_pre_cooling_temperature.get_at(idx) != 0)
m_filpar[idx].precool_target_temp.first = config.filament_pre_cooling_temperature.get_at(idx);
if (!config.filament_pre_cooling_temperature_nc.is_nil(idx) && config.filament_pre_cooling_temperature_nc.get_at(idx) != 0)
m_filpar[idx].precool_target_temp.second = config.filament_pre_cooling_temperature_nc.get_at(idx);
}
}
{
// Precool timing: (nozzle_temp - precool_temp) / hotend_cooling_rate
int extruder_count = m_is_multi_extruder ? 2 : 1; // H2C = 2 extruders
float nozzle_temp = float(config.nozzle_temperature.is_nil(idx) ? 0 : config.nozzle_temperature.get_at(idx));
float nozzle_temp_fl = float(config.nozzle_temperature_initial_layer.is_nil(idx) ? nozzle_temp : config.nozzle_temperature_initial_layer.get_at(idx));
m_filpar[idx].precool_t.first.resize(extruder_count, 0.f);
m_filpar[idx].precool_t.second.resize(extruder_count, 0.f);
m_filpar[idx].precool_t_first_layer.first.resize(extruder_count, 0.f);
m_filpar[idx].precool_t_first_layer.second.resize(extruder_count, 0.f);
std::vector<double> cooling_rates = config.hotend_cooling_rate.values;
for (int i = 0; i < extruder_count && i < (int)cooling_rates.size(); i++) {
if (cooling_rates[i] < EPSILON) continue;
if (m_filpar[idx].precool_target_temp.first != 0) {
m_filpar[idx].precool_t.first[i] = std::max(0.f, nozzle_temp - float(m_filpar[idx].precool_target_temp.first)) / float(cooling_rates[i]);
m_filpar[idx].precool_t_first_layer.first[i] = std::max(0.f, nozzle_temp_fl - float(m_filpar[idx].precool_target_temp.first)) / float(cooling_rates[i]);
}
if (m_filpar[idx].precool_target_temp.second != 0) {
m_filpar[idx].precool_t.second[i] = std::max(0.f, nozzle_temp - float(m_filpar[idx].precool_target_temp.second)) / float(cooling_rates[i]);
m_filpar[idx].precool_t_first_layer.second[i] = std::max(0.f, nozzle_temp_fl - float(m_filpar[idx].precool_target_temp.second)) / float(cooling_rates[i]);
}
}
}
{
// Ramming travel time: .first = extruder change, .second = nozzle change (carousel)
m_filpar[idx].ramming_travel_time = {0.f, 0.f};
if (!config.filament_ramming_travel_time.is_nil(idx))
m_filpar[idx].ramming_travel_time.first = float(config.filament_ramming_travel_time.get_at(idx));
if (!config.filament_ramming_travel_time_nc.is_nil(idx))
m_filpar[idx].ramming_travel_time.second = float(config.filament_ramming_travel_time_nc.get_at(idx));
}
m_perimeter_width = nozzle_diameter * Width_To_Nozzle_Ratio; // all extruders are now assumed to have the same diameter m_perimeter_width = nozzle_diameter * Width_To_Nozzle_Ratio; // all extruders are now assumed to have the same diameter
m_nozzle_change_perimeter_width = 2*m_perimeter_width; m_nozzle_change_perimeter_width = 2*m_perimeter_width;
// BBS: remove useless config // BBS: remove useless config
@@ -1893,7 +1951,7 @@ WipeTower::NozzleChangeResult WipeTower::nozzle_change(int old_filament_id, int
.set_initial_tool(m_current_tool) .set_initial_tool(m_current_tool)
.set_extrusion_flow(m_extrusion_flow) .set_extrusion_flow(m_extrusion_flow)
.set_y_shift(m_y_shift + (new_filament_id != (unsigned int) (-1) && (m_current_shape == SHAPE_REVERSED) ? m_layer_info->depth - m_layer_info->toolchanges_depth() : 0.f)) .set_y_shift(m_y_shift + (new_filament_id != (unsigned int) (-1) && (m_current_shape == SHAPE_REVERSED) ? m_layer_info->depth - m_layer_info->toolchanges_depth() : 0.f))
.append("; Nozzle change start\n"); .append(format_nozzle_change_tag(true, old_filament_id, new_filament_id));
box_coordinates cleaning_box(Vec2f(m_perimeter_width, m_perimeter_width), m_wipe_tower_width - 2 * m_perimeter_width, box_coordinates cleaning_box(Vec2f(m_perimeter_width, m_perimeter_width), m_wipe_tower_width - 2 * m_perimeter_width,
(new_filament_id != (unsigned int) (-1) ? wipe_depth + m_depth_traversed - m_perimeter_width : m_wipe_tower_depth - m_perimeter_width)); (new_filament_id != (unsigned int) (-1) ? wipe_depth + m_depth_traversed - m_perimeter_width : m_wipe_tower_depth - m_perimeter_width));
@@ -1969,7 +2027,7 @@ WipeTower::NozzleChangeResult WipeTower::nozzle_change(int old_filament_id, int
} }
} }
writer.append("; Nozzle change end\n"); writer.append(format_nozzle_change_tag(false, old_filament_id, new_filament_id));
result.start_pos = writer.start_pos_rotated(); result.start_pos = writer.start_pos_rotated();
result.end_pos = writer.pos(); result.end_pos = writer.pos();
@@ -2546,6 +2604,19 @@ void WipeTower::plan_toolchange(float z_par, float layer_height_par, unsigned in
nozzle_change_depth = nozzle_change_line_count * m_nozzle_change_perimeter_width; nozzle_change_depth = nozzle_change_line_count * m_nozzle_change_perimeter_width;
depth += nozzle_change_depth; depth += nozzle_change_depth;
} }
if (nozzle_change_depth == 0
&& !m_filament_nozzle_map.empty()
&& old_tool < m_filament_nozzle_map.size() && new_tool < m_filament_nozzle_map.size()
&& m_filament_nozzle_map[old_tool] != m_filament_nozzle_map[new_tool]) {
double e_flow = nozzle_change_extrusion_flow(layer_height_par);
double length = m_filaments_change_length[old_tool] / e_flow;
int nozzle_change_line_count = length / (m_wipe_tower_width - 2*m_nozzle_change_perimeter_width) + 1;
if (has_tpu_filament())
nozzle_change_depth = m_tpu_fixed_spacing * nozzle_change_line_count * m_nozzle_change_perimeter_width;
else
nozzle_change_depth = nozzle_change_line_count * m_nozzle_change_perimeter_width;
depth += nozzle_change_depth;
}
WipeTowerInfo::ToolChange tool_change = WipeTowerInfo::ToolChange(old_tool, new_tool, depth, 0.f, 0.f, wipe_volume, length_to_extrude, purge_volume); WipeTowerInfo::ToolChange tool_change = WipeTowerInfo::ToolChange(old_tool, new_tool, depth, 0.f, 0.f, wipe_volume, length_to_extrude, purge_volume);
tool_change.nozzle_change_depth = nozzle_change_depth; tool_change.nozzle_change_depth = nozzle_change_depth;
m_plan.back().tool_changes.push_back(tool_change); m_plan.back().tool_changes.push_back(tool_change);
@@ -2695,6 +2766,13 @@ bool WipeTower::is_petg_filament(int filament_id) const
return m_filpar[filament_id].material == "PETG"; return m_filpar[filament_id].material == "PETG";
} }
bool WipeTower::is_need_reverse_travel(int filament_id, bool extruder_change) const
{
if (extruder_change)
return m_filpar[filament_id].ramming_travel_time.first > EPSILON;
return m_filpar[filament_id].ramming_travel_time.second > EPSILON;
}
// BBS: consider both soluable and support properties // BBS: consider both soluable and support properties
// Return index of first toolchange that switches to non-soluble and non-support extruder // Return index of first toolchange that switches to non-soluble and non-support extruder
// ot -1 if there is no such toolchange. // ot -1 if there is no such toolchange.
@@ -2819,6 +2897,13 @@ WipeTower::ToolChangeResult WipeTower::tool_change_new(size_t new_tool, bool sol
&& is_valid_last_layer(m_current_tool, m_cur_layer_id, m_z_pos)) { && is_valid_last_layer(m_current_tool, m_cur_layer_id, m_z_pos)) {
m_nozzle_change_result = nozzle_change_new(m_current_tool, new_tool, solid_nozzlechange); m_nozzle_change_result = nozzle_change_new(m_current_tool, new_tool, solid_nozzlechange);
} }
if (m_nozzle_change_result.gcode.empty()
&& !m_filament_nozzle_map.empty()
&& m_current_tool < m_filament_nozzle_map.size() && new_tool < m_filament_nozzle_map.size()
&& m_filament_nozzle_map[m_current_tool] != m_filament_nozzle_map[new_tool]
&& is_valid_last_layer(m_current_tool, m_cur_layer_id, m_z_pos)) {
m_nozzle_change_result = nozzle_change_new(m_current_tool, new_tool, solid_nozzlechange);
}
size_t old_tool = m_current_tool; size_t old_tool = m_current_tool;
float wipe_depth = 0.f; float wipe_depth = 0.f;
@@ -2983,20 +3068,40 @@ WipeTower::NozzleChangeResult WipeTower::nozzle_change_new(int old_filament_id,
} }
float nz_extrusion_flow = nozzle_change_extrusion_flow(m_layer_height); float nz_extrusion_flow = nozzle_change_extrusion_flow(m_layer_height);
float nozzle_change_speed = 60.0f * m_filpar[m_current_tool].max_e_speed / nz_extrusion_flow; bool extruder_change = !is_in_same_extruder(old_filament_id, new_filament_id);
nozzle_change_speed = solid_infill ? 40.f * 60.f : nozzle_change_speed;//If the contact layers belong to different categories, then reduce the speed. float max_e_ramming = extruder_change
? m_filpar[m_current_tool].max_e_ramming_speed.first
: m_filpar[m_current_tool].max_e_ramming_speed.second;
if (max_e_ramming < EPSILON) max_e_ramming = m_filpar[m_current_tool].max_e_speed; // fallback
float nozzle_change_speed = 60.0f * max_e_ramming / nz_extrusion_flow;
nozzle_change_speed = solid_infill ? 40.f * 60.f : nozzle_change_speed;
if (is_tpu_filament(m_current_tool)) { if (is_tpu_filament(m_current_tool)) {
nozzle_change_speed *= 0.25; nozzle_change_speed *= 0.25;
} }
float bridge_speed = std::min(60.0f * m_filpar[m_current_tool].max_e_speed / nozzle_change_extrusion_flow(0.2), nozzle_change_speed); // limit the bridge speed by add flow float bridge_speed = std::min(60.0f * max_e_ramming / nozzle_change_extrusion_flow(0.2), nozzle_change_speed);
WipeTowerWriter writer(m_layer_height, m_nozzle_change_perimeter_width, m_gcode_flavor, m_filpar); WipeTowerWriter writer(m_layer_height, m_nozzle_change_perimeter_width, m_gcode_flavor, m_filpar);
writer.set_extrusion_flow(nz_extrusion_flow) writer.set_extrusion_flow(nz_extrusion_flow)
.set_z(m_z_pos) .set_z(m_z_pos)
.set_initial_tool(m_current_tool) .set_initial_tool(m_current_tool)
.set_y_shift(m_y_shift + (new_filament_id != (unsigned int) (-1) && (m_current_shape == SHAPE_REVERSED) ? m_layer_info->depth - m_layer_info->toolchanges_depth() : 0.f)) .set_y_shift(m_y_shift + (new_filament_id != (unsigned int) (-1) && (m_current_shape == SHAPE_REVERSED) ? m_layer_info->depth - m_layer_info->toolchanges_depth() : 0.f))
.append("; Nozzle change start\n"); .append(format_nozzle_change_tag(true, old_filament_id, new_filament_id));
if (!extruder_change && m_is_multiple_nozzle) {
writer.append("M632 S" + std::to_string(new_filament_id) + " M N\n");
// Use m_physical_extruder_map for heater index (matches format_line_M104 in add_M104_by_requirement)
if (m_filpar[m_current_tool].precool_target_temp.second != 0) {
int logical_ext = m_filament_map.empty() ? 0 : m_filament_map[m_current_tool] - 1;
int phys_ext = (logical_ext >= 0 && logical_ext < (int)m_physical_extruder_map.size())
? m_physical_extruder_map[logical_ext] : logical_ext;
writer.append("M400\n");
writer.append("M104 T" + std::to_string(phys_ext) + " S" +
std::to_string(m_filpar[m_current_tool].precool_target_temp.second) + " N0\n");
writer.append("M106 S255\n");
}
writer.append("M633\n");
}
WipeTowerBlock* block = get_block_by_category(m_filpar[old_filament_id].category, false); WipeTowerBlock* block = get_block_by_category(m_filpar[old_filament_id].category, false);
if (!block) { if (!block) {
@@ -3021,6 +3126,23 @@ WipeTower::NozzleChangeResult WipeTower::nozzle_change_new(int old_filament_id,
dy = solid_infill ? m_nozzle_change_perimeter_width : dy; dy = solid_infill ? m_nozzle_change_perimeter_width : dy;
nozzle_change_line_count = solid_infill ? std::numeric_limits<int>::max() : nozzle_change_line_count; nozzle_change_line_count = solid_infill ? std::numeric_limits<int>::max() : nozzle_change_line_count;
m_left_to_right = true; m_left_to_right = true;
if (extruder_change) {
float ramming_length = nozzle_change_line_count * (xr - xl);
int extruder_id = m_filament_map.empty() ? 0 : m_filament_map[m_current_tool] - 1;
float precool_t = (extruder_id >= 0 && extruder_id < (int)m_filpar[m_current_tool].precool_t.first.size())
? m_filpar[m_current_tool].precool_t.first[extruder_id] : 0.f;
float precool_t_fl = (extruder_id >= 0 && extruder_id < (int)m_filpar[m_current_tool].precool_t_first_layer.first.size())
? m_filpar[m_current_tool].precool_t_first_layer.first[extruder_id] : 0.f;
float per_cooling_max_speed = nozzle_change_speed;
if (is_first_layer() && precool_t_fl > EPSILON)
per_cooling_max_speed = ramming_length / precool_t_fl * 60.f;
else if (precool_t > EPSILON)
per_cooling_max_speed = ramming_length / precool_t * 60.f;
if (nozzle_change_speed > per_cooling_max_speed) nozzle_change_speed = per_cooling_max_speed;
if (bridge_speed > per_cooling_max_speed) bridge_speed = per_cooling_max_speed;
}
int real_nozzle_change_line_count = 0; int real_nozzle_change_line_count = 0;
bool need_change_flow = false; bool need_change_flow = false;
for (int i = 0; true; ++i) { for (int i = 0; true; ++i) {
@@ -3053,9 +3175,40 @@ WipeTower::NozzleChangeResult WipeTower::nozzle_change_new(int old_filament_id,
block->last_nozzle_change_id = old_filament_id; block->last_nozzle_change_id = old_filament_id;
NozzleChangeResult result; NozzleChangeResult result;
if (is_tpu_filament(m_current_tool)) { if (!extruder_change && m_is_multiple_nozzle) {
writer.append("M632 S" + std::to_string(new_filament_id) + " M N\n");
}
if (is_need_reverse_travel(m_current_tool, extruder_change)) {
bool left_to_right = !m_left_to_right; bool left_to_right = !m_left_to_right;
int tpu_line_count = (real_nozzle_change_line_count + 2 - 1) / 2; // nozzle_change_line_count / 2 round up int tpu_line_count = real_nozzle_change_line_count;
float reverse_speed = nozzle_change_speed * 2; // reverse travel runs at double the nozzle-change speed
float rt_time = extruder_change ? m_filpar[m_current_tool].ramming_travel_time.first
: m_filpar[m_current_tool].ramming_travel_time.second;
float need_reverse_travel_dis = rt_time * reverse_speed / 60.f;
float real_travel_dis = tpu_line_count * (xr - xl - 2 * m_perimeter_width);
if (real_travel_dis < need_reverse_travel_dis)
reverse_speed *= real_travel_dis / need_reverse_travel_dis;
writer.travel(writer.x(), writer.y() + dy/2);
for (int i = 0; true; ++i) {
need_reverse_travel_dis -= (xr - xl - 2 * m_perimeter_width);
float offset_dis = 0.f;
if (need_reverse_travel_dis < 0)
offset_dis = -need_reverse_travel_dis;
if (left_to_right)
writer.travel(xr - m_perimeter_width - offset_dis, writer.y(), reverse_speed);
else
writer.travel(xl + m_perimeter_width + offset_dis, writer.y(), reverse_speed);
if (need_reverse_travel_dis < EPSILON) break;
if (i == tpu_line_count - 1)
break;
writer.travel(writer.x(), writer.y() - dy);
left_to_right = !left_to_right;
}
} else if (is_tpu_filament(m_current_tool)) {
bool left_to_right = !m_left_to_right;
int tpu_line_count = (real_nozzle_change_line_count + 2 - 1) / 2;
nozzle_change_speed *= 2; nozzle_change_speed *= 2;
writer.travel(writer.x(), writer.y() - m_nozzle_change_perimeter_width); writer.travel(writer.x(), writer.y() - m_nozzle_change_perimeter_width);
@@ -3080,12 +3233,15 @@ WipeTower::NozzleChangeResult WipeTower::nozzle_change_new(int old_filament_id,
} }
} }
writer.append("; Nozzle change end\n"); if (!extruder_change && m_is_multiple_nozzle) writer.append("M633\n");
writer.append(format_nozzle_change_tag(false, old_filament_id, new_filament_id));
result.start_pos = writer.start_pos_rotated(); result.start_pos = writer.start_pos_rotated();
result.origin_start_pos = initial_position; result.origin_start_pos = initial_position;
result.end_pos = writer.pos_rotated(); result.end_pos = writer.pos_rotated();
result.gcode = writer.gcode(); result.gcode = writer.gcode();
result.is_extruder_change = extruder_change;
return result; return result;
} }
@@ -3510,25 +3666,22 @@ void WipeTower::toolchange_wipe_new(WipeTowerWriter &writer, const box_coordinat
// H). The counterproductive fan-on (M106 S255) used for departing-tool cooldown is intentionally // H). The counterproductive fan-on (M106 S255) used for departing-tool cooldown is intentionally
// omitted, since this is a pre-HEAT of the arriving tool. The whole helper is only ever called from // omitted, since this is a pre-HEAT of the arriving tool. The whole helper is only ever called from
// add_M104_by_requirement, which is gated on m_is_multiple_nozzle (extruder_max_nozzle_count>1) => H2C // add_M104_by_requirement, which is gated on m_is_multiple_nozzle (extruder_max_nozzle_count>1) => H2C
// only; every other printer's wipe tower is untouched. The M632 M-flag is itself a firmware barrier, so // only; every other printer's wipe tower is untouched.
// a preceding M400 wait is subsumed. // BBS: extruder change preheat uses M400 + M104 WITHOUT M632/M633 barrier.
// M632 barriers are only for carousel nozzle changes (emitted in nozzle_change_new/ramming).
auto format_line_M104 = [this](int target_temp, int target_extruder = -1, bool wait_for_moves = true, const std::string &comment = "") { auto format_line_M104 = [this](int target_temp, int target_extruder = -1, bool wait_for_moves = true, const std::string &comment = "") {
std::string buffer; std::string buffer;
buffer += "M632 S" + std::to_string(m_current_tool) + " M N\n"; if (wait_for_moves)
buffer += "M400\n";
buffer += "M104"; buffer += "M104";
if (target_extruder != -1 && target_extruder < (int) m_physical_extruder_map.size()) if (target_extruder != -1 && target_extruder < (int) m_physical_extruder_map.size())
buffer += (" T" + std::to_string(m_physical_extruder_map[target_extruder])); buffer += (" T" + std::to_string(m_physical_extruder_map[target_extruder]));
buffer += " S" + std::to_string(target_temp) + " N0"; // N0 means the gcode is generated by the slicer buffer += " S" + std::to_string(target_temp) + " N0"; // N0 means the gcode is generated by the slicer
if (!comment.empty()) buffer += " ;" + comment; if (!comment.empty()) buffer += " ;" + comment;
buffer += '\n'; buffer += '\n';
buffer += "M633\n";
(void) wait_for_moves; // the M632 M-flag barrier replaces the former M400 wait
return buffer; return buffer;
}; };
// Suppress the pre-heat M104 on the first layer and on solid (contact) toolchanges (should_heating). // m_is_multiple_nozzle gate needed because Orca calls toolchange_wipe_new for ALL printers (BBS has it H2C-only).
// m_is_multiple_nozzle folds in the H2C gate so single-nozzle output is untouched.
// Orca: the arriving extruder id is resolved as m_filament_map[tool]-1 (layer-static) because Orca's
// wipe tower is extruder-level rather than tracking a per-layer nozzle map.
bool should_heating = m_is_multiple_nozzle && m_filpar[m_current_tool].filament_cooling_before_tower > EPSILON && bool should_heating = m_is_multiple_nozzle && m_filpar[m_current_tool].filament_cooling_before_tower > EPSILON &&
!solid_tool_toolchange && !is_first_layer(); !solid_tool_toolchange && !is_first_layer();
auto add_M104_by_requirement = [&writer, &format_line_M104, &should_heating, this]() { auto add_M104_by_requirement = [&writer, &format_line_M104, &should_heating, this]() {
@@ -3710,6 +3863,18 @@ bool WipeTower::is_in_same_extruder(int filament_id_1, int filament_id_2)
return m_filament_map[filament_id_1] == m_filament_map[filament_id_2]; return m_filament_map[filament_id_1] == m_filament_map[filament_id_2];
} }
std::string WipeTower::format_nozzle_change_tag(bool start, int old_filament_id, int new_filament_id) const
{
const std::string &tag = start ? GCodeProcessor::Nozzle_Change_Start_Tag : GCodeProcessor::Nozzle_Change_End_Tag;
int old_nozzle = (old_filament_id >= 0 && old_filament_id < (int)m_filament_nozzle_map.size())
? m_filament_nozzle_map[old_filament_id] : -1;
int new_nozzle = (new_filament_id >= 0 && new_filament_id < (int)m_filament_nozzle_map.size())
? m_filament_nozzle_map[new_filament_id] : -1;
char buff[96];
snprintf(buff, sizeof(buff), ";%s OF%d NF%d ON%d NN%d\n", tag.c_str(), old_filament_id, new_filament_id, old_nozzle, new_nozzle);
return std::string(buff);
}
// Per-extruder printable-height clamp: is an extruder still allowed to print on this wipe-tower layer, // Per-extruder printable-height clamp: is an extruder still allowed to print on this wipe-tower layer,
// or is it its final layer above the extruder's printable height? // or is it its final layer above the extruder's printable height?
// Orca: the arriving extruder id is resolved as m_filament_map[tool]-1 (1-based map, layer-static), // Orca: the arriving extruder id is resolved as m_filament_map[tool]-1 (1-based map, layer-static),
@@ -3908,6 +4073,19 @@ void WipeTower::plan_tower_new()
nozzle_change_depth = nozzle_change_line_count * m_nozzle_change_perimeter_width; nozzle_change_depth = nozzle_change_line_count * m_nozzle_change_perimeter_width;
depth += nozzle_change_depth; depth += nozzle_change_depth;
} }
if (nozzle_change_depth == 0
&& !m_filament_nozzle_map.empty()
&& toolchange.old_tool < (int)m_filament_nozzle_map.size() && toolchange.new_tool < (int)m_filament_nozzle_map.size()
&& m_filament_nozzle_map[toolchange.old_tool] != m_filament_nozzle_map[toolchange.new_tool]) {
double e_flow = nozzle_change_extrusion_flow(m_plan[idx].height);
double length = m_filaments_change_length[toolchange.old_tool] / e_flow;
int nozzle_change_line_count = length / (m_wipe_tower_width - 2*m_nozzle_change_perimeter_width) + 1;
if (has_tpu_filament())
nozzle_change_depth = m_tpu_fixed_spacing * nozzle_change_line_count * m_nozzle_change_perimeter_width;
else
nozzle_change_depth = nozzle_change_line_count * m_nozzle_change_perimeter_width;
depth += nozzle_change_depth;
}
toolchange.nozzle_change_depth = nozzle_change_depth; toolchange.nozzle_change_depth = nozzle_change_depth;
toolchange.required_depth = depth; toolchange.required_depth = depth;
} }
+13
View File
@@ -58,6 +58,7 @@ public:
Vec2f origin_start_pos; // not rotated Vec2f origin_start_pos; // not rotated
std::vector<Vec2f> wipe_path; std::vector<Vec2f> wipe_path;
bool is_extruder_change{true};
}; };
struct ToolChangeResult struct ToolChangeResult
@@ -309,6 +310,8 @@ public:
int get_number_of_toolchanges() const { return m_num_tool_changes; } int get_number_of_toolchanges() const { return m_num_tool_changes; }
void set_filament_map(const std::vector<int> &filament_map) { m_filament_map = filament_map; } void set_filament_map(const std::vector<int> &filament_map) { m_filament_map = filament_map; }
// Vortek H2C: filament_id → physical nozzle_id for carousel rotation detection
void set_filament_nozzle_map(const std::vector<int> &nozzle_map) { m_filament_nozzle_map = nozzle_map; }
void set_has_tpu_filament(bool has_tpu) { m_has_tpu_filament = has_tpu; } void set_has_tpu_filament(bool has_tpu) { m_has_tpu_filament = has_tpu; }
bool has_tpu_filament() const { return m_has_tpu_filament; } bool has_tpu_filament() const { return m_has_tpu_filament; }
@@ -356,6 +359,12 @@ public:
// Distance (in mm of filament) that a hotend is allowed to pre-cool before the // Distance (in mm of filament) that a hotend is allowed to pre-cool before the
// tower is reached; drives the prime-tower heating-during-wipe model (multi-nozzle only). // tower is reached; drives the prime-tower heating-during-wipe model (multi-nozzle only).
float filament_cooling_before_tower = 0.f; float filament_cooling_before_tower = 0.f;
// .first = extruder change, .second = nozzle change (carousel)
std::pair<float,float> max_e_ramming_speed{0.f, 0.f};
std::pair<float,float> ramming_travel_time{0.f, 0.f};
std::pair<int,int> precool_target_temp{0, 0};
std::pair<std::vector<float>,std::vector<float>> precool_t;
std::pair<std::vector<float>,std::vector<float>> precool_t_first_layer;
}; };
@@ -395,6 +404,8 @@ public:
void add_depth_to_block(int filament_id, int filament_adhesiveness_category, float depth, bool is_nozzle_change = false); void add_depth_to_block(int filament_id, int filament_adhesiveness_category, float depth, bool is_nozzle_change = false);
int get_filament_category(int filament_id); int get_filament_category(int filament_id);
bool is_in_same_extruder(int filament_id_1, int filament_id_2); bool is_in_same_extruder(int filament_id_1, int filament_id_2);
// Vortek H2C: format BBS-compatible NOZZLE_CHANGE_START/END tag with OF/NF/ON/NN payload
std::string format_nozzle_change_tag(bool start, int old_filament_id, int new_filament_id) const;
void reset_block_status(); void reset_block_status();
int get_wall_filament_for_all_layer(); int get_wall_filament_for_all_layer();
// for generate new wipe tower // for generate new wipe tower
@@ -453,6 +464,7 @@ private:
size_t m_cur_layer_id; size_t m_cur_layer_id;
NozzleChangeResult m_nozzle_change_result; NozzleChangeResult m_nozzle_change_result;
std::vector<int> m_filament_map; std::vector<int> m_filament_map;
std::vector<int> m_filament_nozzle_map; // Vortek H2C: filament_id → physical nozzle_id
bool m_has_tpu_filament{false}; bool m_has_tpu_filament{false};
bool m_is_multi_extruder{false}; bool m_is_multi_extruder{false};
bool m_use_gap_wall{false}; bool m_use_gap_wall{false};
@@ -555,6 +567,7 @@ private:
bool is_tpu_filament(int filament_id) const; bool is_tpu_filament(int filament_id) const;
bool is_petg_filament(int filament_id) const; bool is_petg_filament(int filament_id) const;
bool is_need_reverse_travel(int filament_id, bool extruder_change) const;
// BBS // BBS
box_coordinates align_perimeter(const box_coordinates& perimeter_box); box_coordinates align_perimeter(const box_coordinates& perimeter_box);
+96 -3
View File
@@ -2,6 +2,8 @@
#include "CustomGCode.hpp" #include "CustomGCode.hpp"
#include "I18N.hpp" #include "I18N.hpp"
#include "PrintConfig.hpp" #include "PrintConfig.hpp"
#include "ClipperUtils.hpp"
#include "Line.hpp"
#include <algorithm> #include <algorithm>
#include <iomanip> #include <iomanip>
#include <iostream> #include <iostream>
@@ -99,9 +101,87 @@ void GCodeWriter::apply_print_config(const PrintConfig &print_config)
m_max_jerk_z = LIMITS(machine_max_jerk_z); m_max_jerk_z = LIMITS(machine_max_jerk_z);
m_max_jerk_e = LIMITS(machine_max_jerk_e); m_max_jerk_e = LIMITS(machine_max_jerk_e);
m_resolution = print_config.resolution.value; m_resolution = print_config.resolution.value;
#undef LIMITS #undef LIMITS
#undef LIMITS_UINT #undef LIMITS_UINT
// Orca: capture the printable area(s) so a spiral lift can be skipped when its
// circle would leave the boundary and collide with the print limits. Full polygons
// are stored (not a bounding box) so the check stays correct for non-rectangular
// beds, and per-extruder areas are kept so printers with different boundaries per
// extruder use the right limit for whichever extruder is active.
auto to_scaled_polygon = [](const Pointfs &pts) {
Polygon poly;
poly.points.reserve(pts.size());
for (const Vec2d &p : pts)
poly.points.emplace_back(coord_t(scale_(p.x())), coord_t(scale_(p.y())));
poly.make_counter_clockwise();
return poly;
};
m_bed_printable_area.points.clear();
m_extruder_printable_areas.clear();
if (print_config.printable_area.values.size() >= 3)
m_bed_printable_area = to_scaled_polygon(print_config.printable_area.values);
const std::vector<Pointfs> &extruder_areas = print_config.extruder_printable_area.values;
if (!extruder_areas.empty()) {
m_extruder_printable_areas.resize(extruder_areas.size());
for (size_t i = 0; i < extruder_areas.size(); ++i) {
if (extruder_areas[i].size() < 3) {
// No dedicated area for this extruder: it can reach the whole bed.
m_extruder_printable_areas[i] = m_bed_printable_area;
continue;
}
Polygon extruder_poly = to_scaled_polygon(extruder_areas[i]);
if (m_bed_printable_area.points.size() < 3) {
m_extruder_printable_areas[i] = std::move(extruder_poly);
continue;
}
// The reachable area is the extruder area clipped to the bed. Bed shapes are
// convex in practice, so keep the largest resulting contour.
Polygons clipped = intersection(extruder_poly, m_bed_printable_area);
const Polygon *largest = nullptr;
double best_area = 0.;
for (const Polygon &p : clipped) {
double a = std::abs(p.area());
if (a > best_area) { best_area = a; largest = &p; }
}
m_extruder_printable_areas[i] = largest ? *largest : std::move(extruder_poly);
}
}
}
const Polygon *GCodeWriter::active_printable_area() const
{
if (const Extruder *e = this->filament()) {
size_t id = e->extruder_id();
if (id < m_extruder_printable_areas.size() && m_extruder_printable_areas[id].points.size() >= 3)
return &m_extruder_printable_areas[id];
}
if (m_bed_printable_area.points.size() >= 3)
return &m_bed_printable_area;
return nullptr;
}
bool GCodeWriter::spiral_lift_fits_printable_area(const Vec2d &center, double radius) const
{
const Polygon *area = this->active_printable_area();
if (area == nullptr)
return true; // Boundary unknown: don't restrict (preserve previous behavior).
const Point c = Point::new_scale(center.x(), center.y());
const double r_scaled = scale_(radius);
const double r2 = r_scaled * r_scaled;
// The spiral traces a full circle of `radius` around `center`, so the center must lie
// inside the printable area and every edge must be at least `radius` away from it.
if (!area->contains(c))
return false;
const Points &pts = area->points;
for (size_t i = 0, n = pts.size(); i < n; ++i)
if (Line::distance_to_squared(c, pts[i], pts[(i + 1) % n]) < r2)
return false;
return true;
} }
void GCodeWriter::set_extruders(std::vector<unsigned int> extruder_ids) void GCodeWriter::set_extruders(std::vector<unsigned int> extruder_ids)
@@ -731,14 +811,19 @@ std::string GCodeWriter::eager_lift(const LiftType type) {
} }
// BBS: spiral lift only safe with known position // BBS: spiral lift only safe with known position
// TODO: check the arc will move within bed area
if (type == LiftType::SpiralLift && this->is_current_position_clear()) { if (type == LiftType::SpiralLift && this->is_current_position_clear()) {
double radius = target_lift / (2 * PI * atan(filament()->travel_slope())); double radius = target_lift / (2 * PI * atan(filament()->travel_slope()));
// static spiral alignment when no move in x,y plane. // static spiral alignment when no move in x,y plane.
// spiral centra is a radius distance to the right (y=0) // spiral centra is a radius distance to the right (y=0)
Vec2d ij_offset = { radius, 0 }; Vec2d ij_offset = { radius, 0 };
if (target_lift > 0) { // Orca: keep the spiral inside the active extruder's printable area, otherwise
// fall back to a normal lift to avoid colliding with the print boundary. m_pos
// includes the plate offset, so remove it to match the printable area coordinates.
const Vec2d spiral_center = { m_pos.x() - m_x_offset + ij_offset.x(), m_pos.y() - m_y_offset + ij_offset.y() };
if (target_lift > 0 && this->spiral_lift_fits_printable_area(spiral_center, radius)) {
lift_move = this->_spiral_travel_to_z(m_pos(2) + target_lift, ij_offset, "spiral lift Z"); lift_move = this->_spiral_travel_to_z(m_pos(2) + target_lift, ij_offset, "spiral lift Z");
} else if (target_lift > 0) {
lift_move = _travel_to_z(m_pos(2) + target_lift, "normal lift Z");
} }
} }
//BBS: if position is unknown use normal lift //BBS: if position is unknown use normal lift
@@ -793,7 +878,15 @@ std::string GCodeWriter::travel_to_xyz(const Vec3d &point, const std::string &co
double radius = delta(2) / (2 * PI * atan(this->filament()->travel_slope())); double radius = delta(2) / (2 * PI * atan(this->filament()->travel_slope()));
Vec2d ij_offset = radius * delta_no_z.normalized(); Vec2d ij_offset = radius * delta_no_z.normalized();
ij_offset = { -ij_offset(1), ij_offset(0) }; ij_offset = { -ij_offset(1), ij_offset(0) };
// Orca: only perform the spiral lift if its full circle stays inside the
// printable area of the active extruder, otherwise fall back to a normal
// lift to avoid colliding with the print boundary. `source` is already in
// bed coordinates (plate offset removed), matching the printable area.
const Vec2d spiral_center = { source.x() + ij_offset.x(), source.y() + ij_offset.y() };
if (this->spiral_lift_fits_printable_area(spiral_center, radius))
slop_move = this->_spiral_travel_to_z(target(2), ij_offset, "spiral lift Z"); slop_move = this->_spiral_travel_to_z(target(2), ij_offset, "spiral lift Z");
else
slop_move = _travel_to_z(target.z(), "normal lift Z");
} }
//BBS: SlopeLift //BBS: SlopeLift
else if (m_to_lift_type == LiftType::SlopeLift && else if (m_to_lift_type == LiftType::SlopeLift &&
+15
View File
@@ -6,6 +6,7 @@
#include <charconv> #include <charconv>
#include "Extruder.hpp" #include "Extruder.hpp"
#include "Point.hpp" #include "Point.hpp"
#include "Polygon.hpp"
#include "PrintConfig.hpp" #include "PrintConfig.hpp"
#include "GCode/CoolingBuffer.hpp" #include "GCode/CoolingBuffer.hpp"
@@ -132,6 +133,8 @@ public:
const bool is_bbl_printers() const {return m_is_bbl_printers;} const bool is_bbl_printers() const {return m_is_bbl_printers;}
void set_is_first_layer(bool bval) { m_is_first_layer = bval; } void set_is_first_layer(bool bval) { m_is_first_layer = bval; }
GCodeFlavor get_gcode_flavor() const { return config.gcode_flavor; } GCodeFlavor get_gcode_flavor() const { return config.gcode_flavor; }
void invalidate_acceleration() { m_last_acceleration = 0; m_last_travel_acceleration = 0; }
void invalidate_jerk() { m_last_jerk = 0; }
// Returns whether this flavor supports separate print and travel acceleration. // Returns whether this flavor supports separate print and travel acceleration.
static bool supports_separate_travel_acceleration(GCodeFlavor flavor); static bool supports_separate_travel_acceleration(GCodeFlavor flavor);
@@ -181,6 +184,14 @@ public:
// Orca: slicing resolution in mm // Orca: slicing resolution in mm
double m_resolution = 0.01; double m_resolution = 0.01;
// Orca: printable area polygons (scaled, bed coordinates) used to keep spiral lifts
// from colliding with the print boundary. m_extruder_printable_areas holds the
// per-extruder reachable area (intersected with the bed) when a printer defines
// different boundaries per extruder; m_bed_printable_area is the global fallback.
// Storing full polygons (rather than a bounding box) keeps the check correct for
// non-rectangular beds such as delta/circular printers.
Polygon m_bed_printable_area;
std::vector<Polygon> m_extruder_printable_areas;
std::string m_gcode_label_objects_start; std::string m_gcode_label_objects_start;
std::string m_gcode_label_objects_end; std::string m_gcode_label_objects_end;
@@ -197,6 +208,10 @@ public:
std::string _travel_to_z(double z, const std::string &comment); std::string _travel_to_z(double z, const std::string &comment);
std::string _spiral_travel_to_z(double z, const Vec2d &ij_offset, const std::string &comment); std::string _spiral_travel_to_z(double z, const Vec2d &ij_offset, const std::string &comment);
// Orca: printable area of the active extruder (per-extruder when configured, otherwise the bed). Null when unknown.
const Polygon *active_printable_area() const;
// Orca: true if a full spiral-lift circle (center in bed coordinates, mm) fits inside the active printable area.
bool spiral_lift_fits_printable_area(const Vec2d &center, double radius) const;
std::string _retract(double length, double restart_extra, const std::string &comment); std::string _retract(double length, double restart_extra, const std::string &comment);
std::string set_acceleration_internal(Acceleration type, unsigned int acceleration); std::string set_acceleration_internal(Acceleration type, unsigned int acceleration);
+13 -2
View File
@@ -1177,6 +1177,18 @@ static bool is_volume_sinking(const indexed_triangle_set &its, const Transform3d
//#define MMU_SEGMENTATION_DEBUG_TOP_BOTTOM //#define MMU_SEGMENTATION_DEBUG_TOP_BOTTOM
double resolve_outer_wall_line_width(const PrintRegionConfig &region_config, const PrintObjectConfig &object_config, const PrintConfig &print_config)
{
// A filament id of 0 underflows, and get_at() then falls back to the first nozzle.
const double nozzle_diameter = print_config.nozzle_diameter.get_at(region_config.outer_wall_filament_id - 1);
ConfigOptionFloatOrPercent width = region_config.outer_wall_line_width;
if (width.value == 0)
width = object_config.line_width;
if (!width.percent && width.value <= 0.)
return Flow::auto_extrusion_width(frExternalPerimeter, float(nozzle_diameter));
return width.get_abs_value(nozzle_diameter);
}
// Returns segmentation of top and bottom layers based on painting in segmentation gizmos. // Returns segmentation of top and bottom layers based on painting in segmentation gizmos.
static inline std::vector<std::vector<ExPolygons>> segmentation_top_and_bottom_layers(const PrintObject &print_object, static inline std::vector<std::vector<ExPolygons>> segmentation_top_and_bottom_layers(const PrintObject &print_object,
const std::vector<ExPolygons> &input_expolygons, const std::vector<ExPolygons> &input_expolygons,
@@ -1347,8 +1359,7 @@ static inline std::vector<std::vector<ExPolygons>> segmentation_top_and_bottom_l
// As this region may split existing regions, we collect statistics over all regions for color_idx == 0. // As this region may split existing regions, we collect statistics over all regions for color_idx == 0.
color_idx == 0 || config.outer_wall_filament_id == int(color_idx)) { color_idx == 0 || config.outer_wall_filament_id == int(color_idx)) {
//BBS: the extrusion line width is outer wall rather than inner wall //BBS: the extrusion line width is outer wall rather than inner wall
const double nozzle_diameter = print_object.print()->config().nozzle_diameter.get_at(0); double outer_wall_line_width = resolve_outer_wall_line_width(config, print_object.config(), print_object.print()->config());
double outer_wall_line_width = config.get_abs_value("outer_wall_line_width", nozzle_diameter);
out.extrusion_width = std::max<float>(out.extrusion_width, outer_wall_line_width); out.extrusion_width = std::max<float>(out.extrusion_width, outer_wall_line_width);
out.top_shell_layers = std::max<int>(out.top_shell_layers, config.top_shell_layers); out.top_shell_layers = std::max<int>(out.top_shell_layers, config.top_shell_layers);
out.bottom_shell_layers = std::max<int>(out.bottom_shell_layers, config.bottom_shell_layers); out.bottom_shell_layers = std::max<int>(out.bottom_shell_layers, config.bottom_shell_layers);
@@ -9,6 +9,9 @@ namespace Slic3r {
class ExPolygon; class ExPolygon;
class ModelVolume; class ModelVolume;
class PrintObject; class PrintObject;
class PrintConfig;
class PrintObjectConfig;
class PrintRegionConfig;
class FacetsAnnotation; class FacetsAnnotation;
using ExPolygons = std::vector<ExPolygon>; using ExPolygons = std::vector<ExPolygon>;
@@ -52,6 +55,9 @@ std::vector<std::vector<ExPolygons>> multi_material_segmentation_by_painting(con
// Returns fuzzy skin segmentation based on painting in fuzzy skin segmentation gizmo // Returns fuzzy skin segmentation based on painting in fuzzy skin segmentation gizmo
std::vector<std::vector<ExPolygons>> fuzzy_skin_segmentation_by_painting(const PrintObject &print_object, const std::function<void()> &throw_on_cancel_callback); std::vector<std::vector<ExPolygons>> fuzzy_skin_segmentation_by_painting(const PrintObject &print_object, const std::function<void()> &throw_on_cancel_callback);
// Effective outer-wall line width for a region, resolved against its own nozzle with PrintRegion::flow's fallback.
double resolve_outer_wall_line_width(const PrintRegionConfig &region_config, const PrintObjectConfig &object_config, const PrintConfig &print_config);
} // namespace Slic3r } // namespace Slic3r
namespace boost::polygon { namespace boost::polygon {
+2 -2
View File
@@ -1376,7 +1376,7 @@ static std::vector<std::string> s_Preset_filament_options {/*"filament_colour",
"filament_ramming_travel_time", "filament_ramming_travel_time_nc", "filament_ramming_travel_time", "filament_ramming_travel_time_nc",
"filament_pre_cooling_temperature", "filament_pre_cooling_temperature_nc", "filament_pre_cooling_temperature", "filament_pre_cooling_temperature_nc",
"filament_preheat_temperature_delta", "filament_retract_length_nc", "filament_preheat_temperature_delta", "filament_retract_length_nc",
"filament_change_length_nc", "filament_prime_volume_nc", "filament_change_length_nc", "filament_prime_volume", "filament_prime_volume_nc",
"long_retractions_when_ec", "retraction_distances_when_ec", "long_retractions_when_ec", "retraction_distances_when_ec",
"plugin_config_overrides", "plugin_config_overrides",
//ams chamber //ams chamber
@@ -3504,7 +3504,7 @@ inline t_config_option_keys deep_diff(const ConfigBase &config_this, const Confi
if (this_opt != nullptr && other_opt != nullptr && *this_opt != *other_opt) if (this_opt != nullptr && other_opt != nullptr && *this_opt != *other_opt)
{ {
//BBS: add bed_exclude_area //BBS: add bed_exclude_area
if (opt_key == "printable_area" || opt_key == "bed_exclude_area" || opt_key == "compatible_prints" || opt_key == "compatible_printers" || opt_key == "thumbnails" || opt_key == "wrapping_exclude_area") { if (opt_key == "printable_area" || opt_key == "bed_exclude_area" || opt_key == "compatible_prints" || opt_key == "compatible_printers" || opt_key == "thumbnails" || opt_key == "wrapping_exclude_area" || opt_key == "slicing_pipeline_plugin") {
// Scalar variable, or a vector variable, which is independent from number of extruders, // Scalar variable, or a vector variable, which is independent from number of extruders,
// thus the vector is presented to the user as a single input. // thus the vector is presented to the user as a single input.
diff.emplace_back(opt_key); diff.emplace_back(opt_key);
+80 -13
View File
@@ -3605,7 +3605,8 @@ std::vector<std::set<int>> Print::get_physical_unprintable_filaments(const std::
return physical_unprintables; return physical_unprintables;
auto get_unprintable_extruder_id = [&](unsigned int filament_idx) -> int { auto get_unprintable_extruder_id = [&](unsigned int filament_idx) -> int {
int status = m_config.filament_printable.values[filament_idx]; // filament_printable may be shorter than the filament count; get_at() clamps.
int status = m_config.filament_printable.get_at(filament_idx);
for (int i = 0; i < extruder_num; ++i) { for (int i = 0; i < extruder_num; ++i) {
if (!(status >> i & 1)) { if (!(status >> i & 1)) {
return i; return i;
@@ -4017,6 +4018,8 @@ void Print::_make_wipe_tower()
m_wipe_tower_data.tool_ordering.empty() ? 0.f : m_wipe_tower_data.tool_ordering.back().print_z, m_wipe_tower_data.tool_ordering.all_extruders()); m_wipe_tower_data.tool_ordering.empty() ? 0.f : m_wipe_tower_data.tool_ordering.back().print_z, m_wipe_tower_data.tool_ordering.all_extruders());
wipe_tower.set_has_tpu_filament(this->has_tpu_filament()); wipe_tower.set_has_tpu_filament(this->has_tpu_filament());
wipe_tower.set_filament_map(this->get_filament_maps()); wipe_tower.set_filament_map(this->get_filament_maps());
// Vortek H2C: pass nozzle-level map for carousel rotation detection in tool_change_new()
wipe_tower.set_filament_nozzle_map(this->get_filament_nozzle_maps());
// Feed the has_filament_switcher device flag (develop-only dynamic key, read defensively from // Feed the has_filament_switcher device flag (develop-only dynamic key, read defensively from
// the full config — no shipping profile sets it) and the shared printable bed used by the PETG // the full config — no shipping profile sets it) and the shared printable bed used by the PETG
// pre-extrusion offset clamp. Both are inert unless has_filament_switcher is set. // pre-extrusion offset clamp. Both are inert unless has_filament_switcher is set.
@@ -4052,14 +4055,32 @@ void Print::_make_wipe_tower()
multi_extruder_flush.emplace_back(wipe_volumes); multi_extruder_flush.emplace_back(wipe_volumes);
} }
// Use NozzleStatusRecorder for per-carousel-slot tracking (BBS pattern).
// The original Orca code tracked per-extruder (2 slots), which collapsed all
// carousel filaments into one slot and caused massive redundant AMS flushing.
auto group_result = get_layered_nozzle_group_result();
MultiNozzleUtils::NozzleStatusRecorder nozzle_recorder;
// Fallback (group_result == null) per-physical-nozzle tracking, matching the original
// pre-port behavior: remembers the last filament loaded in each physical nozzle slot.
std::vector<unsigned int> nozzle_cur_filament_ids(nozzle_nums, (unsigned int) -1);
std::vector<int>filament_maps = get_filament_maps(); std::vector<int>filament_maps = get_filament_maps();
int layer_idx = -1;
std::vector<unsigned int> nozzle_cur_filament_ids(nozzle_nums, -1);
unsigned int current_filament_id = m_wipe_tower_data.tool_ordering.first_extruder(); unsigned int current_filament_id = m_wipe_tower_data.tool_ordering.first_extruder();
// Initialize NozzleStatusRecorder with the first filament's carousel slot
if (group_result) {
auto nozzle = group_result->get_nozzle_for_filament(current_filament_id, layer_idx);
if (nozzle)
nozzle_recorder.set_nozzle_status(nozzle->group_id, current_filament_id, nozzle->extruder_id);
} else {
size_t cur_nozzle_id = filament_maps[current_filament_id] - 1; size_t cur_nozzle_id = filament_maps[current_filament_id] - 1;
nozzle_cur_filament_ids[cur_nozzle_id] = current_filament_id; nozzle_cur_filament_ids[cur_nozzle_id] = current_filament_id;
}
for (auto& layer_tools : m_wipe_tower_data.tool_ordering.layer_tools()) { // for all layers for (auto& layer_tools : m_wipe_tower_data.tool_ordering.layer_tools()) { // for all layers
++layer_idx;
if (!layer_tools.has_wipe_tower) continue; if (!layer_tools.has_wipe_tower) continue;
bool first_layer = &layer_tools == &m_wipe_tower_data.tool_ordering.front(); bool first_layer = &layer_tools == &m_wipe_tower_data.tool_ordering.front();
wipe_tower.plan_toolchange((float)layer_tools.print_z, (float)layer_tools.wipe_tower_layer_height, current_filament_id, current_filament_id); wipe_tower.plan_toolchange((float)layer_tools.print_z, (float)layer_tools.wipe_tower_layer_height, current_filament_id, current_filament_id);
@@ -4070,30 +4091,76 @@ void Print::_make_wipe_tower()
if (filament_id == current_filament_id) if (filament_id == current_filament_id)
continue; continue;
float volume_to_purge = 0;
// Per-carousel-slot purge tracking via NozzleStatusRecorder
if (group_result) {
auto nozzle_info = group_result->get_nozzle_for_filament(filament_id, layer_idx);
if (nozzle_info) {
int extruder_id = nozzle_info->extruder_id;
int nozzle_id = nozzle_info->group_id;
int prev_nozzle_filament = nozzle_recorder.get_filament_in_nozzle(nozzle_id);
if (!nozzle_recorder.is_nozzle_empty(nozzle_id) &&
static_cast<int>(filament_id) != prev_nozzle_filament) {
volume_to_purge = multi_extruder_flush[extruder_id][prev_nozzle_filament][filament_id];
// Fast purge mode uses flush_multiplier_fast; Default is inert.
float flush_multiplier = (m_config.prime_volume_mode == PrimeVolumeMode::pvmFast)
? m_config.flush_multiplier_fast.get_at(extruder_id)
: m_config.flush_multiplier.get_at(extruder_id);
volume_to_purge *= flush_multiplier;
volume_to_purge = layer_tools.wiping_extrusions().mark_wiping_extrusions(
*this, current_filament_id, filament_id, volume_to_purge);
}
nozzle_recorder.set_nozzle_status(nozzle_id, filament_id, extruder_id);
}
} else {
// Fallback: original Orca per-physical-nozzle path (non-carousel printers).
// Flush source is the last filament that occupied THIS nozzle, guarded so the
// first use of a nozzle incurs no flush.
int nozzle_id = filament_maps[filament_id] - 1; int nozzle_id = filament_maps[filament_id] - 1;
unsigned int pre_filament_id = nozzle_cur_filament_ids[nozzle_id]; unsigned int pre_filament_id = nozzle_cur_filament_ids[nozzle_id];
if (pre_filament_id != (unsigned int) -1 && pre_filament_id != filament_id) {
float volume_to_purge = 0;
if (pre_filament_id != (unsigned int)(-1) && pre_filament_id != filament_id) {
volume_to_purge = multi_extruder_flush[nozzle_id][pre_filament_id][filament_id]; volume_to_purge = multi_extruder_flush[nozzle_id][pre_filament_id][filament_id];
// Fast purge mode uses flush_multiplier_fast; Default is inert. float flush_multiplier = (m_config.prime_volume_mode == PrimeVolumeMode::pvmFast)
float flush_multiplier = (m_config.prime_volume_mode == PrimeVolumeMode::pvmFast) ? m_config.flush_multiplier_fast.get_at(nozzle_id) ? m_config.flush_multiplier_fast.get_at(nozzle_id)
: m_config.flush_multiplier.get_at(nozzle_id); : m_config.flush_multiplier.get_at(nozzle_id);
volume_to_purge *= flush_multiplier; volume_to_purge *= flush_multiplier;
volume_to_purge = pre_filament_id == -1 ? 0 : volume_to_purge = layer_tools.wiping_extrusions().mark_wiping_extrusions(
layer_tools.wiping_extrusions().mark_wiping_extrusions(*this, current_filament_id, filament_id, volume_to_purge); *this, current_filament_id, filament_id, volume_to_purge);
}
nozzle_cur_filament_ids[nozzle_id] = filament_id;
} }
//During the filament change, the extruder will extrude an extra length of grab_length for the corresponding detection, so the purge can reduce this length. //During the filament change, the extruder will extrude an extra length of grab_length for the corresponding detection, so the purge can reduce this length.
float grab_purge_volume = m_config.grab_length.get_at(nozzle_id) * 2.4; //(diameter/2)^2*PI=2.4 int grab_extruder_id = filament_maps[filament_id] - 1;
float grab_purge_volume = m_config.grab_length.get_at(grab_extruder_id) * 2.4; //(diameter/2)^2*PI=2.4
volume_to_purge = std::max(0.f, volume_to_purge - grab_purge_volume); volume_to_purge = std::max(0.f, volume_to_purge - grab_purge_volume);
// Saving mode reduces the prime volume to 15 mm3; Default is inert. // Select prime volume per-filament: nozzle change (carousel rotation) uses
float prime_volume = (m_config.prime_volume_mode == PrimeVolumeMode::pvmSaving) ? 15.f : (float) m_config.prime_volume; // filament_prime_volume_nc, filament change (same nozzle slot) uses filament_prime_volume.
float wipe_volume_ec = filament_id < m_config.filament_prime_volume.values.size()
? m_config.filament_prime_volume.values[filament_id]
: (float) m_config.prime_volume;
float wipe_volume_nc = filament_id < m_config.filament_prime_volume_nc.values.size()
? m_config.filament_prime_volume_nc.values[filament_id]
: (float) m_config.prime_volume;
float prime_volume = wipe_volume_ec;
if (group_result) {
bool is_nozzle_change = group_result->are_filaments_same_extruder(current_filament_id, filament_id, layer_idx) &&
!group_result->are_filaments_same_nozzle(current_filament_id, filament_id, layer_idx);
if (is_nozzle_change) {
prime_volume = wipe_volume_nc;
}
}
if (m_config.prime_volume_mode == PrimeVolumeMode::pvmSaving) {
prime_volume = 15.f;
}
wipe_tower.plan_toolchange((float)layer_tools.print_z, (float)layer_tools.wipe_tower_layer_height, current_filament_id, filament_id, wipe_tower.plan_toolchange((float)layer_tools.print_z, (float)layer_tools.wipe_tower_layer_height, current_filament_id, filament_id,
prime_volume, volume_to_purge); prime_volume, volume_to_purge);
current_filament_id = filament_id; current_filament_id = filament_id;
nozzle_cur_filament_ids[nozzle_id] = filament_id;
} }
layer_tools.wiping_extrusions().ensure_perimeters_infills_order(*this); layer_tools.wiping_extrusions().ensure_perimeters_infills_order(*this);
+12 -2
View File
@@ -7384,7 +7384,7 @@ void PrintConfigDef::init_fff_params()
def->tooltip = L("The flush multiplier used in fast purge mode."); def->tooltip = L("The flush multiplier used in fast purge mode.");
def->set_default_value(new ConfigOptionFloats{1.2}); def->set_default_value(new ConfigOptionFloats{1.2});
// BBS // Orca: used by the generic (Type2) wipe tower; also the fallback for filament_prime_volume on Type1.
def = this->add("prime_volume", coFloat); def = this->add("prime_volume", coFloat);
def->label = L("Prime volume"); def->label = L("Prime volume");
def->tooltip = L("This is the volume of material to prime the extruder with on the tower."); def->tooltip = L("This is the volume of material to prime the extruder with on the tower.");
@@ -8030,6 +8030,16 @@ void PrintConfigDef::init_fff_params()
def->mode = comDevelop; def->mode = comDevelop;
def->set_default_value(new ConfigOptionBool(false)); def->set_default_value(new ConfigOptionBool(false));
// Used by the Type1 wipe tower: filament_prime_volume on a filament change,
// filament_prime_volume_nc on a hotend/nozzle change. Type2 uses prime_volume instead.
def = this->add("filament_prime_volume", coFloats);
def->label = L("Filament change");
def->tooltip = L("The volume of material required to prime the extruder on the tower, excluding a hotend change.");
def->sidetext = L("mm³");
def->min = 1.0;
def->mode = comSimple;
def->set_default_value(new ConfigOptionFloats{45.});
def = this->add("filament_prime_volume_nc", coFloats); def = this->add("filament_prime_volume_nc", coFloats);
def->label = L("Hotend change"); def->label = L("Hotend change");
def->tooltip = L("The volume of material required to prime the extruder for a hotend change on the tower."); def->tooltip = L("The volume of material required to prime the extruder for a hotend change on the tower.");
@@ -9050,7 +9060,7 @@ void PrintConfigDef::handle_legacy(t_config_option_key &opt_key, std::string &va
"retraction_distance_when_cut", "retraction_distance_when_cut",
"internal_bridge_support_thickness", "top_area_threshold", "reduce_wall_solid_infill","filament_load_time","filament_unload_time", "internal_bridge_support_thickness", "top_area_threshold", "reduce_wall_solid_infill","filament_load_time","filament_unload_time",
"smooth_coefficient", "overhang_totally_speed", "silent_mode", "smooth_coefficient", "overhang_totally_speed", "silent_mode",
"overhang_speed_classic", "filament_prime_volume", "overhang_speed_classic",
"anisotropic_surfaces", // superseded by top_surface_fill_order / bottom_surface_fill_order "anisotropic_surfaces", // superseded by top_surface_fill_order / bottom_surface_fill_order
}; };
+1
View File
@@ -1841,6 +1841,7 @@ PRINT_CONFIG_CLASS_DERIVED_DEFINE(
// BBS: wipe tower is only used for priming // BBS: wipe tower is only used for priming
((ConfigOptionFloat, prime_volume)) ((ConfigOptionFloat, prime_volume))
// Nozzle-change (nc) prime volume + pre-heat delta // Nozzle-change (nc) prime volume + pre-heat delta
((ConfigOptionFloats, filament_prime_volume))
((ConfigOptionFloats, filament_prime_volume_nc)) ((ConfigOptionFloats, filament_prime_volume_nc))
((ConfigOptionFloatsNullable, filament_preheat_temperature_delta)) ((ConfigOptionFloatsNullable, filament_preheat_temperature_delta))
((ConfigOptionFloats, flush_multiplier)) ((ConfigOptionFloats, flush_multiplier))
+1
View File
@@ -771,6 +771,7 @@ CustomGCode::Info CalibPressureAdvancePattern::generate_custom_gcodes(const Dyna
} }
} }
gcode << m_writer.reset_e();
gcode << m_writer.set_pressure_advance(m_params.start); gcode << m_writer.set_pressure_advance(m_params.start);
gcode << "; end pressure advance pattern for layer\n"; gcode << "; end pressure advance pattern for layer\n";
+4 -4
View File
@@ -35,10 +35,10 @@ struct Calib_Params
{ {
Calib_Params() : mode(CalibMode::Calib_None){}; Calib_Params() : mode(CalibMode::Calib_None){};
int extruder_id = 0; int extruder_id = 0;
double start, end, step; double start = 0.0, end = 1.0, step = 0.1;
bool print_numbers; bool print_numbers = false;
double freqStartX, freqEndX, freqStartY, freqEndY; double freqStartX = 0.0, freqEndX = 1.0, freqStartY = 0.0, freqEndY = 1.0;
int test_model; int test_model = 0;
std::string shaper_type; std::string shaper_type;
std::vector<double> accelerations; std::vector<double> accelerations;
std::vector<double> speeds; std::vector<double> speeds;
+1
View File
@@ -558,6 +558,7 @@ namespace Slic3r
} }
else else
{ {
Slic3r::GUI::wxGetApp().reset_unsigned_plugin_warning();
if (m_agent) if (m_agent)
{ {
if (it->second->connection_type() != "lan" || it->second->connection_type().empty()) if (it->second->connection_type() != "lan" || it->second->connection_type().empty())
+14 -8
View File
@@ -1043,14 +1043,14 @@ void TextCtrl::propagate_value()
void TextCtrl::set_value(const boost::any& value, bool change_event/* = false*/) { void TextCtrl::set_value(const boost::any& value, bool change_event/* = false*/) {
m_disable_change_event = !change_event; m_disable_change_event = !change_event;
if (m_opt.nullable) { if (m_opt.nullable) {
const bool m_is_na_val = value.empty() || (boost::any_cast<wxString>(value) == _(L("N/A"))); const bool m_is_na_val = value.empty() || (boost::any_cast<wxString>(value) == _(L("N/A")));
if (!m_is_na_val) if (!m_is_na_val)
m_last_meaningful_value = value; m_last_meaningful_value = value;
text_ctrl()->SetValue(boost::any_cast<wxString>(value)); // BBS
} }
else
text_ctrl()->SetValue(value.empty() ? "" : boost::any_cast<wxString>(value)); // BBS // BBS: null value text_ctrl()->SetValue(value.empty() ? wxString() : boost::any_cast<wxString>(value));
m_disable_change_event = false; m_disable_change_event = false;
if (!change_event) { if (!change_event) {
@@ -1187,18 +1187,24 @@ void CheckBox::set_value(const boost::any& value, bool change_event)
m_disable_change_event = !change_event; m_disable_change_event = !change_event;
if (m_opt.nullable) { if (m_opt.nullable) {
const bool is_value_unsigned_char = value.type() == typeid(unsigned char); const bool is_value_unsigned_char = value.type() == typeid(unsigned char);
bool bool_value = false;
m_is_na_val = value.empty() || (is_value_unsigned_char && m_is_na_val = value.empty() || (is_value_unsigned_char &&
boost::any_cast<unsigned char>(value) == ConfigOptionBoolsNullable::nil_value()); boost::any_cast<unsigned char>(value) == ConfigOptionBoolsNullable::nil_value());
if (!m_is_na_val)
m_last_meaningful_value = is_value_unsigned_char ? value : static_cast<unsigned char>(boost::any_cast<bool>(value));
const auto bool_value = is_value_unsigned_char ? if (!m_is_na_val) {
bool_value = is_value_unsigned_char ?
boost::any_cast<unsigned char>(value) != 0 : boost::any_cast<unsigned char>(value) != 0 :
boost::any_cast<bool>(value); boost::any_cast<bool>(value);
dynamic_cast<::CheckBox*>(window)->SetValue(m_is_na_val ? false : bool_value); // BBS m_last_meaningful_value = is_value_unsigned_char ? value : static_cast<unsigned char>(bool_value);
} }
else if (!value.empty()) // BBS: null value
dynamic_cast<::CheckBox*>(window)->SetValue(bool_value);
}
else if (!value.empty()){ // BBS: null value
dynamic_cast<::CheckBox*>(window)->SetValue(boost::any_cast<bool>(value)); // BBS dynamic_cast<::CheckBox*>(window)->SetValue(boost::any_cast<bool>(value)); // BBS
}
dynamic_cast<::CheckBox*>(window)->SetHalfChecked(value.empty()); dynamic_cast<::CheckBox*>(window)->SetHalfChecked(value.empty());
m_disable_change_event = false; m_disable_change_event = false;
} }
+5
View File
@@ -6127,6 +6127,10 @@ bool GUI_App::process_network_msg(std::string dev_id, std::string msg)
} }
else if (msg == "unsigned_studio") { else if (msg == "unsigned_studio") {
BOOST_LOG_TRIVIAL(info) << "process_network_msg, unsigned_studio"; BOOST_LOG_TRIVIAL(info) << "process_network_msg, unsigned_studio";
// Plugin re-emits this on every subscribe retry; latch it so it shows
// once per connection episode.
if (!m_show_error_msgdlg && !m_unsigned_plugin_warning_shown) {
m_unsigned_plugin_warning_shown = true;
MessageDialog MessageDialog
msg_dlg(nullptr, msg_dlg(nullptr,
_L("To use OrcaSlicer with Bambu Lab printers, you need to enable LAN mode and Developer mode on your printer.\n\n" _L("To use OrcaSlicer with Bambu Lab printers, you need to enable LAN mode and Developer mode on your printer.\n\n"
@@ -6139,6 +6143,7 @@ bool GUI_App::process_network_msg(std::string dev_id, std::string msg)
m_show_error_msgdlg = true; m_show_error_msgdlg = true;
msg_dlg.ShowModal(); msg_dlg.ShowModal();
m_show_error_msgdlg = false; m_show_error_msgdlg = false;
}
return true; return true;
} }
} }
+1
View File
@@ -343,6 +343,7 @@ private:
public: public:
//try again when subscription fails //try again when subscription fails
void on_start_subscribe_again(std::string dev_id); void on_start_subscribe_again(std::string dev_id);
void reset_unsigned_plugin_warning() { m_unsigned_plugin_warning_shown = false; }
std::string get_local_models_path(); std::string get_local_models_path();
bool OnInit() override; bool OnInit() override;
int OnExit() override; int OnExit() override;
@@ -215,8 +215,8 @@ GCodeInputData convert(const Slic3r::GCodeProcessorResult& result, const std::ve
const EOptionType option_type = move_type_to_option(curr_type); const EOptionType option_type = move_type_to_option(curr_type);
if (option_type == EOptionType::COUNT || option_type == EOptionType::Travels || option_type == EOptionType::Wipes) { if (option_type == EOptionType::COUNT || option_type == EOptionType::Travels || option_type == EOptionType::Wipes) {
if (ret.vertices.empty() || prev.type != curr.type || prev.extrusion_role != curr.extrusion_role if (ret.vertices.empty() || prev.type != curr.type || prev.extrusion_role != curr.extrusion_role
// ORCA: Fix issue with flow rate changes being visualized incorrectly // ORCA: Split the path when a preview value changes.
|| prev.mm3_per_mm != curr.mm3_per_mm) { || prev.mm3_per_mm != curr.mm3_per_mm || prev.acceleration != curr.acceleration || prev.jerk != curr.jerk) {
// to allow libvgcode to properly detect the start/end of a path we need to add a 'phantom' vertex // to allow libvgcode to properly detect the start/end of a path we need to add a 'phantom' vertex
// equal to the current one with the exception of the position, which should match the previous move position, // equal to the current one with the exception of the position, which should match the previous move position,
// and the times, which are set to zero // and the times, which are set to zero
+1 -1
View File
@@ -2338,7 +2338,7 @@ void NotificationManager::SharedProfilesNotification::render_text(ImGuiWrapper&
{ {
float hyper_y = starting_y + m_endlines.size() * shift_y + m_line_height * .5f; float hyper_y = starting_y + m_endlines.size() * shift_y + m_line_height * .5f;
float dont_show_y = hyper_y + ImGui::CalcTextSize((m_hypertext + " ").c_str()).y + m_line_height * .5f; float dont_show_y = hyper_y + ImGui::CalcTextSize((m_hypertext + " ").c_str()).y + m_line_height * .5f;
std::string dont_show_text = _u8L("Don't show again") + std::to_string(m_endlines.size()); std::string dont_show_text = _u8L("Don't show again");
ImVec2 part_size = ImGui::CalcTextSize(dont_show_text.c_str()); ImVec2 part_size = ImGui::CalcTextSize(dont_show_text.c_str());
if (!m_multiline && m_lines_count > 2) { if (!m_multiline && m_lines_count > 2) {
+2 -1
View File
@@ -2662,7 +2662,8 @@ bool PartPlate::check_outside(int obj_id, int instance_id, BoundingBoxf3* boundi
if (instance_box.min.z() < SINKING_Z_THRESHOLD) { if (instance_box.min.z() < SINKING_Z_THRESHOLD) {
// Orca: For sinking object, we use a more expensive algorithm so part below build plate won't be considered // Orca: For sinking object, we use a more expensive algorithm so part below build plate won't be considered
if (plate_box.intersects(instance_box)) { // m_plater is null in CLI mode.
if (m_plater && plate_box.intersects(instance_box)) {
// TODO: FIXME: this does not take exclusion area into account // TODO: FIXME: this does not take exclusion area into account
const BuildVolume build_volume(get_shape(), m_plater->build_volume().printable_height(), m_extruder_areas, m_extruder_heights); const BuildVolume build_volume(get_shape(), m_plater->build_volume().printable_height(), m_extruder_areas, m_extruder_heights);
const auto state = instance->calc_print_volume_state(build_volume); const auto state = instance->calc_print_volume_state(build_volume);
+47 -6
View File
@@ -11988,12 +11988,53 @@ bool Plater::priv::check_ams_status_impl(bool is_slice_all)
auto nozzle_volumes_values = preset_bundle->project_config.option<ConfigOptionEnumsGeneric>("nozzle_volume_type")->values; auto nozzle_volumes_values = preset_bundle->project_config.option<ConfigOptionEnumsGeneric>("nozzle_volume_type")->values;
assert(obj->GetExtderSystem()->GetTotalExtderCount() == 2 && nozzle_volumes_values.size() == 2); assert(obj->GetExtderSystem()->GetTotalExtderCount() == 2 && nozzle_volumes_values.size() == 2);
if (obj->GetExtderSystem()->GetTotalExtderCount() == 2 && nozzle_volumes_values.size() == 2) { if (obj->GetExtderSystem()->GetTotalExtderCount() == 2 && nozzle_volumes_values.size() == 2) {
// Map device flow->volume via the table, not `flowtype - 1` (which mis-maps U_FLOW to nvtHybrid). // [Vortek] H2C: Use BBS-style NozzleGroupInfo comparison instead of direct nozzle type match.
NozzleVolumeType right_nozzle_type = DevNozzle::ToNozzleVolumeType(obj->GetExtderSystem()->GetNozzleFlowType(0)); // This correctly handles Hybrid presets (which expand into per-type counts) and detects
NozzleVolumeType left_nozzle_type = DevNozzle::ToNozzleVolumeType(obj->GetExtderSystem()->GetNozzleFlowType(1)); // never-synced state (nozzle_count==0) so the first sync dialog appears.
NozzleVolumeType preset_left_type = NozzleVolumeType(nozzle_volumes_values[0]); // After device sync, extruder_nozzle_stats matches printer → dialog suppressed.
NozzleVolumeType preset_right_type = NozzleVolumeType(nozzle_volumes_values[1]); // Reference to BBS: BambuStudio/src/slic3r/GUI/Plater.cpp is_extruder_stat_synced()
is_same_as_printer = (left_nozzle_type == preset_left_type && right_nozzle_type == preset_right_type); using namespace MultiNozzleUtils;
auto nozzle_diameter_values = preset_bundle->printers.get_edited_preset().config.option<ConfigOptionFloatsNullable>("nozzle_diameter")->values;
// Build preset nozzle groups from extruder_nozzle_stats config
std::vector<std::vector<NozzleGroupInfo>> preset_nozzle_infos(nozzle_diameter_values.size());
for (size_t extruder_id = 0; extruder_id < nozzle_diameter_values.size(); ++extruder_id) {
NozzleVolumeType preset_volume_type = NozzleVolumeType(nozzle_volumes_values[extruder_id]);
std::string preset_diameter = format_diameter_to_str(nozzle_diameter_values[extruder_id]);
if (preset_volume_type == nvtHybrid) {
// Hybrid: expand into separate groups for each nozzle type from stats
int std_count = getExtruderNozzleCount(preset_bundle, extruder_id, nvtStandard);
int hf_count = getExtruderNozzleCount(preset_bundle, extruder_id, nvtHighFlow);
if (std_count > 0)
preset_nozzle_infos[extruder_id].emplace_back(preset_diameter, nvtStandard, extruder_id, std_count);
if (hf_count > 0)
preset_nozzle_infos[extruder_id].emplace_back(preset_diameter, nvtHighFlow, extruder_id, hf_count);
// If both are 0 → never synced → empty group → will mismatch
} else {
int count = getExtruderNozzleCount(preset_bundle, extruder_id, preset_volume_type);
preset_nozzle_infos[extruder_id].emplace_back(preset_diameter, preset_volume_type, extruder_id, count);
}
}
// Compare with printer nozzle groups
auto printer_groups = obj->GetNozzleSystem()->GetNozzleGroups();
for (const auto& preset_groups : preset_nozzle_infos) {
for (const auto& preset_group : preset_groups) {
if (preset_group.nozzle_count == 0) {
// Never synced: if printer has nozzles of this type → needs sync
if (std::find_if(printer_groups.begin(), printer_groups.end(),
[&preset_group](const NozzleGroupInfo& elem) { return preset_group.is_same_type(elem); })
!= printer_groups.end()) {
is_same_as_printer = false;
break;
}
} else if (std::find(printer_groups.begin(), printer_groups.end(), preset_group) == printer_groups.end()) {
is_same_as_printer = false;
break;
}
}
}
} }
std::vector<std::map<int, int>> ams_count_info; std::vector<std::map<int, int>> ams_count_info;
+6 -5
View File
@@ -48,13 +48,14 @@ std::string plugin_defaults_user_script()
return WebViewHostDialog::document_start_injector(css, "orca-plugin-defaults", "beforeend"); return WebViewHostDialog::document_start_injector(css, "orca-plugin-defaults", "beforeend");
} }
// Injected into every page at document start (before the plugin's own scripts). // Injected into the top-level page at document start (before the plugin's own
// Defines window.orca as the only host surface the page may use. It references // scripts). Defines window.orca as the only host surface the page may use. It
// window.wx lazily (at call time) so it never races the backend's deferred // references window.wx lazily (at call time) so it never races the backend's
// registration of the "wx" message handler. Guarded against double-injection so // deferred registration of the "wx" message handler. Guarded against
// it is harmless if also prepended. // double-injection so it is harmless if also prepended.
constexpr char ORCA_BRIDGE_JS[] = R"JS( constexpr char ORCA_BRIDGE_JS[] = R"JS(
(function () { (function () {
if (window.top !== window.self) return;
if (window.orca) return; if (window.orca) return;
var handlers = []; var handlers = [];
function send(kind, data) { function send(kind, data) {
+34 -4
View File
@@ -300,7 +300,12 @@ PluginAvailableActions evaluate_action_policy(const PluginDialogItem& item)
add_action("open_folder", "Show in folder", has_local); add_action("open_folder", "Show in folder", has_local);
if (is_cloud) {
add_action("reinstall_plugin", "Reinstall"); add_action("reinstall_plugin", "Reinstall");
} else {
add_action("reload_plugin", "Reload");
add_action("clear_cache_reload_plugin", "Delete cache and reload");
}
return available_actions; return available_actions;
} }
@@ -739,11 +744,13 @@ void PluginsDialog::handle_plugin_menu_action(const std::string& plugin_key, con
unsubscribe_cloud_plugin(row_data); unsubscribe_cloud_plugin(row_data);
} else if (action == "delete_mine_plugin") { } else if (action == "delete_mine_plugin") {
delete_mine_local_and_cloud_plugin(plugin_key); delete_mine_local_and_cloud_plugin(plugin_key);
} else if (action == "reload_plugin") {
reload_local_plugin(plugin_key, /*clear_cache=*/false);
} else if (action == "clear_cache_reload_plugin") {
reload_local_plugin(plugin_key, /*clear_cache=*/true);
} else if (action == "reinstall_plugin") { } else if (action == "reinstall_plugin") {
if (row_data.is_cloud_plugin()) if (row_data.is_cloud_plugin())
reinstall_cloud_plugin(row_data); reinstall_cloud_plugin(row_data);
else
reinstall_local_plugin(plugin_key);
} }
} }
@@ -1123,7 +1130,7 @@ void PluginsDialog::unsubscribe_cloud_plugin(const PluginDescriptor& plugin)
_L("Unsubscribing plugin"), _L("Deleting local files and unsubscribing plugin...")); _L("Unsubscribing plugin"), _L("Deleting local files and unsubscribing plugin..."));
} }
void PluginsDialog::reinstall_local_plugin(const std::string& plugin_key) void PluginsDialog::reload_local_plugin(const std::string& plugin_key, bool clear_cache)
{ {
if (plugin_key.empty()) if (plugin_key.empty())
return; return;
@@ -1132,11 +1139,34 @@ void PluginsDialog::reinstall_local_plugin(const std::string& plugin_key)
std::pair<bool, std::string> reload_result{false, ""}; std::pair<bool, std::string> reload_result{false, ""};
try { try {
reload_result = run_with_dialog_wait( reload_result = run_with_dialog_wait(
[plugin_key, was_loaded]() -> std::pair<bool, std::string> { [plugin_key, was_loaded, clear_cache]() -> std::pair<bool, std::string> {
PluginManager& manager = PluginManager::instance(); PluginManager& manager = PluginManager::instance();
boost::filesystem::path cache_dir;
if (clear_cache) {
PluginDescriptor descriptor;
if (!manager.try_get_plugin_descriptor(plugin_key, descriptor))
return {false, "Plugin not found."};
boost::filesystem::path resolved_root;
std::string resolve_error;
if (!resolve_allowed_plugin_root(descriptor, {get_orca_plugins_dir()},
"Refusing to clear a plugin cache outside the local plugin directory.",
resolved_root, resolve_error))
return {false, resolve_error};
cache_dir = resolved_root / "__whl_extracted__";
}
if (!manager.unload_plugin(plugin_key)) if (!manager.unload_plugin(plugin_key))
return {false, "Failed to unload plugin."}; return {false, "Failed to unload plugin."};
if (clear_cache) {
boost::system::error_code ec;
boost::filesystem::remove_all(cache_dir, ec);
if (ec)
return {false, "Failed to clear plugin cache: " + ec.message()};
}
manager.load_plugin(plugin_key, false); manager.load_plugin(plugin_key, false);
std::string error; std::string error;
if (!manager.wait_for_plugin_load(plugin_key, std::chrono::minutes(5), error) || !manager.is_plugin_loaded(plugin_key)) if (!manager.wait_for_plugin_load(plugin_key, std::chrono::minutes(5), error) || !manager.is_plugin_loaded(plugin_key))
+1 -1
View File
@@ -96,7 +96,7 @@ private:
void open_plugin_folder(const Slic3r::PluginDescriptor& plugin); void open_plugin_folder(const Slic3r::PluginDescriptor& plugin);
void delete_local_plugin(const Slic3r::PluginDescriptor& plugin); void delete_local_plugin(const Slic3r::PluginDescriptor& plugin);
void unsubscribe_cloud_plugin(const Slic3r::PluginDescriptor& plugin); void unsubscribe_cloud_plugin(const Slic3r::PluginDescriptor& plugin);
void reinstall_local_plugin(const std::string& plugin_key); void reload_local_plugin(const std::string& plugin_key, bool clear_cache);
void reinstall_cloud_plugin(const Slic3r::PluginDescriptor& plugin); void reinstall_cloud_plugin(const Slic3r::PluginDescriptor& plugin);
void delete_mine_local_and_cloud_plugin(const std::string& plugin_key); void delete_mine_local_and_cloud_plugin(const std::string& plugin_key);
+55
View File
@@ -218,6 +218,49 @@ void PrePrintChecker::add_with_link(PrintDialogStatus state, wxString msg, wxStr
} }
} }
// Orca: acknowledgement-checkbox variant of add(); see add_with_link() for the shared classification.
void PrePrintChecker::add_with_checkbox(PrintDialogStatus state, wxString msg, wxString checkbox_label, bool checked, std::function<void(bool)> checkbox_callback)
{
prePrintInfo info;
if (is_error(state)) {
info.level = prePrintInfoLevel::Error;
} else if (is_warning(state)) {
info.level = prePrintInfoLevel::Warning;
} else {
info.level = prePrintInfoLevel::Normal;
}
if (is_error_printer(state)) {
info.type = prePrintInfoType::Printer;
} else if (is_error_filament(state)) {
info.type = prePrintInfoType::Filament;
} else if (is_warning_printer(state)) {
info.type = prePrintInfoType::Printer;
} else if (is_warning_filament(state)) {
info.type = prePrintInfoType::Filament;
}
info.msg = msg;
info.checkbox_label = checkbox_label;
info.checkbox_checked = checked;
info.checkbox_callback = checkbox_callback;
switch (info.type) {
case prePrintInfoType::Filament:
if (std::find(filamentList.begin(), filamentList.end(), info) == filamentList.end()) {
filamentList.push_back(info);
}
break;
case prePrintInfoType::Printer:
if (std::find(printerList.begin(), printerList.end(), info) == printerList.end()) {
printerList.push_back(info);
}
break;
default: break;
}
}
//void PrePrintMsgBoard::add(const wxString &msg, const wxString &tips, bool is_error) //void PrePrintMsgBoard::add(const wxString &msg, const wxString &tips, bool is_error)
//{ //{
@@ -310,6 +353,18 @@ bool PrinterMsgPanel::UpdateInfos(const std::vector<prePrintInfo>& infos)
label->Show(); label->Show();
m_sizer->Add(label, 0, wxBOTTOM, FromDIP(4)); m_sizer->Add(label, 0, wxBOTTOM, FromDIP(4));
} }
// Orca: acknowledgement checkbox rendered beneath the message (e.g. a nozzle diameter that
// differs from the one the printer remembers, which the user may legitimately override).
if (!info.checkbox_label.empty())
{
wxCheckBox* checkbox = new wxCheckBox(this, wxID_ANY, info.checkbox_label);
checkbox->SetForegroundColour(_GetLabelColour(info));
checkbox->SetValue(info.checkbox_checked);
auto cb = info.checkbox_callback;
checkbox->Bind(wxEVT_CHECKBOX, [cb](wxCommandEvent& e) { if (cb) cb(e.IsChecked()); });
m_sizer->Add(checkbox, 0, wxBOTTOM, FromDIP(4));
}
} }
this->Show(); this->Show();
+11 -2
View File
@@ -26,6 +26,9 @@ struct prePrintInfo
wxString wiki_url; wxString wiki_url;
wxString link_label; // optional: clickable text appended after msg wxString link_label; // optional: clickable text appended after msg
std::function<void()> link_callback; // optional: internal action for link_label click std::function<void()> link_callback; // optional: internal action for link_label click
wxString checkbox_label; // optional: an acknowledgement checkbox under msg
bool checkbox_checked{false};
std::function<void(bool)> checkbox_callback; // called with the new state on toggle
int index{0}; int index{0};
public: public:
@@ -33,8 +36,9 @@ public:
return level == other.level && type == other.type && return level == other.level && type == other.type &&
msg == other.msg && tips == other.tips && msg == other.msg && tips == other.tips &&
wiki_url == other.wiki_url && link_label == other.link_label && wiki_url == other.wiki_url && link_label == other.link_label &&
checkbox_label == other.checkbox_label && checkbox_checked == other.checkbox_checked &&
index == other.index; index == other.index;
// link_callback excluded: std::function is not comparable // link_callback / checkbox_callback excluded: std::function is not comparable
} }
}; };
@@ -59,7 +63,6 @@ enum PrintDialogStatus : unsigned int {
PrintStatusInPrinting, PrintStatusInPrinting,
PrintStatusNozzleMatchInvalid, PrintStatusNozzleMatchInvalid,
PrintStatusNozzleDataInvalid, PrintStatusNozzleDataInvalid,
PrintStatusNozzleDiameterMismatch,
PrintStatusNozzleTypeMismatch, PrintStatusNozzleTypeMismatch,
PrintStatusNozzleNoMatchedHotends, PrintStatusNozzleNoMatchedHotends,
PrintStatusNozzleRackMaximumInstalled, PrintStatusNozzleRackMaximumInstalled,
@@ -106,6 +109,9 @@ enum PrintDialogStatus : unsigned int {
PrintStatusFilaSwitcherSlicingNotMatch, PrintStatusFilaSwitcherSlicingNotMatch,
PrintStatusRackNozzleNumUnmeetWarning, PrintStatusRackNozzleNumUnmeetWarning,
PrintStatusHasUnreliableNozzleWarning, PrintStatusHasUnreliableNozzleWarning,
// Orca: a nozzle diameter that differs from the one the printer remembers is a warning,
// not an error, so non-standard nozzles can still be printed with.
PrintStatusNozzleDiameterMismatch,
PrintStatusPrinterWarningEnd, PrintStatusPrinterWarningEnd,
// Warnings for filament // Warnings for filament
@@ -159,6 +165,9 @@ public:
void add(PrintDialogStatus state, wxString msg, wxString tip, const wxString& wiki_url); void add(PrintDialogStatus state, wxString msg, wxString tip, const wxString& wiki_url);
// Orca: minimal callback-link render path instead of the full style-bitmask machinery. // Orca: minimal callback-link render path instead of the full style-bitmask machinery.
void add_with_link(PrintDialogStatus state, wxString msg, wxString link_label, std::function<void()> link_callback); void add_with_link(PrintDialogStatus state, wxString msg, wxString link_label, std::function<void()> link_callback);
// Orca: render msg with an acknowledgement checkbox beneath it, for an overridable warning that
// the user must tick before proceeding (checkbox_callback reports the new state).
void add_with_checkbox(PrintDialogStatus state, wxString msg, wxString checkbox_label, bool checked, std::function<void(bool)> checkbox_callback);
static ::std::string get_print_status_info(PrintDialogStatus status); static ::std::string get_print_status_info(PrintDialogStatus status);
wxString get_pre_state_msg(PrintDialogStatus status); wxString get_pre_state_msg(PrintDialogStatus status);
+4 -1
View File
@@ -246,7 +246,10 @@ void PrinterWebView::SendAPIKey()
// RemoveAllUserScripts causes WebView to forget about our script message handler, // RemoveAllUserScripts causes WebView to forget about our script message handler,
// so re-add it here. // so re-add it here.
m_browser->RemoveScriptMessageHandler("wx"); m_browser->RemoveScriptMessageHandler("wx");
m_browser->AddScriptMessageHandler("wx"); if (m_browser->AddScriptMessageHandler("wx"))
WebView::MarkScriptMessageHandlerAdded(m_browser);
else
wxLogError("Could not add script message handler");
#ifdef __linux__ #ifdef __linux__
// Re-inject the vue-resize/WebKitGTK workaround that RemoveAllUserScripts just cleared. // Re-inject the vue-resize/WebKitGTK workaround that RemoveAllUserScripts just cleared.
+26 -2
View File
@@ -2269,8 +2269,11 @@ void SelectMachineDialog::show_status(PrintDialogStatus status, std::vector<wxSt
Enable_Refresh_Button(true); Enable_Refresh_Button(true);
Enable_Send_Button(false); Enable_Send_Button(false);
} else if (status == PrintStatusNozzleDiameterMismatch) { } else if (status == PrintStatusNozzleDiameterMismatch) {
// Orca: overridable — a non-standard nozzle is a valid reason to differ. Send is gated on
// the acknowledgement checkbox added to the message board below (add_with_checkbox), which
// is enabled only while the user's acknowledgement still matches the current mismatch.
Enable_Refresh_Button(true); Enable_Refresh_Button(true);
Enable_Send_Button(false); Enable_Send_Button(!m_nozzle_diameter_ack_msg.empty() && m_nozzle_diameter_ack_msg == m_nozzle_diameter_mismatch_msg);
} else if (status == PrintStatusNozzleTypeMismatch) { } else if (status == PrintStatusNozzleTypeMismatch) {
Enable_Refresh_Button(true); Enable_Refresh_Button(true);
Enable_Send_Button(false); Enable_Send_Button(false);
@@ -2470,7 +2473,19 @@ void SelectMachineDialog::show_status(PrintDialogStatus status, std::vector<wxSt
/*enter perpare mode*/ /*enter perpare mode*/
prepare_mode(false); prepare_mode(false);
if (status == PrintDialogStatus::PrintStatusNozzleDiameterMismatch) {
// Short label on purpose: the 420px message panel caps its width and a wxCheckBox label
// does not wrap; the full explanation is in the warning text above it.
m_pre_print_checker.add_with_checkbox(status, msg,
_L("I have checked the installed nozzle and want to print anyway."),
!m_nozzle_diameter_ack_msg.empty() && m_nozzle_diameter_ack_msg == m_nozzle_diameter_mismatch_msg,
[this](bool checked) {
m_nozzle_diameter_ack_msg = checked ? m_nozzle_diameter_mismatch_msg : wxString();
Enable_Send_Button(checked);
});
} else {
m_pre_print_checker.add(status, msg, tips, wiki_url); m_pre_print_checker.add(status, msg, tips, wiki_url);
}
} }
@@ -4565,8 +4580,14 @@ bool SelectMachineDialog::CheckErrorExtruderNozzleWithSlicing(MachineObject* obj
} }
msg_params.emplace_back(_L("Tips: If you changed your nozzle of your printer lately, please go to 'Device -> Printer parts' to change your nozzle setting.")); msg_params.emplace_back(_L("Tips: If you changed your nozzle of your printer lately, please go to 'Device -> Printer parts' to change your nozzle setting."));
// Orca: non-blocking. A diameter that differs from the one the printer
// remembers is legitimate with a non-standard nozzle, so the print is held back
// only by the acknowledgement checkbox shown in the message board, not by a
// disabled Send outright. Keep checking the remaining extruders.
m_nozzle_diameter_mismatch_msg = msg_params.front();
show_status(PrintDialogStatus::PrintStatusNozzleDiameterMismatch, msg_params); show_status(PrintDialogStatus::PrintStatusNozzleDiameterMismatch, msg_params);
return false; continue;
} }
} }
} }
@@ -4608,6 +4629,9 @@ static wxString _get_ext_loc_str(const std::unordered_set<int>& extruders, int t
void SelectMachineDialog::update_show_status(MachineObject* obj_) void SelectMachineDialog::update_show_status(MachineObject* obj_)
{ {
m_pre_print_checker.clear(); m_pre_print_checker.clear();
// Orca: re-raised by CheckErrorExtruderNozzleWithSlicing() below if the mismatch is still there,
// so an early return from this pass cannot leave a stale warning behind.
m_nozzle_diameter_mismatch_msg.clear();
/*agent check and printer valid check*/ /*agent check and printer valid check*/
NetworkAgent* agent = Slic3r::GUI::wxGetApp().getAgent(); NetworkAgent* agent = Slic3r::GUI::wxGetApp().getAgent();
+7
View File
@@ -550,6 +550,13 @@ public:
// Compare the slicing file's nozzle requirements (validity, flow, diameter) against the // Compare the slicing file's nozzle requirements (validity, flow, diameter) against the
// printer; the rack extruder is checked against its whole inventory (mounted + rack). // printer; the rack extruder is checked against its whole inventory (mounted + rack).
bool CheckErrorExtruderNozzleWithSlicing(MachineObject* obj_);//return true if no errors bool CheckErrorExtruderNozzleWithSlicing(MachineObject* obj_);//return true if no errors
// Orca: a nozzle diameter differing from the one the printer remembers is a non-blocking
// warning shown in the message board with an acknowledgement checkbox; Send stays disabled
// until the user ticks it. m_nozzle_diameter_mismatch_msg is the current mismatch text (empty
// when there is none), m_nozzle_diameter_ack_msg the text the user acknowledged; Send is
// allowed only while the two match, so a changed or cleared mismatch re-requires acknowledgement.
wxString m_nozzle_diameter_mismatch_msg;
wxString m_nozzle_diameter_ack_msg;
// Warn (without blocking) when a mapped filament would be printed from both extruders on a // Warn (without blocking) when a mapped filament would be printed from both extruders on a
// dual-extruder printer, so per-nozzle manual K-value can't follow it across the whole print. // dual-extruder printer, so per-nozzle manual K-value can't follow it across the whole print.
bool CheckWarningFilamentCrossExtruder(MachineObject* obj_); bool CheckWarningFilamentCrossExtruder(MachineObject* obj_);
+24 -1
View File
@@ -34,6 +34,7 @@
#include "GUI_App.hpp" #include "GUI_App.hpp"
#include "GUI_ObjectList.hpp" #include "GUI_ObjectList.hpp"
#include "slic3r/Utils/PresetUpdater.hpp" #include "slic3r/Utils/PresetUpdater.hpp"
#include "slic3r/plugin/PluginConfig.hpp"
#include "Plater.hpp" #include "Plater.hpp"
#include "MainFrame.hpp" #include "MainFrame.hpp"
#include "format.hpp" #include "format.hpp"
@@ -1795,9 +1796,19 @@ void Tab::on_value_change(const std::string& opt_key, const boost::any& value)
// Keep this preset's "plugins" manifest in sync when a plugin picker changes, so full_config() and // Keep this preset's "plugins" manifest in sync when a plugin picker changes, so full_config() and
// save_to_json() always find resolved "name;uuid;capability" references and rebuild it nowhere else. // save_to_json() always find resolved "name;uuid;capability" references and rebuild it nowhere else.
// Also drop any plugin_config_overrides entries for a capability the change just stopped
// referencing (e.g. a plugin removed from slicing_pipeline_plugin), so a saved preset never
// carries configuration for a capability it no longer names. The Configure button is a separate
// field holding its own cached copy of that value, so it needs to be told explicitly, or it
// keeps showing the stale count until something else happens to refresh it.
if (const ConfigOptionDef* opt_def = m_config->def()->get(opt_key); if (const ConfigOptionDef* opt_def = m_config->def()->get(opt_key);
opt_def && opt_def->is_plugin_backed()) opt_def && opt_def->is_plugin_backed()) {
m_config->update_plugin_manifest(); m_config->update_plugin_manifest();
if (prune_stale_plugin_overrides(*m_config)) {
if (Field* overrides_field = get_field(PLUGIN_OVERRIDES_OPTION_KEY))
overrides_field->set_value(boost::any(m_config->opt_string(PLUGIN_OVERRIDES_OPTION_KEY)), false);
}
}
if (opt_key == "gcode_flavor" && m_type == Preset::TYPE_PRINTER) { if (opt_key == "gcode_flavor" && m_type == Preset::TYPE_PRINTER) {
if (auto printer_tab = dynamic_cast<TabPrinter*>(this)) if (auto printer_tab = dynamic_cast<TabPrinter*>(this))
@@ -3288,10 +3299,22 @@ static std::vector<std::string> intersect(std::vector<std::string> const& l, std
static std::vector<std::string> concat(std::vector<std::string> const& l, std::vector<std::string> const& r) static std::vector<std::string> concat(std::vector<std::string> const& l, std::vector<std::string> const& r)
{ {
std::vector<std::string> t; std::vector<std::string> t;
bool l_is_sorted = std::is_sorted(l.begin(), l.end());
bool r_is_sorted = std::is_sorted(r.begin(), r.end());
if (l_is_sorted && r_is_sorted) {
std::set_union(l.begin(), l.end(), r.begin(), r.end(), std::back_inserter(t)); std::set_union(l.begin(), l.end(), r.begin(), r.end(), std::back_inserter(t));
return t; return t;
} }
std::vector<std::string> l_sorted = l;
std::vector<std::string> r_sorted = r;
std::sort(l_sorted.begin(), l_sorted.end());
std::sort(r_sorted.begin(), r_sorted.end());
std::set_union(l_sorted.begin(), l_sorted.end(), r_sorted.begin(), r_sorted.end(), std::back_inserter(t));
return t;
}
static std::vector<std::string> substruct(std::vector<std::string> const& l, std::vector<std::string> const& r) static std::vector<std::string> substruct(std::vector<std::string> const& l, std::vector<std::string> const& r)
{ {
std::vector<std::string> t; std::vector<std::string> t;
+1 -1
View File
@@ -186,7 +186,7 @@ TroubleshootDialog::TroubleshootDialog()
Fit(); Fit();
}); });
auto link_wiki = new HyperLink(this, _L("Wiki Guide")); auto link_wiki = new HyperLink(this, _L("Wiki Guide"), "https://www.orcaslicer.com/wiki/troubleshoot_center");
// RIGHT SIZER ////////////////////// // RIGHT SIZER //////////////////////
+20
View File
@@ -242,8 +242,15 @@ public:
g_webviews.erase(iter); g_webviews.erase(iter);
} }
wxWebView *m_webView; wxWebView *m_webView;
// Guards against registering the "wx" handler twice (a duplicate throws on WKWebView).
bool m_script_handler_added = false;
}; };
static WebViewRef *webview_ref(wxWebView *webView)
{
return webView ? static_cast<WebViewRef *>(webView->GetRefData()) : nullptr;
}
wxWebView* WebView::CreateWebView(wxWindow * parent, wxString const & url) wxWebView* WebView::CreateWebView(wxWindow * parent, wxString const & url)
{ {
#if wxUSE_WEBVIEW_EDGE #if wxUSE_WEBVIEW_EDGE
@@ -304,10 +311,17 @@ wxWebView* WebView::CreateWebView(wxWindow * parent, wxString const & url)
Slic3r::GUI::WKWebView_setTransparentBackground(wkWebView); Slic3r::GUI::WKWebView_setTransparentBackground(wkWebView);
#endif #endif
auto addScriptMessageHandler = [] (wxWebView *webView) { auto addScriptMessageHandler = [] (wxWebView *webView) {
// Skip if SendAPIKey() already registered "wx"; a duplicate add throws an
// uncatchable NSException on WKWebView, killing the app at startup.
WebViewRef *ref = webview_ref(webView);
if (ref && ref->m_script_handler_added)
return;
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": begin to add script message handler for wx."; BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": begin to add script message handler for wx.";
Slic3r::GUI::wxGetApp().set_adding_script_handler(true); Slic3r::GUI::wxGetApp().set_adding_script_handler(true);
if (!webView->AddScriptMessageHandler("wx")) if (!webView->AddScriptMessageHandler("wx"))
wxLogError("Could not add script message handler"); wxLogError("Could not add script message handler");
else if (ref)
ref->m_script_handler_added = true;
Slic3r::GUI::wxGetApp().set_adding_script_handler(false); Slic3r::GUI::wxGetApp().set_adding_script_handler(false);
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": finished add script message handler for wx."; BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": finished add script message handler for wx.";
}; };
@@ -336,6 +350,12 @@ wxWebView* WebView::CreateWebView(wxWindow * parent, wxString const & url)
g_webviews.push_back(webView); g_webviews.push_back(webView);
return webView; return webView;
} }
void WebView::MarkScriptMessageHandlerAdded(wxWebView * webView)
{
if (WebViewRef *ref = webview_ref(webView))
ref->m_script_handler_added = true;
}
#if wxUSE_WEBVIEW_EDGE #if wxUSE_WEBVIEW_EDGE
bool WebView::CheckWebViewRuntime() bool WebView::CheckWebViewRuntime()
{ {
+3
View File
@@ -18,6 +18,9 @@ public:
static bool RunScript(wxWebView * webView, wxString const & msg); static bool RunScript(wxWebView * webView, wxString const & msg);
// Marks "wx" as registered so CreateWebView's deferred add skips the duplicate.
static void MarkScriptMessageHandlerAdded(wxWebView * webView);
static void RecreateAll(); static void RecreateAll();
}; };
@@ -96,6 +96,9 @@ std::string WebViewHostDialog::document_start_injector(const std::string& markup
const std::string literal = nlohmann::json(markup).dump(); const std::string literal = nlohmann::json(markup).dump();
std::string s; std::string s;
s += "(function(){"; s += "(function(){";
// wxWebView's AddUserScript runs in child frames too (including cross-origin
// frames on WebView2). Host theme state belongs only to the top-level page.
s += "if(window.top!==window.self)return;";
s += prelude; s += prelude;
s += "var css=" + literal + ";"; s += "var css=" + literal + ";";
s += "function inject(){"; s += "function inject(){";
+59
View File
@@ -5,6 +5,7 @@
#include <boost/log/trivial.hpp> #include <boost/log/trivial.hpp>
#include <boost/nowide/fstream.hpp> #include <boost/nowide/fstream.hpp>
#include <libslic3r/Config.hpp>
#include <libslic3r/PresetBundle.hpp> #include <libslic3r/PresetBundle.hpp>
#include <libslic3r/PrintConfig.hpp> #include <libslic3r/PrintConfig.hpp>
#include <slic3r/GUI/GUI.hpp> #include <slic3r/GUI/GUI.hpp>
@@ -164,6 +165,20 @@ bool CapabilityConfigDocument::erase(const PluginCapabilityId& id)
return erased; return erased;
} }
bool CapabilityConfigDocument::prune_unreferenced(const std::set<std::pair<PluginCapabilityType, std::string>>& referenced)
{
bool changed = false;
for (auto it = m_entries.begin(); it != m_entries.end();) {
if (referenced.count({it->first.type, it->first.name}) != 0) {
++it;
} else {
it = m_entries.erase(it);
changed = true;
}
}
return changed;
}
bool CapabilityConfigDocument::empty() const bool CapabilityConfigDocument::empty() const
{ {
return m_entries.empty() && m_opaque_entries.empty(); return m_entries.empty() && m_opaque_entries.empty();
@@ -342,6 +357,50 @@ std::string serialize_plugin_overrides(const CapabilityConfigDocument& document)
return document.empty() ? std::string() : document.serialize_entries().dump(); return document.empty() ? std::string() : document.serialize_entries().dump();
} }
bool prune_stale_plugin_overrides(DynamicConfig& config)
{
const auto* overrides_opt = dynamic_cast<const ConfigOptionString*>(config.option(PLUGIN_OVERRIDES_OPTION_KEY));
if (overrides_opt == nullptr || overrides_opt->value.empty())
return false;
CapabilityConfigDocument overrides;
std::string error;
if (!parse_plugin_overrides(overrides_opt->value, overrides, error)) {
// Malformed text is not ours to fix up here: leave it untouched rather than risk
// discarding data the user might still be able to recover.
BOOST_LOG_TRIVIAL(error) << "prune_stale_plugin_overrides: " << error;
return false;
}
// Capability names currently referenced by a plugin-backed option's value(s) — e.g.
// slicing_pipeline_plugin's ConfigOptionStrings entries name SlicingPipeline capabilities
// directly, the same raw values save_plugin_collection() resolves into the "plugins" manifest.
std::set<std::pair<PluginCapabilityType, std::string>> referenced;
const ConfigDef* def = config.def();
for (const std::string& opt_key : config.keys()) {
const ConfigOptionDef* opt_def = def != nullptr ? def->get(opt_key) : nullptr;
if (opt_def == nullptr || !opt_def->is_plugin_backed())
continue;
const ConfigOption* opt = config.option(opt_key);
const PluginCapabilityType type = plugin_capability_type_from_string(opt_def->plugin_type);
if (const auto* string_opt = dynamic_cast<const ConfigOptionString*>(opt)) {
if (!string_opt->value.empty())
referenced.emplace(type, string_opt->value);
} else if (const auto* vector_opt = dynamic_cast<const ConfigOptionVectorBase*>(opt)) {
for (const std::string& value : vector_opt->vserialize())
if (!value.empty())
referenced.emplace(type, value);
}
}
if (!overrides.prune_unreferenced(referenced))
return false;
config.set_key_value(PLUGIN_OVERRIDES_OPTION_KEY, new ConfigOptionString(serialize_plugin_overrides(overrides)));
return true;
}
EffectiveCapabilityConfig PresetPluginConfigService::get_effective_config(const CapabilityConfigDocument& overrides, EffectiveCapabilityConfig PresetPluginConfigService::get_effective_config(const CapabilityConfigDocument& overrides,
const PluginCapabilityId& id) const const PluginCapabilityId& id) const
{ {
+14
View File
@@ -8,7 +8,9 @@
#include <map> #include <map>
#include <mutex> #include <mutex>
#include <optional> #include <optional>
#include <set>
#include <string> #include <string>
#include <utility>
#include <vector> #include <vector>
#define PLUGIN_CONFIG_DIR "config.json" #define PLUGIN_CONFIG_DIR "config.json"
@@ -16,6 +18,7 @@
namespace Slic3r { namespace Slic3r {
class Preset; class Preset;
class DynamicConfig;
struct CapabilityConfigEntry struct CapabilityConfigEntry
{ {
PluginCapabilityId id; PluginCapabilityId id;
@@ -35,6 +38,9 @@ public:
bool contains(const PluginCapabilityId& id) const; bool contains(const PluginCapabilityId& id) const;
bool upsert(CapabilityConfigEntry entry); bool upsert(CapabilityConfigEntry entry);
bool erase(const PluginCapabilityId& id); bool erase(const PluginCapabilityId& id);
// Drops every entry whose (type, name) is not in `referenced`, e.g. capabilities a preset's
// plugin-backed options no longer name. Returns true if anything was removed.
bool prune_unreferenced(const std::set<std::pair<PluginCapabilityType, std::string>>& referenced);
bool empty() const; bool empty() const;
nlohmann::json serialize_entries() const; nlohmann::json serialize_entries() const;
nlohmann::json root_json() const; nlohmann::json root_json() const;
@@ -50,6 +56,14 @@ std::string plugin_overrides_of(const Preset& preset);
bool parse_plugin_overrides(const std::string& raw, CapabilityConfigDocument& document, std::string& error); bool parse_plugin_overrides(const std::string& raw, CapabilityConfigDocument& document, std::string& error);
std::string serialize_plugin_overrides(const CapabilityConfigDocument& document); std::string serialize_plugin_overrides(const CapabilityConfigDocument& document);
// Drops plugin_config_overrides entries for capabilities no longer named by any plugin-backed
// option's current value in `config` (e.g. slicing_pipeline_plugin cleared or switched to a
// different capability), and writes the result back if anything changed. Called wherever a
// plugin-backed option's value changes, so a saved preset never carries configuration for a
// capability it no longer references. Returns true if `config` was modified, so a caller holding a
// GUI field over PLUGIN_OVERRIDES_OPTION_KEY knows it must refresh that field's displayed value.
bool prune_stale_plugin_overrides(DynamicConfig& config);
struct EffectiveCapabilityConfig struct EffectiveCapabilityConfig
{ {
PluginCapabilityId id; PluginCapabilityId id;
+1 -5
View File
@@ -54,7 +54,7 @@ struct PluginDescriptor
std::string description; // Plugin description std::string description; // Plugin description
std::string author; // Plugin author from manifest, if available std::string author; // Plugin author from manifest, if available
std::string version; // Selected plugin version std::string version; // Selected plugin version
std::string latest_version; // Latest available cloud version fallback when changelog is unavailable. std::string latest_version; // Authoritative latest available cloud version.
std::string installed_version; // Locally installed package version. Preserved across cloud merges, which overwrite `version` with the latest cloud version. Empty when not installed. std::string installed_version; // Locally installed package version. Preserved across cloud merges, which overwrite `version` with the latest cloud version. Empty when not installed.
std::vector<std::string> display_types; // Display-only "compatibility" labels (cloud: raw service labels; local: from real capabilities). Never used for dispatch. std::vector<std::string> display_types; // Display-only "compatibility" labels (cloud: raw service labels; local: from real capabilities). Never used for dispatch.
std::string plugin_root; // Installed plugin directory, even when entry_path is invalid or ambiguous. std::string plugin_root; // Installed plugin directory, even when entry_path is invalid or ambiguous.
@@ -117,10 +117,6 @@ struct PluginDescriptor
bool is_unauthorized() const { return get_update_status() == PluginUpdateStatus::Unauthorized; } bool is_unauthorized() const { return get_update_status() == PluginUpdateStatus::Unauthorized; }
std::string latest_available_version() const std::string latest_available_version() const
{ {
for (const PluginChangelog& entry : changelog) {
if (!entry.version.empty())
return entry.version;
}
if (!latest_version.empty()) if (!latest_version.empty())
return latest_version; return latest_version;
return version; return version;
+88
View File
@@ -0,0 +1,88 @@
# Compare Analyzer — G-code Slicing Comparison Tools
Tools for deep comparison and analysis of `.3mf` slicing project files, designed for
verifying multi-nozzle (H2C carousel) and multi-extruder slicing correctness.
## Tools
### `compare_slices.py` — Slice Comparison Analyzer
Deep comparison of two `.3mf` files (OrcaSlicer, BambuStudio, or any compatible slicer).
Generates a comprehensive Markdown report covering:
- **Filament usage** — per-filament weight/length with color mapping
- **Nozzle/extruder mapping** — Vortek carousel slot assignments
- **Tool change sequences** — T-code ordering and count
- **Prime tower analysis** — tower entries, G-code line count
- **Temperature timeline** — pre-heat lead times, target temperatures per tool change
- **Retract parameters** — M620.11 analysis during nozzle switches
- **Filament change G-code blocks** — line-by-line diff of change_filament_gcode
- **Control command diff** — timeline of M/G-code differences
- **Critical discrepancy detection** — automatic flagging of weight/time anomalies
#### Usage
```bash
# Compare two slice files
python3 compare_slices.py file1.3mf file2.3mf
# With custom labels
python3 compare_slices.py file1.3mf file2.3mf --labels "Upstream" "Fixed"
```
#### Output
Markdown report saved to `mp_reports/compare_report_YYYYMMDD_HHMMSS.md`
#### Example: Detecting H2C purge regression
```
⚠️ CRITICAL DISCREPANCY: Huge difference in part weight:
OrcaSlicer 60.90 g vs BambuStudio 17.47 g (difference 43.43 g or 71.3%).
The reason is incorrect nozzle mapping, causing huge AMS flushing.
```
---
### `show_temp_plot.py` — Temperature Timeline Plotter
Generates interactive HTML temperature plots for analyzing thermal profiles during
multi-nozzle prints. Visualizes heater temperature commands (M104/M109) per tool change,
showing pre-heat timing and temperature convergence.
#### Architecture
- H2C dual-extruder layout with Vortek carousel nozzles
- Physical heaters mapped dynamically:
- Heater 0: Extruder 2 (right nozzle slot, T0/T2/T3/T4)
- Heater 1: Extruder 1 (left nozzle slot, T1)
- Active heater mapping derived from G-code temperature signals
#### Usage
```bash
# Single file analysis
python3 show_temp_plot.py file.3mf
# Side-by-side comparison of two files
python3 show_temp_plot.py file1.3mf file2.3mf
```
#### Output
Interactive HTML report saved to Desktop as `temp_plot_v3.html`
---
## Requirements
- **Python 3.8+**
- **No external dependencies** — uses only Python standard library
(`json`, `zipfile`, `xml.etree.ElementTree`, `difflib`, `webbrowser`)
## Use Cases
1. **Regression testing** — compare slices before/after code changes to verify
no unintended differences in purge volumes, tool ordering, or temperature timing
2. **BBS compatibility verification** — compare OrcaSlicer output against BambuStudio
reference slices to ensure behavioral parity
3. **H2C carousel validation** — verify per-slot nozzle tracking produces correct
purge volumes (not collapsed per-extruder)
4. **Temperature protocol analysis** — verify pre-heat lead times and cooling
temperatures during nozzle changes match expected profiles
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+222
View File
@@ -1,12 +1,18 @@
#include <catch2/catch_all.hpp> #include <catch2/catch_all.hpp>
#include <algorithm>
#include <cmath>
#include <map>
#include <numeric> #include <numeric>
#include <sstream> #include <sstream>
#include <string>
#include <vector>
#include "libslic3r/ClipperUtils.hpp" #include "libslic3r/ClipperUtils.hpp"
#include "libslic3r/Fill/Fill.hpp" #include "libslic3r/Fill/Fill.hpp"
#include "libslic3r/Flow.hpp" #include "libslic3r/Flow.hpp"
#include "libslic3r/Geometry.hpp" #include "libslic3r/Geometry.hpp"
#include "libslic3r/Layer.hpp"
#include "libslic3r/Print.hpp" #include "libslic3r/Print.hpp"
#include "libslic3r/SVG.hpp" #include "libslic3r/SVG.hpp"
#include "libslic3r/libslic3r.h" #include "libslic3r/libslic3r.h"
@@ -476,3 +482,219 @@ bool test_if_solid_surface_filled(const ExPolygon& expolygon, double flow_spacin
return uncovered.empty(); // solid surface is fully filled return uncovered.empty(); // solid surface is fully filled
} }
// Length-weighted dominant direction of the layer's role_wanted extrusions, whole degrees
// [0, 180), or -1 if it has none. Needs a line pattern such as monotonic or rectilinear.
template<typename RolePred> static int dominant_fill_angle(const Layer &layer, RolePred role_wanted)
{
std::map<int, double> weight_per_degree;
auto account = [&weight_per_degree, &role_wanted](const ExtrusionPath &path) {
if (!role_wanted(path.role()))
return;
const Points3 &pts = path.polyline.points;
for (size_t i = 1; i < pts.size(); ++i) {
const double dx = double(pts[i].x() - pts[i - 1].x());
const double dy = double(pts[i].y() - pts[i - 1].y());
const double len = std::hypot(dx, dy);
if (len <= 0.)
continue;
int deg = int(std::lround(Geometry::rad2deg(std::atan2(dy, dx)))) % 180;
if (deg < 0)
deg += 180;
weight_per_degree[deg] += len;
}
};
for (const LayerRegion *region : layer.regions())
for (const ExtrusionEntity *entity : region->fills.flatten().entities) {
if (auto *path = dynamic_cast<const ExtrusionPath *>(entity))
account(*path);
else if (auto *multi = dynamic_cast<const ExtrusionMultiPath *>(entity))
for (const ExtrusionPath &p : multi->paths)
account(p);
else if (auto *loop = dynamic_cast<const ExtrusionLoop *>(entity))
for (const ExtrusionPath &p : loop->paths)
account(p);
}
if (weight_per_degree.empty())
return -1;
return std::max_element(weight_per_degree.begin(), weight_per_degree.end(),
[](const auto &a, const auto &b) { return a.second < b.second; })->first;
}
template<typename RolePred> static std::vector<int> angles_per_layer(const Print &print, RolePred role_wanted)
{
std::vector<int> angles;
for (const Layer *layer : print.objects().front()->layers())
angles.push_back(dominant_fill_angle(*layer, role_wanted));
return angles;
}
static bool solid_role(ExtrusionRole role) { return is_solid_infill(role) && role != erIroning; }
static bool sparse_role(ExtrusionRole role) { return role == erInternalInfill; }
static bool ironing_role(ExtrusionRole role) { return role == erIroning; }
TEST_CASE("Infill rotation template is unaffected by a raft", "[Fill][Regression]")
{
// More angles than raft layers, so a raft cannot alias back to the same angle.
const std::string template_string = GENERATE("+45", "0,25,50,75,100,125,150");
const int raft_layers = GENERATE(1, 3);
CAPTURE(template_string, raft_layers);
auto angles_for = [&template_string](int rafts) {
Print print;
// 100% density makes every layer solid, so the template shows on all 100, not just shells.
Slic3r::Test::init_and_process_print({Slic3r::Test::cube(20)}, print,
{{"solid_infill_rotate_template", template_string},
{"sparse_infill_density", "100%"},
{"internal_solid_infill_pattern", "monotonic"},
{"layer_height", 0.2},
{"raft_layers", rafts}});
return angles_per_layer(print, solid_role);
};
const std::vector<int> without_raft = angles_for(0);
const std::vector<int> with_raft = angles_for(raft_layers);
REQUIRE(without_raft.size() == 100);
REQUIRE(with_raft.size() == without_raft.size());
REQUIRE(std::count(without_raft.begin(), without_raft.end(), -1) == 0);
CHECK(with_raft == without_raft);
}
TEST_CASE("Sparse infill rotation template turns the infill layer by layer", "[Fill]")
{
const std::vector<int> expected_cycle = {0, 25, 50, 75, 100, 125, 150};
Print print;
// No shells, so every layer is sparse infill rather than solid.
Slic3r::Test::init_and_process_print({Slic3r::Test::cube(10)}, print,
{{"sparse_infill_rotate_template", "0,25,50,75,100,125,150"},
{"sparse_infill_density", "40%"},
{"sparse_infill_pattern", "rectilinear"},
{"top_shell_layers", 0},
{"bottom_shell_layers", 0},
{"layer_height", 0.2}});
const std::vector<int> angles = angles_per_layer(print, sparse_role);
REQUIRE(angles.size() == 50);
REQUIRE(std::count(angles.begin(), angles.end(), -1) == 0);
std::vector<int> expected;
for (size_t i = 0; i < angles.size(); ++i)
expected.push_back(expected_cycle[i % expected_cycle.size()]);
CHECK(angles == expected);
}
TEST_CASE("Infill rotation template layer count modifier holds each angle for N layers", "[Fill]")
{
Print print;
// "+45#2" turns 45 degrees every 2 layers, so equal angles come in pairs.
Slic3r::Test::init_and_process_print({Slic3r::Test::cube(10)}, print,
{{"solid_infill_rotate_template", "+45#2"},
{"sparse_infill_density", "100%"},
{"internal_solid_infill_pattern", "monotonic"},
{"layer_height", 0.2}});
const std::vector<int> angles = angles_per_layer(print, solid_role);
REQUIRE(angles.size() == 50);
REQUIRE(std::count(angles.begin(), angles.end(), -1) == 0);
std::vector<int> run_lengths;
for (size_t i = 0; i < angles.size();) {
size_t j = i;
while (j < angles.size() && angles[j] == angles[i])
++j;
run_lengths.push_back(int(j - i));
i = j;
}
// The first and last runs can be clipped by the start and end of the object.
REQUIRE(run_lengths.size() > 3);
const std::vector<int> interior(run_lengths.begin() + 1, run_lengths.end() - 1);
CHECK(std::count(interior.begin(), interior.end(), 2) == int(interior.size()));
}
TEST_CASE("Z anti-aliasing keeps the infill rotation template's step", "[Fill]")
{
Print print;
Slic3r::Test::init_and_process_print({Slic3r::Test::cube(10)}, print,
{{"solid_infill_rotate_template", "+45"},
{"sparse_infill_density", "100%"},
{"internal_solid_infill_pattern", "monotonic"},
{"zaa_enabled", 1},
{"zaa_min_z", 0.05},
{"layer_height", 0.2}});
// Z contouring varies the layer heights, so the layer count is not 10mm / 0.2mm here.
const std::vector<int> angles = angles_per_layer(print, solid_role);
REQUIRE(angles.size() > 10);
REQUIRE(std::count(angles.begin(), angles.end(), -1) == 0);
// Z contouring may change when the template advances, but each step must still be 45 degrees.
int steps = 0;
for (size_t i = 1; i < angles.size(); ++i) {
const int delta = ((angles[i] - angles[i - 1]) % 180 + 180) % 180;
CAPTURE(i, angles[i - 1], angles[i]);
// Split rather than "delta == 0 || delta == 45" so Catch2 can show the operands.
REQUIRE(delta % 45 == 0);
REQUIRE(delta <= 45);
steps += delta == 45;
}
CHECK(steps > 0);
}
TEST_CASE("Ironing follows the solid infill rotation template", "[Fill]")
{
Print print;
Slic3r::Test::init_and_process_print({Slic3r::Test::cube(10)}, print,
{{"solid_infill_rotate_template", "+45"},
{"internal_solid_infill_pattern", "monotonic"},
{"top_surface_pattern", "monotonic"},
// Every solid surface, so the comparison covers every layer.
{"ironing_type", "solid"},
{"sparse_infill_density", "100%"},
{"ironing_angle", 0},
{"ironing_angle_fixed", 0},
{"layer_height", 0.2}});
const std::vector<int> ironing = angles_per_layer(print, ironing_role);
const std::vector<int> solid = angles_per_layer(print, solid_role);
REQUIRE(ironing.size() == solid.size());
// With no fixed angle and no offset, ironing runs along the template's angle for that layer.
int compared = 0;
for (size_t i = 0; i < ironing.size(); ++i)
if (ironing[i] != -1 && solid[i] != -1) {
CAPTURE(i, ironing[i], solid[i]);
CHECK(ironing[i] == solid[i]);
++compared;
}
// Most of the object, not one lucky layer.
REQUIRE(compared > int(ironing.size()) / 2);
}
TEST_CASE("Solid infill direction offsets every layer when no template is set", "[Fill]")
{
auto angles_for = [](int direction) {
Print print;
Slic3r::Test::init_and_process_print({Slic3r::Test::cube(10)}, print,
{{"solid_infill_direction", direction},
{"sparse_infill_density", "100%"},
{"internal_solid_infill_pattern", "monotonic"},
{"layer_height", 0.2}});
return angles_per_layer(print, solid_role);
};
const std::vector<int> at_0 = angles_for(0);
const std::vector<int> at_30 = angles_for(30);
REQUIRE(at_0.size() == at_30.size());
REQUIRE(std::count(at_0.begin(), at_0.end(), -1) == 0);
for (size_t i = 0; i < at_0.size(); ++i) {
const int delta = ((at_30[i] - at_0[i]) % 180 + 180) % 180;
CAPTURE(i, at_0[i], at_30[i]);
CHECK(delta == 30);
}
}
+27
View File
@@ -820,3 +820,30 @@ SCENARIO("Shipped dual-nozzle change_filament_gcode resolves during a real slice
} }
} }
} }
TEST_CASE("Custom G-code motion limits are restored before generated moves", "[GCodeWriter]")
{
const std::string gcode = Slic3r::Test::slice({ cube(20) }, {
{ "gcode_flavor", "marlin" },
{ "gcode_comments", "1" },
{ "machine_start_gcode", "" },
{ "layer_change_gcode", "M204 S5000\nm205 x5 y5\n" },
{ "layer_height", "0.2" },
{ "initial_layer_print_height", "0.2" },
{ "initial_layer_line_width", "0" },
{ "z_hop", "0" },
{ "default_acceleration", "6000" },
{ "initial_layer_acceleration", "6000" },
{ "outer_wall_acceleration", "6000" },
{ "inner_wall_acceleration", "0" },
{ "default_jerk", "8" },
{ "initial_layer_jerk", "8" },
{ "outer_wall_jerk", "8" },
{ "inner_wall_jerk", "0" },
});
const size_t custom_gcode_pos = gcode.find("m205 x5 y5");
REQUIRE(custom_gcode_pos != std::string::npos);
REQUIRE(gcode.find("M204 S6000 ; adjust acceleration", custom_gcode_pos) != std::string::npos);
REQUIRE(gcode.find("M205 X8 Y8 ; adjust jerk", custom_gcode_pos) != std::string::npos);
}
+1
View File
@@ -19,6 +19,7 @@ add_executable(${_TEST_NAME}_tests
test_preset_diff.cpp test_preset_diff.cpp
test_elephant_foot_compensation.cpp test_elephant_foot_compensation.cpp
test_geometry.cpp test_geometry.cpp
test_multimaterial_segmentation.cpp
test_placeholder_parser.cpp test_placeholder_parser.cpp
test_polygon.cpp test_polygon.cpp
test_mutable_polygon.cpp test_mutable_polygon.cpp
+70
View File
@@ -1,5 +1,9 @@
#include <catch2/catch_all.hpp> #include <catch2/catch_all.hpp>
#include <algorithm>
#include <sstream>
#include <string>
#include "libslic3r/calib.hpp" #include "libslic3r/calib.hpp"
#include "libslic3r/Model.hpp" #include "libslic3r/Model.hpp"
#include "libslic3r/TriangleMesh.hpp" #include "libslic3r/TriangleMesh.hpp"
@@ -38,3 +42,69 @@ TEST_CASE("Zero calibration line width resolves to a positive default", "[Calib]
REQUIRE(pattern.line_width() > 0.); REQUIRE(pattern.line_width() > 0.);
REQUIRE(pattern.line_width_first_layer() > 0.); REQUIRE(pattern.line_width_first_layer() > 0.);
} }
namespace {
struct EndState { double final_e; double max_e; };
EndState simulate_absolute_e(const std::string &gcode)
{
double final_e = 0.;
double max_e = 0.;
std::istringstream lines(gcode);
std::string line;
while (std::getline(lines, line)) {
std::istringstream words(line);
std::string op;
if (!(words >> op))
continue;
if (op != "G1" && op != "G0" && op != "G92")
continue;
std::string word;
while (words >> word) {
if (word.size() >= 2 && word[0] == 'E') {
final_e = std::stod(word.substr(1));
max_e = std::max(max_e, final_e);
break;
}
}
}
return {final_e, max_e};
}
} // namespace
TEST_CASE("PA pattern resets the extruder after the final layer in absolute E mode", "[Calib][Regression]")
{
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
config.set_deserialize_strict({
{"use_relative_e_distances", "0"},
{"line_width", "0.45"},
{"initial_layer_line_width", "0.45"},
});
Model model;
model.add_object("cube", "", make_cube(20, 20, 20))->add_instance();
Calib_Params params;
params.mode = CalibMode::Calib_PA_Pattern;
params.start = 0.;
params.end = 0.08;
params.step = 0.002;
CalibPressureAdvancePattern pattern(params, config, /* is_bbl_machine */ false, *model.objects.front(), Vec3d(0, 0, 0));
const CustomGCode::Info info = pattern.generate_custom_gcodes(config, /* is_bbl_machine */ false, *model.objects.front(),
Vec3d(0, 0, 0));
std::string gcode;
for (const CustomGCode::Item &item : info.gcodes)
gcode += item.extra;
const EndState state = simulate_absolute_e(gcode);
REQUIRE(state.max_e > 1.);
REQUIRE_THAT(state.final_e, Catch::Matchers::WithinAbs(0., 1e-9));
}
@@ -0,0 +1,57 @@
#include <catch2/catch_all.hpp>
// MultiMaterialSegmentation.hpp declares boost::polygon traits for ColoredLine, so its
// geometry/boost dependencies must be included first.
#include <boost/polygon/polygon.hpp>
#include "libslic3r/Line.hpp"
#include "libslic3r/Flow.hpp"
#include "libslic3r/MultiMaterialSegmentation.hpp"
#include "libslic3r/PrintConfig.hpp"
using namespace Slic3r;
TEST_CASE("Multi-material segmentation resolves the outer-wall line width", "[MultiMaterialSegmentation][Regression]")
{
struct Case
{
std::string description;
double outer_value;
bool outer_percent;
double line_value;
bool line_percent;
std::vector<double> nozzle_diameters;
int outer_wall_filament_id;
double expected;
};
auto c = GENERATE(values<Case>({
{"absolute outer-wall width is used as-is", 0.6, false, 0.42, false, {0.4}, 1, 0.6},
{"percent outer-wall width uses the nozzle", 120, true, 0.42, false, {0.5}, 1, 0.6},
{"zero outer-wall width uses the line width", 0, false, 0.5, false, {0.4}, 1, 0.5},
{"zero outer-wall width uses a percent line", 0, false, 100, true, {0.5}, 1, 0.5},
{"zero width falls back to auto", 0, false, 0, false, {0.4}, 1, Flow::auto_extrusion_width(frExternalPerimeter, 0.4)},
{"the auto fallback scales with the nozzle", 0, false, 0, false, {0.6}, 1, Flow::auto_extrusion_width(frExternalPerimeter, 0.6)},
{"a percent width uses the outer wall's nozzle", 120, true, 0.42, false, {0.4, 0.8}, 2, 0.96},
{"the auto width uses the outer wall's nozzle", 0, false, 0, false, {0.4, 0.8}, 2, Flow::auto_extrusion_width(frExternalPerimeter, 0.8)},
{"an absolute width ignores the nozzle", 0.6, false, 0.42, false, {0.4, 0.8}, 2, 0.6},
{"a zero percent width uses the line width", 0, true, 0.5, false, {0.4}, 1, 0.5},
{"an unset filament id uses the first nozzle", 0, false, 0, false, {0.4, 0.8}, 0, Flow::auto_extrusion_width(frExternalPerimeter, 0.4)},
{"an out-of-range filament id uses nozzle 1", 0, false, 0, false, {0.4, 0.8}, 5, Flow::auto_extrusion_width(frExternalPerimeter, 0.4)},
}));
DYNAMIC_SECTION(c.description)
{
PrintConfig print_config;
print_config.nozzle_diameter.values = c.nozzle_diameters;
PrintObjectConfig object_config;
object_config.line_width = ConfigOptionFloatOrPercent(c.line_value, c.line_percent);
PrintRegionConfig region_config;
region_config.outer_wall_line_width = ConfigOptionFloatOrPercent(c.outer_value, c.outer_percent);
region_config.outer_wall_filament_id.value = c.outer_wall_filament_id;
REQUIRE_THAT(resolve_outer_wall_line_width(region_config, object_config, print_config),
Catch::Matchers::WithinAbs(c.expected, 1e-9));
}
}
@@ -52,6 +52,29 @@ print('ok')
} // namespace } // namespace
TEST_CASE("plugin latest version uses the authoritative catalog field", "[PluginDescriptor]")
{
PluginDescriptor descriptor;
descriptor.version = "1.3.0";
descriptor.latest_version = "1.3.0";
PluginChangelog changelog;
changelog.version = "1.2.0";
descriptor.changelog.push_back(changelog);
CHECK(descriptor.latest_available_version() == "1.3.0");
}
TEST_CASE("plugin latest version falls back to the descriptor version", "[PluginDescriptor]")
{
PluginDescriptor descriptor;
descriptor.version = "1.1.0";
PluginChangelog changelog;
changelog.version = "1.0.0";
descriptor.changelog.push_back(changelog);
CHECK(descriptor.latest_available_version() == "1.1.0");
}
// Regression: update_cloud_metadata() replaces a matched entry's descriptor wholesale with the // Regression: update_cloud_metadata() replaces a matched entry's descriptor wholesale with the
// cloud catalog record (`entry = cloud_entry`). Configuration used to ride on the descriptor, so // cloud catalog record (`entry = cloud_entry`). Configuration used to ride on the descriptor, so
// that overwrite silently wiped it and plugins fell back to their built-in defaults (found via // that overwrite silently wiped it and plugins fell back to their built-in defaults (found via