diff --git a/localization/i18n/OrcaSlicer.pot b/localization/i18n/OrcaSlicer.pot
index 4eb71d7180..3cbb4500ce 100644
--- a/localization/i18n/OrcaSlicer.pot
+++ b/localization/i18n/OrcaSlicer.pot
@@ -5097,7 +5097,7 @@ msgstr ""
msgid "Fan speed (%)"
msgstr ""
-msgid "Temperature (°C)"
+msgid "Temperature (℃)"
msgstr ""
msgid "Volumetric flow rate (mm³/s)"
@@ -15194,7 +15194,7 @@ msgid ""
msgstr ""
msgid ""
-"This is the chamber temperature at which printing should start, while the chamber continues heating toward the \"Target\" chamber temperature. For example, set the Target to 60 and the Minimal to 50 to begin printing once the chamber reaches 50°C, without waiting for the full 60°C.\n"
+"This is the chamber temperature at which printing should start, while the chamber continues heating toward the \"Target\" chamber temperature. For example, set the Target to 60 and the Minimal to 50 to begin printing once the chamber reaches 50℃, without waiting for the full 60℃.\n"
"\n"
"It sets a G-code variable named chamber_minimal_temperature, which can be passed to your print start macro or a heat soak macro, like this: PRINT_START (other variables) CHAMBER_MIN_TEMP=[chamber_minimal_temperature].\n"
"\n"
diff --git a/localization/i18n/ca/OrcaSlicer_ca.po b/localization/i18n/ca/OrcaSlicer_ca.po
index 91ceb8d4f2..5e45f1a3c5 100644
--- a/localization/i18n/ca/OrcaSlicer_ca.po
+++ b/localization/i18n/ca/OrcaSlicer_ca.po
@@ -5349,8 +5349,8 @@ msgstr "Jerk (mm/s)"
msgid "Fan speed (%)"
msgstr "Velocitat Ventilador ( % )"
-msgid "Temperature (°C)"
-msgstr "Temperatura ( °C )"
+msgid "Temperature (℃)"
+msgstr "Temperatura ( ℃ )"
msgid "Volumetric flow rate (mm³/s)"
msgstr "Taxa de flux volumètric ( mm³/seg )"
@@ -16143,7 +16143,7 @@ msgstr ""
"Si està activat, aquest paràmetre també estableix una variable de codi g anomenada chamber_temperature, que es pot utilitzar per passar la temperatura de la cambra desitjada a la macro d'inici d'impressió, o una macro de remull tèrmic com aquesta: PRINT_START (altres variables) CHAMBER_TEMP=[chamber_temperature]. Això pot ser útil si la vostra impressora no admet les ordres M141/M191, o si voleu gestionar el remull de calor a la macro d'inici d'impressió si no hi ha instal·lat un escalfador de cambra actiu."
msgid ""
-"This is the chamber temperature at which printing should start, while the chamber continues heating toward the \"Target\" chamber temperature. For example, set the Target to 60 and the Minimal to 50 to begin printing once the chamber reaches 50°C, without waiting for the full 60°C.\n"
+"This is the chamber temperature at which printing should start, while the chamber continues heating toward the \"Target\" chamber temperature. For example, set the Target to 60 and the Minimal to 50 to begin printing once the chamber reaches 50℃, without waiting for the full 60℃.\n"
"\n"
"It sets a G-code variable named chamber_minimal_temperature, which can be passed to your print start macro or a heat soak macro, like this: PRINT_START (other variables) CHAMBER_MIN_TEMP=[chamber_minimal_temperature].\n"
"\n"
diff --git a/localization/i18n/cs/OrcaSlicer_cs.po b/localization/i18n/cs/OrcaSlicer_cs.po
index 2e7e0c09b6..d18cc9ca4d 100644
--- a/localization/i18n/cs/OrcaSlicer_cs.po
+++ b/localization/i18n/cs/OrcaSlicer_cs.po
@@ -5346,8 +5346,8 @@ msgstr "Jerk (mm/s)"
msgid "Fan speed (%)"
msgstr "Rychlost ventilátoru (%)"
-msgid "Temperature (°C)"
-msgstr "Teplota (°C)"
+msgid "Temperature (℃)"
+msgstr "Teplota (℃)"
msgid "Volumetric flow rate (mm³/s)"
msgstr "Rychlost průtoku (mm³/s)"
@@ -16186,7 +16186,7 @@ msgstr ""
"Pokud je povoleno, tento parametr rovněž nastaví G-code proměnnou s názvem chamber_temperature, kterou lze použít k předání požadované teploty komory do makra spuštění tisku, nebo do makra pro tepelnou stabilizaci, například takto: PRINT_START (další proměnné) CHAMBER_TEMP=[chamber_temperature]. To může být užitečné, pokud vaše tiskárna nepodporuje příkazy M141/M191, nebo pokud chcete provádět tepelnou stabilizaci v makru spuštění tisku, pokud není instalováno aktivní topení komory."
msgid ""
-"This is the chamber temperature at which printing should start, while the chamber continues heating toward the \"Target\" chamber temperature. For example, set the Target to 60 and the Minimal to 50 to begin printing once the chamber reaches 50°C, without waiting for the full 60°C.\n"
+"This is the chamber temperature at which printing should start, while the chamber continues heating toward the \"Target\" chamber temperature. For example, set the Target to 60 and the Minimal to 50 to begin printing once the chamber reaches 50℃, without waiting for the full 60℃.\n"
"\n"
"It sets a G-code variable named chamber_minimal_temperature, which can be passed to your print start macro or a heat soak macro, like this: PRINT_START (other variables) CHAMBER_MIN_TEMP=[chamber_minimal_temperature].\n"
"\n"
diff --git a/localization/i18n/de/OrcaSlicer_de.po b/localization/i18n/de/OrcaSlicer_de.po
index 2fcddbf963..052cde4e7d 100644
--- a/localization/i18n/de/OrcaSlicer_de.po
+++ b/localization/i18n/de/OrcaSlicer_de.po
@@ -5350,8 +5350,8 @@ msgstr "Ruck (mm/s)"
msgid "Fan speed (%)"
msgstr "Lüftergeschwindigkeit (%)"
-msgid "Temperature (°C)"
-msgstr "Temperatur (°C)"
+msgid "Temperature (℃)"
+msgstr "Temperatur (℃)"
msgid "Volumetric flow rate (mm³/s)"
msgstr "Volumetrische Flussrate (mm³/s)"
@@ -8891,10 +8891,10 @@ msgid "Show incompatible/unsupported presets in the printer and filament dropdow
msgstr "Zeigt inkompatible/nicht unterstützte Profile in den Dropdown-Listen für Drucker und Filament an. Diese Profile können nicht ausgewählt werden."
msgid "Experimental Features"
-msgstr ""
+msgstr "Experimentelle Funktionen"
msgid "Keep painted feature after mesh change"
-msgstr ""
+msgstr "Bemalte Funktionen nach Mesh-Änderung beibehalten"
msgid ""
"Attempt to keep painted features (color/seam/support/fuzzy etc.) after changing the object mesh (such as cut/reload from disk/simplify/fix etc.)\n"
@@ -9719,10 +9719,10 @@ msgid "Search in preset"
msgstr "In Profilen suchen"
msgid "Synchronization of different extruder drives or nozzle volume types is not supported."
-msgstr ""
+msgstr "Die Synchronisierung verschiedener Extrudertypen oder Düsenvolumentypen wird nicht unterstützt."
msgid "Synchronize the modification of parameters to the corresponding parameters of another extruder."
-msgstr ""
+msgstr "Änderungen der Parameter mit den entsprechenden Parametern eines anderen Extruders synchronisieren."
msgid "Click to reset all settings to the last saved preset."
msgstr "Klicken Sie hier, um alle Einstellungen auf die zuletzt gespeicherten Parameter zurückzusetzen."
@@ -10018,13 +10018,13 @@ msgid "Chamber temperature"
msgstr "Druckraum Temperatur"
msgid "Target chamber temperature, and the minimal chamber temperature at which printing should start"
-msgstr ""
+msgstr "Ziel-Druckraumtemperatur und die minimale Druckraumtemperatur, bei der der Druck beginnen sollte"
msgid "Target"
-msgstr ""
+msgstr "Ziel"
msgid "Minimal"
-msgstr ""
+msgstr "Minimal"
msgid "Print temperature"
msgstr "Drucktemperatur"
@@ -10047,7 +10047,7 @@ msgstr "Strukturierte kalte Druckplatte"
# TODO: Review, changed by lang refactor. PR 14254
msgid "This is the bed temperature when the Textured Cool Plate is installed. A value of 0 means the filament does not support printing on the Textured Cool Plate."
-msgstr "Dies ist die Betttemperatur, wenn die kalte Druckplatte installiert ist. Ein Wert von 0 bedeutet, dass das Filament auf der texturierten kalten Druckplatte nicht unterstützt wird."
+msgstr "Dies ist die Betttemperatur, wenn die texturierte kalte Druckplatte installiert ist. Ein Wert von 0 bedeutet, dass das Filament auf der texturierten kalten Druckplatte nicht unterstützt wird."
# TODO: Review, changed by lang refactor. PR 14254
msgid "This is the bed temperature when the engineering plate is installed. A value of 0 means the filament does not support printing on the Engineering Plate."
@@ -10368,21 +10368,21 @@ msgstr "Nicht erneut für dieses Profil warnen"
#, c-format, boost-format
msgid "%s: %s"
-msgstr ""
+msgstr "%s: %s"
msgid "No modifications need to be copied."
-msgstr ""
+msgstr "Es müssen keine Änderungen kopiert werden."
msgid "Copy paramters"
-msgstr ""
+msgstr "Parameter kopieren"
#, c-format, boost-format
msgid "Modify paramters of %s"
-msgstr ""
+msgstr "Parameter von %s ändern"
#, c-format, boost-format
msgid "Do you want to modify the following parameters of the %s to that of the %s?"
-msgstr ""
+msgstr "Möchten Sie die folgenden Parameter von %s auf die von %s ändern?"
msgid "Click to reset current value and attach to the global value."
msgstr "Klicken Sie hier, um den aktuellen Wert zurückzusetzen und ihn dem globalen Wert zuzuordnen."
@@ -10528,10 +10528,10 @@ msgid "Capabilities"
msgstr "Fähigkeiten"
msgid "Left: "
-msgstr ""
+msgstr "Links: "
msgid "Right: "
-msgstr ""
+msgstr "Rechts: "
msgid "Show all presets (including incompatible)"
msgstr "Alle Profile anzeigen (auch inkompatible)"
@@ -13887,7 +13887,7 @@ msgid "layer"
msgstr "Schicht"
msgid "First layer fan speed"
-msgstr ""
+msgstr "Lüftergeschwindigkeit der ersten Schicht"
msgid ""
"Sets an exact fan speed for the first layer, overriding all other cooling settings. Useful for protecting 3D-printed toolhead parts (e.g. Voron-style ABS/ASA ducts) from a hot bed. A small amount of airflow cools the ducts down, without using full cooling that may in certain conditions hurt first-layer adhesion.\n"
@@ -13896,6 +13896,11 @@ msgid ""
"Only available when \"No cooling for the first\" is 0.\n"
"Set to -1 to disable it."
msgstr ""
+"Legt eine genaue Lüftergeschwindigkeit für die erste Schicht fest und überschreibt alle anderen Kühlungseinstellungen. Nützlich zum Schutz von 3D-gedruckten Werkzeugkopfteilen (z.B. Voron-Style ABS/ASA-Düsen) vor einem heißen Bett. Eine kleine Menge Luftstrom kühlt die Düsen ab, ohne die volle Kühlung zu verwenden, die unter bestimmten Bedingungen die Haftung der ersten Schicht beeinträchtigen kann.\n"
+"Ab der zweiten Schicht wird die normale Kühlung wieder aufgenommen.\n"
+"Wenn auch \"Volle Lüfterdrehzahl ab Schicht\" eingestellt ist, steigt der Lüfter von diesem Wert in der ersten Schicht bis zu Ihrem Zielwert in der gewählten Schicht.\n"
+"Nur verfügbar, wenn \"Keine Kühlung für die erste\" 0 ist.\n"
+"Auf -1 setzen, um es zu deaktivieren."
msgid "Support interface fan speed"
msgstr "Stützstruktur-Schnittstelle"
@@ -16291,15 +16296,20 @@ msgstr ""
"Wenn diese Option aktiviert ist, wird auch eine G-Code-Variable namens chamber_temperature gesetzt, die verwendet werden kann, um die gewünschte Druckraumtemperatur an Ihr Druckstart-Makro oder ein Wärmespeicher-Makro weiterzugeben, wie z.B. PRINT_START (andere Variablen) CHAMBER_TEMP=[chamber_temperature]. Dies kann nützlich sein, wenn Ihr Drucker die Befehle M141/M191 nicht unterstützt oder wenn Sie das Wärmespeichern im Druckstart-Makro behandeln möchten, wenn kein aktiver Druckraumheizer installiert ist."
msgid ""
-"This is the chamber temperature at which printing should start, while the chamber continues heating toward the \"Target\" chamber temperature. For example, set the Target to 60 and the Minimal to 50 to begin printing once the chamber reaches 50°C, without waiting for the full 60°C.\n"
+"This is the chamber temperature at which printing should start, while the chamber continues heating toward the \"Target\" chamber temperature. For example, set the Target to 60 and the Minimal to 50 to begin printing once the chamber reaches 50℃, without waiting for the full 60℃.\n"
"\n"
"It sets a G-code variable named chamber_minimal_temperature, which can be passed to your print start macro or a heat soak macro, like this: PRINT_START (other variables) CHAMBER_MIN_TEMP=[chamber_minimal_temperature].\n"
"\n"
"Unlike the \"Target\" chamber temperature, this option does not emit any M141/M191 commands; it only exposes the value to your custom G-code. It should not exceed the \"Target\" chamber temperature."
msgstr ""
+"Dies ist die Druckraumtemperatur, bei der der Druckvorgang beginnen soll, während der Druckraum weiterhin auf die \"Ziel\"-Druckraumtemperatur erhitzt wird. Zum Beispiel: Setzen Sie das Ziel auf 60 und das Minimum auf 50, um mit dem Drucken zu beginnen, sobald der Druckraum 50°C erreicht hat, ohne auf die vollen 60°C zu warten.\n"
+"\n"
+"Es wird eine G-Code-Variable namens chamber_minimal_temperature gesetzt, die an Ihr Druckstart-Makro oder ein Wärmespeicher-Makro übergeben werden kann, wie z.B.: PRINT_START (andere Variablen) CHAMBER_MIN_TEMP=[chamber_minimal_temperature].\n"
+"\n"
+"Im Gegensatz zur \"Ziel\"-Druckraumtemperatur sendet diese Option keine M141/M191-Befehle; sie stellt den Wert nur Ihrem benutzerdefinierten G-Code zur Verfügung. Sie sollte die \"Ziel\"-Druckraumtemperatur nicht überschreiten."
msgid "Chamber minimal temperature"
-msgstr ""
+msgstr "Minimale Druckraumtemperatur"
# TODO: Review, changed by lang refactor. PR 14254
msgid "Nozzle temperature after the first layer"
@@ -19110,130 +19120,132 @@ msgid "Connection to printers connected via the print host failed."
msgstr "Die Verbindung zu den über den Druck-Host verbundenen Druckern ist fehlgeschlagen."
msgid "Detect Creality K-series printer"
-msgstr ""
+msgstr "Creality K-Serie Drucker erkennen"
msgid "Click Scan to look for K-series printers on your network."
-msgstr ""
+msgstr "Klicken Sie auf 'Scannen', um nach K-Serie Druckern in Ihrem Netzwerk zu suchen."
msgid "Use Selected"
-msgstr ""
+msgstr "Ausgewählten verwenden"
msgid "Scanning the LAN for K-series printers... this takes a few seconds."
-msgstr ""
+msgstr "Suche im LAN nach K-Serie Druckern... dies dauert einige Sekunden."
msgid "No K-series printers found. Make sure the printer is on the same network and not blocked by Wi-Fi client isolation, then click Scan again."
-msgstr ""
+msgstr "Keine K-Serie Drucker gefunden. Stellen Sie sicher, dass der Drucker im selben Netzwerk ist und nicht durch Wi-Fi-Client-Isolation blockiert wird, und klicken Sie dann erneut auf 'Scannen'."
#, c-format
msgid "Found %zu Creality printer(s). Select one and click Use Selected."
-msgstr ""
+msgstr "Es wurden %zu Creality-Drucker gefunden. Wählen Sie einen aus und klicken Sie auf 'Ausgewählten verwenden'."
msgid "Active"
-msgstr ""
+msgstr "Aktiv"
msgid "Printers"
-msgstr ""
+msgstr "Drucker"
msgid "Processes"
-msgstr ""
+msgstr "Prozesse"
msgid "Show/Hide system information"
-msgstr ""
+msgstr "Systeminformationen anzeigen/verbergen"
msgid "Copy system information to clipboard"
-msgstr ""
+msgstr "Systeminformationen in die Zwischenablage kopieren"
msgid "We need information for diagnosing source of the issue. Check wiki page for detailed guide."
-msgstr ""
+msgstr "Wir benötigen Informationen zur Diagnose der Ursache des Problems. Überprüfen Sie die Wiki-Seite für eine detaillierte Anleitung."
msgid "Pack button collects project file and logs of current session onto a zip file."
-msgstr ""
+msgstr "Die Schaltfläche „Packen“ sammelt die Projektdatei und Protokolle der aktuellen Sitzung in einer ZIP-Datei."
msgid "Any additional visual examples like images or screen recordings might be helpful while reporting the issue."
-msgstr ""
+msgstr "Zusätzliche visuelle Beispiele wie Bilder oder Bildschirmaufnahmen könnten beim Melden des Problems hilfreich sein."
msgid "Report issue"
-msgstr ""
+msgstr "Problem melden"
msgid "Pack"
-msgstr ""
+msgstr "Packen"
msgid "Cleans and rebuilds system profiles cache on next launch"
-msgstr ""
+msgstr "Bereinigt und erstellt den Systemprofil-Cache beim nächsten Start neu"
msgid "Clean system profiles cache"
-msgstr ""
+msgstr "Systemprofil-Cache bereinigen"
msgid "Clean"
-msgstr ""
+msgstr "Bereinigen"
msgid "Loaded profiles overview"
-msgstr ""
+msgstr "Übersicht der geladenen Profile"
msgid "This section shows information for loaded profiles"
-msgstr ""
+msgstr "Dieser Abschnitt zeigt Informationen zu den geladenen Profilen"
msgid "Exports detailed overview of loaded profiles in json format"
-msgstr ""
+msgstr "Exportiert eine detaillierte Übersicht der geladenen Profile im JSON-Format"
msgid "Configurations folder"
-msgstr ""
+msgstr "Konfigurationsordner"
msgid "Opens configurations folder"
-msgstr ""
+msgstr "Öffnet den Konfigurationsordner"
msgid "Log level"
-msgstr ""
+msgstr "Protokollstufe"
msgid "Stored logs"
-msgstr ""
+msgstr "Gespeicherte Protokolle"
msgid "Packs all stored logs onto a zip file."
-msgstr ""
+msgstr "Packt alle gespeicherten Protokolle in eine ZIP-Datei."
msgid "Profiles"
-msgstr ""
+msgstr "Profile"
msgid "Select NO to close dialog and review project"
-msgstr ""
+msgstr "Wählen Sie NEIN, um das Dialogfeld zu schließen und das Projekt zu überprüfen"
msgid "No project file on current session. Only logs will be included to package"
-msgstr ""
+msgstr "Keine Projektdatei in der aktuellen Sitzung. Nur Protokolle werden in das Paket aufgenommen"
msgid "Select NO to close dialog and review project."
-msgstr ""
+msgstr "Wählen Sie NEIN, um das Dialogfeld zu schließen und das Projekt zu überprüfen."
msgid "Please make sure any instances of OrcaSlicer are not running"
-msgstr ""
+msgstr "Bitte stellen Sie sicher, dass keine Instanzen von OrcaSlicer ausgeführt werden."
msgid "System folder cannot be deleted because some files are in use by another application. Please close any applications using these files and try again."
-msgstr ""
+msgstr "Der Systemordner kann nicht gelöscht werden, da einige Dateien von einer anderen Anwendung verwendet werden. Bitte schließen Sie alle Anwendungen, die diese Dateien verwenden, und versuchen Sie es erneut."
msgid "Failed to delete system folder..."
-msgstr ""
+msgstr "Systemordner konnte nicht gelöscht werden..."
msgid "Failed to determine executable path."
-msgstr ""
+msgstr "Konnte den Pfad zur ausführbaren Datei nicht bestimmen."
msgid "Failed to launch a new instance."
-msgstr ""
+msgstr "Konnte keine neue Instanz starten."
msgid "log(s)"
-msgstr ""
+msgstr "Protokoll(e)"
msgid "Choose where to save the exported JSON file"
-msgstr ""
+msgstr "Wählen Sie, wo die exportierte JSON-Datei gespeichert werden soll"
msgid ""
"Export failed\n"
"Please check write permissions or file in use by another application"
msgstr ""
+"Export fehlgeschlagen\n"
+"Bitte überprüfen Sie die Schreibberechtigungen oder ob die Datei von einer anderen Anwendung verwendet wird."
msgid "Choose where to save the exported ZIP file"
-msgstr ""
+msgstr "Wählen Sie, wo die exportierte ZIP-Datei gespeichert werden soll"
msgid "File already exists. Overwrite?"
-msgstr ""
+msgstr "Datei existiert bereits. Überschreiben?"
msgid "3DPrinterOS Cloud upload options"
msgstr "3DPrinterOS Cloud-Upload-Optionen"
@@ -19318,17 +19330,17 @@ msgid "Could not connect to MKS"
msgstr "Konnte keine Verbindung zu MKS herstellen"
msgid "Connection to Moonraker is working correctly."
-msgstr ""
+msgstr "Verbindung zu Moonraker funktioniert korrekt."
msgid "Could not connect to Moonraker"
-msgstr ""
+msgstr "Konnte keine Verbindung zu Moonraker herstellen"
msgid "The host responded but it doesn't look like Moonraker (missing result.klippy_state)."
-msgstr ""
+msgstr "Der Host hat geantwortet, sieht aber nicht wie Moonraker aus (fehlender result.klippy_state)."
#, c-format, boost-format
msgid "Could not parse Moonraker server response: %s"
-msgstr ""
+msgstr "Konnte die Moonraker-Serverantwort nicht analysieren: %s"
msgid "Connection to OctoPrint is working correctly."
msgstr "Verbindung zu OctoPrint funktioniert korrekt."
diff --git a/localization/i18n/en/OrcaSlicer_en.po b/localization/i18n/en/OrcaSlicer_en.po
index a7bce2d2d6..5e10b1c81f 100644
--- a/localization/i18n/en/OrcaSlicer_en.po
+++ b/localization/i18n/en/OrcaSlicer_en.po
@@ -5093,7 +5093,7 @@ msgstr ""
msgid "Fan speed (%)"
msgstr ""
-msgid "Temperature (°C)"
+msgid "Temperature (℃)"
msgstr ""
msgid "Volumetric flow rate (mm³/s)"
@@ -15190,7 +15190,7 @@ msgid ""
msgstr ""
msgid ""
-"This is the chamber temperature at which printing should start, while the chamber continues heating toward the \"Target\" chamber temperature. For example, set the Target to 60 and the Minimal to 50 to begin printing once the chamber reaches 50°C, without waiting for the full 60°C.\n"
+"This is the chamber temperature at which printing should start, while the chamber continues heating toward the \"Target\" chamber temperature. For example, set the Target to 60 and the Minimal to 50 to begin printing once the chamber reaches 50℃, without waiting for the full 60℃.\n"
"\n"
"It sets a G-code variable named chamber_minimal_temperature, which can be passed to your print start macro or a heat soak macro, like this: PRINT_START (other variables) CHAMBER_MIN_TEMP=[chamber_minimal_temperature].\n"
"\n"
diff --git a/localization/i18n/es/OrcaSlicer_es.po b/localization/i18n/es/OrcaSlicer_es.po
index 69b40124e8..df95bdcb22 100644
--- a/localization/i18n/es/OrcaSlicer_es.po
+++ b/localization/i18n/es/OrcaSlicer_es.po
@@ -5251,8 +5251,8 @@ msgstr "Jerk (mm/s)"
msgid "Fan speed (%)"
msgstr "Velocidad Ventilador (%)"
-msgid "Temperature (°C)"
-msgstr "Temperatura (°C)"
+msgid "Temperature (℃)"
+msgstr "Temperatura (℃)"
msgid "Volumetric flow rate (mm³/s)"
msgstr "Tasa de flujo volumétrico (mm³/seg)"
@@ -15997,7 +15997,7 @@ msgstr ""
"Esta funciuón es útil para imrpesoras no compatibles con los comandos M141 o M191, o si prefiere realizar un precalentamiento usando un macro si no dispone de un sistema de calentamiento activo de cámara."
msgid ""
-"This is the chamber temperature at which printing should start, while the chamber continues heating toward the \"Target\" chamber temperature. For example, set the Target to 60 and the Minimal to 50 to begin printing once the chamber reaches 50°C, without waiting for the full 60°C.\n"
+"This is the chamber temperature at which printing should start, while the chamber continues heating toward the \"Target\" chamber temperature. For example, set the Target to 60 and the Minimal to 50 to begin printing once the chamber reaches 50℃, without waiting for the full 60℃.\n"
"\n"
"It sets a G-code variable named chamber_minimal_temperature, which can be passed to your print start macro or a heat soak macro, like this: PRINT_START (other variables) CHAMBER_MIN_TEMP=[chamber_minimal_temperature].\n"
"\n"
diff --git a/localization/i18n/fr/OrcaSlicer_fr.po b/localization/i18n/fr/OrcaSlicer_fr.po
index 9b63eb780c..9b88ef7532 100644
--- a/localization/i18n/fr/OrcaSlicer_fr.po
+++ b/localization/i18n/fr/OrcaSlicer_fr.po
@@ -5248,8 +5248,8 @@ msgstr "Jerk (mm/s)"
msgid "Fan speed (%)"
msgstr "Vitesse du ventilateur (%)"
-msgid "Temperature (°C)"
-msgstr "Température (°C)"
+msgid "Temperature (℃)"
+msgstr "Température (℃)"
msgid "Volumetric flow rate (mm³/s)"
msgstr "Débit volumétrique (mm³/s)"
@@ -15968,7 +15968,7 @@ msgstr ""
"S’il est activé, ce paramètre définit également une variable gcode nommée chamber_temperature, qui peut être utilisée pour transmettre la température de caisson souhaitée à votre macro de démarrage de l’impression, ou à une macro de trempe thermique comme celle-ci : PRINT_START (autres variables) CHAMBER_TEMP=[chamber_temperature]. Cela peut être utile si votre imprimante ne prend pas en charge les commandes M141/M191, ou si vous souhaitez gérer le préchauffage dans la macro de démarrage de l’impression si aucun chauffage de caisson actif n’est installé."
msgid ""
-"This is the chamber temperature at which printing should start, while the chamber continues heating toward the \"Target\" chamber temperature. For example, set the Target to 60 and the Minimal to 50 to begin printing once the chamber reaches 50°C, without waiting for the full 60°C.\n"
+"This is the chamber temperature at which printing should start, while the chamber continues heating toward the \"Target\" chamber temperature. For example, set the Target to 60 and the Minimal to 50 to begin printing once the chamber reaches 50℃, without waiting for the full 60℃.\n"
"\n"
"It sets a G-code variable named chamber_minimal_temperature, which can be passed to your print start macro or a heat soak macro, like this: PRINT_START (other variables) CHAMBER_MIN_TEMP=[chamber_minimal_temperature].\n"
"\n"
diff --git a/localization/i18n/hu/OrcaSlicer_hu.po b/localization/i18n/hu/OrcaSlicer_hu.po
index f8f63db127..d41cccaed2 100644
--- a/localization/i18n/hu/OrcaSlicer_hu.po
+++ b/localization/i18n/hu/OrcaSlicer_hu.po
@@ -5340,7 +5340,7 @@ msgstr "Rántás (mm/s)"
msgid "Fan speed (%)"
msgstr "Ventilátor fordulatszám (%)"
-msgid "Temperature (°C)"
+msgid "Temperature (℃)"
msgstr "Hőmérséklet (°C)"
msgid "Volumetric flow rate (mm³/s)"
@@ -16222,7 +16222,7 @@ msgstr ""
"Bekapcsolva ez a paraméter egy chamber_temperature nevű G-code változót is beállít, amely felhasználható a kívánt kamrahőmérséklet átadására a nyomtatásindító makrónak, vagy egy ilyen hőkiegyenlítő makrónak: PRINT_START (egyéb változók) CHAMBER_TEMP=[chamber_temperature]. Ez akkor lehet hasznos, ha a nyomtatód nem támogatja az M141/M191 parancsokat, vagy ha az aktív kamrafűtés hiányában a hőkiegyenlítést a nyomtatásindító makróban szeretnéd kezelni."
msgid ""
-"This is the chamber temperature at which printing should start, while the chamber continues heating toward the \"Target\" chamber temperature. For example, set the Target to 60 and the Minimal to 50 to begin printing once the chamber reaches 50°C, without waiting for the full 60°C.\n"
+"This is the chamber temperature at which printing should start, while the chamber continues heating toward the \"Target\" chamber temperature. For example, set the Target to 60 and the Minimal to 50 to begin printing once the chamber reaches 50℃, without waiting for the full 60℃.\n"
"\n"
"It sets a G-code variable named chamber_minimal_temperature, which can be passed to your print start macro or a heat soak macro, like this: PRINT_START (other variables) CHAMBER_MIN_TEMP=[chamber_minimal_temperature].\n"
"\n"
diff --git a/localization/i18n/it/OrcaSlicer_it.po b/localization/i18n/it/OrcaSlicer_it.po
index a599189886..0bdcec19a7 100644
--- a/localization/i18n/it/OrcaSlicer_it.po
+++ b/localization/i18n/it/OrcaSlicer_it.po
@@ -5341,8 +5341,8 @@ msgstr "Scatto (mm/s)"
msgid "Fan speed (%)"
msgstr "Velocità ventola (%)"
-msgid "Temperature (°C)"
-msgstr "Temperatura (°C)"
+msgid "Temperature (℃)"
+msgstr "Temperatura (℃)"
msgid "Volumetric flow rate (mm³/s)"
msgstr "Portata volumetrica (mm³/s)"
@@ -16191,7 +16191,7 @@ msgstr ""
"Se abilitato, questo parametro imposta anche una variabile G-code denominata chamber_temperature, che può essere utilizzata per passare la temperatura desiderata della camera nella macro di inizio stampa o in una macro di preriscaldamento in questo modo: PRINT_START (altre variabili) CHAMBER_TEMP=[chamber_temperature]. Questo può essere utile se la stampante non supporta i comandi M141/M191 o, se non è presente un sistema di riscaldamento della camera, si desidera gestire il preriscaldamento nella macro di inizio stampa."
msgid ""
-"This is the chamber temperature at which printing should start, while the chamber continues heating toward the \"Target\" chamber temperature. For example, set the Target to 60 and the Minimal to 50 to begin printing once the chamber reaches 50°C, without waiting for the full 60°C.\n"
+"This is the chamber temperature at which printing should start, while the chamber continues heating toward the \"Target\" chamber temperature. For example, set the Target to 60 and the Minimal to 50 to begin printing once the chamber reaches 50℃, without waiting for the full 60℃.\n"
"\n"
"It sets a G-code variable named chamber_minimal_temperature, which can be passed to your print start macro or a heat soak macro, like this: PRINT_START (other variables) CHAMBER_MIN_TEMP=[chamber_minimal_temperature].\n"
"\n"
diff --git a/localization/i18n/ja/OrcaSlicer_ja.po b/localization/i18n/ja/OrcaSlicer_ja.po
index f109c2480b..df0dc9dc7a 100644
--- a/localization/i18n/ja/OrcaSlicer_ja.po
+++ b/localization/i18n/ja/OrcaSlicer_ja.po
@@ -5314,7 +5314,7 @@ msgstr "Jerk (mm/s)"
msgid "Fan speed (%)"
msgstr "ファン回転速度 (%)"
-msgid "Temperature (°C)"
+msgid "Temperature (℃)"
msgstr "温度 (℃)"
msgid "Volumetric flow rate (mm³/s)"
@@ -15803,7 +15803,7 @@ msgid ""
msgstr ""
msgid ""
-"This is the chamber temperature at which printing should start, while the chamber continues heating toward the \"Target\" chamber temperature. For example, set the Target to 60 and the Minimal to 50 to begin printing once the chamber reaches 50°C, without waiting for the full 60°C.\n"
+"This is the chamber temperature at which printing should start, while the chamber continues heating toward the \"Target\" chamber temperature. For example, set the Target to 60 and the Minimal to 50 to begin printing once the chamber reaches 50℃, without waiting for the full 60℃.\n"
"\n"
"It sets a G-code variable named chamber_minimal_temperature, which can be passed to your print start macro or a heat soak macro, like this: PRINT_START (other variables) CHAMBER_MIN_TEMP=[chamber_minimal_temperature].\n"
"\n"
diff --git a/localization/i18n/ko/OrcaSlicer_ko.po b/localization/i18n/ko/OrcaSlicer_ko.po
index f3a683bff0..6b514ac644 100644
--- a/localization/i18n/ko/OrcaSlicer_ko.po
+++ b/localization/i18n/ko/OrcaSlicer_ko.po
@@ -5340,7 +5340,7 @@ msgstr "Jerk (mm/s)"
msgid "Fan speed (%)"
msgstr "팬 속도 (%)"
-msgid "Temperature (°C)"
+msgid "Temperature (℃)"
msgstr "온도 (℃)"
msgid "Volumetric flow rate (mm³/s)"
@@ -16052,7 +16052,7 @@ msgstr ""
"활성화된 경우 이 매개변수는 원하는 챔버 온도를 출력 시작 매크로 또는 PRINT_START(기타 변수) CHAMBER_TEMP=[chamber_temp]와 같은 열 흡수 매크로에 전달하는 데 사용할 수 있는 Chamber_temp라는 Gcode 변수도 설정합니다. 이는 프린터가 M141/M191 명령을 지원하지 않거나 활성 챔버 히터가 설치되지 않은 경우 출력 시작 매크로에서 열 흡수를 처리하려는 경우 유용할 수 있습니다."
msgid ""
-"This is the chamber temperature at which printing should start, while the chamber continues heating toward the \"Target\" chamber temperature. For example, set the Target to 60 and the Minimal to 50 to begin printing once the chamber reaches 50°C, without waiting for the full 60°C.\n"
+"This is the chamber temperature at which printing should start, while the chamber continues heating toward the \"Target\" chamber temperature. For example, set the Target to 60 and the Minimal to 50 to begin printing once the chamber reaches 50℃, without waiting for the full 60℃.\n"
"\n"
"It sets a G-code variable named chamber_minimal_temperature, which can be passed to your print start macro or a heat soak macro, like this: PRINT_START (other variables) CHAMBER_MIN_TEMP=[chamber_minimal_temperature].\n"
"\n"
diff --git a/localization/i18n/list.txt b/localization/i18n/list.txt
index 3e99dbbaae..1614bc453b 100644
--- a/localization/i18n/list.txt
+++ b/localization/i18n/list.txt
@@ -287,4 +287,6 @@ src/slic3r/GUI/DownloaderFileGet.cpp
src/slic3r/GUI/FileArchiveDialog.cpp
src/slic3r/GUI/PrinterCloudAuthDialog.cpp
src/slic3r/GUI/PrinterWebViewHandler.cpp
+src/slic3r/GUI/AMSDryControl.cpp
+src/slic3r/GUI/AMSDryControl.hpp
src/libslic3r/PresetBundle.cpp
diff --git a/localization/i18n/lt/OrcaSlicer_lt.po b/localization/i18n/lt/OrcaSlicer_lt.po
index f8685b4eb2..a22a2efe86 100644
--- a/localization/i18n/lt/OrcaSlicer_lt.po
+++ b/localization/i18n/lt/OrcaSlicer_lt.po
@@ -5222,8 +5222,8 @@ msgstr "Trūktelėjimas (mm/s)"
msgid "Fan speed (%)"
msgstr ""
-msgid "Temperature (°C)"
-msgstr "Temperatūra (°C)"
+msgid "Temperature (℃)"
+msgstr "Temperatūra (℃)"
msgid "Volumetric flow rate (mm³/s)"
msgstr "Tūrinis srautas (mm³/s)"
diff --git a/localization/i18n/nl/OrcaSlicer_nl.po b/localization/i18n/nl/OrcaSlicer_nl.po
index 8370b5b2c3..d70de4d1b0 100644
--- a/localization/i18n/nl/OrcaSlicer_nl.po
+++ b/localization/i18n/nl/OrcaSlicer_nl.po
@@ -5285,8 +5285,8 @@ msgstr "Jerk (mm/s)"
msgid "Fan speed (%)"
msgstr "Ventilator snelheid (%)"
-msgid "Temperature (°C)"
-msgstr "Temperatuur (°C)"
+msgid "Temperature (℃)"
+msgstr "Temperatuur (℃)"
msgid "Volumetric flow rate (mm³/s)"
msgstr "Volumestroom (mm³/s)"
@@ -15730,7 +15730,7 @@ msgid ""
msgstr ""
msgid ""
-"This is the chamber temperature at which printing should start, while the chamber continues heating toward the \"Target\" chamber temperature. For example, set the Target to 60 and the Minimal to 50 to begin printing once the chamber reaches 50°C, without waiting for the full 60°C.\n"
+"This is the chamber temperature at which printing should start, while the chamber continues heating toward the \"Target\" chamber temperature. For example, set the Target to 60 and the Minimal to 50 to begin printing once the chamber reaches 50℃, without waiting for the full 60℃.\n"
"\n"
"It sets a G-code variable named chamber_minimal_temperature, which can be passed to your print start macro or a heat soak macro, like this: PRINT_START (other variables) CHAMBER_MIN_TEMP=[chamber_minimal_temperature].\n"
"\n"
diff --git a/localization/i18n/pl/OrcaSlicer_pl.po b/localization/i18n/pl/OrcaSlicer_pl.po
index 9cd81ae430..df50298f17 100644
--- a/localization/i18n/pl/OrcaSlicer_pl.po
+++ b/localization/i18n/pl/OrcaSlicer_pl.po
@@ -5337,8 +5337,8 @@ msgstr "Jerk (mm/s)"
msgid "Fan speed (%)"
msgstr "Prędkość wentylatora (%)"
-msgid "Temperature (°C)"
-msgstr "Temperatura (°C)"
+msgid "Temperature (℃)"
+msgstr "Temperatura (℃)"
msgid "Volumetric flow rate (mm³/s)"
msgstr "Natężenie przepływu (mm³/s)"
@@ -16069,7 +16069,7 @@ msgstr ""
"Jeśli włączona, ten parametr ustawia także zmienną gcode o nazwie chamber_temperature, która może być użyta do przekazania żądanej temperatury komory do makra rozpoczynającego drukowanie, lub makra utrzymywania ciepła, na przykład: PRINT_START (inne zmienne) CHAMBER_TEMP=[chamber_temperature]. Może to być przydatne, jeśli twoja drukarka nie obsługuje poleceń M141/M191 lub jeśli chcesz zarządzać utrzymywaniem ciepła w makrze rozpoczynającym drukowanie, jeśli nie jest zainstalowany aktywna podgrzewacz komory."
msgid ""
-"This is the chamber temperature at which printing should start, while the chamber continues heating toward the \"Target\" chamber temperature. For example, set the Target to 60 and the Minimal to 50 to begin printing once the chamber reaches 50°C, without waiting for the full 60°C.\n"
+"This is the chamber temperature at which printing should start, while the chamber continues heating toward the \"Target\" chamber temperature. For example, set the Target to 60 and the Minimal to 50 to begin printing once the chamber reaches 50℃, without waiting for the full 60℃.\n"
"\n"
"It sets a G-code variable named chamber_minimal_temperature, which can be passed to your print start macro or a heat soak macro, like this: PRINT_START (other variables) CHAMBER_MIN_TEMP=[chamber_minimal_temperature].\n"
"\n"
@@ -22423,8 +22423,8 @@ msgstr ""
#~ msgid "Enter the nozzle_temperature needed for extruding your filament."
#~ msgstr "Wprowadź temperaturę dyszy potrzebną do ekstruzji filamentu."
-#~ msgid "A rule of thumb is 160 to 230 °C for PLA, and 215 to 250 °C for ABS."
-#~ msgstr "Generalną zasadą jest 160 do 230 °C dla PLA i 215 do 250 °C dla ABS."
+#~ msgid "A rule of thumb is 160 to 230℃ for PLA, and 215 to 250℃ for ABS."
+#~ msgstr "Generalną zasadą jest 160 do 230℃ dla PLA i 215 do 250℃ dla ABS."
#~ msgid "Extrusion Temperature:"
#~ msgstr "Temperatura ekstrudera:"
@@ -22432,8 +22432,8 @@ msgstr ""
#~ msgid "Enter the bed temperature needed for getting your filament to stick to your heated bed."
#~ msgstr "Wprowadź temperaturę potrzebną do dobrego przylegania filamentu do powierzchni podgrzewanego stołu."
-#~ msgid "A rule of thumb is 60 °C for PLA and 110 °C for ABS. Leave zero if you have no heated bed."
-#~ msgstr "Generalną zasadą jest 60 °C dla PLA i 110 °C dla ABS. Ustaw zero, jeśli nie masz podgrzewanego stołu."
+#~ msgid "A rule of thumb is 60℃ for PLA and 110℃ for ABS. Leave zero if you have no heated bed."
+#~ msgstr "Generalną zasadą jest 60℃ dla PLA i 110℃ dla ABS. Ustaw zero, jeśli nie masz podgrzewanego stołu."
#~ msgid "Bed Temperature:"
#~ msgstr "Temperatura stołu"
diff --git a/localization/i18n/pt_BR/OrcaSlicer_pt_BR.po b/localization/i18n/pt_BR/OrcaSlicer_pt_BR.po
index b09793b90d..985c56e0da 100644
--- a/localization/i18n/pt_BR/OrcaSlicer_pt_BR.po
+++ b/localization/i18n/pt_BR/OrcaSlicer_pt_BR.po
@@ -5256,7 +5256,7 @@ msgstr "Jerk (mm/s)"
msgid "Fan speed (%)"
msgstr "Velocidade do ventilador (%)"
-msgid "Temperature (°C)"
+msgid "Temperature (℃)"
msgstr "Temperatura (℃)"
msgid "Volumetric flow rate (mm³/s)"
@@ -16049,7 +16049,7 @@ msgstr ""
"Se habilitado, este parâmetro também define uma variável G-code chamada chamber_temperature, que pode ser usada para passar a temperatura desejada da câmara para sua macro de início de impressão ou uma macro de absorção de calor como esta: PRINT_START (outras variáveis) CHAMBER_TEMP=[chamber_temperature]. Isso pode ser útil se sua impressora não suportar comandos M141/M191 ou se você desejar lidar com a absorção de calor na macro de início de impressão se nenhum aquecedor de câmara ativo estiver instalado."
msgid ""
-"This is the chamber temperature at which printing should start, while the chamber continues heating toward the \"Target\" chamber temperature. For example, set the Target to 60 and the Minimal to 50 to begin printing once the chamber reaches 50°C, without waiting for the full 60°C.\n"
+"This is the chamber temperature at which printing should start, while the chamber continues heating toward the \"Target\" chamber temperature. For example, set the Target to 60 and the Minimal to 50 to begin printing once the chamber reaches 50℃, without waiting for the full 60℃.\n"
"\n"
"It sets a G-code variable named chamber_minimal_temperature, which can be passed to your print start macro or a heat soak macro, like this: PRINT_START (other variables) CHAMBER_MIN_TEMP=[chamber_minimal_temperature].\n"
"\n"
diff --git a/localization/i18n/ru/OrcaSlicer_ru.po b/localization/i18n/ru/OrcaSlicer_ru.po
index e3dfca8145..7bc6e42c44 100644
--- a/localization/i18n/ru/OrcaSlicer_ru.po
+++ b/localization/i18n/ru/OrcaSlicer_ru.po
@@ -5417,7 +5417,7 @@ msgstr "Jerk (mm/s)"
msgid "Fan speed (%)"
msgstr "Скорость вентилятора (%)"
-msgid "Temperature (°C)"
+msgid "Temperature (℃)"
msgstr "Температура (°C)"
msgid "Volumetric flow rate (mm³/s)"
@@ -16781,7 +16781,7 @@ msgstr ""
"Это особенно полезно, если принтер не поддерживает команды M141/M191, или если управление температурой термокамеры реализовано через макросы (например, при отсутствии активного нагревателя камеры)."
msgid ""
-"This is the chamber temperature at which printing should start, while the chamber continues heating toward the \"Target\" chamber temperature. For example, set the Target to 60 and the Minimal to 50 to begin printing once the chamber reaches 50°C, without waiting for the full 60°C.\n"
+"This is the chamber temperature at which printing should start, while the chamber continues heating toward the \"Target\" chamber temperature. For example, set the Target to 60 and the Minimal to 50 to begin printing once the chamber reaches 50℃, without waiting for the full 60℃.\n"
"\n"
"It sets a G-code variable named chamber_minimal_temperature, which can be passed to your print start macro or a heat soak macro, like this: PRINT_START (other variables) CHAMBER_MIN_TEMP=[chamber_minimal_temperature].\n"
"\n"
diff --git a/localization/i18n/sv/OrcaSlicer_sv.po b/localization/i18n/sv/OrcaSlicer_sv.po
index d3f1d20d8b..8ea2df6ba9 100644
--- a/localization/i18n/sv/OrcaSlicer_sv.po
+++ b/localization/i18n/sv/OrcaSlicer_sv.po
@@ -5275,8 +5275,8 @@ msgstr "Jerk (mm/s)"
msgid "Fan speed (%)"
msgstr "Fläkt hastighet (%)"
-msgid "Temperature (°C)"
-msgstr "Temperatur (°C)"
+msgid "Temperature (℃)"
+msgstr "Temperatur (℃)"
msgid "Volumetric flow rate (mm³/s)"
msgstr "Volymetrisk flödeshastighet (mm³/s)"
@@ -15701,7 +15701,7 @@ msgid ""
msgstr ""
msgid ""
-"This is the chamber temperature at which printing should start, while the chamber continues heating toward the \"Target\" chamber temperature. For example, set the Target to 60 and the Minimal to 50 to begin printing once the chamber reaches 50°C, without waiting for the full 60°C.\n"
+"This is the chamber temperature at which printing should start, while the chamber continues heating toward the \"Target\" chamber temperature. For example, set the Target to 60 and the Minimal to 50 to begin printing once the chamber reaches 50℃, without waiting for the full 60℃.\n"
"\n"
"It sets a G-code variable named chamber_minimal_temperature, which can be passed to your print start macro or a heat soak macro, like this: PRINT_START (other variables) CHAMBER_MIN_TEMP=[chamber_minimal_temperature].\n"
"\n"
diff --git a/localization/i18n/th/OrcaSlicer_th.po b/localization/i18n/th/OrcaSlicer_th.po
index b4e955f68e..55b5bd6a95 100644
--- a/localization/i18n/th/OrcaSlicer_th.po
+++ b/localization/i18n/th/OrcaSlicer_th.po
@@ -5223,8 +5223,8 @@ msgstr "กระตุก (มม./วินาที)"
msgid "Fan speed (%)"
msgstr "ความเร็วพัดลม (%)"
-msgid "Temperature (°C)"
-msgstr "อุณหภูมิ (°C)"
+msgid "Temperature (℃)"
+msgstr "อุณหภูมิ (℃)"
msgid "Volumetric flow rate (mm³/s)"
msgstr "อัตราการไหลเชิงปริมาตร (มม.³/วินาที)"
@@ -15887,7 +15887,7 @@ msgstr ""
"หากเปิดใช้งาน พารามิเตอร์นี้จะตั้งค่าตัวแปร G-code ชื่อ Chamber_temperature ซึ่งสามารถใช้เพื่อส่งอุณหภูมิห้องเพาะเลี้ยงที่ต้องการไปยังมาโครเริ่มการพิมพ์ของคุณ หรือมาโครความร้อนแช่เช่นนี้: PRINT_START (ตัวแปรอื่นๆ) CHAMBER_TEMP=[chamber_temperature] วิธีนี้อาจเป็นประโยชน์หากเครื่องพิมพ์ของคุณไม่รองรับคำสั่ง M141/M191 หรือหากคุณต้องการจัดการกับความร้อนที่แช่อยู่ในมาโครเริ่มการพิมพ์ หากไม่มีการติดตั้งเครื่องทำความร้อนในห้องที่ใช้งานอยู่"
msgid ""
-"This is the chamber temperature at which printing should start, while the chamber continues heating toward the \"Target\" chamber temperature. For example, set the Target to 60 and the Minimal to 50 to begin printing once the chamber reaches 50°C, without waiting for the full 60°C.\n"
+"This is the chamber temperature at which printing should start, while the chamber continues heating toward the \"Target\" chamber temperature. For example, set the Target to 60 and the Minimal to 50 to begin printing once the chamber reaches 50℃, without waiting for the full 60℃.\n"
"\n"
"It sets a G-code variable named chamber_minimal_temperature, which can be passed to your print start macro or a heat soak macro, like this: PRINT_START (other variables) CHAMBER_MIN_TEMP=[chamber_minimal_temperature].\n"
"\n"
diff --git a/localization/i18n/tr/OrcaSlicer_tr.po b/localization/i18n/tr/OrcaSlicer_tr.po
index 9c104a8baf..c5d91ffbf9 100644
--- a/localization/i18n/tr/OrcaSlicer_tr.po
+++ b/localization/i18n/tr/OrcaSlicer_tr.po
@@ -5341,8 +5341,8 @@ msgstr "Jerk (mm/s)"
msgid "Fan speed (%)"
msgstr "Fan hızı (%)"
-msgid "Temperature (°C)"
-msgstr "Sıcaklık (°C)"
+msgid "Temperature (℃)"
+msgstr "Sıcaklık (℃)"
msgid "Volumetric flow rate (mm³/s)"
msgstr "Hacimsel akış hızı (mm³/s)"
@@ -16113,7 +16113,7 @@ msgstr ""
"Etkinleştirilirse, bu parametre aynı zamanda istenen oda sıcaklığını yazdırma başlatma makronuza veya şuna benzer bir ısı emme makrosuna iletmek için kullanılabilecek Chamber_temperature adlı bir gcode değişkenini de ayarlar: PRINT_START (diğer değişkenler) CHAMBER_TEMP=[chamber_temperature]. Yazıcınız M141/M191 komutlarını desteklemiyorsa veya aktif oda ısıtıcısı takılı değilse yazdırma başlatma makrosunda ısı bekletme işlemini gerçekleştirmek istiyorsanız bu yararlı olabilir."
msgid ""
-"This is the chamber temperature at which printing should start, while the chamber continues heating toward the \"Target\" chamber temperature. For example, set the Target to 60 and the Minimal to 50 to begin printing once the chamber reaches 50°C, without waiting for the full 60°C.\n"
+"This is the chamber temperature at which printing should start, while the chamber continues heating toward the \"Target\" chamber temperature. For example, set the Target to 60 and the Minimal to 50 to begin printing once the chamber reaches 50℃, without waiting for the full 60℃.\n"
"\n"
"It sets a G-code variable named chamber_minimal_temperature, which can be passed to your print start macro or a heat soak macro, like this: PRINT_START (other variables) CHAMBER_MIN_TEMP=[chamber_minimal_temperature].\n"
"\n"
diff --git a/localization/i18n/uk/OrcaSlicer_uk.po b/localization/i18n/uk/OrcaSlicer_uk.po
index 4fc4fb151c..4fbe7445b3 100644
--- a/localization/i18n/uk/OrcaSlicer_uk.po
+++ b/localization/i18n/uk/OrcaSlicer_uk.po
@@ -5344,8 +5344,8 @@ msgstr "Jerk (mm/s)"
msgid "Fan speed (%)"
msgstr "Швидкість Вентилятора (%)"
-msgid "Temperature (°C)"
-msgstr "Температура (°С)"
+msgid "Temperature (℃)"
+msgstr "Температура (℃)"
msgid "Volumetric flow rate (mm³/s)"
msgstr "Об'ємна витрата (мм³/с)"
@@ -16089,7 +16089,7 @@ msgstr ""
"Це може бути корисним, якщо ваш принтер не підтримує команди M141/M191 або якщо ви хочете керувати прогрівом камери у макросі запуску друку за відсутності активного нагрівача камери."
msgid ""
-"This is the chamber temperature at which printing should start, while the chamber continues heating toward the \"Target\" chamber temperature. For example, set the Target to 60 and the Minimal to 50 to begin printing once the chamber reaches 50°C, without waiting for the full 60°C.\n"
+"This is the chamber temperature at which printing should start, while the chamber continues heating toward the \"Target\" chamber temperature. For example, set the Target to 60 and the Minimal to 50 to begin printing once the chamber reaches 50℃, without waiting for the full 60℃.\n"
"\n"
"It sets a G-code variable named chamber_minimal_temperature, which can be passed to your print start macro or a heat soak macro, like this: PRINT_START (other variables) CHAMBER_MIN_TEMP=[chamber_minimal_temperature].\n"
"\n"
diff --git a/localization/i18n/vi/OrcaSlicer_vi.po b/localization/i18n/vi/OrcaSlicer_vi.po
index fffd7dd6e6..f0fe2f3613 100644
--- a/localization/i18n/vi/OrcaSlicer_vi.po
+++ b/localization/i18n/vi/OrcaSlicer_vi.po
@@ -5315,8 +5315,8 @@ msgstr "Jerk (mm/s)"
msgid "Fan speed (%)"
msgstr "Tốc độ quạt (%)"
-msgid "Temperature (°C)"
-msgstr "Nhiệt độ (°C)"
+msgid "Temperature (℃)"
+msgstr "Nhiệt độ (℃)"
msgid "Volumetric flow rate (mm³/s)"
msgstr "Tốc độ flow thể tích (mm³/s)"
@@ -16000,7 +16000,7 @@ msgstr ""
"Nếu được bật, tham số này cũng đặt biến G-code có tên chamber_temperature, có thể được sử dụng để truyền nhiệt độ buồng mong muốn cho macro bắt đầu in của bạn, hoặc macro ngâm nhiệt như thế này: PRINT_START (các biến khác) CHAMBER_TEMP=[chamber_temperature]. Điều này có thể hữu ích nếu máy in của bạn không hỗ trợ lệnh M141/M191, hoặc nếu bạn muốn xử lý ngâm nhiệt trong macro bắt đầu in nếu không có bộ sưởi buồng hoạt động được cài đặt."
msgid ""
-"This is the chamber temperature at which printing should start, while the chamber continues heating toward the \"Target\" chamber temperature. For example, set the Target to 60 and the Minimal to 50 to begin printing once the chamber reaches 50°C, without waiting for the full 60°C.\n"
+"This is the chamber temperature at which printing should start, while the chamber continues heating toward the \"Target\" chamber temperature. For example, set the Target to 60 and the Minimal to 50 to begin printing once the chamber reaches 50℃, without waiting for the full 60℃.\n"
"\n"
"It sets a G-code variable named chamber_minimal_temperature, which can be passed to your print start macro or a heat soak macro, like this: PRINT_START (other variables) CHAMBER_MIN_TEMP=[chamber_minimal_temperature].\n"
"\n"
diff --git a/localization/i18n/zh_CN/OrcaSlicer_zh_CN.po b/localization/i18n/zh_CN/OrcaSlicer_zh_CN.po
index 66ac4a246b..0209b27104 100644
--- a/localization/i18n/zh_CN/OrcaSlicer_zh_CN.po
+++ b/localization/i18n/zh_CN/OrcaSlicer_zh_CN.po
@@ -5328,7 +5328,7 @@ msgstr "抖动 (mm/s)"
msgid "Fan speed (%)"
msgstr "风扇速度(%)"
-msgid "Temperature (°C)"
+msgid "Temperature (℃)"
msgstr "温度(℃)"
msgid "Volumetric flow rate (mm³/s)"
@@ -16215,7 +16215,7 @@ msgid ""
msgstr "对于 ABS、ASA、PC 和 PA 等高温材料,较高的腔室温度有助于抑制或减少翘曲,并有可能提高层间粘合强度。但同时,较高的腔室温度会降低 ABS 和 ASA 的空气过滤效率。 对于 PLA、PETG、TPU、PVA 等低温材料,应禁用此选项(设置为 0),因为腔室温度应较低,以避免热断时材料软化导致挤出机堵塞。 如果启用,此参数还会设置一个名为 chamber_temple 的 G-code 变量,该变量可用于将所需的腔室温度传递给打印启动宏,或热浸宏,如下所示:PRINT_START(其他变量)CHAMBER_TEMP=[chamber_Temperature]。如果您的打印机不支持 M141/M191 命令,或者如果您希望在未安装活动室加热器的情况下在打印启动宏中处理热浸,则这可能很有用。"
msgid ""
-"This is the chamber temperature at which printing should start, while the chamber continues heating toward the \"Target\" chamber temperature. For example, set the Target to 60 and the Minimal to 50 to begin printing once the chamber reaches 50°C, without waiting for the full 60°C.\n"
+"This is the chamber temperature at which printing should start, while the chamber continues heating toward the \"Target\" chamber temperature. For example, set the Target to 60 and the Minimal to 50 to begin printing once the chamber reaches 50℃, without waiting for the full 60℃.\n"
"\n"
"It sets a G-code variable named chamber_minimal_temperature, which can be passed to your print start macro or a heat soak macro, like this: PRINT_START (other variables) CHAMBER_MIN_TEMP=[chamber_minimal_temperature].\n"
"\n"
diff --git a/localization/i18n/zh_TW/OrcaSlicer_zh_TW.po b/localization/i18n/zh_TW/OrcaSlicer_zh_TW.po
index 6ab21ea1cc..dbca75c52d 100644
--- a/localization/i18n/zh_TW/OrcaSlicer_zh_TW.po
+++ b/localization/i18n/zh_TW/OrcaSlicer_zh_TW.po
@@ -5370,7 +5370,7 @@ msgstr "急衝速度 (mm/s)"
msgid "Fan speed (%)"
msgstr "風扇速度(%)"
-msgid "Temperature (°C)"
+msgid "Temperature (℃)"
msgstr "溫度(℃)"
msgid "Volumetric flow rate (mm³/s)"
@@ -16263,7 +16263,7 @@ msgid ""
msgstr "對於 ABS、ASA、PC 和 PA 等高溫材料,較高的機箱溫度能有效抑制或減少翹曲,並可能提升層間結合強度。然而,較高的機箱溫度也會降低 ABS 和 ASA 的空氣過濾效率。對於 PLA、PETG、TPU、PVA 和其他低溫材料,建議將此選項停用(設為 0),因機箱溫度應保持較低,以避免因材料在熱端軟化而導致擠出機堵塞。啟用此選項後,會設置一個名為 chamber_temperature 的 G-code 變數,可用於將所需的機箱溫度傳遞給列印開始宏,或像以下的熱浸泡宏(預熱):PRINT_START (其他變數) CHAMBER_TEMP=[chamber_temperature]。這對於不支援 M141/M191 指令的列印設備,或者希望在列印開始宏中處理熱浸泡(預熱)但未安裝主動機箱加熱器的情況下非常實用。"
msgid ""
-"This is the chamber temperature at which printing should start, while the chamber continues heating toward the \"Target\" chamber temperature. For example, set the Target to 60 and the Minimal to 50 to begin printing once the chamber reaches 50°C, without waiting for the full 60°C.\n"
+"This is the chamber temperature at which printing should start, while the chamber continues heating toward the \"Target\" chamber temperature. For example, set the Target to 60 and the Minimal to 50 to begin printing once the chamber reaches 50℃, without waiting for the full 60℃.\n"
"\n"
"It sets a G-code variable named chamber_minimal_temperature, which can be passed to your print start macro or a heat soak macro, like this: PRINT_START (other variables) CHAMBER_MIN_TEMP=[chamber_minimal_temperature].\n"
"\n"
diff --git a/resources/images/dev_ams_dry_ctr_disable.svg b/resources/images/dev_ams_dry_ctr_disable.svg
new file mode 100644
index 0000000000..f2212204e9
--- /dev/null
+++ b/resources/images/dev_ams_dry_ctr_disable.svg
@@ -0,0 +1,5 @@
+
diff --git a/resources/images/dev_ams_dry_ctr_enable.svg b/resources/images/dev_ams_dry_ctr_enable.svg
new file mode 100644
index 0000000000..31eb482d17
--- /dev/null
+++ b/resources/images/dev_ams_dry_ctr_enable.svg
@@ -0,0 +1,4 @@
+
diff --git a/resources/images/dev_ams_dry_ctr_filament_in_chamber.png b/resources/images/dev_ams_dry_ctr_filament_in_chamber.png
new file mode 100644
index 0000000000..1b36643c42
Binary files /dev/null and b/resources/images/dev_ams_dry_ctr_filament_in_chamber.png differ
diff --git a/resources/images/dev_ams_dry_ctr_heating_icon.svg b/resources/images/dev_ams_dry_ctr_heating_icon.svg
new file mode 100644
index 0000000000..614b0c9a92
--- /dev/null
+++ b/resources/images/dev_ams_dry_ctr_heating_icon.svg
@@ -0,0 +1,3 @@
+
diff --git a/resources/images/dev_ams_dry_ctr_n3f_cooling.png b/resources/images/dev_ams_dry_ctr_n3f_cooling.png
new file mode 100644
index 0000000000..6a43425da5
Binary files /dev/null and b/resources/images/dev_ams_dry_ctr_n3f_cooling.png differ
diff --git a/resources/images/dev_ams_dry_ctr_n3f_dehumidifying.png b/resources/images/dev_ams_dry_ctr_n3f_dehumidifying.png
new file mode 100644
index 0000000000..92064da370
Binary files /dev/null and b/resources/images/dev_ams_dry_ctr_n3f_dehumidifying.png differ
diff --git a/resources/images/dev_ams_dry_ctr_n3f_error.png b/resources/images/dev_ams_dry_ctr_n3f_error.png
new file mode 100644
index 0000000000..e0e49df9d1
Binary files /dev/null and b/resources/images/dev_ams_dry_ctr_n3f_error.png differ
diff --git a/resources/images/dev_ams_dry_ctr_n3f_heating.png b/resources/images/dev_ams_dry_ctr_n3f_heating.png
new file mode 100644
index 0000000000..7c48397b63
Binary files /dev/null and b/resources/images/dev_ams_dry_ctr_n3f_heating.png differ
diff --git a/resources/images/dev_ams_dry_ctr_n3s_cooling.png b/resources/images/dev_ams_dry_ctr_n3s_cooling.png
new file mode 100644
index 0000000000..f663c57c5f
Binary files /dev/null and b/resources/images/dev_ams_dry_ctr_n3s_cooling.png differ
diff --git a/resources/images/dev_ams_dry_ctr_n3s_dehumidifying.png b/resources/images/dev_ams_dry_ctr_n3s_dehumidifying.png
new file mode 100644
index 0000000000..5926a6cb54
Binary files /dev/null and b/resources/images/dev_ams_dry_ctr_n3s_dehumidifying.png differ
diff --git a/resources/images/dev_ams_dry_ctr_n3s_error.png b/resources/images/dev_ams_dry_ctr_n3s_error.png
new file mode 100644
index 0000000000..0b3df1c3fe
Binary files /dev/null and b/resources/images/dev_ams_dry_ctr_n3s_error.png differ
diff --git a/resources/images/dev_ams_dry_ctr_n3s_heating.png b/resources/images/dev_ams_dry_ctr_n3s_heating.png
new file mode 100644
index 0000000000..3613a3aefc
Binary files /dev/null and b/resources/images/dev_ams_dry_ctr_n3s_heating.png differ
diff --git a/resources/profiles/BBL/filament/Bambu ASA-Aero @base.json b/resources/profiles/BBL/filament/Bambu ASA-Aero @base.json
index 2dcf633a26..1a36118d7f 100644
--- a/resources/profiles/BBL/filament/Bambu ASA-Aero @base.json
+++ b/resources/profiles/BBL/filament/Bambu ASA-Aero @base.json
@@ -42,8 +42,8 @@
"filament_z_hop_types": [
"Normal Lift"
],
- "filament_scarf_seam_type": [
- "none"
+ "filament_dev_drying_cooling_temperature": [
+ "70"
],
"impact_strength_z": [
"3.4"
diff --git a/resources/profiles/BBL/filament/Bambu ASA-CF @base.json b/resources/profiles/BBL/filament/Bambu ASA-CF @base.json
index 9c425dba90..69624032bb 100644
--- a/resources/profiles/BBL/filament/Bambu ASA-CF @base.json
+++ b/resources/profiles/BBL/filament/Bambu ASA-CF @base.json
@@ -32,6 +32,9 @@
"filament_vendor": [
"Bambu Lab"
],
+ "filament_dev_drying_cooling_temperature": [
+ "95"
+ ],
"hot_plate_temp": [
"100"
],
diff --git a/resources/profiles/BBL/filament/Bambu PAHT-CF @base.json b/resources/profiles/BBL/filament/Bambu PAHT-CF @base.json
index 54122f3bbf..28b1391f84 100644
--- a/resources/profiles/BBL/filament/Bambu PAHT-CF @base.json
+++ b/resources/profiles/BBL/filament/Bambu PAHT-CF @base.json
@@ -33,6 +33,9 @@
"full_fan_speed_layer": [
"2"
],
+ "filament_dev_chamber_drying_time": [
+ "16"
+ ],
"impact_strength_z": [
"13.3"
],
diff --git a/resources/profiles/BBL/filament/Bambu PET-CF @base.json b/resources/profiles/BBL/filament/Bambu PET-CF @base.json
index b982cd1361..ff8b40070f 100644
--- a/resources/profiles/BBL/filament/Bambu PET-CF @base.json
+++ b/resources/profiles/BBL/filament/Bambu PET-CF @base.json
@@ -45,6 +45,27 @@
"filament_adhesiveness_category": [
"800"
],
+ "filament_dev_chamber_drying_bed_temperature": [
+ "90"
+ ],
+ "filament_dev_ams_drying_ams_limitations": [
+ "1"
+ ],
+ "filament_dev_ams_drying_heat_distortion_temperature": [
+ "165"
+ ],
+ "filament_dev_drying_cooling_temperature": [
+ "150"
+ ],
+ "filament_dev_ams_drying_temperature": [
+ "65",
+ "80",
+ "65",
+ "80"
+ ],
+ "filament_dev_drying_softening_temperature": [
+ "150"
+ ],
"hot_plate_temp": [
"100"
],
diff --git a/resources/profiles/BBL/filament/Bambu Support For PA PET @base.json b/resources/profiles/BBL/filament/Bambu Support For PA PET @base.json
index a622bff105..d199cdb0a0 100644
--- a/resources/profiles/BBL/filament/Bambu Support For PA PET @base.json
+++ b/resources/profiles/BBL/filament/Bambu Support For PA PET @base.json
@@ -24,6 +24,24 @@
"filament_adhesiveness_category": [
"703"
],
+ "filament_dev_chamber_drying_time": [
+ "16"
+ ],
+ "filament_dev_ams_drying_heat_distortion_temperature": [
+ "90"
+ ],
+ "filament_dev_drying_cooling_temperature": [
+ "75"
+ ],
+ "filament_dev_ams_drying_temperature": [
+ "65",
+ "80",
+ "65",
+ "80"
+ ],
+ "filament_dev_drying_softening_temperature": [
+ "85"
+ ],
"nozzle_temperature": [
"280"
],
diff --git a/resources/profiles/BBL/filament/Bambu Support For PLA @base.json b/resources/profiles/BBL/filament/Bambu Support For PLA @base.json
index 10e4983ebf..9bfdef5bf9 100644
--- a/resources/profiles/BBL/filament/Bambu Support For PLA @base.json
+++ b/resources/profiles/BBL/filament/Bambu Support For PLA @base.json
@@ -24,11 +24,20 @@
"filament_vendor": [
"Bambu Lab"
],
- "filament_scarf_seam_type": [
- "none"
- ],
"filament_adhesiveness_category": [
"702"
+ ],
+ "filament_dev_ams_drying_temperature": [
+ "60",
+ "60",
+ "50",
+ "50"
+ ],
+ "filament_dev_ams_drying_heat_distortion_temperature": [
+ "50"
+ ],
+ "filament_dev_drying_softening_temperature": [
+ "55"
],
"slow_down_layer_time": [
"8"
diff --git a/resources/profiles/BBL/filament/Bambu Support For PLA-PETG @base.json b/resources/profiles/BBL/filament/Bambu Support For PLA-PETG @base.json
index 0f1b87e83f..38443f5be3 100644
--- a/resources/profiles/BBL/filament/Bambu Support For PLA-PETG @base.json
+++ b/resources/profiles/BBL/filament/Bambu Support For PLA-PETG @base.json
@@ -27,14 +27,26 @@
"filament_max_volumetric_speed": [
"6"
],
- "filament_scarf_seam_type": [
- "none"
- ],
"filament_vendor": [
"Bambu Lab"
],
"filament_adhesiveness_category": [
"705"
+ ],
+ "filament_dev_chamber_drying_bed_temperature": [
+ "80"
+ ],
+ "filament_dev_ams_drying_temperature": [
+ "60",
+ "60",
+ "50",
+ "50"
+ ],
+ "filament_dev_ams_drying_heat_distortion_temperature": [
+ "50"
+ ],
+ "filament_dev_drying_softening_temperature": [
+ "55"
],
"hot_plate_temp": [
"60"
diff --git a/resources/profiles/BBL/filament/Bambu Support G @base.json b/resources/profiles/BBL/filament/Bambu Support G @base.json
index c903f92d07..9cb01972b8 100644
--- a/resources/profiles/BBL/filament/Bambu Support G @base.json
+++ b/resources/profiles/BBL/filament/Bambu Support G @base.json
@@ -21,11 +21,26 @@
"filament_vendor": [
"Bambu Lab"
],
- "filament_scarf_seam_type": [
- "none"
- ],
"filament_adhesiveness_category": [
"701"
+ ],
+ "filament_dev_chamber_drying_time": [
+ "16"
+ ],
+ "filament_dev_ams_drying_heat_distortion_temperature": [
+ "90"
+ ],
+ "filament_dev_drying_cooling_temperature": [
+ "75"
+ ],
+ "filament_dev_ams_drying_temperature": [
+ "65",
+ "80",
+ "65",
+ "80"
+ ],
+ "filament_dev_drying_softening_temperature": [
+ "85"
],
"nozzle_temperature": [
"280"
diff --git a/resources/profiles/BBL/filament/Bambu Support for ABS @base.json b/resources/profiles/BBL/filament/Bambu Support for ABS @base.json
index 35e13e67a0..bc8867db2a 100644
--- a/resources/profiles/BBL/filament/Bambu Support for ABS @base.json
+++ b/resources/profiles/BBL/filament/Bambu Support for ABS @base.json
@@ -27,11 +27,14 @@
"filament_vendor": [
"Bambu Lab"
],
- "filament_scarf_seam_type": [
- "none"
- ],
"filament_adhesiveness_category": [
"706"
+ ],
+ "filament_dev_ams_drying_time": [
+ "12",
+ "4",
+ "12",
+ "4"
],
"nozzle_temperature_range_high": [
"270"
diff --git a/resources/profiles/BBL/filament/Generic PA @base.json b/resources/profiles/BBL/filament/Generic PA @base.json
index 5947f5a859..fe819487b0 100644
--- a/resources/profiles/BBL/filament/Generic PA @base.json
+++ b/resources/profiles/BBL/filament/Generic PA @base.json
@@ -21,6 +21,21 @@
"filament_max_volumetric_speed": [
"12"
],
+ "filament_dev_ams_drying_temperature": [
+ "65",
+ "80",
+ "65",
+ "80"
+ ],
+ "filament_dev_ams_drying_heat_distortion_temperature": [
+ "90"
+ ],
+ "filament_dev_drying_cooling_temperature": [
+ "65"
+ ],
+ "filament_dev_drying_softening_temperature": [
+ "85"
+ ],
"nozzle_temperature": [
"260"
],
diff --git a/resources/profiles/BBL/filament/Generic PCTG @base.json b/resources/profiles/BBL/filament/Generic PCTG @base.json
index d0ec60848f..e8b7c1ce0a 100644
--- a/resources/profiles/BBL/filament/Generic PCTG @base.json
+++ b/resources/profiles/BBL/filament/Generic PCTG @base.json
@@ -38,6 +38,34 @@
"filament_max_volumetric_speed": [
"6"
],
+ "filament_dev_chamber_drying_bed_temperature": [
+ "80"
+ ],
+ "filament_dev_ams_drying_time": [
+ "12",
+ "12",
+ "12",
+ "12"
+ ],
+ "filament_dev_ams_drying_ams_limitations": [
+ "1",
+ "0"
+ ],
+ "filament_dev_ams_drying_heat_distortion_temperature": [
+ "75"
+ ],
+ "filament_dev_drying_cooling_temperature": [
+ "55"
+ ],
+ "filament_dev_ams_drying_temperature": [
+ "65",
+ "65",
+ "55",
+ "55"
+ ],
+ "filament_dev_drying_softening_temperature": [
+ "60"
+ ],
"hot_plate_temp": [
"70"
],
diff --git a/resources/profiles/BBL/filament/Generic PPS @base.json b/resources/profiles/BBL/filament/Generic PPS @base.json
index e8da776dfc..882fea94e0 100644
--- a/resources/profiles/BBL/filament/Generic PPS @base.json
+++ b/resources/profiles/BBL/filament/Generic PPS @base.json
@@ -4,5 +4,26 @@
"inherits": "fdm_filament_pps",
"from": "system",
"filament_id": "GFT97",
- "instantiation": "false"
+ "instantiation": "false",
+ "filament_dev_chamber_drying_bed_temperature": [
+ "90"
+ ],
+ "filament_dev_ams_drying_ams_limitations": [
+ "1"
+ ],
+ "filament_dev_ams_drying_heat_distortion_temperature": [
+ "90"
+ ],
+ "filament_dev_drying_cooling_temperature": [
+ "80"
+ ],
+ "filament_dev_ams_drying_temperature": [
+ "65",
+ "80",
+ "65",
+ "80"
+ ],
+ "filament_dev_drying_softening_temperature": [
+ "85"
+ ]
}
diff --git a/resources/profiles/BBL/filament/Polymaker/Fiberon PET-CF @base.json b/resources/profiles/BBL/filament/Polymaker/Fiberon PET-CF @base.json
index 921ce55dd6..365ebb2fd7 100644
--- a/resources/profiles/BBL/filament/Polymaker/Fiberon PET-CF @base.json
+++ b/resources/profiles/BBL/filament/Polymaker/Fiberon PET-CF @base.json
@@ -41,6 +41,30 @@
"filament_vendor": [
"Polymaker"
],
+ "filament_adhesiveness_category": [
+ "800"
+ ],
+ "filament_dev_chamber_drying_bed_temperature": [
+ "90"
+ ],
+ "filament_dev_ams_drying_ams_limitations": [
+ "1"
+ ],
+ "filament_dev_ams_drying_heat_distortion_temperature": [
+ "165"
+ ],
+ "filament_dev_drying_cooling_temperature": [
+ "150"
+ ],
+ "filament_dev_ams_drying_temperature": [
+ "65",
+ "80",
+ "65",
+ "80"
+ ],
+ "filament_dev_drying_softening_temperature": [
+ "150"
+ ],
"hot_plate_temp": [
"70"
],
@@ -83,9 +107,6 @@
"textured_plate_temp_initial_layer": [
"70"
],
- "filament_adhesiveness_category": [
- "800"
- ],
"filament_start_gcode": [
"; filament start gcode\n{if (bed_temperature[current_extruder] >80)||(bed_temperature_initial_layer[current_extruder] >80)}M106 P3 S255\n{elsif (bed_temperature[current_extruder] >60)||(bed_temperature_initial_layer[current_extruder] >60)}M106 P3 S180\n{endif}\n\n{if activate_air_filtration[current_extruder] && support_air_filtration}\nM106 P3 S{during_print_exhaust_fan_speed_num[current_extruder]} \n{endif}"
]
diff --git a/resources/profiles/BBL/filament/fdm_filament_abs.json b/resources/profiles/BBL/filament/fdm_filament_abs.json
index d05b005db9..4eb47ce0bd 100644
--- a/resources/profiles/BBL/filament/fdm_filament_abs.json
+++ b/resources/profiles/BBL/filament/fdm_filament_abs.json
@@ -43,6 +43,27 @@
"filament_adhesiveness_category": [
"200"
],
+ "filament_dev_ams_drying_time": [
+ "12",
+ "8.0",
+ "12",
+ "8.0"
+ ],
+ "filament_dev_ams_drying_heat_distortion_temperature": [
+ "90"
+ ],
+ "filament_dev_drying_cooling_temperature": [
+ "75"
+ ],
+ "filament_dev_ams_drying_temperature": [
+ "65",
+ "80",
+ "65",
+ "75"
+ ],
+ "filament_dev_drying_softening_temperature": [
+ "80"
+ ],
"hot_plate_temp": [
"90"
],
diff --git a/resources/profiles/BBL/filament/fdm_filament_asa.json b/resources/profiles/BBL/filament/fdm_filament_asa.json
index de053b5e14..b5ae3ad8b5 100644
--- a/resources/profiles/BBL/filament/fdm_filament_asa.json
+++ b/resources/profiles/BBL/filament/fdm_filament_asa.json
@@ -43,6 +43,27 @@
"filament_adhesiveness_category": [
"200"
],
+ "filament_dev_ams_drying_time": [
+ "12",
+ "8.0",
+ "12",
+ "8.0"
+ ],
+ "filament_dev_ams_drying_heat_distortion_temperature": [
+ "100"
+ ],
+ "filament_dev_drying_cooling_temperature": [
+ "85"
+ ],
+ "filament_dev_ams_drying_temperature": [
+ "65",
+ "80",
+ "65",
+ "80"
+ ],
+ "filament_dev_drying_softening_temperature": [
+ "85"
+ ],
"hot_plate_temp": [
"90"
],
diff --git a/resources/profiles/BBL/filament/fdm_filament_bvoh.json b/resources/profiles/BBL/filament/fdm_filament_bvoh.json
index 1a766a5a71..ee15cc5e30 100644
--- a/resources/profiles/BBL/filament/fdm_filament_bvoh.json
+++ b/resources/profiles/BBL/filament/fdm_filament_bvoh.json
@@ -43,6 +43,34 @@
"filament_type": [
"BVOH"
],
+ "filament_dev_chamber_drying_bed_temperature": [
+ "80"
+ ],
+ "filament_dev_ams_drying_time": [
+ "12",
+ "12",
+ "12",
+ "12"
+ ],
+ "filament_dev_ams_drying_ams_limitations": [
+ "1",
+ "0"
+ ],
+ "filament_dev_ams_drying_heat_distortion_temperature": [
+ "65"
+ ],
+ "filament_dev_drying_cooling_temperature": [
+ "40"
+ ],
+ "filament_dev_ams_drying_temperature": [
+ "60",
+ "60",
+ "45",
+ "45"
+ ],
+ "filament_dev_drying_softening_temperature": [
+ "50"
+ ],
"hot_plate_temp": [
"55"
],
diff --git a/resources/profiles/BBL/filament/fdm_filament_eva.json b/resources/profiles/BBL/filament/fdm_filament_eva.json
index 283606c21e..a3f9ab0011 100644
--- a/resources/profiles/BBL/filament/fdm_filament_eva.json
+++ b/resources/profiles/BBL/filament/fdm_filament_eva.json
@@ -7,6 +7,34 @@
"filament_type": [
"EVA"
],
+ "filament_dev_chamber_drying_bed_temperature": [
+ "70"
+ ],
+ "filament_dev_ams_drying_time": [
+ "12",
+ "12",
+ "12",
+ "12"
+ ],
+ "filament_dev_ams_drying_ams_limitations": [
+ "1",
+ "0"
+ ],
+ "filament_dev_ams_drying_heat_distortion_temperature": [
+ "45"
+ ],
+ "filament_dev_drying_cooling_temperature": [
+ "40"
+ ],
+ "filament_dev_ams_drying_temperature": [
+ "45",
+ "45",
+ "45",
+ "45"
+ ],
+ "filament_dev_drying_softening_temperature": [
+ "50"
+ ],
"supertack_plate_temp": [
"0"
],
diff --git a/resources/profiles/BBL/filament/fdm_filament_hips.json b/resources/profiles/BBL/filament/fdm_filament_hips.json
index 25c0acbb6f..7016a866f6 100644
--- a/resources/profiles/BBL/filament/fdm_filament_hips.json
+++ b/resources/profiles/BBL/filament/fdm_filament_hips.json
@@ -4,9 +4,6 @@
"inherits": "fdm_filament_common",
"from": "system",
"instantiation": "false",
- "additional_cooling_fan_speed": [
- "0"
- ],
"cool_plate_temp": [
"0"
],
@@ -42,6 +39,27 @@
],
"filament_adhesiveness_category": [
"798"
+ ],
+ "filament_dev_ams_drying_time": [
+ "12",
+ "12",
+ "12",
+ "12"
+ ],
+ "filament_dev_ams_drying_heat_distortion_temperature": [
+ "90"
+ ],
+ "filament_dev_drying_cooling_temperature": [
+ "75"
+ ],
+ "filament_dev_ams_drying_temperature": [
+ "65",
+ "80",
+ "65",
+ "75"
+ ],
+ "filament_dev_drying_softening_temperature": [
+ "80"
],
"hot_plate_temp": [
"90"
diff --git a/resources/profiles/BBL/filament/fdm_filament_pa.json b/resources/profiles/BBL/filament/fdm_filament_pa.json
index 629ef70885..61aeed66ce 100644
--- a/resources/profiles/BBL/filament/fdm_filament_pa.json
+++ b/resources/profiles/BBL/filament/fdm_filament_pa.json
@@ -43,6 +43,30 @@
"filament_adhesiveness_category": [
"400"
],
+ "filament_dev_ams_drying_time": [
+ "12",
+ "12",
+ "12",
+ "12"
+ ],
+ "filament_dev_chamber_drying_time": [
+ "18"
+ ],
+ "filament_dev_ams_drying_heat_distortion_temperature": [
+ "165"
+ ],
+ "filament_dev_drying_cooling_temperature": [
+ "150"
+ ],
+ "filament_dev_ams_drying_temperature": [
+ "65",
+ "85",
+ "65",
+ "85"
+ ],
+ "filament_dev_drying_softening_temperature": [
+ "150"
+ ],
"hot_plate_temp": [
"100"
],
diff --git a/resources/profiles/BBL/filament/fdm_filament_pc.json b/resources/profiles/BBL/filament/fdm_filament_pc.json
index 11dcf5ea46..a73a56b440 100644
--- a/resources/profiles/BBL/filament/fdm_filament_pc.json
+++ b/resources/profiles/BBL/filament/fdm_filament_pc.json
@@ -40,6 +40,27 @@
"filament_adhesiveness_category": [
"500"
],
+ "filament_dev_ams_drying_time": [
+ "12",
+ "8.0",
+ "12",
+ "8.0"
+ ],
+ "filament_dev_ams_drying_heat_distortion_temperature": [
+ "105"
+ ],
+ "filament_dev_drying_cooling_temperature": [
+ "90"
+ ],
+ "filament_dev_ams_drying_temperature": [
+ "65",
+ "80",
+ "65",
+ "80"
+ ],
+ "filament_dev_drying_softening_temperature": [
+ "90"
+ ],
"hot_plate_temp": [
"110"
],
diff --git a/resources/profiles/BBL/filament/fdm_filament_pctg.json b/resources/profiles/BBL/filament/fdm_filament_pctg.json
index f735c26f48..01810a2ddc 100644
--- a/resources/profiles/BBL/filament/fdm_filament_pctg.json
+++ b/resources/profiles/BBL/filament/fdm_filament_pctg.json
@@ -28,6 +28,37 @@
"filament_type": [
"PCTG"
],
+ "filament_dev_chamber_drying_bed_temperature": [
+ "80"
+ ],
+ "filament_dev_ams_drying_time": [
+ "12",
+ "12",
+ "12",
+ "12"
+ ],
+ "filament_dev_ams_drying_ams_limitations": [
+ "1",
+ "0"
+ ],
+ "filament_dev_chamber_drying_time": [
+ "12"
+ ],
+ "filament_dev_ams_drying_heat_distortion_temperature": [
+ "75"
+ ],
+ "filament_dev_drying_cooling_temperature": [
+ "55"
+ ],
+ "filament_dev_ams_drying_temperature": [
+ "65",
+ "65",
+ "55",
+ "55"
+ ],
+ "filament_dev_drying_softening_temperature": [
+ "60"
+ ],
"hot_plate_temp": [
"80"
],
diff --git a/resources/profiles/BBL/filament/fdm_filament_pe.json b/resources/profiles/BBL/filament/fdm_filament_pe.json
index 51549b2d8d..9235c3301e 100644
--- a/resources/profiles/BBL/filament/fdm_filament_pe.json
+++ b/resources/profiles/BBL/filament/fdm_filament_pe.json
@@ -40,6 +40,31 @@
"filament_type": [
"PE"
],
+ "filament_dev_chamber_drying_bed_temperature": [
+ "70"
+ ],
+ "filament_dev_ams_drying_time": [
+ "12",
+ "12",
+ "12",
+ "12"
+ ],
+ "filament_dev_ams_drying_ams_limitations": [
+ "1",
+ "0"
+ ],
+ "filament_dev_drying_cooling_temperature": [
+ "50"
+ ],
+ "filament_dev_ams_drying_temperature": [
+ "45",
+ "45",
+ "45",
+ "45"
+ ],
+ "filament_dev_drying_softening_temperature": [
+ "50"
+ ],
"hot_plate_temp": [
"55"
],
diff --git a/resources/profiles/BBL/filament/fdm_filament_pet.json b/resources/profiles/BBL/filament/fdm_filament_pet.json
index 8fde5f2faa..23a8f73fe4 100644
--- a/resources/profiles/BBL/filament/fdm_filament_pet.json
+++ b/resources/profiles/BBL/filament/fdm_filament_pet.json
@@ -31,6 +31,34 @@
"filament_adhesiveness_category": [
"300"
],
+ "filament_dev_chamber_drying_bed_temperature": [
+ "80"
+ ],
+ "filament_dev_ams_drying_time": [
+ "12",
+ "12",
+ "12",
+ "12"
+ ],
+ "filament_dev_ams_drying_ams_limitations": [
+ "1",
+ "0"
+ ],
+ "filament_dev_ams_drying_heat_distortion_temperature": [
+ "75"
+ ],
+ "filament_dev_drying_cooling_temperature": [
+ "55"
+ ],
+ "filament_dev_ams_drying_temperature": [
+ "65",
+ "65",
+ "55",
+ "55"
+ ],
+ "filament_dev_drying_softening_temperature": [
+ "60"
+ ],
"hot_plate_temp": [
"80"
],
diff --git a/resources/profiles/BBL/filament/fdm_filament_pha.json b/resources/profiles/BBL/filament/fdm_filament_pha.json
index b9ac18587d..2bd348e2de 100644
--- a/resources/profiles/BBL/filament/fdm_filament_pha.json
+++ b/resources/profiles/BBL/filament/fdm_filament_pha.json
@@ -40,6 +40,31 @@
"filament_type": [
"PHA"
],
+ "filament_dev_chamber_drying_bed_temperature": [
+ "70"
+ ],
+ "filament_dev_ams_drying_time": [
+ "12",
+ "12",
+ "12",
+ "12"
+ ],
+ "filament_dev_ams_drying_ams_limitations": [
+ "1",
+ "0"
+ ],
+ "filament_dev_drying_cooling_temperature": [
+ "45"
+ ],
+ "filament_dev_ams_drying_temperature": [
+ "45",
+ "45",
+ "45",
+ "45"
+ ],
+ "filament_dev_drying_softening_temperature": [
+ "50"
+ ],
"hot_plate_temp": [
"55"
],
diff --git a/resources/profiles/BBL/filament/fdm_filament_pla.json b/resources/profiles/BBL/filament/fdm_filament_pla.json
index 3c6713b86d..f1e1c9a244 100644
--- a/resources/profiles/BBL/filament/fdm_filament_pla.json
+++ b/resources/profiles/BBL/filament/fdm_filament_pla.json
@@ -37,14 +37,39 @@
"filament_max_volumetric_speed": [
"12"
],
- "filament_scarf_seam_type": [
- "none"
- ],
"filament_scarf_gap": [
"15%"
],
"filament_adhesiveness_category": [
"100"
+ ],
+ "filament_dev_chamber_drying_bed_temperature": [
+ "70"
+ ],
+ "filament_dev_ams_drying_time": [
+ "12",
+ "12",
+ "12",
+ "12"
+ ],
+ "filament_dev_ams_drying_ams_limitations": [
+ "1",
+ "0"
+ ],
+ "filament_dev_ams_drying_heat_distortion_temperature": [
+ "45"
+ ],
+ "filament_dev_drying_cooling_temperature": [
+ "45"
+ ],
+ "filament_dev_ams_drying_temperature": [
+ "45",
+ "45",
+ "45",
+ "45"
+ ],
+ "filament_dev_drying_softening_temperature": [
+ "50"
],
"hot_plate_temp": [
"55"
diff --git a/resources/profiles/BBL/filament/fdm_filament_pp.json b/resources/profiles/BBL/filament/fdm_filament_pp.json
index dd390e0a82..2d8c3772ba 100644
--- a/resources/profiles/BBL/filament/fdm_filament_pp.json
+++ b/resources/profiles/BBL/filament/fdm_filament_pp.json
@@ -43,6 +43,34 @@
"filament_adhesiveness_category": [
"902"
],
+ "filament_dev_chamber_drying_bed_temperature": [
+ "70"
+ ],
+ "filament_dev_ams_drying_time": [
+ "12",
+ "12",
+ "12",
+ "12"
+ ],
+ "filament_dev_ams_drying_ams_limitations": [
+ "1",
+ "0"
+ ],
+ "filament_dev_ams_drying_heat_distortion_temperature": [
+ "60"
+ ],
+ "filament_dev_drying_cooling_temperature": [
+ "50"
+ ],
+ "filament_dev_ams_drying_temperature": [
+ "60",
+ "60",
+ "50",
+ "50"
+ ],
+ "filament_dev_drying_softening_temperature": [
+ "55"
+ ],
"hot_plate_temp": [
"55"
],
diff --git a/resources/profiles/BBL/filament/fdm_filament_ppa.json b/resources/profiles/BBL/filament/fdm_filament_ppa.json
index bb06a73c76..3bba318bc5 100644
--- a/resources/profiles/BBL/filament/fdm_filament_ppa.json
+++ b/resources/profiles/BBL/filament/fdm_filament_ppa.json
@@ -46,6 +46,34 @@
"filament_vendor": [
"Bambu Lab"
],
+ "filament_dev_chamber_drying_bed_temperature": [
+ "100"
+ ],
+ "filament_dev_ams_drying_time": [
+ "12",
+ "12",
+ "12",
+ "12"
+ ],
+ "filament_dev_ams_drying_ams_limitations": [],
+ "filament_dev_chamber_drying_time": [
+ "18"
+ ],
+ "filament_dev_ams_drying_heat_distortion_temperature": [
+ "165"
+ ],
+ "filament_dev_drying_cooling_temperature": [
+ "150"
+ ],
+ "filament_dev_ams_drying_temperature": [
+ "65",
+ "85",
+ "65",
+ "85"
+ ],
+ "filament_dev_drying_softening_temperature": [
+ "150"
+ ],
"hot_plate_temp": [
"100"
],
diff --git a/resources/profiles/BBL/filament/fdm_filament_pps.json b/resources/profiles/BBL/filament/fdm_filament_pps.json
index a19a5aca2b..d1c6a6aed2 100644
--- a/resources/profiles/BBL/filament/fdm_filament_pps.json
+++ b/resources/profiles/BBL/filament/fdm_filament_pps.json
@@ -43,6 +43,31 @@
"filament_adhesiveness_category": [
"801"
],
+ "filament_dev_chamber_drying_bed_temperature": [
+ "100"
+ ],
+ "filament_dev_ams_drying_time": [
+ "12",
+ "12",
+ "12",
+ "12"
+ ],
+ "filament_dev_ams_drying_ams_limitations": [],
+ "filament_dev_ams_drying_heat_distortion_temperature": [
+ "165"
+ ],
+ "filament_dev_drying_cooling_temperature": [
+ "150"
+ ],
+ "filament_dev_ams_drying_temperature": [
+ "65",
+ "85",
+ "65",
+ "85"
+ ],
+ "filament_dev_drying_softening_temperature": [
+ "150"
+ ],
"hot_plate_temp": [
"110"
],
diff --git a/resources/profiles/BBL/filament/fdm_filament_pva.json b/resources/profiles/BBL/filament/fdm_filament_pva.json
index 98ca7e63b9..55d83517ce 100644
--- a/resources/profiles/BBL/filament/fdm_filament_pva.json
+++ b/resources/profiles/BBL/filament/fdm_filament_pva.json
@@ -49,6 +49,27 @@
"filament_adhesiveness_category": [
"704"
],
+ "filament_dev_ams_drying_time": [
+ "12",
+ "18",
+ "12",
+ "18"
+ ],
+ "filament_dev_ams_drying_heat_distortion_temperature": [
+ "75"
+ ],
+ "filament_dev_drying_cooling_temperature": [
+ "40"
+ ],
+ "filament_dev_ams_drying_temperature": [
+ "65",
+ "85",
+ "65",
+ "70"
+ ],
+ "filament_dev_drying_softening_temperature": [
+ "75"
+ ],
"hot_plate_temp": [
"55"
],
diff --git a/resources/profiles/BBL/filament/fdm_filament_tpu.json b/resources/profiles/BBL/filament/fdm_filament_tpu.json
index f6ab1d3edf..d2ef5db84e 100644
--- a/resources/profiles/BBL/filament/fdm_filament_tpu.json
+++ b/resources/profiles/BBL/filament/fdm_filament_tpu.json
@@ -46,6 +46,30 @@
"filament_adhesiveness_category": [
"600"
],
+ "filament_dev_ams_drying_time": [
+ "12",
+ "18",
+ "12",
+ "18"
+ ],
+ "filament_dev_chamber_drying_time": [
+ "16"
+ ],
+ "filament_dev_ams_drying_heat_distortion_temperature": [
+ "45"
+ ],
+ "filament_dev_drying_cooling_temperature": [
+ "40"
+ ],
+ "filament_dev_ams_drying_temperature": [
+ "65",
+ "75",
+ "45",
+ "45"
+ ],
+ "filament_dev_drying_softening_temperature": [
+ "50"
+ ],
"hot_plate_temp": [
"35"
],
diff --git a/resources/profiles/BBL/machine/Bambu Lab X2D 0.4 nozzle.json b/resources/profiles/BBL/machine/Bambu Lab X2D 0.4 nozzle.json
index ed731be291..0c79b7fed0 100644
--- a/resources/profiles/BBL/machine/Bambu Lab X2D 0.4 nozzle.json
+++ b/resources/profiles/BBL/machine/Bambu Lab X2D 0.4 nozzle.json
@@ -5,10 +5,10 @@
"from": "system",
"setting_id": "GM045",
"instantiation": "true",
- "change_filament_gcode": "======== X2D filament_change gcode ==========\n;===== 2026/05/15 =====\n\nM620 S[next_filament_id]A B H[next_hotend]\n;M204 S9000\n{if toolchange_count > 1 && (z_hop_types[current_filament_id] == 0 || z_hop_types[current_filament_id] == 3)}\nG17\nG2 Z{z_after_toolchange + 0.4} I0.86 J0.86 P1 F10000 ; spiral lift a little from second lift\n{endif}\n\n;nozzle_change_gcode\n\nG1 Z{max_layer_z + 3.0} F1200\n\nM400\nM106 P1 S0\n\n{if toolchange_count == 2}\n; get travel path for change filament\n;M620.1 X[travel_point_1_x] Y[travel_point_1_y] F21000 P0\n;M620.1 X[travel_point_2_x] Y[travel_point_2_y] F21000 P1\n;M620.1 X[travel_point_3_x] Y[travel_point_3_y] F21000 P2\n{endif}\n\n{if ((filament_type[current_filament_id] == \"PLA\") || (filament_type[current_filament_id] == \"PLA-CF\") || (filament_type[current_filament_id] == \"PETG\")) && (nozzle_diameter_at_nozzle_id[current_nozzle_id] == 0.2)}\nM620.10 A0 F74.8347 L[flush_length] H{nozzle_diameter_at_nozzle_id[current_nozzle_id]} T{flush_temperatures[current_filament_id]} P[old_filament_temp] S1\n{else}\nM620.10 A0 F{flush_volumetric_speeds[current_filament_id]/2.4053*60} L[flush_length] H{nozzle_diameter_at_nozzle_id[current_nozzle_id]} T{flush_temperatures[current_filament_id]} P[old_filament_temp] S1\n{endif}\n\n{if ((filament_type[next_filament_id] == \"PLA\") || (filament_type[next_filament_id] == \"PLA-CF\") || (filament_type[next_filament_id] == \"PETG\")) && (nozzle_diameter_at_nozzle_id[next_nozzle_id] == 0.2)}\nM620.10 A1 F74.8347 L[flush_length] H{nozzle_diameter_at_nozzle_id[next_nozzle_id]} T{flush_temperatures[next_filament_id]} P[new_filament_temp] S1\n{else}\nM620.10 A1 F{flush_volumetric_speeds[next_filament_id]/2.4053*60} L[flush_length] H{nozzle_diameter_at_nozzle_id[next_nozzle_id]} T{flush_temperatures[next_filament_id]} P[new_filament_temp] S1\n{endif}\n\nM620.15 C{new_filament_temp - filament_cooling_before_tower[next_filament_id]}\n\n{if long_retraction_when_cut}\nM620.11 P1 L0 I[current_filament_id] B[current_hotend] E-{retraction_distance_when_cut} F{max((flush_volumetric_speeds[current_filament_id]/2.4053*60), 200)}\n{else}\nM620.11 P0 L0 I[current_filament_id] B[current_hotend] E0\n{endif}\n\n{if long_retraction_when_ec}\nM620.11 K1 I[current_filament_id] B[current_hotend] R{retraction_distance_when_ec} F{max((flush_volumetric_speeds[current_filament_id]/2.4053*60), 200)}\n{else}\nM620.11 K0 I[current_filament_id] B[current_hotend] R0\n{endif}\n\nM620.22 I[next_filament_id] P1 ; enable remote extruder runout auto purge.\n\nT[next_filament_id] H[next_hotend]\n\n;deretract\n{if filament_type[next_filament_id] == \"TPU\"}\n{else}\n{if filament_type[next_filament_id] == \"PA\"}\n;VG1 E1 F{max(new_filament_e_feedrate, 200)}\n;VG1 E1 F{max(new_filament_e_feedrate/2, 100)}\n{else}\n;VG1 E4 F{max(new_filament_e_feedrate, 200)}\n;VG1 E4 F{max(new_filament_e_feedrate/2, 100)}\n{endif}\n{endif}\n\n; VFLUSH_START\n{if flush_length>41.5}\n;VG1 E41.5 F{min(old_filament_e_feedrate,new_filament_e_feedrate)}\n;VG1 E{flush_length-41.5} F{new_filament_e_feedrate}\n{else}\n;VG1 E{flush_length} F{min(old_filament_e_feedrate,new_filament_e_feedrate)}\n{endif}\nSYNC T{ceil(flush_length / 125) * 5}\n; VFLUSH_END\n\nM1002 set_filament_type:{filament_type[next_filament_id]}\n\nM400\nM83\n{if next_filament_id < 255}\nM620.10 R{retract_length_toolchange[filament_map[next_filament_id]-1]}\nM628 S0\n;VM109 S[new_filament_temp]\nM629\nM400\n\n;prime_tower_interface\n{if is_prime_tower_interface && filament_tower_interface_purge_volume !=0}\nG150.1\nM620.13 W0 L{filament_tower_interface_purge_volume} T{filament_tower_interface_print_temp} R0.0\n{endif}\n;prime_tower_interface\n\nM983.3 F{filament_max_volumetric_speed[next_filament_id]/2.4} A0.4 R{retract_length_toolchange[filament_map[next_filament_id]-1]}\n\nM400\n\nG1 Z{max_layer_z + 3.0} F3000\n\n{else}\nG1 X[x_after_toolchange] Y[y_after_toolchange] Z[z_after_toolchange] F12000\n{endif}\n\n\nM621 S[next_filament_id]A B\n\nM622.1 S0 ;for prev version, default skip\nM1002 judge_flag powerloss_resume_flag\nM622 J1\nM983.3 F{filament_max_volumetric_speed[next_filament_id]/2.4} A0.4 R{retract_length_toolchange[filament_map[next_filament_id]-1]}\nM400\nG1 Z{max_layer_z + 3.0} F3000\nM1002 set_flag powerloss_resume_flag=0\nM623\n\nM620.6 I[next_filament_id] H[next_hotend] W1 ;enable ams air printing detect\n\n{if (filament_type[next_filament_id] == \"TPU\")}\nM1015.3 S1 H{nozzle_diameter_at_nozzle_id[next_nozzle_id]};enable tpu clog detect\n{else}\nM1015.3 S0;disable tpu clog detect\n{endif}\n\n{if (filament_type[next_filament_id] == \"PLA\") || (filament_type[next_filament_id] == \"PETG\")\n || (filament_type[next_filament_id] == \"PLA-CF\") || (filament_type[next_filament_id] == \"PETG-CF\")}\nM1015.4 S1 K1 H{nozzle_diameter_at_nozzle_id[next_nozzle_id]} ;enable E air printing detect\n{else}\nM1015.4 S0 K0 H{nozzle_diameter_at_nozzle_id[next_nozzle_id]} ;disable E air printing detect\n{endif}\n\n{if layer_z <= (initial_layer_print_height + 0.001)}\nM204 S[initial_layer_acceleration]\n{else}\nM204 S[travel_acceleration]\n{endif}\n\nG1 Y256 F18000\n\n\n{if (overall_chamber_temperature < 40)}\n{if (layer_num + 1 <= close_additional_fan_first_x_layers[next_filament_id])}\n M106 P2 S{first_x_layer_fan_speed[next_filament_id]*255.0/100.0 };set first x_layer fan\n\tM106 P10 S{first_x_layer_fan_speed[next_filament_id]*255.0/100.0 };set first x_layer fan\n{elsif (layer_num + 1 < additional_fan_full_speed_layer[next_filament_id] && additional_fan_full_speed_layer[next_filament_id] > close_additional_fan_first_x_layers[next_filament_id])}\n M106 P2 S{(first_x_layer_fan_speed[next_filament_id] + (additional_cooling_fan_speed[next_filament_id] - first_x_layer_fan_speed[next_filament_id]) * (layer_num + 1 - close_additional_fan_first_x_layers[next_filament_id]) / max(additional_fan_full_speed_layer[next_filament_id] - close_additional_fan_first_x_layers[next_filament_id], 1)) * 255.0/100.0}\n\tM106 P10 S{(first_x_layer_fan_speed[next_filament_id] + (additional_cooling_fan_speed[next_filament_id] - first_x_layer_fan_speed[next_filament_id]) * (layer_num + 1 - close_additional_fan_first_x_layers[next_filament_id]) / max(additional_fan_full_speed_layer[next_filament_id] - close_additional_fan_first_x_layers[next_filament_id], 1)) * 255.0/100.0}\n{else}\n {if (min_vitrification_temperature <= 50)}\n {if (nozzle_diameter_at_nozzle_id[current_nozzle_id] == 0.2)}\n M142 P1 R30 S35 U{max_additional_fan/100.0} V1.0 O40; set PLA/TPU ND0.2 chamber autocooling\n {else}\n M142 P1 R30 S40 U{max_additional_fan/100.0} V1.0 O45; set PLA/TPU ND0.4 chamber autocooling\n {endif}\n {else}\n {if (nozzle_diameter_at_nozzle_id[current_nozzle_id] == 0.2)}\n M142 P1 R35 S45 U{max_additional_fan/100.0} V0.5 O50; set PETG ND0.2 chamber autocooling\n {else}\n M142 P1 R35 S50 U{max_additional_fan/100.0} V0.5 O55; set PETG ND0.4 chamber autocooling\n {endif}\n {endif}\n{endif}\n{endif}\n;not set fan changing filament",
- "layer_change_gcode": ";======== X2D layer_change gcode ==========\n;===== 2026/05/15 =====\n\n{if (layer_num + 1 == 1)}\n{if (overall_chamber_temperature >= 40)}\n ;not reset filter fan in first layer\n ;not reset fan\n{endif}\n{endif}\n\n{if (layer_num + 1 <= close_additional_fan_first_x_layers[current_filament_id])}\n{if (overall_chamber_temperature < 40)}\n M106 P2 S{first_x_layer_fan_speed[current_filament_id]*255.0/100.0}\n\tM106 P10 S{first_x_layer_fan_speed[current_filament_id]*255.0/100.0}\n{endif}\n;not reset fan\n{elsif (layer_num + 1 < additional_fan_full_speed_layer[current_filament_id] && additional_fan_full_speed_layer[current_filament_id] > close_additional_fan_first_x_layers[current_filament_id])}\n{if (overall_chamber_temperature < 40)}\n M106 P2 S{(first_x_layer_fan_speed[current_filament_id] + (additional_cooling_fan_speed[current_filament_id] - first_x_layer_fan_speed[current_filament_id]) * (layer_num + 1 - close_additional_fan_first_x_layers[current_filament_id]) / max(additional_fan_full_speed_layer[current_filament_id] - close_additional_fan_first_x_layers[current_filament_id], 1)) * 255.0/100.0}\n\tM106 P10 S{(first_x_layer_fan_speed[current_filament_id] + (additional_cooling_fan_speed[current_filament_id] - first_x_layer_fan_speed[current_filament_id]) * (layer_num + 1 - close_additional_fan_first_x_layers[current_filament_id]) / max(additional_fan_full_speed_layer[current_filament_id] - close_additional_fan_first_x_layers[current_filament_id], 1)) * 255.0/100.0}\n{endif}\n;not reset fan\n{elsif (layer_num + 1 == max(close_additional_fan_first_x_layers[current_filament_id] + 1, additional_fan_full_speed_layer[current_filament_id]))}\n{if (overall_chamber_temperature < 40)}\n ;updata chamber autocooling in Xth layer\n {if (min_vitrification_temperature <= 50)}\n {if (nozzle_diameter_at_nozzle_id[current_nozzle_id] == 0.2)}\n M142 P1 R30 S35 U{max_additional_fan/100.0} V1.0 O40; set PLA/TPU ND0.2 chamber autocooling\n {else}\n M142 P1 R30 S40 U{max_additional_fan/100.0} V1.0 O45; set PLA/TPU ND0.4 chamber autocooling\n {endif}\n {else}\n {if (nozzle_diameter_at_nozzle_id[current_nozzle_id] == 0.2)}\n M142 P1 R35 S45 U{max_additional_fan/100.0} V0.5 O50; set PETG ND0.2 chamber autocooling\n {else}\n M142 P1 R35 S50 U{max_additional_fan/100.0} V0.5 O55; set PETG ND0.4 chamber autocooling\n {endif}\n {endif}\n{else}\n ;not reset filter fan in Xth layer\n{endif}\n;not reset fan\n{endif}\n\n\n; update layer progress\nM73 L{layer_num+1}\nM991 S0 P{layer_num} ;notify layer change\n",
+ "change_filament_gcode": "======== X2D filament_change gcode ==========\n;===== 2026/07/01 =====\n\nM620 S[next_filament_id]A B H[next_hotend]\n;M204 S9000\n{if toolchange_count > 1 && (z_hop_types[current_filament_id] == 0 || z_hop_types[current_filament_id] == 3)}\nG17\nG2 Z{z_after_toolchange + 0.4} I0.86 J0.86 P1 F10000 ; spiral lift a little from second lift\n{endif}\n\n;nozzle_change_gcode\n\nG1 Z{max_layer_z + 3.0} F1200\n\nM400\nM106 P1 S0\n\n{if toolchange_count == 2}\n; get travel path for change filament\n;M620.1 X[travel_point_1_x] Y[travel_point_1_y] F21000 P0\n;M620.1 X[travel_point_2_x] Y[travel_point_2_y] F21000 P1\n;M620.1 X[travel_point_3_x] Y[travel_point_3_y] F21000 P2\n{endif}\n\n{if ((filament_type[current_filament_id] == \"PLA\") || (filament_type[current_filament_id] == \"PLA-CF\") || (filament_type[current_filament_id] == \"PETG\")) && (nozzle_diameter_at_nozzle_id[current_nozzle_id] == 0.2)}\nM620.10 A0 F74.8347 L[flush_length] H{nozzle_diameter_at_nozzle_id[current_nozzle_id]} T{flush_temperatures[current_filament_id]} P[old_filament_temp] S1\n{else}\nM620.10 A0 F{flush_volumetric_speeds[current_filament_id]/2.4053*60} L[flush_length] H{nozzle_diameter_at_nozzle_id[current_nozzle_id]} T{flush_temperatures[current_filament_id]} P[old_filament_temp] S1\n{endif}\n\n{if ((filament_type[next_filament_id] == \"PLA\") || (filament_type[next_filament_id] == \"PLA-CF\") || (filament_type[next_filament_id] == \"PETG\")) && (nozzle_diameter_at_nozzle_id[next_nozzle_id] == 0.2)}\nM620.10 A1 F74.8347 L[flush_length] H{nozzle_diameter_at_nozzle_id[next_nozzle_id]} T{flush_temperatures[next_filament_id]} P[new_filament_temp] S1\n{else}\nM620.10 A1 F{flush_volumetric_speeds[next_filament_id]/2.4053*60} L[flush_length] H{nozzle_diameter_at_nozzle_id[next_nozzle_id]} T{flush_temperatures[next_filament_id]} P[new_filament_temp] S1\n{endif}\n\nM620.15 C{new_filament_temp - filament_cooling_before_tower[next_filament_id]}\n\n{if long_retraction_when_cut}\nM620.11 P1 L0 I[current_filament_id] B[current_hotend] E-{retraction_distance_when_cut} F{max((flush_volumetric_speeds[current_filament_id]/2.4053*60), 200)}\n{else}\nM620.11 P0 L0 I[current_filament_id] B[current_hotend] E0\n{endif}\n\n{if long_retraction_when_ec}\nM620.11 K1 I[current_filament_id] B[current_hotend] R{retraction_distance_when_ec} F{max((flush_volumetric_speeds[current_filament_id]/2.4053*60), 200)}\n{else}\nM620.11 K0 I[current_filament_id] B[current_hotend] R0\n{endif}\n\nM620.22 I[next_filament_id] P1 ; enable remote extruder runout auto purge.\n\nT[next_filament_id] H[next_hotend]\n\n;deretract\n{if filament_type[next_filament_id] == \"TPU\"}\n{else}\n{if filament_type[next_filament_id] == \"PA\"}\n;VG1 E1 F{max(new_filament_e_feedrate, 200)}\n;VG1 E1 F{max(new_filament_e_feedrate/2, 100)}\n{else}\n;VG1 E4 F{max(new_filament_e_feedrate, 200)}\n;VG1 E4 F{max(new_filament_e_feedrate/2, 100)}\n{endif}\n{endif}\n\n; VFLUSH_START\n{if flush_length>41.5}\n;VG1 E41.5 F{min(old_filament_e_feedrate,new_filament_e_feedrate)}\n;VG1 E{flush_length-41.5} F{new_filament_e_feedrate}\n{else}\n;VG1 E{flush_length} F{min(old_filament_e_feedrate,new_filament_e_feedrate)}\n{endif}\nSYNC T{ceil(flush_length / 125) * 5}\n; VFLUSH_END\n\nM1002 set_filament_type:{filament_type[next_filament_id]}\n\nM400\nM83\n{if next_filament_id < 255}\nM620.10 R{retract_length_toolchange[filament_map[next_filament_id]-1]}\nM628 S0\n;VM109 S[new_filament_temp]\nM629\nM400\n\n;prime_tower_interface\n{if is_prime_tower_interface && filament_tower_interface_purge_volume !=0}\nG150.1\nM620.13 W0 L{filament_tower_interface_purge_volume} T{filament_tower_interface_print_temp} R0.0\n{endif}\n;prime_tower_interface\n\nM983.3 F{filament_max_volumetric_speed[next_filament_id]/2.4} A0.4 R{retract_length_toolchange[filament_map[next_filament_id]-1]}\n\nM400\n\nG1 Z{max_layer_z + 3.0} F3000\n\n{else}\nG1 X[x_after_toolchange] Y[y_after_toolchange] Z[z_after_toolchange] F12000\n{endif}\n\n\nM621 S[next_filament_id]A B\n\nM622.1 S0 ;for prev version, default skip\nM1002 judge_flag powerloss_resume_flag\nM622 J1\nM983.3 F{filament_max_volumetric_speed[next_filament_id]/2.4} A0.4 R{retract_length_toolchange[filament_map[next_filament_id]-1]}\nM400\nG1 Z{max_layer_z + 3.0} F3000\nM1002 set_flag powerloss_resume_flag=0\nM623\n\nM620.6 I[next_filament_id] H[next_hotend] W1 ;enable ams air printing detect\n\n{if (filament_type[next_filament_id] == \"TPU\")}\nM1015.3 S1 H{nozzle_diameter_at_nozzle_id[next_nozzle_id]};enable tpu clog detect\n{else}\nM1015.3 S0;disable tpu clog detect\n{endif}\n\n{if (filament_type[next_filament_id] == \"PLA\") || (filament_type[next_filament_id] == \"PETG\")\n || (filament_type[next_filament_id] == \"PLA-CF\") || (filament_type[next_filament_id] == \"PETG-CF\")}\nM1015.4 S1 K1 H{nozzle_diameter_at_nozzle_id[next_nozzle_id]} ;enable E air printing detect\n{else}\nM1015.4 S0 K0 H{nozzle_diameter_at_nozzle_id[next_nozzle_id]} ;disable E air printing detect\n{endif}\n\n{if layer_z <= (initial_layer_print_height + 0.001)}\nM204 S[initial_layer_acceleration]\n{else}\nM204 S[travel_acceleration]\n{endif}\n\nG1 Y256 F18000\n\n\n{if (overall_chamber_temperature < 40)}\n{if (layer_num + 1 <= close_additional_fan_first_x_layers[next_filament_id])}\n M106 P2 S{first_x_layer_fan_speed[next_filament_id]*255.0/100.0 };set first x_layer fan\n\tM106 P10 S{first_x_layer_fan_speed[next_filament_id]*255.0/100.0 };set first x_layer fan\n{elsif (layer_num + 1 < additional_fan_full_speed_layer[next_filament_id] && additional_fan_full_speed_layer[next_filament_id] > close_additional_fan_first_x_layers[next_filament_id])}\n M106 P2 S{(first_x_layer_fan_speed[next_filament_id] + (additional_cooling_fan_speed[next_filament_id] - first_x_layer_fan_speed[next_filament_id]) * (layer_num + 1 - close_additional_fan_first_x_layers[next_filament_id]) / max(additional_fan_full_speed_layer[next_filament_id] - close_additional_fan_first_x_layers[next_filament_id], 1)) * 255.0/100.0}\n\tM106 P10 S{(first_x_layer_fan_speed[next_filament_id] + (additional_cooling_fan_speed[next_filament_id] - first_x_layer_fan_speed[next_filament_id]) * (layer_num + 1 - close_additional_fan_first_x_layers[next_filament_id]) / max(additional_fan_full_speed_layer[next_filament_id] - close_additional_fan_first_x_layers[next_filament_id], 1)) * 255.0/100.0}\n{else}\n {if (min_vitrification_temperature <= 50)}\n {if (nozzle_diameter_at_nozzle_id[current_nozzle_id] == 0.2)}\n M142 P1 R30 S40 U{max_additional_fan/100.0} V1.0 O45; set PLA/TPU ND0.2 chamber autocooling\n {else}\n M142 P1 R30 S40 U{max_additional_fan/100.0} V1.0 O45; set PLA/TPU ND0.4 chamber autocooling\n {endif}\n {else}\n {if (!is_all_bbl_filament)}\n M142 P1 R40 S45 U{max_additional_fan/100.0} V0.5 O50; set third-party PETG chamber \n {else}\n {if (nozzle_diameter_at_nozzle_id[current_nozzle_id] == 0.2)}\n M142 P1 R45 S50 U{max_additional_fan/100.0} V0.5 O50; set PETG ND0.2 chamber autocooling\n {else}\n M142 P1 R50 S55 U{max_additional_fan/100.0} V0.5 O55; set PETG ND0.4 chamber autocooling\n {endif}\n {endif}\n {endif}\n{endif}\n{endif}\n;not set fan changing filament",
+ "layer_change_gcode": ";======== X2D layer_change gcode ==========\n;===== 2026/07/01 =====\n\n{if (layer_num + 1 == 1)}\n{if (overall_chamber_temperature >= 40)}\n ;not reset filter fan in first layer\n ;not reset fan\n{endif}\n{endif}\n\n{if (layer_num + 1 <= close_additional_fan_first_x_layers[current_filament_id])}\n{if (overall_chamber_temperature < 40)}\n M106 P2 S{first_x_layer_fan_speed[current_filament_id]*255.0/100.0}\n\tM106 P10 S{first_x_layer_fan_speed[current_filament_id]*255.0/100.0}\n{endif}\n;not reset fan\n{elsif (layer_num + 1 < additional_fan_full_speed_layer[current_filament_id] && additional_fan_full_speed_layer[current_filament_id] > close_additional_fan_first_x_layers[current_filament_id])}\n{if (overall_chamber_temperature < 40)}\n M106 P2 S{(first_x_layer_fan_speed[current_filament_id] + (additional_cooling_fan_speed[current_filament_id] - first_x_layer_fan_speed[current_filament_id]) * (layer_num + 1 - close_additional_fan_first_x_layers[current_filament_id]) / max(additional_fan_full_speed_layer[current_filament_id] - close_additional_fan_first_x_layers[current_filament_id], 1)) * 255.0/100.0}\n\tM106 P10 S{(first_x_layer_fan_speed[current_filament_id] + (additional_cooling_fan_speed[current_filament_id] - first_x_layer_fan_speed[current_filament_id]) * (layer_num + 1 - close_additional_fan_first_x_layers[current_filament_id]) / max(additional_fan_full_speed_layer[current_filament_id] - close_additional_fan_first_x_layers[current_filament_id], 1)) * 255.0/100.0}\n{endif}\n;not reset fan\n{elsif (layer_num + 1 == max(close_additional_fan_first_x_layers[current_filament_id] + 1, additional_fan_full_speed_layer[current_filament_id]))}\n{if (overall_chamber_temperature < 40)}\n ;updata chamber autocooling in Xth layer\n {if (min_vitrification_temperature <= 50)}\n {if (nozzle_diameter_at_nozzle_id[current_nozzle_id] == 0.2)}\n M142 P1 R30 S40 U{max_additional_fan/100.0} V1.0 O45; set PLA/TPU ND0.2 chamber autocooling\n {else}\n M142 P1 R30 S40 U{max_additional_fan/100.0} V1.0 O45; set PLA/TPU ND0.4 chamber autocooling\n {endif}\n {else}\n {if (!is_all_bbl_filament)}\n M142 P1 R40 S45 U{max_additional_fan/100.0} V0.5 O50; set third-party PETG chamber \n {else}\n {if (nozzle_diameter_at_nozzle_id[current_nozzle_id] == 0.2)}\n M142 P1 R45 S50 U{max_additional_fan/100.0} V0.5 O50; set PETG ND0.2 chamber autocooling\n {else}\n M142 P1 R50 S55 U{max_additional_fan/100.0} V0.5 O55; set PETG ND0.4 chamber autocooling\n {endif}\n {endif}\n {endif}\n{else}\n ;not reset filter fan in Xth layer\n{endif}\n;not reset fan\n{endif}\n\n\n; update layer progress\nM73 L{layer_num+1}\nM991 S0 P{layer_num} ;notify layer change\n",
"machine_end_gcode": ";======== X2D end gcode ==========\n;===== 2026/05/18 =====\n\nM400 ; wait for buffer to clear\nG92 E0 ; zero the extruder\nM211 Z1\n\nG90\nG1 Z{max_layer_z + 0.4} F900 ; lower z a little\nM1002 judge_flag timelapse_record_flag\nM622 J1\n G150.3\n M400 ; wait all motion done\n M991 S0 P-1 ;end smooth timelapse at safe pos\n M400 S5 ;wait for last picture to be taken\nM623 ;end of \"timelapse_record_flag\"\n\nG90\nG1 Z{max_layer_z + 10} F900 ; lower z a little\n\nM140 S0 ; turn off bed\nM141 S0 ; turn off chamber heating\nM106 S0 ; turn off fan\nM106 P2 S0 ; turn off remote part cooling fan\nM106 P3 S0 ; turn off chamber cooling fan\nM106 P10 S0 ; turn off remote part1 cooling fan\n\n; pull back filament to AMS\nM620 S65279 B\n; M620.11 P1 L0 I65279 E-3\nT65279\nG150.1 F8000\nM621 S65279 B\n\nM620 S65535 B\n; M620.11 P1 L0 I65535 E-4\nT65535\nG150.1 F8000\nM621 S65535 B\n\nG150.3\n\nM104 S0 T0; turn off hotend\nM104 S0 T1; turn off hotend\n\nM400 ; wait all motion done\nM17 S\nM17 Z0.4 ; lower z motor current to reduce impact if there is something in the bottom\n{if (80.0 - max_layer_z/2) > 0}\n {if (max_layer_z + 80.0 - max_layer_z/2) < 256}\n G1 Z{max_layer_z + 80.0 - max_layer_z/2} F600\n G1 Z{max_layer_z + 78.0 - max_layer_z/2}\n {else}\n G1 Z256 F600\n G1 Z256\n {endif}\n{else}\n {if (max_layer_z + 4.0) < 256}\n G1 Z{max_layer_z + 4.0} F600\n G1 Z{max_layer_z + 2.0}\n {else}\n G1 Z256 F600\n G1 Z256\n {endif}\n{endif}\nM400 P100\nM17 R ; restore z current\n\nM220 S100 ; Reset feedrate magnitude\nM201.2 K1.0 ; Reset acc magnitude\nM73.2 R1.0 ;Reset left time magnitude\nM1002 set_gcode_claim_speed_level : 0\n\nM1015.3 S0 ;disable clog detect\nM1015.4 S0 K0 ;disable air printing detect\n\n;=====printer finish air purification=========\nM622.1 S0\nM1002 judge_flag print_finish_air_filt_flag\n\nM622 J1\nM1002 gcode_claim_action : 66\nM145 P1\nM106 P10 S255\nM400 S180\nM106 P10 S0\nM623\n\nM622 J2\nM1002 gcode_claim_action : 66\nM145 P0\nM106 P3 S255\nM400 S180\nM106 P3 S0\nM623\n;=====printer finish air purification=========\n\n;=====printer finish sound=========\nM17\nM400 S1\nM1006 S1\nM1006 A53 B10 L50 C53 D10 M50 E53 F10 N50 \nM1006 A57 B10 L50 C57 D10 M50 E57 F10 N50 \nM1006 A0 B15 L0 C0 D15 M0 E0 F15 N0 \nM1006 A53 B10 L50 C53 D10 M50 E53 F10 N50 \nM1006 A57 B10 L50 C57 D10 M50 E57 F10 N50 \nM1006 A0 B15 L0 C0 D15 M0 E0 F15 N0 \nM1006 A48 B10 L50 C48 D10 M50 E48 F10 N50 \nM1006 A0 B15 L0 C0 D15 M0 E0 F15 N0 \nM1006 A60 B10 L50 C60 D10 M50 E60 F10 N50 \nM1006 W\n;=====printer finish sound=========\nM400\nM18\n\n",
- "machine_start_gcode": ";M1002 set_flag extrude_cali_flag=1\n;M1002 set_flag g29_before_print_flag=1\n;M1002 set_flag auto_cali_toolhead_offset_flag=1\n;M1002 set_flag build_plate_detect_flag=1\n\n;======== X2D start gcode==========\n;===== 2026/05/18 =====\n\n M140 S[bed_temperature_initial_layer_single] ; heat heatbed first\n M993 A0 B0 C0 ; nozzle cam detection not allowed.\n M400\n ;M73 P99\n\n;=====printer start sound ===================\nM17\nM400 S1\nM1006 S1\nM1006 A53 B9 L50 C53 D9 M50 E53 F9 N50\nM1006 A56 B9 L50 C56 D9 M50 E56 F9 N50\nM1006 A61 B9 L50 C61 D9 M50 E61 F9 N50\nM1006 A53 B9 L50 C53 D9 M50 E53 F9 N50\nM1006 A56 B9 L50 C56 D9 M50 E56 F9 N50\nM1006 A61 B18 L50 C61 D18 M50 E61 F18 N50\nM1006 W\n;=====printer start sound ===================\n\n M1012.1 T1100\n M620 M ;enable remap\n M622.1 S0\n G383.4\n\n;===== avoid end stop =================\n G91\n G380 S2 Z22 F1200\n G380 S2 Z-12 F1200\n G90\n;===== avoid end stop =================\n\n;===== reset machine status =================\n M204 S10000\n M630 S0 P1\n G90\n M17 D ; reset motor current to default\n M960 S5 P1 ; turn on logo lamp\n M220 S100 ;Reset Feedrate\n M1002 set_gcode_claim_speed_level: 5\n M221 S100 ;Reset Flowrate\n M73.2 R1.0 ;Reset left time magnitude\n G29.1 Z{+0.0} ; clear z-trim value first\n M983.1 M1\n M982.2 S1 ; turn on cog noise reduction\n;===== reset machine status =================\n\n;==== set airduct mode ====\n{if (overall_chamber_temperature >= 40)}\nM145 P1 ; set airduct mode to heating mode for heating\nM106 P2 S0 ; turn off auxiliary fan\nM106 P10 S255 ; turn on filter fan\n{else}\nM145 P0 ; set airduct mode to cooling mode for cooling\nM106 P2 S255 ; turn on auxiliary fan for cooling\nM106 P10 S255 ; turn on auxiliary fan for cooling\nM106 P3 S127 ; turn on chamber fan for cooling\n;M140 S0 ; stop heatbed from heating\nM1002 gcode_claim_action : 29\nM191 S0 ; wait for chamber temp\nM106 P2 S102 ; turn on auxiliary fan\nM106 P10 S102 ; turn on chamber fan\nM142 P6 R30 S40 U0.6 V0.8 ; set PLA/TPU/PETG exhaust chamber autocooling\n{endif}\n;==== set airduct mode ====\n\n;===== start to heat heatbed & hotend==========\n M1002 gcode_claim_action : 2\n M1002 set_filament_type:{filament_type[initial_no_support_filament_id]}\n\n ;===== set chamber temperature ==========\n {if (overall_chamber_temperature >= 40)}\n M145 P1 ; set airduct mode to heating mode\n M141 S[overall_chamber_temperature] ; Let Chamber begin to heat\n {endif}\n;===== set chamber temperature ==========\n\n G29.2 S0 ; avoid invalid abl data\n\n;===== first homing start =====\n M1002 gcode_claim_action : 13\n G28 X T300 R\n G150.1 F8000 ; wipe mouth to avoid filament stick to heatbed\n G150.3\n M972 S24 P0\n M1002 gcode_claim_action : 74 ; Heatbed surface foreign object detection\n M972 S26 P0 C0\n G90\n M83\n G1 Y128 F30000\n G1 X128\n G28 Z P0 T400\n M400\n;===== first homign end =====\n\n;===== detection start =====\n M1002 gcode_claim_action : 11\n\n M104 S0 T0\n M104 S0 T1\n M562 P1 E0 B1\n M562 P2 E0 B1\n M18 E\n M400 P200\n M1028 S1\n M972 S19 P0 ;heatbed detection\n M972 S31 P0 ;toolhead camera dirt detection\n M1002 gcode_claim_action : 73 ; Build plate alignment detection\n M972 S34 P0 ;print plate deviation detection\n M1028 S0\n M562 P1 E1 B1\n M562 P2 E1 B1\n M17 D\n\n ;M400\n M104 S{nozzle_temperature_initial_layer[initial_no_support_filament_id]} T{filament_map[initial_no_support_filament_id] % 2} ; rise temp in advance\n\n {if max_print_z >= 145}\n G151 P{filament_map[initial_no_support_filament_id] % 2} M ; plug the heat nozzle\n M1002 gcode_claim_action : 75 ; Detect obstacles at the botton of the heated bed\n G3811 Z{max_print_z} ; Detect obstacles at the bottom of the heated bed\n {endif}\n;===== detection end =====\n\n;===== prepare print temperature and material ==========\n M104 S{nozzle_temperature_initial_layer[initial_no_support_filament_id]-40} A ; rise temp in advance\n M400\n M211 X0 Y0 Z0 ;turn off soft endstop\n M975 S1 ; turn on input shaping\n\n G29.2 S0 ; avoid invalid abl data\n G150.3\n{if ((filament_type[initial_no_support_filament_id] == \"PLA\") || (filament_type[initial_no_support_filament_id] == \"PLA-CF\") || (filament_type[initial_no_support_filament_id] == \"PETG\")) && (nozzle_diameter_at_nozzle_id[initial_nozzle_id] == 0.2)}\nM620.10 A0 F74.8347 H{nozzle_diameter_at_nozzle_id[initial_nozzle_id]} T{flush_temperatures[initial_no_support_filament_id]} P{nozzle_temperature_initial_layer[initial_no_support_filament_id]} S1\nM620.10 A1 F74.8347 H{nozzle_diameter_at_nozzle_id[initial_nozzle_id]} T{flush_temperatures[initial_no_support_filament_id]} P{nozzle_temperature_initial_layer[initial_no_support_filament_id]} S1\n{else}\nM620.10 A0 F{flush_volumetric_speeds[initial_no_support_filament_id]/2.4053*60} H{nozzle_diameter_at_nozzle_id[initial_nozzle_id]} T{flush_temperatures[initial_no_support_filament_id]} P{nozzle_temperature_initial_layer[initial_no_support_filament_id]} S1\nM620.10 A1 F{flush_volumetric_speeds[initial_no_support_filament_id]/2.4053*60} H{nozzle_diameter_at_nozzle_id[initial_nozzle_id]} T{flush_temperatures[initial_no_support_filament_id]} P{nozzle_temperature_initial_layer[initial_no_support_filament_id]} S1\n{endif}\n\n M620.11 P0 L0 I[initial_no_support_filament_id] B[initial_no_support_hotend] E0\n M620.11 K0 I[initial_no_support_filament_id] B[initial_no_support_hotend] R0\n\n M620 S[initial_no_support_filament_id]A H[initial_no_support_hotend] B ; switch material if AMS exist\n M620.22 I[initial_no_support_filament_id] P1 ; enable remote extruder runout auto purge.\n M1002 gcode_claim_action : 4\n M1002 set_filament_type:UNKNOWN\n M400\n T[initial_no_support_filament_id] H[initial_no_support_hotend]\n M400\n M628 S0\n M629\n M400\n M1002 set_filament_type:{filament_type[initial_no_support_filament_id]}\n M621 S[initial_no_support_filament_id]A B\n M104 S{nozzle_temperature_initial_layer[initial_no_support_filament_id]}\n M400\n M106 P1 S0\n M400\n G29.2 S1\n;===== prepare print temperature and material ==========\n\n;===== auto extrude cali start =========================\n M975 S1\n M1002 judge_flag extrude_cali_flag\n M622 J0\n M983.3 F{filament_max_volumetric_speed[initial_no_support_filament_id]/2.4} A0.4 ; cali dynamic extrusion compensation\n M623\n\n M622 J1\n M1002 set_filament_type:{filament_type[initial_no_support_filament_id]}\n M1002 gcode_claim_action : 8\n M109 S{nozzle_temperature[initial_no_support_filament_id]}\n G90\n M83\n M983.3 F{filament_max_volumetric_speed[initial_no_support_filament_id]/2.4} A0.4 ; cali dynamic extrusion compensation\n M400\n M106 P1 S255\n M400 S5\n M106 P1 S0\n G150.3\n M623\n\n M622 J2\n M1002 set_filament_type:{filament_type[initial_no_support_filament_id]}\n M1002 gcode_claim_action : 8\n M109 S{nozzle_temperature[initial_no_support_filament_id]}\n G90\n M83\n M983.3 F{filament_max_volumetric_speed[initial_no_support_filament_id]/2.4} A0.4 ; cali dynamic extrusion compensation\n M400\n M106 P1 S255\n M400 S5\n M106 P1 S0\n G150.3\n M623\n;===== auto extrude cali end =========================\n\n {if hold_chamber_temp_for_flat_print}\n G150.3\n M1002 gcode_claim_action : 58\n M104 S{first_layer_temperature[initial_no_support_filament_id]}\n {if bed_temperature_initial_layer_single > 89}\n {if overall_chamber_temperature < 40}\n M1030 S1200\n SYNC R0 T1200\n {else}\n M1030 S600\n SYNC R0 T600\n {endif}\n {else}\n M1030 S300\n SYNC R0 T300\n {endif}\n M1030 C\n {endif}\n\n {if filament_type[initial_filament_id] == \"TPU\" || filament_type[initial_filament_id] == \"PVA\"}\n {else}\n M83\n G1 E-3 F1800\n M400 P500\n {endif}\n G150.2\n G150.1 F8000\n G150.2\n G150.1 F8000\n\n G91\n G1 Y-16 F12000 ; move away from the trash bin\n G90\n M400\n\n M104 S{nozzle_temperature_initial_layer[initial_no_support_filament_id]-80} A\n\n;===== wipe right nozzle start =====\n M1002 gcode_claim_action : 14\n G150 T{nozzle_temperature_initial_layer[initial_no_support_filament_id]}\n M400\n;===== wipe left nozzle end =====\n\n{if filament_type[initial_filament_id] == \"PC\"}\n M109 S170 A\n{else}\n M109 S140 A\n{endif}\n M106 S0 ; turn off fan , too noisy\n G91\n G1 Z5 F1200\n G90\n M400\n G150.1\n\n{if (overall_chamber_temperature >= 40)}\nM1002 gcode_claim_action : 49\nM191 S[overall_chamber_temperature] ; wait for chamber temp\n{endif}\n\n;===== z ofst cali start =====\n M190 S[bed_temperature_initial_layer_single]; ensure bed temp\n G383 O0 M1 T140\n M400\n;===== z ofst cali end =====\nG90\nM83\nG0 Y200 F18000\n\n;===== bed leveling ==================================\n M1002 gcode_claim_action : 54\n M190 S[bed_temperature_initial_layer_single]; ensure bed temp\n M109 S140 A\n M106 S0 ; turn off fan , too noisy\n M1002 judge_flag g29_before_print_flag\n M622 J1\n M1002 gcode_claim_action : 1\n {if hold_chamber_temp_for_flat_print}\n G29 H R\n {else}\n G29 A1 X{first_layer_print_min[0]} Y{first_layer_print_min[1]} I{first_layer_print_size[0]} J{first_layer_print_size[1]} R\n {endif}\n M400\n M623\n\n M622 J2\n M1002 gcode_claim_action : 1\n {if hold_chamber_temp_for_flat_print}\n G29 H R\n {else}\n G29 A2 X{first_layer_print_min[0]} Y{first_layer_print_min[1]} I{first_layer_print_size[0]} J{first_layer_print_size[1]} R\n {endif}\n M400\n M623\n\n M622 J0\n G28 R\n M623\n G29.2 S1\n;===== bed leveling end ================================\n\n; cali eddy z pos\n;G383.13 T1 C1\n\nM104 S{nozzle_temperature_initial_layer[initial_no_support_filament_id]} A\n;===== mech mode sweep start =====\n M1002 gcode_claim_action : 3\n G90\n G1 X128 Y128 F20000\n G1 Z5 F1200\n M400 P200\n M970.3 Q1 A5 K0 O1\n M974 Q1 S2 P0\n M970.3 Q0 A7 K0 O1\n M970.2 Q0 W73 K1 Z0.01\n M974 Q0 S2 P0\n M975 S1\n M400\n;===== mech mode sweep end =====\n\nM104 S{nozzle_temperature_initial_layer[initial_filament_id]} A\nG150.3\n\n;===== xy ofst cali start =====\nM1002 judge_flag auto_cali_toolhead_offset_flag\n\nM622 J0\n M1012.5 N1 R1\nM623\n\nM622 J1\n M1002 gcode_claim_action : 39\n M141 S0\n M620.17 T0 S{nozzle_temperature_initial_layer[(first_non_support_filaments[0] != -1 ? first_non_support_filaments[0] : first_filaments[0])]} L{(first_non_support_filaments[0] != -1 ? first_non_support_filaments[0] : first_filaments[0])}\n M620.17 T1 S{nozzle_temperature_initial_layer[(first_non_support_filaments[1] != -1 ? first_non_support_filaments[1] : first_filaments[1])]} L{(first_non_support_filaments[1] != -1 ? first_non_support_filaments[1] : first_filaments[1])}\n M620 D[initial_no_support_hotend]\n G383 O1 T{nozzle_temperature_initial_layer[initial_no_support_filament_id]} L{initial_no_support_filament_id}\n M141 S[overall_chamber_temperature]\nM623\n\nM622 J2\n M1002 gcode_claim_action : 39\n M141 S0\n M620.17 T0 S{nozzle_temperature_initial_layer[(first_non_support_filaments[0] != -1 ? first_non_support_filaments[0] : first_filaments[0])]} L{(first_non_support_filaments[0] != -1 ? first_non_support_filaments[0] : first_filaments[0])}\n M620.17 T1 S{nozzle_temperature_initial_layer[(first_non_support_filaments[1] != -1 ? first_non_support_filaments[1] : first_filaments[1])]} L{(first_non_support_filaments[1] != -1 ? first_non_support_filaments[1] : first_filaments[1])}\n M620 D[initial_no_support_hotend]\n G383.3 T{nozzle_temperature_initial_layer[initial_no_support_filament_id]} L{initial_no_support_filament_id}\n M141 S[overall_chamber_temperature]\nM623\n;===== xy ofst cali end =====\n\n M104 S{nozzle_temperature_initial_layer[initial_filament_id]} A\n\n G150.3 ; move to garbage can to wait for temp\n\n;===== wait temperature reaching the reference value =======\n M140 S[bed_temperature_initial_layer_single]\n M190 S[bed_temperature_initial_layer_single]\n\n ;========turn off light and fans =============\n M960 S1 P0 ; turn off laser\n M960 S2 P0 ; turn off laser\n M106 S0 ; turn off cooling fan\n\n;===== wait temperature reaching the reference value =======\n\n M1002 gcode_claim_action : 255\n M400\n M975 S1 ; turn on mech mode supression\n M983.4 S0 ; turn off deformation compensation\n\n;============switch again==================\n M211 X0 Y0 Z0 ;turn off soft endstop\n G91\n G1 Z6 F1200\n G90\n M1002 set_filament_type:{filament_type[initial_no_support_filament_id]}\n M620 S[initial_no_support_filament_id]A H[initial_no_support_hotend] B\n M620.22 I[initial_no_support_filament_id] P1 ; enable remote extruder runout auto purge.\n M400\n T[initial_no_support_filament_id] H[initial_no_support_hotend]\n M400\n M628 S0\n M629\n M400\n M621 S[initial_no_support_filament_id]A B\n;============switch again==================\n\n;===== for Textured PEI Plate , lower the nozzle as the nozzle was touching topmost of the texture when homing ==\n {if bed_temperature_initial_layer_single > 70}\n {if curr_bed_type==\"Textured PEI Plate\"}\n G29.1 Z{-0.003} ; for Textured PEI Plate\n {else}\n G29.1 Z{0.017}\n {endif}\n {else}\n {if curr_bed_type==\"Textured PEI Plate\"}\n G29.1 Z{0.002} ; for Textured PEI Plate\n {else}\n G29.1 Z{0.022}\n {endif}\n {endif}\n\n;===== nozzle load line ===============================\nM1002 gcode_claim_action : 51\n G29.2 S1 ; ensure z comp turn on\n G90\n M83\n M400 P50\n M500 D1\n M400 S3\n M109 S{nozzle_temperature_initial_layer[initial_no_support_filament_id]}\n G0 X100 Y0 F24000\n M400\n ;G130 O0 X100 Y-0.4 Z0.6 F{filament_max_volumetric_speed[initial_no_support_filament_id]/2/2.4053} L40 E20 D5\n G130 O0 X100 Y-0.2 Z0.6 F{filament_max_volumetric_speed[initial_no_support_filament_id]/2/2.4053} L40 E12 D4\nG90\n G90\n M83\n G1 Z1\n M400\n;===== noozle load line end ===========================\nM1002 gcode_claim_action : 0\n G29.99\n\n;M993 A1 B1 C1 ; nozzle cam detection allowed.\n\nM620.6 I[initial_no_support_filament_id] H[initial_no_support_hotend] W1 ;enable ams air printing detect\n\n\n{if (filament_type[initial_no_support_filament_id] == \"TPU\")}\nM1015.3 S1 H{nozzle_diameter_at_nozzle_id[initial_nozzle_id]};enable tpu clog detect\n{else}\nM1015.3 S0;disable tpu clog detect\n{endif}\n\n{if (filament_type[initial_no_support_filament_id] == \"PLA\") || (filament_type[initial_no_support_filament_id] == \"PETG\")\n || (filament_type[initial_no_support_filament_id] == \"PLA-CF\") || (filament_type[initial_no_support_filament_id] == \"PETG-CF\")}\nM1015.4 S1 K1 H{nozzle_diameter_at_nozzle_id[initial_nozzle_id]} ;enable E air printing detect\n{else}\nM1015.4 S0 K0 H{nozzle_diameter_at_nozzle_id[initial_nozzle_id]} ;disable E air printing detect\n{endif}\n\n",
+ "machine_start_gcode": ";M1002 set_flag extrude_cali_flag=1\n;M1002 set_flag g29_before_print_flag=1\n;M1002 set_flag auto_cali_toolhead_offset_flag=1\n;M1002 set_flag build_plate_detect_flag=1\n\n;======== X2D start gcode==========\n;===== 2026/06/05 =====\n\n M140 S[bed_temperature_initial_layer_single] ; heat heatbed first\n M993 A0 B0 C0 ; nozzle cam detection not allowed.\n M400\n ;M73 P99\n\n;=====printer start sound ===================\nM17\nM400 S1\nM1006 S1\nM1006 A53 B9 L50 C53 D9 M50 E53 F9 N50\nM1006 A56 B9 L50 C56 D9 M50 E56 F9 N50\nM1006 A61 B9 L50 C61 D9 M50 E61 F9 N50\nM1006 A53 B9 L50 C53 D9 M50 E53 F9 N50\nM1006 A56 B9 L50 C56 D9 M50 E56 F9 N50\nM1006 A61 B18 L50 C61 D18 M50 E61 F18 N50\nM1006 W\n;=====printer start sound ===================\n\n M1012.1 T1100\n M620 M ;enable remap\n M622.1 S0\n G383.4\n\n;===== avoid end stop =================\n G91\n G380 S2 Z22 F1200\n G380 S2 Z-12 F1200\n G90\n;===== avoid end stop =================\n\n;===== reset machine status =================\n M204 S10000\n M630 S0 P1\n G90\n M17 D ; reset motor current to default\n M960 S5 P1 ; turn on logo lamp\n M220 S100 ;Reset Feedrate\n M1002 set_gcode_claim_speed_level: 5\n M221 S100 ;Reset Flowrate\n M73.2 R1.0 ;Reset left time magnitude\n G29.1 Z{+0.0} ; clear z-trim value first\n M983.1 M1\n M982.2 S1 ; turn on cog noise reduction\n;===== reset machine status =================\n\n;==== set airduct mode ====\n{if (overall_chamber_temperature >= 40)}\nM145 P1 ; set airduct mode to heating mode for heating\nM106 P2 S0 ; turn off auxiliary fan\nM106 P10 S255 ; turn on filter fan\n{else}\nM145 P0 ; set airduct mode to cooling mode for cooling\nM106 P2 S255 ; turn on auxiliary fan for cooling\nM106 P10 S255 ; turn on auxiliary fan for cooling\nM106 P3 S127 ; turn on chamber fan for cooling\n;M140 S0 ; stop heatbed from heating\nM1002 gcode_claim_action : 29\nM191 S0 ; wait for chamber temp\nM106 P2 S102 ; turn on auxiliary fan\nM106 P10 S102 ; turn on chamber fan\nM142 P6 R30 S40 U0.6 V0.8 ; set PLA/TPU/PETG exhaust chamber autocooling\n{endif}\n;==== set airduct mode ====\n\n;===== start to heat heatbed & hotend==========\n M1002 gcode_claim_action : 2\n M1002 set_filament_type:{filament_type[initial_no_support_filament_id]}\n\n ;===== set chamber temperature ==========\n {if (overall_chamber_temperature >= 40)}\n M145 P1 ; set airduct mode to heating mode\n M141 S[overall_chamber_temperature] ; Let Chamber begin to heat\n {endif}\n;===== set chamber temperature ==========\n\n G29.2 S0 ; avoid invalid abl data\n\n;===== first homing start =====\n M1002 gcode_claim_action : 13\n G28 X T300 R\n G150.1 F8000 ; wipe mouth to avoid filament stick to heatbed\n G150.3\n M972 S24 P0\n M1002 gcode_claim_action : 74 ; Heatbed surface foreign object detection\n M972 S26 P0 C0\n G90\n M83\n G1 Y128 F30000\n G1 X128\n G28 Z P0 T400\n M400\n;===== first homign end =====\n\n;===== detection start =====\n M1002 gcode_claim_action : 11\n\n M104 S0 T0\n M104 S0 T1\n M562 P1 E0 B1\n M562 P2 E0 B1\n M18 E\n M400 P200\n M1028 S1\n M972 S19 P0 ;heatbed detection\n M972 S31 P0 ;toolhead camera dirt detection\n M1002 gcode_claim_action : 73 ; Build plate alignment detection\n M972 S34 P0 ;print plate deviation detection\n M1028 S0\n M562 P1 E1 B1\n M562 P2 E1 B1\n M17 D\n\n ;M400\n M104 S{nozzle_temperature_initial_layer[initial_no_support_filament_id]} T{filament_map[initial_no_support_filament_id] % 2} ; rise temp in advance\n\n {if max_print_z >= 145}\n G151 P{filament_map[initial_no_support_filament_id] % 2} M ; plug the heat nozzle\n M1002 gcode_claim_action : 75 ; Detect obstacles at the botton of the heated bed\n G3811 Z{max_print_z} ; Detect obstacles at the bottom of the heated bed\n {endif}\n;===== detection end =====\n\n;===== prepare print temperature and material ==========\n M104 S{nozzle_temperature_initial_layer[initial_no_support_filament_id]-40} A ; rise temp in advance\n M400\n M211 X0 Y0 Z0 ;turn off soft endstop\n M975 S1 ; turn on input shaping\n\n G29.2 S0 ; avoid invalid abl data\n G150.3\n{if ((filament_type[initial_no_support_filament_id] == \"PLA\") || (filament_type[initial_no_support_filament_id] == \"PLA-CF\") || (filament_type[initial_no_support_filament_id] == \"PETG\")) && (nozzle_diameter_at_nozzle_id[initial_nozzle_id] == 0.2)}\nM620.10 A0 F74.8347 H{nozzle_diameter_at_nozzle_id[initial_nozzle_id]} T{flush_temperatures[initial_no_support_filament_id]} P{nozzle_temperature_initial_layer[initial_no_support_filament_id]} S1\nM620.10 A1 F74.8347 H{nozzle_diameter_at_nozzle_id[initial_nozzle_id]} T{flush_temperatures[initial_no_support_filament_id]} P{nozzle_temperature_initial_layer[initial_no_support_filament_id]} S1\n{else}\nM620.10 A0 F{flush_volumetric_speeds[initial_no_support_filament_id]/2.4053*60} H{nozzle_diameter_at_nozzle_id[initial_nozzle_id]} T{flush_temperatures[initial_no_support_filament_id]} P{nozzle_temperature_initial_layer[initial_no_support_filament_id]} S1\nM620.10 A1 F{flush_volumetric_speeds[initial_no_support_filament_id]/2.4053*60} H{nozzle_diameter_at_nozzle_id[initial_nozzle_id]} T{flush_temperatures[initial_no_support_filament_id]} P{nozzle_temperature_initial_layer[initial_no_support_filament_id]} S1\n{endif}\n\n M620.11 P0 L0 I[initial_no_support_filament_id] B[initial_no_support_hotend] E0\n M620.11 K0 I[initial_no_support_filament_id] B[initial_no_support_hotend] R0\n\n M620 S[initial_no_support_filament_id]A H[initial_no_support_hotend] B ; switch material if AMS exist\n M620.22 I[initial_no_support_filament_id] P1 ; enable remote extruder runout auto purge.\n M1002 gcode_claim_action : 4\n M1002 set_filament_type:UNKNOWN\n M400\n T[initial_no_support_filament_id] H[initial_no_support_hotend]\n M400\n M628 S0\n M629\n M400\n M1002 set_filament_type:{filament_type[initial_no_support_filament_id]}\n M621 S[initial_no_support_filament_id]A B\n M104 S{nozzle_temperature_initial_layer[initial_no_support_filament_id]}\n M400\n M106 P1 S0\n M400\n G29.2 S1\n;===== prepare print temperature and material ==========\n\n;===== auto extrude cali start =========================\n M975 S1\n M1002 judge_flag extrude_cali_flag\n M622 J0\n M983.3 F{filament_max_volumetric_speed[initial_no_support_filament_id]/2.4} A0.4 ; cali dynamic extrusion compensation\n M623\n\n M622 J1\n M1002 set_filament_type:{filament_type[initial_no_support_filament_id]}\n M1002 gcode_claim_action : 8\n M109 S{nozzle_temperature[initial_no_support_filament_id]}\n G90\n M83\n M983.3 F{filament_max_volumetric_speed[initial_no_support_filament_id]/2.4} A0.4 ; cali dynamic extrusion compensation\n M400\n M106 P1 S255\n M400 S5\n M106 P1 S0\n G150.3\n M623\n\n M622 J2\n M1002 set_filament_type:{filament_type[initial_no_support_filament_id]}\n M1002 gcode_claim_action : 8\n M109 S{nozzle_temperature[initial_no_support_filament_id]}\n G90\n M83\n M983.3 F{filament_max_volumetric_speed[initial_no_support_filament_id]/2.4} A0.4 ; cali dynamic extrusion compensation\n M400\n M106 P1 S255\n M400 S5\n M106 P1 S0\n G150.3\n M623\n;===== auto extrude cali end =========================\n\n {if hold_chamber_temp_for_flat_print}\n G150.3\n M1002 gcode_claim_action : 58\n M104 S{first_layer_temperature[initial_no_support_filament_id]}\n {if bed_temperature_initial_layer_single > 89}\n {if overall_chamber_temperature < 40}\n M1030 S1200\n SYNC R0 T1200\n {else}\n M1030 S600\n SYNC R0 T600\n {endif}\n {else}\n M1030 S300\n SYNC R0 T300\n {endif}\n M1030 C\n {endif}\n\n {if filament_type[initial_filament_id] == \"TPU\" || filament_type[initial_filament_id] == \"PVA\"}\n {else}\n M83\n G1 E-3 F1800\n M400 P500\n {endif}\n G150.2\n G150.1 F8000\n G150.2\n G150.1 F8000\n\n G91\n G1 Y-16 F12000 ; move away from the trash bin\n G90\n M400\n\n M104 S{nozzle_temperature_initial_layer[initial_no_support_filament_id]-80} A\n\n;===== wipe right nozzle start =====\n M1002 gcode_claim_action : 14\n G150 T{nozzle_temperature_initial_layer[initial_no_support_filament_id]}\n M400\n;===== wipe left nozzle end =====\n\n{if filament_type[initial_filament_id] == \"PC\"}\n M109 S170 A\n{else}\n M109 S140 A\n{endif}\n M106 S0 ; turn off fan , too noisy\n G91\n G1 Z5 F1200\n G90\n M400\n G150.1\n\n{if (overall_chamber_temperature >= 40)}\nM1002 gcode_claim_action : 49\nM191 S[overall_chamber_temperature] ; wait for chamber temp\n{endif}\n\n;===== z ofst cali start =====\n M190 S[bed_temperature_initial_layer_single]; ensure bed temp\n G383 O0 M1 T140\n M400\n;===== z ofst cali end =====\nG90\nM83\nG0 Y200 F18000\n\n;===== bed leveling ==================================\n M1002 gcode_claim_action : 54\n M190 S[bed_temperature_initial_layer_single]; ensure bed temp\n M109 S140 A\n M106 S0 ; turn off fan , too noisy\n M1002 judge_flag g29_before_print_flag\n M622 J1\n M1002 gcode_claim_action : 1\n {if hold_chamber_temp_for_flat_print}\n G29 H R\n {else}\n G29 A1 X{first_layer_print_min[0]} Y{first_layer_print_min[1]} I{first_layer_print_size[0]} J{first_layer_print_size[1]} R\n {endif}\n M400\n M623\n\n M622 J2\n M1002 gcode_claim_action : 1\n {if hold_chamber_temp_for_flat_print}\n G29 H R\n {else}\n G29 A2 X{first_layer_print_min[0]} Y{first_layer_print_min[1]} I{first_layer_print_size[0]} J{first_layer_print_size[1]} R\n {endif}\n M400\n M623\n\n M622 J0\n G28 R\n M623\n G29.2 S1\n;===== bed leveling end ================================\n\n; cali eddy z pos\n;G383.13 T1 C1\n\nM104 S{nozzle_temperature_initial_layer[initial_no_support_filament_id]} A\n;===== mech mode sweep start =====\n M1002 gcode_claim_action : 3\n G90\n G1 X128 Y128 F20000\n G1 Z5 F1200\n M400 P200\n M970.3 Q1 A5 K0 O1\n M974 Q1 S2 P0\n M970.3 Q0 A7 K0 O1\n M970.2 Q0 W73 K1 Z0.01\n M974 Q0 S2 P0\n M975 S1\n M400\n;===== mech mode sweep end =====\n\nM104 S{nozzle_temperature_initial_layer[initial_filament_id]} A\nG150.3\n\n;===== xy ofst cali start =====\nM1002 judge_flag auto_cali_toolhead_offset_flag\n\nM622 J0\n M1012.5 N1 R1\nM623\n\nM622 J1\n M1002 gcode_claim_action : 39\n M141 S0\n M620.17 T0 S{nozzle_temperature_initial_layer[(first_non_support_filaments[0] != -1 ? first_non_support_filaments[0] : (first_filaments[0] != -1 ? first_filaments[0] : 0))]} L{(first_non_support_filaments[0] != -1 ? first_non_support_filaments[0] : (first_filaments[0] != -1 ? first_filaments[0] : 0))}\n M620.17 T1 S{nozzle_temperature_initial_layer[(first_non_support_filaments[1] != -1 ? first_non_support_filaments[1] : (first_filaments[1] != -1 ? first_filaments[1] : 0))]} L{(first_non_support_filaments[1] != -1 ? first_non_support_filaments[1] : (first_filaments[1] != -1 ? first_filaments[1] : 0))}\n M620 D[initial_no_support_hotend]\n G383 O1 T{nozzle_temperature_initial_layer[initial_no_support_filament_id]} L{initial_no_support_filament_id}\n M141 S[overall_chamber_temperature]\nM623\n\nM622 J2\n M1002 gcode_claim_action : 39\n M141 S0\n M620.17 T0 S{nozzle_temperature_initial_layer[(first_non_support_filaments[0] != -1 ? first_non_support_filaments[0] : (first_filaments[0] != -1 ? first_filaments[0] : 0))]} L{(first_non_support_filaments[0] != -1 ? first_non_support_filaments[0] : (first_filaments[0] != -1 ? first_filaments[0] : 0))}\n M620.17 T1 S{nozzle_temperature_initial_layer[(first_non_support_filaments[1] != -1 ? first_non_support_filaments[1] : (first_filaments[1] != -1 ? first_filaments[1] : 0))]} L{(first_non_support_filaments[1] != -1 ? first_non_support_filaments[1] : (first_filaments[1] != -1 ? first_filaments[1] : 0))}\n M620 D[initial_no_support_hotend]\n G383.3 T{nozzle_temperature_initial_layer[initial_no_support_filament_id]} L{initial_no_support_filament_id}\n M141 S[overall_chamber_temperature]\nM623\n;===== xy ofst cali end =====\n\n M104 S{nozzle_temperature_initial_layer[initial_filament_id]} A\n\n G150.3 ; move to garbage can to wait for temp\n\n;===== wait temperature reaching the reference value =======\n M140 S[bed_temperature_initial_layer_single]\n M190 S[bed_temperature_initial_layer_single]\n\n ;========turn off light and fans =============\n M960 S1 P0 ; turn off laser\n M960 S2 P0 ; turn off laser\n M106 S0 ; turn off cooling fan\n\n;===== wait temperature reaching the reference value =======\n\n M1002 gcode_claim_action : 255\n M400\n M975 S1 ; turn on mech mode supression\n M983.4 S0 ; turn off deformation compensation\n\n;============switch again==================\n M211 X0 Y0 Z0 ;turn off soft endstop\n G91\n G1 Z6 F1200\n G90\n M1002 set_filament_type:{filament_type[initial_no_support_filament_id]}\n M620 S[initial_no_support_filament_id]A H[initial_no_support_hotend] B\n M620.22 I[initial_no_support_filament_id] P1 ; enable remote extruder runout auto purge.\n M400\n T[initial_no_support_filament_id] H[initial_no_support_hotend]\n M400\n M628 S0\n M629\n M400\n M621 S[initial_no_support_filament_id]A B\n;============switch again==================\n\n;===== for Textured PEI Plate , lower the nozzle as the nozzle was touching topmost of the texture when homing ==\n {if bed_temperature_initial_layer_single > 70}\n {if curr_bed_type==\"Textured PEI Plate\"}\n G29.1 Z{-0.003} ; for Textured PEI Plate\n {else}\n G29.1 Z{0.017}\n {endif}\n {else}\n {if curr_bed_type==\"Textured PEI Plate\"}\n G29.1 Z{0.002} ; for Textured PEI Plate\n {else}\n G29.1 Z{0.022}\n {endif}\n {endif}\n\n;===== nozzle load line ===============================\nM1002 gcode_claim_action : 51\n G29.2 S1 ; ensure z comp turn on\n G90\n M83\n M400 P50\n M500 D1\n M400 S3\n M109 S{nozzle_temperature_initial_layer[initial_no_support_filament_id]}\n G0 X100 Y0 F24000\n M400\n ;G130 O0 X100 Y-0.4 Z0.6 F{filament_max_volumetric_speed[initial_no_support_filament_id]/2/2.4053} L40 E20 D5\n G130 O0 X100 Y-0.2 Z0.6 F{filament_max_volumetric_speed[initial_no_support_filament_id]/2/2.4053} L40 E12 D4\nG90\n G90\n M83\n G1 Z1\n M400\n;===== noozle load line end ===========================\nM1002 gcode_claim_action : 0\n G29.99\n\n;M993 A1 B1 C1 ; nozzle cam detection allowed.\n\nM620.6 I[initial_no_support_filament_id] H[initial_no_support_hotend] W1 ;enable ams air printing detect\n\n\n{if (filament_type[initial_no_support_filament_id] == \"TPU\")}\nM1015.3 S1 H{nozzle_diameter_at_nozzle_id[initial_nozzle_id]};enable tpu clog detect\n{else}\nM1015.3 S0;disable tpu clog detect\n{endif}\n\n{if (filament_type[initial_no_support_filament_id] == \"PLA\") || (filament_type[initial_no_support_filament_id] == \"PETG\")\n || (filament_type[initial_no_support_filament_id] == \"PLA-CF\") || (filament_type[initial_no_support_filament_id] == \"PETG-CF\")}\nM1015.4 S1 K1 H{nozzle_diameter_at_nozzle_id[initial_nozzle_id]} ;enable E air printing detect\n{else}\nM1015.4 S0 K0 H{nozzle_diameter_at_nozzle_id[initial_nozzle_id]} ;disable E air printing detect\n{endif}\n\n",
"time_lapse_gcode": ";======== X2D timelapse gcode ========\n;======== 2026/06/03 ========\n; SKIPPABLE_START\n; SKIPTYPE: timelapse\nM622.1 S1 ; for prev firware, default turned on\n\nM1002 judge_flag timelapse_record_flag\n\nM622 J1\n {if !spiral_mode && !(has_timelapse_safe_pos) }\n {if most_used_physical_extruder_id!= curr_physical_extruder_id || timelapse_type == 1}\n M83\n G1 Z{max_layer_z + 0.4} F1200\n M400\n {endif}\n {endif}\n\n {if timelapse_inline_photo}\n M971 S11 C10 O0\n M1004 S5 P1 ; external shutter\n {elsif has_timelapse_safe_pos && !spiral_mode}\n M9711 M{timelapse_type} E{most_used_physical_extruder_id} U{timelapse_pos_x} V{timelapse_pos_y} Z{layer_z + (farthest_point_timelapse_enabled ? 0.0 : 0.4)} S11 C10 O0 T3000\n {else}\n {if spiral_mode}\n M971 S11 C10 O0\n M1004 S5 P1 ; external shutter\n {else}\n M9711 M{timelapse_type} E{most_used_physical_extruder_id} Z{layer_z + (farthest_point_timelapse_enabled ? 0.0 : 0.4)} S11 C10 O0 T3000\n {endif}\n {endif}\n\n {if !spiral_mode && !(has_timelapse_safe_pos) }\n {if most_used_physical_extruder_id!= curr_physical_extruder_id || timelapse_type == 1}\n G90\n G1 Z{max_layer_z + 3.0} F1200\n G91\n G0 Y-20 F18000\n G90\n M83\n {endif}\n {endif}\nM623\n; SKIPPABLE_END\n",
"nozzle_diameter": [
"0.4",
diff --git a/resources/profiles/Creality/process/0.40mm Standard @Creality K1 SE 0.8 nozzle.json b/resources/profiles/Creality/process/0.40mm Standard @Creality K1 SE 0.8 nozzle.json
index 46a80e8498..88bc46add2 100644
--- a/resources/profiles/Creality/process/0.40mm Standard @Creality K1 SE 0.8 nozzle.json
+++ b/resources/profiles/Creality/process/0.40mm Standard @Creality K1 SE 0.8 nozzle.json
@@ -3,7 +3,7 @@
"name": "0.40mm Standard @Creality K1 SE 0.8 nozzle",
"inherits": "fdm_process_creality_common",
"from": "system",
- "setting_id": "caYmVSsFmtESzLNF",
+ "setting_id": "5mfhrVJYlK4lWDUs",
"instantiation": "true",
"reduce_crossing_wall": "0",
"max_travel_detour_distance": "0",
diff --git a/resources/profiles/Qidi.json b/resources/profiles/Qidi.json
index d3d083c074..9e7a290f96 100644
--- a/resources/profiles/Qidi.json
+++ b/resources/profiles/Qidi.json
@@ -1,6 +1,6 @@
{
"name": "Qidi",
- "version": "02.04.00.07",
+ "version": "02.04.00.08",
"force_update": "0",
"description": "Qidi configurations",
"machine_model_list": [
diff --git a/resources/profiles/Qidi/machine/fdm_qidi_x3_common.json b/resources/profiles/Qidi/machine/fdm_qidi_x3_common.json
index ada1be5d24..9796856379 100644
--- a/resources/profiles/Qidi/machine/fdm_qidi_x3_common.json
+++ b/resources/profiles/Qidi/machine/fdm_qidi_x3_common.json
@@ -10,36 +10,6 @@
"machine_pause_gcode": "M0",
"support_chamber_temp_control": "1",
"wipe_tower_type": "type1",
- "filament_dev_ams_drying_ams_limitations": [
- "1"
- ],
- "filament_dev_ams_drying_temperature": [
- "40.0",
- "40.0",
- "40.0",
- "40.0"
- ],
- "filament_dev_ams_drying_time": [
- "8.0",
- "8.0",
- "8.0",
- "8.0"
- ],
- "filament_dev_drying_softening_temperature": [
- "40.0"
- ],
- "filament_dev_ams_drying_heat_distortion_temperature": [
- "45.0"
- ],
- "filament_dev_drying_cooling_temperature": [
- "35.0"
- ],
- "filament_dev_chamber_drying_bed_temperature": [
- "90.0"
- ],
- "filament_dev_chamber_drying_time": [
- "12.0"
- ],
"retraction_length": [
"1"
],
diff --git a/resources/profiles/Ratrig/process/0.30mm Big @RatRig V-Core 4 0.8.json b/resources/profiles/Ratrig/process/0.30mm Big @RatRig V-Core 4 0.8.json
index 2eee1dc710..9069167cde 100644
--- a/resources/profiles/Ratrig/process/0.30mm Big @RatRig V-Core 4 0.8.json
+++ b/resources/profiles/Ratrig/process/0.30mm Big @RatRig V-Core 4 0.8.json
@@ -3,7 +3,7 @@
"name": "0.30mm Big @RatRig V-Core 4 0.8",
"inherits": "fdm_process_ratrig_common",
"from": "system",
- "setting_id": "7FOG8fkUbrLknvuz",
+ "setting_id": "2tM04aSQ8NcTsmQo",
"instantiation": "true",
"layer_height": "0.3",
"inital_layer_height": "0.35",
diff --git a/resources/profiles/iQ/process/0.15mm Standard @iQ TiQ2 (0.25 Nozzle).json b/resources/profiles/iQ/process/0.15mm Standard @iQ TiQ2 (0.25 Nozzle).json
index 7bbb5a15f3..1791058107 100644
--- a/resources/profiles/iQ/process/0.15mm Standard @iQ TiQ2 (0.25 Nozzle).json
+++ b/resources/profiles/iQ/process/0.15mm Standard @iQ TiQ2 (0.25 Nozzle).json
@@ -3,6 +3,7 @@
"name": "0.15mm Standard @iQ TiQ2 (0.25 Nozzle)",
"inherits": "fdm_process_tiq_common",
"from": "system",
+ "setting_id": "PNxZXC0peOlGlKwG",
"instantiation": "true",
"compatible_printers": [
"iQ TiQ2 0.25 Nozzle"
diff --git a/resources/profiles/iQ/process/0.15mm Standard @iQ TiQ8 (0.25 Nozzle).json b/resources/profiles/iQ/process/0.15mm Standard @iQ TiQ8 (0.25 Nozzle).json
index 0cfcdae16e..b643441055 100644
--- a/resources/profiles/iQ/process/0.15mm Standard @iQ TiQ8 (0.25 Nozzle).json
+++ b/resources/profiles/iQ/process/0.15mm Standard @iQ TiQ8 (0.25 Nozzle).json
@@ -3,6 +3,7 @@
"name": "0.15mm Standard @iQ TiQ8 (0.25 Nozzle)",
"inherits": "fdm_process_tiq_common",
"from": "system",
+ "setting_id": "K0Ybw0qUqEIL05PY",
"instantiation": "true",
"compatible_printers": [
"iQ TiQ8 0.25 Nozzle"
diff --git a/resources/profiles/iQ/process/0.30mm Standard @iQ TiQ2 (0.6 Nozzle).json b/resources/profiles/iQ/process/0.30mm Standard @iQ TiQ2 (0.6 Nozzle).json
index e44c962ebf..6a71a4ba84 100644
--- a/resources/profiles/iQ/process/0.30mm Standard @iQ TiQ2 (0.6 Nozzle).json
+++ b/resources/profiles/iQ/process/0.30mm Standard @iQ TiQ2 (0.6 Nozzle).json
@@ -3,6 +3,7 @@
"name": "0.30mm Standard @iQ TiQ2 (0.6 Nozzle)",
"inherits": "fdm_process_tiq_common",
"from": "system",
+ "setting_id": "IGN3ZUR73J0veLrE",
"instantiation": "true",
"compatible_printers": [
"iQ TiQ2 0.6 Nozzle"
diff --git a/resources/profiles/iQ/process/0.30mm Standard @iQ TiQ8 (0.6 Nozzle).json b/resources/profiles/iQ/process/0.30mm Standard @iQ TiQ8 (0.6 Nozzle).json
index e71c6e80ce..0c8a567846 100644
--- a/resources/profiles/iQ/process/0.30mm Standard @iQ TiQ8 (0.6 Nozzle).json
+++ b/resources/profiles/iQ/process/0.30mm Standard @iQ TiQ8 (0.6 Nozzle).json
@@ -3,6 +3,7 @@
"name": "0.30mm Standard @iQ TiQ8 (0.6 Nozzle)",
"inherits": "fdm_process_tiq_common",
"from": "system",
+ "setting_id": "QEODQzrO9UZEgrtB",
"instantiation": "true",
"compatible_printers": [
"iQ TiQ8 0.6 Nozzle"
diff --git a/resources/profiles/iQ/process/0.40mm Standard @iQ TiQ2 (0.8 Nozzle).json b/resources/profiles/iQ/process/0.40mm Standard @iQ TiQ2 (0.8 Nozzle).json
index b487541526..5dba6b07c2 100644
--- a/resources/profiles/iQ/process/0.40mm Standard @iQ TiQ2 (0.8 Nozzle).json
+++ b/resources/profiles/iQ/process/0.40mm Standard @iQ TiQ2 (0.8 Nozzle).json
@@ -3,6 +3,7 @@
"name": "0.40mm Standard @iQ TiQ2 (0.8 Nozzle)",
"inherits": "fdm_process_tiq_common",
"from": "system",
+ "setting_id": "ZoiYkGXOTBh39HTB",
"instantiation": "true",
"compatible_printers": [
"iQ TiQ2 0.8 Nozzle"
diff --git a/resources/profiles/iQ/process/0.40mm Standard @iQ TiQ8 (0.8 Nozzle).json b/resources/profiles/iQ/process/0.40mm Standard @iQ TiQ8 (0.8 Nozzle).json
index 7c5bd973e7..5cd189771f 100644
--- a/resources/profiles/iQ/process/0.40mm Standard @iQ TiQ8 (0.8 Nozzle).json
+++ b/resources/profiles/iQ/process/0.40mm Standard @iQ TiQ8 (0.8 Nozzle).json
@@ -3,6 +3,7 @@
"name": "0.40mm Standard @iQ TiQ8 (0.8 Nozzle)",
"inherits": "fdm_process_tiq_common",
"from": "system",
+ "setting_id": "C7TZWdCzQroB8pKL",
"instantiation": "true",
"compatible_printers": [
"iQ TiQ8 0.8 Nozzle"
diff --git a/resources/shaders/110/gouraud.fs b/resources/shaders/110/gouraud.fs
index 010baaa0dd..59c4e4d410 100644
--- a/resources/shaders/110/gouraud.fs
+++ b/resources/shaders/110/gouraud.fs
@@ -231,6 +231,8 @@ void main()
s = max(s, DetectSilho(fragCoord.xy + vec2(i, 0)));
s = max(s, DetectSilho(fragCoord.xy + vec2(0, i)));
}
+ if (s < 0.01)
+ discard;
gl_FragColor = vec4(mix(color.rgb, getBackfaceColor(color.rgb), s), color.a);
}
#ifdef ENABLE_ENVIRONMENT_MAP
diff --git a/resources/shaders/110/gouraud.vs b/resources/shaders/110/gouraud.vs
index ff3a190c79..184529be82 100644
--- a/resources/shaders/110/gouraud.vs
+++ b/resources/shaders/110/gouraud.vs
@@ -30,6 +30,8 @@ uniform mat4 projection_matrix;
uniform mat3 view_normal_matrix;
uniform mat4 volume_world_matrix;
uniform SlopeDetection slope;
+uniform bool is_outline;
+uniform vec2 screen_size;
// Clipping plane, x = min z, y = max z. Used by the FFF and SLA previews to clip with a top / bottom plane.
uniform vec2 z_range;
@@ -75,6 +77,15 @@ void main()
world_normal_z = slope.actived ? (normalize(slope.volume_world_normal_matrix * v_normal)).z : 0.0;
gl_Position = projection_matrix * position;
+ if (is_outline) {
+ vec3 n = normalize((view_normal_matrix * v_normal).xyz);
+ vec2 dir = normalize(n.xy);
+ if (dot(dir, dir) > 0.0) {
+ //set outline thickness
+ float px = 3.0;
+ gl_Position.xy += dir * (px * 2.0 / screen_size) * gl_Position.w;
+ }
+ }
// Fill in the scalars for fragment shader clipping. Fragments with any of these components lower than zero are discarded.
clipping_planes_dots = vec3(dot(world_pos, clipping_plane), world_pos.z - z_range.x, z_range.y - world_pos.z);
color_clip_plane_dot = dot(world_pos, color_clip_plane);
diff --git a/resources/shaders/110/imgui.fs b/resources/shaders/110/imgui.fs
index 4b0e27ce9d..3af9c82653 100644
--- a/resources/shaders/110/imgui.fs
+++ b/resources/shaders/110/imgui.fs
@@ -1,11 +1,11 @@
#version 110
-uniform sampler2D Texture;
+uniform sampler2D s_texture;
varying vec2 Frag_UV;
varying vec4 Frag_Color;
void main()
{
- gl_FragColor = Frag_Color * texture2D(Texture, Frag_UV.st);
+ gl_FragColor = Frag_Color * texture2D(s_texture, Frag_UV.st);
}
\ No newline at end of file
diff --git a/resources/shaders/110/phong.fs b/resources/shaders/110/phong.fs
index a5c1c5b8ab..5fedf7edff 100644
--- a/resources/shaders/110/phong.fs
+++ b/resources/shaders/110/phong.fs
@@ -273,6 +273,8 @@ void main()
s = max(s, DetectSilho(fragCoord.xy + vec2(i, 0)));
s = max(s, DetectSilho(fragCoord.xy + vec2(0, i)));
}
+ if (s < 0.01)
+ discard;
gl_FragColor = vec4(mix(shaded_color.rgb, getBackfaceColor(shaded_color.rgb), s), shaded_color.a);
}
#ifdef ENABLE_ENVIRONMENT_MAP
diff --git a/resources/shaders/110/phong.vs b/resources/shaders/110/phong.vs
index b9e90e7cc2..10d36e233f 100644
--- a/resources/shaders/110/phong.vs
+++ b/resources/shaders/110/phong.vs
@@ -14,6 +14,8 @@ uniform mat4 projection_matrix;
uniform mat3 view_normal_matrix;
uniform mat4 volume_world_matrix;
uniform SlopeDetection slope;
+uniform bool is_outline;
+uniform vec2 screen_size;
// Clipping plane, x = min z, y = max z. Used by the FFF and SLA previews to clip with a top / bottom plane.
uniform vec2 z_range;
@@ -48,6 +50,15 @@ void main()
world_normal_z = slope.actived ? (normalize(slope.volume_world_normal_matrix * v_normal)).z : 0.0;
gl_Position = projection_matrix * position;
+ if (is_outline) {
+ vec3 n = normalize((view_normal_matrix * v_normal).xyz);
+ vec2 dir = normalize(n.xy);
+ if (dot(dir, dir) > 0.0) {
+ //set outline thickness
+ float px = 3.0;
+ gl_Position.xy += dir * (px * 2.0 / screen_size) * gl_Position.w;
+ }
+ }
// Fill in the scalars for fragment shader clipping. Fragments with any of these components lower than zero are discarded.
clipping_planes_dots = vec3(dot(world_pos, clipping_plane), world_pos.z - z_range.x, z_range.y - world_pos.z);
color_clip_plane_dot = dot(world_pos, color_clip_plane);
diff --git a/resources/shaders/110/ssao.fs b/resources/shaders/110/ssao.fs
index 43b52a3f29..5aada05883 100644
--- a/resources/shaders/110/ssao.fs
+++ b/resources/shaders/110/ssao.fs
@@ -11,6 +11,7 @@ uniform sampler2D normal_texture;
uniform vec2 inv_tex_size;
uniform float z_near;
uniform float z_far;
+uniform bool is_outline;
varying vec2 tex_coord;
@@ -22,6 +23,10 @@ float linearize_depth(float depth)
void main()
{
+ if (is_outline) {
+ gl_FragColor = vec4(texture2D(color_texture, tex_coord).rgb, 1.0);
+ return;
+ }
vec3 base = texture2D(color_texture, tex_coord).rgb;
float depth_center = linearize_depth(texture2D(depth_texture, tex_coord).r);
diff --git a/resources/shaders/140/gouraud.fs b/resources/shaders/140/gouraud.fs
index 8ebb1d8a60..728b7f942c 100644
--- a/resources/shaders/140/gouraud.fs
+++ b/resources/shaders/140/gouraud.fs
@@ -232,6 +232,8 @@ void main()
s = max(s, DetectSilho(fragCoord.xy + vec2(i, 0)));
s = max(s, DetectSilho(fragCoord.xy + vec2(0, i)));
}
+ if (s < 0.01)
+ discard;
out_color = vec4(mix(color.rgb, getBackfaceColor(color.rgb), s), color.a);
}
#ifdef ENABLE_ENVIRONMENT_MAP
diff --git a/resources/shaders/140/gouraud.vs b/resources/shaders/140/gouraud.vs
index 72dd4ff1bf..a31e751964 100644
--- a/resources/shaders/140/gouraud.vs
+++ b/resources/shaders/140/gouraud.vs
@@ -30,6 +30,8 @@ uniform mat4 projection_matrix;
uniform mat3 view_normal_matrix;
uniform mat4 volume_world_matrix;
uniform SlopeDetection slope;
+uniform bool is_outline;
+uniform vec2 screen_size;
// Clipping plane, x = min z, y = max z. Used by the FFF and SLA previews to clip with a top / bottom plane.
uniform vec2 z_range;
@@ -75,6 +77,15 @@ void main()
world_normal_z = slope.actived ? (normalize(slope.volume_world_normal_matrix * v_normal)).z : 0.0;
gl_Position = projection_matrix * position;
+ if (is_outline) {
+ vec3 n = normalize((view_normal_matrix * v_normal).xyz);
+ vec2 dir = normalize(n.xy);
+ if (dot(dir, dir) > 0.0) {
+ //set outline thickness
+ float px = 3.0;
+ gl_Position.xy += dir * (px * 2.0 / screen_size) * gl_Position.w;
+ }
+ }
// Fill in the scalars for fragment shader clipping. Fragments with any of these components lower than zero are discarded.
clipping_planes_dots = vec3(dot(world_pos, clipping_plane), world_pos.z - z_range.x, z_range.y - world_pos.z);
color_clip_plane_dot = dot(world_pos, color_clip_plane);
diff --git a/resources/shaders/140/imgui.fs b/resources/shaders/140/imgui.fs
index 1666c814eb..daebb189de 100644
--- a/resources/shaders/140/imgui.fs
+++ b/resources/shaders/140/imgui.fs
@@ -1,6 +1,6 @@
#version 140
-uniform sampler2D Texture;
+uniform sampler2D s_texture;
in vec2 Frag_UV;
in vec4 Frag_Color;
@@ -9,5 +9,5 @@ out vec4 out_color;
void main()
{
- out_color = Frag_Color * texture(Texture, Frag_UV.st);
+ out_color = Frag_Color * texture(s_texture, Frag_UV.st);
}
\ No newline at end of file
diff --git a/resources/shaders/140/phong.fs b/resources/shaders/140/phong.fs
index 894e3eb863..bbde72592c 100644
--- a/resources/shaders/140/phong.fs
+++ b/resources/shaders/140/phong.fs
@@ -277,6 +277,8 @@ void main()
s = max(s, DetectSilho(fragCoord.xy + vec2(i, 0)));
s = max(s, DetectSilho(fragCoord.xy + vec2(0, i)));
}
+ if (s < 0.01)
+ discard;
out_color = vec4(mix(shaded_color.rgb, getBackfaceColor(shaded_color.rgb), s), shaded_color.a);
}
#ifdef ENABLE_ENVIRONMENT_MAP
diff --git a/resources/shaders/140/phong.vs b/resources/shaders/140/phong.vs
index 2ccbaf16ee..c7570edb95 100644
--- a/resources/shaders/140/phong.vs
+++ b/resources/shaders/140/phong.vs
@@ -14,6 +14,8 @@ uniform mat4 projection_matrix;
uniform mat3 view_normal_matrix;
uniform mat4 volume_world_matrix;
uniform SlopeDetection slope;
+uniform bool is_outline;
+uniform vec2 screen_size;
// Clipping plane, x = min z, y = max z. Used by the FFF and SLA previews to clip with a top / bottom plane.
uniform vec2 z_range;
@@ -48,6 +50,15 @@ void main()
world_normal_z = slope.actived ? (normalize(slope.volume_world_normal_matrix * v_normal)).z : 0.0;
gl_Position = projection_matrix * position;
+ if (is_outline) {
+ vec3 n = normalize((view_normal_matrix * v_normal).xyz);
+ vec2 dir = normalize(n.xy);
+ if (dot(dir, dir) > 0.0) {
+ //set outline thickness
+ float px = 3.0;
+ gl_Position.xy += dir * (px * 2.0 / screen_size) * gl_Position.w;
+ }
+ }
// Fill in the scalars for fragment shader clipping. Fragments with any of these components lower than zero are discarded.
clipping_planes_dots = vec3(dot(world_pos, clipping_plane), world_pos.z - z_range.x, z_range.y - world_pos.z);
color_clip_plane_dot = dot(world_pos, color_clip_plane);
diff --git a/resources/shaders/140/ssao.fs b/resources/shaders/140/ssao.fs
index 86ea6a54bf..b3b6df6877 100644
--- a/resources/shaders/140/ssao.fs
+++ b/resources/shaders/140/ssao.fs
@@ -10,6 +10,7 @@ uniform sampler2D depth_texture;
uniform sampler2D normal_texture;
uniform float z_near;
uniform float z_far;
+uniform bool is_outline;
in vec2 tex_coord;
out vec4 frag_color;
@@ -22,6 +23,10 @@ float linearize_depth(float depth)
void main()
{
+ if (is_outline) {
+ frag_color = vec4(texture(color_texture, tex_coord).rgb, 1.0);
+ return;
+ }
ivec2 pixel = ivec2(gl_FragCoord.xy);
float center_depth = linearize_depth(texelFetch(depth_texture, pixel, 0).r);
diff --git a/resources/web/dialog/PluginsDialog/index.js b/resources/web/dialog/PluginsDialog/index.js
index b67cc8c785..cd690ef8b1 100644
--- a/resources/web/dialog/PluginsDialog/index.js
+++ b/resources/web/dialog/PluginsDialog/index.js
@@ -1518,7 +1518,7 @@ function RunScriptPlugin(pluginId, capabilityName = "") {
if (!plugin || !capability)
return;
- SendMessage("run_script_plugin", {
+ SendMessage("run_script_plugin_capability", {
plugin_key: pluginId,
capability_name: String(capability.name || "")
});
diff --git a/src/libslic3r/Fill/FillBase.cpp b/src/libslic3r/Fill/FillBase.cpp
index 29eb6120ed..2606529099 100644
--- a/src/libslic3r/Fill/FillBase.cpp
+++ b/src/libslic3r/Fill/FillBase.cpp
@@ -162,11 +162,14 @@ void Fill::fill_surface_extrusion(const Surface* surface, const FillParams& para
out.push_back(eec = new ExtrusionEntityCollection());
// Only concentric fills are not sorted.
eec->no_sort = this->no_sort();
+ // ORCA: special flag for flow rate calibration
+ auto is_flow_calib = params.extrusion_role == erTopSolidInfill && this->print_object_config->has("calib_flowrate_topinfill_special_order") &&
+ this->print_object_config->option("calib_flowrate_topinfill_special_order")->getBool();
// Orca: a forced surface fill order must survive the G-code path planner, which would
- // otherwise re-chain and possibly reverse the paths. This also covers the flow rate
- // calibration, which forces an outward fill order on its top surfaces.
+ // otherwise re-chain and possibly reverse the paths. The same applies to the flow rate
+ // calibration's special toolpath order.
const bool keep_fill_order = params.fill_order != SurfaceFillOrder::Default;
- if (keep_fill_order) {
+ if (is_flow_calib || keep_fill_order) {
eec->no_sort = true;
}
size_t idx = eec->entities.size();
@@ -181,7 +184,7 @@ void Fill::fill_surface_extrusion(const Surface* surface, const FillParams& para
params.extrusion_role,
flow_mm3_per_mm, float(flow_width), params.flow.height());
}
- if (!params.can_reverse || keep_fill_order) {
+ if (!params.can_reverse || is_flow_calib || keep_fill_order) {
for (size_t i = idx; i < eec->entities.size(); i++)
eec->entities[i]->set_reverse();
}
diff --git a/src/libslic3r/Fill/FillPlanePath.cpp b/src/libslic3r/Fill/FillPlanePath.cpp
index edfbaef7d6..7ce8e4abb3 100644
--- a/src/libslic3r/Fill/FillPlanePath.cpp
+++ b/src/libslic3r/Fill/FillPlanePath.cpp
@@ -134,7 +134,34 @@ void FillPlanePath::_fill_surface_single(
if (!polylines.empty()) {
Polylines chained;
if (params.dont_connect() || params.density > 0.5) {
- if (params.fill_order != SurfaceFillOrder::Default) {
+ // ORCA: special flag for flow rate calibration. The chords chained ahead of the
+ // inside-out center spiral collide with it in opposing directions, raising a
+ // tactile lip that the calibration reads. Only applies while the fill order is
+ // Default, so it can be overridden from the calibration objects.
+ auto is_flow_calib = params.fill_order == SurfaceFillOrder::Default &&
+ params.extrusion_role == erTopSolidInfill &&
+ this->print_object_config->has("calib_flowrate_topinfill_special_order") &&
+ this->print_object_config->option("calib_flowrate_topinfill_special_order")->getBool() &&
+ dynamic_cast(this);
+ if (is_flow_calib) {
+ // We want the spiral part to be printed inside-out
+ // Find the center spiral line first, by looking for the longest one
+ auto it = std::max_element(polylines.begin(), polylines.end(),
+ [](const Polyline& a, const Polyline& b) { return a.length() < b.length(); });
+ Polyline center_spiral = std::move(*it);
+
+ // Ensure the spiral is printed from inside to out
+ if (center_spiral.first_point().squaredNorm() > center_spiral.last_point().squaredNorm()) {
+ center_spiral.reverse();
+ }
+
+ // Chain the other polylines
+ polylines.erase(it);
+ chained = chain_polylines(std::move(polylines), nullptr);
+
+ // Then add the center spiral back
+ chained.push_back(std::move(center_spiral));
+ } else if (params.fill_order != SurfaceFillOrder::Default) {
// Orca: print the fragments in the order they appear along the generated
// path, which runs from the center outwards. The Euclidean distance from
// the center cannot be used for this: along the Octagram Spiral the radius
diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp
index 3a46e6fa34..ccb80cd125 100644
--- a/src/libslic3r/Preset.cpp
+++ b/src/libslic3r/Preset.cpp
@@ -470,6 +470,8 @@ void Preset::normalize(DynamicPrintConfig &config)
continue;
if (filament_options_with_variant.find(key) != filament_options_with_variant.end())
continue;
+ if (filament_dev_options.find(key) != filament_dev_options.end())
+ continue;
auto *opt = config.option(key, false);
/*assert(opt != nullptr);
assert(opt->is_vector());*/
@@ -1311,6 +1313,7 @@ static std::vector s_Preset_print_options{
"interlocking_depth",
"interlocking_boundary_avoidance",
"interlocking_beam_width",
+ "calib_flowrate_topinfill_special_order",
// Z Anti-Aliasing (ZAA)
"zaa_enabled",
"zaa_minimize_perimeter_height",
@@ -1375,7 +1378,11 @@ static std::vector s_Preset_filament_options {/*"filament_colour",
"filament_preheat_temperature_delta", "filament_retract_length_nc",
"filament_change_length_nc", "filament_prime_volume_nc",
"long_retractions_when_ec", "retraction_distances_when_ec",
- "plugin_preference_overrides"
+ "plugin_preference_overrides",
+ //ams chamber
+ "filament_dev_ams_drying_ams_limitations", "filament_dev_ams_drying_temperature", "filament_dev_ams_drying_time", "filament_dev_ams_drying_heat_distortion_temperature",
+ "filament_dev_chamber_drying_bed_temperature", "filament_dev_chamber_drying_time",
+ "filament_dev_drying_softening_temperature", "filament_dev_drying_cooling_temperature"
};
static std::vector s_Preset_machine_limits_options {
@@ -3828,6 +3835,26 @@ void PresetCollection::set_custom_preset_alias(Preset &preset)
set_printer_hold_alias(preset.alias, preset);
}
+std::string PresetCollection::get_preset_alias(Preset &preset, bool force)
+{
+ if (!preset.alias.empty())
+ return preset.alias;
+ else
+ set_custom_preset_alias(preset);
+
+ if (!preset.alias.empty() || !force)
+ return preset.alias;
+
+ std::string alias_name;
+ std::string preset_name = preset.name;
+ size_t end_pos = preset_name.find_first_of("@");
+ if (end_pos != std::string::npos) {
+ alias_name = preset_name.substr(0, end_pos);
+ boost::trim_right(alias_name);
+ }
+ return alias_name;
+}
+
void PresetCollection::set_printer_hold_alias(const std::string &alias, Preset &preset, bool remove)
{
auto compatible_printers = dynamic_cast(preset.config.option("compatible_printers"));
diff --git a/src/libslic3r/Preset.hpp b/src/libslic3r/Preset.hpp
index 75150f85c9..8863f04672 100644
--- a/src/libslic3r/Preset.hpp
+++ b/src/libslic3r/Preset.hpp
@@ -805,6 +805,9 @@ public:
std::string path_from_name(const std::string &new_name, bool detach = false) const;
std::string path_for_preset(const Preset & preset) const;
+ // Get the alias of a preset, setting it if it's empty
+ std::string get_preset_alias(Preset &preset, bool force = false);
+
size_t num_default_presets() { return m_num_default_presets; }
protected:
diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp
index 832bb6f33f..af1d380dde 100644
--- a/src/libslic3r/PrintConfig.cpp
+++ b/src/libslic3r/PrintConfig.cpp
@@ -2031,7 +2031,7 @@ void PrintConfigDef::init_fff_params()
def = this->add("initial_layer_travel_acceleration", coFloatsOrPercents);
def->label = L("First layer travel");
def->tooltip = L("Travel acceleration of first layer.\nThe percentage value is relative to Travel Acceleration.");
- def->sidetext = L("mm/s² or %");
+ def->sidetext = L(u8"mm/s² or %");
def->min = 0;
def->mode = comAdvanced;
def->ratio_over = "travel_acceleration";
@@ -2042,7 +2042,7 @@ void PrintConfigDef::init_fff_params()
def->label = L("Bridge");
def->category = L("Speed");
def->tooltip = L("Acceleration of bridges. If the value is expressed as a percentage (e.g. 50%), it will be calculated based on the outer wall acceleration.");
- def->sidetext = L("mm/s² or %");
+ def->sidetext = L(u8"mm/s² or %");
def->min = 0;
def->mode = comAdvanced;
def->ratio_over = "outer_wall_acceleration";
@@ -3466,7 +3466,7 @@ void PrintConfigDef::init_fff_params()
def->label = L("Sparse infill");
def->category = L("Speed");
def->tooltip = L("Acceleration of sparse infill. If the value is expressed as a percentage (e.g. 100%), it will be calculated based on the default acceleration.");
- def->sidetext = L("mm/s² or %");
+ def->sidetext = L(u8"mm/s² or %");
def->min = 0;
def->mode = comAdvanced;
def->ratio_over = "default_acceleration";
@@ -3477,7 +3477,7 @@ void PrintConfigDef::init_fff_params()
def->label = L("Internal solid infill");
def->category = L("Speed");
def->tooltip = L("Acceleration of internal solid infill. If the value is expressed as a percentage (e.g. 100%), it will be calculated based on the default acceleration.");
- def->sidetext = L("mm/s² or %");
+ def->sidetext = L(u8"mm/s² or %");
def->min = 0;
def->mode = comAdvanced;
def->ratio_over = "default_acceleration";
@@ -3933,7 +3933,7 @@ void PrintConfigDef::init_fff_params()
"The shift is applied once every number of layers set by Layers between ripple offset, so layers within the same group are printed identically.");
def->min = 0;
def->max = 100;
- def->sidetext = ("%");
+ def->sidetext = "%";
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionPercent(50));
@@ -4141,7 +4141,7 @@ void PrintConfigDef::init_fff_params()
"value that the firmware would silently drop, and the fan never receives a value below the one "
"you know it can actually spool at."
"\nSet to 0 to deactivate.");
- def->sidetext = L("%");
+ def->sidetext = "%";
def->min = 0;
def->max = 100;
def->mode = comAdvanced;
@@ -4661,6 +4661,11 @@ void PrintConfigDef::init_fff_params()
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionInt(2));
+ // ORCA: special flag for flow rate calibration
+ def = this->add("calib_flowrate_topinfill_special_order", coBool);
+ def->mode = comDevelop;
+ def->set_default_value(new ConfigOptionBool(false));
+
def = this->add("ironing_type", coEnum);
def->label = L("Ironing type");
def->category = L("Quality");
@@ -5091,7 +5096,7 @@ void PrintConfigDef::init_fff_params()
def = this->add("input_shaping_freq_x", coFloat);
def->label = L("X");
def->tooltip = L("Resonant frequency for the X axis input shaper.\nZero will use the firmware frequency.\nTo disable input shaping, use the Disable type.\nRRF: X and Y values are equal.");
- def->sidetext = "Hz";
+ def->sidetext = L("Hz"); // Hertz, CIS languages need translation
def->min = 0;
def->max = 1000;
def->mode = comExpert;
@@ -5100,7 +5105,7 @@ void PrintConfigDef::init_fff_params()
def = this->add("input_shaping_freq_y", coFloat);
def->label = L("Y");
def->tooltip = L("Resonant frequency for the Y axis input shaper.\nZero will use the firmware frequency.\nTo disable input shaping, use the Disable type.");
- def->sidetext = "Hz";
+ def->sidetext = L("Hz"); // Hertz, CIS languages need translation
def->min = 0;
def->max = 1000;
def->mode = comExpert;
@@ -7150,7 +7155,7 @@ void PrintConfigDef::init_fff_params()
def->label = L("Minimal");
def->tooltip = L("This is the chamber temperature at which printing should start, while the chamber continues heating "
"toward the \"Target\" chamber temperature. For example, set the Target to 60 and the Minimal to 50 to "
- "begin printing once the chamber reaches 50°C, without waiting for the full 60°C.\n\n"
+ "begin printing once the chamber reaches 50℃, without waiting for the full 60℃.\n\n"
"It sets a G-code variable named chamber_minimal_temperature, which can be passed to your print start macro "
"or a heat soak macro, like this: PRINT_START (other variables) CHAMBER_MIN_TEMP=[chamber_minimal_temperature].\n\n"
"Unlike the \"Target\" chamber temperature, this option does not emit any M141/M191 commands; it only exposes "
@@ -7827,6 +7832,30 @@ void PrintConfigDef::init_fff_params()
def->min = 0;
def->set_default_value(new ConfigOptionPercent(85));
+ def = this->add("filament_dev_ams_drying_ams_limitations", coStrings);
+ def->set_default_value(new ConfigOptionStrings{""});
+
+ def = this->add("filament_dev_ams_drying_temperature", coFloats);
+ def->set_default_value(new ConfigOptionFloats{0});
+
+ def = this->add("filament_dev_ams_drying_time", coFloats);
+ def->set_default_value(new ConfigOptionFloats{0});
+
+ def = this->add("filament_dev_ams_drying_heat_distortion_temperature", coFloats);
+ def->set_default_value(new ConfigOptionFloats{0});
+
+ def = this->add("filament_dev_chamber_drying_bed_temperature", coFloats);
+ def->set_default_value(new ConfigOptionFloats{0});
+
+ def = this->add("filament_dev_chamber_drying_time", coFloats);
+ def->set_default_value(new ConfigOptionFloats{0});
+
+ def = this->add("filament_dev_drying_softening_temperature", coFloats);
+ def->set_default_value(new ConfigOptionFloats{0});
+
+ def = this->add("filament_dev_drying_cooling_temperature", coFloats);
+ def->set_default_value(new ConfigOptionFloats{0});
+
// Declare retract values for filament profile, overriding the printer's extruder profile.
for (auto& opt_key : filament_extruder_override_keys) {
const std::string filament_prefix = "filament_";
@@ -9022,7 +9051,6 @@ void PrintConfigDef::handle_legacy(t_config_option_key &opt_key, std::string &va
"internal_bridge_support_thickness", "top_area_threshold", "reduce_wall_solid_infill","filament_load_time","filament_unload_time",
"smooth_coefficient", "overhang_totally_speed", "silent_mode",
"overhang_speed_classic", "filament_prime_volume",
- "calib_flowrate_topinfill_special_order",
"anisotropic_surfaces", // superseded by top_surface_fill_order / bottom_surface_fill_order
};
@@ -9262,6 +9290,17 @@ std::set printer_options_with_variant_2 = {
std::set empty_options;
+std::set filament_dev_options = {
+ "filament_dev_ams_drying_ams_limitations",
+ "filament_dev_ams_drying_temperature",
+ "filament_dev_ams_drying_time",
+ "filament_dev_ams_drying_heat_distortion_temperature",
+ "filament_dev_chamber_drying_bed_temperature",
+ "filament_dev_chamber_drying_time",
+ "filament_dev_drying_softening_temperature",
+ "filament_dev_drying_cooling_temperature"
+};
+
DynamicPrintConfig DynamicPrintConfig::full_print_config()
{
return DynamicPrintConfig((const PrintRegionConfig&)FullPrintConfig::defaults());
diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp
index b5f7005039..c830158700 100644
--- a/src/libslic3r/PrintConfig.hpp
+++ b/src/libslic3r/PrintConfig.hpp
@@ -840,6 +840,8 @@ extern std::set printer_options_with_variant_1;
extern std::set printer_options_with_variant_2;
extern std::set empty_options;
+extern std::set filament_dev_options;
+
extern void update_static_print_config_from_dynamic(ConfigBase& config, const DynamicPrintConfig& dest_config, std::vector variant_index, std::set& key_set1, int stride = 1);
extern void compute_filament_override_value(const std::string& opt_key, const ConfigOption *opt_old_machine, const ConfigOption *opt_new_machine, const ConfigOption *opt_new_filament, const DynamicPrintConfig& new_full_config,
t_config_option_keys& diff_keys, DynamicPrintConfig& filament_overrides, std::vector& f_map_indices);
@@ -1219,6 +1221,9 @@ PRINT_CONFIG_CLASS_DEFINE(
((ConfigOptionInt, interlocking_beam_layer_count))
((ConfigOptionInt, interlocking_depth))
((ConfigOptionInt, interlocking_boundary_avoidance))
+
+ // Orca: internal use only
+ ((ConfigOptionBool, calib_flowrate_topinfill_special_order)) // ORCA: special flag for flow rate calibration
)
// This object is mapped to Perl as Slic3r::Config::PrintRegion.
@@ -1678,6 +1683,16 @@ PRINT_CONFIG_CLASS_DEFINE(
// Printer flag: whether the printer offers the fast-purge mode selector.
// Default false; no shipping profile sets it, so the fast-purge UI stays hidden.
((ConfigOptionBool, support_fast_purge_mode))
+
+ //ams chamber
+ ((ConfigOptionStrings, filament_dev_ams_drying_ams_limitations))
+ ((ConfigOptionFloats, filament_dev_ams_drying_temperature))
+ ((ConfigOptionFloats, filament_dev_ams_drying_time))
+ ((ConfigOptionFloats, filament_dev_ams_drying_heat_distortion_temperature))
+ ((ConfigOptionFloats, filament_dev_chamber_drying_bed_temperature))
+ ((ConfigOptionFloats, filament_dev_chamber_drying_time))
+ ((ConfigOptionFloats, filament_dev_drying_softening_temperature))
+ ((ConfigOptionFloats, filament_dev_drying_cooling_temperature))
)
// This object is mapped to Perl as Slic3r::Config::Print.
diff --git a/src/slic3r/CMakeLists.txt b/src/slic3r/CMakeLists.txt
index 7c26f0e5b5..78bf91fd60 100644
--- a/src/slic3r/CMakeLists.txt
+++ b/src/slic3r/CMakeLists.txt
@@ -28,6 +28,8 @@ set(SLIC3R_GUI_SOURCES
GUI/AMSMaterialsSetting.hpp
GUI/AMSSetting.cpp
GUI/AMSSetting.hpp
+ GUI/AMSDryControl.cpp
+ GUI/AMSDryControl.hpp
GUI/AmsWidgets.cpp
GUI/AmsWidgets.hpp
GUI/Auxiliary.cpp
@@ -596,8 +598,6 @@ set(SLIC3R_GUI_SOURCES
GUI/wxExtensions.hpp
plugin/PythonInterpreter.cpp
plugin/PythonInterpreter.hpp
- plugin/PythonFileUtils.cpp
- plugin/PythonFileUtils.hpp
plugin/PythonPluginBridge.cpp
plugin/PythonPluginBridge.hpp
plugin/PythonPluginInterface.hpp
diff --git a/src/slic3r/GUI/3DScene.cpp b/src/slic3r/GUI/3DScene.cpp
index 84709dd13b..335dd6b825 100644
--- a/src/slic3r/GUI/3DScene.cpp
+++ b/src/slic3r/GUI/3DScene.cpp
@@ -495,7 +495,29 @@ void GLVolume::render_with_outline(const GUI::Size& cnv_size)
simple_render(shader, model_objects, colors);
return;
}
-
+ // 0th. render pass, render the model using stencil buffer
+ glsafe(::glEnable(GL_STENCIL_TEST));
+ glsafe(::glStencilMask(0xFF));
+ glsafe(::glStencilOp(GL_KEEP, GL_REPLACE, GL_REPLACE));
+ glsafe(::glClearStencil(0));
+ glsafe(::glClear(GL_STENCIL_BUFFER_BIT));
+ glsafe(::glStencilFunc(GL_ALWAYS, 0xFF, 0xFF));
+ if (tverts_range == std::make_pair(0, -1))
+ model.render(shader);
+ else
+ model.render(this->tverts_range, shader);
+ glsafe(::glStencilFunc(GL_NOTEQUAL, 0xFF, 0xFF));
+ glsafe(::glStencilMask(0x00));
+ shader->set_uniform("is_outline", true);
+ shader->set_uniform("screen_size", Vec2f{cnv_size.get_width(), cnv_size.get_height()});
+ if (tverts_range == std::make_pair(0, -1))
+ model.render(shader);
+ else
+ model.render(this->tverts_range, shader);
+ shader->set_uniform("is_outline", false);
+ glsafe(::glStencilMask(0xFF));
+ glsafe(::glDisable(GL_STENCIL_TEST));
+ // render the outline using depth buffer and discard the pixels that are not on the outline
// 1st. render pass, render the model into a separate render target that has only depth buffer
GLuint depth_fbo = 0;
GLuint depth_tex = 0;
diff --git a/src/slic3r/GUI/AMSDryControl.cpp b/src/slic3r/GUI/AMSDryControl.cpp
new file mode 100644
index 0000000000..9efe7faa87
--- /dev/null
+++ b/src/slic3r/GUI/AMSDryControl.cpp
@@ -0,0 +1,1864 @@
+#include "AMSDryControl.hpp"
+#include "slic3r/GUI/DeviceCore/DevFilaSystem.h"
+#include "GUI_App.hpp"
+#include "I18N.hpp"
+
+#include "slic3r/GUI/DeviceCore/DevExtruderSystem.h"
+
+#include "slic3r/GUI/DeviceCore/DevManager.h"
+
+#include "slic3r/GUI/MsgDialog.hpp"
+
+#include "slic3r/GUI/Widgets/AnimaController.hpp"
+#include "slic3r/GUI/Widgets/Label.hpp"
+#include "slic3r/GUI/Widgets/ComboBox.hpp"
+#include "slic3r/GUI/Widgets/ProgressBar.hpp"
+#include "slic3r/GUI/DeviceCore/DevUtilBackend.h"
+
+#include "libslic3r/PresetBundle.hpp"
+#include "libslic3r/Preset.hpp"
+
+#include
+
+namespace Slic3r { namespace GUI {
+
+const int AMS_DRY_STATUS_IMAGE_SIZE = 96;
+
+static std::string get_humidity_level_img_path(int humidity_percent)
+{
+ const int num_levels = 5;
+ int hum_level;
+
+ if (humidity_percent <= 100 / num_levels) {
+ hum_level = 5;
+ } else if (humidity_percent <= 100 / num_levels * 2) {
+ hum_level = 4;
+ } else if (humidity_percent <= 100 / num_levels * 3) {
+ hum_level = 3;
+ } else if (humidity_percent <= 100 / num_levels * 4) {
+ hum_level = 2;
+ } else {
+ hum_level = 1;
+ }
+
+ if (wxGetApp().dark_mode()) {
+ return "hum_level" + std::to_string(hum_level) + "_no_num_dark";
+ } else {
+ return "hum_level" + std::to_string(hum_level) + "_no_num_light";
+ }
+
+}
+
+static std::string get_dry_status_img_path(DevAmsType type, DevAms::DryStatus status, DevAms::DrySubStatus sub_status)
+{
+ std::string img_name = "dev_ams_dry_ctr_";
+ switch (type) {
+ case DevAmsType::N3S:
+ img_name += "n3s";
+ break;
+ case DevAmsType::N3F:
+ img_name += "n3f";
+ break;
+ default:
+ return "";
+ }
+
+ img_name += "_";
+ switch (status) {
+ case DevAms::DryStatus::Error:
+ img_name += "error";
+ return img_name;
+ }
+
+ switch (sub_status) {
+ case DevAms::DrySubStatus::Heating:
+ img_name += "heating";
+ return img_name;
+ case DevAms::DrySubStatus::Dehumidify:
+ img_name += "dehumidifying";
+ return img_name;
+ }
+
+ return "";
+}
+
+FilamentItemPanel::FilamentItemPanel(wxWindow* parent, const wxString& text, const std::string& icon_name, wxWindowID id)
+ : wxPanel(parent, id)
+ , m_icon_name(icon_name)
+{
+ SetBackgroundColour(wxColour("#F0F0F1")); // Light gray background
+ SetMinSize(wxSize(FromDIP(64), FromDIP(106))); // Width: 64, Height: 106
+ SetSize(wxSize(FromDIP(64), FromDIP(106))); // Fixed size
+
+ // Create sizer for vertical layout
+ wxBoxSizer* sizer = new wxBoxSizer(wxVERTICAL);
+
+ // Top section with text - moved to be closer to center
+ wxBoxSizer* top_sizer = new wxBoxSizer(wxVERTICAL);
+ top_sizer->AddStretchSpacer(5);
+
+ m_text_label = new Label(this, text);
+ m_text_label->SetForegroundColour(StateColor::darkModeColorFor(wxColour(*wxBLACK)));
+ m_text_label->SetBackgroundColour(StateColor::darkModeColorFor(wxColour("#F0F0F1")));
+ m_text_label->SetFont(Label::Body_12);
+ m_text_label->Wrap(FromDIP(40));
+ top_sizer->Add(m_text_label, 0, wxALIGN_CENTER_HORIZONTAL);
+
+ top_sizer->AddStretchSpacer(1); // Increased bottom spacer to push text down
+ sizer->Add(top_sizer, 1, wxEXPAND);
+
+ // Bottom section with icon - vertically centered in bottom half
+ wxBoxSizer* bottom_sizer = new wxBoxSizer(wxVERTICAL);
+ bottom_sizer->AddStretchSpacer(1);
+
+ m_icon_bitmap = new wxStaticBitmap(this, wxID_ANY, wxNullBitmap);
+ m_icon_bitmap->SetBackgroundColour(StateColor::darkModeColorFor(wxColour("#F0F0F1")));
+ m_icon_bitmap->SetMinSize(wxSize(FromDIP(24), FromDIP(24)));
+ m_icon_bitmap->SetMaxSize(wxSize(FromDIP(24), FromDIP(24)));
+ bottom_sizer->Add(m_icon_bitmap, 0, wxALIGN_CENTER_HORIZONTAL);
+
+ bottom_sizer->AddStretchSpacer(1);
+ sizer->Add(bottom_sizer, 1, wxEXPAND);
+
+ SetSizer(sizer);
+
+ // Bind events
+ Bind(wxEVT_PAINT, &FilamentItemPanel::OnPaint, this);
+ Bind(wxEVT_SIZE, &FilamentItemPanel::OnSize, this);
+
+ SetIcon(icon_name);
+ wxGetApp().UpdateDarkUI(this);
+}
+
+void FilamentItemPanel::SetText(const wxString& text)
+{
+ m_text_label->SetLabel(text);
+ Layout();
+}
+
+void FilamentItemPanel::SetIcon(const std::string& icon_name)
+{
+ m_icon_name = icon_name;
+ if (!icon_name.empty()) {
+ try {
+ m_icon = ScalableBitmap(this, icon_name, 20);
+ m_icon.msw_rescale();
+ m_icon_bitmap->SetBitmap(m_icon.bmp());
+ } catch (Exception&) {
+ BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": failed to load filament icon";
+ m_icon_bitmap->SetBitmap(wxNullBitmap);
+ }
+ } else {
+ m_icon_bitmap->SetBitmap(wxNullBitmap);
+ }
+ m_icon_bitmap->Refresh();
+}
+
+void FilamentItemPanel::msw_rescale()
+{
+ if (m_icon_bitmap && !m_icon_name.empty()) {
+ m_icon.msw_rescale();
+ m_icon_bitmap->SetBitmap(m_icon.bmp());
+ m_icon_bitmap->Refresh();
+ }
+ Layout();
+}
+
+void FilamentItemPanel::OnPaint(wxPaintEvent& event)
+{
+ wxPaintDC dc(this);
+ wxSize size = GetSize();
+
+ // bool is_dark_mode = wxGetApp().dark_mode();
+ wxColour backgroundColor = StateColor::darkModeColorFor(wxColour("#F0F0F1"));
+ wxColour borderColor = StateColor::darkModeColorFor(wxColour("#DBDBDB"));
+
+ // Draw white background rectangle with rounded corners inside the thick vertical lines
+ dc.SetBrush(wxBrush(backgroundColor));
+ dc.SetPen(wxPen(backgroundColor));
+ dc.DrawRectangle(FromDIP(6), FromDIP(12), size.GetWidth() - FromDIP(12), size.GetHeight() - 2 * FromDIP(12));
+
+ // Draw much thicker vertical rounded rectangles on left and right (3 times thicker)
+ dc.SetBrush(wxBrush(wxColour(borderColor)));
+ dc.SetPen(wxPen(wxColour(borderColor)));
+ dc.DrawRoundedRectangle(FromDIP(0), FromDIP(0), FromDIP(6), size.GetHeight(), FromDIP(6)); // Left rounded rectangle
+ dc.DrawRoundedRectangle(size.GetWidth() - FromDIP(6), FromDIP(0), FromDIP(6), size.GetHeight(), FromDIP(6)); // Right rounded rectangle
+
+ // Draw thin horizontal rounded rectangles on top and bottom (moved closer to center by half the distance)
+ dc.SetBrush(wxBrush(wxColour(borderColor)));
+ dc.SetPen(wxPen(wxColour(borderColor)));
+ dc.DrawRoundedRectangle(FromDIP(6), FromDIP(12), size.GetWidth() - FromDIP(12), FromDIP(3), FromDIP(1)); // Top rounded rectangle
+ dc.DrawRoundedRectangle(FromDIP(6), size.GetHeight() - FromDIP(13), size.GetWidth() - FromDIP(12), FromDIP(3), FromDIP(1)); // Bottom rounded rectangle
+}
+
+void FilamentItemPanel::OnSize(wxSizeEvent& event)
+{
+ Refresh();
+ event.Skip();
+}
+
+// class AMSFilamentPanel
+
+AMSFilamentPanel::AMSFilamentPanel(wxWindow* parent, const wxString& ams_name, wxWindowID id)
+ : wxPanel(parent, id)
+{
+ SetBackgroundColour(wxColour("#DBDBDB"));
+
+ wxBoxSizer* main_sizer = new wxBoxSizer(wxVERTICAL);
+
+ // Filament items section
+ m_filament_container = new wxPanel(this);
+ m_filament_container->SetBackgroundColour(wxColour("#F0F0F1"));
+ m_filament_sizer = new wxBoxSizer(wxHORIZONTAL);
+ m_filament_container->SetSizer(m_filament_sizer);
+
+ // AMS name section
+ m_ams_name_label = new Label(this, ams_name);
+ m_ams_name_label->SetForegroundColour(wxColour("#858585"));
+ m_ams_name_label->SetFont(Label::Body_14);
+ m_ams_name_label->SetBackgroundColour(wxColour("#DBDBDB"));
+
+ main_sizer->Add(m_filament_container, 1, wxEXPAND | wxALL, 0);
+ main_sizer->Add(m_ams_name_label, 0, wxALIGN_LEFT | wxALL, FromDIP(5));
+
+ SetSizer(main_sizer);
+ wxGetApp().UpdateDarkUI(this);
+}
+
+void AMSFilamentPanel::AddFilamentItem(FilamentItemPanel* panel)
+{
+ if (panel) {
+ panel->Reparent(m_filament_container);
+ m_filament_sizer->Add(panel, 0, wxALL, FromDIP(5));
+ m_filament_items.push_back(panel);
+ Layout();
+ }
+}
+
+void AMSFilamentPanel::AddFilamentItem(const wxString& text, const std::string& icon_name)
+{
+ FilamentItemPanel* item = new FilamentItemPanel(m_filament_container, text, icon_name);
+ m_filament_items.push_back(item);
+ m_filament_sizer->Add(item, 0, wxALL, FromDIP(5));
+ Layout();
+}
+
+void AMSFilamentPanel::SetAmsName(const wxString& ams_name)
+{
+ m_ams_name_label->SetLabel(ams_name);
+ Layout();
+}
+
+void AMSFilamentPanel::Clear()
+{
+ m_filament_sizer->Clear(true);
+ m_filament_items.clear();
+}
+
+void AMSFilamentPanel::msw_rescale()
+{
+ for (auto item : m_filament_items) {
+ item->msw_rescale();
+ }
+ Layout();
+}
+
+
+AMSDryCtrWin::AMSDryCtrWin(wxWindow *parent)
+ :DPIDialog(parent, wxID_ANY, _L("AMS Dryness Control"), wxDefaultPosition, wxDefaultSize, wxCAPTION | wxCLOSE_BOX)
+{
+ create();
+}
+
+AMSDryCtrWin::~AMSDryCtrWin()
+{
+ if (m_progress_timer) {
+ delete m_progress_timer;
+ }
+}
+
+
+wxScrolledWindow* AMSDryCtrWin::create_preview_scrolled_window(wxWindow* parent)
+{
+ wxScrolledWindow* panel = new wxScrolledWindow(parent, wxID_ANY);
+ panel->SetScrollRate(10, 0);
+ panel->SetSize(AMS_ITEMS_PANEL_SIZE);
+ panel->SetMinSize(AMS_ITEMS_PANEL_SIZE);
+ panel->SetBackgroundColour(AMS_CONTROL_DEF_BLOCK_BK_COLOUR);
+
+ return panel;
+}
+
+wxBoxSizer* AMSDryCtrWin::create_humidity_status_section(wxPanel* parent)
+{
+ wxBoxSizer* image_sizer = new wxBoxSizer(wxVERTICAL);
+
+ m_humidity_img = new wxStaticBitmap(parent, wxID_ANY, wxNullBitmap);
+ image_sizer->Add(m_humidity_img, 1, wxALIGN_CENTER_HORIZONTAL, 0);
+
+ m_humidity_img->SetBitmap(wxNullBitmap);
+
+ // Create a horizontal sizer for description and icon
+ wxBoxSizer* desc_sizer = new wxBoxSizer(wxHORIZONTAL);
+
+ m_image_description_icon = new wxStaticBitmap(parent, wxID_ANY, wxNullBitmap);
+ m_image_description_icon->SetBackgroundColour(*wxWHITE);
+ m_image_description_icon->SetMinSize(wxSize(FromDIP(20), FromDIP(20)));
+ m_image_description_icon->SetMaxSize(wxSize(FromDIP(20), FromDIP(20)));
+ m_image_description_icon->Show(false);
+ desc_sizer->Add(m_image_description_icon, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(5));
+
+ m_image_description = new Label(parent, _L("Idle"));
+ m_image_description->SetFont(Label::Head_14);
+ m_image_description->SetForegroundColour(*wxBLACK);
+ m_image_description->SetBackgroundColour(*wxWHITE);
+ desc_sizer->Add(m_image_description, 0, wxALIGN_CENTER_VERTICAL);
+
+ image_sizer->Add(desc_sizer, 0, wxALIGN_CENTER | wxALL, FromDIP(5));
+
+ return image_sizer;
+}
+
+wxBoxSizer* AMSDryCtrWin::create_description_item(wxPanel* parent, const wxString& title, Label*& dataLabel)
+{
+ wxBoxSizer* item_sizer = new wxBoxSizer(wxVERTICAL);
+
+ Label* titleLabel = new Label(parent, title);
+ titleLabel->SetForegroundColour(*wxBLACK);
+ titleLabel->SetFont(Label::Body_16);
+
+ dataLabel = new Label(parent, wxT("--"));
+ dataLabel->SetForegroundColour(*wxBLACK);
+ dataLabel->SetFont(Label::Body_14);
+
+ item_sizer->AddStretchSpacer();
+ item_sizer->Add(titleLabel, 0, wxALIGN_CENTER | wxALL, FromDIP(2));
+ item_sizer->Add(dataLabel, 0, wxALIGN_CENTER | wxALL, FromDIP(2));
+ item_sizer->AddStretchSpacer();
+
+ return item_sizer;
+}
+
+wxBoxSizer* AMSDryCtrWin::create_status_descriptions_section(wxPanel* parent)
+{
+ wxBoxSizer* desc_sizer = new wxBoxSizer(wxHORIZONTAL);
+
+ wxBoxSizer* hum_desc_sizer = create_description_item(parent, _L("Humidity"), m_humidity_data_label);
+ desc_sizer->Add(hum_desc_sizer, 1, wxEXPAND, FromDIP(1));
+
+ wxStaticLine* vert_separator = new wxStaticLine(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_VERTICAL);
+ desc_sizer->Add(vert_separator, 0, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(5));
+
+ wxBoxSizer* temp_desc_sizer = create_description_item(parent, _L("Temperature"), m_temperature_data_label);
+ desc_sizer->Add(temp_desc_sizer, 1, wxEXPAND, FromDIP(1));
+
+ m_time_descrition_container = new wxBoxSizer(wxHORIZONTAL);
+ wxStaticLine* vert_separator2 = new wxStaticLine(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_VERTICAL);
+ m_time_descrition_container->Add(vert_separator2, 0, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(5));
+
+ wxBoxSizer* time_desc_sizer = create_description_item(parent, _L("Left Time"), m_time_data_label);
+ m_time_descrition_container->Add(time_desc_sizer, 1, wxEXPAND, FromDIP(1));
+
+ desc_sizer->Add(m_time_descrition_container, 1, wxEXPAND, 0);
+ // m_time_descrition_container->Show(false);
+
+ return desc_sizer;
+}
+
+wxBoxSizer* AMSDryCtrWin::create_left_panel(wxPanel* parent)
+{
+ wxBoxSizer* left_sizer = new wxBoxSizer(wxVERTICAL);
+
+ wxBoxSizer* image_section = create_humidity_status_section(parent);
+ left_sizer->Add(image_section, 1, wxEXPAND | wxALL, FromDIP(5));
+
+ wxBoxSizer* descriptions_section = create_status_descriptions_section(parent);
+ left_sizer->Add(descriptions_section, 0, wxEXPAND | wxALL, FromDIP(20));
+
+ return left_sizer;
+}
+
+Button* AMSDryCtrWin::create_button(wxPanel* parent, const wxString& title,
+ const wxColour& background_color, const wxColour& border_color, const wxColour& text_color)
+{
+ Button* button = new Button(parent, title);
+
+ // Create state colors for background
+ StateColor bg_color(
+ std::pair(AMS_CONTROL_DISABLE_COLOUR, StateColor::Disabled),
+ std::pair(background_color.ChangeLightness(80), StateColor::Pressed),
+ std::pair(background_color.ChangeLightness(120), StateColor::Hovered),
+ std::pair(background_color, StateColor::Normal)
+ );
+
+ // Create state colors for border
+ StateColor bd_color(
+ std::pair(AMS_CONTROL_WHITE_COLOUR, StateColor::Disabled),
+ std::pair(border_color, StateColor::Enabled)
+ );
+
+ button->SetBackgroundColor(bg_color);
+ button->SetBorderColor(bd_color);
+ button->SetTextColor(text_color);
+ button->SetFont(Label::Body_14);
+
+ // Auto-size button based on text content with padding
+ wxSize best_size = button->GetBestSize();
+ int padding_width = FromDIP(4);
+ int padding_height = FromDIP(2);
+ wxSize final_size(best_size.GetWidth() + padding_width, best_size.GetHeight() + padding_height);
+ button->SetMinSize(final_size);
+
+ return button;
+}
+
+wxBoxSizer* AMSDryCtrWin::create_normal_state_panel(wxPanel* parent)
+{
+ wxBoxSizer* normal_state_sizer = new wxBoxSizer(wxVERTICAL);
+
+ Label* description_label = new Label(parent, _L("Filament Drying Settings"));
+ description_label->SetForegroundColour(*wxBLACK);
+ description_label->SetFont(Label::Head_14);
+ normal_state_sizer->Add(description_label, 0, wxALL, FromDIP(5));
+
+ // Part 2: ComboBox for material selection
+ wxBoxSizer* combo_sizer = new wxBoxSizer(wxHORIZONTAL);
+ m_trays_combo = new ComboBox(parent, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize(-1, FromDIP(26)), 0, nullptr, wxCB_READONLY);
+ m_trays_combo->SetFont(Label::Body_14);
+
+ combo_sizer->Add(m_trays_combo, 1, wxEXPAND);
+ normal_state_sizer->Add(combo_sizer, 0, wxEXPAND | wxALL, FromDIP(5));
+
+ m_trays_combo->Bind(wxEVT_COMBOBOX, &AMSDryCtrWin::OnFilamentSelectionChanged, this);
+
+ // part 3 time and temperature
+ // Temperature part
+ wxBoxSizer* temp_sizer = new wxBoxSizer(wxHORIZONTAL);
+ m_temperature_input = new wxTextCtrl(parent, wxID_ANY, "", wxDefaultPosition, wxSize(FromDIP(80), -1));
+ m_temperature_input->SetMaxLength(3); // Limit to 3 digits
+
+ m_temperature_input->Bind(wxEVT_CHAR, [this](wxKeyEvent& event) {
+ int keycode = event.GetKeyCode();
+ if (keycode >= '0' && keycode <= '9') {
+ event.Skip();
+ } else if (keycode == WXK_BACK || keycode == WXK_DELETE || keycode == WXK_LEFT || keycode == WXK_RIGHT) {
+ event.Skip();
+ }
+ });
+
+ m_temperature_input->Bind(wxEVT_TEXT, [this](wxCommandEvent&) {
+ m_next_button->Disable();
+ m_start_button->Disable();
+ });
+
+ m_temperature_input->SetBackgroundColour(StateColor::darkModeColorFor(*wxWHITE));
+ m_temperature_input->SetForegroundColour(StateColor::darkModeColorFor(*wxBLACK));
+
+ Label* temp_unit_label = new Label(parent, wxString::FromUTF8("℃"));
+ temp_unit_label->SetForegroundColour(*wxBLACK);
+ temp_sizer->Add(m_temperature_input, 1, wxRIGHT, FromDIP(1));
+ temp_sizer->Add(temp_unit_label, 0, wxALIGN_CENTER_VERTICAL);
+
+ // Time part
+ wxBoxSizer* time_sizer = new wxBoxSizer(wxHORIZONTAL);
+ m_time_input = new wxTextCtrl(parent, wxID_ANY, "", wxDefaultPosition, wxSize(FromDIP(100), -1));
+ m_time_input->SetMaxLength(3); // Limit to 3 digits
+
+ m_time_input->Bind(wxEVT_CHAR, [this](wxKeyEvent& event) {
+ int keycode = event.GetKeyCode();
+ if (keycode >= '0' && keycode <= '9') {
+ event.Skip();
+ } else if (keycode == WXK_BACK || keycode == WXK_DELETE || keycode == WXK_LEFT || keycode == WXK_RIGHT) {
+ event.Skip();
+ }
+ });
+
+ m_time_input->Bind(wxEVT_TEXT, [this](wxCommandEvent&) {
+ m_next_button->Disable();
+ m_start_button->Disable();
+ });
+
+ m_time_input->SetBackgroundColour(StateColor::darkModeColorFor(*wxWHITE));
+ m_time_input->SetForegroundColour(StateColor::darkModeColorFor(*wxBLACK));
+
+ Label* time_unit_label = new Label(parent, "H");
+ time_unit_label->SetForegroundColour(*wxBLACK);
+ time_sizer->Add(m_time_input, 1, wxRIGHT, FromDIP(1));
+ time_sizer->Add(time_unit_label, 0, wxALIGN_CENTER_VERTICAL);
+
+ wxBoxSizer* input_sizer = new wxBoxSizer(wxHORIZONTAL);
+ input_sizer->Add(temp_sizer, 1, wxRIGHT, FromDIP(10));
+ input_sizer->Add(time_sizer, 1, 0);
+ normal_state_sizer->Add(input_sizer, 0, wxEXPAND | wxALL, FromDIP(5));
+
+ // Part 4: Abnormal description/message area
+ m_normal_description = new Label(parent, "");
+ m_normal_description->SetForegroundColour(wxColour("#F09A17"));
+ m_normal_description->SetFont(Label::Body_12);
+ normal_state_sizer->Add(m_normal_description, 0, wxALL, FromDIP(5));
+
+ // Part 5: Start button
+ m_next_button = create_button(
+ parent,
+ _L("Start"),
+ AMS_CONTROL_BRAND_COLOUR, // Background color - green
+ AMS_CONTROL_BRAND_COLOUR, // Border color - green
+ wxColour("#FFFFFE") // Text color - white
+ );
+
+ m_next_button->Bind(wxEVT_COMMAND_BUTTON_CLICKED, [this](wxCommandEvent& event) {
+ m_main_simplebook->SetSelection(1);
+ });
+
+ normal_state_sizer->Add(m_next_button, 0, wxALL, FromDIP(5));
+ m_next_button->Disable();
+
+ m_stop_button = create_button(
+ parent,
+ _L("Stop"),
+ wxColour(255, 0, 0), // Background color - red
+ wxColour(255, 0, 0), // Border color - red
+ wxColour("#FFFFFE") // Text color - white
+ );
+
+ m_stop_button->Bind(wxEVT_COMMAND_BUTTON_CLICKED, [this](wxCommandEvent& event) {
+ auto fila_system = get_fila_system();
+ if (!fila_system) {
+ BOOST_LOG_TRIVIAL(info) << "AMSDryCtrWin::start_sending_drying_command: Invalid FilaSystem Pointer";
+ return;
+ }
+
+ fila_system->CtrlAmsStopDrying(std::stoi(m_ams_info.m_ams_id));
+ // Temporarily show stopping state, then restore after 2 seconds
+ if (m_stop_button) {
+ m_stop_button->SetLabel(_L("Stopping"));
+ update_button_size(m_stop_button); // Adjust button size for longer text
+ m_stop_button->Disable();
+ m_stop_button->Layout();
+ m_stop_button->GetParent()->Layout(); // Relayout parent container
+ m_stop_button->Refresh();
+
+ m_stop_button_restore_deadline.reset();
+ m_stop_button_restore_deadline = std::chrono::steady_clock::now() + std::chrono::seconds(2);
+ }
+ });
+
+ m_stop_button->Show(false);
+ normal_state_sizer->Add(m_stop_button, 0, wxALL, FromDIP(5));
+
+ return normal_state_sizer;
+}
+
+wxBoxSizer* AMSDryCtrWin::create_cannot_dry_panel(wxPanel* parent)
+{
+ wxBoxSizer* abnormal_sizer = new wxBoxSizer(wxVERTICAL);
+
+ Label* description_label = new Label(parent, _L("Unable to dry temporarily due to ..."));
+ description_label->SetForegroundColour(*wxBLACK);
+ description_label->SetFont(Label::Head_16);
+ description_label->Wrap(FromDIP(300));
+ abnormal_sizer->Add(description_label, 0, wxALIGN_CENTER_VERTICAL, FromDIP(10));
+
+ // Add a description label for the abnormal state
+ m_cannot_dry_description_label = new Label(parent, (""));
+ m_cannot_dry_description_label->SetForegroundColour(*wxBLACK);
+ m_cannot_dry_description_label->SetFont(Label::Body_14);
+ m_cannot_dry_description_label->Wrap(FromDIP(250)); // Wrap text to fit within panel
+
+ abnormal_sizer->Add(m_cannot_dry_description_label, 0, wxALIGN_CENTER_VERTICAL, FromDIP(10));
+
+ // Unload button shown only when ConsumableAtAmsOutlet reason is active
+ m_unload_button = create_button(
+ parent,
+ _L("Unload"),
+ AMS_CONTROL_BRAND_COLOUR, // Background color - light gray
+ AMS_CONTROL_BRAND_COLOUR, // Border color - gray
+ *wxWHITE // Text color - white
+ );
+
+ m_unload_button->Bind(wxEVT_COMMAND_BUTTON_CLICKED, [this](wxCommandEvent& event) {
+ auto fila_system = get_fila_system();
+ if (!fila_system) {
+ BOOST_LOG_TRIVIAL(info) << "AMSDryCtrWin::unload_button: Invalid FilaSystem Pointer";
+ return;
+ }
+
+ MachineObject* obj = fila_system->GetOwner();
+ if (!obj) {
+ BOOST_LOG_TRIVIAL(info) << "AMSDryCtrWin::unload_button: Invalid MachineObject Pointer";
+ return;
+ }
+
+ obj->command_ams_change_filament(false, m_ams_info.m_ams_id, "255");
+
+ m_unload_button->Disable();
+ m_unload_button_restore_deadline.reset();
+ m_unload_button_restore_deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5);
+ });
+
+ m_unload_button->Show(false);
+ abnormal_sizer->Add(m_unload_button, 0, wxALL, FromDIP(5));
+
+ return abnormal_sizer;
+}
+
+wxBoxSizer* AMSDryCtrWin::create_drying_error_panel(wxPanel* parent)
+{
+ wxBoxSizer* err_sizer = new wxBoxSizer(wxVERTICAL);
+ Label* description_label = new Label(parent, _L("Drying Error"));
+ description_label->SetForegroundColour(*wxRED);
+ description_label->SetFont(Label::Body_14); // Wrap text to fit within panel
+ err_sizer->Add(description_label, 0, wxALIGN_CENTER_VERTICAL, FromDIP(10));
+
+ Label* additional_description_label = new Label(parent, _L("Please check the Assistant for troubleshooting"));
+ additional_description_label->SetForegroundColour(*wxBLACK);
+ additional_description_label->SetFont(Label::Body_14); // Wrap text to fit within panel
+ err_sizer->Add(additional_description_label, 0, wxALIGN_CENTER_VERTICAL, FromDIP(10));
+
+ return err_sizer;
+}
+
+wxBoxSizer* AMSDryCtrWin::create_right_panel(wxPanel* parent)
+{
+ wxBoxSizer* right_sizer = new wxBoxSizer(wxHORIZONTAL);
+
+ m_normal_state_sizer = create_normal_state_panel(parent);
+ m_cannot_dry_sizer = create_cannot_dry_panel(parent);
+ m_dry_error_sizer = create_drying_error_panel(parent);
+
+ right_sizer->Add(m_normal_state_sizer, 1, wxEXPAND);
+ right_sizer->Add(m_cannot_dry_sizer, 1, wxEXPAND);
+
+ m_cannot_dry_sizer->Show(false);
+ m_dry_error_sizer->Show(false);
+
+ return right_sizer;
+}
+
+wxBoxSizer* AMSDryCtrWin::create_main_content_section(wxPanel* parent)
+{
+ wxBoxSizer* content_sizer = new wxBoxSizer(wxHORIZONTAL);
+
+ wxBoxSizer* left_panel = create_left_panel(parent);
+ content_sizer->Add(left_panel, 2, wxALIGN_CENTER_VERTICAL | wxALL, FromDIP(5));
+ left_panel->SetMinSize(wxSize(FromDIP(250), -1));
+
+ wxBoxSizer* right_panel = create_right_panel(parent);
+ content_sizer->Add(right_panel, 1, wxALIGN_CENTER_VERTICAL | wxALL, FromDIP(5));
+ right_panel->SetMinSize(wxSize(FromDIP(250), -1));
+
+ return content_sizer;
+}
+
+wxBoxSizer* AMSDryCtrWin::create_main_page_sizer(wxPanel* parent)
+{
+ wxBoxSizer* main_sizer = new wxBoxSizer(wxVERTICAL);
+
+ wxBoxSizer* content_section = create_main_content_section(parent);
+ main_sizer->Add(content_section, 1, wxEXPAND | wxALL, FromDIP(5));
+
+ return main_sizer;
+}
+
+wxBoxSizer* AMSDryCtrWin::create_guide_info_filament(wxPanel* parent)
+{
+ wxBoxSizer* filament_section = new wxBoxSizer(wxHORIZONTAL);
+
+ m_ams_filament_panel = new AMSFilamentPanel(parent, "");
+
+ filament_section->Add(m_ams_filament_panel, 0, wxEXPAND | wxALL | wxALIGN_CENTER_HORIZONTAL, FromDIP(5));
+
+ return filament_section;
+}
+
+wxBoxSizer* AMSDryCtrWin::create_guide_info_section(wxPanel* parent)
+{
+ wxBoxSizer* info_section = new wxBoxSizer(wxVERTICAL);
+
+ // Part 1: Title
+ m_guide_title_label = new Label(parent, _L("Please remove and store the filament (as shown)."));
+ m_guide_title_label->SetForegroundColour(*wxBLACK);
+ m_guide_title_label->SetFont(Label::Head_18);
+ info_section->Add(m_guide_title_label, 0, wxEXPAND | wxALL, FromDIP(5));
+
+ // Part 2: Description
+ m_guide_description_label = new Label(parent, _L("The AMS can rotate the filament which is properly stored, providing better drying results."));
+ m_guide_description_label->SetForegroundColour(*wxBLACK);
+ m_guide_description_label->SetFont(Label::Body_14);
+ m_guide_description_label->Wrap(FromDIP(300)); // Wrap text to fit within panel
+ info_section->Add(m_guide_description_label, 0, wxEXPAND | wxALL, FromDIP(5));
+
+ // Part 3: filament panel
+ wxBoxSizer* fila_section = create_guide_info_filament(parent);
+ info_section->Add(fila_section, 0, wxEXPAND | wxALL, FromDIP(5));
+
+ // Part 4: Circular toggle with description
+ wxBoxSizer* toggle_section = new wxBoxSizer(wxHORIZONTAL);
+
+ m_rotate_spool_toggle = new wxCheckBox(parent, wxID_ANY, "");
+ m_rotate_spool_toggle->SetValue(false);
+
+ m_rotate_spool_toggle->Bind(wxEVT_CHECKBOX, [this](wxCommandEvent& event) {
+ bool is_checked = event.IsChecked();
+ // Add toggle behavior logic here
+ });
+
+ toggle_section->Add(m_rotate_spool_toggle, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(10));
+
+ // Toggle description
+ Label* toggle_description = new Label(parent, _L("Rotate spool when drying"), LB_AUTO_WRAP);
+ toggle_description->SetForegroundColour(*wxBLACK);
+ toggle_description->SetFont(Label::Body_12);
+ toggle_section->Add(toggle_description, 1, wxALIGN_CENTER_VERTICAL | wxEXPAND, 0);
+
+ info_section->Add(toggle_section, 0, wxEXPAND | wxALL, FromDIP(5));
+
+ return info_section;
+}
+
+wxBoxSizer* AMSDryCtrWin::create_guide_right_section(wxPanel* parent)
+{
+ wxBoxSizer* right_section = new wxBoxSizer(wxVERTICAL);
+
+ // Upper part: Image section
+ m_image_placeholder = new wxStaticBitmap(parent, wxID_ANY, wxNullBitmap);
+ m_guide_image = ScalableBitmap(parent, "dev_ams_dry_ctr_filament_in_chamber", 256);
+ m_image_placeholder->SetBitmap(m_guide_image.bmp());
+
+ wxBoxSizer* image_container = new wxBoxSizer(wxHORIZONTAL);
+ image_container->AddStretchSpacer(1);
+ image_container->Add(m_image_placeholder, 0, wxALL, FromDIP(5));
+
+ right_section->Add(image_container, 0, wxEXPAND | wxALL, FromDIP(5));
+
+ wxBoxSizer* buttons_container = new wxBoxSizer(wxHORIZONTAL);
+ buttons_container->AddStretchSpacer(1);
+
+ m_back_button = create_button(
+ parent,
+ wxString::FromUTF8(_CTX_utf8(L_CONTEXT("Back", "amsdrying"), "amsdrying")),
+ wxColour("#F8F8F8"), // Background color - light gray
+ wxColour("#D0D0D0"), // Border color - gray
+ *wxBLACK // Text color - black
+ );
+
+ m_back_button->Bind(wxEVT_COMMAND_BUTTON_CLICKED, [this](wxCommandEvent& event) {
+ m_main_simplebook->SetSelection(0);
+ });
+
+ buttons_container->Add(m_back_button, 0, wxALL, FromDIP(5));
+
+ m_start_button = create_button(
+ parent,
+ _L("Start"),
+ AMS_CONTROL_BRAND_COLOUR, // Background color - green
+ AMS_CONTROL_BRAND_COLOUR, // Border color - green
+ wxColour("#FFFFFE") // Text color - white
+ );
+
+ m_start_button->Bind(wxEVT_COMMAND_BUTTON_CLICKED, [this](wxCommandEvent& event) {
+ m_main_simplebook->SetSelection(2);
+ m_progress_gauge->SetValue(0);
+
+ m_progress_timer->Start(70);
+ m_progress_value = 0;
+ start_sending_drying_command();
+ });
+
+ buttons_container->Add(m_start_button, 0, wxALL, FromDIP(5));
+
+ right_section->Add(buttons_container, 0, wxEXPAND | wxALL, FromDIP(5));
+
+ return right_section;
+}
+
+wxBoxSizer* AMSDryCtrWin::create_guide_page_sizer(wxPanel* parent)
+{
+ wxBoxSizer* guide_sizer = new wxBoxSizer(wxHORIZONTAL);
+
+ wxBoxSizer* info_section = create_guide_info_section(parent);
+ guide_sizer->Add(info_section, 1, wxEXPAND | wxALL, FromDIP(10));
+
+ wxBoxSizer* right_section = create_guide_right_section(parent);
+ guide_sizer->Add(right_section, 0, wxEXPAND | wxALL, FromDIP(10));
+
+ return guide_sizer;
+}
+
+void AMSDryCtrWin::start_sending_drying_command()
+{
+ auto fila_system = get_fila_system();
+ if (!fila_system) {
+ BOOST_LOG_TRIVIAL(info) << "AMSDryCtrWin::start_sending_drying_command: Invalid FilaSystem Pointer";
+ return;
+ }
+
+ if (m_temperature_input->GetValue().IsEmpty() || m_time_input->GetValue().IsEmpty()) {
+ // Show error message to user
+ BOOST_LOG_TRIVIAL(info) << "AMSDryCtrWin::start_sending_drying_command: Time or temperature is empty";
+ return;
+ }
+
+ long temperature, time;
+ if (!m_temperature_input->GetValue().ToLong(&temperature) ||
+ !m_time_input->GetValue().ToLong(&time)) {
+ BOOST_LOG_TRIVIAL(info) << "AMSDryCtrWin::start_sending_drying_command: Failed to convert temperature or time";
+ return;
+ }
+
+ int tray_index;
+ tray_index = m_trays_combo->GetSelection();
+ if (m_tray_ids.empty() || tray_index < 0 || tray_index >= m_tray_ids.size()) {
+ BOOST_LOG_TRIVIAL(warning) << "AMSDryCtrWin::start_sending_drying_command: Invalid tray_index " << tray_index
+ << ", m_tray_ids.size=" << m_tray_ids.size();
+ return;
+ }
+
+ int cooling_temp = 50;
+ std::optional preset = DevUtilBackend::GetFilamentDryingPreset(m_tray_ids[tray_index].filament_id);
+ if (preset.has_value()) {
+ cooling_temp = static_cast(preset.value().filament_dev_drying_softening_temperature);
+ }
+
+ fila_system->CtrlAmsStartDryingHour(std::stoi(m_ams_info.m_ams_id), m_tray_ids[tray_index].filament_type,
+ temperature, time, m_rotate_spool_toggle->GetValue(), cooling_temp, false);
+
+ m_dry_setting.m_filament_names[m_ams_info.m_ams_id] = m_tray_ids[tray_index].filament_name;
+ m_dry_setting.m_filament_type[m_ams_info.m_ams_id] = m_tray_ids[tray_index].filament_type;
+ m_dry_setting.m_dry_temp[m_ams_info.m_ams_id] = temperature;
+ m_dry_setting.m_dry_time[m_ams_info.m_ams_id] = time;
+}
+
+void AMSDryCtrWin::OnProgressTimer(wxTimerEvent& event)
+{
+ m_progress_value += 1;
+
+ if (m_progress_value <= 100)
+ m_progress_gauge->SetValue(m_progress_value);
+
+ if (m_progress_value == 1) { // First tick, reset message index
+ m_progress_message_index = 0;
+ if (!m_progress_text.empty()) {
+ m_progress_title->SetLabel(m_progress_text[0]);
+ }
+ }
+ else if (m_progress_value % 15 == 0) { // Approximately every second
+ m_progress_message_index++;
+ if (m_progress_message_index < m_progress_text.size()) {
+ m_progress_title->SetLabel(m_progress_text[m_progress_message_index]);
+ m_progress_title->Refresh();
+ }
+ }
+
+ if (m_progress_value >= 100 && !is_dry_ctr_idle()) {
+ m_progress_value = 0;
+ m_progress_timer->Stop();
+ m_main_simplebook->SetSelection(0);
+ }
+}
+
+void AMSDryCtrWin::restore_stop_button_if_deadline_passed()
+{
+ if (m_stop_button_restore_deadline.has_value()) {
+ if (std::chrono::steady_clock::now() >= m_stop_button_restore_deadline.value()) {
+ if (m_stop_button) {
+ m_stop_button->SetLabel(_L("Stop"));
+ update_button_size(m_stop_button); // Adjust button size back to original
+ m_stop_button->Enable();
+ m_stop_button->Layout();
+ m_stop_button->GetParent()->Layout();
+ m_stop_button->Refresh();
+ }
+ m_stop_button_restore_deadline.reset();
+ }
+ }
+}
+
+void AMSDryCtrWin::restore_unload_button_if_deadline_passed()
+{
+ if (m_unload_button_restore_deadline.has_value()) {
+ if (std::chrono::steady_clock::now() >= m_unload_button_restore_deadline.value()) {
+ if (m_unload_button) {
+ m_unload_button->Enable();
+ }
+ m_unload_button_restore_deadline.reset();
+ }
+ }
+}
+
+void AMSDryCtrWin::update_button_size(Button* button)
+{
+ if (!button) return;
+
+ // Reset MinSize first to avoid accumulation of size
+ button->SetMinSize(wxSize(-1, -1));
+
+ // Recalculate button size based on current text and DPI settings
+ wxSize best_size = button->GetBestSize();
+ int padding_width = FromDIP(4);
+ int padding_height = FromDIP(2);
+ wxSize final_size(best_size.GetWidth() + padding_width, best_size.GetHeight() + padding_height);
+ button->SetMinSize(final_size);
+}
+
+void AMSDryCtrWin::OnShow(wxShowEvent& event)
+{
+ if (event.IsShown()) {
+ wxGetApp().UpdateDlgDarkUI(this);
+ msw_rescale();
+ }
+ event.Skip();
+}
+
+void AMSDryCtrWin::OnClose(wxCloseEvent& event)
+{
+ if (m_progress_timer && m_progress_timer->IsRunning()) {
+ m_progress_timer->Stop();
+ }
+
+ if (m_progress_gauge) {
+ m_progress_gauge->SetValue(0);
+ }
+
+ m_progress_value = 0;
+ m_progress_message_index = 0;
+
+ if (m_main_simplebook) {
+ m_main_simplebook->SetSelection(0);
+ }
+
+ if (m_progress_title && !m_progress_text.empty()) {
+ m_progress_title->SetLabel(m_progress_text[0]);
+ }
+
+ // Clean up and restore stop button state
+ if (m_stop_button) {
+ m_stop_button->SetLabel(_L("Stop"));
+ update_button_size(m_stop_button); // Adjust button size back to original
+ m_stop_button->Enable();
+ m_stop_button->Layout();
+ m_stop_button->GetParent()->Layout();
+ m_stop_button->Refresh();
+ }
+ m_stop_button_restore_deadline.reset();
+
+ // Clean up unload button state
+ if (m_unload_button) {
+ m_unload_button->Enable();
+ }
+ m_unload_button_restore_deadline.reset();
+
+ event.Skip();
+}
+
+wxBoxSizer* AMSDryCtrWin::create_progress_page_sizer(wxPanel* parent)
+{
+ wxBoxSizer* progress_sizer = new wxBoxSizer(wxVERTICAL);
+
+ m_progress_title = new Label(parent, m_progress_text[0]);
+ m_progress_title->SetForegroundColour(*wxBLACK);
+ m_progress_title->SetFont(Label::Body_16);
+
+ progress_sizer->Add(0, 0, 1, wxEXPAND, 0); // Spacer
+ progress_sizer->Add(m_progress_title, 0, wxALIGN_CENTER | wxALL, FromDIP(30));
+
+ m_progress_gauge = new ProgressBar(parent, wxID_ANY, 100, wxDefaultPosition, wxSize(FromDIP(300), FromDIP(8)));
+ m_progress_gauge->SetValue(0);
+ m_progress_gauge->SetHeight(FromDIP(8));
+
+ progress_sizer->Add(m_progress_gauge, 0, wxALIGN_CENTER | wxALL, FromDIP(10));
+ progress_sizer->Add(0, 0, 1, wxEXPAND, 0);
+
+ return progress_sizer;
+}
+
+void AMSDryCtrWin::create()
+{
+ // set title icon
+ SetBackgroundColour(StateColor::darkModeColorFor(*wxWHITE));
+ this->SetDoubleBuffered(true);
+ std::string icon_path = (boost::format("%1%/images/OrcaSlicerTitle.ico") % resources_dir()).str();
+ SetIcon(wxIcon(encode_path(icon_path.c_str()), wxBITMAP_TYPE_ICO));
+
+ SetSize(wxSize(FromDIP(700), FromDIP(500)));
+ SetMinSize(wxSize(FromDIP(700), FromDIP(500)));
+ SetMaxSize(wxSize(FromDIP(700), FromDIP(500)));
+
+ m_main_simplebook = new wxSimplebook(this, wxID_ANY, wxDefaultPosition, wxDefaultSize);
+
+ // Create main page
+ m_original_page = new wxPanel(m_main_simplebook, wxID_ANY);
+ m_original_page->SetBackgroundColour(*wxWHITE);
+ wxBoxSizer* main_sizer = create_main_page_sizer(m_original_page);
+ m_original_page->SetSizer(main_sizer);
+
+ m_main_simplebook->AddPage(m_original_page, "Main Page");
+
+ m_guide_page = new wxPanel(m_main_simplebook, wxID_ANY);
+ m_guide_page->SetBackgroundColour(*wxWHITE);
+ wxBoxSizer* guide_sizer = create_guide_page_sizer(m_guide_page);
+ m_guide_page->SetSizer(guide_sizer);
+ m_main_simplebook->AddPage(m_guide_page, "Guide Page");
+
+ // Create progress page
+ m_progress_page = new wxPanel(m_main_simplebook, wxID_ANY);
+ m_progress_page->SetBackgroundColour(*wxWHITE);
+ wxBoxSizer* progress_sizer = create_progress_page_sizer(m_progress_page);
+ m_progress_page->SetSizer(progress_sizer);
+ m_main_simplebook->AddPage(m_progress_page, "Progress Page");
+
+ m_progress_timer = new wxTimer(this, wxID_ANY);
+ Bind(wxEVT_TIMER, &AMSDryCtrWin::OnProgressTimer, this, m_progress_timer->GetId());
+
+ Bind(wxEVT_CLOSE_WINDOW, &AMSDryCtrWin::OnClose, this);
+
+ wxBoxSizer* top_level_sizer = new wxBoxSizer(wxVERTICAL);
+ top_level_sizer->Add(m_main_simplebook, 1, wxEXPAND | wxALL, FromDIP(10));
+
+ Bind(wxEVT_SHOW, &AMSDryCtrWin::OnShow, this);
+
+ SetSizer(top_level_sizer);
+ Layout();
+ Refresh();
+ wxGetApp().UpdateDlgDarkUI(this);
+}
+
+void AMSDryCtrWin::on_dpi_changed(const wxRect &suggested_rect)
+{
+ msw_rescale();
+}
+
+void AMSDryCtrWin::msw_rescale()
+{
+ if (!IsShown()) {
+ return;
+ }
+
+ if (m_ams_filament_panel) {m_ams_filament_panel->msw_rescale();}
+ if (m_trays_combo) {m_trays_combo->Rescale(); m_trays_combo->Layout(); m_trays_combo->SetFont(Label::Body_14), m_trays_combo->Refresh();}
+ if (m_humidity_img && m_humidity_image.bmp().IsOk()) {m_humidity_image.msw_rescale(); m_humidity_img->SetBitmap(m_humidity_image.bmp());}
+ if (m_image_placeholder && m_guide_image.bmp().IsOk()) {m_guide_image.msw_rescale();m_image_placeholder->SetBitmap(m_guide_image.bmp());}
+ if (m_image_description_icon && m_description_icon_bitmap.bmp().IsOk()) {m_description_icon_bitmap.msw_rescale(); m_image_description_icon->SetBitmap(m_description_icon_bitmap.bmp());}
+
+ // Rescale and update button sizes based on text content
+ if (m_next_button) {
+ m_next_button->Rescale();
+ update_button_size(m_next_button);
+ m_next_button->Layout();
+ m_next_button->Refresh();
+ }
+ if (m_stop_button) {
+ m_stop_button->Rescale();
+ update_button_size(m_stop_button);
+ m_stop_button->Layout();
+ m_stop_button->Refresh();
+ }
+ if (m_start_button) {
+ m_start_button->Rescale();
+ update_button_size(m_start_button);
+ m_start_button->Layout();
+ m_start_button->Refresh();
+ }
+ if (m_back_button) {
+ m_back_button->Rescale();
+ update_button_size(m_back_button);
+ m_back_button->Layout();
+ m_back_button->Refresh();
+ }
+ if (m_unload_button) {
+ m_unload_button->Rescale();
+ update_button_size(m_unload_button);
+ m_unload_button->Layout();
+ m_unload_button->Refresh();
+ }
+
+ if (m_guide_page) {m_guide_page->Layout();}
+ if (m_original_page) {m_original_page->Layout();}
+
+ Fit();
+ Layout();
+ Refresh();
+}
+
+void AMSDryCtrWin::set_ams_id(const std::string& ams_id)
+{
+ m_ams_info.m_ams_id = ams_id;
+ m_is_ams_changed = true;
+}
+
+void AMSDryCtrWin::update_img_description(DevAms::DryStatus status, DevAms::DrySubStatus sub_status)
+{
+ if (status == DevAms::DryStatus::Off || status == DevAms::DryStatus::Cooling) {
+ m_image_description->SetLabel(_L("Idle"));
+ m_image_description_icon->Show(false);
+ return;
+ }
+
+ // Determine label text for non-idle states
+ wxString label_text;
+ if (status == DevAms::DryStatus::Error) {
+ label_text = _L("Drying");
+ } else if (sub_status == DevAms::DrySubStatus::Heating) {
+ label_text = _L("Drying-Heating");
+ } else if (sub_status == DevAms::DrySubStatus::Dehumidify) {
+ label_text = _L("Drying-Dehumidifying");
+ } else {
+ m_image_description_icon->Show(false);
+ return;
+ }
+
+ m_image_description->SetLabel(label_text);
+
+ try {
+ m_description_icon_bitmap = ScalableBitmap(this, "dev_ams_dry_ctr_heating_icon", 20);
+ m_description_icon_bitmap.msw_rescale();
+ m_image_description_icon->SetBitmap(m_description_icon_bitmap.bmp());
+ m_image_description_icon->Show(true);
+ } catch (Exception&) {
+ BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " Error loading drying icon";
+ m_image_description_icon->Show(false);
+ }
+}
+
+int AMSDryCtrWin::update_image(DevAmsType model, DevAms::DryStatus status, DevAms::DrySubStatus sub_status, int humidity_percent)
+{
+ if (model == m_ams_info.m_model && status == m_ams_info.m_dry_status
+ && sub_status == m_ams_info.m_dry_sub_status && humidity_percent == m_ams_info.m_humidity_percent) {
+ return 0;
+ }
+
+ std::string img_path;
+
+ if (status == DevAms::DryStatus::Off && sub_status == DevAms::DrySubStatus::Off) {
+ img_path = get_humidity_level_img_path(humidity_percent);
+ } else {
+ img_path = get_dry_status_img_path(model, status, sub_status);
+ }
+
+ if (img_path.empty()) {
+ return 0;
+ }
+
+ m_humidity_image = ScalableBitmap(this, img_path, AMS_DRY_STATUS_IMAGE_SIZE);
+ m_humidity_img->SetBitmap(m_humidity_image.bmp());
+
+ update_img_description(status, sub_status);
+
+ return 1;
+}
+
+bool AMSDryCtrWin::check_values_changed(DevAms* dev_ams)
+{
+ bool changed = false;
+
+ if (m_ams_info.m_model != dev_ams->GetAmsType()) {
+ m_ams_info.m_model = dev_ams->GetAmsType();
+ changed = true;
+ }
+
+ if (dev_ams->GetDryStatus().has_value() && m_ams_info.m_dry_status != dev_ams->GetDryStatus()) {
+ m_ams_info.m_dry_status = dev_ams->GetDryStatus().value();
+ changed = true;
+ }
+
+ if (dev_ams->GetDrySubStatus().has_value() && m_ams_info.m_dry_sub_status != dev_ams->GetDrySubStatus()) {
+ m_ams_info.m_dry_sub_status = dev_ams->GetDrySubStatus().value();
+ changed = true;
+ }
+
+ if (m_ams_info.m_humidity_percent != dev_ams->GetHumidityPercent()) {
+ m_ams_info.m_humidity_percent = dev_ams->GetHumidityPercent();
+ changed = true;
+ }
+
+ return changed;
+}
+
+void AMSDryCtrWin::update_normal_description(DevAms* dev_ams)
+{
+ if (m_tray_ids.empty() || m_trays_combo->GetSelection() >= m_tray_ids.size()) { return; }
+
+ FilamentBaseInfo info = m_tray_ids[m_trays_combo->GetSelection()];
+ wxString warning_text;
+ bool can_enable_button = true;
+ long temp_val = 0, time_val = 0;
+ m_temperature_input->GetValue().ToLong(&temp_val);
+ m_time_input->GetValue().ToLong(&time_val);
+ std::optional preset = DevUtilBackend::GetFilamentDryingPreset(info.filament_id);
+ auto total_dry = preset.has_value() ? preset.value().ams_limitations : std::unordered_set();
+
+ struct AmsTempLimit { DevAmsType type; int min_temp; int max_temp; const char* name; };
+ static const AmsTempLimit ams_limits[] = {
+ { DevAmsType::N3F, 45, 65, "AMS2" },
+ { DevAmsType::N3S, 45, 85, "AMS-S" }
+ };
+
+ for (const auto& lim : ams_limits) {
+ if (dev_ams->GetAmsType() == lim.type) {
+ if (temp_val > lim.max_temp) {
+ wxString msg = wxString(lim.name) + _L(" maximum drying temperature is ") + wxString::Format(wxT("%d"), lim.max_temp) + wxString::FromUTF8("°C.");
+ warning_text += msg + "\n";
+ can_enable_button = false;
+ } else if (temp_val < lim.min_temp) {
+ wxString msg = wxString(lim.name) + _L(" minimum drying temperature is ") + wxString::Format(wxT("%d"), lim.min_temp) + wxString::FromUTF8("°C.");
+ warning_text += msg + "\n";
+ can_enable_button = false;
+ }
+ if (total_dry.find(lim.type) == total_dry.end()) {
+ warning_text += _L("This filament may not be completely dried.") + "\n\n";
+ }
+ break;
+ }
+ }
+
+ if (m_printer_status.m_is_printing && temp_val > m_ams_info.m_recommand_dry_temp) {
+ warning_text += _L("This AMS is currently printing. To ensure print quality, the drying temperature cannot exceed the recommended drying temperature.") + "\n";
+ can_enable_button = false;
+ } else if (preset.has_value()) {
+ // Only check heat distortion temperature when AMS has filament inserted;
+ // empty AMS only needs to respect the device hardware temperature limit.
+ bool has_filament_inserted = false;
+ for (const auto& tray_pair : dev_ams->GetTrays()) {
+ if (tray_pair.second && tray_pair.second->is_exists) {
+ has_filament_inserted = true;
+ break;
+ }
+ }
+ if (has_filament_inserted) {
+ auto limit_temperature = preset.value().filament_dev_ams_drying_heat_distortion_temperature;
+ if (temp_val > limit_temperature) {
+ warning_text += _L("The temperature shall not exceed the filament's heat distortion temperature") + "(" +
+ wxString::Format(wxT("%d"), static_cast(limit_temperature)) + wxString::FromUTF8("°C)\n");
+ can_enable_button = false;
+ }
+ }
+ }
+
+ if (time_val < 1) {
+ warning_text += _L("Minimum time value cannot be less than 1.") + "\n";
+ can_enable_button = false;
+ } else if (time_val > 24) {
+ warning_text += _L("Maximum time value cannot be greater than 24.") + "\n";
+ can_enable_button = false;
+ }
+
+ m_next_button->Enable(can_enable_button);
+ m_normal_description->SetLabel(warning_text);
+ m_normal_description->Wrap(FromDIP(250));
+ m_normal_description->GetParent()->Layout();
+}
+
+void AMSDryCtrWin::update_normal_state(DevAms* dev_ams)
+{
+ if (is_dry_ctr_idle(dev_ams)) {
+ m_next_button->Show(true);
+ m_normal_description->Show(true);
+ m_trays_combo->Enable();
+ m_temperature_input->SetEditable(true);
+ m_time_input->SetEditable(true);
+
+ m_stop_button->Hide();
+ m_dry_error_sizer->Show(false);
+ } else if (is_dry_ctr_err(dev_ams)) {
+ m_dry_error_sizer->Show(true);
+ m_next_button->Hide();
+ m_normal_description->Hide();
+
+ m_trays_combo->Hide();
+ m_temperature_input->Hide();
+ m_time_input->Hide();
+ m_stop_button->Show(true);
+ } else {
+ m_next_button->Hide();
+ m_normal_description->Hide();
+
+ m_trays_combo->Disable();
+ m_temperature_input->SetEditable(false);
+ m_time_input->SetEditable(false);
+ m_stop_button->Show(true);
+ m_dry_error_sizer->Show(false);
+ }
+
+ if (m_temperature_input->GetValue().IsEmpty() || m_time_input->GetValue().IsEmpty()) {
+ m_next_button->Disable();
+ m_start_button->Disable();
+ } else {
+ m_next_button->Enable();
+ }
+
+ update_normal_description(dev_ams);
+}
+
+void AMSDryCtrWin::OnFilamentSelectionChanged(wxCommandEvent& event)
+{
+ int selectionIndex = event.GetSelection();
+ wxString selectedFilament = event.GetString();
+
+ auto fila_system = get_fila_system();
+ if (!fila_system) {
+ BOOST_LOG_TRIVIAL(info) << "AMSDryCtrWin::OnFilamentSelectionChanged: Invalid FilaSystem Pointer";
+ return;
+ }
+
+ DevAms* dev_ams = fila_system->GetAmsById(m_ams_info.m_ams_id);
+ if (!dev_ams) {
+ BOOST_LOG_TRIVIAL(info) << "AMSDryCtrWin::OnFilamentSelectionChanged: Invalid AMS id";
+ return;
+ }
+
+ std::optional preset = DevUtilBackend::GetFilamentDryingPreset(m_tray_ids[selectionIndex].filament_id);
+ if (preset.has_value()) {
+ DevFilamentDryingPreset info = preset.value();
+ if (m_printer_status.m_is_printing) {
+ m_temperature_input->SetValue(std::to_string(static_cast(info.filament_dev_ams_drying_temperature_on_print[dev_ams->GetAmsType()])));
+ m_time_input->SetValue(std::to_string(static_cast(info.filament_dev_ams_drying_time_on_print[dev_ams->GetAmsType()])));
+ } else {
+ m_temperature_input->SetValue(std::to_string(static_cast(info.filament_dev_ams_drying_temperature_on_idle[dev_ams->GetAmsType()])));
+ m_time_input->SetValue(std::to_string(static_cast(info.filament_dev_ams_drying_time_on_idle[dev_ams->GetAmsType()])));
+ }
+ }
+
+ update_filament_guide_info(dev_ams);
+}
+
+wxString get_cannot_reason_text(DevAms::CannotDryReason reason)
+{
+ wxString cannot_reason_text;
+ switch (reason)
+ {
+ case DevAms::CannotDryReason::InsufficientPower:
+ cannot_reason_text = "*" + _L("Insufficient power") + "\n";
+ cannot_reason_text += _L(" Too many AMS drying simultaneously. Please plug in the power or stop other drying processes before starting.") + "\n";
+ break;
+ case DevAms::CannotDryReason::AmsBusy:
+ cannot_reason_text = "*" + _L("AMS is busy") + "\n";
+ cannot_reason_text += _L(" AMS is calibrating | reading RFID | loading/unloading material, please wait.") + "\n";
+ break;
+ case DevAms::CannotDryReason::ConsumableAtAmsOutlet:
+ cannot_reason_text = "*" + _L("Filament in AMS outlet") + "\n";
+ cannot_reason_text += _L(" The high drying temperature may cause AMS blockage, please unload first.");
+ break;
+ case DevAms::CannotDryReason::InitiatingAmsDrying:
+ cannot_reason_text = "*" + _L("Initiating AMS drying") + "\n";
+ break;
+ case DevAms::CannotDryReason::NotSupportedIn2dMode:
+ cannot_reason_text = "*" + _L("Not supported in 2D mode") + "\n";
+ break;
+ case DevAms::CannotDryReason::DryingInProgress:
+ cannot_reason_text = "*" + _L("Task in progress") + "\n";
+ cannot_reason_text += _L(" The AMS might be in use during Task.") + "\n";
+ break;
+ case DevAms::CannotDryReason::Upgrading:
+ cannot_reason_text = "*" + _L("Upgrading") + "\n";
+ cannot_reason_text += _L(" Firmware update in progress, please wait...") + "\n";
+ break;
+ case DevAms::CannotDryReason::InsufficientPowerNeedPluginPower:
+ cannot_reason_text = "*" + _L("Insufficient power") + "\n";
+ cannot_reason_text += _L(" Please plug in the power and then use the drying function.") + "\n";
+ break;
+ case DevAms::CannotDryReason::FilamentAtAmsOutletManualUnload:
+ cannot_reason_text = "*" + _L("Filament in AMS outlet") + "\n";
+ cannot_reason_text += _L(" The high drying temperature may cause AMS blockage. Please unload the filament manually before proceeding.") + "\n";
+ break;
+ default:
+ cannot_reason_text = "*" + _L("System is busy") + "\n";
+ cannot_reason_text += _L(" Initiating other drying processes, please wait a few seconds...") + "\n";
+ BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": unknown cannot dry reason";
+ break;
+ }
+ return cannot_reason_text;
+}
+
+wxString organize_cannot_reasons_text(std::vector& reasons)
+{
+ wxString cannot_reasons_text;
+ if (std::find(reasons.begin(), reasons.end(), DevAms::CannotDryReason::DryingInProgress) != reasons.end()) {
+ cannot_reasons_text += get_cannot_reason_text(DevAms::CannotDryReason::DryingInProgress);
+ } else if (std::find(reasons.begin(), reasons.end(), DevAms::CannotDryReason::AmsBusy) != reasons.end()) {
+ cannot_reasons_text += get_cannot_reason_text(DevAms::CannotDryReason::AmsBusy);
+ } else if (std::find(reasons.begin(), reasons.end(), DevAms::CannotDryReason::ConsumableAtAmsOutlet) != reasons.end()) {
+ cannot_reasons_text += get_cannot_reason_text(DevAms::CannotDryReason::ConsumableAtAmsOutlet);
+ } else if (std::find(reasons.begin(), reasons.end(), DevAms::CannotDryReason::FilamentAtAmsOutletManualUnload) != reasons.end()) {
+ cannot_reasons_text += get_cannot_reason_text(DevAms::CannotDryReason::FilamentAtAmsOutletManualUnload);
+ }
+
+ if (std::find(reasons.begin(), reasons.end(), DevAms::CannotDryReason::InsufficientPower) != reasons.end()) {
+ cannot_reasons_text += get_cannot_reason_text(DevAms::CannotDryReason::InsufficientPower);
+ }
+
+ for (auto reason : reasons) {
+ if (reason == DevAms::CannotDryReason::InsufficientPowerNeedPluginPower) {
+ cannot_reasons_text += get_cannot_reason_text(DevAms::CannotDryReason::InsufficientPowerNeedPluginPower);
+ } else if (reason == DevAms::CannotDryReason::NotSupportedIn2dMode) {
+ cannot_reasons_text += get_cannot_reason_text(DevAms::CannotDryReason::NotSupportedIn2dMode);
+ } else if (reason == DevAms::CannotDryReason::InitiatingAmsDrying) {
+ cannot_reasons_text += get_cannot_reason_text(DevAms::CannotDryReason::InitiatingAmsDrying);
+ } else if (reason == DevAms::CannotDryReason::Upgrading) {
+ cannot_reasons_text += get_cannot_reason_text(DevAms::CannotDryReason::Upgrading);
+ }
+ }
+
+ if (cannot_reasons_text.empty()) {
+ cannot_reasons_text += "*" + _L("System is busy") + "\n";
+ cannot_reasons_text += _L(" Initiating other drying processes, please wait a few seconds...") + "\n";
+ }
+
+ return cannot_reasons_text;
+}
+
+int AMSDryCtrWin::update_state(DevAms* dev_ams)
+{
+ std::vector cannot_reasons = dev_ams->GetCannotDryReason().has_value()?
+ dev_ams->GetCannotDryReason().value(): std::vector();
+
+ if (cannot_reasons.size() == 0 ||
+ (cannot_reasons.size() == 1 && cannot_reasons[0] == DevAms::CannotDryReason::DryingInProgress)) {
+ m_normal_state_sizer->Show(true);
+ m_cannot_dry_sizer->Show(false);
+ m_unload_button->Show(false);
+ update_normal_state(dev_ams);
+ } else if (cannot_reasons.size() > 0) {
+ m_normal_state_sizer->Show(false);
+ m_cannot_dry_sizer->Show(true);
+
+ m_cannot_dry_description_label->SetLabel(organize_cannot_reasons_text(cannot_reasons));
+ m_cannot_dry_description_label->Wrap(FromDIP(300));
+
+ // Show unload button when ConsumableAtAmsOutlet reason is present
+ bool has_consumable_at_outlet = std::find(cannot_reasons.begin(), cannot_reasons.end(),
+ DevAms::CannotDryReason::ConsumableAtAmsOutlet) != cannot_reasons.end();
+ m_unload_button->Show(has_consumable_at_outlet);
+ }
+
+ restore_stop_button_if_deadline_passed();
+ restore_unload_button_if_deadline_passed();
+
+ return 1;
+}
+
+int AMSDryCtrWin::update_ams_change(DevAms* dev_ams)
+{
+ if (!is_ams_changed(dev_ams)) {
+ return 0;
+ }
+
+ m_ams_info.m_ams_id = dev_ams->GetAmsId();
+ if (dev_ams->GetAmsType() == DevAmsType::N3F) {
+ m_temperature_input->SetHint("45-65" + wxString::FromUTF8("°C"));
+ } else if (dev_ams->GetAmsType() == DevAmsType::N3S) {
+ m_temperature_input->SetHint("45-85" + wxString::FromUTF8("°C"));
+ }
+
+ m_time_input->SetHint("1-24 h");
+
+ return 0;
+}
+
+int AMSDryCtrWin::update_dryness_status(DevAms* dev_ams)
+{
+ int updated = 0;
+
+ if (m_ams_info.m_humidity_percent != dev_ams->GetHumidityPercent()) {
+ updated += 1;
+ m_ams_info.m_humidity_percent = dev_ams->GetHumidityPercent();
+ m_humidity_data_label->SetLabel(std::to_string(m_ams_info.m_humidity_percent) + "%");
+ }
+
+ if (m_ams_info.m_temperature != dev_ams->GetCurrentTemperature()) {
+ updated += 1;
+ m_ams_info.m_temperature = dev_ams->GetCurrentTemperature();
+ m_temperature_data_label->SetLabel(std::to_string(m_ams_info.m_temperature) + wxString::FromUTF8("°C"));
+ }
+
+ if (is_dry_ctr_idle(dev_ams)) {
+ m_time_descrition_container->Show(false);
+ m_original_page->Layout();
+ } else {
+ if (is_dry_status_changed(dev_ams)) {
+ m_time_descrition_container->Show(true);
+ m_original_page->Layout();
+ }
+
+ if (m_ams_info.m_left_dry_time != dev_ams->GetLeftDryTime()) {
+ updated += 1;
+ m_ams_info.m_left_dry_time = dev_ams->GetLeftDryTime();
+ m_time_data_label->SetLabel(wxString::Format("%02d : %02d",
+ m_ams_info.m_left_dry_time / 60, m_ams_info.m_left_dry_time % 60));
+ }
+ }
+
+ return updated;
+}
+
+bool AMSDryCtrWin::is_tray_changed(DevAms* dev_ams)
+{
+ return false;
+}
+
+void AMSDryCtrWin::update_filament_guide_info(DevAms* dev_ams)
+{
+ if (!IsShown()) {
+ return;
+ }
+
+ m_guide_page->Freeze();
+ m_ams_filament_panel->Clear();
+ m_ams_filament_panel->SetAmsName(dev_ams->GetDisplayName());
+
+ // Get the temperature input value
+ long input_temp = 0;
+ bool valid_temp = !m_temperature_input->GetValue().IsEmpty() &&
+ m_temperature_input->GetValue().ToLong(&input_temp);
+ bool can_start = true;
+
+ int slot_count = 0, empty_count = 0;
+ for (auto& tray_pair : dev_ams->GetTrays()) {
+ if (!tray_pair.second) {
+ continue;
+ }
+ ++slot_count;
+ if (!tray_pair.second->is_exists) {
+ m_ams_filament_panel->AddFilamentItem("/", "dev_ams_dry_ctr_enable");
+ ++empty_count;
+ continue;
+ }
+ auto preset_opt = tray_pair.second->get_ams_drying_preset();
+ wxString filament_type = tray_pair.second->get_display_filament_type();
+ DevFilamentDryingPreset preset;
+ if (filament_type.IsEmpty()) {
+ auto fallback_preset = DevUtilBackend::GetFilamentDryingPreset("GFA00");
+ preset = fallback_preset.value();
+ filament_type = "?";
+ } else if (preset_opt.has_value()) {
+ preset = preset_opt.value();
+ } else {
+ auto fallback_preset = DevUtilBackend::GetFilamentDryingPreset("GFA00");
+ preset = fallback_preset.value();
+ }
+ std::string icon_path = "dev_ams_dry_ctr_enable";
+ int distortion_temp = static_cast(preset.filament_dev_ams_drying_heat_distortion_temperature);
+ if (valid_temp && distortion_temp < input_temp) {
+ icon_path = "dev_ams_dry_ctr_disable";
+ can_start = false;
+ }
+ m_ams_filament_panel->AddFilamentItem(filament_type, icon_path);
+ }
+ if (slot_count == 0) {
+ can_start = false;
+ }
+
+ if (can_start) {
+ m_guide_title_label->SetLabel(_L("For better drying results, remove the filament and allow it to rotate."));
+ m_guide_description_label->SetLabel(_L("The AMS will automatically rotate the stored filament slots to enhance the drying performance.") +
+ "\n" + _L("Alternatively, you can dry the filament without removing it.") +
+ "\n" + "*" + _L("Unknown filaments will be treated as PLA."));
+ m_guide_description_label->Wrap(FromDIP(300));
+ } else {
+ m_guide_title_label->SetLabel(_L("Please store the filament marked with an exclamation mark."));
+ m_guide_description_label->SetLabel(_L("Filament left in the feeder during drying may soften because the drying temperature exceeds the softening point of materials like PLA and TPU.") +
+ "\n" + "*" + _L("Unknown filaments will be treated as PLA."));
+ m_guide_description_label->Wrap(FromDIP(300));
+ }
+
+ m_start_button->Enable(can_start);
+ m_guide_page->Layout();
+ m_guide_page->Thaw();
+}
+
+int AMSDryCtrWin::update_filament_list(DevAms* dev_ams, MachineObject* obj)
+{
+ const bool ams_changed = is_ams_changed(dev_ams);
+ bool rebuilt_filament_list = false;
+
+ // Build `m_tray_ids` and the combobox items from the preset bundle.
+ // This list is also used as a lookup table in non-idle state (type -> name),
+ // so it must be available even when the combobox is disabled.
+ auto rebuild_filament_list = [&]() -> bool {
+ m_trays_combo->Clear();
+ m_tray_ids.clear();
+
+ PresetBundle *preset_bundle = wxGetApp().preset_bundle;
+ if (!preset_bundle || !obj) {
+ return false;
+ }
+
+ std::set filament_id_set;
+ auto & filaments = preset_bundle->filaments;
+
+ // Get nozzle diameter using the same method as AMSMaterialsSetting::Popup
+ std::ostringstream stream;
+ int extruder_id = obj->GetFilaSystem()->GetExtruderIdByAmsId(m_ams_info.m_ams_id);
+ if (!obj->GetExtderSystem()->GetExtderById(extruder_id)) {
+ BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " get extruder id failed";
+ extruder_id = 0;
+ }
+ stream << std::fixed << std::setprecision(1) << obj->GetExtderSystem()->GetNozzleDiameter(extruder_id);
+ std::string nozzle_diameter_str = stream.str();
+ std::set printer_names = preset_bundle->get_printer_names_by_printer_type_and_nozzle(
+ DevPrinterConfigUtil::get_printer_display_name(obj->printer_type), nozzle_diameter_str);
+
+ for (auto filament_it = filaments.begin(); filament_it != filaments.end(); ++filament_it) {
+ Preset& preset = *filament_it;
+ // Filter by system preset: root preset and (system preset or user preset is supported)
+ if (filaments.get_preset_base(*filament_it) != &preset || (!filament_it->is_system && !obj->is_support_user_preset)) {
+ continue;
+ }
+
+ ConfigOption * printer_opt = filament_it->config.option("compatible_printers");
+ ConfigOptionStrings *printer_strs = dynamic_cast(printer_opt);
+ if (!printer_strs) continue;
+
+ for (auto printer_str : printer_strs->values) {
+ if (printer_names.find(printer_str) != printer_names.end()) {
+ if (filament_id_set.find(filament_it->filament_id) != filament_id_set.end()) {
+ continue;
+ }
+
+ filament_id_set.insert(filament_it->filament_id);
+ auto filament_alias = filaments.get_preset_alias(*filament_it, true);
+ if (!filament_alias.empty()) {
+ auto opt_info = preset_bundle->get_filament_by_filament_id(filament_it->filament_id);
+ if (opt_info.has_value()) {
+ auto real_info = opt_info.value();
+ real_info.filament_name = filament_alias;
+ m_tray_ids.push_back(std::move(real_info));
+ m_trays_combo->Append(wxString::FromUTF8(filament_alias));
+ }
+ }
+ }
+ }
+ }
+
+ if (m_tray_ids.empty()) {
+ BOOST_LOG_TRIVIAL(warning) << "AMSDryCtrWin::update_filament_list: No valid filaments found";
+ return false;
+ }
+
+ return true;
+ };
+
+ if (ams_changed || m_tray_ids.empty() || m_trays_combo->GetCount() == 0) {
+ // Disable start button during rebuild
+ if (m_start_button) {
+ m_start_button->Disable();
+ }
+
+ rebuilt_filament_list = rebuild_filament_list();
+ }
+
+ if (!is_dry_ctr_idle(dev_ams)) {
+ const std::string& ams_id = dev_ams->GetAmsId();
+ const auto settings_opt = dev_ams->GetDrySettings();
+
+ // Set the combobox display label by the filament "type" string.
+ // Priority: last saved selection for this AMS (if the type matches) -> fallback to current `m_tray_ids`.
+ auto set_filament_label_by_type = [&](const std::string& filament_type) {
+ if (filament_type.empty()) {
+ return;
+ }
+
+ auto it_name = m_dry_setting.m_filament_names.find(ams_id);
+ auto it_type = m_dry_setting.m_filament_type.find(ams_id);
+ if (it_name != m_dry_setting.m_filament_names.end() && it_type != m_dry_setting.m_filament_type.end()
+ && it_type->second == filament_type) {
+ m_trays_combo->SetLabel(wxString::FromUTF8(it_name->second));
+ return;
+ }
+
+ const auto it = std::find_if(m_tray_ids.begin(), m_tray_ids.end(), [&](const FilamentBaseInfo& tray) {
+ return tray.filament_type == filament_type;
+ });
+ if (it != m_tray_ids.end()) {
+ m_trays_combo->SetLabel(wxString::FromUTF8(it->filament_name));
+ }
+ };
+
+ if (settings_opt.has_value()) {
+ const auto& settings = settings_opt.value();
+ m_temperature_input->SetValue(std::to_string(settings.dry_temp));
+ m_time_input->SetValue(std::to_string(settings.dry_hour));
+ set_filament_label_by_type(settings.dry_filament);
+ } else {
+ auto it_name = m_dry_setting.m_filament_names.find(ams_id);
+ if (it_name != m_dry_setting.m_filament_names.end()) {
+ m_trays_combo->SetLabel(wxString::FromUTF8(it_name->second));
+ m_temperature_input->SetValue(std::to_string(m_dry_setting.m_dry_temp[ams_id]));
+ m_time_input->SetValue(std::to_string(m_dry_setting.m_dry_time[ams_id]));
+ }
+ }
+
+ return 0;
+ }
+
+ // Idle state: avoid re-selecting / firing selection change unless the list was rebuilt.
+ if (!rebuilt_filament_list && !ams_changed && is_dry_ctr_idle()) {
+ return 0;
+ }
+
+ if (m_tray_ids.empty()) {
+ return 0;
+ }
+
+ // Select recommended drying temperature and default filament
+ float min_dry_temp = std::numeric_limits::max();
+ std::string default_filament_id = "GFA00";
+ bool has_ready = false;
+ const auto fallback_preset = DevUtilBackend::GetFilamentDryingPreset("GFA00");
+ for (const auto& tray_pair : dev_ams->GetTrays()) {
+ if (!tray_pair.second || !tray_pair.second->is_tray_info_ready()) continue;
+ has_ready = true;
+ wxString filament_type = tray_pair.second->get_display_filament_type();
+ Slic3r::DevFilamentDryingPreset preset;
+ if (filament_type.IsEmpty()) {
+ // no filament type, is PLA
+ if (!fallback_preset) continue;
+ preset = fallback_preset.value();
+ } else {
+ auto preset_opt = tray_pair.second->get_ams_drying_preset();
+ if (preset_opt.has_value()) {
+ preset = preset_opt.value();
+ } else {
+ // no preset,is PLA
+ if (!fallback_preset) continue;
+ preset = fallback_preset.value();
+ }
+ }
+ float cur_temp = m_printer_status.m_is_printing
+ ? std::min({preset.filament_dev_ams_drying_temperature_on_print[dev_ams->GetAmsType()],
+ preset.filament_dev_drying_softening_temperature, preset.filament_dev_ams_drying_heat_distortion_temperature})
+ : preset.filament_dev_ams_drying_temperature_on_idle[dev_ams->GetAmsType()];
+ if (cur_temp < min_dry_temp) {
+ min_dry_temp = cur_temp;
+ default_filament_id = preset.filament_id;
+ }
+ }
+ // if no tray is ready, use PLA as default
+ if (!has_ready) {
+ if (fallback_preset.has_value()) {
+ Slic3r::DevFilamentDryingPreset preset = fallback_preset.value();
+ float cur_temp = m_printer_status.m_is_printing
+ ? std::min({preset.filament_dev_ams_drying_temperature_on_print[dev_ams->GetAmsType()],
+ preset.filament_dev_drying_softening_temperature, preset.filament_dev_ams_drying_heat_distortion_temperature})
+ : preset.filament_dev_ams_drying_temperature_on_idle[dev_ams->GetAmsType()];
+ min_dry_temp = cur_temp;
+ default_filament_id = preset.filament_id;
+ }
+ }
+ m_ams_info.m_recommand_dry_temp = min_dry_temp;
+
+ // Set default selection
+ unsigned int default_index = 0;
+ for (unsigned int i = 0; i < m_tray_ids.size(); i++) {
+ if (m_tray_ids[i].filament_id == default_filament_id) {
+ default_index = i;
+ break;
+ }
+ }
+ m_trays_combo->SetSelection(default_index);
+ wxCommandEvent evt(wxEVT_COMBOBOX, m_trays_combo->GetId());
+ evt.SetInt(default_index);
+ OnFilamentSelectionChanged(evt);
+ return 1;
+}
+
+std::shared_ptr AMSDryCtrWin::get_fila_system() const
+{
+ std::shared_ptr fila_system = m_fila_system.lock();
+ if (!fila_system) {
+ const_cast(this)->Close();
+ }
+ return fila_system;
+}
+
+void AMSDryCtrWin::update_printer_state(MachineObject* obj)
+{
+ m_printer_status.m_is_printing = obj->is_in_printing()
+ && obj->GetExtderSystem()->GetCurrentAmsId() == m_ams_info.m_ams_id;
+}
+
+void AMSDryCtrWin::update(std::shared_ptr fila_system, MachineObject* obj)
+{
+ if (!fila_system || !obj) {
+ return;
+ }
+
+ update_printer_state(obj);
+ m_fila_system = fila_system;
+
+ DevAms* dev_ams = fila_system->GetAmsById(m_ams_info.m_ams_id);
+ if (!dev_ams) {
+ BOOST_LOG_TRIVIAL(info) << "AMSDryCtrWin::update: Invalid AMS id";
+ m_ams_info.m_ams_id = "";
+ Close();
+ return;
+ }
+
+ if (!dev_ams->IsSupportRemoteDry(fila_system->GetOwner())) {
+ BOOST_LOG_TRIVIAL(info) << "AMSDryCtrWin::update: Selected AMS does not support remote drying";
+ Close();
+ return;
+ }
+
+
+ update_ams_change(dev_ams);
+
+ update_image(dev_ams->GetAmsType(),
+ dev_ams->GetDryStatus().has_value()? dev_ams->GetDryStatus().value(): DevAms::DryStatus::Off,
+ dev_ams->GetDrySubStatus().has_value()? dev_ams->GetDrySubStatus().value(): DevAms::DrySubStatus::Off,
+ dev_ams->GetHumidityPercent());
+
+ update_state(dev_ams);
+ update_dryness_status(dev_ams);
+
+ update_filament_list(dev_ams, obj);
+ update_filament_guide_info(dev_ams);
+
+ m_is_ams_changed = false;
+
+ check_values_changed(dev_ams);
+
+ Layout();
+ Refresh();
+}
+
+bool AMSDryCtrWin::is_ams_changed(DevAms* dev_ams)
+{
+ return m_ams_info.m_ams_id != dev_ams->GetAmsId() || m_is_ams_changed;
+}
+
+bool AMSDryCtrWin::is_dry_status_changed(DevAms* dev_ams)
+{
+ if (!dev_ams->GetDryStatus().has_value() || !dev_ams->GetDrySubStatus().has_value()) {
+ return true;
+ }
+
+ return dev_ams->GetDryStatus().value() != m_ams_info.m_dry_status
+ || dev_ams->GetDrySubStatus().value() != m_ams_info.m_dry_sub_status;
+}
+
+bool AMSDryCtrWin::is_dry_ctr_idle(DevAms* dev_ams)
+{
+ if (!dev_ams->GetDryStatus().has_value() || !dev_ams->GetDrySubStatus().has_value()) {
+ return true;
+ }
+
+ return dev_ams->GetDryStatus().value() == DevAms::DryStatus::Off
+ || dev_ams->GetDryStatus().value() == DevAms::DryStatus::Cooling;
+}
+
+bool AMSDryCtrWin::is_dry_ctr_idle()
+{
+ return m_ams_info.m_dry_status == DevAms::DryStatus::Off || m_ams_info.m_dry_status == DevAms::DryStatus::Cooling;
+}
+
+bool AMSDryCtrWin::is_dry_ctr_err(DevAms* dev_ams)
+{
+ if (!dev_ams->GetDryStatus().has_value() || !dev_ams->GetDrySubStatus().has_value()) {
+ return false;
+ }
+
+ return dev_ams->GetDryStatus().value() == DevAms::DryStatus::Error;
+}
+
+} // GUI
+} // Slic3r
diff --git a/src/slic3r/GUI/AMSDryControl.hpp b/src/slic3r/GUI/AMSDryControl.hpp
new file mode 100644
index 0000000000..43e177eeb1
--- /dev/null
+++ b/src/slic3r/GUI/AMSDryControl.hpp
@@ -0,0 +1,253 @@
+#pragma once
+#include "GUI_ObjectLayers.hpp"
+#include "slic3r/GUI/Widgets/AMSItem.hpp"
+#include "slic3r/GUI/Widgets/Label.hpp"
+#include "slic3r/GUI/Widgets/PopupWindow.hpp"
+
+#include "slic3r/GUI/wxExtensions.hpp"
+#include "slic3r/GUI/DeviceCore/DevFilaSystem.h"
+#include "slic3r/GUI/I18N.hpp"
+
+#include
+#include
+
+
+//Previous defintions
+class wxGrid;
+
+namespace Slic3r {
+
+namespace GUI {
+
+
+enum class DryCtrState {
+ IDLE,
+ DRY_WHEN_PRINT,
+ UNKNOWN
+};
+
+enum class DryCtrDev {
+ N3S,
+ N3F,
+ UNKNOWN
+};
+
+struct DryingPreset {
+ DryCtrState state;
+ DryCtrDev dev;
+ int dry_temp;
+ int dry_time;
+};
+
+class FilamentItemPanel : public wxPanel
+{
+public:
+ FilamentItemPanel(wxWindow* parent, const wxString& text, const std::string& icon_name = "",
+ wxWindowID id = wxID_ANY);
+
+ void SetText(const wxString& text);
+ void SetIcon(const std::string& icon_name);
+ void msw_rescale();
+
+private:
+ void OnPaint(wxPaintEvent& event);
+ void OnSize(wxSizeEvent& event);
+
+ Label* m_text_label;
+ wxStaticBitmap* m_icon_bitmap;
+ int m_target_size;
+ std::string m_icon_name;
+ ScalableBitmap m_icon;
+};
+
+class AMSFilamentPanel : public wxPanel
+{
+ wxBoxSizer* m_filament_sizer;
+ Label* m_ams_name_label;
+ int m_border_radius;
+ wxPanel* m_filament_container{nullptr};
+ std::vector m_filament_items;
+
+public:
+ AMSFilamentPanel(wxWindow* parent, const wxString& ams_name, wxWindowID id = wxID_ANY);
+
+ void AddFilamentItem(const wxString& text, const std::string& icon_name);
+ void AddFilamentItem(FilamentItemPanel* panel);
+ void SetAmsName(const wxString& ams_name);
+ void Clear();
+ void msw_rescale();
+private:
+ void OnPaint(wxPaintEvent& event);
+};
+
+
+class AMSDryCtrWin : public DPIDialog
+{
+public:
+ AMSDryCtrWin(wxWindow *parent);
+ ~AMSDryCtrWin();
+
+ void msw_rescale();
+ void update(std::shared_ptr fila_system, MachineObject* obj);
+ void set_ams_id(const std::string& ams_id);
+
+protected:
+ void on_dpi_changed(const wxRect &suggested_rect) override;
+
+private:
+ wxSimplebook* m_main_simplebook{nullptr};
+ wxPanel* m_original_page{nullptr};
+
+ wxWindow* m_amswin{nullptr};
+ wxBoxSizer* m_sizer_ams_items{nullptr};
+ wxScrolledWindow* m_panel_prv_left {nullptr};
+ wxScrolledWindow* m_panel_prv_right{nullptr};
+ wxBoxSizer* m_sizer_prv_left{nullptr};
+ wxBoxSizer* m_sizer_prv_right{nullptr};
+
+ // left panel related members
+ ScalableBitmap m_humidity_image;
+ wxStaticBitmap* m_humidity_img{nullptr};
+ Label* m_image_description{nullptr};
+ wxStaticBitmap* m_image_description_icon{nullptr};
+ ScalableBitmap m_description_icon_bitmap;
+
+ Label* m_humidity_data_label = nullptr;
+ Label* m_temperature_data_label = nullptr;
+ Label* m_time_data_label = nullptr;
+ wxBoxSizer* m_time_descrition_container = nullptr;
+
+ // right panel related members
+ wxBoxSizer* m_normal_state_sizer{nullptr};
+ wxBoxSizer* m_cannot_dry_sizer{nullptr};
+ wxBoxSizer* m_dry_error_sizer{nullptr};
+ Label* m_cannot_dry_description_label = nullptr;
+
+ // right panel normal state
+ ComboBox* m_trays_combo;
+ std::vector m_tray_ids;
+ wxTextCtrl* m_temperature_input;
+ wxTextCtrl* m_time_input;
+ Label* m_normal_description;
+ Button* m_start_button{nullptr};
+ Button* m_next_button{nullptr};
+ Button* m_stop_button{nullptr};
+ Button* m_back_button{nullptr};
+ Button* m_unload_button{nullptr};
+
+ // guide page description
+ Label* m_guide_title_label{nullptr};
+ Label* m_guide_description_label{nullptr};
+
+ wxCheckBox* m_rotate_spool_toggle{nullptr};
+
+ wxPanel* m_progress_page;
+ ProgressBar* m_progress_gauge;
+ wxTimer* m_progress_timer;
+
+ std::optional m_stop_button_restore_deadline;
+ std::optional m_unload_button_restore_deadline;
+
+ Label* m_progress_title;
+ int m_progress_value;
+ int m_progress_message_index;
+ std::vector m_progress_text = {
+ _L("Starting: Checking adapter connection"),
+ _L("Starting: Checking filament status"),
+ _L("Starting: Checking drying presets"),
+ _L("Starting: Checking filament location"),
+ _L("Starting: Checking air intake"),
+ _L("Starting: Checking air vent")
+ };
+
+ // Guide page
+ wxPanel* m_guide_page{nullptr};
+ AMSFilamentPanel* m_ams_filament_panel{nullptr};
+ std::map m_filament_items;
+ wxStaticBitmap* m_image_placeholder{nullptr};
+ ScalableBitmap m_guide_image;
+
+
+ bool m_is_ams_changed = false;
+ std::weak_ptr m_fila_system;
+ struct {
+ std::string m_ams_id;
+ DevAmsType m_model = DevAmsType::EXT_SPOOL;
+ DevAms::DryStatus m_dry_status = DevAms::DryStatus::Off;
+ DevAms::DrySubStatus m_dry_sub_status = DevAms::DrySubStatus::Off;
+ int m_humidity_percent;
+ int m_temperature;
+ int m_left_dry_time;
+ float m_recommand_dry_temp;
+ } m_ams_info;
+
+ struct {
+ std::unordered_map m_filament_names;
+ std::unordered_map m_filament_type;
+ std::unordered_map m_dry_temp;
+ std::unordered_map m_dry_time;
+ } m_dry_setting;
+
+ struct {
+ bool m_is_printing = false;
+ } m_printer_status;
+
+private:
+ void create();
+ wxBoxSizer* create_guide_page_sizer(wxPanel* parent);
+ wxBoxSizer* create_main_content_section(wxPanel* parent);
+ wxBoxSizer* create_guide_info_filament(wxPanel* parent);
+ wxBoxSizer* create_guide_info_section(wxPanel* parent);
+ wxBoxSizer* create_guide_right_section(wxPanel* parent);
+ wxBoxSizer* create_main_page_sizer(wxPanel* parent);
+ wxBoxSizer* create_left_panel(wxPanel* parent);
+ wxBoxSizer* create_humidity_status_section(wxPanel* parent);
+ wxBoxSizer* create_description_item(wxPanel* parent, const wxString& title, Label*& dataLabel);
+ wxBoxSizer* create_status_descriptions_section(wxPanel* parent);
+
+ wxBoxSizer* create_right_panel(wxPanel* parent);
+ wxBoxSizer* create_normal_state_panel(wxPanel* parent);
+ wxBoxSizer* create_cannot_dry_panel(wxPanel* parent);
+ wxBoxSizer* create_drying_error_panel(wxPanel* parent);
+ Button* create_button(wxPanel* parent, const wxString& title,
+ const wxColour& background_color, const wxColour& border_color, const wxColour& text_color);
+
+ wxBoxSizer* create_progress_page_sizer(wxPanel* parent);
+ void OnProgressTimer(wxTimerEvent& event);
+ void OnClose(wxCloseEvent& event);
+ void OnShow(wxShowEvent& event);
+
+ void OnFilamentSelectionChanged(wxCommandEvent& event);
+
+
+ wxScrolledWindow* create_preview_scrolled_window(wxWindow* parent);
+
+ bool check_values_changed(DevAms* dev_ams);
+ int update_image(DevAmsType type, DevAms::DryStatus status, DevAms::DrySubStatus sub_status, int humidity_percent);
+ void update_img_description(DevAms::DryStatus status, DevAms::DrySubStatus sub_status);
+ void update_normal_description(DevAms* dev_ams);
+ int update_state(DevAms* dev_ams);
+ int update_dryness_status(DevAms* dev_ams);
+ int update_ams_change(DevAms* dev_ams);
+ int update_filament_list(DevAms* dev_ams, MachineObject* obj);
+ void update_filament_guide_info(DevAms* dev_ams);
+ void update_normal_state(DevAms* dev_ams);
+ void update_printer_state(MachineObject* obj);
+
+ std::shared_ptr get_fila_system() const;
+ void start_sending_drying_command();
+ void restore_stop_button_if_deadline_passed();
+ void restore_unload_button_if_deadline_passed();
+ void update_button_size(Button* button);
+
+ bool is_dry_status_changed(DevAms* dev_ams);
+ bool is_dry_ctr_idle(DevAms* dev_ams);
+ bool is_ams_changed(DevAms* dev_ams);
+ bool is_dry_ctr_idle();
+ bool is_tray_changed(DevAms* dev_ams);
+ bool is_dry_ctr_err(DevAms* dev_ams);
+
+};
+
+} // GUI
+} // Slic3r
diff --git a/src/slic3r/GUI/ConfigWizard.cpp b/src/slic3r/GUI/ConfigWizard.cpp
index 9d2a6e9905..0bbbc15f87 100644
--- a/src/slic3r/GUI/ConfigWizard.cpp
+++ b/src/slic3r/GUI/ConfigWizard.cpp
@@ -1440,7 +1440,7 @@ PageTemperatures::PageTemperatures(ConfigWizard *parent)
spin_bed->SetValue(default_bed != nullptr && default_bed->size() > 0 ? default_bed->get_at(0) : 0);
append_text(_L("Enter the nozzle_temperature needed for extruding your filament."));
- append_text(_L("A rule of thumb is 160 to 230 °C for PLA, and 215 to 250 °C for ABS."));
+ append_text(_L("A rule of thumb is 160 to 230℃ for PLA, and 215 to 250℃ for ABS."));
#endif
auto *sizer_extr = new wxFlexGridSizer(3, 5, 5);
@@ -1455,7 +1455,7 @@ PageTemperatures::PageTemperatures(ConfigWizard *parent)
append_spacer(VERTICAL_SPACING);
append_text(_L("Enter the bed temperature needed for getting your filament to stick to your heated bed."));
- append_text(_L("A rule of thumb is 60 °C for PLA and 110 °C for ABS. Leave zero if you have no heated bed."));
+ append_text(_L("A rule of thumb is 60℃ for PLA and 110℃ for ABS. Leave zero if you have no heated bed."));
auto *sizer_bed = new wxFlexGridSizer(3, 5, 5);
auto *text_bed = new wxStaticText(this, wxID_ANY, _L("Bed Temperature:"));
diff --git a/src/slic3r/GUI/DeviceCore/DevDefs.h b/src/slic3r/GUI/DeviceCore/DevDefs.h
index 4e55f7cbae..640f984024 100644
--- a/src/slic3r/GUI/DeviceCore/DevDefs.h
+++ b/src/slic3r/GUI/DeviceCore/DevDefs.h
@@ -41,6 +41,16 @@ enum AmsStatusMain
AMS_STATUS_MAIN_UNKNOWN = 0xFF,
};
+enum DevAmsType : int
+{
+ EXT_SPOOL = 0, // EXT
+ AMS = 1, // AMS1
+ AMS_LITE = 2, // AMS-Lite
+ N3F = 3, // N3F, AMS 2PRO
+ N3S = 4, // N3S, AMS HT
+ AMS_LITE_MIXED = 5, // AMS-Lite for N9
+};
+
// Device-numbered filament-change step codes. Newer firmware reports the exact change-step
// sequence through the AMS (ams.cfs), keyed by these codes, instead of the client hardcoding
// steps per model. Distinct from the legacy GUI-side Slic3r::GUI::FilamentStep enum.
diff --git a/src/slic3r/GUI/DeviceCore/DevFilaSystem.cpp b/src/slic3r/GUI/DeviceCore/DevFilaSystem.cpp
index 2e9c7a65ac..806af9d5f0 100644
--- a/src/slic3r/GUI/DeviceCore/DevFilaSystem.cpp
+++ b/src/slic3r/GUI/DeviceCore/DevFilaSystem.cpp
@@ -7,6 +7,7 @@
#include "slic3r/GUI/I18N.hpp"
#include "DevUtil.h"
+#include "DevUtilBackend.h"
using namespace nlohmann;
@@ -99,6 +100,11 @@ std::string DevAmsTray::get_filament_type()
return m_fila_type;
}
+std::optional DevAmsTray::get_ams_drying_preset() const
+{
+ return DevUtilBackend::GetFilamentDryingPreset(setting_id);
+}
+
DevAms::DevAms(const std::string& ams_id, int extruder_id, AmsType type)
{
@@ -112,7 +118,7 @@ DevAms::DevAms(const std::string& ams_id, int nozzle_id, int type)
m_ams_id = ams_id;
m_ext_id = nozzle_id;
m_ams_type = (AmsType)type;
- assert(DUMMY < type && m_ams_type <= AMS_LITE_MIXED);
+ assert(EXT_SPOOL < type && m_ams_type <= AMS_LITE_MIXED);
}
DevAms::~DevAms()
@@ -193,6 +199,27 @@ DevAmsTray* DevAms::GetTray(const std::string& tray_id) const
return nullptr;
}
+bool DevAms::IsSupportRemoteDry(const MachineObject* obj) const
+{
+ if (obj && obj->is_support_remote_dry) {
+ return SupportDrying();
+ }
+
+ return false;
+}
+
+bool DevAms::AmsIsDrying()
+{
+ if (!GetDryStatus().has_value()) {
+ return false;
+ }
+
+ return GetDryStatus().value() == DevAms::DryStatus::Checking
+ || GetDryStatus().value() == DevAms::DryStatus::Drying
+ || GetDryStatus().value() == DevAms::DryStatus::Error
+ || GetDryStatus().value() == DevAms::DryStatus::CannotStopHeatOutofControl;
+}
+
DevFilaSystem::~DevFilaSystem()
{
for (auto it = amsList.begin(); it != amsList.end(); it++)
@@ -515,9 +542,18 @@ void DevFilaSystemParser::ParseV1_0(const json& jj, MachineObject* obj, DevFilaS
;
}
- if (it->contains("dry_time") && (*it)["dry_time"].is_number())
+
+ if (it->contains("temp"))
{
- curr_ams->m_left_dry_time = (*it)["dry_time"].get();
+ std::string temp = (*it)["temp"].get();
+ try
+ {
+ curr_ams->m_current_temperature = DevUtil::string_to_float(temp);
+ }
+ catch (...)
+ {
+ curr_ams->m_current_temperature = INVALID_AMS_TEMPERATURE;
+ }
}
if (it->contains("humidity"))
@@ -546,17 +582,32 @@ void DevFilaSystemParser::ParseV1_0(const json& jj, MachineObject* obj, DevFilaS
}
}
-
- if (it->contains("temp"))
+ if (it->contains("dry_time") && (*it)["dry_time"].is_number())
{
- std::string temp = (*it)["temp"].get();
- try
- {
- curr_ams->m_current_temperature = DevUtil::string_to_float(temp);
+ curr_ams->m_left_dry_time = (*it)["dry_time"].get();
+ }
+
+ // Drying status — only parse if printer supports remote drying
+ if (obj->is_support_remote_dry) {
+ if (it->contains("info")) {
+ const std::string& info = (*it)["info"].get();
+ curr_ams->m_dry_status = (DevAms::DryStatus)DevUtil::get_flag_bits(info, 4, 4);
+ curr_ams->m_dry_fan1_status = (DevAms::DryFanStatus)DevUtil::get_flag_bits(info, 18, 2);
+ curr_ams->m_dry_fan2_status = (DevAms::DryFanStatus)DevUtil::get_flag_bits(info, 20, 2);
+ curr_ams->m_dry_sub_status = (DevAms::DrySubStatus)DevUtil::get_flag_bits(info, 22, 2);
}
- catch (...)
- {
- curr_ams->m_current_temperature = INVALID_AMS_TEMPERATURE;
+
+ if (it->contains("dry_setting")) {
+ const auto& j_dry_settings = (*it)["dry_setting"];
+ DevAms::DrySettings dry_settings;
+ DevJsonValParser::ParseVal(j_dry_settings, "dry_filament", dry_settings.dry_filament);
+ DevJsonValParser::ParseVal(j_dry_settings, "dry_temperature", dry_settings.dry_temp);
+ DevJsonValParser::ParseVal(j_dry_settings, "dry_duration", dry_settings.dry_hour);
+ curr_ams->m_dry_settings = dry_settings;
+ }
+
+ if (it->contains("dry_sf_reason")) {
+ curr_ams->m_dry_cannot_reasons = DevJsonValParser::GetVal>((*it), "dry_sf_reason");
}
}
diff --git a/src/slic3r/GUI/DeviceCore/DevFilaSystem.h b/src/slic3r/GUI/DeviceCore/DevFilaSystem.h
index f33a636680..1d841ec3b7 100644
--- a/src/slic3r/GUI/DeviceCore/DevFilaSystem.h
+++ b/src/slic3r/GUI/DeviceCore/DevFilaSystem.h
@@ -12,6 +12,9 @@
#include
#include
#include
+#include
+#include
+#include
#include
#include
@@ -20,6 +23,7 @@
namespace Slic3r
{
class MachineObject;
+struct DevFilamentDryingPreset;
/**
* DevAmsTray - Represents a single filament tray/slot in an AMS unit or virtual tray.
@@ -106,6 +110,8 @@ public:
// static
static wxColour decode_color(const std::string& color);
+
+ std::optional get_ams_drying_preset() const;
};
/**
@@ -133,14 +139,67 @@ class DevAms
{
friend class DevFilaSystemParser;
public:
- enum AmsType : int
+ using AmsType = DevAmsType;
+ static constexpr AmsType EXT_SPOOL = DevAmsType::EXT_SPOOL;
+ static constexpr AmsType AMS = DevAmsType::AMS;
+ static constexpr AmsType AMS_LITE = DevAmsType::AMS_LITE;
+ static constexpr AmsType N3F = DevAmsType::N3F;
+ static constexpr AmsType N3S = DevAmsType::N3S;
+ static constexpr AmsType AMS_LITE_MIXED = DevAmsType::AMS_LITE_MIXED;
+
+public:
+
+ enum class DryCtrlMode : int
{
- DUMMY = 0,
- AMS = 1, // AMS
- AMS_LITE = 2, // AMS-Lite
- N3F = 3, // N3F
- N3S = 4, // N3S
- AMS_LITE_MIXED = 5, // AMS-Lite mixed (A2L / N9). Presents as AMS_LITE to generic callers; see GetAmsType()/IsAmsLiteMixed().
+ Off = 0,
+ OnTime = 1,
+ OnHumidity = 2,
+ };
+
+ enum class DryStatus : char
+ {
+ Off = 0,
+ Checking = 1,
+ Drying = 2,
+ Cooling = 3,
+ Stopping = 4,
+ Error = 5,
+ CannotStopHeatOutofControl = 6,
+ PrdTesting = 7,
+ };
+
+ enum class DrySubStatus
+ {
+ Off = 0,
+ Heating = 1,
+ Dehumidify = 2,
+ };
+
+ enum class DryFanStatus : char
+ {
+ Off = 0,
+ On = 1,
+ };
+
+ enum class CannotDryReason : int
+ {
+ TaskOccupied = 0,
+ InsufficientPower = 1,
+ AmsBusy = 2,
+ ConsumableAtAmsOutlet = 3,
+ InitiatingAmsDrying = 4,
+ NotSupportedIn2dMode = 5,
+ DryingInProgress = 6,
+ Upgrading = 7,
+ InsufficientPowerNeedPluginPower = 8,
+ FilamentAtAmsOutletManualUnload = 10,
+ };
+
+ struct DrySettings
+ {
+ std::string dry_filament;
+ int dry_temp = -1; // -1 means invalid
+ int dry_hour = -1; // -1 means invalid, hours
};
public:
@@ -193,11 +252,20 @@ public:
int GetHumidityLevel() const { return m_humidity_level; }
int GetHumidityPercent() const { return m_humidity_percent; }
- // Only N3F/N3S dry. Written explicitly (not `> AMS_LITE`) so the new AMS_LITE_MIXED (N9) is
- // excluded; identical to the old expression for every pre-existing type.
- bool SupportDrying() const { return (m_ams_type == N3F) || (m_ams_type == N3S); }
+ bool SupportDrying() const { return m_ams_type == DevAmsType::N3F || m_ams_type == DevAmsType::N3S; }
int GetLeftDryTime() const { return m_left_dry_time; }
+ // remote drying control
+ bool IsSupportRemoteDry(const MachineObject* obj) const;
+ std::optional GetDryStatus() const { return m_dry_status; };
+ std::optional GetDrySubStatus() const { return m_dry_sub_status; }
+ std::optional GetFan1Status() const { return m_dry_fan1_status; }
+ std::optional GetFan2Status() const { return m_dry_fan2_status; }
+ std::optional> GetCannotDryReason() const { return m_dry_cannot_reasons; }
+ std::optional GetDrySettings() const { return m_dry_settings; };
+
+ bool AmsIsDrying();
+
private:
AmsType m_ams_type = AmsType::AMS;
std::string m_ams_id;
@@ -218,6 +286,14 @@ private:
int m_humidity_level = 5; // AmsType::AMS
int m_humidity_percent = -1; // N3F N3S, the percentage, -1 means invalid. eg. 100 means 100%
int m_left_dry_time = 0;
+
+ // see is_support_remote_dry
+ std::optional m_dry_status;
+ std::optional m_dry_sub_status;
+ std::optional m_dry_fan1_status;
+ std::optional m_dry_fan2_status;
+ std::optional> m_dry_cannot_reasons;
+ std::optional m_dry_settings;
};
/**
@@ -299,7 +375,11 @@ public:
public:
// ctrls
int CtrlAmsReset() const;
-
+
+ // crtl
+ int CtrlAmsStartDryingHour(int ams_id, std::string filament_type, int tag_temp, int tag_duration_hour, bool rotate_tray, int cooling_temp, bool close_power_conflict = false) const;
+ int CtrlAmsStopDrying(int ams_id) const;
+
public:
static bool IsBBL_Filament(std::string tag_uid);
@@ -339,4 +419,18 @@ public:
static void ParseAgentFilament(const json& data, MachineObject* obj, DevFilaSystem* system);
};
+struct DevFilamentDryingPreset
+{
+ std::string filament_id;
+
+ std::unordered_set ams_limitations; // only use ams types in the set
+ std::unordered_map filament_dev_ams_drying_time_on_idle; // hour
+ std::unordered_map filament_dev_ams_drying_temperature_on_idle;
+ std::unordered_map filament_dev_ams_drying_time_on_print; // hour
+ std::unordered_map filament_dev_ams_drying_temperature_on_print;
+ float filament_dev_drying_cooling_temperature = 0.0f;
+ float filament_dev_drying_softening_temperature = 0.0f;
+ float filament_dev_ams_drying_heat_distortion_temperature = 0.0f;
+};
+
}// namespace Slic3r
\ No newline at end of file
diff --git a/src/slic3r/GUI/DeviceCore/DevFilaSystemCtrl.cpp b/src/slic3r/GUI/DeviceCore/DevFilaSystemCtrl.cpp
index f81d6a2835..acdc81dd8c 100644
--- a/src/slic3r/GUI/DeviceCore/DevFilaSystemCtrl.cpp
+++ b/src/slic3r/GUI/DeviceCore/DevFilaSystemCtrl.cpp
@@ -16,4 +16,44 @@ int DevFilaSystem::CtrlAmsReset() const
return m_owner->publish_json(jj_command);
}
+int DevFilaSystem::CtrlAmsStartDryingHour(int ams_id,
+ std::string filament_type,
+ int tag_temp,
+ int tag_duration_hour,
+ bool rotate_tray,
+ int cooling_temp,
+ bool close_power_conflict) const
+{
+ json jj_command;
+ jj_command["print"]["command"] = "ams_filament_drying";
+ jj_command["print"]["sequence_id"] = std::to_string(MachineObject::m_sequence_id++);
+ jj_command["print"]["ams_id"] = ams_id;
+ jj_command["print"]["mode"] = static_cast(DevAms::DryCtrlMode::OnTime);
+ jj_command["print"]["filament"] = filament_type;
+ jj_command["print"]["temp"] = tag_temp;
+ jj_command["print"]["duration"] = tag_duration_hour;
+ jj_command["print"]["humidity"] = 0;
+ jj_command["print"]["rotate_tray"] = rotate_tray;
+ jj_command["print"]["cooling_temp"] = cooling_temp;
+ jj_command["print"]["close_power_conflict"] = close_power_conflict;
+ return m_owner->publish_json(jj_command);
+}
+
+int DevFilaSystem::CtrlAmsStopDrying(int ams_id) const
+{
+ json jj_command;
+ jj_command["print"]["command"] = "ams_filament_drying";
+ jj_command["print"]["sequence_id"] = std::to_string(MachineObject::m_sequence_id++);
+ jj_command["print"]["ams_id"] = ams_id;
+ jj_command["print"]["mode"] = static_cast(DevAms::DryCtrlMode::Off);
+ jj_command["print"]["filament"] = "";
+ jj_command["print"]["temp"] = 0;
+ jj_command["print"]["duration"] = 0;
+ jj_command["print"]["humidity"] = 0;
+ jj_command["print"]["rotate_tray"] = false;
+ jj_command["print"]["cooling_temp"] = 0;
+ jj_command["print"]["close_power_conflict"] = false;
+ return m_owner->publish_json(jj_command);
+}
+
}
\ No newline at end of file
diff --git a/src/slic3r/GUI/DeviceCore/DevMapping.cpp b/src/slic3r/GUI/DeviceCore/DevMapping.cpp
index cccc62d606..5dc16f4250 100644
--- a/src/slic3r/GUI/DeviceCore/DevMapping.cpp
+++ b/src/slic3r/GUI/DeviceCore/DevMapping.cpp
@@ -199,7 +199,7 @@ namespace Slic3r
}
}
FilamentInfo info;
- _parse_tray_info(atoi(tray.id.c_str()), 0, DevAms::DUMMY, tray, info);
+ _parse_tray_info(atoi(tray.id.c_str()), 0, DevAms::EXT_SPOOL, tray, info);
tray_filaments.emplace(std::make_pair(info.tray_id, info));
}
}
diff --git a/src/slic3r/GUI/DeviceCore/DevUtilBackend.cpp b/src/slic3r/GUI/DeviceCore/DevUtilBackend.cpp
index 4d909bf68f..32d30379f6 100644
--- a/src/slic3r/GUI/DeviceCore/DevUtilBackend.cpp
+++ b/src/slic3r/GUI/DeviceCore/DevUtilBackend.cpp
@@ -1,7 +1,11 @@
#include "DevUtilBackend.h"
#include "slic3r/GUI/BackgroundSlicingProcess.hpp"
+
#include "slic3r/GUI/Plater.hpp"
+#include "slic3r/GUI/GUI_App.hpp"
+
+#include
namespace Slic3r
{
@@ -15,4 +19,68 @@ std::shared_ptr DevUtilBackend::GetNozz
return nullptr;
}
+static std::unordered_map s_ams_type_map = {
+ {"0", DevAmsType::N3F},
+ {"1", DevAmsType::N3S},
+};
+
+std::optional DevUtilBackend::GetFilamentDryingPreset(const std::string& fila_id)
+{
+ if (fila_id.empty() || !GUI::wxGetApp().preset_bundle) {
+ return std::nullopt;
+ }
+
+ for (auto iter = GUI::wxGetApp().preset_bundle->filaments.begin(); iter != GUI::wxGetApp().preset_bundle->filaments.end(); ++iter) {
+ const Preset& filament_preset = *iter;
+ const auto& config = filament_preset.config;
+ if (filament_preset.filament_id == fila_id) {
+ DevFilamentDryingPreset info;
+ info.filament_id = fila_id;
+ try {
+ if (config.has("filament_dev_ams_drying_ams_limitations")) {
+ std::vector types = config.option("filament_dev_ams_drying_ams_limitations")->values;
+ for (auto type : types) {
+ if (s_ams_type_map.count(type) == 0) {
+ continue;
+ }
+ info.ams_limitations.insert(s_ams_type_map[type]);
+ }
+ }
+
+ if (config.has("filament_dev_ams_drying_temperature")) {
+ info.filament_dev_ams_drying_temperature_on_idle[DevAmsType::N3F] = config.option("filament_dev_ams_drying_temperature")->get_at(0);
+ info.filament_dev_ams_drying_temperature_on_idle[DevAmsType::N3S] = config.option("filament_dev_ams_drying_temperature")->get_at(1);
+ info.filament_dev_ams_drying_temperature_on_print[DevAmsType::N3F] = config.option("filament_dev_ams_drying_temperature")->get_at(2);
+ info.filament_dev_ams_drying_temperature_on_print[DevAmsType::N3S] = config.option("filament_dev_ams_drying_temperature")->get_at(3);
+ }
+
+ if (config.has("filament_dev_ams_drying_time")) {
+ info.filament_dev_ams_drying_time_on_idle[DevAmsType::N3F] = config.option("filament_dev_ams_drying_time")->get_at(0);
+ info.filament_dev_ams_drying_time_on_idle[DevAmsType::N3S] = config.option("filament_dev_ams_drying_time")->get_at(1);
+ info.filament_dev_ams_drying_time_on_print[DevAmsType::N3F] = config.option("filament_dev_ams_drying_time")->get_at(2);
+ info.filament_dev_ams_drying_time_on_print[DevAmsType::N3S] = config.option("filament_dev_ams_drying_time")->get_at(3);
+ }
+
+ if (config.has("filament_dev_drying_softening_temperature")) {
+ info.filament_dev_drying_softening_temperature = config.option("filament_dev_drying_softening_temperature")->get_at(0);
+ }
+
+ if (config.has("filament_dev_ams_drying_heat_distortion_temperature")) {
+ info.filament_dev_ams_drying_heat_distortion_temperature = config.option("filament_dev_ams_drying_heat_distortion_temperature")->get_at(0);
+ }
+
+ if (config.has("filament_dev_drying_cooling_temperature")) {
+ info.filament_dev_drying_cooling_temperature = config.option("filament_dev_drying_cooling_temperature")->get_at(0);
+ }
+
+ return info;
+ } catch (const std::exception& e) {
+ BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " exception: " << e.what();
+ }
+ }
+ }
+
+ return std::nullopt;
+}
+
}; // namespace Slic3r
diff --git a/src/slic3r/GUI/DeviceCore/DevUtilBackend.h b/src/slic3r/GUI/DeviceCore/DevUtilBackend.h
index 54839d9315..5c7869f1e4 100644
--- a/src/slic3r/GUI/DeviceCore/DevUtilBackend.h
+++ b/src/slic3r/GUI/DeviceCore/DevUtilBackend.h
@@ -1,18 +1,15 @@
/**
* @file DevUtilBackend.h
- * @brief Static helpers bridging the backend (slicer/preset) to the device GUI.
- *
- * Scope (H2C nozzle rack): exposes only GetNozzleGroupResult — the accessor the print-dispatch
- * nozzle-mapping V1 request and result-resolve helpers read. Related helpers (per-nozzle info
- * collection via the ExtruderNozzleInfos/NozzleDef type pair, and the drying-preset lookup) are
- * not implemented here and left to the consuming code.
+ * @brief Provides common static utility methods for backend (preset/slicing).
*/
#pragma once
+#include "DevDefs.h"
+#include "DevFilaSystem.h"
#include "libslic3r/MultiNozzleUtils.hpp"
-
-#include
+#include
+#include
namespace Slic3r
{
@@ -23,10 +20,15 @@ class DevUtilBackend
public:
DevUtilBackend() = delete;
+public:
+
// for rack: the slicer's per-filament -> logical-nozzle grouping for the current plate, read off
// the post-slice GCodeProcessorResult (plater->background_process().get_current_gcode_result()).
// Returns nullptr when there is no plater / no current result.
static std::shared_ptr GetNozzleGroupResult(Slic3r::GUI::Plater* plater);
+
+ // for filament preset
+ static std::optional GetFilamentDryingPreset(const std::string& fila_id);
};
}; // namespace Slic3r
diff --git a/src/slic3r/GUI/DeviceManager.cpp b/src/slic3r/GUI/DeviceManager.cpp
index 527fe4f372..b23528a372 100644
--- a/src/slic3r/GUI/DeviceManager.cpp
+++ b/src/slic3r/GUI/DeviceManager.cpp
@@ -568,7 +568,7 @@ MachineObject::MachineObject(DeviceManager* manager, NetworkAgent* agent, std::s
m_extder_system = new DevExtderSystem(this);
m_extension_tool = DevExtensionTool::Create(this);
m_nozzle_system = new DevNozzleSystem(this);
- m_fila_system = new DevFilaSystem(this);
+ m_fila_system = std::make_shared(this);
m_fila_switch = new DevFilaSwitch(this);
m_hms_system = new DevHMS(this);
m_config = new DevConfig(this);
@@ -617,8 +617,6 @@ MachineObject::~MachineObject()
delete m_ctrl;
m_ctrl = nullptr;
- delete m_fila_system;
- m_fila_system = nullptr;
delete m_fila_switch;
m_fila_switch = nullptr;
@@ -3770,7 +3768,7 @@ int MachineObject::parse_json(std::string tunnel, std::string payload, bool key_
update_printer_preset_name();
update_filament_list();
if (jj.contains("ams")) {
- DevFilaSystemParser::ParseV1_0(jj, this, m_fila_system, key_field_only);
+ DevFilaSystemParser::ParseV1_0(jj, this, m_fila_system.get(), key_field_only);
}
/* vitrual tray*/
@@ -5095,6 +5093,7 @@ void MachineObject::parse_new_info(json print)
// fun2 may have infinite length, use get_flag_bits_no_border
if (!fun2.empty()) {
is_support_print_with_emmc = get_flag_bits_no_border(fun2, 0) == 1;
+ is_support_remote_dry = (get_flag_bits_no_border(fun2, 5) == 1);
is_support_check_track_switch_match_slice_printer = get_flag_bits_no_border(fun2, 19) == 1;
}
diff --git a/src/slic3r/GUI/DeviceManager.hpp b/src/slic3r/GUI/DeviceManager.hpp
index 773bdc200b..4ce4cad8de 100644
--- a/src/slic3r/GUI/DeviceManager.hpp
+++ b/src/slic3r/GUI/DeviceManager.hpp
@@ -117,7 +117,7 @@ private:
std::shared_ptr m_extension_tool;
DevExtderSystem* m_extder_system;
DevNozzleSystem* m_nozzle_system;
- DevFilaSystem* m_fila_system;
+ std::shared_ptr m_fila_system;
DevFilaSwitch* m_fila_switch;
DevFan* m_fan;
DevBed * m_bed;
@@ -342,7 +342,7 @@ public:
std::shared_ptr get_nozzle_mapping_result() const { return m_nozzle_mapping_ptr; }
void clear_auto_nozzle_mapping();// defined in DevMappingNozzle.cpp
- DevFilaSystem* GetFilaSystem() const { return m_fila_system;}
+ std::shared_ptr GetFilaSystem() const { return m_fila_system;}
DevFilaSwitch* GetFilaSwitch() const { return m_fila_switch;}
bool HasAms() const;
@@ -635,6 +635,7 @@ public:
// fun2
bool is_support_print_with_emmc{false};
+ bool is_support_remote_dry = false;
bool is_support_check_track_switch_match_slice_printer{false};
bool installed_upgrade_kit{false};
diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp
index 61b3e52a98..3b3d4248e3 100644
--- a/src/slic3r/GUI/GCodeViewer.cpp
+++ b/src/slic3r/GUI/GCodeViewer.cpp
@@ -445,7 +445,7 @@ void GCodeViewer::SequentialView::Marker::render_position_window(const libvgcode
add_row(_u8L("Flow rate"), buff);
sprintf(buff, "%.0f %%", vertex.fan_speed);
add_row(_u8L("Fan speed"), buff);
- sprintf(buff, ("%.0f " + _u8L("°C")).c_str(), vertex.temperature);
+ sprintf(buff, ("%.0f " + _u8L("\u2103" /* °C */)).c_str(), vertex.temperature);
add_row(_u8L("Temperature"), buff);
sprintf(buff, "%.4f", vertex.pressure_advance);
add_row(_u8L("Pressure Advance"), buff);
@@ -3711,7 +3711,7 @@ void GCodeViewer::render_legend(float &legend_height, int canvas_width, int canv
break;
}
case libvgcode::EViewType::FanSpeed: { imgui.title(_u8L("Fan speed (%)")); break; }
- case libvgcode::EViewType::Temperature: { imgui.title(_u8L("Temperature (°C)")); break; }
+ case libvgcode::EViewType::Temperature: { imgui.title(_u8L("Temperature (℃)")); break; }
// ORCA: Add Pressure Advance visualization support
case libvgcode::EViewType::PressureAdvance:{ imgui.title(_u8L("Pressure Advance")); break; }
case libvgcode::EViewType::VolumetricFlowRate:
diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp
index b279c668c2..47ed2ed714 100644
--- a/src/slic3r/GUI/GLCanvas3D.cpp
+++ b/src/slic3r/GUI/GLCanvas3D.cpp
@@ -2338,11 +2338,6 @@ void GLCanvas3D::remove_curr_plate_all()
m_dirty = true;
}
-void GLCanvas3D::update_plate_thumbnails()
-{
- _update_imgui_select_plate_toolbar();
-}
-
void GLCanvas3D::select_all()
{
if (!m_gizmos.is_allow_select_all()) {
@@ -3228,7 +3223,6 @@ void GLCanvas3D::on_idle(wxIdleEvent& evt)
// BBS
//m_dirty |= wxGetApp().plater()->get_view_toolbar().update_items_state();
m_dirty |= wxGetApp().plater()->get_collapse_toolbar().update_items_state();
- _update_imgui_select_plate_toolbar();
bool mouse3d_controller_applied = wxGetApp().plater()->get_mouse3d_controller().apply(wxGetApp().plater()->get_camera());
m_dirty |= mouse3d_controller_applied;
m_dirty |= wxGetApp().plater()->get_notification_manager()->update_notifications(*this);
@@ -4838,11 +4832,6 @@ void GLCanvas3D::force_set_focus() {
void GLCanvas3D::on_set_focus(wxFocusEvent& evt)
{
m_tooltip_enabled = false;
- if (m_canvas_type == ECanvasType::CanvasPreview) {
- // update thumbnails and update plate toolbar
- wxGetApp().plater()->update_all_plate_thumbnails();
- _update_imgui_select_plate_toolbar();
- }
_refresh_if_shown_on_screen();
m_tooltip_enabled = true;
m_is_touchpad_navigation = wxGetApp().app_config->get_bool("camera_navigation_style");
@@ -6925,13 +6914,28 @@ void GLCanvas3D::_update_select_plate_toolbar_stats_item(bool force_selected) {
bool GLCanvas3D::_update_imgui_select_plate_toolbar()
{
bool result = true;
- if (!m_sel_plate_toolbar.is_enabled() || m_sel_plate_toolbar.is_render_finish) return false;
+ if (!m_sel_plate_toolbar.is_enabled()) {
+ return false;
+ }
+
+ const auto& p_plater = wxGetApp().plater();
+ if (!p_plater) {
+ return false;
+ }
+
+ if (!p_plater->is_plate_toolbar_image_dirty()) {
+ return false;
+ }
+
+ if (!p_plater->is_gcode_3mf()) {
+ p_plater->update_all_plate_thumbnails(true);
+ }
_update_select_plate_toolbar_stats_item();
m_sel_plate_toolbar.del_all_item();
- PartPlateList& plate_list = wxGetApp().plater()->get_partplate_list();
+ PartPlateList& plate_list = p_plater->get_partplate_list();
for (int i = 0; i < plate_list.get_plate_count(); i++) {
IMToolbarItem* item = new IMToolbarItem();
PartPlate* plate = plate_list.get_plate(i);
@@ -6944,7 +6948,7 @@ bool GLCanvas3D::_update_imgui_select_plate_toolbar()
}
m_sel_plate_toolbar.m_items.push_back(item);
}
-
+ p_plater->clear_plate_toolbar_image_dirty();
m_sel_plate_toolbar.is_display_scrollbar = false;
return result;
}
@@ -8802,6 +8806,8 @@ void GLCanvas3D::_render_imgui_select_plate_toolbar()
return;
}
+ _update_imgui_select_plate_toolbar();
+
IMToolbarItem* all_plates_stats_item = m_sel_plate_toolbar.m_all_plates_stats_item;
PartPlateList& plate_list = wxGetApp().plater()->get_partplate_list();
@@ -9218,7 +9224,6 @@ void GLCanvas3D::_render_imgui_select_plate_toolbar()
m_sel_plate_toolbar.is_display_scrollbar = is_win_hovered;
imgui.end();
- m_sel_plate_toolbar.is_render_finish = true;
}
//BBS: GUI refactor: GLToolbar adjust
diff --git a/src/slic3r/GUI/GLCanvas3D.hpp b/src/slic3r/GUI/GLCanvas3D.hpp
index 4c2402c546..55e118fa4c 100644
--- a/src/slic3r/GUI/GLCanvas3D.hpp
+++ b/src/slic3r/GUI/GLCanvas3D.hpp
@@ -997,7 +997,6 @@ public:
void select_curr_plate_all();
void select_object_from_idx(std::vector& object_idxs);
void remove_curr_plate_all();
- void update_plate_thumbnails();
void select_all();
void deselect_all();
diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp
index bb9e9010af..ed990c3ec6 100644
--- a/src/slic3r/GUI/GUI_App.cpp
+++ b/src/slic3r/GUI/GUI_App.cpp
@@ -4775,7 +4775,7 @@ void GUI_App::request_user_logout(const std::string& provider/* = ORCA_CLOUD_PRO
remove_user_presets();
enable_user_preset_folder(false);
Slic3r::PluginManager::instance().unload_cloud_plugins();
- Slic3r::PluginManager::instance().clear_cloud_plugin_catalog();
+ Slic3r::PluginManager::instance().clear_cloud_plugin_metadata();
Slic3r::PluginManager::instance().set_cloud_user("");
preset_bundle->load_user_presets(DEFAULT_USER_FOLDER_NAME, ForwardCompatibilitySubstitutionRule::Enable);
mainframe->update_side_preset_ui();
@@ -8255,22 +8255,32 @@ void GUI_App::open_plugins_dialog(size_t open_on_tab, const std::string& highlig
void GUI_App::open_terminal_dialog()
{
- if (m_terminal_dlg) {
+ // Reached from the plugins dialog's webview ("open_terminal" command), i.e. from
+ // inside the webview script-message callback, which GTK/macOS deliver synchronously
+ // (see ui_create_window in PluginHostUi.cpp). TerminalDialog hosts a webview of its
+ // own, so creating or presenting it on that stack is the same class as the Linux
+ // gtk_window_present crash — defer all window work to a clean main-loop iteration.
+ CallAfter([this]() {
+ if (m_terminal_dlg) {
+ // Re-front the existing window; guard Show() per #13657 (GTK re-enters
+ // layout when showing an already-visible window).
+ if (!m_terminal_dlg->IsShown())
+ m_terminal_dlg->Show();
+ m_terminal_dlg->Raise();
+ return;
+ }
+
+ m_terminal_dlg = new TerminalDialog(mainframe, wxID_ANY, _L("Plugin Terminal"),
+ wxDefaultPosition, wxSize(820, 600));
+ m_terminal_dlg->Bind(wxEVT_DESTROY, [this](wxWindowDestroyEvent& event) {
+ if (event.GetEventObject() == m_terminal_dlg)
+ m_terminal_dlg = nullptr;
+ event.Skip();
+ });
+
+ // Show() alone activates and fronts a freshly created window on every platform.
m_terminal_dlg->Show();
- m_terminal_dlg->Raise();
- return;
- }
-
- m_terminal_dlg = new TerminalDialog(mainframe, wxID_ANY, _L("Plugin Terminal"),
- wxDefaultPosition, wxSize(820, 600));
- m_terminal_dlg->Bind(wxEVT_DESTROY, [this](wxWindowDestroyEvent& event) {
- if (event.GetEventObject() == m_terminal_dlg)
- m_terminal_dlg = nullptr;
- event.Skip();
});
-
- m_terminal_dlg->Show();
- m_terminal_dlg->Raise();
}
void GUI_App::open_exportpresetbundledialog(size_t open_on_tab, const std::string& highlight_option)
diff --git a/src/slic3r/GUI/IMToolbar.cpp b/src/slic3r/GUI/IMToolbar.cpp
index 733683366f..2a569c960b 100644
--- a/src/slic3r/GUI/IMToolbar.cpp
+++ b/src/slic3r/GUI/IMToolbar.cpp
@@ -61,8 +61,6 @@ void IMToolbar::del_stats_item()
void IMToolbar::set_enabled(bool enable)
{
m_enabled = enable;
- if (!m_enabled)
- is_render_finish = false;
}
bool IMReturnToolbar::init()
diff --git a/src/slic3r/GUI/IMToolbar.hpp b/src/slic3r/GUI/IMToolbar.hpp
index 24152f20ee..bf5bf70756 100644
--- a/src/slic3r/GUI/IMToolbar.hpp
+++ b/src/slic3r/GUI/IMToolbar.hpp
@@ -51,7 +51,6 @@ public:
float icon_height;
bool is_display_scrollbar;
bool show_stats_item{ false };
- bool is_render_finish{false};
IMToolbar() {
icon_width = DEFAULT_TOOLBAR_BUTTON_WIDTH;
icon_height = DEFAULT_TOOLBAR_BUTTON_HEIGHT;
diff --git a/src/slic3r/GUI/ImGuiWrapper.cpp b/src/slic3r/GUI/ImGuiWrapper.cpp
index c7609be5ef..8974a169d1 100644
--- a/src/slic3r/GUI/ImGuiWrapper.cpp
+++ b/src/slic3r/GUI/ImGuiWrapper.cpp
@@ -3158,6 +3158,9 @@ void ImGuiWrapper::render_draw_data(ImDrawData *draw_data)
shader->set_uniform("Texture", 0);
shader->set_uniform("ProjMtx", ortho_projection);
+ const uint8_t stage = 0;
+ shader->set_uniform("s_texture", stage);
+
// Will project scissor/clipping rectangles into framebuffer space
const ImVec2 clip_off = draw_data->DisplayPos; // (0,0) unless using multi-viewports
const ImVec2 clip_scale = draw_data->FramebufferScale; // (1,1) unless using retina display which are often (2,2)
@@ -3222,6 +3225,7 @@ void ImGuiWrapper::render_draw_data(ImDrawData *draw_data)
glsafe(::glScissor((int)clip_min.x, (int)(fb_height - clip_max.y), (int)(clip_max.x - clip_min.x), (int)(clip_max.y - clip_min.y)));
// Bind texture, Draw
+ glsafe(::glActiveTexture(GL_TEXTURE0 + stage));
glsafe(::glBindTexture(GL_TEXTURE_2D, (GLuint)(intptr_t)pcmd->GetTexID()));
glsafe(::glDrawElements(GL_TRIANGLES, (GLsizei)pcmd->ElemCount, sizeof(ImDrawIdx) == 2 ? GL_UNSIGNED_SHORT : GL_UNSIGNED_INT, (void*)(intptr_t)(pcmd->IdxOffset * sizeof(ImDrawIdx))));
}
diff --git a/src/slic3r/GUI/Jobs/FillBedJob.cpp b/src/slic3r/GUI/Jobs/FillBedJob.cpp
index 711228c0bc..a9a66cb96f 100644
--- a/src/slic3r/GUI/Jobs/FillBedJob.cpp
+++ b/src/slic3r/GUI/Jobs/FillBedJob.cpp
@@ -348,6 +348,8 @@ void FillBedJob::finalize(bool canceled, std::exception_ptr &eptr)
}
m_plater->update();
}
+
+ m_plater->mark_plate_toolbar_image_dirty();
}
}} // namespace Slic3r::GUI
diff --git a/src/slic3r/GUI/OpenGLManager.cpp b/src/slic3r/GUI/OpenGLManager.cpp
index 2b5ebcd086..382a9176b9 100644
--- a/src/slic3r/GUI/OpenGLManager.cpp
+++ b/src/slic3r/GUI/OpenGLManager.cpp
@@ -266,7 +266,12 @@ bool OpenGLManager::init_gl(bool popup_error)
else
s_compressed_textures_supported = false;
- if (GLAD_GL_ARB_framebuffer_object) {
+ if (s_gl_info.is_version_greater_or_equal_to(3, 0)) {
+ // ARB framebuffer became a mandatory part of core OpenGL 3.0
+ s_framebuffers_type = EFramebufferType::Arb;
+ BOOST_LOG_TRIVIAL(info) << "Opengl version >= 30, FrameBuffer Type ARB." << std::endl;
+ }
+ else if (GLAD_GL_ARB_framebuffer_object) {
s_framebuffers_type = EFramebufferType::Arb;
BOOST_LOG_TRIVIAL(info) << "Found Framebuffer Type ARB."<< std::endl;
}
diff --git a/src/slic3r/GUI/PartPlate.cpp b/src/slic3r/GUI/PartPlate.cpp
index 1f0454c778..415e1ea0f5 100644
--- a/src/slic3r/GUI/PartPlate.cpp
+++ b/src/slic3r/GUI/PartPlate.cpp
@@ -2433,6 +2433,9 @@ void PartPlate::set_pos_and_size(Vec3d& origin, int width, int depth, int height
m_depth = depth;
m_height = height;
+ if (with_instance_move && m_plater)
+ m_plater->mark_plate_toolbar_image_dirty();
+
return;
}
@@ -2780,6 +2783,8 @@ int PartPlate::add_instance(int obj_id, int instance_id, bool move_position, Bou
}
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": plate %1% , m_ready_for_slice changes to %2%") % m_plate_index %m_ready_for_slice;
+ if (m_plater)
+ m_plater->mark_plate_toolbar_image_dirty();
return 0;
}
@@ -5344,6 +5349,9 @@ int PartPlateList::notify_instance_removed(int obj_id, int instance_id)
unprintable_plate.update_object_index(obj_id, m_model->objects.size());
}
+ if (m_plater)
+ m_plater->mark_plate_toolbar_image_dirty();
+
return 0;
}
@@ -6265,6 +6273,9 @@ int PartPlateList::rebuild_plates_after_arrangement(bool recycle_plates, bool ex
}
#endif
+ if (m_plater)
+ m_plater->mark_plate_toolbar_image_dirty();
+
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(":after rebuild, plates count %1%") % m_plate_list.size();
return ret;
}
diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp
index 0f858e3c90..dbff5162fc 100644
--- a/src/slic3r/GUI/Plater.cpp
+++ b/src/slic3r/GUI/Plater.cpp
@@ -290,6 +290,21 @@ void Plater::show_illegal_characters_warning(wxWindow* parent)
show_error(parent, _L("Invalid name, the following characters are not allowed:") + " <>:/\\|?*\"");
}
+void Plater::mark_plate_toolbar_image_dirty()
+{
+ m_b_plate_toolbar_image_dirty = true;
+}
+
+bool Plater::is_plate_toolbar_image_dirty() const
+{
+ return m_b_plate_toolbar_image_dirty;
+}
+
+void Plater::clear_plate_toolbar_image_dirty()
+{
+ m_b_plate_toolbar_image_dirty = false;
+}
+
static std::map bed_type_thumbnails = {
{BedType::btPC, "bed_cool" },
{BedType::btEP, "bed_engineering" },
@@ -6348,10 +6363,8 @@ void Plater::priv::update(unsigned int flags)
//BBS assemble view
this->assemble_view->reload_scene(false, flags);
- if (current_panel && is_preview_shown()) {
- q->force_update_all_plate_thumbnails();
- //update_fff_scene_only_shells(true);
- }
+ // todo: better to mark thumbnail dirty here
+ q->mark_plate_toolbar_image_dirty();
if (force_background_processing_restart)
this->restart_background_process(update_status);
@@ -7906,6 +7919,7 @@ std::vector Plater::priv::load_files(const std::vector& input_
}
}
q->schedule_background_process(true);
+ q->mark_plate_toolbar_image_dirty();
return obj_idxs;
}
@@ -8285,6 +8299,8 @@ void Plater::priv::object_list_changed()
main_frame->update_slice_print_status(MainFrame::eEventObjectUpdate, can_slice);
wxGetApp().params_panel()->notify_object_config_changed();
+
+ q->mark_plate_toolbar_image_dirty();
}
void Plater::priv::select_curr_plate_all()
@@ -9188,6 +9204,8 @@ void Plater::priv::update_fff_scene()
view3D->reload_scene(true);
//BBS: add assemble view related logic
assemble_view->reload_scene(true);
+
+ q->mark_plate_toolbar_image_dirty();
}
//BBS: add print project related logic
@@ -10096,9 +10114,6 @@ void Plater::priv::set_current_panel(wxPanel* panel, bool no_slice)
preview->get_canvas3d()->enable_select_plate_toolbar(true);
}
}
- else {
- preview->get_canvas3d()->enable_select_plate_toolbar(false);
- }
if (current_panel == panel)
{
@@ -10957,6 +10972,7 @@ void Plater::priv::on_process_completed(SlicingProcessCompletedEvent &evt)
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(":finished, reload print soon");
m_is_slicing = false;
this->preview->reload_print(false);
+ q->mark_plate_toolbar_image_dirty();
/* BBS if in publishing progress */
if (m_is_publishing) {
if (m_publish_dlg && !m_publish_dlg->was_cancelled()) {
@@ -13940,8 +13956,13 @@ void adjust_settings_for_flowrate_calib(ModelObjectPtrs& objects, bool linear, i
_obj->config.set_key_value("seam_slope_type", new ConfigOptionEnum(SeamScarfType::None));
_obj->config.set_key_value("gap_fill_target", new ConfigOptionEnum(GapFillTarget::gftNowhere));
print_config->set_key_value("max_volumetric_extrusion_rate_slope", new ConfigOptionFloat(0));
- // ORCA: print the top surface spiral from the center outwards, so the tiles are comparable.
- _obj->config.set_key_value("top_surface_fill_order", new ConfigOptionEnum(SurfaceFillOrder::Outward));
+ // ORCA: request the calibration's special toolpath order (chords first, center spiral
+ // last and inside-out) so opposing directions collide into the tactile lip the test
+ // reads. The special order only applies while the fill order is Default, so reset the
+ // profile's fill order on the calibration objects; changing the setting on the object
+ // afterwards deliberately overrides the special order.
+ _obj->config.set_key_value("calib_flowrate_topinfill_special_order", new ConfigOptionBool(true));
+ _obj->config.set_key_value("top_surface_fill_order", new ConfigOptionEnum(SurfaceFillOrder::Default));
// extract flowrate from name, filename format: flowrate_xxx
std::string obj_name = _obj->name;
@@ -14678,6 +14699,7 @@ void Plater::invalid_all_plate_thumbnails()
plate->thumbnail_data.reset();
plate->no_light_thumbnail_data.reset();
}
+ mark_plate_toolbar_image_dirty();
}
void Plater::force_update_all_plate_thumbnails()
@@ -14688,7 +14710,6 @@ void Plater::force_update_all_plate_thumbnails()
invalid_all_plate_thumbnails();
update_all_plate_thumbnails(true);
}
- get_preview_canvas3D()->update_plate_thumbnails();
}
// BBS: backup
diff --git a/src/slic3r/GUI/Plater.hpp b/src/slic3r/GUI/Plater.hpp
index 48037a9a9e..82e56e68e0 100644
--- a/src/slic3r/GUI/Plater.hpp
+++ b/src/slic3r/GUI/Plater.hpp
@@ -955,6 +955,10 @@ public:
bool is_loading_project() const { return m_loading_project; }
+ void mark_plate_toolbar_image_dirty();
+ bool is_plate_toolbar_image_dirty() const;
+ void clear_plate_toolbar_image_dirty();
+
private:
struct priv;
std::unique_ptr p;
@@ -975,6 +979,7 @@ private:
std::string m_preview_only_filename;
int m_valid_plates_count { 0 };
int m_check_status = 0; // 0 not check, 1 check success, 2 check failed
+ bool m_b_plate_toolbar_image_dirty{ true };
void suppress_snapshots();
void allow_snapshots();
diff --git a/src/slic3r/GUI/PluginWebDialog.cpp b/src/slic3r/GUI/PluginWebDialog.cpp
index b316cb1fe0..974d87f5e3 100644
--- a/src/slic3r/GUI/PluginWebDialog.cpp
+++ b/src/slic3r/GUI/PluginWebDialog.cpp
@@ -163,14 +163,6 @@ PluginWebDialog* PluginWebDialog::create_modeless_dialog(wxWindow* parent,
std::move(on_destroyed));
}
-void PluginWebDialog::show_modeless_dialog(PluginWebDialog* dialog)
-{
- if (dialog == nullptr)
- return;
- dialog->Show();
- dialog->Raise();
-}
-
void PluginWebDialog::post_message(PluginWebDialog* dialog, const nlohmann::json& data)
{
if (dialog != nullptr && dialog->is_open())
diff --git a/src/slic3r/GUI/PluginWebDialog.hpp b/src/slic3r/GUI/PluginWebDialog.hpp
index 1ac025e6f5..24dc945980 100644
--- a/src/slic3r/GUI/PluginWebDialog.hpp
+++ b/src/slic3r/GUI/PluginWebDialog.hpp
@@ -52,7 +52,6 @@ public:
MessageHandler on_message,
CloseHandler on_close,
CloseHandler on_destroyed);
- static void show_modeless_dialog(PluginWebDialog* dialog);
static void post_message(PluginWebDialog* dialog, const nlohmann::json& data);
static void request_close(PluginWebDialog* dialog);
diff --git a/src/slic3r/GUI/PluginsDialog.cpp b/src/slic3r/GUI/PluginsDialog.cpp
index cddc090c84..74911a38b9 100644
--- a/src/slic3r/GUI/PluginsDialog.cpp
+++ b/src/slic3r/GUI/PluginsDialog.cpp
@@ -116,8 +116,8 @@ struct PluginDialogItem
PluginAvailableActions available_actions;
};
-constexpr bool kFetchCloudCatalog = true;
-constexpr bool kUseCurrentCloudCatalog = false;
+constexpr bool kFetchCloudMeta = true;
+constexpr bool kUseCurrentCloudMeta = false;
// The type of a package's first loaded capability. A package that is not loaded has no type.
PluginCapabilityType primary_capability_type_of(PluginManager& manager, const std::string& plugin_key)
@@ -126,7 +126,7 @@ PluginCapabilityType primary_capability_type_of(PluginManager& manager, const st
return capabilities.empty() ? PluginCapabilityType::Unknown : capabilities.front()->type();
}
-std::vector current_cloud_catalog_snapshot()
+std::vector current_cloud_metadata_snapshot()
{
std::vector cloud_entries;
for (const PluginDescriptor& entry : PluginManager::instance().get_plugin_descriptors(/*include_invalid=*/true))
@@ -147,20 +147,18 @@ PluginDescriptor as_cloud_only_descriptor(PluginDescriptor descriptor)
return descriptor;
}
-// rescan_plugins() clears the whole catalog and rediscovers only local packages, so when no fresh cloud
-// data is needed, restore the current cloud rows afterwards or cloud-only rows would disappear.
-void refresh_plugin_catalog_blocking(bool fetch_cloud)
+void refresh_plugin_metadata_blocking(bool fetch_cloud)
{
PluginManager& manager = PluginManager::instance();
std::vector not_found, unauthorized;
- const std::vector current_cloud_catalog = fetch_cloud ? std::vector{} :
- current_cloud_catalog_snapshot();
+ const std::vector current_cloud_metadata = fetch_cloud ? std::vector{} :
+ current_cloud_metadata_snapshot();
manager.rescan_plugins();
if (!fetch_cloud) {
- manager.update_cloud_catalog(current_cloud_catalog);
+ manager.update_cloud_metadata(current_cloud_metadata);
return;
}
@@ -450,7 +448,7 @@ void PluginsDialog::set_open_terminal_dlg_fn()
void PluginsDialog::update_plugin_dialog_ui()
{
- // Called after the shared catalog is already updated, for example from the
+ // Called after the any metadata is already updated, for example from the
// cloud-plugin state callback. Do not fetch here or the callback can re-enter.
send_plugins();
resolve_pending_activation();
@@ -482,10 +480,21 @@ void PluginsDialog::on_script_message(const nlohmann::json& payload)
if (handle_common_script_command(payload))
return;
+ // Defer command handling out of the webview script-message callback: GTK and macOS
+ // deliver it synchronously inside the native webview callback (see ui_create_window
+ // in PluginHostUi.cpp), and window work on that stack is the crash class fixed in
+ // b779a7bfed/f2ccbfc8b5. Deferring at this single entry point keeps every command
+ // handler, current and future, off that stack by construction.
+ wxGetApp().CallAfter([this, alive = m_alive, payload]() {
+ if (alive->load(std::memory_order_acquire))
+ handle_web_command(payload);
+ });
+}
+
+void PluginsDialog::handle_web_command(const nlohmann::json& payload)
+{
const std::string command = payload.value("command", "");
if (command == "request_plugins") {
- // Discovery already runs at startup and on login, so the shared catalog is up to date by the
- // time the dialog opens: render it without a blocking fetch. Refresh is what rediscovers.
send_plugins();
} else if (command == "refresh_plugins") {
refresh_plugins();
@@ -498,8 +507,8 @@ void PluginsDialog::on_script_message(const nlohmann::json& payload)
install_plugin_from_file();
} else if (command == "plugin_menu_action") {
handle_plugin_menu_action(payload.value("plugin_key", ""), payload.value("action", ""));
- } else if (command == "run_script_plugin") {
- run_script_plugin(payload.value("plugin_key", ""), payload.value("capability_name", ""));
+ } else if (command == "run_script_plugin_capability") {
+ run_script_plugin_capability(payload.value("plugin_key", ""), payload.value("capability_name", ""));
} else if (command == "open_terminal") {
m_open_terminal_dlg_fn();
} else if (command == "update_plugin") {
@@ -575,16 +584,16 @@ bool PluginsDialog::get_descriptor(const std::string& plugin_key, PluginDescript
return manager.try_get_plugin_descriptor(plugin_key, descriptor) && descriptor.is_invalid_package();
}
-void PluginsDialog::refresh_plugin_catalog_async(const wxString& title, const wxString& message, bool fetch_cloud)
+void PluginsDialog::refresh_plugin_metadata_async(const wxString& title, const wxString& message, bool fetch_cloud)
{
- run_with_dialog([fetch_cloud]() { refresh_plugin_catalog_blocking(fetch_cloud); }, [this]() { send_plugins(); }, title, message);
+ run_with_dialog([fetch_cloud]() { refresh_plugin_metadata_blocking(fetch_cloud); }, [this]() { send_plugins(); }, title, message);
}
void PluginsDialog::refresh_plugins()
{
BOOST_LOG_TRIVIAL(info) << "Refreshing plugins from Plugins dialog";
- refresh_plugin_catalog_async(_L("Refreshing"), _L("Refreshing plugins data"), kFetchCloudCatalog);
+ refresh_plugin_metadata_async(_L("Refreshing"), _L("Refreshing plugins data"), kFetchCloudMeta);
}
void PluginsDialog::toggle_plugin(const std::string& plugin_key, bool enabled)
@@ -596,7 +605,6 @@ void PluginsDialog::toggle_plugin(const std::string& plugin_key, bool enabled)
PluginDescriptor row_data;
if (!get_descriptor(plugin_key, row_data)) {
- // The catalog changed under the UI. Toggling is local, so just re-render from it.
send_plugins();
return;
}
@@ -688,7 +696,6 @@ void PluginsDialog::toggle_plugin_capability(const std::string& plugin_key,
PluginDescriptor row_data;
if (!get_descriptor(plugin_key, row_data)) {
- // The catalog changed under the UI. Toggling is local, so just re-render from it.
send_plugins();
return;
}
@@ -741,12 +748,24 @@ void PluginsDialog::handle_plugin_menu_action(const std::string& plugin_key, con
}
}
+void PluginsDialog::restore_z_order()
+{
+ // Deferred so it runs on a clean stack after the modal has fully torn down; the
+ // alive guard covers the dialog being destroyed while the CallAfter is queued.
+ wxGetApp().CallAfter([this, alive = m_alive]() {
+ if (alive->load(std::memory_order_acquire) && IsShown())
+ Raise();
+ });
+}
+
void PluginsDialog::install_plugin_from_file()
{
wxFileDialog dialog(this, _L("Select plugin package"), wxEmptyString, wxEmptyString, _L("Plugin files (*.py;*.whl)|*.py;*.whl"),
wxFD_OPEN | wxFD_FILE_MUST_EXIST);
- if (dialog.ShowModal() != wxID_OK)
+ const int rc = dialog.ShowModal();
+ restore_z_order();
+ if (rc != wxID_OK)
return;
if (!install_plugin_package(dialog.GetPath().ToUTF8().data())) {
@@ -799,7 +818,9 @@ bool PluginsDialog::install_plugin_package(const std::string& package_path)
plugin_name),
kOverwritePluginTitle, wxOK | wxCANCEL | wxCANCEL_DEFAULT | wxICON_WARNING);
dialog.SetOKCancelLabels(_L("Overwrite"), _L("Cancel"));
- if (dialog.ShowModal() != wxID_OK) {
+ const int overwrite_rc = dialog.ShowModal();
+ restore_z_order();
+ if (overwrite_rc != wxID_OK) {
BOOST_LOG_TRIVIAL(info) << "Plugin package installation cancelled before overwrite. package=" << package_path
<< " plugin=" << plugin_descriptor.name;
return false;
@@ -827,7 +848,7 @@ bool PluginsDialog::install_plugin_package(const std::string& package_path)
BOOST_LOG_TRIVIAL(info) << "Plugin package installed successfully from " << package_path;
const wxString installed_name = from_u8(plugin_descriptor.name.empty() ? package_file.filename().string() : plugin_descriptor.name);
show_status(wxString::Format(_L("Installed \"%s\"."), installed_name), "success");
- refresh_plugin_catalog_async(_L("Refreshing"), _L("Refreshing plugins data"), kUseCurrentCloudCatalog);
+ refresh_plugin_metadata_async(_L("Refreshing"), _L("Refreshing plugins data"), kUseCurrentCloudMeta);
return true;
}
@@ -910,7 +931,7 @@ void PluginsDialog::restore_capability_config(const PluginCapabilityId& id)
show_status(_L("Default configuration restored."), "success");
}
-void PluginsDialog::run_script_plugin(const std::string& plugin_key, const std::string& capability_name)
+void PluginsDialog::run_script_plugin_capability(const std::string& plugin_key, const std::string& capability_name)
{
PluginManager& manager = PluginManager::instance();
@@ -1046,16 +1067,17 @@ void PluginsDialog::delete_local_plugin(const PluginDescriptor& plugin)
const wxString plugin_name = from_u8(plugin.name);
const int rc = wxMessageBox(wxString::Format(_L("Delete plugin \"%s\"?\n\nThis permanently removes the plugin folder."), plugin_name),
kDeletePluginTitle, wxYES_NO | wxNO_DEFAULT | wxICON_WARNING, this);
+ restore_z_order();
if (rc != wxYES)
return;
auto state = std::make_shared();
run_with_dialog(
- [plugin_key = plugin.plugin_key, refresh_catalog = plugin.is_cloud_plugin(), state]() {
+ [plugin_key = plugin.plugin_key, should_refresh = plugin.is_cloud_plugin(), state]() {
std::string error;
const bool succeeded = PluginManager::instance().delete_plugin(plugin_key, error);
- if (succeeded && refresh_catalog)
- refresh_plugin_catalog_blocking(kFetchCloudCatalog);
+ if (succeeded && should_refresh)
+ refresh_plugin_metadata_blocking(kFetchCloudMeta);
store_plugin_operation_result(state, succeeded, std::move(error));
},
[this, state, plugin_name]() {
@@ -1078,6 +1100,7 @@ void PluginsDialog::unsubscribe_cloud_plugin(const PluginDescriptor& plugin)
wxString::Format(_L("Unsubscribe plugin \"%s\"?\n\nThis will stop tracking the plugin and delete any local plugin files."),
plugin_name),
kUnsubscribeTitle, wxYES_NO | wxNO_DEFAULT | wxICON_WARNING, this);
+ restore_z_order();
if (rc != wxYES)
return;
@@ -1159,7 +1182,7 @@ void PluginsDialog::reinstall_cloud_plugin(const PluginDescriptor& plugin)
return;
}
- manager.update_cloud_catalog(std::vector{as_cloud_only_descriptor(plugin)});
+ manager.update_cloud_metadata(std::vector{as_cloud_only_descriptor(plugin)});
}
if (!install_cloud_plugin(plugin_key, plugin.version, from_u8(plugin.name))) {
@@ -1207,6 +1230,7 @@ void PluginsDialog::delete_mine_local_and_cloud_plugin(const std::string& plugin
"deletes the plugin from the cloud. This action cannot be undone."),
plugin_name),
kDeletePluginTitle, wxYES_NO | wxNO_DEFAULT | wxICON_WARNING, this);
+ restore_z_order();
if (rc != wxYES)
return;
@@ -1216,8 +1240,6 @@ void PluginsDialog::delete_mine_local_and_cloud_plugin(const std::string& plugin
return;
}
- // delete_mine_local_and_cloud_plugin already updated the in-memory catalog (see
- // finalize_cloud_plugin_removal), so a UI refresh is sufficient here.
send_plugins();
show_status(wxString::Format(_L("Deleted \"%s\"."), plugin_name), "success");
}
diff --git a/src/slic3r/GUI/PluginsDialog.hpp b/src/slic3r/GUI/PluginsDialog.hpp
index b041d8db71..222b5c3873 100644
--- a/src/slic3r/GUI/PluginsDialog.hpp
+++ b/src/slic3r/GUI/PluginsDialog.hpp
@@ -53,6 +53,13 @@ private:
void open_plugin_on_cloud(const std::string& sharing_token);
void open_plugin_hub();
void on_script_message(const nlohmann::json& payload) override;
+ // Runs one web command on a clean main-loop stack; see on_script_message.
+ void handle_web_command(const nlohmann::json& payload);
+ // Re-raises this dialog after a transient modal it opened (file dialog, message box,
+ // progress dialog). Native macOS panels end by re-activating the app's main window
+ // (the mainframe) instead of this webview-hosting dialog, burying it; wx only
+ // compensates for generic wxDialog modals (wxDialog::EndModal raises the parent).
+ void restore_z_order();
void send_plugins();
void set_plugin_sort(const std::string& sort_key, const std::string& sort_order);
@@ -60,7 +67,7 @@ private:
bool get_descriptor(const std::string& plugin_key, Slic3r::PluginDescriptor& descriptor) const;
- void refresh_plugin_catalog_async(const wxString& title, const wxString& message, bool fetch_cloud);
+ void refresh_plugin_metadata_async(const wxString& title, const wxString& message, bool fetch_cloud);
void refresh_plugins();
void toggle_plugin(const std::string& plugin_key, bool enabled);
void toggle_plugin_capability(const std::string& plugin_key, PluginCapabilityType type, const std::string& capability_name, bool enabled);
@@ -69,7 +76,7 @@ private:
void install_plugin_from_file();
bool install_plugin_package(const std::string& package_path);
bool install_cloud_plugin(const std::string& uuid, const std::string& version, const wxString& name);
- void run_script_plugin(const std::string& plugin_key, const std::string& capability_name);
+ void run_script_plugin_capability(const std::string& plugin_key, const std::string& capability_name);
// Config tab. Both are scoped to the full capability ID: a request naming a
// capability that is gone or not configurable is refused rather than served from, or written
// to, some other entry.
@@ -114,7 +121,8 @@ private:
timer->Start(100);
- std::thread([alive,
+ std::thread([this,
+ alive,
progress,
timer,
run = std::forward(run),
@@ -131,7 +139,8 @@ private:
if (wxTheApp == nullptr)
return;
- wxTheApp->CallAfter([alive,
+ wxTheApp->CallAfter([this,
+ alive,
progress,
timer,
on_finish = std::move(on_finish),
@@ -141,6 +150,7 @@ private:
if (alive->load(std::memory_order_acquire)) {
progress->Destroy();
+ restore_z_order();
on_finish();
} else if (finish_after_dialog_destroyed) {
on_finish();
@@ -240,9 +250,9 @@ private:
PluginSortKey m_plugin_sort_key = PluginSortKey::None;
PluginSortOrder m_plugin_sort_order = PluginSortOrder::Asc;
- // Serializes run_script_plugin. With main-thread execution a plugin's orca.host.ui modal
+ // Serializes run_script_plugin_capability. With main-thread execution a plugin's orca.host.ui modal
// (message/show_dialog) or the result message box pumps a nested event loop, which could
- // re-dispatch the web "run_script_plugin" command and start a second, overlapping run.
+ // re-dispatch the web "run_script_plugin_capability" command and start a second, overlapping run.
bool m_script_running = false;
// Plugin whose asynchronous activation is in flight, awaited by resolve_pending_activation().
diff --git a/src/slic3r/GUI/SelectMachine.cpp b/src/slic3r/GUI/SelectMachine.cpp
index d8bab12a03..f7d7b6b6fe 100644
--- a/src/slic3r/GUI/SelectMachine.cpp
+++ b/src/slic3r/GUI/SelectMachine.cpp
@@ -521,6 +521,14 @@ SelectMachineDialog::SelectMachineDialog(Plater *plater)
//m_change_filament_times_sizer->Add(m_img_change_filament_times, 0, wxTOP, FromDIP(2));
m_change_filament_times_sizer->Add(m_txt_change_filament_times, 0, wxTOP, 0);
+ m_warn_when_drying_sizer = new wxBoxSizer(wxHORIZONTAL);
+ m_txt_warn_when_drying = new Label(m_scroll_area, wxEmptyString);
+ m_txt_warn_when_drying->SetFont(::Label::Body_13);
+ m_txt_warn_when_drying->SetForegroundColour(wxColour("#F09A17"));
+ m_txt_warn_when_drying->SetBackgroundColour(*wxWHITE);
+ m_txt_warn_when_drying->SetLabel(_L("To ensure print quality, the drying temperature will be lowered during printing."));
+ m_warn_when_drying_sizer->Add(m_txt_warn_when_drying, 0, wxTOP, FromDIP(2));
+
/*Advanced Options*/
wxBoxSizer* sizer_split_options = new wxBoxSizer(wxHORIZONTAL);
auto m_split_options_line = new wxPanel(m_scroll_area, wxID_ANY);
@@ -735,6 +743,8 @@ SelectMachineDialog::SelectMachineDialog(Plater *plater)
m_scroll_sizer->Add(m_change_filament_times_sizer, 0,wxLEFT|wxRIGHT, FromDIP(15));
// m_scroll_sizer->Add(m_link_edit_nozzle, 0, wxLEFT|wxRIGHT, FromDIP(15));
m_scroll_sizer->Add(suggestion_sizer, 0, wxLEFT|wxRIGHT|wxEXPAND, FromDIP(15));
+ m_scroll_sizer->Add(0, 0, 0, wxTOP, FromDIP(10));
+ m_scroll_sizer->Add(m_warn_when_drying_sizer, 0, wxLEFT|wxRIGHT, FromDIP(15));
m_scroll_sizer->Add(sizer_split_options, 1, wxEXPAND|wxLEFT|wxRIGHT, FromDIP(15));
m_scroll_sizer->Add(0, 0, 0, wxTOP, FromDIP(10));
m_scroll_sizer->Add(m_options_other, 0, wxEXPAND|wxLEFT|wxRIGHT, FromDIP(15));
@@ -1734,6 +1744,47 @@ bool SelectMachineDialog::CheckErrorSyncNozzleMappingResultV0(MachineObject* obj
return true;
}
+bool SelectMachineDialog::is_ams_drying(MachineObject* obj)
+{
+ const auto& ams_list = obj->GetFilaSystem()->GetAmsList();
+ for (auto ams = ams_list.begin(); ams != ams_list.end(); ams++) {
+ if (ams->second->AmsIsDrying()) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+bool SelectMachineDialog::is_selected_ams_drying(MachineObject* obj)
+{
+ if (!obj) return false;
+
+ // If a UI material is selected, only when that material is mapped to an AMS
+ // and that AMS is currently drying.
+ for (const auto &kv : m_materialList) {
+ Material *mat = kv.second;
+ if (!mat || !mat->item) continue;
+ if (!mat->item->m_selected) continue;
+
+ // find mapping entry for this material id
+ for (const FilamentInfo &f : m_ams_mapping_result) {
+ if (f.id != mat->id) continue;
+
+ if (f.ams_id.empty()) return false;
+
+ auto fila_system = obj->GetFilaSystem();
+ if (!fila_system) return false;
+ DevAms* dev_ams = fila_system->GetAmsById(f.ams_id);
+ return (dev_ams && dev_ams->AmsIsDrying());
+ }
+
+ return false;
+ }
+
+ return false;
+}
+
void SelectMachineDialog::prepare(int print_plate_idx)
{
m_print_plate_idx = print_plate_idx;
@@ -3888,6 +3939,16 @@ void SelectMachineDialog::update_show_status(MachineObject* obj_)
m_check_ext_change_assist->Enable(false);
}
+ {
+ const bool show_warn_when_drying = is_selected_ams_drying(obj_);
+ const bool is_currently_shown = (m_txt_warn_when_drying != nullptr) ? m_txt_warn_when_drying->IsShown() : false;
+ if (is_currently_shown != show_warn_when_drying) {
+ m_warn_when_drying_sizer->Show(show_warn_when_drying);
+ Layout();
+ Fit();
+ }
+ }
+
/*reading done*/
if (wxGetApp().app_config) {
if (obj_->upgrade_force_upgrade) {
@@ -4404,6 +4465,7 @@ void SelectMachineDialog::set_default()
m_mapping_sugs_sizer->Show(false);
m_change_filament_times_sizer->Show(false);
m_txt_change_filament_times->Show(false);
+ m_warn_when_drying_sizer->Show(false);
// rset status bar
m_status_bar->reset();
@@ -4513,6 +4575,10 @@ void SelectMachineDialog::reset_and_sync_ams_list()
m_filaments_map = wxGetApp().plater()->get_partplate_list().get_curr_plate()->get_real_filament_maps(project_config);
}
+ bool selected_any = false;
+ MaterialItem *first_enabled = nullptr;
+ int first_enabled_id = -1;
+
for (auto i = 0; i < extruders.size(); i++) {
auto extruder = extruders[i] - 1;
auto colour = wxGetApp().preset_bundle->project_config.opt_string("filament_colour", (unsigned int) extruder);
@@ -4542,7 +4608,20 @@ void SelectMachineDialog::reset_and_sync_ams_list()
item = new MaterialItem(m_filament_panel, colour_rgb, _L(display_materials[extruder]));
m_sizer_ams_mapping->Add(item, 0, wxALL, FromDIP(5));
}
+
+ if (!item) continue;
+
item->SetToolTip(m_ams_tooltip);
+
+ if (!selected_any && extruder == m_current_filament_id && item->m_enable) {
+ item->on_selected();
+ selected_any = true;
+ }
+ if (!first_enabled && item->m_enable) {
+ first_enabled = item;
+ first_enabled_id = extruder;
+ }
+
item->Bind(wxEVT_LEFT_UP, [this, item, materials, extruder](wxMouseEvent &e) {});
item->Bind(wxEVT_LEFT_DOWN, [this, item, materials, extruder](wxMouseEvent &e) {
if (!item->m_enable) {return;}
@@ -4621,6 +4700,11 @@ void SelectMachineDialog::reset_and_sync_ams_list()
}
}
+ if (!selected_any && first_enabled) {
+ m_current_filament_id = first_enabled_id;
+ first_enabled->on_selected();
+ }
+
if (use_double_extruder)
{
m_filament_left_panel->Show();
@@ -5031,6 +5115,10 @@ void SelectMachineDialog::set_default_from_sdcard()
m_materialList.clear();
m_filaments.clear();
+ bool selected_any = false;
+ MaterialItem *first_enabled = nullptr;
+ int first_enabled_id = -1;
+
for (auto i = 0; i < m_required_data_plate_data_list[m_print_plate_idx]->slice_filaments_info.size(); i++) {
FilamentInfo fo = m_required_data_plate_data_list[m_print_plate_idx]->slice_filaments_info[i];
@@ -5053,6 +5141,17 @@ void SelectMachineDialog::set_default_from_sdcard()
m_sizer_ams_mapping->Add(item, 0, wxALL, FromDIP(5));
}
+ if (!item) continue;
+
+ if (!selected_any && fo.id == m_current_filament_id && item->m_enable) {
+ item->on_selected();
+ selected_any = true;
+ }
+ if (!first_enabled && item->m_enable) {
+ first_enabled = item;
+ first_enabled_id = fo.id;
+ }
+
item->Bind(wxEVT_LEFT_UP, [this, item, materials](wxMouseEvent& e) {});
item->Bind(wxEVT_LEFT_DOWN, [this, obj_, item, materials, diameters_count, fo](wxMouseEvent& e) {
if (!item->m_enable) {return;}
@@ -5129,6 +5228,11 @@ void SelectMachineDialog::set_default_from_sdcard()
wxSize screenSize = wxGetDisplaySize();
auto dialogSize = this->GetSize();
+ if (!selected_any && first_enabled) {
+ m_current_filament_id = first_enabled_id;
+ first_enabled->on_selected();
+ }
+
reset_ams_material();
// basic info
diff --git a/src/slic3r/GUI/SelectMachine.hpp b/src/slic3r/GUI/SelectMachine.hpp
index 5d51f17f4b..a62e8de3d2 100644
--- a/src/slic3r/GUI/SelectMachine.hpp
+++ b/src/slic3r/GUI/SelectMachine.hpp
@@ -28,6 +28,7 @@
#include "boost/bimap/bimap.hpp"
#include "AmsMappingPopup.hpp"
+#include "GUI_ObjectLayers.hpp"
#include "ReleaseNote.hpp"
#include "GUI_Utils.hpp"
#include "wxExtensions.hpp"
@@ -354,6 +355,7 @@ protected:
wxBoxSizer* m_sizer_autorefill{ nullptr };
wxBoxSizer* m_mapping_sugs_sizer{ nullptr };
wxBoxSizer* m_change_filament_times_sizer{ nullptr };
+ wxBoxSizer* m_warn_when_drying_sizer{ nullptr };
Button* m_button_ensure{ nullptr };
wxStaticBitmap * m_rename_button{nullptr};
wxStaticBitmap* m_staticbitmap{ nullptr };
@@ -387,6 +389,8 @@ protected:
CheckBox* m_check_ext_change_assist{ nullptr };
Label* m_label_ext_change_assist{ nullptr };
+ Label* m_txt_warn_when_drying{ nullptr };
+
PrinterInfoBox* m_printer_box { nullptr};
PrinterMsgPanel * m_text_printer_msg{nullptr};
Label* m_text_printer_msg_tips{ nullptr };
@@ -542,6 +546,9 @@ public:
bool CheckErrorDynamicSwitchNozzle(MachineObject* obj_);
void on_flow_cali_option_changed();
+ bool is_ams_drying(MachineObject* obj);
+ bool is_selected_ams_drying(MachineObject* obj);
+
PrintFromType get_print_type() {return m_print_type;};
wxString format_steel_name(NozzleType type);
PrintDialogStatus get_status() { return m_print_status; }
diff --git a/src/slic3r/GUI/StatusPanel.cpp b/src/slic3r/GUI/StatusPanel.cpp
index 2be47207d6..1bd564b5e1 100644
--- a/src/slic3r/GUI/StatusPanel.cpp
+++ b/src/slic3r/GUI/StatusPanel.cpp
@@ -3392,6 +3392,7 @@ void StatusPanel::update_ams(MachineObject *obj)
// must select a current can
m_ams_control->UpdateAms(obj->get_printer_series_str(), obj->printer_type, ams_info, ext_info, *obj->GetExtderSystem(), obj->get_dev_id(), false);
+ m_ams_control->UpdateAmsDryControl(obj);
last_tray_exist_bits = obj->tray_exist_bits;
last_ams_exist_bits = obj->ams_exist_bits;
diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp
index 332c7b9a56..55e4d8e14a 100644
--- a/src/slic3r/GUI/Tab.cpp
+++ b/src/slic3r/GUI/Tab.cpp
@@ -7917,7 +7917,7 @@ bool Tab::validate_filament_temperature_pairs()
if (delta <= rule.max_delta)
continue;
- const wxString deg_c = wxString::FromUTF8("°C");
+ const wxString deg_c = wxString::FromUTF8("℃");
const wxString bullet = wxString::FromUTF8("•");
invalid_pairs += wxString::Format(_L(" - %s:\n %s first layer %d %s, other layers %d %s\n %s max delta %d %s, current delta %d %s\n"),
rule.label, bullet, first_temp, deg_c, other_temp, deg_c, bullet, rule.max_delta, deg_c, delta, deg_c);
diff --git a/src/slic3r/GUI/Widgets/AMSControl.cpp b/src/slic3r/GUI/Widgets/AMSControl.cpp
index ea693fbd4c..7334cb960a 100644
--- a/src/slic3r/GUI/Widgets/AMSControl.cpp
+++ b/src/slic3r/GUI/Widgets/AMSControl.cpp
@@ -31,6 +31,7 @@ AMSControl::AMSControl(wxWindow *parent, wxWindowID id, const wxPoint &pos, cons
, m_Humidity_tip_popup(AmsHumidityTipPopup(this))
, m_percent_humidity_dry_popup(new uiAmsPercentHumidityDryPopup(this))
, m_ams_introduce_popup(AmsIntroducePopup(this))
+ , m_ams_dry_ctr_win(new AMSDryCtrWin(this))
{
Slic3r::DeviceManager* dev = Slic3r::GUI::wxGetApp().getDeviceManager();
if (dev) {
@@ -252,6 +253,12 @@ AMSControl::AMSControl(wxWindow *parent, wxWindowID id, const wxPoint &pos, cons
uiAmsHumidityInfo *info = (uiAmsHumidityInfo *) evt.GetClientData();
if (info)
{
+ Slic3r::DeviceManager* dev = Slic3r::GUI::wxGetApp().getDeviceManager();
+ MachineObject *obj = nullptr;
+ if (dev) {
+ obj = dev->get_selected_machine();
+ }
+
if (info->ams_type == AMSModel::GENERIC_AMS)
{
wxPoint img_pos = ClientToScreen(wxPoint(0, 0));
@@ -261,9 +268,14 @@ AMSControl::AMSControl(wxWindow *parent, wxWindowID id, const wxPoint &pos, cons
int humidity_value = info->humidity_display_idx;
if (humidity_value > 0 && humidity_value <= 5) { m_Humidity_tip_popup.set_humidity_level(humidity_value); }
m_Humidity_tip_popup.Popup();
- }
- else
- {
+ } else if (obj && obj->is_support_remote_dry && (info->ams_type == AMSModel::N3F_AMS || info->ams_type == AMSModel::N3S_AMS)){
+ m_ams_dry_ctr_win->set_ams_id(info->ams_id);
+
+ wxPoint img_pos = ClientToScreen(wxPoint(0, 0));
+ wxPoint popup_pos(img_pos.x - m_ams_dry_ctr_win->GetSize().GetWidth() + FromDIP(150), img_pos.y - FromDIP(80));
+ m_ams_dry_ctr_win->Move(popup_pos);
+ m_ams_dry_ctr_win->ShowModal();
+ } else {
m_percent_humidity_dry_popup->Update(info);
wxPoint img_pos = ClientToScreen(wxPoint(0, 0));
@@ -283,7 +295,12 @@ void AMSControl::on_retry()
post_event(wxCommandEvent(EVT_AMS_RETRY));
}
-AMSControl::~AMSControl() {}
+AMSControl::~AMSControl()
+{
+ if (m_ams_dry_ctr_win) {
+ delete m_ams_dry_ctr_win;
+ }
+}
std::string AMSControl::GetCurentAms() {
return m_current_ams;
@@ -501,6 +518,10 @@ void AMSControl::msw_rescale()
m_percent_humidity_dry_popup->msw_rescale();
}
+ if (m_ams_dry_ctr_win) {
+ m_ams_dry_ctr_win->msw_rescale();
+ }
+
m_Humidity_tip_popup.msw_rescale();
Layout();
@@ -824,6 +845,27 @@ void AMSControl::show_vams_kn_value(bool show)
//m_vams_lib->show_kn_value(show);
}
+void AMSControl::UpdateAmsDryControl(MachineObject* obj)
+{
+ if (!m_ams_dry_ctr_win->IsShown()) {
+ return;
+ }
+
+ if (!obj || !obj->GetFilaSystem()) {
+ m_ams_dry_ctr_win->Close();
+ return;
+ }
+
+ std::weak_ptr weak_fila_system = obj->GetFilaSystem();
+
+ if (auto locaked_fila_system = weak_fila_system.lock()) {
+ m_ams_dry_ctr_win->update(locaked_fila_system, obj);
+ } else {
+ m_ams_dry_ctr_win->Close();
+ return;
+ }
+}
+
std::vector AMSControl::GenerateSimulateData() {
auto caninfo0_0 = Caninfo{ "0", (""), *wxRED, AMSCanType::AMS_CAN_TYPE_VIRTUAL };
auto caninfo0_1 = Caninfo{ "1", (""), *wxGREEN, AMSCanType::AMS_CAN_TYPE_VIRTUAL };
diff --git a/src/slic3r/GUI/Widgets/AMSControl.hpp b/src/slic3r/GUI/Widgets/AMSControl.hpp
index 7616d269f2..1fe8876eba 100644
--- a/src/slic3r/GUI/Widgets/AMSControl.hpp
+++ b/src/slic3r/GUI/Widgets/AMSControl.hpp
@@ -16,6 +16,7 @@
#include
#include "slic3r/GUI/DeviceCore/DevExtruderSystem.h"
+#include "slic3r/GUI/AMSDryControl.hpp"
namespace Slic3r { namespace GUI {
@@ -127,6 +128,7 @@ protected:
AmsHumidityTipPopup m_Humidity_tip_popup;
uiAmsPercentHumidityDryPopup* m_percent_humidity_dry_popup;
+ AMSDryCtrWin* m_ams_dry_ctr_win;
std::string m_last_ams_id = "";
std::string m_last_tray_id = "";
@@ -170,6 +172,7 @@ public:
void CreateAmsDoubleNozzle(const std::string &series_name, const std::string& printer_type);
void CreateAmsSingleNozzle(const std::string &series_name, const std::string &printer_type);
void ClearAms();
+ void UpdateAmsDryControl(MachineObject* obj);
void UpdateAms(const std::string &series_name,
const std::string &printer_type,
std::vector ams_info,
diff --git a/src/slic3r/Utils/MoonrakerPrinterAgent.cpp b/src/slic3r/Utils/MoonrakerPrinterAgent.cpp
index f1d892134b..b0ea653dd3 100644
--- a/src/slic3r/Utils/MoonrakerPrinterAgent.cpp
+++ b/src/slic3r/Utils/MoonrakerPrinterAgent.cpp
@@ -536,7 +536,7 @@ void MoonrakerPrinterAgent::build_ams_payload(int ams_count, int max_lane_index,
print_json["ams"] = ams_json;
// Call the parser to populate DevFilaSystem
- DevFilaSystemParser::ParseV1_0(print_json, obj, obj->GetFilaSystem(), false);
+ DevFilaSystemParser::ParseV1_0(print_json, obj, obj->GetFilaSystem().get(), false);
BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent::build_ams_payload: Parsed " << trays.size() << " trays";
// Set printer_type so update_sync_status() can match it against the preset's printer type.
diff --git a/src/slic3r/plugin/PluginAuditManager.cpp b/src/slic3r/plugin/PluginAuditManager.cpp
index 067efee30b..2f955c5945 100644
--- a/src/slic3r/plugin/PluginAuditManager.cpp
+++ b/src/slic3r/plugin/PluginAuditManager.cpp
@@ -14,11 +14,11 @@ namespace Slic3r {
// Path safety
// ---------------------------------------------------------------------------
-bool is_inside_allowed_root(const std::filesystem::path& candidate, const std::filesystem::path& allowed_root)
+bool is_inside_allowed_root(const boost::filesystem::path& candidate, const boost::filesystem::path& allowed_root)
{
- namespace fs = std::filesystem;
+ namespace fs = boost::filesystem;
- std::error_code ec;
+ boost::system::error_code ec;
// Canonicalize both paths. weakly_canonical resolves symlinks but does
// NOT require the path to exist — it canonicalizes the prefix that exists
@@ -79,7 +79,7 @@ bool is_inside_allowed_root(const std::filesystem::path& candidate, const std::f
thread_local std::string PluginAuditManager::m_current_plugin_key = "";
thread_local std::string PluginAuditManager::m_current_capability_name = "";
thread_local PluginAuditManager::AuditMode PluginAuditManager::m_audit_mode = PluginAuditManager::AuditMode::Loading;
-thread_local std::vector PluginAuditManager::m_scoped_allowed_roots;
+thread_local std::vector PluginAuditManager::m_scoped_allowed_roots;
thread_local bool PluginAuditManager::m_has_last_violation = false;
thread_local AuditViolation PluginAuditManager::m_last_violation;
@@ -127,7 +127,7 @@ std::string PluginAuditManager::current_capability() const { return m_current_ca
void PluginAuditManager::clear_current_capability() { m_current_capability_name.clear(); }
-void PluginAuditManager::add_global_allowed_root(const std::filesystem::path& root)
+void PluginAuditManager::add_global_allowed_root(const boost::filesystem::path& root)
{
if (root.empty())
return;
@@ -137,7 +137,7 @@ void PluginAuditManager::add_global_allowed_root(const std::filesystem::path& ro
BOOST_LOG_TRIVIAL(info) << "[AUDIT] Global allowed root: " << root.string();
}
-void PluginAuditManager::add_scoped_allowed_root(const std::filesystem::path& root)
+void PluginAuditManager::add_scoped_allowed_root(const boost::filesystem::path& root)
{
if (root.empty())
return;
@@ -175,12 +175,12 @@ AuditDecision PluginAuditManager::check_open(const std::string& path_str, const
return {true, ""};
}
- namespace fs = std::filesystem;
+ namespace fs = boost::filesystem;
fs::path candidate(path_str);
// Resolve relative paths against the current working directory
if (candidate.is_relative()) {
- std::error_code ec;
+ boost::system::error_code ec;
candidate = fs::absolute(candidate, ec);
if (ec)
candidate = fs::path(path_str);
diff --git a/src/slic3r/plugin/PluginAuditManager.hpp b/src/slic3r/plugin/PluginAuditManager.hpp
index 2e77821c41..addc63b0fa 100644
--- a/src/slic3r/plugin/PluginAuditManager.hpp
+++ b/src/slic3r/plugin/PluginAuditManager.hpp
@@ -2,11 +2,12 @@
#define slic3r_PluginAuditManager_hpp_
#include
-#include
#include
#include
#include
+#include
+
namespace Slic3r {
struct AuditDecision {
@@ -17,14 +18,14 @@ struct AuditDecision {
struct AuditViolation {
std::string plugin_key;
std::string event_name;
- std::filesystem::path path;
+ boost::filesystem::path path;
std::string reason;
};
// Returns true if candidate resolves to a path inside allowed_root.
// Uses weakly_canonical and component-wise comparison to reject traversal attacks.
-bool is_inside_allowed_root(const std::filesystem::path& candidate,
- const std::filesystem::path& allowed_root);
+bool is_inside_allowed_root(const boost::filesystem::path& candidate,
+ const boost::filesystem::path& allowed_root);
class PluginAuditManager
{
@@ -48,8 +49,8 @@ public:
void clear_current_capability();
// --- allowed-roots registry ---
- void add_global_allowed_root(const std::filesystem::path& root);
- void add_scoped_allowed_root(const std::filesystem::path& root);
+ void add_global_allowed_root(const boost::filesystem::path& root);
+ void add_scoped_allowed_root(const boost::filesystem::path& root);
// --- enforcement mode ---
enum class AuditMode {
@@ -85,12 +86,12 @@ private:
static thread_local std::string m_current_plugin_key;
static thread_local std::string m_current_capability_name;
static thread_local AuditMode m_audit_mode;
- static thread_local std::vector m_scoped_allowed_roots;
+ static thread_local std::vector m_scoped_allowed_roots;
static thread_local bool m_has_last_violation;
static thread_local AuditViolation m_last_violation;
std::mutex m_mutex;
- std::vector m_global_allowed_roots;
+ std::vector m_global_allowed_roots;
};
// RAII guard that sets the current plugin key and capability name, restoring the previous
@@ -113,7 +114,7 @@ private:
std::string m_previous_id;
std::string m_previous_capability;
PluginAuditManager::AuditMode m_previous_mode;
- std::vector m_previous_scoped_roots;
+ std::vector m_previous_scoped_roots;
};
} // namespace Slic3r
diff --git a/src/slic3r/plugin/PluginFsUtils.cpp b/src/slic3r/plugin/PluginFsUtils.cpp
index f1636dcf97..5da2707fa8 100644
--- a/src/slic3r/plugin/PluginFsUtils.cpp
+++ b/src/slic3r/plugin/PluginFsUtils.cpp
@@ -1,16 +1,31 @@
#include "PluginFsUtils.hpp"
-#include "PythonFileUtils.hpp"
#include "libslic3r/Utils.hpp"
+#include "libslic3r/miniz_extension.hpp"
#include
#include
+#include
+
+#include "PluginAuditManager.hpp"
+#include "PythonInterpreter.hpp"
+
+#include
#include
#include
#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
-#include "PluginAuditManager.hpp"
+#ifdef WIN32
+#include
+#endif
namespace Slic3r {
@@ -52,7 +67,8 @@ bool is_plugin_root_allowed(const boost::filesystem::path& candidate_root, const
return false;
for (const auto& allowed_dir : allowed_dirs) {
- if (is_inside_allowed_root(std::filesystem::path(resolved_root.string()), std::filesystem::path(allowed_dir)))
+ if (is_inside_allowed_root(boost::filesystem::path(resolved_root.string()),
+ boost::filesystem::path(allowed_dir)))
return true;
}
@@ -255,5 +271,929 @@ std::vector discover_plugin_packages(const std::vector(static_cast(p[2])) |
+ static_cast(static_cast(p[3]) << 8);
+ if (p[0] == '\x75' && p[1] == '\x70' && len >= 5 && p + 4 + len <= e && p[4] == '\x01')
+ return std::string(p + 9, p + 4 + len);
+ p += 4 + len;
+ }
+
+ return decode_path(fallback.c_str());
+}
+
+std::string zip_entry_name(mz_zip_archive& archive, const mz_zip_archive_file_stat& stat)
+{
+ if (stat.m_is_utf8)
+ return stat.m_filename;
+
+ std::string extra(1024, 0);
+ const size_t n = mz_zip_reader_get_extra(&archive, stat.m_file_index, extra.data(), extra.size());
+ return decode_zip_entry_extra_path(extra.substr(0, n), stat.m_filename);
+}
+
+std::string normalize_zip_entry_name(std::string entry_name)
+{
+ std::replace(entry_name.begin(), entry_name.end(), '\\', '/');
+ while (!entry_name.empty() && entry_name.back() == '/')
+ entry_name.pop_back();
+ return entry_name;
+}
+
+struct ZipReaderGuard
+{
+ mz_zip_archive archive;
+ bool opened = false;
+
+ ZipReaderGuard() { mz_zip_zero_struct(&archive); }
+
+ ~ZipReaderGuard()
+ {
+ if (opened)
+ close_zip_reader(&archive);
+ }
+};
+
+} // namespace
+
+bool is_valid_plugin_id(const std::string& id)
+{
+ if (id.empty())
+ return false;
+ if (id == "." || id == ".." || id[0] == '.' || id.rfind("__", 0) == 0)
+ return false;
+
+ for (unsigned char ch : id) {
+ if (std::isalnum(ch) || ch == '_' || ch == '-' || ch == '.')
+ continue;
+ return false;
+ }
+
+ return true;
+}
+
+namespace {
+
+// RAII helper to free heap-allocated memory from miniz.
+struct MzHeapFree {
+ void* ptr = nullptr;
+ ~MzHeapFree() { if (ptr) std::free(ptr); }
+};
+
+// Read a text file from within a zip archive into a string.
+// Returns true on success, false if the file is not found or cannot be read.
+bool read_zip_text_file(mz_zip_archive& archive, const char* filename, std::string& out, std::string& error)
+{
+ size_t size = 0;
+ void* data = mz_zip_reader_extract_file_to_heap(&archive, filename, &size, 0);
+ if (!data) {
+ error = std::string("Wheel does not contain ") + filename;
+ return false;
+ }
+ MzHeapFree guard{data};
+ out.assign(static_cast(data), size);
+ return true;
+}
+
+// TOML section parsing states.
+enum class TomlSection { Root, OrcaPlugin, OrcaPluginSettings, InDepsArray };
+
+// Strip a quoted string value: "foo" → foo, 'foo' → foo.
+// Returns the unquoted value or the input unchanged if not quoted.
+std::string unquote_toml_string(const std::string& val)
+{
+ if (val.size() >= 2 && ((val.front() == '"' && val.back() == '"') || (val.front() == '\'' && val.back() == '\'')))
+ return val.substr(1, val.size() - 2);
+ return val;
+}
+
+// Split a TOML inline array: ["a", "b"] → {"a", "b"}.
+// Handles trailing commas and single-line format.
+std::vector parse_toml_inline_array(const std::string& val)
+{
+ std::vector result;
+ std::string inner = val;
+ // Strip outer brackets.
+ if (!inner.empty() && inner.front() == '[')
+ inner.erase(0, 1);
+ if (!inner.empty() && inner.back() == ']')
+ inner.pop_back();
+
+ // Simple split by comma, strip quotes and whitespace.
+ std::istringstream ss(inner);
+ std::string item;
+ while (std::getline(ss, item, ',')) {
+ // Trim whitespace.
+ size_t s = 0, e = item.size();
+ while (s < e && (item[s] == ' ' || item[s] == '\t')) ++s;
+ while (e > s && (item[e - 1] == ' ' || item[e - 1] == '\t')) --e;
+ item = item.substr(s, e - s);
+ if (!item.empty())
+ result.push_back(unquote_toml_string(item));
+ }
+ return result;
+}
+
+// Parse PEP 723 TOML subset for dependencies, requires-python, and
+// [tool.orcaslicer.plugin] identity fields.
+//
+// requires-python = ">=3.12"
+// dependencies = ["pkg>=1.0", ]
+//
+// [tool.orcaslicer.plugin]
+// id = "my-plugin"
+// name = "My Plugin"
+// description = "Does things."
+// author = "Author"
+// version = "1.0.0"
+//
+// Returns false only on parse errors; missing block is not an error.
+bool parse_pep723_toml(const std::string& toml_content,
+ std::vector& out_deps,
+ std::string& out_requires_python,
+ std::string& out_name,
+ std::string& out_description,
+ std::string& out_author,
+ std::string& out_version,
+ std::string& error)
+{
+ out_deps.clear();
+ out_requires_python.clear();
+ out_name.clear();
+ out_description.clear();
+ out_author.clear();
+ out_version.clear();
+
+ TomlSection section = TomlSection::Root;
+
+ std::istringstream stream(toml_content);
+ std::string line;
+
+ while (std::getline(stream, line)) {
+ // Trim leading/trailing whitespace.
+ size_t start = 0;
+ while (start < line.size() && (line[start] == ' ' || line[start] == '\t'))
+ ++start;
+ size_t end = line.size();
+ while (end > start && (line[end - 1] == ' ' || line[end - 1] == '\t'))
+ --end;
+ std::string trimmed = line.substr(start, end - start);
+
+ if (trimmed.empty() || trimmed[0] == '#')
+ continue;
+
+ // TOML section header.
+ if (trimmed[0] == '[') {
+ if (trimmed == "[tool.orcaslicer.plugin]") {
+ section = TomlSection::OrcaPlugin;
+ } else if (trimmed == "[tool.orcaslicer.plugin.settings]") {
+ section = TomlSection::OrcaPluginSettings; // per-plugin params table
+ } else {
+ section = TomlSection::Root; // Unknown section — skip.
+ }
+ continue;
+ }
+
+ if (section == TomlSection::InDepsArray) {
+ if (trimmed == "]") {
+ section = TomlSection::Root;
+ continue;
+ }
+ std::string val = trimmed;
+ if (!val.empty() && val.back() == ',')
+ val.pop_back();
+ val = unquote_toml_string(val);
+ if (!val.empty())
+ out_deps.push_back(val);
+ continue;
+ }
+
+ // Look for key = value.
+ size_t eq = trimmed.find('=');
+ if (eq == std::string::npos)
+ continue;
+
+ std::string key = trimmed.substr(0, eq);
+ while (!key.empty() && (key.back() == ' ' || key.back() == '\t'))
+ key.pop_back();
+
+ std::string val = trimmed.substr(eq + 1);
+ while (!val.empty() && (val.front() == ' ' || val.front() == '\t'))
+ val.erase(0, 1);
+ // Trim trailing.
+ while (!val.empty() && (val.back() == ' ' || val.back() == '\t'))
+ val.pop_back();
+
+ if (section == TomlSection::Root) {
+ if (key == "requires-python") {
+ out_requires_python = unquote_toml_string(val);
+ } else if (key == "dependencies") {
+ if (val == "[") {
+ section = TomlSection::InDepsArray;
+ } else {
+ // Inline array: dependencies = ["a", "b"]
+ out_deps = parse_toml_inline_array(val);
+ }
+ }
+ } else if (section == TomlSection::OrcaPlugin) {
+ if (key == "name") out_name = unquote_toml_string(val);
+ else if (key == "description") out_description = unquote_toml_string(val);
+ else if (key == "author") out_author = unquote_toml_string(val);
+ else if (key == "version") out_version = unquote_toml_string(val);
+ }
+ }
+
+ // Check for unclosed arrays.
+ if (section == TomlSection::InDepsArray) {
+ error = "PEP 723 metadata: unclosed dependencies array";
+ return false;
+ }
+ return true;
+}
+
+// Normalize a distribution name to a Python import package name.
+// Converts hyphens to underscores and lowercases.
+std::string normalize_package_name(const std::string& name)
+{
+ std::string result;
+ result.reserve(name.size());
+ for (unsigned char ch : name) {
+ if (ch == '-' || ch == '.')
+ result += '_';
+ else
+ result += static_cast(std::tolower(ch));
+ }
+ return result;
+}
+
+// Parse METADATA (RFC 822 style) into a flat multimap.
+// https://packaging.python.org/en/latest/specifications/core-metadata/
+void parse_metadata_rfc822(const std::string& content,
+ std::string& out_name,
+ std::string& out_version,
+ std::string& out_summary,
+ std::string& out_author,
+ std::string& out_requires_python,
+ std::string& out_import_name,
+ std::vector& out_requires_dist,
+ std::string& error)
+{
+ std::istringstream stream(content);
+ std::string line;
+ std::string current_header;
+ std::string current_value;
+
+ auto flush = [&]() {
+ if (current_header.empty())
+ return;
+ std::string lower = current_header;
+ std::transform(lower.begin(), lower.end(), lower.begin(),
+ [](unsigned char c) { return std::tolower(c); });
+
+ if (lower == "name")
+ out_name = current_value;
+ else if (lower == "version")
+ out_version = current_value;
+ else if (lower == "summary")
+ out_summary = current_value;
+ else if (lower == "author")
+ out_author = current_value;
+ else if (lower == "requires-python")
+ out_requires_python = current_value;
+ else if (lower == "import-name")
+ out_import_name = current_value;
+ else if (lower == "requires-dist")
+ out_requires_dist.push_back(current_value);
+
+ current_header.clear();
+ current_value.clear();
+ };
+
+ while (std::getline(stream, line)) {
+ // Blank line after headers marks the start of the body.
+ if (line.empty() && current_header.empty())
+ continue;
+ if (line.empty()) {
+ flush();
+ // Remaining content is the body (description); stop parsing headers.
+ break;
+ }
+
+ // Continuation line.
+ if (line[0] == ' ' || line[0] == '\t') {
+ if (!current_value.empty())
+ current_value += '\n';
+ size_t pos = line.find_first_not_of(" \t");
+ current_value += (pos != std::string::npos) ? line.substr(pos) : "";
+ continue;
+ }
+
+ flush();
+
+ size_t colon = line.find(':');
+ if (colon == std::string::npos)
+ continue;
+
+ current_header = line.substr(0, colon);
+ size_t val_start = colon + 1;
+ while (val_start < line.size() && (line[val_start] == ' ' || line[val_start] == '\t'))
+ ++val_start;
+ current_value = line.substr(val_start);
+ }
+
+ flush();
+}
+
+} // namespace
+
+bool is_ignored_plugin_directory(const boost::filesystem::path& path)
+{
+ const std::string name = path.filename().string();
+ return name.empty() || name[0] == '.' || name.rfind("__", 0) == 0 || name == PLUGIN_SUBSCRIBED_DIR;
+}
+
+bool is_safe_relative_path(const boost::filesystem::path& path)
+{
+ if (path.empty() || path.is_absolute() || path.has_root_directory() || path.has_root_name())
+ return false;
+
+ for (const auto& part : path) {
+ const std::string token = part.string();
+ if (token == "..")
+ return false;
+ }
+
+ return true;
+}
+
+bool extract_zip_to_directory(const boost::filesystem::path& zip_path, const boost::filesystem::path& destination, std::string& error)
+{
+ namespace fs = boost::filesystem;
+
+ boost::system::error_code ec;
+ fs::create_directories(destination, ec);
+ if (ec) {
+ error = "Failed to create plugin staging directory: " + ec.message();
+ return false;
+ }
+
+ ZipReaderGuard reader;
+ if (!open_zip_reader(&reader.archive, zip_path.string())) {
+ error = "Failed to open plugin zip: " + MZ_Archive::get_errorstr(mz_zip_get_last_error(&reader.archive));
+ return false;
+ }
+ reader.opened = true;
+
+ std::unordered_set extracted_entries;
+ const mz_uint num_entries = mz_zip_reader_get_num_files(&reader.archive);
+ mz_zip_archive_file_stat stat;
+ for (mz_uint i = 0; i < num_entries; ++i) {
+ if (!mz_zip_reader_file_stat(&reader.archive, i, &stat)) {
+ error = "Failed to read plugin zip entry metadata: " + MZ_Archive::get_errorstr(mz_zip_get_last_error(&reader.archive));
+ return false;
+ }
+
+ std::string entry_name = normalize_zip_entry_name(zip_entry_name(reader.archive, stat));
+ if (entry_name.empty())
+ continue;
+ if (entry_name.find(':') != std::string::npos) {
+ error = "Plugin zip entry contains an invalid path: " + entry_name;
+ return false;
+ }
+
+ const fs::path relative_path(entry_name);
+ if (!is_safe_archive_entry_path(relative_path)) {
+ error = "Plugin zip entry escapes the plugin package: " + entry_name;
+ return false;
+ }
+
+ const std::string relative_key = relative_path.generic_string();
+ if (!extracted_entries.insert(relative_key).second) {
+ error = "Plugin zip contains duplicate entry: " + relative_key;
+ return false;
+ }
+
+ const fs::path output_path = destination / relative_path;
+ if (stat.m_is_directory || mz_zip_reader_is_file_a_directory(&reader.archive, stat.m_file_index)) {
+ fs::create_directories(output_path, ec);
+ if (ec) {
+ error = "Failed to create plugin zip directory " + output_path.string() + ": " + ec.message();
+ return false;
+ }
+ continue;
+ }
+
+ fs::create_directories(output_path.parent_path(), ec);
+ if (ec) {
+ error = "Failed to create plugin zip parent directory " + output_path.parent_path().string() + ": " + ec.message();
+ return false;
+ }
+
+ if (fs::exists(output_path, ec) && fs::is_directory(output_path, ec)) {
+ error = "Plugin zip file conflicts with an existing directory: " + output_path.string();
+ return false;
+ }
+
+ const std::string encoded_output_path = encode_path(output_path.string().c_str());
+ mz_bool extracted = mz_zip_reader_extract_to_file(&reader.archive, stat.m_file_index, encoded_output_path.c_str(), 0);
+#ifdef WIN32
+ if (!extracted) {
+ const std::wstring wide_output_path = boost::locale::conv::utf_to_utf(output_path.generic_string());
+ extracted = mz_zip_reader_extract_to_file_w(&reader.archive, stat.m_file_index, wide_output_path.c_str(), 0);
+ }
+#endif
+ if (!extracted) {
+ error = "Failed to extract plugin zip entry " + relative_key + ": " +
+ MZ_Archive::get_errorstr(mz_zip_get_last_error(&reader.archive));
+ return false;
+ }
+ }
+
+ return true;
+}
+
+void read_install_state(const boost::filesystem::path& plugin_dir, PluginDescriptor& entry)
+{
+ PluginInstallState state;
+ if (!read_install_state(plugin_dir, state))
+ return;
+
+ // The cloud identity and the persisted installed version are read back. plugin_key
+ // is always derived by the catalog scan (filename for local, the cloud uuid for
+ // cloud), so it is not read from the sidecar. installed_version is the source of
+ // truth for a cloud plugin's installed version: it records the version fetched from
+ // the cloud at install time, independent of the (possibly stale) manifest/PEP723
+ // header that scan_directory parses into entry.version.
+ if (!state.installed_version.empty())
+ entry.installed_version = state.installed_version;
+ if (!state.cloud_uuid.empty())
+ entry.cloud = CloudPluginState{state.cloud_uuid, true, false, false};
+
+ // Package-level auto-load flag only. The per-capability enable flags stay in the sidecar: a
+ // capability has no existence — and so no state — until it is materialized, at which point the
+ // loader seeds the flag onto the capability itself.
+ entry.enabled = state.enabled;
+}
+
+bool read_install_state(const boost::filesystem::path& plugin_dir, PluginInstallState& out)
+{
+ namespace fs = boost::filesystem;
+ const fs::path sidecar_path = plugin_dir / ".install_state.json";
+
+ if (!fs::exists(sidecar_path) || !fs::is_regular_file(sidecar_path))
+ return false;
+
+ boost::nowide::ifstream f(sidecar_path.string());
+ if (!f)
+ return false;
+
+ try {
+ nlohmann::json state = nlohmann::json::parse(f, nullptr, false, true);
+ if (state.is_discarded() || !state.is_object())
+ return false;
+
+ PluginInstallState parsed;
+ if (state.contains("installed_from") && state["installed_from"].is_string())
+ parsed.installed_from = state["installed_from"].get();
+ if (state.contains("installed_version") && state["installed_version"].is_string())
+ parsed.installed_version = state["installed_version"].get();
+ if (state.contains("plugin_name") && state["plugin_name"].is_string())
+ parsed.plugin_name = state["plugin_name"].get();
+ if (state.contains("cloud_uuid") && state["cloud_uuid"].is_string())
+ parsed.cloud_uuid = state["cloud_uuid"].get();
+ if (state.contains("enabled") && state["enabled"].is_boolean())
+ parsed.enabled = state["enabled"].get();
+
+ // capabilities is a JSON array of single-key objects {: }.
+ if (state.contains("capabilities") && state["capabilities"].is_array()) {
+ for (const auto& item : state["capabilities"]) {
+ if (!item.is_object())
+ continue;
+ for (auto it = item.begin(); it != item.end(); ++it) {
+ if (it.value().is_boolean())
+ parsed.capabilities.emplace_back(it.key(), it.value().get());
+ }
+ }
+ }
+
+ out = std::move(parsed);
+ return true;
+ } catch (...) {
+ return false;
+ }
+}
+
+bool write_install_state(const boost::filesystem::path& plugin_dir, const PluginInstallState& state)
+{
+ namespace fs = boost::filesystem;
+ const fs::path sidecar_path = plugin_dir / ".install_state.json";
+
+ nlohmann::json json;
+ json["installed_from"] = state.installed_from;
+ json["installed_version"] = state.installed_version;
+ json["plugin_name"] = state.plugin_name;
+ json["enabled"] = state.enabled;
+ if (!state.cloud_uuid.empty())
+ json["cloud_uuid"] = state.cloud_uuid;
+
+ nlohmann::json capabilities = nlohmann::json::array();
+ for (const auto& [name, enabled] : state.capabilities)
+ capabilities.push_back(nlohmann::json{{name, enabled}});
+ json["capabilities"] = std::move(capabilities);
+
+ boost::nowide::ofstream f(sidecar_path.string());
+ if (!f)
+ return false;
+
+ f << json.dump(2);
+ return static_cast(f);
+}
+
+bool write_install_state(const boost::filesystem::path& plugin_dir, const PluginDescriptor& entry, bool enabled,
+ const std::vector>& capabilities)
+{
+ PluginInstallState state;
+ state.installed_from = entry.is_cloud_plugin() ? "cloud" : "local";
+ // Prefer the descriptor's recorded installed_version (the version fetched from the cloud
+ // at install time, preserved across sidecar re-writes) so a stale manifest/PEP723 header
+ // never overwrites the source-of-truth version. Fall back to the manifest version for
+ // first-time/local installs where installed_version is not yet populated.
+ state.installed_version = !entry.installed_version.empty() ? entry.installed_version : entry.version;
+ state.plugin_name = entry.name;
+ state.cloud_uuid = entry.cloud_uuid();
+ state.enabled = enabled;
+ state.capabilities = capabilities;
+ return write_install_state(plugin_dir, state);
+}
+
+bool write_install_state(const boost::filesystem::path& plugin_dir, const PluginDescriptor& entry)
+{
+ // Install-time writer: the package is not loaded, so its capabilities are not known yet and the
+ // sidecar is (re)initialized to "auto-load, nothing disabled". PluginManager writes the real
+ // per-capability flags once the package is loaded, via the (dir, entry, enabled, capabilities)
+ // overload.
+ return write_install_state(plugin_dir, entry, true, {});
+}
+
+bool read_python_plugin_metadata(const boost::filesystem::path& py_path, PluginDescriptor& descriptor, std::string& error)
+{
+ namespace fs = boost::filesystem;
+
+ if (!fs::exists(py_path) || !fs::is_regular_file(py_path)) {
+ error = "Python plugin file does not exist: " + py_path.string();
+ return false;
+ }
+
+ boost::nowide::ifstream f(py_path.string());
+ if (!f) {
+ error = "Failed to open Python plugin file: " + py_path.string();
+ return false;
+ }
+
+ // Scan for PEP 723 inline script metadata block.
+ // The block is delimited by:
+ // # /// script
+ // #
+ // # ///
+ std::string pep723_content;
+ bool in_block = false;
+ std::string line;
+
+ while (std::getline(f, line)) {
+ // Strip trailing carriage return (Windows line endings).
+ if (!line.empty() && line.back() == '\r')
+ line.pop_back();
+
+ if (!in_block) {
+ // Look for opening delimiter.
+ if (line == "# /// script")
+ in_block = true;
+ continue;
+ }
+
+ if (line == "# ///") {
+ in_block = false;
+ continue;
+ }
+
+ // Extract TOML content from the comment line.
+ // Lines must start with "# " or "#\t" per PEP 723.
+ if (line.size() >= 2 && line[0] == '#' && (line[1] == ' ' || line[1] == '\t'))
+ pep723_content += line.substr(2) + "\n";
+ else if (line == "#")
+ pep723_content += "\n";
+ // If line doesn't start with "# ", it's still part of the block content
+ // but we skip it as it doesn't follow the spec.
+ }
+
+ if (!pep723_content.empty()) {
+ std::string pep723_error;
+ std::string requires_python;
+ std::string pep_name, pep_desc, pep_author, pep_version;
+ if (!parse_pep723_toml(pep723_content,
+ descriptor.dependencies,
+ requires_python,
+ pep_name,
+ pep_desc,
+ pep_author,
+ pep_version,
+ pep723_error)) {
+ error = "Failed to parse PEP 723 metadata: " + pep723_error;
+ return false;
+ }
+ // requires-python is stored but not validated against the bundled Python here.
+ (void) requires_python;
+
+ // Populate identity fields from the PEP 723 [tool.orcaslicer.plugin] section.
+ // Cloud metadata overrides these when available; they serve as the local
+ // source of truth for side-loaded .py plugins and as fallback values.
+ if (!pep_name.empty()) descriptor.name = sanitize_plugin_name(pep_name);
+ if (!pep_desc.empty()) descriptor.description = pep_desc;
+ if (!pep_author.empty()) descriptor.author = pep_author;
+ if (!pep_version.empty()) descriptor.version = pep_version;
+ }
+
+ // Validate that required identity fields are present (either from PEP 723 or
+ // from cloud metadata already set on the manifest by the caller).
+ // Validation is deferred to the install/discovery layer so cloud metadata
+ // can fill in gaps.
+ return true;
+}
+
+bool read_wheel_plugin_metadata(const boost::filesystem::path& whl_path, PluginDescriptor& descriptor, std::string& error)
+{
+ namespace fs = boost::filesystem;
+
+ if (!fs::exists(whl_path) || !fs::is_regular_file(whl_path)) {
+ error = "Wheel plugin file does not exist: " + whl_path.string();
+ return false;
+ }
+
+ ZipReaderGuard reader;
+ if (!open_zip_reader(&reader.archive, whl_path.string())) {
+ error = "Failed to open wheel as zip: " + MZ_Archive::get_errorstr(mz_zip_get_last_error(&reader.archive));
+ return false;
+ }
+ reader.opened = true;
+
+ // Find the single .dist-info directory.
+ // Scan ALL entries, not just directory entries — some zip writers omit
+ // explicit directory entries. normalize_zip_entry_name strips trailing
+ // slashes, so we match ".dist-info" as the last path component of a
+ // directory entry, and ".dist-info/" embedded in a file path.
+ const mz_uint num_entries = mz_zip_reader_get_num_files(&reader.archive);
+ std::string dist_info_dir;
+ mz_zip_archive_file_stat stat;
+
+ for (mz_uint i = 0; i < num_entries; ++i) {
+ if (!mz_zip_reader_file_stat(&reader.archive, i, &stat))
+ continue;
+
+ std::string entry_name = normalize_zip_entry_name(zip_entry_name(reader.archive, stat));
+ if (entry_name.empty())
+ continue;
+
+ // Find .dist-info as a path component.
+ size_t pos = entry_name.find(".dist-info");
+ if (pos == std::string::npos)
+ continue;
+
+ std::string candidate;
+ if (pos + 10 == entry_name.size()) {
+ // Directory entry itself (trailing / stripped by normalize).
+ candidate = entry_name + "/";
+ } else if (pos + 10 < entry_name.size() && entry_name[pos + 10] == '/') {
+ // File inside .dist-info/: name.dist-info/METADATA
+ candidate = entry_name.substr(0, pos + 11); // include trailing /
+ } else {
+ continue; // .dist-info mid-name, not a path component.
+ }
+
+ if (!dist_info_dir.empty() && candidate != dist_info_dir) {
+ error = "Wheel contains multiple .dist-info directories: " + dist_info_dir + " and " + candidate;
+ return false;
+ }
+ dist_info_dir = candidate;
+ }
+
+ if (dist_info_dir.empty()) {
+ error = "Wheel does not contain a .dist-info directory";
+ return false;
+ }
+
+ // Read METADATA.
+ const std::string metadata_path = dist_info_dir + "METADATA";
+ std::string meta_content;
+ if (!read_zip_text_file(reader.archive, metadata_path.c_str(), meta_content, error))
+ return false;
+
+ std::string meta_name, meta_version, meta_summary, meta_author, meta_requires_python, meta_import_name;
+ std::vector requires_dist;
+ std::string meta_error;
+ parse_metadata_rfc822(meta_content, meta_name, meta_version, meta_summary, meta_author,
+ meta_requires_python, meta_import_name, requires_dist, meta_error);
+
+ if (meta_name.empty()) {
+ error = "Wheel METADATA missing required Name field";
+ return false;
+ }
+ if (meta_version.empty()) {
+ error = "Wheel METADATA missing required Version field";
+ return false;
+ }
+
+ // Read WHEEL (verify existence and Wheel-Version).
+ const std::string wheel_path = dist_info_dir + "WHEEL";
+ std::string wheel_content;
+ if (!read_zip_text_file(reader.archive, wheel_path.c_str(), wheel_content, error))
+ return false;
+ // Verify there's at least a Wheel-Version header line.
+ if (wheel_content.find("Wheel-Version:") == std::string::npos) {
+ error = "Wheel WHEEL file missing Wheel-Version header";
+ return false;
+ }
+
+ // Parse and validate wheel platform tags.
+ {
+ std::vector wheel_tags;
+ std::istringstream wstream(wheel_content);
+ std::string wline;
+ while (std::getline(wstream, wline)) {
+ while (!wline.empty() && (wline.back() == '\r' || wline.back() == '\n'))
+ wline.pop_back();
+ if (wline.rfind("Tag:", 0) == 0) {
+ std::string tag = wline.substr(4);
+ size_t s = 0, e = tag.size();
+ while (s < e && (tag[s] == ' ' || tag[s] == '\t')) ++s;
+ while (e > s && (tag[e - 1] == ' ' || tag[e - 1] == '\t')) --e;
+ wheel_tags.push_back(tag.substr(s, e - s));
+ }
+ }
+
+ if (!wheel_tags.empty()) {
+ bool compatible = false;
+ const std::string abi_tag = PythonInterpreter::python_abi_tag();
+ for (const auto& tag : wheel_tags) {
+ // Pure Python wheel: py3-none-any or cp312-none-any
+ if (tag.find("-none-any") != std::string::npos) {
+ compatible = true;
+ break;
+ }
+ // Platform-specific: check ABI tag matches.
+ if (tag.find(abi_tag) == 0) {
+ // Accept if the platform tag matches the current OS.
+#ifdef _WIN32
+ if (tag.find("-win") != std::string::npos)
+ compatible = true;
+#elif __APPLE__
+ if (tag.find("-macosx") != std::string::npos)
+ compatible = true;
+#else
+ if (tag.find("-linux") != std::string::npos || tag.find("-manylinux") != std::string::npos)
+ compatible = true;
+#endif
+ }
+ }
+ if (!compatible) {
+ error = "Wheel is incompatible with this platform. Tags: ";
+ for (size_t i = 0; i < wheel_tags.size(); ++i) {
+ if (i > 0) error += ", ";
+ error += wheel_tags[i];
+ }
+ error += "; expected ABI: " + abi_tag;
+ return false;
+ }
+ }
+ }
+
+ // Read RECORD (verify existence).
+ const std::string record_path = dist_info_dir + "RECORD";
+ std::string record_content;
+ if (!read_zip_text_file(reader.archive, record_path.c_str(), record_content, error))
+ return false;
+ if (record_content.empty()) {
+ error = "Wheel RECORD file is empty";
+ return false;
+ }
+
+ // Parse top_level.txt if present.
+ std::string top_level;
+ const std::string top_level_path = dist_info_dir + "top_level.txt";
+ std::string top_level_content;
+ if (read_zip_text_file(reader.archive, top_level_path.c_str(), top_level_content, error)) {
+ // top_level.txt contains one package name per line.
+ std::istringstream tl_stream(top_level_content);
+ std::string tl_line;
+ std::vector top_levels;
+ while (std::getline(tl_stream, tl_line)) {
+ while (!tl_line.empty() && (tl_line.back() == '\r' || tl_line.back() == '\n'))
+ tl_line.pop_back();
+ if (!tl_line.empty())
+ top_levels.push_back(tl_line);
+ }
+ if (top_levels.size() == 1)
+ top_level = top_levels[0];
+ else if (top_levels.size() > 1) {
+ // Ambiguous: multiple top-level packages. Fall through to Name-based fallback.
+ }
+ // Zero entries: leave top_level empty.
+ }
+ // If top_level.txt is not found, that's OK — it's optional per the wheel spec.
+
+ // Determine the entry package in priority order.
+ // 1. Cloud/catalog metadata — handled by caller, not here.
+ // 2. Core Metadata Import-Name.
+ // 3. top_level.txt if unambiguous.
+ // 4. Normalized Name as fallback.
+ if (!meta_import_name.empty()) {
+ descriptor.entry_package = meta_import_name;
+ } else if (!top_level.empty()) {
+ descriptor.entry_package = top_level;
+ } else {
+ descriptor.entry_package = normalize_package_name(meta_name);
+ }
+
+ descriptor.dependencies = std::move(requires_dist);
+
+ // Populate local identity fallbacks from wheel metadata.
+ // Cloud metadata will override these when available.
+ descriptor.name = sanitize_plugin_name(meta_name);
+ descriptor.version = meta_version;
+ descriptor.description = meta_summary;
+ descriptor.author = meta_author;
+
+ return true;
+}
+
+boost::filesystem::path find_installed_plugin_entry(const boost::filesystem::path& plugin_dir, std::string& error)
+{
+ namespace fs = boost::filesystem;
+
+ if (!fs::exists(plugin_dir) || !fs::is_directory(plugin_dir)) {
+ error = "Plugin directory does not exist: " + plugin_dir.string();
+ return {};
+ }
+
+ fs::path py_entry;
+ fs::path whl_entry;
+
+ for (fs::directory_iterator it(plugin_dir); it != fs::directory_iterator(); ++it) {
+ if (is_ignored_plugin_directory(it->path()))
+ continue;
+ if (!fs::is_regular_file(it->status()))
+ continue;
+
+ const fs::path ext = it->path().extension();
+ if (ext == ".py") {
+ if (!py_entry.empty()) {
+ error = "Plugin directory contains multiple .py files: " + py_entry.filename().string() +
+ " and " + it->path().filename().string();
+ return {};
+ }
+ py_entry = it->path();
+ } else if (ext == ".whl") {
+ if (!whl_entry.empty()) {
+ error = "Plugin directory contains multiple .whl files: " + whl_entry.filename().string() +
+ " and " + it->path().filename().string();
+ return {};
+ }
+ whl_entry = it->path();
+ }
+ }
+
+ if (!py_entry.empty() && !whl_entry.empty()) {
+ error = "Plugin directory contains both .py and .whl entry files";
+ return {};
+ }
+
+ if (!py_entry.empty())
+ return py_entry;
+
+ if (!whl_entry.empty())
+ return whl_entry;
+
+ error = "Plugin directory does not contain a .py or .whl entry file";
+ return {};
+}
} // namespace Slic3r
diff --git a/src/slic3r/plugin/PluginFsUtils.hpp b/src/slic3r/plugin/PluginFsUtils.hpp
index ea6cacd367..f65a5e0e69 100644
--- a/src/slic3r/plugin/PluginFsUtils.hpp
+++ b/src/slic3r/plugin/PluginFsUtils.hpp
@@ -81,6 +81,15 @@ inline nlohmann::json py_to_json(const pybind11::handle& o)
return py::str(o).cast(); // fallback: str()
}
+struct PluginInstallState {
+ std::string installed_from; // "local" | "cloud"
+ std::string installed_version;
+ std::string plugin_name;
+ std::string cloud_uuid; // empty for local
+ bool enabled = true;
+ std::vector> capabilities; // name -> enabled, ordered
+};
+
// Returns the cloud plugin install/scan directory for a given user_id.
// Path: {data_dir}/orca_plugins/_subscribed/{user_id}/
std::string get_cloud_plugin_dir(const std::string& user_id);
@@ -118,4 +127,38 @@ std::vector get_plugin_directories(const std::string& cloud_user_id
// descriptor.enabled. Capabilities are NOT discovered here — a package has none until it is loaded.
std::vector discover_plugin_packages(const std::vector& dirs, std::string& error);
+bool is_ignored_plugin_directory(const boost::filesystem::path& path);
+bool is_safe_relative_path(const boost::filesystem::path& path);
+bool is_valid_plugin_id(const std::string& id);
+bool extract_zip_to_directory(const boost::filesystem::path& zip_path, const boost::filesystem::path& destination, std::string& error);
+
+// Read PEP 723 inline script metadata from a .py plugin file.
+// Populates metadata.dependencies and local identity fallbacks.
+// Returns true on success (including when no PEP 723 block is found — deps will be empty).
+bool read_python_plugin_metadata(const boost::filesystem::path& py_path, PluginDescriptor& descriptor, std::string& error);
+
+// Read wheel metadata from a .whl plugin file (zip archive).
+// Reads METADATA, WHEEL, RECORD, and top_level.txt from the .dist-info directory.
+// Populates metadata.entry_package, metadata.dependencies, and local identity fallbacks.
+// Returns true on success.
+bool read_wheel_plugin_metadata(const boost::filesystem::path& whl_path, PluginDescriptor& descriptor, std::string& error);
+
+// Find the single plugin entry file (.py or .whl) in a directory.
+// Ignores __whl_extracted__ and hidden files/dirs.
+// Returns the path to the entry file, or an empty path with error set if zero or multiple candidates.
+boost::filesystem::path find_installed_plugin_entry(const boost::filesystem::path& plugin_dir, std::string& error);
+
+// Canonical writer: emits the .install_state.json schema for the given state.
+bool write_install_state(const boost::filesystem::path& plugin_dir, const PluginInstallState& state);
+// Builds a PluginInstallState from the descriptor and delegates to the canonical writer.
+bool write_install_state(const boost::filesystem::path& plugin_dir, const PluginDescriptor& entry, bool enabled,
+ const std::vector>& capabilities);
+// Convenience overload: write(dir, entry, /*enabled=*/true, /*capabilities=*/{}).
+bool write_install_state(const boost::filesystem::path& plugin_dir, const PluginDescriptor& entry);
+
+// Reads only the cloud identity (uuid) back into the descriptor; plugin_key is always derived.
+void read_install_state(const boost::filesystem::path& plugin_dir, PluginDescriptor& entry);
+// Full read of the sidecar; returns false if there is no/invalid sidecar.
+bool read_install_state(const boost::filesystem::path& plugin_dir, PluginInstallState& out);
+
} // namespace Slic3r
diff --git a/src/slic3r/plugin/PluginLoader.cpp b/src/slic3r/plugin/PluginLoader.cpp
index a0c2af6758..92fe92068f 100644
--- a/src/slic3r/plugin/PluginLoader.cpp
+++ b/src/slic3r/plugin/PluginLoader.cpp
@@ -6,7 +6,6 @@
#include "PluginManager.hpp"
#include "PluginFsUtils.hpp"
-#include "PythonFileUtils.hpp"
#include "PythonInterpreter.hpp"
#include "PythonPluginBridge.hpp"
#include "libslic3r/Utils.hpp"
diff --git a/src/slic3r/plugin/PluginManager.cpp b/src/slic3r/plugin/PluginManager.cpp
index 2dee799575..613cf0465b 100644
--- a/src/slic3r/plugin/PluginManager.cpp
+++ b/src/slic3r/plugin/PluginManager.cpp
@@ -6,7 +6,6 @@
#include "PluginFsUtils.hpp"
#include "PluginHooks.hpp"
-#include "PythonFileUtils.hpp"
#include "PythonInterpreter.hpp"
#include "PythonPluginBridge.hpp"
@@ -1282,7 +1281,7 @@ const char* const CLOUD_PLUGIN_NOT_FOUND_ERROR = "Plugin was not found in the cl
} // namespace
-void PluginManager::update_cloud_catalog(const std::vector& cloud_list)
+void PluginManager::update_cloud_metadata(const std::vector& cloud_list)
{
std::lock_guard lock(m_mutex);
@@ -1368,7 +1367,7 @@ void PluginManager::update_cloud_catalog(const std::vector& cl
}
}
-void PluginManager::clear_cloud_plugin_catalog()
+void PluginManager::clear_cloud_plugin_metadata()
{
// Cloud entries may own live Python modules. Unload them before erasing their vector entries,
// and do so outside m_mutex because both Python teardown and lifecycle callbacks can re-enter
@@ -1384,7 +1383,7 @@ void PluginManager::clear_cloud_plugin_catalog()
}
});
- BOOST_LOG_TRIVIAL(info) << "Cleared cloud plugin catalog entries";
+ BOOST_LOG_TRIVIAL(info) << "Cleared cloud plugin metadata";
}
void PluginManager::fetch_plugins_from_cloud(std::vector* out_not_found, std::vector* out_unauthorized)
@@ -1409,7 +1408,7 @@ void PluginManager::fetch_plugins_from_cloud(std::vector* out_not_f
}
}
- update_cloud_catalog(cloud_list);
+ update_cloud_metadata(cloud_list);
{
std::lock_guard lock(m_mutex);
@@ -1473,7 +1472,6 @@ bool PluginManager::subscribe_and_install_cloud_plugin(const std::string& plugin
PluginDescriptor descriptor;
bool found = try_get_plugin_descriptor(plugin_key, descriptor) && descriptor.is_cloud_plugin();
if (!found) {
- // The plugin may already be subscribed or owned while the local catalog is stale.
fetch_plugins_from_cloud();
found = try_get_plugin_descriptor(plugin_key, descriptor) && descriptor.is_cloud_plugin();
}
@@ -1559,9 +1557,9 @@ bool PluginManager::download_and_install_cloud_plugin(const std::string& plugin_
std::lock_guard lock(m_mutex);
Plugin* entry = find_plugin_locked(plugin_key);
if (entry == nullptr) {
- error = "Plugin Manifest not found.";
+ error = "Plugin not found.";
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": Cloud plugin " << plugin_key
- << " downloaded successfully but failed to update plugin manifest. Manifest not found in catalog.";
+ << " downloaded successfully but failed to update plugin metadata. Plugin object not found.";
} else {
entry->descriptor = std::move(updated_descriptor);
}
@@ -1762,9 +1760,6 @@ bool PluginManager::keep_installed_plugin_as_local(const PluginDescriptor& plugi
bool PluginManager::finalize_cloud_plugin_removal(const PluginDescriptor& plugin, bool keep_local, std::string& error)
{
- // Shared by all four cloud-removal entrypoints after the cloud-side request succeeds. Handles
- // the common local follow-up of keeping a detached local copy, deleting local files, or
- // dropping a cloud-only row from the catalog.
if (keep_local && plugin.has_local_package()) {
if (!keep_installed_plugin_as_local(plugin, error))
return false;
@@ -1775,8 +1770,7 @@ bool PluginManager::finalize_cloud_plugin_removal(const PluginDescriptor& plugin
if (plugin.has_local_package()) {
if (!delete_installed_plugin_package(plugin, error))
return false;
- // Re-sync the cloud catalog so observers/UI see the updated cloud list after the local
- // package has been removed.
+
fetch_plugins_from_cloud();
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": Deleted local package after cloud removal: " << plugin.plugin_key;
return true;
@@ -1788,7 +1782,7 @@ bool PluginManager::finalize_cloud_plugin_removal(const PluginDescriptor& plugin
[&plugin](const Plugin& entry) { return entry.descriptor.plugin_key == plugin.plugin_key; }),
m_plugins.end());
}
- BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": Removed cloud-only plugin from catalog: " << plugin.plugin_key;
+ BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": Removed cloud-only plugin: " << plugin.plugin_key;
return true;
}
diff --git a/src/slic3r/plugin/PluginManager.hpp b/src/slic3r/plugin/PluginManager.hpp
index 84a5c062e1..59a2791355 100644
--- a/src/slic3r/plugin/PluginManager.hpp
+++ b/src/slic3r/plugin/PluginManager.hpp
@@ -185,8 +185,8 @@ public:
bool clear_plugin_error(const std::string& plugin_key);
void fetch_plugins_from_cloud(std::vector* out_not_found = nullptr, std::vector* out_unauthorized = nullptr);
- void update_cloud_catalog(const std::vector& cloud_list);
- void clear_cloud_plugin_catalog();
+ void update_cloud_metadata(const std::vector& cloud_list);
+ void clear_cloud_plugin_metadata();
bool download_and_install_cloud_plugin(const std::string& plugin_key, const std::string& version, std::string& error);
bool subscribe_and_install_cloud_plugin(const std::string& plugin_key, std::string& error);
diff --git a/src/slic3r/plugin/PythonFileUtils.cpp b/src/slic3r/plugin/PythonFileUtils.cpp
deleted file mode 100644
index eeb9bf3a3e..0000000000
--- a/src/slic3r/plugin/PythonFileUtils.cpp
+++ /dev/null
@@ -1,951 +0,0 @@
-#include "PythonFileUtils.hpp"
-
-#include "PluginFsUtils.hpp"
-#include "PythonInterpreter.hpp"
-#include "libslic3r/Utils.hpp"
-#include "PluginDescriptor.hpp"
-#include "libslic3r/miniz_extension.hpp"
-
-#include
-#include
-#include
-
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-
-#ifdef WIN32
-#include
-#endif
-
-namespace Slic3r {
-namespace {
-
-bool is_safe_archive_entry_path(const boost::filesystem::path& path)
-{
- if (!is_safe_relative_path(path))
- return false;
-
- for (const auto& part : path) {
- const std::string token = part.string();
- if (token.empty() || token == "." || token == "..")
- return false;
- }
-
- return true;
-}
-
-std::string decode_zip_entry_extra_path(const std::string& extra, const std::string& fallback)
-{
- const char* p = extra.data();
- const char* e = p + extra.length();
- while (p + 4 <= e) {
- const auto len = static_cast(static_cast(p[2])) |
- static_cast(static_cast(p[3]) << 8);
- if (p[0] == '\x75' && p[1] == '\x70' && len >= 5 && p + 4 + len <= e && p[4] == '\x01')
- return std::string(p + 9, p + 4 + len);
- p += 4 + len;
- }
-
- return decode_path(fallback.c_str());
-}
-
-std::string zip_entry_name(mz_zip_archive& archive, const mz_zip_archive_file_stat& stat)
-{
- if (stat.m_is_utf8)
- return stat.m_filename;
-
- std::string extra(1024, 0);
- const size_t n = mz_zip_reader_get_extra(&archive, stat.m_file_index, extra.data(), extra.size());
- return decode_zip_entry_extra_path(extra.substr(0, n), stat.m_filename);
-}
-
-std::string normalize_zip_entry_name(std::string entry_name)
-{
- std::replace(entry_name.begin(), entry_name.end(), '\\', '/');
- while (!entry_name.empty() && entry_name.back() == '/')
- entry_name.pop_back();
- return entry_name;
-}
-
-struct ZipReaderGuard
-{
- mz_zip_archive archive;
- bool opened = false;
-
- ZipReaderGuard() { mz_zip_zero_struct(&archive); }
-
- ~ZipReaderGuard()
- {
- if (opened)
- close_zip_reader(&archive);
- }
-};
-
-} // namespace
-
-bool is_valid_plugin_id(const std::string& id)
-{
- if (id.empty())
- return false;
- if (id == "." || id == ".." || id[0] == '.' || id.rfind("__", 0) == 0)
- return false;
-
- for (unsigned char ch : id) {
- if (std::isalnum(ch) || ch == '_' || ch == '-' || ch == '.')
- continue;
- return false;
- }
-
- return true;
-}
-
-namespace {
-
-// RAII helper to free heap-allocated memory from miniz.
-struct MzHeapFree {
- void* ptr = nullptr;
- ~MzHeapFree() { if (ptr) std::free(ptr); }
-};
-
-// Read a text file from within a zip archive into a string.
-// Returns true on success, false if the file is not found or cannot be read.
-bool read_zip_text_file(mz_zip_archive& archive, const char* filename, std::string& out, std::string& error)
-{
- size_t size = 0;
- void* data = mz_zip_reader_extract_file_to_heap(&archive, filename, &size, 0);
- if (!data) {
- error = std::string("Wheel does not contain ") + filename;
- return false;
- }
- MzHeapFree guard{data};
- out.assign(static_cast(data), size);
- return true;
-}
-
-// TOML section parsing states.
-enum class TomlSection { Root, OrcaPlugin, OrcaPluginSettings, InDepsArray };
-
-// Strip a quoted string value: "foo" → foo, 'foo' → foo.
-// Returns the unquoted value or the input unchanged if not quoted.
-std::string unquote_toml_string(const std::string& val)
-{
- if (val.size() >= 2 && ((val.front() == '"' && val.back() == '"') || (val.front() == '\'' && val.back() == '\'')))
- return val.substr(1, val.size() - 2);
- return val;
-}
-
-// Split a TOML inline array: ["a", "b"] → {"a", "b"}.
-// Handles trailing commas and single-line format.
-std::vector parse_toml_inline_array(const std::string& val)
-{
- std::vector result;
- std::string inner = val;
- // Strip outer brackets.
- if (!inner.empty() && inner.front() == '[')
- inner.erase(0, 1);
- if (!inner.empty() && inner.back() == ']')
- inner.pop_back();
-
- // Simple split by comma, strip quotes and whitespace.
- std::istringstream ss(inner);
- std::string item;
- while (std::getline(ss, item, ',')) {
- // Trim whitespace.
- size_t s = 0, e = item.size();
- while (s < e && (item[s] == ' ' || item[s] == '\t')) ++s;
- while (e > s && (item[e - 1] == ' ' || item[e - 1] == '\t')) --e;
- item = item.substr(s, e - s);
- if (!item.empty())
- result.push_back(unquote_toml_string(item));
- }
- return result;
-}
-
-// Parse PEP 723 TOML subset for dependencies, requires-python, and
-// [tool.orcaslicer.plugin] identity fields.
-//
-// requires-python = ">=3.12"
-// dependencies = ["pkg>=1.0", ]
-//
-// [tool.orcaslicer.plugin]
-// id = "my-plugin"
-// name = "My Plugin"
-// description = "Does things."
-// author = "Author"
-// version = "1.0.0"
-//
-// Returns false only on parse errors; missing block is not an error.
-bool parse_pep723_toml(const std::string& toml_content,
- std::vector& out_deps,
- std::string& out_requires_python,
- std::string& out_name,
- std::string& out_description,
- std::string& out_author,
- std::string& out_version,
- std::string& error)
-{
- out_deps.clear();
- out_requires_python.clear();
- out_name.clear();
- out_description.clear();
- out_author.clear();
- out_version.clear();
-
- TomlSection section = TomlSection::Root;
-
- std::istringstream stream(toml_content);
- std::string line;
-
- while (std::getline(stream, line)) {
- // Trim leading/trailing whitespace.
- size_t start = 0;
- while (start < line.size() && (line[start] == ' ' || line[start] == '\t'))
- ++start;
- size_t end = line.size();
- while (end > start && (line[end - 1] == ' ' || line[end - 1] == '\t'))
- --end;
- std::string trimmed = line.substr(start, end - start);
-
- if (trimmed.empty() || trimmed[0] == '#')
- continue;
-
- // TOML section header.
- if (trimmed[0] == '[') {
- if (trimmed == "[tool.orcaslicer.plugin]") {
- section = TomlSection::OrcaPlugin;
- } else if (trimmed == "[tool.orcaslicer.plugin.settings]") {
- section = TomlSection::OrcaPluginSettings; // per-plugin params table
- } else {
- section = TomlSection::Root; // Unknown section — skip.
- }
- continue;
- }
-
- if (section == TomlSection::InDepsArray) {
- if (trimmed == "]") {
- section = TomlSection::Root;
- continue;
- }
- std::string val = trimmed;
- if (!val.empty() && val.back() == ',')
- val.pop_back();
- val = unquote_toml_string(val);
- if (!val.empty())
- out_deps.push_back(val);
- continue;
- }
-
- // Look for key = value.
- size_t eq = trimmed.find('=');
- if (eq == std::string::npos)
- continue;
-
- std::string key = trimmed.substr(0, eq);
- while (!key.empty() && (key.back() == ' ' || key.back() == '\t'))
- key.pop_back();
-
- std::string val = trimmed.substr(eq + 1);
- while (!val.empty() && (val.front() == ' ' || val.front() == '\t'))
- val.erase(0, 1);
- // Trim trailing.
- while (!val.empty() && (val.back() == ' ' || val.back() == '\t'))
- val.pop_back();
-
- if (section == TomlSection::Root) {
- if (key == "requires-python") {
- out_requires_python = unquote_toml_string(val);
- } else if (key == "dependencies") {
- if (val == "[") {
- section = TomlSection::InDepsArray;
- } else {
- // Inline array: dependencies = ["a", "b"]
- out_deps = parse_toml_inline_array(val);
- }
- }
- } else if (section == TomlSection::OrcaPlugin) {
- if (key == "name") out_name = unquote_toml_string(val);
- else if (key == "description") out_description = unquote_toml_string(val);
- else if (key == "author") out_author = unquote_toml_string(val);
- else if (key == "version") out_version = unquote_toml_string(val);
- }
- }
-
- // Check for unclosed arrays.
- if (section == TomlSection::InDepsArray) {
- error = "PEP 723 metadata: unclosed dependencies array";
- return false;
- }
- return true;
-}
-
-// Normalize a distribution name to a Python import package name.
-// Converts hyphens to underscores and lowercases.
-std::string normalize_package_name(const std::string& name)
-{
- std::string result;
- result.reserve(name.size());
- for (unsigned char ch : name) {
- if (ch == '-' || ch == '.')
- result += '_';
- else
- result += static_cast(std::tolower(ch));
- }
- return result;
-}
-
-// Parse METADATA (RFC 822 style) into a flat multimap.
-// https://packaging.python.org/en/latest/specifications/core-metadata/
-void parse_metadata_rfc822(const std::string& content,
- std::string& out_name,
- std::string& out_version,
- std::string& out_summary,
- std::string& out_author,
- std::string& out_requires_python,
- std::string& out_import_name,
- std::vector& out_requires_dist,
- std::string& error)
-{
- std::istringstream stream(content);
- std::string line;
- std::string current_header;
- std::string current_value;
-
- auto flush = [&]() {
- if (current_header.empty())
- return;
- std::string lower = current_header;
- std::transform(lower.begin(), lower.end(), lower.begin(),
- [](unsigned char c) { return std::tolower(c); });
-
- if (lower == "name")
- out_name = current_value;
- else if (lower == "version")
- out_version = current_value;
- else if (lower == "summary")
- out_summary = current_value;
- else if (lower == "author")
- out_author = current_value;
- else if (lower == "requires-python")
- out_requires_python = current_value;
- else if (lower == "import-name")
- out_import_name = current_value;
- else if (lower == "requires-dist")
- out_requires_dist.push_back(current_value);
-
- current_header.clear();
- current_value.clear();
- };
-
- while (std::getline(stream, line)) {
- // Blank line after headers marks the start of the body.
- if (line.empty() && current_header.empty())
- continue;
- if (line.empty()) {
- flush();
- // Remaining content is the body (description); stop parsing headers.
- break;
- }
-
- // Continuation line.
- if (line[0] == ' ' || line[0] == '\t') {
- if (!current_value.empty())
- current_value += '\n';
- size_t pos = line.find_first_not_of(" \t");
- current_value += (pos != std::string::npos) ? line.substr(pos) : "";
- continue;
- }
-
- flush();
-
- size_t colon = line.find(':');
- if (colon == std::string::npos)
- continue;
-
- current_header = line.substr(0, colon);
- size_t val_start = colon + 1;
- while (val_start < line.size() && (line[val_start] == ' ' || line[val_start] == '\t'))
- ++val_start;
- current_value = line.substr(val_start);
- }
-
- flush();
-}
-
-} // namespace
-
-bool is_ignored_plugin_directory(const boost::filesystem::path& path)
-{
- const std::string name = path.filename().string();
- return name.empty() || name[0] == '.' || name.rfind("__", 0) == 0 || name == PLUGIN_SUBSCRIBED_DIR;
-}
-
-bool is_safe_relative_path(const boost::filesystem::path& path)
-{
- if (path.empty() || path.is_absolute() || path.has_root_directory() || path.has_root_name())
- return false;
-
- for (const auto& part : path) {
- const std::string token = part.string();
- if (token == "..")
- return false;
- }
-
- return true;
-}
-
-bool extract_zip_to_directory(const boost::filesystem::path& zip_path, const boost::filesystem::path& destination, std::string& error)
-{
- namespace fs = boost::filesystem;
-
- boost::system::error_code ec;
- fs::create_directories(destination, ec);
- if (ec) {
- error = "Failed to create plugin staging directory: " + ec.message();
- return false;
- }
-
- ZipReaderGuard reader;
- if (!open_zip_reader(&reader.archive, zip_path.string())) {
- error = "Failed to open plugin zip: " + MZ_Archive::get_errorstr(mz_zip_get_last_error(&reader.archive));
- return false;
- }
- reader.opened = true;
-
- std::unordered_set extracted_entries;
- const mz_uint num_entries = mz_zip_reader_get_num_files(&reader.archive);
- mz_zip_archive_file_stat stat;
- for (mz_uint i = 0; i < num_entries; ++i) {
- if (!mz_zip_reader_file_stat(&reader.archive, i, &stat)) {
- error = "Failed to read plugin zip entry metadata: " + MZ_Archive::get_errorstr(mz_zip_get_last_error(&reader.archive));
- return false;
- }
-
- std::string entry_name = normalize_zip_entry_name(zip_entry_name(reader.archive, stat));
- if (entry_name.empty())
- continue;
- if (entry_name.find(':') != std::string::npos) {
- error = "Plugin zip entry contains an invalid path: " + entry_name;
- return false;
- }
-
- const fs::path relative_path(entry_name);
- if (!is_safe_archive_entry_path(relative_path)) {
- error = "Plugin zip entry escapes the plugin package: " + entry_name;
- return false;
- }
-
- const std::string relative_key = relative_path.generic_string();
- if (!extracted_entries.insert(relative_key).second) {
- error = "Plugin zip contains duplicate entry: " + relative_key;
- return false;
- }
-
- const fs::path output_path = destination / relative_path;
- if (stat.m_is_directory || mz_zip_reader_is_file_a_directory(&reader.archive, stat.m_file_index)) {
- fs::create_directories(output_path, ec);
- if (ec) {
- error = "Failed to create plugin zip directory " + output_path.string() + ": " + ec.message();
- return false;
- }
- continue;
- }
-
- fs::create_directories(output_path.parent_path(), ec);
- if (ec) {
- error = "Failed to create plugin zip parent directory " + output_path.parent_path().string() + ": " + ec.message();
- return false;
- }
-
- if (fs::exists(output_path, ec) && fs::is_directory(output_path, ec)) {
- error = "Plugin zip file conflicts with an existing directory: " + output_path.string();
- return false;
- }
-
- const std::string encoded_output_path = encode_path(output_path.string().c_str());
- mz_bool extracted = mz_zip_reader_extract_to_file(&reader.archive, stat.m_file_index, encoded_output_path.c_str(), 0);
-#ifdef WIN32
- if (!extracted) {
- const std::wstring wide_output_path = boost::locale::conv::utf_to_utf(output_path.generic_string());
- extracted = mz_zip_reader_extract_to_file_w(&reader.archive, stat.m_file_index, wide_output_path.c_str(), 0);
- }
-#endif
- if (!extracted) {
- error = "Failed to extract plugin zip entry " + relative_key + ": " +
- MZ_Archive::get_errorstr(mz_zip_get_last_error(&reader.archive));
- return false;
- }
- }
-
- return true;
-}
-
-void read_install_state(const boost::filesystem::path& plugin_dir, PluginDescriptor& entry)
-{
- PluginInstallState state;
- if (!read_install_state(plugin_dir, state))
- return;
-
- // The cloud identity and the persisted installed version are read back. plugin_key
- // is always derived by the catalog scan (filename for local, the cloud uuid for
- // cloud), so it is not read from the sidecar. installed_version is the source of
- // truth for a cloud plugin's installed version: it records the version fetched from
- // the cloud at install time, independent of the (possibly stale) manifest/PEP723
- // header that scan_directory parses into entry.version.
- if (!state.installed_version.empty())
- entry.installed_version = state.installed_version;
- if (!state.cloud_uuid.empty())
- entry.cloud = CloudPluginState{state.cloud_uuid, true, false, false};
-
- // Package-level auto-load flag only. The per-capability enable flags stay in the sidecar: a
- // capability has no existence — and so no state — until it is materialized, at which point the
- // loader seeds the flag onto the capability itself.
- entry.enabled = state.enabled;
-}
-
-bool read_install_state(const boost::filesystem::path& plugin_dir, PluginInstallState& out)
-{
- namespace fs = boost::filesystem;
- const fs::path sidecar_path = plugin_dir / ".install_state.json";
-
- if (!fs::exists(sidecar_path) || !fs::is_regular_file(sidecar_path))
- return false;
-
- boost::nowide::ifstream f(sidecar_path.string());
- if (!f)
- return false;
-
- try {
- nlohmann::json state = nlohmann::json::parse(f, nullptr, false, true);
- if (state.is_discarded() || !state.is_object())
- return false;
-
- PluginInstallState parsed;
- if (state.contains("installed_from") && state["installed_from"].is_string())
- parsed.installed_from = state["installed_from"].get();
- if (state.contains("installed_version") && state["installed_version"].is_string())
- parsed.installed_version = state["installed_version"].get();
- if (state.contains("plugin_name") && state["plugin_name"].is_string())
- parsed.plugin_name = state["plugin_name"].get();
- if (state.contains("cloud_uuid") && state["cloud_uuid"].is_string())
- parsed.cloud_uuid = state["cloud_uuid"].get();
- if (state.contains("enabled") && state["enabled"].is_boolean())
- parsed.enabled = state["enabled"].get();
-
- // capabilities is a JSON array of single-key objects {: }.
- if (state.contains("capabilities") && state["capabilities"].is_array()) {
- for (const auto& item : state["capabilities"]) {
- if (!item.is_object())
- continue;
- for (auto it = item.begin(); it != item.end(); ++it) {
- if (it.value().is_boolean())
- parsed.capabilities.emplace_back(it.key(), it.value().get());
- }
- }
- }
-
- out = std::move(parsed);
- return true;
- } catch (...) {
- return false;
- }
-}
-
-bool write_install_state(const boost::filesystem::path& plugin_dir, const PluginInstallState& state)
-{
- namespace fs = boost::filesystem;
- const fs::path sidecar_path = plugin_dir / ".install_state.json";
-
- nlohmann::json json;
- json["installed_from"] = state.installed_from;
- json["installed_version"] = state.installed_version;
- json["plugin_name"] = state.plugin_name;
- json["enabled"] = state.enabled;
- if (!state.cloud_uuid.empty())
- json["cloud_uuid"] = state.cloud_uuid;
-
- nlohmann::json capabilities = nlohmann::json::array();
- for (const auto& [name, enabled] : state.capabilities)
- capabilities.push_back(nlohmann::json{{name, enabled}});
- json["capabilities"] = std::move(capabilities);
-
- boost::nowide::ofstream f(sidecar_path.string());
- if (!f)
- return false;
-
- f << json.dump(2);
- return static_cast