mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-19 15:03:05 +00:00
Merge branch 'main' into weilun/speed_dial
This commit is contained in:
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-09-02 09:39-0300\n"
|
||||
"POT-Creation-Date: 2026-09-03 10:27+0800\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
@@ -4571,6 +4571,9 @@ msgid ""
|
||||
"No - Cancel enabling spiral mode"
|
||||
msgstr ""
|
||||
|
||||
msgid "N/A"
|
||||
msgstr ""
|
||||
|
||||
msgid "Printing"
|
||||
msgstr ""
|
||||
|
||||
@@ -5019,9 +5022,6 @@ msgstr ""
|
||||
msgid "Invalid format. Expected vector format: \"%1%\""
|
||||
msgstr ""
|
||||
|
||||
msgid "N/A"
|
||||
msgstr ""
|
||||
|
||||
msgid "System agents"
|
||||
msgstr ""
|
||||
|
||||
@@ -5951,6 +5951,12 @@ msgstr ""
|
||||
msgid "Save current project as"
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish 3MF"
|
||||
msgstr ""
|
||||
|
||||
msgid "Export a 3MF file with the selected settings embedded"
|
||||
msgstr ""
|
||||
|
||||
msgid "Import 3MF/STL/STEP/SVG/OBJ/AMF"
|
||||
msgstr ""
|
||||
|
||||
@@ -7696,6 +7702,12 @@ msgstr ""
|
||||
msgid "Customized Preset"
|
||||
msgstr ""
|
||||
|
||||
msgid "Some published settings could not be applied:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Some filament slots were changed:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Component name(s) inside step file not in UTF-8 format!"
|
||||
msgstr ""
|
||||
|
||||
@@ -8074,6 +8086,17 @@ msgstr ""
|
||||
msgid "The file %s has been sent to the printer's storage space and can be viewed on the printer."
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish 3MF file as:"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"Failed to export the published 3MF file.\n"
|
||||
"Please check whether the folder exists online or if other programs have the file open."
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish"
|
||||
msgstr ""
|
||||
|
||||
msgid "The nozzle type is not set. Please set the nozzle and try again."
|
||||
msgstr ""
|
||||
|
||||
@@ -9124,9 +9147,6 @@ msgstr ""
|
||||
msgid "Note: The preparation may take several minutes. Please be patient."
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish"
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish was canceled"
|
||||
msgstr ""
|
||||
|
||||
@@ -9142,6 +9162,64 @@ msgstr ""
|
||||
msgid "Jump to webpage"
|
||||
msgstr ""
|
||||
|
||||
msgid "Material"
|
||||
msgstr ""
|
||||
|
||||
msgid "Mixed filament"
|
||||
msgstr ""
|
||||
|
||||
msgid "Some mixed filaments rely on filaments that will not be published:"
|
||||
msgstr ""
|
||||
|
||||
#, possible-c-format, possible-boost-format
|
||||
msgid "Filament %d (mixed)"
|
||||
msgstr ""
|
||||
|
||||
msgid "needs"
|
||||
msgstr ""
|
||||
|
||||
msgid "not enabled"
|
||||
msgstr ""
|
||||
|
||||
msgid "material not published"
|
||||
msgstr ""
|
||||
|
||||
msgid "To publish a mixed filament, enable every filament it uses and choose Full Publish or check its Type requirement."
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish anyway"
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish 3MF..."
|
||||
msgstr ""
|
||||
|
||||
msgid "Select which settings to be published in the 3MF file"
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish 3MF Wiki"
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish 3MF Video Guide"
|
||||
msgstr ""
|
||||
|
||||
msgid "Mixed filament - published as a whole when \"Enable\" above is selected"
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish this mixed filament and enable + Full Publish its component filaments"
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish this filament slot in the 3MF file"
|
||||
msgstr ""
|
||||
|
||||
msgid "Full Publish"
|
||||
msgstr ""
|
||||
|
||||
msgid "Embed the entire filament of this slot in the 3MF file"
|
||||
msgstr ""
|
||||
|
||||
msgid "Filter non-selected"
|
||||
msgstr ""
|
||||
|
||||
#, possible-c-format, possible-boost-format
|
||||
msgid "Save %s as"
|
||||
msgstr ""
|
||||
@@ -19337,6 +19415,15 @@ msgstr ""
|
||||
msgid "Skipping objects."
|
||||
msgstr ""
|
||||
|
||||
msgid "Material Ratio"
|
||||
msgstr ""
|
||||
|
||||
msgid "Model Height"
|
||||
msgstr ""
|
||||
|
||||
msgid "Ratio"
|
||||
msgstr ""
|
||||
|
||||
msgid "Select Filament"
|
||||
msgstr ""
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: \n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-09-02 09:39-0300\n"
|
||||
"POT-Creation-Date: 2026-09-03 10:27+0800\n"
|
||||
"PO-Revision-Date: 2025-03-15 10:55+0100\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: \n"
|
||||
@@ -4995,6 +4995,9 @@ msgstr ""
|
||||
"Sí: canviar aquesta configuració i activar el mode d'espiral automàticament\n"
|
||||
"No - Renunciar a utilitzar el mode espiral aquesta vegada"
|
||||
|
||||
msgid "N/A"
|
||||
msgstr "N/D"
|
||||
|
||||
msgid "Printing"
|
||||
msgstr "Imprimint"
|
||||
|
||||
@@ -5456,9 +5459,6 @@ msgstr "Patró no vàlid. Utilitzeu N, N#K, o una llista separada per comes amb
|
||||
msgid "Invalid format. Expected vector format: \"%1%\""
|
||||
msgstr "Format no vàlid. Format vectorial esperat: \"%1%\""
|
||||
|
||||
msgid "N/A"
|
||||
msgstr "N/D"
|
||||
|
||||
# AI Translated
|
||||
msgid "System agents"
|
||||
msgstr "Agents del sistema"
|
||||
@@ -6421,6 +6421,12 @@ msgstr "Desa el projecte com a"
|
||||
msgid "Save current project as"
|
||||
msgstr "Desar el projecte actual com"
|
||||
|
||||
msgid "Publish 3MF"
|
||||
msgstr ""
|
||||
|
||||
msgid "Export a 3MF file with the selected settings embedded"
|
||||
msgstr ""
|
||||
|
||||
msgid "Import 3MF/STL/STEP/SVG/OBJ/AMF"
|
||||
msgstr "Importar 3MF STL/STEP/SVG/OBJ/AMF"
|
||||
|
||||
@@ -8275,6 +8281,12 @@ msgstr "Confirmeu que els Codis-G d'aquests perfils són segurs per evitar danys
|
||||
msgid "Customized Preset"
|
||||
msgstr "Perfil personalitzat"
|
||||
|
||||
msgid "Some published settings could not be applied:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Some filament slots were changed:"
|
||||
msgstr ""
|
||||
|
||||
# AI Translated
|
||||
msgid "Component name(s) inside step file not in UTF-8 format!"
|
||||
msgstr "Els noms dels components dins del fitxer STEP no tenen format UTF-8!"
|
||||
@@ -8687,6 +8699,17 @@ msgstr "Desa el fitxer Laminat com a:"
|
||||
msgid "The file %s has been sent to the printer's storage space and can be viewed on the printer."
|
||||
msgstr "El fitxer %s s'ha enviat a l'emmagatzematge de la impressora i es pot visualitzar a la impressora."
|
||||
|
||||
msgid "Publish 3MF file as:"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"Failed to export the published 3MF file.\n"
|
||||
"Please check whether the folder exists online or if other programs have the file open."
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish"
|
||||
msgstr "Publicar"
|
||||
|
||||
msgid "The nozzle type is not set. Please set the nozzle and try again."
|
||||
msgstr "El tipus de broquet no està establert. Establiu el broquet i torneu-ho a provar."
|
||||
|
||||
@@ -9880,9 +9903,6 @@ msgstr "Anar a la pàgina web de publicació de models"
|
||||
msgid "Note: The preparation may take several minutes. Please be patient."
|
||||
msgstr "Nota: La preparació pot trigar uns quants minuts. Si us plau, sigui pacient."
|
||||
|
||||
msgid "Publish"
|
||||
msgstr "Publicar"
|
||||
|
||||
msgid "Publish was canceled"
|
||||
msgstr "La publicació ha estat cancel·lada"
|
||||
|
||||
@@ -9898,6 +9918,64 @@ msgstr "Carregant dades"
|
||||
msgid "Jump to webpage"
|
||||
msgstr "Anar a la pàgina web"
|
||||
|
||||
msgid "Material"
|
||||
msgstr ""
|
||||
|
||||
msgid "Mixed filament"
|
||||
msgstr ""
|
||||
|
||||
msgid "Some mixed filaments rely on filaments that will not be published:"
|
||||
msgstr ""
|
||||
|
||||
#, c-format, boost-format
|
||||
msgid "Filament %d (mixed)"
|
||||
msgstr ""
|
||||
|
||||
msgid "needs"
|
||||
msgstr ""
|
||||
|
||||
msgid "not enabled"
|
||||
msgstr ""
|
||||
|
||||
msgid "material not published"
|
||||
msgstr ""
|
||||
|
||||
msgid "To publish a mixed filament, enable every filament it uses and choose Full Publish or check its Type requirement."
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish anyway"
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish 3MF..."
|
||||
msgstr ""
|
||||
|
||||
msgid "Select which settings to be published in the 3MF file"
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish 3MF Wiki"
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish 3MF Video Guide"
|
||||
msgstr ""
|
||||
|
||||
msgid "Mixed filament - published as a whole when \"Enable\" above is selected"
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish this mixed filament and enable + Full Publish its component filaments"
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish this filament slot in the 3MF file"
|
||||
msgstr ""
|
||||
|
||||
msgid "Full Publish"
|
||||
msgstr ""
|
||||
|
||||
msgid "Embed the entire filament of this slot in the 3MF file"
|
||||
msgstr ""
|
||||
|
||||
msgid "Filter non-selected"
|
||||
msgstr ""
|
||||
|
||||
#, c-format, boost-format
|
||||
msgid "Save %s as"
|
||||
msgstr "Desar %s com a"
|
||||
@@ -21387,6 +21465,15 @@ msgstr "Aquesta acció no es pot desfer. Continuar?"
|
||||
msgid "Skipping objects."
|
||||
msgstr "Ometent objectes."
|
||||
|
||||
msgid "Material Ratio"
|
||||
msgstr ""
|
||||
|
||||
msgid "Model Height"
|
||||
msgstr ""
|
||||
|
||||
msgid "Ratio"
|
||||
msgstr ""
|
||||
|
||||
msgid "Select Filament"
|
||||
msgstr "Seleccioneu filament"
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Orca Slicer\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-09-02 09:39-0300\n"
|
||||
"POT-Creation-Date: 2026-09-03 10:27+0800\n"
|
||||
"PO-Revision-Date: \n"
|
||||
"Last-Translator: Jakub Hencl\n"
|
||||
"Language-Team: \n"
|
||||
@@ -4953,6 +4953,10 @@ msgstr ""
|
||||
"Ano – změnit tato nastavení a automaticky povolit spirálový režim\n"
|
||||
"Ne – tentokrát nepoužít spirálový režim"
|
||||
|
||||
# AI Translated
|
||||
msgid "N/A"
|
||||
msgstr "N/A"
|
||||
|
||||
msgid "Printing"
|
||||
msgstr "Tisk"
|
||||
|
||||
@@ -5412,10 +5416,6 @@ msgstr "Neplatný vzor. Použijte N, N#K nebo seznam oddělený čárkami s voli
|
||||
msgid "Invalid format. Expected vector format: \"%1%\""
|
||||
msgstr "Neplatný formát. Očekávaný vektorový formát: \"%1%\""
|
||||
|
||||
# AI Translated
|
||||
msgid "N/A"
|
||||
msgstr "N/A"
|
||||
|
||||
# AI Translated
|
||||
msgid "System agents"
|
||||
msgstr "Systémoví agenti"
|
||||
@@ -6382,6 +6382,12 @@ msgstr "Uložit projekt jako"
|
||||
msgid "Save current project as"
|
||||
msgstr "Uložit aktuální projekt jako"
|
||||
|
||||
msgid "Publish 3MF"
|
||||
msgstr ""
|
||||
|
||||
msgid "Export a 3MF file with the selected settings embedded"
|
||||
msgstr ""
|
||||
|
||||
msgid "Import 3MF/STL/STEP/SVG/OBJ/AMF"
|
||||
msgstr "Importovat 3MF/STL/STEP/SVG/OBJ/AMF"
|
||||
|
||||
@@ -8237,6 +8243,12 @@ msgstr "Potvrďte prosím, že je G-code v těchto předvolbách bezpečný, aby
|
||||
msgid "Customized Preset"
|
||||
msgstr "Přizpůsobená předvolba"
|
||||
|
||||
msgid "Some published settings could not be applied:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Some filament slots were changed:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Component name(s) inside step file not in UTF-8 format!"
|
||||
msgstr "Názvy komponent v souboru STEP nejsou ve formátu UTF-8!"
|
||||
|
||||
@@ -8647,6 +8659,17 @@ msgstr "Uložit rozřezaný soubor jako:"
|
||||
msgid "The file %s has been sent to the printer's storage space and can be viewed on the printer."
|
||||
msgstr "Soubor %s byl odeslán do úložiště tiskárny a lze jej zobrazit na tiskárně."
|
||||
|
||||
msgid "Publish 3MF file as:"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"Failed to export the published 3MF file.\n"
|
||||
"Please check whether the folder exists online or if other programs have the file open."
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish"
|
||||
msgstr "Publikovat"
|
||||
|
||||
msgid "The nozzle type is not set. Please set the nozzle and try again."
|
||||
msgstr "Typ trysky není nastaven. Nastavte prosím trysku a zkuste to znovu."
|
||||
|
||||
@@ -9828,9 +9851,6 @@ msgstr "Přejít na webovou stránku publikace modelu"
|
||||
msgid "Note: The preparation may take several minutes. Please be patient."
|
||||
msgstr "Poznámka: Příprava může trvat několik minut. Buďte prosím trpěliví."
|
||||
|
||||
msgid "Publish"
|
||||
msgstr "Publikovat"
|
||||
|
||||
msgid "Publish was canceled"
|
||||
msgstr "Publikování bylo zrušeno"
|
||||
|
||||
@@ -9846,6 +9866,64 @@ msgstr "Nahrávání dat"
|
||||
msgid "Jump to webpage"
|
||||
msgstr "Přejít na webovou stránku"
|
||||
|
||||
msgid "Material"
|
||||
msgstr ""
|
||||
|
||||
msgid "Mixed filament"
|
||||
msgstr ""
|
||||
|
||||
msgid "Some mixed filaments rely on filaments that will not be published:"
|
||||
msgstr ""
|
||||
|
||||
#, c-format, boost-format
|
||||
msgid "Filament %d (mixed)"
|
||||
msgstr ""
|
||||
|
||||
msgid "needs"
|
||||
msgstr ""
|
||||
|
||||
msgid "not enabled"
|
||||
msgstr ""
|
||||
|
||||
msgid "material not published"
|
||||
msgstr ""
|
||||
|
||||
msgid "To publish a mixed filament, enable every filament it uses and choose Full Publish or check its Type requirement."
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish anyway"
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish 3MF..."
|
||||
msgstr ""
|
||||
|
||||
msgid "Select which settings to be published in the 3MF file"
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish 3MF Wiki"
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish 3MF Video Guide"
|
||||
msgstr ""
|
||||
|
||||
msgid "Mixed filament - published as a whole when \"Enable\" above is selected"
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish this mixed filament and enable + Full Publish its component filaments"
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish this filament slot in the 3MF file"
|
||||
msgstr ""
|
||||
|
||||
msgid "Full Publish"
|
||||
msgstr ""
|
||||
|
||||
msgid "Embed the entire filament of this slot in the 3MF file"
|
||||
msgstr ""
|
||||
|
||||
msgid "Filter non-selected"
|
||||
msgstr ""
|
||||
|
||||
#, c-format, boost-format
|
||||
msgid "Save %s as"
|
||||
msgstr "Uložit %s jako"
|
||||
@@ -21331,6 +21409,15 @@ msgstr "Tuto akci nelze vrátit zpět. Pokračovat?"
|
||||
msgid "Skipping objects."
|
||||
msgstr "Přeskakování objektů."
|
||||
|
||||
msgid "Material Ratio"
|
||||
msgstr ""
|
||||
|
||||
msgid "Model Height"
|
||||
msgstr ""
|
||||
|
||||
msgid "Ratio"
|
||||
msgstr ""
|
||||
|
||||
# AI Translated
|
||||
msgid "Select Filament"
|
||||
msgstr "Vybrat filament"
|
||||
|
||||
@@ -3,7 +3,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Orca Slicer\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-09-02 09:39-0300\n"
|
||||
"POT-Creation-Date: 2026-09-03 10:27+0800\n"
|
||||
"PO-Revision-Date: \n"
|
||||
"Last-Translator: Heiko Liebscher <hliebschergmail.com>\n"
|
||||
"Language-Team: \n"
|
||||
@@ -4859,6 +4859,9 @@ msgstr ""
|
||||
"Ja - Diese Einstellungen ändern und den Spiralmodus automatisch aktivieren\n"
|
||||
"Nein - Spiralmodus nicht aktivieren"
|
||||
|
||||
msgid "N/A"
|
||||
msgstr "Nicht verfügbar"
|
||||
|
||||
msgid "Printing"
|
||||
msgstr "Drucken"
|
||||
|
||||
@@ -5318,9 +5321,6 @@ msgstr "Ungültiges Muster. Verwenden Sie N, N#K oder eine durch Kommas getrennt
|
||||
msgid "Invalid format. Expected vector format: \"%1%\""
|
||||
msgstr "Ungültiges Format. Erwartetes Vektorformat: \"%1%\""
|
||||
|
||||
msgid "N/A"
|
||||
msgstr "Nicht verfügbar"
|
||||
|
||||
# AI Translated
|
||||
msgid "System agents"
|
||||
msgstr "Systemagenten"
|
||||
@@ -6274,6 +6274,12 @@ msgstr "Projekt speichern als"
|
||||
msgid "Save current project as"
|
||||
msgstr "Aktuelles Projekt speichern als"
|
||||
|
||||
msgid "Publish 3MF"
|
||||
msgstr ""
|
||||
|
||||
msgid "Export a 3MF file with the selected settings embedded"
|
||||
msgstr ""
|
||||
|
||||
msgid "Import 3MF/STL/STEP/SVG/OBJ/AMF"
|
||||
msgstr "Importiere 3MF/STL/STEP/SVG/OBJ/AMF"
|
||||
|
||||
@@ -8107,6 +8113,12 @@ msgstr "Bitte bestätigen Sie, dass die G-Codes innerhalb dieser Profile sicher
|
||||
msgid "Customized Preset"
|
||||
msgstr "Benutzerdefinierte Profile"
|
||||
|
||||
msgid "Some published settings could not be applied:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Some filament slots were changed:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Component name(s) inside step file not in UTF-8 format!"
|
||||
msgstr "Der Name der Komponenten in der Step-Datei ist nicht im UTF-8-Format!"
|
||||
|
||||
@@ -8519,6 +8531,17 @@ msgstr "Geslicte Datei speichern unter:"
|
||||
msgid "The file %s has been sent to the printer's storage space and can be viewed on the printer."
|
||||
msgstr "Die Datei %s wurde an den Speicher des Druckers gesendet und kann auf dem Drucker angezeigt werden."
|
||||
|
||||
msgid "Publish 3MF file as:"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"Failed to export the published 3MF file.\n"
|
||||
"Please check whether the folder exists online or if other programs have the file open."
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish"
|
||||
msgstr "Veröffentlichen"
|
||||
|
||||
msgid "The nozzle type is not set. Please set the nozzle and try again."
|
||||
msgstr "Die Düsenart ist nicht eingestellt. Bitte stellen Sie die Düse ein und versuchen Sie es erneut."
|
||||
|
||||
@@ -9662,9 +9685,6 @@ msgstr "Zur Modellveröffentlichungs-Webseite springen"
|
||||
msgid "Note: The preparation may take several minutes. Please be patient."
|
||||
msgstr "Hinweis: Die Vorbereitung kann einige Minuten dauern. Bitte haben Sie Geduld."
|
||||
|
||||
msgid "Publish"
|
||||
msgstr "Veröffentlichen"
|
||||
|
||||
msgid "Publish was canceled"
|
||||
msgstr "Veröffentlichung wurde abgebrochen"
|
||||
|
||||
@@ -9680,6 +9700,64 @@ msgstr "Daten werden hochgeladen"
|
||||
msgid "Jump to webpage"
|
||||
msgstr "Zu einer Website springen"
|
||||
|
||||
msgid "Material"
|
||||
msgstr ""
|
||||
|
||||
msgid "Mixed filament"
|
||||
msgstr ""
|
||||
|
||||
msgid "Some mixed filaments rely on filaments that will not be published:"
|
||||
msgstr ""
|
||||
|
||||
#, c-format, boost-format
|
||||
msgid "Filament %d (mixed)"
|
||||
msgstr ""
|
||||
|
||||
msgid "needs"
|
||||
msgstr ""
|
||||
|
||||
msgid "not enabled"
|
||||
msgstr ""
|
||||
|
||||
msgid "material not published"
|
||||
msgstr ""
|
||||
|
||||
msgid "To publish a mixed filament, enable every filament it uses and choose Full Publish or check its Type requirement."
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish anyway"
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish 3MF..."
|
||||
msgstr ""
|
||||
|
||||
msgid "Select which settings to be published in the 3MF file"
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish 3MF Wiki"
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish 3MF Video Guide"
|
||||
msgstr ""
|
||||
|
||||
msgid "Mixed filament - published as a whole when \"Enable\" above is selected"
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish this mixed filament and enable + Full Publish its component filaments"
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish this filament slot in the 3MF file"
|
||||
msgstr ""
|
||||
|
||||
msgid "Full Publish"
|
||||
msgstr ""
|
||||
|
||||
msgid "Embed the entire filament of this slot in the 3MF file"
|
||||
msgstr ""
|
||||
|
||||
msgid "Filter non-selected"
|
||||
msgstr ""
|
||||
|
||||
#, c-format, boost-format
|
||||
msgid "Save %s as"
|
||||
msgstr "%s speichern als"
|
||||
@@ -20816,6 +20894,15 @@ msgstr "Diese Aktion kann nicht rückgängig gemacht werden. Fortsetzen?"
|
||||
msgid "Skipping objects."
|
||||
msgstr "Objekte werden übersprungen."
|
||||
|
||||
msgid "Material Ratio"
|
||||
msgstr ""
|
||||
|
||||
msgid "Model Height"
|
||||
msgstr ""
|
||||
|
||||
msgid "Ratio"
|
||||
msgstr ""
|
||||
|
||||
msgid "Select Filament"
|
||||
msgstr "Filament auswählen"
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Orca Slicer\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-09-02 09:39-0300\n"
|
||||
"POT-Creation-Date: 2026-09-03 10:27+0800\n"
|
||||
"PO-Revision-Date: 2026-06-17 15:44-0300\n"
|
||||
"Last-Translator: Alexandre Folle de Menezes\n"
|
||||
"Language-Team: \n"
|
||||
@@ -4567,6 +4567,9 @@ msgid ""
|
||||
"No - Cancel enabling spiral mode"
|
||||
msgstr ""
|
||||
|
||||
msgid "N/A"
|
||||
msgstr ""
|
||||
|
||||
msgid "Printing"
|
||||
msgstr ""
|
||||
|
||||
@@ -5015,9 +5018,6 @@ msgstr ""
|
||||
msgid "Invalid format. Expected vector format: \"%1%\""
|
||||
msgstr ""
|
||||
|
||||
msgid "N/A"
|
||||
msgstr ""
|
||||
|
||||
msgid "System agents"
|
||||
msgstr ""
|
||||
|
||||
@@ -5947,6 +5947,12 @@ msgstr ""
|
||||
msgid "Save current project as"
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish 3MF"
|
||||
msgstr ""
|
||||
|
||||
msgid "Export a 3MF file with the selected settings embedded"
|
||||
msgstr ""
|
||||
|
||||
msgid "Import 3MF/STL/STEP/SVG/OBJ/AMF"
|
||||
msgstr ""
|
||||
|
||||
@@ -7692,6 +7698,12 @@ msgstr ""
|
||||
msgid "Customized Preset"
|
||||
msgstr ""
|
||||
|
||||
msgid "Some published settings could not be applied:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Some filament slots were changed:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Component name(s) inside step file not in UTF-8 format!"
|
||||
msgstr ""
|
||||
|
||||
@@ -8070,6 +8082,17 @@ msgstr ""
|
||||
msgid "The file %s has been sent to the printer's storage space and can be viewed on the printer."
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish 3MF file as:"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"Failed to export the published 3MF file.\n"
|
||||
"Please check whether the folder exists online or if other programs have the file open."
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish"
|
||||
msgstr ""
|
||||
|
||||
msgid "The nozzle type is not set. Please set the nozzle and try again."
|
||||
msgstr ""
|
||||
|
||||
@@ -9120,9 +9143,6 @@ msgstr ""
|
||||
msgid "Note: The preparation may take several minutes. Please be patient."
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish"
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish was canceled"
|
||||
msgstr ""
|
||||
|
||||
@@ -9138,6 +9158,64 @@ msgstr ""
|
||||
msgid "Jump to webpage"
|
||||
msgstr ""
|
||||
|
||||
msgid "Material"
|
||||
msgstr ""
|
||||
|
||||
msgid "Mixed filament"
|
||||
msgstr ""
|
||||
|
||||
msgid "Some mixed filaments rely on filaments that will not be published:"
|
||||
msgstr ""
|
||||
|
||||
#, c-format, boost-format
|
||||
msgid "Filament %d (mixed)"
|
||||
msgstr ""
|
||||
|
||||
msgid "needs"
|
||||
msgstr ""
|
||||
|
||||
msgid "not enabled"
|
||||
msgstr ""
|
||||
|
||||
msgid "material not published"
|
||||
msgstr ""
|
||||
|
||||
msgid "To publish a mixed filament, enable every filament it uses and choose Full Publish or check its Type requirement."
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish anyway"
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish 3MF..."
|
||||
msgstr ""
|
||||
|
||||
msgid "Select which settings to be published in the 3MF file"
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish 3MF Wiki"
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish 3MF Video Guide"
|
||||
msgstr ""
|
||||
|
||||
msgid "Mixed filament - published as a whole when \"Enable\" above is selected"
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish this mixed filament and enable + Full Publish its component filaments"
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish this filament slot in the 3MF file"
|
||||
msgstr ""
|
||||
|
||||
msgid "Full Publish"
|
||||
msgstr ""
|
||||
|
||||
msgid "Embed the entire filament of this slot in the 3MF file"
|
||||
msgstr ""
|
||||
|
||||
msgid "Filter non-selected"
|
||||
msgstr ""
|
||||
|
||||
#, c-format, boost-format
|
||||
msgid "Save %s as"
|
||||
msgstr ""
|
||||
@@ -19333,6 +19411,15 @@ msgstr ""
|
||||
msgid "Skipping objects."
|
||||
msgstr ""
|
||||
|
||||
msgid "Material Ratio"
|
||||
msgstr ""
|
||||
|
||||
msgid "Model Height"
|
||||
msgstr ""
|
||||
|
||||
msgid "Ratio"
|
||||
msgstr ""
|
||||
|
||||
msgid "Select Filament"
|
||||
msgstr ""
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Orca Slicer\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-09-02 09:39-0300\n"
|
||||
"POT-Creation-Date: 2026-09-03 10:27+0800\n"
|
||||
"PO-Revision-Date: \n"
|
||||
"Last-Translator: Ian A. Bassi <>\n"
|
||||
"Language-Team: \n"
|
||||
@@ -4729,6 +4729,9 @@ msgstr ""
|
||||
"Sí - Cambiar estos ajustes y activar el modo espiral automáticamente\n"
|
||||
"No - Dejar de usar el modo espiral esta vez"
|
||||
|
||||
msgid "N/A"
|
||||
msgstr "N/A"
|
||||
|
||||
msgid "Printing"
|
||||
msgstr "Imprimiendo"
|
||||
|
||||
@@ -5182,9 +5185,6 @@ msgstr "Patrón inválido. Use N, N#K, o una lista separada por comas con #K opc
|
||||
msgid "Invalid format. Expected vector format: \"%1%\""
|
||||
msgstr "Formato inválido. Formato de vector esperado: \"%1%\""
|
||||
|
||||
msgid "N/A"
|
||||
msgstr "N/A"
|
||||
|
||||
msgid "System agents"
|
||||
msgstr "Agentes del sistema"
|
||||
|
||||
@@ -6129,6 +6129,12 @@ msgstr "Guardar proyecto como"
|
||||
msgid "Save current project as"
|
||||
msgstr "Guardar el proyecto actual como"
|
||||
|
||||
msgid "Publish 3MF"
|
||||
msgstr ""
|
||||
|
||||
msgid "Export a 3MF file with the selected settings embedded"
|
||||
msgstr ""
|
||||
|
||||
msgid "Import 3MF/STL/STEP/SVG/OBJ/AMF"
|
||||
msgstr "Importar 3MF/STL/STEP/SVG/OBJ/AMF"
|
||||
|
||||
@@ -7910,6 +7916,12 @@ msgstr "¡Por favor, confirme que el G-Code dentro de los perfiles son seguros p
|
||||
msgid "Customized Preset"
|
||||
msgstr "Perfil Personalizado"
|
||||
|
||||
msgid "Some published settings could not be applied:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Some filament slots were changed:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Component name(s) inside step file not in UTF-8 format!"
|
||||
msgstr "¡El nombre de los componentes dentro del archivo de pasos no tiene formato UTF-8!"
|
||||
|
||||
@@ -8305,6 +8317,17 @@ msgstr "Guardar el archivo laminado como:"
|
||||
msgid "The file %s has been sent to the printer's storage space and can be viewed on the printer."
|
||||
msgstr "El archivo %s ha sido mandado al almacenamiento de la impresora y puede ser visualizado en la impresora."
|
||||
|
||||
msgid "Publish 3MF file as:"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"Failed to export the published 3MF file.\n"
|
||||
"Please check whether the folder exists online or if other programs have the file open."
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish"
|
||||
msgstr "Publicar"
|
||||
|
||||
msgid "The nozzle type is not set. Please set the nozzle and try again."
|
||||
msgstr "El tipo de boquilla no está establecido. Configure la boquilla e inténtelo de nuevo."
|
||||
|
||||
@@ -9419,9 +9442,6 @@ msgstr "Ir a la página web de publicación de modelos"
|
||||
msgid "Note: The preparation may take several minutes. Please be patient."
|
||||
msgstr "Nota: La preparación puede llevar varios minutos. Por favor, sea paciente."
|
||||
|
||||
msgid "Publish"
|
||||
msgstr "Publicar"
|
||||
|
||||
msgid "Publish was canceled"
|
||||
msgstr "La publicación fue cancelada"
|
||||
|
||||
@@ -9437,6 +9457,64 @@ msgstr "Cargando datos"
|
||||
msgid "Jump to webpage"
|
||||
msgstr "Ir a la página web"
|
||||
|
||||
msgid "Material"
|
||||
msgstr ""
|
||||
|
||||
msgid "Mixed filament"
|
||||
msgstr ""
|
||||
|
||||
msgid "Some mixed filaments rely on filaments that will not be published:"
|
||||
msgstr ""
|
||||
|
||||
#, c-format, boost-format
|
||||
msgid "Filament %d (mixed)"
|
||||
msgstr ""
|
||||
|
||||
msgid "needs"
|
||||
msgstr ""
|
||||
|
||||
msgid "not enabled"
|
||||
msgstr ""
|
||||
|
||||
msgid "material not published"
|
||||
msgstr ""
|
||||
|
||||
msgid "To publish a mixed filament, enable every filament it uses and choose Full Publish or check its Type requirement."
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish anyway"
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish 3MF..."
|
||||
msgstr ""
|
||||
|
||||
msgid "Select which settings to be published in the 3MF file"
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish 3MF Wiki"
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish 3MF Video Guide"
|
||||
msgstr ""
|
||||
|
||||
msgid "Mixed filament - published as a whole when \"Enable\" above is selected"
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish this mixed filament and enable + Full Publish its component filaments"
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish this filament slot in the 3MF file"
|
||||
msgstr ""
|
||||
|
||||
msgid "Full Publish"
|
||||
msgstr ""
|
||||
|
||||
msgid "Embed the entire filament of this slot in the 3MF file"
|
||||
msgstr ""
|
||||
|
||||
msgid "Filter non-selected"
|
||||
msgstr ""
|
||||
|
||||
#, c-format, boost-format
|
||||
msgid "Save %s as"
|
||||
msgstr "Guardar %s como"
|
||||
@@ -20409,6 +20487,15 @@ msgstr "Esta acción no se puede deshacer. ¿Continuar?"
|
||||
msgid "Skipping objects."
|
||||
msgstr "Omitiendo objetos."
|
||||
|
||||
msgid "Material Ratio"
|
||||
msgstr ""
|
||||
|
||||
msgid "Model Height"
|
||||
msgstr ""
|
||||
|
||||
msgid "Ratio"
|
||||
msgstr ""
|
||||
|
||||
msgid "Select Filament"
|
||||
msgstr "Seleccionar Filamento"
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: \n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-09-02 09:39-0300\n"
|
||||
"POT-Creation-Date: 2026-09-03 10:27+0800\n"
|
||||
"PO-Revision-Date: 2026-07-20 13:33+0200\n"
|
||||
"Last-Translator: Manu Goiogana <mgoiogana@gmail.com>\n"
|
||||
"Language-Team: \n"
|
||||
@@ -4771,6 +4771,9 @@ msgstr ""
|
||||
"Bai - Aldatu ezarpen hauek eta gaitu espiral/loreontzi modua automatikoki\n"
|
||||
"Ez - Utzi bertan behera espiral modua gaitzea"
|
||||
|
||||
msgid "N/A"
|
||||
msgstr "N/A"
|
||||
|
||||
msgid "Printing"
|
||||
msgstr "Inprimatzen"
|
||||
|
||||
@@ -5231,9 +5234,6 @@ msgstr "Patroi baliogabea. Erabili N, N#K edo komaz bereizitako zerrenda bat, sa
|
||||
msgid "Invalid format. Expected vector format: \"%1%\""
|
||||
msgstr "Formatuak ez du balio. Espero den formatu bektoriala: \"%1%\""
|
||||
|
||||
msgid "N/A"
|
||||
msgstr "N/A"
|
||||
|
||||
msgid "System agents"
|
||||
msgstr "Sistema-agenteak"
|
||||
|
||||
@@ -6175,6 +6175,12 @@ msgstr "Gorde proiektua honela"
|
||||
msgid "Save current project as"
|
||||
msgstr "Gorde uneko proiektua honela"
|
||||
|
||||
msgid "Publish 3MF"
|
||||
msgstr ""
|
||||
|
||||
msgid "Export a 3MF file with the selected settings embedded"
|
||||
msgstr ""
|
||||
|
||||
msgid "Import 3MF/STL/STEP/SVG/OBJ/AMF"
|
||||
msgstr "Inportatu 3MF/STL/STEP/SVG/OBJ/AMF"
|
||||
|
||||
@@ -7988,6 +7994,12 @@ msgstr "Berretsi aurrezarpen hauetako G-codea segurua dela, makinari kalterik ez
|
||||
msgid "Customized Preset"
|
||||
msgstr "Aurrezarpen pertsonalizatua"
|
||||
|
||||
msgid "Some published settings could not be applied:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Some filament slots were changed:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Component name(s) inside step file not in UTF-8 format!"
|
||||
msgstr "STEP fitxategiko osagai-izena(k) ez dago/daude UTF-8 formatuan!"
|
||||
|
||||
@@ -8388,6 +8400,17 @@ msgstr "Gorde xerratutako fitxategia honela:"
|
||||
msgid "The file %s has been sent to the printer's storage space and can be viewed on the printer."
|
||||
msgstr "%s fitxategia inprimagailuaren biltegiratze-eremura bidali da eta inprimagailuan ikus daiteke."
|
||||
|
||||
msgid "Publish 3MF file as:"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"Failed to export the published 3MF file.\n"
|
||||
"Please check whether the folder exists online or if other programs have the file open."
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish"
|
||||
msgstr "Argitaratu"
|
||||
|
||||
msgid "The nozzle type is not set. Please set the nozzle and try again."
|
||||
msgstr "Pita mota ez dago ezarrita. Ezarri pita eta saiatu berriro."
|
||||
|
||||
@@ -9505,9 +9528,6 @@ msgstr "Joan modeloa argitaratzeko web-orrira"
|
||||
msgid "Note: The preparation may take several minutes. Please be patient."
|
||||
msgstr "Oharra: prestaketak minutu batzuk iraun ditzake. Izan pazientzia."
|
||||
|
||||
msgid "Publish"
|
||||
msgstr "Argitaratu"
|
||||
|
||||
msgid "Publish was canceled"
|
||||
msgstr "Argitaratzea bertan behera utzi da"
|
||||
|
||||
@@ -9523,6 +9543,64 @@ msgstr "Datuak igotzen"
|
||||
msgid "Jump to webpage"
|
||||
msgstr "Joan web-orrira"
|
||||
|
||||
msgid "Material"
|
||||
msgstr ""
|
||||
|
||||
msgid "Mixed filament"
|
||||
msgstr ""
|
||||
|
||||
msgid "Some mixed filaments rely on filaments that will not be published:"
|
||||
msgstr ""
|
||||
|
||||
#, c-format, boost-format
|
||||
msgid "Filament %d (mixed)"
|
||||
msgstr ""
|
||||
|
||||
msgid "needs"
|
||||
msgstr ""
|
||||
|
||||
msgid "not enabled"
|
||||
msgstr ""
|
||||
|
||||
msgid "material not published"
|
||||
msgstr ""
|
||||
|
||||
msgid "To publish a mixed filament, enable every filament it uses and choose Full Publish or check its Type requirement."
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish anyway"
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish 3MF..."
|
||||
msgstr ""
|
||||
|
||||
msgid "Select which settings to be published in the 3MF file"
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish 3MF Wiki"
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish 3MF Video Guide"
|
||||
msgstr ""
|
||||
|
||||
msgid "Mixed filament - published as a whole when \"Enable\" above is selected"
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish this mixed filament and enable + Full Publish its component filaments"
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish this filament slot in the 3MF file"
|
||||
msgstr ""
|
||||
|
||||
msgid "Full Publish"
|
||||
msgstr ""
|
||||
|
||||
msgid "Embed the entire filament of this slot in the 3MF file"
|
||||
msgstr ""
|
||||
|
||||
msgid "Filter non-selected"
|
||||
msgstr ""
|
||||
|
||||
#, c-format, boost-format
|
||||
msgid "Save %s as"
|
||||
msgstr "Gorde %s honela"
|
||||
@@ -20596,6 +20674,15 @@ msgstr "Ekintza hau ezin da desegin. Jarraitu?"
|
||||
msgid "Skipping objects."
|
||||
msgstr "Objektuak saltatzen."
|
||||
|
||||
msgid "Material Ratio"
|
||||
msgstr ""
|
||||
|
||||
msgid "Model Height"
|
||||
msgstr ""
|
||||
|
||||
msgid "Ratio"
|
||||
msgstr ""
|
||||
|
||||
msgid "Select Filament"
|
||||
msgstr "Hautatu filamentua"
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Orca Slicer\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-09-02 09:39-0300\n"
|
||||
"POT-Creation-Date: 2026-09-03 10:27+0800\n"
|
||||
"PO-Revision-Date: \n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Guislain Cyril, Thomas Lété\n"
|
||||
@@ -4809,6 +4809,9 @@ msgstr ""
|
||||
"Oui - Modifiez ces paramètres et activez automatiquement le mode spirale/vase\n"
|
||||
"Non - Annuler l'activation du mode spirale"
|
||||
|
||||
msgid "N/A"
|
||||
msgstr "N/A"
|
||||
|
||||
msgid "Printing"
|
||||
msgstr "Impression"
|
||||
|
||||
@@ -5268,9 +5271,6 @@ msgstr "Motif invalide. Utilisez N, N#K, ou une liste séparée par des virgules
|
||||
msgid "Invalid format. Expected vector format: \"%1%\""
|
||||
msgstr "Format invalide. Format vectoriel attendu : \"%1%\""
|
||||
|
||||
msgid "N/A"
|
||||
msgstr "N/A"
|
||||
|
||||
# AI Translated
|
||||
msgid "System agents"
|
||||
msgstr "Agents système"
|
||||
@@ -6223,6 +6223,12 @@ msgstr "Enregistrer le projet sous"
|
||||
msgid "Save current project as"
|
||||
msgstr "Enregistrer le projet actuel sous"
|
||||
|
||||
msgid "Publish 3MF"
|
||||
msgstr ""
|
||||
|
||||
msgid "Export a 3MF file with the selected settings embedded"
|
||||
msgstr ""
|
||||
|
||||
msgid "Import 3MF/STL/STEP/SVG/OBJ/AMF"
|
||||
msgstr "Importer des fichiers 3MF/STL/STEP/SVG/OBJ/AMF"
|
||||
|
||||
@@ -8044,6 +8050,12 @@ msgstr "Veuillez vous assurer que les G-codes de ces préréglages sont sûrs af
|
||||
msgid "Customized Preset"
|
||||
msgstr "Préréglage personnalisé"
|
||||
|
||||
msgid "Some published settings could not be applied:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Some filament slots were changed:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Component name(s) inside step file not in UTF-8 format!"
|
||||
msgstr "Le nom des composants dans le fichier STEP n'est pas au format UTF-8 !"
|
||||
|
||||
@@ -8445,6 +8457,17 @@ msgstr "Enregistrer le fichier découpé sous :"
|
||||
msgid "The file %s has been sent to the printer's storage space and can be viewed on the printer."
|
||||
msgstr "Le fichier %s a été envoyé vers l'espace de stockage de l'imprimante et peut être visualisé sur l'imprimante."
|
||||
|
||||
msgid "Publish 3MF file as:"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"Failed to export the published 3MF file.\n"
|
||||
"Please check whether the folder exists online or if other programs have the file open."
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish"
|
||||
msgstr "Publier"
|
||||
|
||||
msgid "The nozzle type is not set. Please set the nozzle and try again."
|
||||
msgstr "Le type de buse n'est pas défini. Veuillez définir la buse et réessayer."
|
||||
|
||||
@@ -9576,9 +9599,6 @@ msgstr "Accéder à la page internet de publication des modèles"
|
||||
msgid "Note: The preparation may take several minutes. Please be patient."
|
||||
msgstr "Remarque : La préparation peut prendre plusieurs minutes. Veuillez patienter."
|
||||
|
||||
msgid "Publish"
|
||||
msgstr "Publier"
|
||||
|
||||
msgid "Publish was canceled"
|
||||
msgstr "La publication a été annulée"
|
||||
|
||||
@@ -9594,6 +9614,64 @@ msgstr "Téléversement des données"
|
||||
msgid "Jump to webpage"
|
||||
msgstr "Ouvrir la page internet"
|
||||
|
||||
msgid "Material"
|
||||
msgstr ""
|
||||
|
||||
msgid "Mixed filament"
|
||||
msgstr ""
|
||||
|
||||
msgid "Some mixed filaments rely on filaments that will not be published:"
|
||||
msgstr ""
|
||||
|
||||
#, c-format, boost-format
|
||||
msgid "Filament %d (mixed)"
|
||||
msgstr ""
|
||||
|
||||
msgid "needs"
|
||||
msgstr ""
|
||||
|
||||
msgid "not enabled"
|
||||
msgstr ""
|
||||
|
||||
msgid "material not published"
|
||||
msgstr ""
|
||||
|
||||
msgid "To publish a mixed filament, enable every filament it uses and choose Full Publish or check its Type requirement."
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish anyway"
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish 3MF..."
|
||||
msgstr ""
|
||||
|
||||
msgid "Select which settings to be published in the 3MF file"
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish 3MF Wiki"
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish 3MF Video Guide"
|
||||
msgstr ""
|
||||
|
||||
msgid "Mixed filament - published as a whole when \"Enable\" above is selected"
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish this mixed filament and enable + Full Publish its component filaments"
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish this filament slot in the 3MF file"
|
||||
msgstr ""
|
||||
|
||||
msgid "Full Publish"
|
||||
msgstr ""
|
||||
|
||||
msgid "Embed the entire filament of this slot in the 3MF file"
|
||||
msgstr ""
|
||||
|
||||
msgid "Filter non-selected"
|
||||
msgstr ""
|
||||
|
||||
#, c-format, boost-format
|
||||
msgid "Save %s as"
|
||||
msgstr "Enregistrer %s sous"
|
||||
@@ -20708,6 +20786,15 @@ msgstr "Cette action ne peut pas être annulée. Continuer ?"
|
||||
msgid "Skipping objects."
|
||||
msgstr "Ignorer des objets."
|
||||
|
||||
msgid "Material Ratio"
|
||||
msgstr ""
|
||||
|
||||
msgid "Model Height"
|
||||
msgstr ""
|
||||
|
||||
msgid "Ratio"
|
||||
msgstr ""
|
||||
|
||||
msgid "Select Filament"
|
||||
msgstr "Sélectionner le filament"
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Orca Slicer\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-09-02 09:39-0300\n"
|
||||
"POT-Creation-Date: 2026-09-03 10:27+0800\n"
|
||||
"Language: hu\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
@@ -4905,6 +4905,10 @@ msgstr ""
|
||||
"Igen - Módosítsd ezeket a beállításokat, és automatikusan kapcsold be a spirál módot\n"
|
||||
"Nem - Most ne kapcsold be a spirál módot"
|
||||
|
||||
# AI Translated
|
||||
msgid "N/A"
|
||||
msgstr "N/A"
|
||||
|
||||
msgid "Printing"
|
||||
msgstr "Nyomtatás"
|
||||
|
||||
@@ -5364,10 +5368,6 @@ msgstr "Érvénytelen minta. Használj N, N#K formátumot, vagy vesszővel elvá
|
||||
msgid "Invalid format. Expected vector format: \"%1%\""
|
||||
msgstr "Érvénytelen formátum. Elvárt vektor formátum: \"%1%\""
|
||||
|
||||
# AI Translated
|
||||
msgid "N/A"
|
||||
msgstr "N/A"
|
||||
|
||||
# AI Translated
|
||||
msgid "System agents"
|
||||
msgstr "Rendszerügynökök"
|
||||
@@ -6325,6 +6325,12 @@ msgstr "Projekt mentése másként"
|
||||
msgid "Save current project as"
|
||||
msgstr "Jelenlegi projekt mentése másként"
|
||||
|
||||
msgid "Publish 3MF"
|
||||
msgstr ""
|
||||
|
||||
msgid "Export a 3MF file with the selected settings embedded"
|
||||
msgstr ""
|
||||
|
||||
msgid "Import 3MF/STL/STEP/SVG/OBJ/AMF"
|
||||
msgstr "Importálás 3MF/STL/STEP/SVG/OBJ/AMF"
|
||||
|
||||
@@ -8159,6 +8165,12 @@ msgstr "Kérlek, győződj meg arról, hogy a beállításokban található G-k
|
||||
msgid "Customized Preset"
|
||||
msgstr "Egyedi beállítás"
|
||||
|
||||
msgid "Some published settings could not be applied:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Some filament slots were changed:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Component name(s) inside step file not in UTF-8 format!"
|
||||
msgstr "A STEP fájlon belüli komponens neve nem UTF-8 formátumban van!"
|
||||
|
||||
@@ -8571,6 +8583,17 @@ msgstr "Szeletelt fájl mentése mint:"
|
||||
msgid "The file %s has been sent to the printer's storage space and can be viewed on the printer."
|
||||
msgstr "A(z) %s fájlt elküldtük a nyomtató tárhelyére. A fájl a nyomtatón tekinthető meg."
|
||||
|
||||
msgid "Publish 3MF file as:"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"Failed to export the published 3MF file.\n"
|
||||
"Please check whether the folder exists online or if other programs have the file open."
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish"
|
||||
msgstr "Közzététel"
|
||||
|
||||
msgid "The nozzle type is not set. Please set the nozzle and try again."
|
||||
msgstr "A fúvókatípus nincs beállítva. Állítsd be a fúvókát, majd próbáld újra."
|
||||
|
||||
@@ -9736,9 +9759,6 @@ msgstr "Ugrás a modell közzététele weboldalra"
|
||||
msgid "Note: The preparation may take several minutes. Please be patient."
|
||||
msgstr "Megjegyzés: Az előkészítés több percig is eltarthat. Kérlek, várj."
|
||||
|
||||
msgid "Publish"
|
||||
msgstr "Közzététel"
|
||||
|
||||
msgid "Publish was canceled"
|
||||
msgstr "A közzététel törlésre került"
|
||||
|
||||
@@ -9754,6 +9774,64 @@ msgstr "Adatok feltöltése"
|
||||
msgid "Jump to webpage"
|
||||
msgstr "Ugrás a weboldalra"
|
||||
|
||||
msgid "Material"
|
||||
msgstr ""
|
||||
|
||||
msgid "Mixed filament"
|
||||
msgstr ""
|
||||
|
||||
msgid "Some mixed filaments rely on filaments that will not be published:"
|
||||
msgstr ""
|
||||
|
||||
#, c-format, boost-format
|
||||
msgid "Filament %d (mixed)"
|
||||
msgstr ""
|
||||
|
||||
msgid "needs"
|
||||
msgstr ""
|
||||
|
||||
msgid "not enabled"
|
||||
msgstr ""
|
||||
|
||||
msgid "material not published"
|
||||
msgstr ""
|
||||
|
||||
msgid "To publish a mixed filament, enable every filament it uses and choose Full Publish or check its Type requirement."
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish anyway"
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish 3MF..."
|
||||
msgstr ""
|
||||
|
||||
msgid "Select which settings to be published in the 3MF file"
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish 3MF Wiki"
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish 3MF Video Guide"
|
||||
msgstr ""
|
||||
|
||||
msgid "Mixed filament - published as a whole when \"Enable\" above is selected"
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish this mixed filament and enable + Full Publish its component filaments"
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish this filament slot in the 3MF file"
|
||||
msgstr ""
|
||||
|
||||
msgid "Full Publish"
|
||||
msgstr ""
|
||||
|
||||
msgid "Embed the entire filament of this slot in the 3MF file"
|
||||
msgstr ""
|
||||
|
||||
msgid "Filter non-selected"
|
||||
msgstr ""
|
||||
|
||||
#, c-format, boost-format
|
||||
msgid "Save %s as"
|
||||
msgstr "%s mentése"
|
||||
@@ -21110,6 +21188,15 @@ msgstr "Ez a művelet nem vonható vissza. Folytatod?"
|
||||
msgid "Skipping objects."
|
||||
msgstr "Objektumok kihagyása."
|
||||
|
||||
msgid "Material Ratio"
|
||||
msgstr ""
|
||||
|
||||
msgid "Model Height"
|
||||
msgstr ""
|
||||
|
||||
msgid "Ratio"
|
||||
msgstr ""
|
||||
|
||||
msgid "Select Filament"
|
||||
msgstr "Filament kiválasztása"
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Orca Slicer\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-09-02 09:39-0300\n"
|
||||
"POT-Creation-Date: 2026-09-03 10:27+0800\n"
|
||||
"PO-Revision-Date: \n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: \n"
|
||||
@@ -4907,6 +4907,9 @@ msgstr ""
|
||||
"Sì - Modifica queste impostazioni ed abilita la modalità spirale automaticamente\n"
|
||||
"No - Annulla l'attivazione della modalità a spirale"
|
||||
|
||||
msgid "N/A"
|
||||
msgstr "N/D"
|
||||
|
||||
msgid "Printing"
|
||||
msgstr "Stampa"
|
||||
|
||||
@@ -5366,9 +5369,6 @@ msgstr "Schema non valido. Utilizzare N, N#K o un elenco separato da virgole con
|
||||
msgid "Invalid format. Expected vector format: \"%1%\""
|
||||
msgstr "Formato non valido. Formato vettoriale previsto: \"%1%\""
|
||||
|
||||
msgid "N/A"
|
||||
msgstr "N/D"
|
||||
|
||||
# AI Translated
|
||||
msgid "System agents"
|
||||
msgstr "Agenti di sistema"
|
||||
@@ -6327,6 +6327,12 @@ msgstr "Salva progetto con nome"
|
||||
msgid "Save current project as"
|
||||
msgstr "Salva progetto corrente con nome"
|
||||
|
||||
msgid "Publish 3MF"
|
||||
msgstr ""
|
||||
|
||||
msgid "Export a 3MF file with the selected settings embedded"
|
||||
msgstr ""
|
||||
|
||||
msgid "Import 3MF/STL/STEP/SVG/OBJ/AMF"
|
||||
msgstr "Importa 3MF/STL/STEP/SVG/OBJ/AMF"
|
||||
|
||||
@@ -8163,6 +8169,12 @@ msgstr "Si prega di confermare che i G-code all'interno di questi profili sono s
|
||||
msgid "Customized Preset"
|
||||
msgstr "Profilo personalizzato"
|
||||
|
||||
msgid "Some published settings could not be applied:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Some filament slots were changed:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Component name(s) inside step file not in UTF-8 format!"
|
||||
msgstr "Il nome dei componenti all'interno del file STEP non è in formato UTF-8!"
|
||||
|
||||
@@ -8570,6 +8582,17 @@ msgstr "Salva file elaborato con nome:"
|
||||
msgid "The file %s has been sent to the printer's storage space and can be viewed on the printer."
|
||||
msgstr "Il file %s è stato inviato alla memoria della stampante e può essere visualizzato da lì."
|
||||
|
||||
msgid "Publish 3MF file as:"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"Failed to export the published 3MF file.\n"
|
||||
"Please check whether the folder exists online or if other programs have the file open."
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish"
|
||||
msgstr "Pubblica"
|
||||
|
||||
msgid "The nozzle type is not set. Please set the nozzle and try again."
|
||||
msgstr "Il tipo di ugello non è impostato. Impostare l'ugello e riprovare."
|
||||
|
||||
@@ -9754,9 +9777,6 @@ msgstr "Vai alla pagina web di pubblicazione del modello"
|
||||
msgid "Note: The preparation may take several minutes. Please be patient."
|
||||
msgstr "Nota: la preparazione può richiedere alcuni minuti. Si prega di avere pazienza."
|
||||
|
||||
msgid "Publish"
|
||||
msgstr "Pubblica"
|
||||
|
||||
msgid "Publish was canceled"
|
||||
msgstr "La pubblicazione è stata annullata"
|
||||
|
||||
@@ -9772,6 +9792,64 @@ msgstr "Caricamento dati"
|
||||
msgid "Jump to webpage"
|
||||
msgstr "Vai alla pagina web"
|
||||
|
||||
msgid "Material"
|
||||
msgstr ""
|
||||
|
||||
msgid "Mixed filament"
|
||||
msgstr ""
|
||||
|
||||
msgid "Some mixed filaments rely on filaments that will not be published:"
|
||||
msgstr ""
|
||||
|
||||
#, c-format, boost-format
|
||||
msgid "Filament %d (mixed)"
|
||||
msgstr ""
|
||||
|
||||
msgid "needs"
|
||||
msgstr ""
|
||||
|
||||
msgid "not enabled"
|
||||
msgstr ""
|
||||
|
||||
msgid "material not published"
|
||||
msgstr ""
|
||||
|
||||
msgid "To publish a mixed filament, enable every filament it uses and choose Full Publish or check its Type requirement."
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish anyway"
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish 3MF..."
|
||||
msgstr ""
|
||||
|
||||
msgid "Select which settings to be published in the 3MF file"
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish 3MF Wiki"
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish 3MF Video Guide"
|
||||
msgstr ""
|
||||
|
||||
msgid "Mixed filament - published as a whole when \"Enable\" above is selected"
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish this mixed filament and enable + Full Publish its component filaments"
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish this filament slot in the 3MF file"
|
||||
msgstr ""
|
||||
|
||||
msgid "Full Publish"
|
||||
msgstr ""
|
||||
|
||||
msgid "Embed the entire filament of this slot in the 3MF file"
|
||||
msgstr ""
|
||||
|
||||
msgid "Filter non-selected"
|
||||
msgstr ""
|
||||
|
||||
#, c-format, boost-format
|
||||
msgid "Save %s as"
|
||||
msgstr "Salva %s con nome"
|
||||
@@ -21134,6 +21212,15 @@ msgstr "Questa azione non può essere annullata. Continuare?"
|
||||
msgid "Skipping objects."
|
||||
msgstr "Salto degli oggetti."
|
||||
|
||||
msgid "Material Ratio"
|
||||
msgstr ""
|
||||
|
||||
msgid "Model Height"
|
||||
msgstr ""
|
||||
|
||||
msgid "Ratio"
|
||||
msgstr ""
|
||||
|
||||
msgid "Select Filament"
|
||||
msgstr "Seleziona filamento"
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Orca Slicer\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-09-02 09:39-0300\n"
|
||||
"POT-Creation-Date: 2026-09-03 10:27+0800\n"
|
||||
"PO-Revision-Date: \n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: \n"
|
||||
@@ -4920,6 +4920,10 @@ msgstr ""
|
||||
"はい - 変更して、スパイラルモードを有効にします\n"
|
||||
"いいえ - 変更せず、スパイラルモードを有効しません"
|
||||
|
||||
# AI Translated
|
||||
msgid "N/A"
|
||||
msgstr "N/A"
|
||||
|
||||
msgid "Printing"
|
||||
msgstr "造形中"
|
||||
|
||||
@@ -5378,10 +5382,6 @@ msgstr "無効なパターンです。N、N#K、またはオプション#K付き
|
||||
msgid "Invalid format. Expected vector format: \"%1%\""
|
||||
msgstr "無効なフォーマット、%1%であるはずです。"
|
||||
|
||||
# AI Translated
|
||||
msgid "N/A"
|
||||
msgstr "N/A"
|
||||
|
||||
# AI Translated
|
||||
msgid "System agents"
|
||||
msgstr "システムエージェント"
|
||||
@@ -6336,6 +6336,12 @@ msgstr "プロジェクトを名前を付けて保存"
|
||||
msgid "Save current project as"
|
||||
msgstr "プロジェクトを名前を付けて保存"
|
||||
|
||||
msgid "Publish 3MF"
|
||||
msgstr ""
|
||||
|
||||
msgid "Export a 3MF file with the selected settings embedded"
|
||||
msgstr ""
|
||||
|
||||
msgid "Import 3MF/STL/STEP/SVG/OBJ/AMF"
|
||||
msgstr "3MF/STL/STEP/SVG/OBJ/AMFをインポート"
|
||||
|
||||
@@ -8177,6 +8183,12 @@ msgstr "これらのプリセット内のG-codeがマシンに損傷を与えな
|
||||
msgid "Customized Preset"
|
||||
msgstr "カスタマイズされたプリセット"
|
||||
|
||||
msgid "Some published settings could not be applied:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Some filament slots were changed:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Component name(s) inside step file not in UTF-8 format!"
|
||||
msgstr "ファイルのエンコーディング方式はUTF-8形式ではありません"
|
||||
|
||||
@@ -8589,6 +8601,17 @@ msgstr "名前を付けて保存:"
|
||||
msgid "The file %s has been sent to the printer's storage space and can be viewed on the printer."
|
||||
msgstr "%sを送信しました、プリンターにて確認できます"
|
||||
|
||||
msgid "Publish 3MF file as:"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"Failed to export the published 3MF file.\n"
|
||||
"Please check whether the folder exists online or if other programs have the file open."
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish"
|
||||
msgstr "公開する"
|
||||
|
||||
msgid "The nozzle type is not set. Please set the nozzle and try again."
|
||||
msgstr "ノズルタイプが設定されていません。ノズルを設定して再試行してください。"
|
||||
|
||||
@@ -9776,9 +9799,6 @@ msgstr "モデル公開ページに移動"
|
||||
msgid "Note: The preparation may take several minutes. Please be patient."
|
||||
msgstr "注意: 準備するには数分かかる場合があります、暫くお待ち下さい。"
|
||||
|
||||
msgid "Publish"
|
||||
msgstr "公開する"
|
||||
|
||||
msgid "Publish was canceled"
|
||||
msgstr "公開は取り消しました"
|
||||
|
||||
@@ -9794,6 +9814,64 @@ msgstr "データをアップロード中"
|
||||
msgid "Jump to webpage"
|
||||
msgstr "ウェブページに移動"
|
||||
|
||||
msgid "Material"
|
||||
msgstr ""
|
||||
|
||||
msgid "Mixed filament"
|
||||
msgstr ""
|
||||
|
||||
msgid "Some mixed filaments rely on filaments that will not be published:"
|
||||
msgstr ""
|
||||
|
||||
#, c-format, boost-format
|
||||
msgid "Filament %d (mixed)"
|
||||
msgstr ""
|
||||
|
||||
msgid "needs"
|
||||
msgstr ""
|
||||
|
||||
msgid "not enabled"
|
||||
msgstr ""
|
||||
|
||||
msgid "material not published"
|
||||
msgstr ""
|
||||
|
||||
msgid "To publish a mixed filament, enable every filament it uses and choose Full Publish or check its Type requirement."
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish anyway"
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish 3MF..."
|
||||
msgstr ""
|
||||
|
||||
msgid "Select which settings to be published in the 3MF file"
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish 3MF Wiki"
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish 3MF Video Guide"
|
||||
msgstr ""
|
||||
|
||||
msgid "Mixed filament - published as a whole when \"Enable\" above is selected"
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish this mixed filament and enable + Full Publish its component filaments"
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish this filament slot in the 3MF file"
|
||||
msgstr ""
|
||||
|
||||
msgid "Full Publish"
|
||||
msgstr ""
|
||||
|
||||
msgid "Embed the entire filament of this slot in the 3MF file"
|
||||
msgstr ""
|
||||
|
||||
msgid "Filter non-selected"
|
||||
msgstr ""
|
||||
|
||||
#, c-format, boost-format
|
||||
msgid "Save %s as"
|
||||
msgstr "%sを名前つけて保存"
|
||||
@@ -21678,6 +21756,15 @@ msgstr "この操作は元に戻せません。続行しますか?"
|
||||
msgid "Skipping objects."
|
||||
msgstr "オブジェクトをスキップ中。"
|
||||
|
||||
msgid "Material Ratio"
|
||||
msgstr ""
|
||||
|
||||
msgid "Model Height"
|
||||
msgstr ""
|
||||
|
||||
msgid "Ratio"
|
||||
msgstr ""
|
||||
|
||||
msgid "Select Filament"
|
||||
msgstr "フィラメントを選択"
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Orca Slicer\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-09-02 09:39-0300\n"
|
||||
"POT-Creation-Date: 2026-09-03 10:27+0800\n"
|
||||
"PO-Revision-Date: 2025-06-02 17:12+0900\n"
|
||||
"Last-Translator: crwusiz <crwusiz@gmail.com>\n"
|
||||
"Language-Team: \n"
|
||||
@@ -4931,6 +4931,10 @@ msgstr ""
|
||||
"예 - 이 설정을 변경하고 나선 모드를 자동으로 활성화합니다\n"
|
||||
"아니오 - 이번에는 나선 모드 사용을 포기합니다"
|
||||
|
||||
# AI Translated
|
||||
msgid "N/A"
|
||||
msgstr "해당 없음"
|
||||
|
||||
msgid "Printing"
|
||||
msgstr "출력 중"
|
||||
|
||||
@@ -5390,10 +5394,6 @@ msgstr "잘못된 패턴입니다. N, N#K 또는 항목당 선택적 #K가 있
|
||||
msgid "Invalid format. Expected vector format: \"%1%\""
|
||||
msgstr "잘못된 형식입니다. 필요한 벡터 형식: \"%1%\""
|
||||
|
||||
# AI Translated
|
||||
msgid "N/A"
|
||||
msgstr "해당 없음"
|
||||
|
||||
# AI Translated
|
||||
msgid "System agents"
|
||||
msgstr "시스템 에이전트"
|
||||
@@ -6350,6 +6350,12 @@ msgstr "프로젝트 다른 이름으로 저장"
|
||||
msgid "Save current project as"
|
||||
msgstr "현재 프로젝트 다른 이름으로 저장"
|
||||
|
||||
msgid "Publish 3MF"
|
||||
msgstr ""
|
||||
|
||||
msgid "Export a 3MF file with the selected settings embedded"
|
||||
msgstr ""
|
||||
|
||||
msgid "Import 3MF/STL/STEP/SVG/OBJ/AMF"
|
||||
msgstr "3MF/STL/STEP/SVG/OBJ/AMF 가져오기"
|
||||
|
||||
@@ -8195,6 +8201,12 @@ msgstr "이러한 사전 설정 내의 Gcode가 기계 손상을 방지할 수
|
||||
msgid "Customized Preset"
|
||||
msgstr "사용자 정의 프리셋"
|
||||
|
||||
msgid "Some published settings could not be applied:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Some filament slots were changed:"
|
||||
msgstr ""
|
||||
|
||||
# AI Translated
|
||||
msgid "Component name(s) inside step file not in UTF-8 format!"
|
||||
msgstr "STEP 파일 내부의 구성 요소 이름이 UTF-8 형식이 아닙니다!"
|
||||
@@ -8622,6 +8634,17 @@ msgstr "슬라이스 파일을 다음으로 저장:"
|
||||
msgid "The file %s has been sent to the printer's storage space and can be viewed on the printer."
|
||||
msgstr "%s 파일이 프린터의 저장 공간으로 전송되었으며 프린터에서 볼 수 있습니다."
|
||||
|
||||
msgid "Publish 3MF file as:"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"Failed to export the published 3MF file.\n"
|
||||
"Please check whether the folder exists online or if other programs have the file open."
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish"
|
||||
msgstr "게시"
|
||||
|
||||
msgid "The nozzle type is not set. Please set the nozzle and try again."
|
||||
msgstr "노즐 유형이 설정되지 않았습니다. 노즐을 설정하고 다시 시도하세요."
|
||||
|
||||
@@ -9865,9 +9888,6 @@ msgstr "모델 게시 웹 페이지로 이동"
|
||||
msgid "Note: The preparation may take several minutes. Please be patient."
|
||||
msgstr "참고: 준비하는 데 몇 분 정도 걸릴 수 있습니다. 조금만 기다려 주십시오."
|
||||
|
||||
msgid "Publish"
|
||||
msgstr "게시"
|
||||
|
||||
msgid "Publish was canceled"
|
||||
msgstr "게시가 취소되었습니다"
|
||||
|
||||
@@ -9884,6 +9904,64 @@ msgstr "데이터 업로드 중"
|
||||
msgid "Jump to webpage"
|
||||
msgstr "웹 페이지로 이동"
|
||||
|
||||
msgid "Material"
|
||||
msgstr ""
|
||||
|
||||
msgid "Mixed filament"
|
||||
msgstr ""
|
||||
|
||||
msgid "Some mixed filaments rely on filaments that will not be published:"
|
||||
msgstr ""
|
||||
|
||||
#, c-format, boost-format
|
||||
msgid "Filament %d (mixed)"
|
||||
msgstr ""
|
||||
|
||||
msgid "needs"
|
||||
msgstr ""
|
||||
|
||||
msgid "not enabled"
|
||||
msgstr ""
|
||||
|
||||
msgid "material not published"
|
||||
msgstr ""
|
||||
|
||||
msgid "To publish a mixed filament, enable every filament it uses and choose Full Publish or check its Type requirement."
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish anyway"
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish 3MF..."
|
||||
msgstr ""
|
||||
|
||||
msgid "Select which settings to be published in the 3MF file"
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish 3MF Wiki"
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish 3MF Video Guide"
|
||||
msgstr ""
|
||||
|
||||
msgid "Mixed filament - published as a whole when \"Enable\" above is selected"
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish this mixed filament and enable + Full Publish its component filaments"
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish this filament slot in the 3MF file"
|
||||
msgstr ""
|
||||
|
||||
msgid "Full Publish"
|
||||
msgstr ""
|
||||
|
||||
msgid "Embed the entire filament of this slot in the 3MF file"
|
||||
msgstr ""
|
||||
|
||||
msgid "Filter non-selected"
|
||||
msgstr ""
|
||||
|
||||
#, c-format, boost-format
|
||||
msgid "Save %s as"
|
||||
msgstr "%s을(를) 다음으로 저장"
|
||||
@@ -21536,6 +21614,15 @@ msgstr "이 작업은 취소할 수 없습니다. 계속하시겠습니까?"
|
||||
msgid "Skipping objects."
|
||||
msgstr "물체 건너뛰기"
|
||||
|
||||
msgid "Material Ratio"
|
||||
msgstr ""
|
||||
|
||||
msgid "Model Height"
|
||||
msgstr ""
|
||||
|
||||
msgid "Ratio"
|
||||
msgstr ""
|
||||
|
||||
msgid "Select Filament"
|
||||
msgstr "필라멘트 선택"
|
||||
|
||||
|
||||
@@ -136,6 +136,7 @@ src/slic3r/GUI/BackgroundSlicingProcess.cpp
|
||||
src/slic3r/GUI/BedShapeDialog.cpp
|
||||
src/slic3r/GUI/BedShapeDialog.hpp
|
||||
src/slic3r/GUI/ConfigManipulation.cpp
|
||||
src/slic3r/GUI/ConfigValueFormatter.cpp
|
||||
src/slic3r/GUI/DeviceManager.cpp
|
||||
src/slic3r/GUI/DeviceErrorDialog.cpp
|
||||
src/slic3r/GUI/ExtraRenderers.cpp
|
||||
@@ -175,6 +176,7 @@ src/slic3r/GUI/ProgressStatusBar.cpp
|
||||
src/slic3r/GUI/PlateSettingsDialog.cpp
|
||||
src/slic3r/GUI/PrivacyUpdateDialog.cpp
|
||||
src/slic3r/GUI/PublishDialog.cpp
|
||||
src/slic3r/GUI/PublishSettingsDialog.cpp
|
||||
src/slic3r/GUI/SavePresetDialog.cpp
|
||||
src/slic3r/GUI/Search.cpp
|
||||
src/slic3r/GUI/Selection.cpp
|
||||
|
||||
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: \n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-09-02 09:39-0300\n"
|
||||
"POT-Creation-Date: 2026-09-03 10:27+0800\n"
|
||||
"PO-Revision-Date: 2026-07-02 14:13+0300\n"
|
||||
"Last-Translator: Gintaras Kučinskas <sharanchius@gmail.com>\n"
|
||||
"Language-Team: \n"
|
||||
@@ -4893,6 +4893,9 @@ msgstr ""
|
||||
"Taip – pakeisti šiuos nustatymus ir automatiškai įjungti spiralinį režimą\n"
|
||||
"Ne – nenaudoti spiralinio režimo"
|
||||
|
||||
msgid "N/A"
|
||||
msgstr "N/A"
|
||||
|
||||
msgid "Printing"
|
||||
msgstr "Spausdinimas"
|
||||
|
||||
@@ -5352,9 +5355,6 @@ msgstr "Neteisingas šablonas. Naudokite N, N#K arba kableliais atskirtą sąra
|
||||
msgid "Invalid format. Expected vector format: \"%1%\""
|
||||
msgstr "Netinkamas formatas. Tinkamas vektorinis formatas: \"%1%\""
|
||||
|
||||
msgid "N/A"
|
||||
msgstr "N/A"
|
||||
|
||||
# AI Translated
|
||||
msgid "System agents"
|
||||
msgstr "Sisteminiai agentai"
|
||||
@@ -6312,6 +6312,12 @@ msgstr "Įrašyti projektą kaip"
|
||||
msgid "Save current project as"
|
||||
msgstr "Įrašyti dabartinį projektą kaip"
|
||||
|
||||
msgid "Publish 3MF"
|
||||
msgstr ""
|
||||
|
||||
msgid "Export a 3MF file with the selected settings embedded"
|
||||
msgstr ""
|
||||
|
||||
msgid "Import 3MF/STL/STEP/SVG/OBJ/AMF"
|
||||
msgstr "Importuoti 3MF/STL/STEP/SVG/OBJ/AMF"
|
||||
|
||||
@@ -8153,6 +8159,12 @@ msgstr "Patvirtinkite, kad šiuose profiliuose esantis G-kodas yra saugus, kad i
|
||||
msgid "Customized Preset"
|
||||
msgstr "Pritaikytas profilis"
|
||||
|
||||
msgid "Some published settings could not be applied:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Some filament slots were changed:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Component name(s) inside step file not in UTF-8 format!"
|
||||
msgstr "Komponentų pavadinimai STEP faile nėra UTF-8 formato!"
|
||||
|
||||
@@ -8564,6 +8576,17 @@ msgstr "Išsaugoti susluoksniuotą failą kaip:"
|
||||
msgid "The file %s has been sent to the printer's storage space and can be viewed on the printer."
|
||||
msgstr "Failas %s išsiųstas į spausdintuvo laikmeną ir gali būti peržiūrimas spausdintuve."
|
||||
|
||||
msgid "Publish 3MF file as:"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"Failed to export the published 3MF file.\n"
|
||||
"Please check whether the folder exists online or if other programs have the file open."
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish"
|
||||
msgstr "Talpinti"
|
||||
|
||||
msgid "The nozzle type is not set. Please set the nozzle and try again."
|
||||
msgstr "Purkštuko tipas nenustatytas. Nustatykite purkštuką ir bandykite dar kartą."
|
||||
|
||||
@@ -9693,9 +9716,6 @@ msgstr "Pereiti į modelio talpinimo interneto puslapį"
|
||||
msgid "Note: The preparation may take several minutes. Please be patient."
|
||||
msgstr "Pastaba: paruošimas gali užtrukti kelias minutes. Būkite kantrūs."
|
||||
|
||||
msgid "Publish"
|
||||
msgstr "Talpinti"
|
||||
|
||||
msgid "Publish was canceled"
|
||||
msgstr "Publikavimas buvo atšauktas"
|
||||
|
||||
@@ -9711,6 +9731,64 @@ msgstr "Įkeliami duomenys"
|
||||
msgid "Jump to webpage"
|
||||
msgstr "Pereiti į interneto puslapį"
|
||||
|
||||
msgid "Material"
|
||||
msgstr ""
|
||||
|
||||
msgid "Mixed filament"
|
||||
msgstr ""
|
||||
|
||||
msgid "Some mixed filaments rely on filaments that will not be published:"
|
||||
msgstr ""
|
||||
|
||||
#, c-format, boost-format
|
||||
msgid "Filament %d (mixed)"
|
||||
msgstr ""
|
||||
|
||||
msgid "needs"
|
||||
msgstr ""
|
||||
|
||||
msgid "not enabled"
|
||||
msgstr ""
|
||||
|
||||
msgid "material not published"
|
||||
msgstr ""
|
||||
|
||||
msgid "To publish a mixed filament, enable every filament it uses and choose Full Publish or check its Type requirement."
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish anyway"
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish 3MF..."
|
||||
msgstr ""
|
||||
|
||||
msgid "Select which settings to be published in the 3MF file"
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish 3MF Wiki"
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish 3MF Video Guide"
|
||||
msgstr ""
|
||||
|
||||
msgid "Mixed filament - published as a whole when \"Enable\" above is selected"
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish this mixed filament and enable + Full Publish its component filaments"
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish this filament slot in the 3MF file"
|
||||
msgstr ""
|
||||
|
||||
msgid "Full Publish"
|
||||
msgstr ""
|
||||
|
||||
msgid "Embed the entire filament of this slot in the 3MF file"
|
||||
msgstr ""
|
||||
|
||||
msgid "Filter non-selected"
|
||||
msgstr ""
|
||||
|
||||
#, c-format, boost-format
|
||||
msgid "Save %s as"
|
||||
msgstr "Išsaugoti %s kaip"
|
||||
@@ -20865,6 +20943,15 @@ msgstr "Šio veiksmo atšaukti nebus galima. Tęsti?"
|
||||
msgid "Skipping objects."
|
||||
msgstr "Praleidžiami objektai."
|
||||
|
||||
msgid "Material Ratio"
|
||||
msgstr ""
|
||||
|
||||
msgid "Model Height"
|
||||
msgstr ""
|
||||
|
||||
msgid "Ratio"
|
||||
msgstr ""
|
||||
|
||||
msgid "Select Filament"
|
||||
msgstr "Pasirinkti giją"
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Orca Slicer\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-09-02 09:39-0300\n"
|
||||
"POT-Creation-Date: 2026-09-03 10:27+0800\n"
|
||||
"PO-Revision-Date: \n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: \n"
|
||||
@@ -5325,6 +5325,9 @@ msgstr ""
|
||||
"Ja - Pas de instellingen aan en zet de vaas modus automatisch aan\n"
|
||||
"Nee - Pas de vaas modus deze keer niet toe"
|
||||
|
||||
msgid "N/A"
|
||||
msgstr "N/B"
|
||||
|
||||
msgid "Printing"
|
||||
msgstr "Printen"
|
||||
|
||||
@@ -5861,9 +5864,6 @@ msgstr "Ongeldig patroon. Gebruik N, N#K of een door komma's gescheiden lijst me
|
||||
msgid "Invalid format. Expected vector format: \"%1%\""
|
||||
msgstr "Onjuist formaat. Het Vector formaat wordt verwacht: \"%1%\""
|
||||
|
||||
msgid "N/A"
|
||||
msgstr "N/B"
|
||||
|
||||
# AI Translated
|
||||
msgid "System agents"
|
||||
msgstr "Systeemagenten"
|
||||
@@ -6888,6 +6888,12 @@ msgstr "Bewaar project als"
|
||||
msgid "Save current project as"
|
||||
msgstr "Bewaar huidig project als"
|
||||
|
||||
msgid "Publish 3MF"
|
||||
msgstr ""
|
||||
|
||||
msgid "Export a 3MF file with the selected settings embedded"
|
||||
msgstr ""
|
||||
|
||||
msgid "Import 3MF/STL/STEP/SVG/OBJ/AMF"
|
||||
msgstr "3MF/STL/STEP/SVG/OBJ/AMF importeren"
|
||||
|
||||
@@ -8904,6 +8910,12 @@ msgstr "Controleer of de G-codes in deze presets veilig zijn om schade aan de ma
|
||||
msgid "Customized Preset"
|
||||
msgstr "Aangepaste voorinstelling"
|
||||
|
||||
msgid "Some published settings could not be applied:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Some filament slots were changed:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Component name(s) inside step file not in UTF-8 format!"
|
||||
msgstr "Naam van componenten in step-bestand is niet UTF-8-formaat!"
|
||||
|
||||
@@ -9339,6 +9351,17 @@ msgstr "Bewaar het geslicede bestand als:"
|
||||
msgid "The file %s has been sent to the printer's storage space and can be viewed on the printer."
|
||||
msgstr "Het bestand %s is naar de opslagruimte van de printer gestuurd en kan op de printer worden bekeken."
|
||||
|
||||
msgid "Publish 3MF file as:"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"Failed to export the published 3MF file.\n"
|
||||
"Please check whether the folder exists online or if other programs have the file open."
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish"
|
||||
msgstr "Publiceren"
|
||||
|
||||
# AI Translated
|
||||
msgid "The nozzle type is not set. Please set the nozzle and try again."
|
||||
msgstr "Het mondstuktype is niet ingesteld. Stel het mondstuk in en probeer het opnieuw."
|
||||
@@ -10626,9 +10649,6 @@ msgstr "Ga naar de website om het model te publiceren"
|
||||
msgid "Note: The preparation may take several minutes. Please be patient."
|
||||
msgstr "Notitie: het voorbereiden kan enkele minuten duren. Even geduld alstublieft."
|
||||
|
||||
msgid "Publish"
|
||||
msgstr "Publiceren"
|
||||
|
||||
msgid "Publish was canceled"
|
||||
msgstr "Het publiceren is geannuleerd"
|
||||
|
||||
@@ -10645,6 +10665,64 @@ msgstr "Gegevens uploaden"
|
||||
msgid "Jump to webpage"
|
||||
msgstr "Ga naar de website"
|
||||
|
||||
msgid "Material"
|
||||
msgstr ""
|
||||
|
||||
msgid "Mixed filament"
|
||||
msgstr ""
|
||||
|
||||
msgid "Some mixed filaments rely on filaments that will not be published:"
|
||||
msgstr ""
|
||||
|
||||
#, c-format, boost-format
|
||||
msgid "Filament %d (mixed)"
|
||||
msgstr ""
|
||||
|
||||
msgid "needs"
|
||||
msgstr ""
|
||||
|
||||
msgid "not enabled"
|
||||
msgstr ""
|
||||
|
||||
msgid "material not published"
|
||||
msgstr ""
|
||||
|
||||
msgid "To publish a mixed filament, enable every filament it uses and choose Full Publish or check its Type requirement."
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish anyway"
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish 3MF..."
|
||||
msgstr ""
|
||||
|
||||
msgid "Select which settings to be published in the 3MF file"
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish 3MF Wiki"
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish 3MF Video Guide"
|
||||
msgstr ""
|
||||
|
||||
msgid "Mixed filament - published as a whole when \"Enable\" above is selected"
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish this mixed filament and enable + Full Publish its component filaments"
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish this filament slot in the 3MF file"
|
||||
msgstr ""
|
||||
|
||||
msgid "Full Publish"
|
||||
msgstr ""
|
||||
|
||||
msgid "Embed the entire filament of this slot in the 3MF file"
|
||||
msgstr ""
|
||||
|
||||
msgid "Filter non-selected"
|
||||
msgstr ""
|
||||
|
||||
#, c-format, boost-format
|
||||
msgid "Save %s as"
|
||||
msgstr "Bewaar %s als"
|
||||
@@ -23245,6 +23323,15 @@ msgstr "Deze actie kan niet ongedaan worden gemaakt. Doorgaan?"
|
||||
msgid "Skipping objects."
|
||||
msgstr "Objecten worden overgeslagen."
|
||||
|
||||
msgid "Material Ratio"
|
||||
msgstr ""
|
||||
|
||||
msgid "Model Height"
|
||||
msgstr ""
|
||||
|
||||
msgid "Ratio"
|
||||
msgstr ""
|
||||
|
||||
msgid "Select Filament"
|
||||
msgstr "Selecteer filament"
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: OrcaSlicer 2.3.0-rc\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-09-02 09:39-0300\n"
|
||||
"POT-Creation-Date: 2026-09-03 10:27+0800\n"
|
||||
"PO-Revision-Date: \n"
|
||||
"Last-Translator: Krzysztof Morga <<tlumaczeniebs@gmail.com>>\n"
|
||||
"Language-Team: \n"
|
||||
@@ -5013,6 +5013,10 @@ msgstr ""
|
||||
"Tak - Zmień te ustawienia automatycznie i włącz tryb Wazy\n"
|
||||
"Nie - Zrezygnuj tym razem z używania trybu Wazy"
|
||||
|
||||
# AI Translated
|
||||
msgid "N/A"
|
||||
msgstr "Nie dotyczy"
|
||||
|
||||
msgid "Printing"
|
||||
msgstr "Drukowanie"
|
||||
|
||||
@@ -5480,10 +5484,6 @@ msgstr "Nieprawidłowy wzorzec. Użyj N, N#K lub listy rozdzielonej przecinkami
|
||||
msgid "Invalid format. Expected vector format: \"%1%\""
|
||||
msgstr "Nieprawidłowy format. Oczekiwano formatu wektorowego: „%1%”"
|
||||
|
||||
# AI Translated
|
||||
msgid "N/A"
|
||||
msgstr "Nie dotyczy"
|
||||
|
||||
# AI Translated
|
||||
msgid "System agents"
|
||||
msgstr "Agenci systemowi"
|
||||
@@ -6469,6 +6469,12 @@ msgstr "Zapisz projekt jako"
|
||||
msgid "Save current project as"
|
||||
msgstr "Zapisz bieżący projekt jako"
|
||||
|
||||
msgid "Publish 3MF"
|
||||
msgstr ""
|
||||
|
||||
msgid "Export a 3MF file with the selected settings embedded"
|
||||
msgstr ""
|
||||
|
||||
msgid "Import 3MF/STL/STEP/SVG/OBJ/AMF"
|
||||
msgstr "Importuj 3MF/STL/STEP/SVG/OBJ/AMF"
|
||||
|
||||
@@ -8353,6 +8359,12 @@ msgstr "Proszę potwierdź, że G-code w tych profilach jest bezpieczny, aby zap
|
||||
msgid "Customized Preset"
|
||||
msgstr "Dostosowany profil"
|
||||
|
||||
msgid "Some published settings could not be applied:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Some filament slots were changed:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Component name(s) inside step file not in UTF-8 format!"
|
||||
msgstr "Nazwa komponentów w pliku step nie jest w formacie UTF-8!"
|
||||
|
||||
@@ -8779,6 +8791,17 @@ msgstr "Zapisz plik po wykonaniu cięcia jako:"
|
||||
msgid "The file %s has been sent to the printer's storage space and can be viewed on the printer."
|
||||
msgstr "Plik %s został wysłany do pamięci drukarki i można go obejrzeć na urządzeniu."
|
||||
|
||||
msgid "Publish 3MF file as:"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"Failed to export the published 3MF file.\n"
|
||||
"Please check whether the folder exists online or if other programs have the file open."
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish"
|
||||
msgstr "Opublikuj"
|
||||
|
||||
msgid "The nozzle type is not set. Please set the nozzle and try again."
|
||||
msgstr "Nie ustawiono typu dyszy Wprowadź ustawienia dyszy i spróbuj ponownie."
|
||||
|
||||
@@ -10021,9 +10044,6 @@ msgstr "Przejdź do strony publikacji modelu"
|
||||
msgid "Note: The preparation may take several minutes. Please be patient."
|
||||
msgstr "Uwaga: Przygotowanie może zająć kilka minut. Proszę o cierpliwość."
|
||||
|
||||
msgid "Publish"
|
||||
msgstr "Opublikuj"
|
||||
|
||||
msgid "Publish was canceled"
|
||||
msgstr "Publikacja została anulowana"
|
||||
|
||||
@@ -10040,6 +10060,64 @@ msgstr "Przesyłanie danych"
|
||||
msgid "Jump to webpage"
|
||||
msgstr "Przejdź na stronę"
|
||||
|
||||
msgid "Material"
|
||||
msgstr ""
|
||||
|
||||
msgid "Mixed filament"
|
||||
msgstr ""
|
||||
|
||||
msgid "Some mixed filaments rely on filaments that will not be published:"
|
||||
msgstr ""
|
||||
|
||||
#, c-format, boost-format
|
||||
msgid "Filament %d (mixed)"
|
||||
msgstr ""
|
||||
|
||||
msgid "needs"
|
||||
msgstr ""
|
||||
|
||||
msgid "not enabled"
|
||||
msgstr ""
|
||||
|
||||
msgid "material not published"
|
||||
msgstr ""
|
||||
|
||||
msgid "To publish a mixed filament, enable every filament it uses and choose Full Publish or check its Type requirement."
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish anyway"
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish 3MF..."
|
||||
msgstr ""
|
||||
|
||||
msgid "Select which settings to be published in the 3MF file"
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish 3MF Wiki"
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish 3MF Video Guide"
|
||||
msgstr ""
|
||||
|
||||
msgid "Mixed filament - published as a whole when \"Enable\" above is selected"
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish this mixed filament and enable + Full Publish its component filaments"
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish this filament slot in the 3MF file"
|
||||
msgstr ""
|
||||
|
||||
msgid "Full Publish"
|
||||
msgstr ""
|
||||
|
||||
msgid "Embed the entire filament of this slot in the 3MF file"
|
||||
msgstr ""
|
||||
|
||||
msgid "Filter non-selected"
|
||||
msgstr ""
|
||||
|
||||
#, c-format, boost-format
|
||||
msgid "Save %s as"
|
||||
msgstr "Zapisz %s jako"
|
||||
@@ -21723,6 +21801,15 @@ msgstr "Tego działania nie będzie można cofnąć. Kontynuować?"
|
||||
msgid "Skipping objects."
|
||||
msgstr "Pomijanie obiektów."
|
||||
|
||||
msgid "Material Ratio"
|
||||
msgstr ""
|
||||
|
||||
msgid "Model Height"
|
||||
msgstr ""
|
||||
|
||||
msgid "Ratio"
|
||||
msgstr ""
|
||||
|
||||
msgid "Select Filament"
|
||||
msgstr "Wybierz filament"
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Orca Slicer\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-09-02 09:39-0300\n"
|
||||
"POT-Creation-Date: 2026-09-03 10:27+0800\n"
|
||||
"PO-Revision-Date: 2026-07-26 11:14-0300\n"
|
||||
"Last-Translator: Alexandre Folle de Menezes\n"
|
||||
"Language-Team: Portuguese, Brazilian\n"
|
||||
@@ -4743,6 +4743,9 @@ msgstr ""
|
||||
"Sim - Alterar essas configurações e ativar o modo espiral/vaso automaticamente\n"
|
||||
"Não - Cancelar ativação do modo espiral"
|
||||
|
||||
msgid "N/A"
|
||||
msgstr "N/D"
|
||||
|
||||
msgid "Printing"
|
||||
msgstr "Imprimindo"
|
||||
|
||||
@@ -5196,9 +5199,6 @@ msgstr "Padrão inválido. Use N, N#K, ou uma lista separa por vírgulas com #K
|
||||
msgid "Invalid format. Expected vector format: \"%1%\""
|
||||
msgstr "Formato inválido. Formato de vetor esperado: \"%1%\""
|
||||
|
||||
msgid "N/A"
|
||||
msgstr "N/D"
|
||||
|
||||
msgid "System agents"
|
||||
msgstr "Agentes do sistema"
|
||||
|
||||
@@ -6145,6 +6145,12 @@ msgstr "Salvar projeto como"
|
||||
msgid "Save current project as"
|
||||
msgstr "Salvar o projeto atual como"
|
||||
|
||||
msgid "Publish 3MF"
|
||||
msgstr ""
|
||||
|
||||
msgid "Export a 3MF file with the selected settings embedded"
|
||||
msgstr ""
|
||||
|
||||
msgid "Import 3MF/STL/STEP/SVG/OBJ/AMF"
|
||||
msgstr "Importar 3MF/STL/STEP/SVG/OBJ/AMF"
|
||||
|
||||
@@ -7941,6 +7947,12 @@ msgstr "Por favor, confirme se o G-code dentro dessas predefinições é seguro
|
||||
msgid "Customized Preset"
|
||||
msgstr "Predefinição Personalizada"
|
||||
|
||||
msgid "Some published settings could not be applied:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Some filament slots were changed:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Component name(s) inside step file not in UTF-8 format!"
|
||||
msgstr "Os nomes dos componentes dentro do arquivo STEP não estão no formato UTF-8!"
|
||||
|
||||
@@ -8345,6 +8357,17 @@ msgstr "Salvar arquivo fatiado como:"
|
||||
msgid "The file %s has been sent to the printer's storage space and can be viewed on the printer."
|
||||
msgstr "O arquivo %s foi enviado para o espaço de armazenamento da impressora e pode ser visualizado na impressora."
|
||||
|
||||
msgid "Publish 3MF file as:"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"Failed to export the published 3MF file.\n"
|
||||
"Please check whether the folder exists online or if other programs have the file open."
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish"
|
||||
msgstr "Publicar"
|
||||
|
||||
msgid "The nozzle type is not set. Please set the nozzle and try again."
|
||||
msgstr "O tipo de bico não está configurado. Configure o bico e tente novamente."
|
||||
|
||||
@@ -9478,9 +9501,6 @@ msgstr "Ir para a página web de publicação de modelos"
|
||||
msgid "Note: The preparation may take several minutes. Please be patient."
|
||||
msgstr "Nota: A preparação pode levar vários minutos. Por favor, seja paciente."
|
||||
|
||||
msgid "Publish"
|
||||
msgstr "Publicar"
|
||||
|
||||
msgid "Publish was canceled"
|
||||
msgstr "Publicação cancelada"
|
||||
|
||||
@@ -9496,6 +9516,64 @@ msgstr "Enviando dados"
|
||||
msgid "Jump to webpage"
|
||||
msgstr "Ir para a página web"
|
||||
|
||||
msgid "Material"
|
||||
msgstr ""
|
||||
|
||||
msgid "Mixed filament"
|
||||
msgstr ""
|
||||
|
||||
msgid "Some mixed filaments rely on filaments that will not be published:"
|
||||
msgstr ""
|
||||
|
||||
#, c-format, boost-format
|
||||
msgid "Filament %d (mixed)"
|
||||
msgstr ""
|
||||
|
||||
msgid "needs"
|
||||
msgstr ""
|
||||
|
||||
msgid "not enabled"
|
||||
msgstr ""
|
||||
|
||||
msgid "material not published"
|
||||
msgstr ""
|
||||
|
||||
msgid "To publish a mixed filament, enable every filament it uses and choose Full Publish or check its Type requirement."
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish anyway"
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish 3MF..."
|
||||
msgstr ""
|
||||
|
||||
msgid "Select which settings to be published in the 3MF file"
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish 3MF Wiki"
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish 3MF Video Guide"
|
||||
msgstr ""
|
||||
|
||||
msgid "Mixed filament - published as a whole when \"Enable\" above is selected"
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish this mixed filament and enable + Full Publish its component filaments"
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish this filament slot in the 3MF file"
|
||||
msgstr ""
|
||||
|
||||
msgid "Full Publish"
|
||||
msgstr ""
|
||||
|
||||
msgid "Embed the entire filament of this slot in the 3MF file"
|
||||
msgstr ""
|
||||
|
||||
msgid "Filter non-selected"
|
||||
msgstr ""
|
||||
|
||||
#, c-format, boost-format
|
||||
msgid "Save %s as"
|
||||
msgstr "Salvar %s como"
|
||||
@@ -20522,6 +20600,15 @@ msgstr "Esta ação não pode ser desfeita. Continuar?"
|
||||
msgid "Skipping objects."
|
||||
msgstr "Ignorando objetos."
|
||||
|
||||
msgid "Material Ratio"
|
||||
msgstr ""
|
||||
|
||||
msgid "Model Height"
|
||||
msgstr ""
|
||||
|
||||
msgid "Ratio"
|
||||
msgstr ""
|
||||
|
||||
msgid "Select Filament"
|
||||
msgstr "Selecionar Filamento"
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: OrcaSlicer V2.5.0\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-09-02 09:39-0300\n"
|
||||
"POT-Creation-Date: 2026-09-03 10:27+0800\n"
|
||||
"PO-Revision-Date: 2026-02-25 13:38+0300\n"
|
||||
"Last-Translator: Felix14_v2\n"
|
||||
"Language-Team: Felix14_v2 (ДС/ТГ: @felix14_v2, почта: aleks111001@list.ru), Andylg <andylg@yandex.ru>\n"
|
||||
@@ -4891,6 +4891,11 @@ msgid ""
|
||||
"No - Cancel enabling spiral mode"
|
||||
msgstr "Использовать эти настройки и режим вазы?"
|
||||
|
||||
# Не знаю, как и почему, но это, похоже, исправляет "вопросики" вместо
|
||||
# символов
|
||||
msgid "N/A"
|
||||
msgstr "–"
|
||||
|
||||
msgid "Printing"
|
||||
msgstr "Печать"
|
||||
|
||||
@@ -5360,11 +5365,6 @@ msgstr "Недопустимый шаблон. Используйте N, N#K и
|
||||
msgid "Invalid format. Expected vector format: \"%1%\""
|
||||
msgstr "Недопустимый формат. Ожидаемый векторный формат: \"%1%\""
|
||||
|
||||
# Не знаю, как и почему, но это, похоже, исправляет "вопросики" вместо
|
||||
# символов
|
||||
msgid "N/A"
|
||||
msgstr "–"
|
||||
|
||||
msgid "System agents"
|
||||
msgstr "Системные агенты"
|
||||
|
||||
@@ -6378,6 +6378,12 @@ msgstr "Сохранить проект как"
|
||||
msgid "Save current project as"
|
||||
msgstr "Сохранить текущий проект как"
|
||||
|
||||
msgid "Publish 3MF"
|
||||
msgstr ""
|
||||
|
||||
msgid "Export a 3MF file with the selected settings embedded"
|
||||
msgstr ""
|
||||
|
||||
msgid "Import 3MF/STL/STEP/SVG/OBJ/AMF"
|
||||
msgstr "Импорт 3MF/STL/STEP/SVG/OBJ/AMF"
|
||||
|
||||
@@ -8212,6 +8218,12 @@ msgstr "Во избежание повреждения принтера убед
|
||||
msgid "Customized Preset"
|
||||
msgstr "Пользовательский профиль"
|
||||
|
||||
msgid "Some published settings could not be applied:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Some filament slots were changed:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Component name(s) inside step file not in UTF-8 format!"
|
||||
msgstr "Имена компонентов внутри файла STEP не в формате UTF-8."
|
||||
|
||||
@@ -8624,6 +8636,17 @@ msgstr "Сохранить нарезанный файл как:"
|
||||
msgid "The file %s has been sent to the printer's storage space and can be viewed on the printer."
|
||||
msgstr "Файл %s отправлен в память принтера и может быть просмотрен на нём."
|
||||
|
||||
msgid "Publish 3MF file as:"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"Failed to export the published 3MF file.\n"
|
||||
"Please check whether the folder exists online or if other programs have the file open."
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish"
|
||||
msgstr "Опубликовать"
|
||||
|
||||
msgid "The nozzle type is not set. Please set the nozzle and try again."
|
||||
msgstr "Не указан тип сопла. Укажите его и попробуйте ещё раз."
|
||||
|
||||
@@ -9770,9 +9793,6 @@ msgstr "Перейти на веб-страницу публикации мод
|
||||
msgid "Note: The preparation may take several minutes. Please be patient."
|
||||
msgstr "Примечание: подготовка может занять несколько минут. Пожалуйста, наберитесь терпения."
|
||||
|
||||
msgid "Publish"
|
||||
msgstr "Опубликовать"
|
||||
|
||||
msgid "Publish was canceled"
|
||||
msgstr "Публикация была отменена"
|
||||
|
||||
@@ -9788,6 +9808,64 @@ msgstr "Отправка данных"
|
||||
msgid "Jump to webpage"
|
||||
msgstr "Перейти на страницу"
|
||||
|
||||
msgid "Material"
|
||||
msgstr ""
|
||||
|
||||
msgid "Mixed filament"
|
||||
msgstr ""
|
||||
|
||||
msgid "Some mixed filaments rely on filaments that will not be published:"
|
||||
msgstr ""
|
||||
|
||||
#, c-format, boost-format
|
||||
msgid "Filament %d (mixed)"
|
||||
msgstr ""
|
||||
|
||||
msgid "needs"
|
||||
msgstr ""
|
||||
|
||||
msgid "not enabled"
|
||||
msgstr ""
|
||||
|
||||
msgid "material not published"
|
||||
msgstr ""
|
||||
|
||||
msgid "To publish a mixed filament, enable every filament it uses and choose Full Publish or check its Type requirement."
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish anyway"
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish 3MF..."
|
||||
msgstr ""
|
||||
|
||||
msgid "Select which settings to be published in the 3MF file"
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish 3MF Wiki"
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish 3MF Video Guide"
|
||||
msgstr ""
|
||||
|
||||
msgid "Mixed filament - published as a whole when \"Enable\" above is selected"
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish this mixed filament and enable + Full Publish its component filaments"
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish this filament slot in the 3MF file"
|
||||
msgstr ""
|
||||
|
||||
msgid "Full Publish"
|
||||
msgstr ""
|
||||
|
||||
msgid "Embed the entire filament of this slot in the 3MF file"
|
||||
msgstr ""
|
||||
|
||||
msgid "Filter non-selected"
|
||||
msgstr ""
|
||||
|
||||
#, c-format, boost-format
|
||||
msgid "Save %s as"
|
||||
msgstr "Сохранить %s как"
|
||||
@@ -21541,6 +21619,15 @@ msgstr "Это действие необратимо. Продолжить?"
|
||||
msgid "Skipping objects."
|
||||
msgstr "Исключение объектов."
|
||||
|
||||
msgid "Material Ratio"
|
||||
msgstr ""
|
||||
|
||||
msgid "Model Height"
|
||||
msgstr ""
|
||||
|
||||
msgid "Ratio"
|
||||
msgstr ""
|
||||
|
||||
msgid "Select Filament"
|
||||
msgstr "Выбрать материал"
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Orca Slicer\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-09-02 09:39-0300\n"
|
||||
"POT-Creation-Date: 2026-09-03 10:27+0800\n"
|
||||
"Language: sv\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
@@ -5387,6 +5387,10 @@ msgstr ""
|
||||
"JA -Ändra dessa inställningar och möjliggör Spiral läge automatiskt\n"
|
||||
"NEJ -Avbryt Spiral läge denna gång"
|
||||
|
||||
# AI Translated
|
||||
msgid "N/A"
|
||||
msgstr "Ej tillämpligt"
|
||||
|
||||
msgid "Printing"
|
||||
msgstr "Utskrift pågår"
|
||||
|
||||
@@ -5936,10 +5940,6 @@ msgstr "Ogiltigt mönster. Använd N, N#K eller en kommaseparerad lista med valf
|
||||
msgid "Invalid format. Expected vector format: \"%1%\""
|
||||
msgstr "Ogiltligt format. Förväntat vector format: \"%1%\""
|
||||
|
||||
# AI Translated
|
||||
msgid "N/A"
|
||||
msgstr "Ej tillämpligt"
|
||||
|
||||
# AI Translated
|
||||
msgid "System agents"
|
||||
msgstr "Systemagenter"
|
||||
@@ -6972,6 +6972,12 @@ msgstr "Spara Projekt som"
|
||||
msgid "Save current project as"
|
||||
msgstr "Spara nuvarande projekt som"
|
||||
|
||||
msgid "Publish 3MF"
|
||||
msgstr ""
|
||||
|
||||
msgid "Export a 3MF file with the selected settings embedded"
|
||||
msgstr ""
|
||||
|
||||
msgid "Import 3MF/STL/STEP/SVG/OBJ/AMF"
|
||||
msgstr "Importera 3MF/STL/STEP/SVG/OBJ/AMF"
|
||||
|
||||
@@ -8996,6 +9002,12 @@ msgstr "Bekräfta att G-koderna i dessa inställningar är säkra för att förh
|
||||
msgid "Customized Preset"
|
||||
msgstr "Anpassad inställning"
|
||||
|
||||
msgid "Some published settings could not be applied:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Some filament slots were changed:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Component name(s) inside step file not in UTF-8 format!"
|
||||
msgstr "Komponent namnet i STEP filen är inte UTF-8 format!"
|
||||
|
||||
@@ -9432,6 +9444,17 @@ msgstr "Spara beredningen som:"
|
||||
msgid "The file %s has been sent to the printer's storage space and can be viewed on the printer."
|
||||
msgstr "Filen %s har skickats till skrivarens lagringsutrymme och kan visas på skrivaren."
|
||||
|
||||
msgid "Publish 3MF file as:"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"Failed to export the published 3MF file.\n"
|
||||
"Please check whether the folder exists online or if other programs have the file open."
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish"
|
||||
msgstr "Publicera"
|
||||
|
||||
# AI Translated
|
||||
msgid "The nozzle type is not set. Please set the nozzle and try again."
|
||||
msgstr "Nozzeltypen är inte angiven. Ange nozzeln och försök igen."
|
||||
@@ -10740,9 +10763,6 @@ msgstr "Växla till modell publicerings hemsidan"
|
||||
msgid "Note: The preparation may take several minutes. Please be patient."
|
||||
msgstr "Notera: Förberedelserna kan ta flera minuter. Vänligen vänta."
|
||||
|
||||
msgid "Publish"
|
||||
msgstr "Publicera"
|
||||
|
||||
msgid "Publish was canceled"
|
||||
msgstr "Publiceringen avbröts"
|
||||
|
||||
@@ -10759,6 +10779,64 @@ msgstr "Laddar upp data"
|
||||
msgid "Jump to webpage"
|
||||
msgstr "Växla till hemsidan"
|
||||
|
||||
msgid "Material"
|
||||
msgstr ""
|
||||
|
||||
msgid "Mixed filament"
|
||||
msgstr ""
|
||||
|
||||
msgid "Some mixed filaments rely on filaments that will not be published:"
|
||||
msgstr ""
|
||||
|
||||
#, c-format, boost-format
|
||||
msgid "Filament %d (mixed)"
|
||||
msgstr ""
|
||||
|
||||
msgid "needs"
|
||||
msgstr ""
|
||||
|
||||
msgid "not enabled"
|
||||
msgstr ""
|
||||
|
||||
msgid "material not published"
|
||||
msgstr ""
|
||||
|
||||
msgid "To publish a mixed filament, enable every filament it uses and choose Full Publish or check its Type requirement."
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish anyway"
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish 3MF..."
|
||||
msgstr ""
|
||||
|
||||
msgid "Select which settings to be published in the 3MF file"
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish 3MF Wiki"
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish 3MF Video Guide"
|
||||
msgstr ""
|
||||
|
||||
msgid "Mixed filament - published as a whole when \"Enable\" above is selected"
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish this mixed filament and enable + Full Publish its component filaments"
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish this filament slot in the 3MF file"
|
||||
msgstr ""
|
||||
|
||||
msgid "Full Publish"
|
||||
msgstr ""
|
||||
|
||||
msgid "Embed the entire filament of this slot in the 3MF file"
|
||||
msgstr ""
|
||||
|
||||
msgid "Filter non-selected"
|
||||
msgstr ""
|
||||
|
||||
#, c-format, boost-format
|
||||
msgid "Save %s as"
|
||||
msgstr "Spara %s som"
|
||||
@@ -23513,6 +23591,15 @@ msgstr "Åtgärden kan inte ångras. Vill du fortsätta?"
|
||||
msgid "Skipping objects."
|
||||
msgstr "Hoppar över objekt."
|
||||
|
||||
msgid "Material Ratio"
|
||||
msgstr ""
|
||||
|
||||
msgid "Model Height"
|
||||
msgstr ""
|
||||
|
||||
msgid "Ratio"
|
||||
msgstr ""
|
||||
|
||||
msgid "Select Filament"
|
||||
msgstr "Välj filament"
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Orca Slicer\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-09-02 09:39-0300\n"
|
||||
"POT-Creation-Date: 2026-09-03 10:27+0800\n"
|
||||
"PO-Revision-Date: 2026-06-19 13:40+0700\n"
|
||||
"Last-Translator: Icezaza\n"
|
||||
"Language-Team: Thai\n"
|
||||
@@ -4887,6 +4887,9 @@ msgstr ""
|
||||
"ใช่ - เปลี่ยนการตั้งค่าเหล่านี้และเปิดใช้งานโหมดเกลียวโดยอัตโนมัติ\n"
|
||||
"ไม่ - เลิกใช้โหมดเกลียวในครั้งนี้"
|
||||
|
||||
msgid "N/A"
|
||||
msgstr "ไม่มี"
|
||||
|
||||
msgid "Printing"
|
||||
msgstr "กำลังพิมพ์"
|
||||
|
||||
@@ -5346,9 +5349,6 @@ msgstr "รูปแบบไม่ถูกต้อง ใช้ N, N#K หร
|
||||
msgid "Invalid format. Expected vector format: \"%1%\""
|
||||
msgstr "รูปแบบไม่ถูกต้อง รูปแบบเวกเตอร์ที่ต้องการ: \"%1%\""
|
||||
|
||||
msgid "N/A"
|
||||
msgstr "ไม่มี"
|
||||
|
||||
# AI Translated
|
||||
msgid "System agents"
|
||||
msgstr "เอเจนต์ระบบ"
|
||||
@@ -6304,6 +6304,12 @@ msgstr "บันทึกโปรเจกต์เป็น"
|
||||
msgid "Save current project as"
|
||||
msgstr "บันทึกโครงการปัจจุบันเป็น"
|
||||
|
||||
msgid "Publish 3MF"
|
||||
msgstr ""
|
||||
|
||||
msgid "Export a 3MF file with the selected settings embedded"
|
||||
msgstr ""
|
||||
|
||||
msgid "Import 3MF/STL/STEP/SVG/OBJ/AMF"
|
||||
msgstr "นำเข้า 3MF/STL/STEP/SVG/OBJ/AMF"
|
||||
|
||||
@@ -8118,6 +8124,12 @@ msgstr "โปรดยืนยันว่ารหัส G ภายในค
|
||||
msgid "Customized Preset"
|
||||
msgstr "ค่าที่ตั้งไว้ล่วงหน้าที่กำหนดเอง"
|
||||
|
||||
msgid "Some published settings could not be applied:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Some filament slots were changed:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Component name(s) inside step file not in UTF-8 format!"
|
||||
msgstr "ชื่อของส่วนประกอบภายในไฟล์ STEP ไม่ใช่รูปแบบ UTF-8!"
|
||||
|
||||
@@ -8525,6 +8537,17 @@ msgstr "บันทึกไฟล์ที่สไลซ์เป็น:"
|
||||
msgid "The file %s has been sent to the printer's storage space and can be viewed on the printer."
|
||||
msgstr "ไฟล์ %s ถูกส่งไปยังพื้นที่เก็บข้อมูลของเครื่องพิมพ์แล้ว และสามารถดูได้บนเครื่องพิมพ์"
|
||||
|
||||
msgid "Publish 3MF file as:"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"Failed to export the published 3MF file.\n"
|
||||
"Please check whether the folder exists online or if other programs have the file open."
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish"
|
||||
msgstr "เผยแพร่"
|
||||
|
||||
msgid "The nozzle type is not set. Please set the nozzle and try again."
|
||||
msgstr "ไม่ได้ตั้งค่าประเภทหัวฉีด โปรดตั้งหัวฉีดแล้วลองอีกครั้ง"
|
||||
|
||||
@@ -9667,9 +9690,6 @@ msgstr "ข้ามไปที่โมเดลเผยแพร่หน้
|
||||
msgid "Note: The preparation may take several minutes. Please be patient."
|
||||
msgstr "หมายเหตุ: การเตรียมการอาจใช้เวลาหลายนาที กรุณาอดทน."
|
||||
|
||||
msgid "Publish"
|
||||
msgstr "เผยแพร่"
|
||||
|
||||
msgid "Publish was canceled"
|
||||
msgstr "การเผยแพร่ถูกยกเลิก"
|
||||
|
||||
@@ -9685,6 +9705,64 @@ msgstr "กำลังอัพโหลดข้อมูล"
|
||||
msgid "Jump to webpage"
|
||||
msgstr "ข้ามไปที่หน้าเว็บ"
|
||||
|
||||
msgid "Material"
|
||||
msgstr ""
|
||||
|
||||
msgid "Mixed filament"
|
||||
msgstr ""
|
||||
|
||||
msgid "Some mixed filaments rely on filaments that will not be published:"
|
||||
msgstr ""
|
||||
|
||||
#, c-format, boost-format
|
||||
msgid "Filament %d (mixed)"
|
||||
msgstr ""
|
||||
|
||||
msgid "needs"
|
||||
msgstr ""
|
||||
|
||||
msgid "not enabled"
|
||||
msgstr ""
|
||||
|
||||
msgid "material not published"
|
||||
msgstr ""
|
||||
|
||||
msgid "To publish a mixed filament, enable every filament it uses and choose Full Publish or check its Type requirement."
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish anyway"
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish 3MF..."
|
||||
msgstr ""
|
||||
|
||||
msgid "Select which settings to be published in the 3MF file"
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish 3MF Wiki"
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish 3MF Video Guide"
|
||||
msgstr ""
|
||||
|
||||
msgid "Mixed filament - published as a whole when \"Enable\" above is selected"
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish this mixed filament and enable + Full Publish its component filaments"
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish this filament slot in the 3MF file"
|
||||
msgstr ""
|
||||
|
||||
msgid "Full Publish"
|
||||
msgstr ""
|
||||
|
||||
msgid "Embed the entire filament of this slot in the 3MF file"
|
||||
msgstr ""
|
||||
|
||||
msgid "Filter non-selected"
|
||||
msgstr ""
|
||||
|
||||
#, c-format, boost-format
|
||||
msgid "Save %s as"
|
||||
msgstr "บันทึก %s เป็น"
|
||||
@@ -20891,6 +20969,15 @@ msgstr "การทำงานนี้ไม่สามารถย้อน
|
||||
msgid "Skipping objects."
|
||||
msgstr "ข้ามวัตถุ"
|
||||
|
||||
msgid "Material Ratio"
|
||||
msgstr ""
|
||||
|
||||
msgid "Model Height"
|
||||
msgstr ""
|
||||
|
||||
msgid "Ratio"
|
||||
msgstr ""
|
||||
|
||||
msgid "Select Filament"
|
||||
msgstr "เลือกเส้นพลาสติก"
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Orca Slicer\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-09-02 09:39-0300\n"
|
||||
"POT-Creation-Date: 2026-09-03 10:27+0800\n"
|
||||
"PO-Revision-Date: 2026-08-21 23:18+0300\n"
|
||||
"Last-Translator: GlauTech\n"
|
||||
"Language-Team: \n"
|
||||
@@ -4949,6 +4949,10 @@ msgstr ""
|
||||
"Evet - Bu ayarları değiştirin ve spiral modunu otomatik olarak etkinleştirin\n"
|
||||
"Hayır - Bu sefer spiral modunu kullanmaktan vazgeçin"
|
||||
|
||||
# AI Translated
|
||||
msgid "N/A"
|
||||
msgstr "Yok"
|
||||
|
||||
msgid "Printing"
|
||||
msgstr "Baskı"
|
||||
|
||||
@@ -5408,10 +5412,6 @@ msgstr "Geçersiz kalıp. N, N#K veya giriş başına isteğe bağlı #K ile vir
|
||||
msgid "Invalid format. Expected vector format: \"%1%\""
|
||||
msgstr "Geçersiz format. Beklenen vektör formatı: \"%1%\""
|
||||
|
||||
# AI Translated
|
||||
msgid "N/A"
|
||||
msgstr "Yok"
|
||||
|
||||
# AI Translated
|
||||
msgid "System agents"
|
||||
msgstr "Sistem aracıları"
|
||||
@@ -6372,6 +6372,12 @@ msgstr "Projeyi farklı kaydet"
|
||||
msgid "Save current project as"
|
||||
msgstr "Mevcut projeyi farklı kaydet"
|
||||
|
||||
msgid "Publish 3MF"
|
||||
msgstr ""
|
||||
|
||||
msgid "Export a 3MF file with the selected settings embedded"
|
||||
msgstr ""
|
||||
|
||||
msgid "Import 3MF/STL/STEP/SVG/OBJ/AMF"
|
||||
msgstr "3MF/STL/STEP/SVG/OBJ/AMF'yi içe aktar"
|
||||
|
||||
@@ -8215,6 +8221,12 @@ msgstr "Lütfen bu ön ayarlar içindeki G-code'larının makineye herhangi bir
|
||||
msgid "Customized Preset"
|
||||
msgstr "Özel Ayar"
|
||||
|
||||
msgid "Some published settings could not be applied:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Some filament slots were changed:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Component name(s) inside step file not in UTF-8 format!"
|
||||
msgstr "Step dosyasındaki bileşenlerin adı UTF-8 formatında değil!"
|
||||
|
||||
@@ -8624,6 +8636,17 @@ msgstr "Dilimlenmiş dosyayı şu şekilde kaydedin:"
|
||||
msgid "The file %s has been sent to the printer's storage space and can be viewed on the printer."
|
||||
msgstr "%s dosyası yazıcının depolama alanına gönderildi ve yazıcıda görüntülenebiliyor."
|
||||
|
||||
msgid "Publish 3MF file as:"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"Failed to export the published 3MF file.\n"
|
||||
"Please check whether the folder exists online or if other programs have the file open."
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish"
|
||||
msgstr "Yayınla"
|
||||
|
||||
msgid "The nozzle type is not set. Please set the nozzle and try again."
|
||||
msgstr "Nozul tipi ayarlanmamış. Lütfen nozulu ayarlayın ve tekrar deneyin."
|
||||
|
||||
@@ -9811,9 +9834,6 @@ msgstr "Model yayınlama web sayfasına git"
|
||||
msgid "Note: The preparation may take several minutes. Please be patient."
|
||||
msgstr "Not: Hazırlık birkaç dakika sürebilir. Lütfen sabırlı olun."
|
||||
|
||||
msgid "Publish"
|
||||
msgstr "Yayınla"
|
||||
|
||||
msgid "Publish was canceled"
|
||||
msgstr "Yayınlama iptal edildi"
|
||||
|
||||
@@ -9829,6 +9849,64 @@ msgstr "Veriler yükleniyor"
|
||||
msgid "Jump to webpage"
|
||||
msgstr "Web sayfasına atla"
|
||||
|
||||
msgid "Material"
|
||||
msgstr ""
|
||||
|
||||
msgid "Mixed filament"
|
||||
msgstr ""
|
||||
|
||||
msgid "Some mixed filaments rely on filaments that will not be published:"
|
||||
msgstr ""
|
||||
|
||||
#, c-format, boost-format
|
||||
msgid "Filament %d (mixed)"
|
||||
msgstr ""
|
||||
|
||||
msgid "needs"
|
||||
msgstr ""
|
||||
|
||||
msgid "not enabled"
|
||||
msgstr ""
|
||||
|
||||
msgid "material not published"
|
||||
msgstr ""
|
||||
|
||||
msgid "To publish a mixed filament, enable every filament it uses and choose Full Publish or check its Type requirement."
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish anyway"
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish 3MF..."
|
||||
msgstr ""
|
||||
|
||||
msgid "Select which settings to be published in the 3MF file"
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish 3MF Wiki"
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish 3MF Video Guide"
|
||||
msgstr ""
|
||||
|
||||
msgid "Mixed filament - published as a whole when \"Enable\" above is selected"
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish this mixed filament and enable + Full Publish its component filaments"
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish this filament slot in the 3MF file"
|
||||
msgstr ""
|
||||
|
||||
msgid "Full Publish"
|
||||
msgstr ""
|
||||
|
||||
msgid "Embed the entire filament of this slot in the 3MF file"
|
||||
msgstr ""
|
||||
|
||||
msgid "Filter non-selected"
|
||||
msgstr ""
|
||||
|
||||
#, c-format, boost-format
|
||||
msgid "Save %s as"
|
||||
msgstr "%s'yi farklı kaydet"
|
||||
@@ -21318,6 +21396,15 @@ msgstr "Bu eylem geri alınamaz. Devam etmek?"
|
||||
msgid "Skipping objects."
|
||||
msgstr "Nesneleri atlama."
|
||||
|
||||
msgid "Material Ratio"
|
||||
msgstr ""
|
||||
|
||||
msgid "Model Height"
|
||||
msgstr ""
|
||||
|
||||
msgid "Ratio"
|
||||
msgstr ""
|
||||
|
||||
msgid "Select Filament"
|
||||
msgstr "Filament Seçin"
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: orcaslicerua\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-09-02 09:39-0300\n"
|
||||
"POT-Creation-Date: 2026-09-03 10:27+0800\n"
|
||||
"PO-Revision-Date: 2026-07-17 16:25+0300\n"
|
||||
"Last-Translator: Andrij Mizyk <andm1zyk@proton.me>\n"
|
||||
"Language-Team: Ukrainian\n"
|
||||
@@ -4887,6 +4887,9 @@ msgstr ""
|
||||
"Так – змінити ці налаштування та автоматично включити режим спіральна ваза\n"
|
||||
"Ні - цього разу відмовитися від використання режиму спіральна ваза"
|
||||
|
||||
msgid "N/A"
|
||||
msgstr "Н/Д"
|
||||
|
||||
msgid "Printing"
|
||||
msgstr "Друк"
|
||||
|
||||
@@ -5358,9 +5361,6 @@ msgstr "Некоректний шаблон. Використовуйте N, N#K
|
||||
msgid "Invalid format. Expected vector format: \"%1%\""
|
||||
msgstr "Невірний формат. Очікуваний векторний формат: \"%1%\""
|
||||
|
||||
msgid "N/A"
|
||||
msgstr "Н/Д"
|
||||
|
||||
# AI Translated
|
||||
msgid "System agents"
|
||||
msgstr "Системні агенти"
|
||||
@@ -6341,6 +6341,12 @@ msgstr "Зберегти проєкт як"
|
||||
msgid "Save current project as"
|
||||
msgstr "Зберегти поточний проєкт як"
|
||||
|
||||
msgid "Publish 3MF"
|
||||
msgstr ""
|
||||
|
||||
msgid "Export a 3MF file with the selected settings embedded"
|
||||
msgstr ""
|
||||
|
||||
msgid "Import 3MF/STL/STEP/SVG/OBJ/AMF"
|
||||
msgstr "Імпорт 3MF/STL/STEP/SVG/OBJ/AMF"
|
||||
|
||||
@@ -8223,6 +8229,12 @@ msgstr "Будь ласка, підтвердьте, що G-коди в цих
|
||||
msgid "Customized Preset"
|
||||
msgstr "Пристосований пресет"
|
||||
|
||||
msgid "Some published settings could not be applied:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Some filament slots were changed:"
|
||||
msgstr ""
|
||||
|
||||
# AI Translated
|
||||
msgid "Component name(s) inside step file not in UTF-8 format!"
|
||||
msgstr "Назви компонентів усередині файлу STEP не у форматі UTF-8!"
|
||||
@@ -8635,6 +8647,17 @@ msgstr "Зберегти нарізаний файл як:"
|
||||
msgid "The file %s has been sent to the printer's storage space and can be viewed on the printer."
|
||||
msgstr "Файл %s надіслано до памʼяті принтера та доступний для перегляду на принтері."
|
||||
|
||||
msgid "Publish 3MF file as:"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"Failed to export the published 3MF file.\n"
|
||||
"Please check whether the folder exists online or if other programs have the file open."
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish"
|
||||
msgstr "Публікувати"
|
||||
|
||||
msgid "The nozzle type is not set. Please set the nozzle and try again."
|
||||
msgstr "Тип сопла не встановлений. Будь ласка, оберіть сопло та спробуйте ще раз."
|
||||
|
||||
@@ -9814,9 +9837,6 @@ msgstr "Перейти на веб-сторінку публікації мод
|
||||
msgid "Note: The preparation may take several minutes. Please be patient."
|
||||
msgstr "Примітка. Підготовка може тривати кілька хвилин. Будь ласка, будьте терплячі."
|
||||
|
||||
msgid "Publish"
|
||||
msgstr "Публікувати"
|
||||
|
||||
msgid "Publish was canceled"
|
||||
msgstr "Публікація скасована"
|
||||
|
||||
@@ -9832,6 +9852,64 @@ msgstr "Відвантаження даних"
|
||||
msgid "Jump to webpage"
|
||||
msgstr "Перейти на вебсторінку"
|
||||
|
||||
msgid "Material"
|
||||
msgstr ""
|
||||
|
||||
msgid "Mixed filament"
|
||||
msgstr ""
|
||||
|
||||
msgid "Some mixed filaments rely on filaments that will not be published:"
|
||||
msgstr ""
|
||||
|
||||
#, c-format, boost-format
|
||||
msgid "Filament %d (mixed)"
|
||||
msgstr ""
|
||||
|
||||
msgid "needs"
|
||||
msgstr ""
|
||||
|
||||
msgid "not enabled"
|
||||
msgstr ""
|
||||
|
||||
msgid "material not published"
|
||||
msgstr ""
|
||||
|
||||
msgid "To publish a mixed filament, enable every filament it uses and choose Full Publish or check its Type requirement."
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish anyway"
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish 3MF..."
|
||||
msgstr ""
|
||||
|
||||
msgid "Select which settings to be published in the 3MF file"
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish 3MF Wiki"
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish 3MF Video Guide"
|
||||
msgstr ""
|
||||
|
||||
msgid "Mixed filament - published as a whole when \"Enable\" above is selected"
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish this mixed filament and enable + Full Publish its component filaments"
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish this filament slot in the 3MF file"
|
||||
msgstr ""
|
||||
|
||||
msgid "Full Publish"
|
||||
msgstr ""
|
||||
|
||||
msgid "Embed the entire filament of this slot in the 3MF file"
|
||||
msgstr ""
|
||||
|
||||
msgid "Filter non-selected"
|
||||
msgstr ""
|
||||
|
||||
#, c-format, boost-format
|
||||
msgid "Save %s as"
|
||||
msgstr "Зберегти %s як"
|
||||
@@ -21495,6 +21573,15 @@ msgstr "Дію не можливо завершити. Продовжити?"
|
||||
msgid "Skipping objects."
|
||||
msgstr "Пропускання обʼєктів."
|
||||
|
||||
msgid "Material Ratio"
|
||||
msgstr ""
|
||||
|
||||
msgid "Model Height"
|
||||
msgstr ""
|
||||
|
||||
msgid "Ratio"
|
||||
msgstr ""
|
||||
|
||||
msgid "Select Filament"
|
||||
msgstr "Вибрати філамент"
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Orca Slicer\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-09-02 09:39-0300\n"
|
||||
"POT-Creation-Date: 2026-09-03 10:27+0800\n"
|
||||
"PO-Revision-Date: 2025-10-02 17:43+0700\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: hainguyen.ts13@gmail.com\n"
|
||||
@@ -5143,6 +5143,10 @@ msgstr ""
|
||||
"Yes - Thay đổi các cài đặt này và bật chế độ spiral tự động\n"
|
||||
"No - Từ bỏ dùng chế độ spiral lần này"
|
||||
|
||||
# AI Translated
|
||||
msgid "N/A"
|
||||
msgstr "N/A"
|
||||
|
||||
msgid "Printing"
|
||||
msgstr "Đang in"
|
||||
|
||||
@@ -5661,10 +5665,6 @@ msgstr "Mẫu không hợp lệ. Dùng N, N#K, hoặc danh sách phân cách d
|
||||
msgid "Invalid format. Expected vector format: \"%1%\""
|
||||
msgstr "Định dạng không hợp lệ. Mong đợi định dạng vector: \"%1%\""
|
||||
|
||||
# AI Translated
|
||||
msgid "N/A"
|
||||
msgstr "N/A"
|
||||
|
||||
# AI Translated
|
||||
msgid "System agents"
|
||||
msgstr "Tác nhân hệ thống"
|
||||
@@ -6689,6 +6689,12 @@ msgstr "Lưu dự án thành"
|
||||
msgid "Save current project as"
|
||||
msgstr "Lưu dự án hiện tại thành"
|
||||
|
||||
msgid "Publish 3MF"
|
||||
msgstr ""
|
||||
|
||||
msgid "Export a 3MF file with the selected settings embedded"
|
||||
msgstr ""
|
||||
|
||||
msgid "Import 3MF/STL/STEP/SVG/OBJ/AMF"
|
||||
msgstr "Nhập 3MF/STL/STEP/SVG/OBJ/AMF"
|
||||
|
||||
@@ -8629,6 +8635,12 @@ msgstr "Vui lòng xác nhận G-code trong các preset này an toàn để ngăn
|
||||
msgid "Customized Preset"
|
||||
msgstr "Preset tùy chỉnh"
|
||||
|
||||
msgid "Some published settings could not be applied:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Some filament slots were changed:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Component name(s) inside step file not in UTF-8 format!"
|
||||
msgstr "Tên của các thành phần bên trong file STEP không phải định dạng UTF-8!"
|
||||
|
||||
@@ -9056,6 +9068,17 @@ msgstr "Lưu file đã slice dưới dạng:"
|
||||
msgid "The file %s has been sent to the printer's storage space and can be viewed on the printer."
|
||||
msgstr "File %s đã được gửi đến không gian lưu trữ của máy in và có thể được xem trên máy in."
|
||||
|
||||
msgid "Publish 3MF file as:"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"Failed to export the published 3MF file.\n"
|
||||
"Please check whether the folder exists online or if other programs have the file open."
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish"
|
||||
msgstr "Xuất bản"
|
||||
|
||||
# AI Translated
|
||||
msgid "The nozzle type is not set. Please set the nozzle and try again."
|
||||
msgstr "Chưa đặt loại đầu phun. Vui lòng đặt đầu phun rồi thử lại."
|
||||
@@ -10326,9 +10349,6 @@ msgstr "Chuyển đến trang web xuất bản model"
|
||||
msgid "Note: The preparation may take several minutes. Please be patient."
|
||||
msgstr "Lưu ý: Chuẩn bị có thể mất vài phút. Vui lòng kiên nhẫn."
|
||||
|
||||
msgid "Publish"
|
||||
msgstr "Xuất bản"
|
||||
|
||||
msgid "Publish was canceled"
|
||||
msgstr "Xuất bản đã bị hủy"
|
||||
|
||||
@@ -10345,6 +10365,64 @@ msgstr "Đang tải dữ liệu lên"
|
||||
msgid "Jump to webpage"
|
||||
msgstr "Chuyển đến trang web"
|
||||
|
||||
msgid "Material"
|
||||
msgstr ""
|
||||
|
||||
msgid "Mixed filament"
|
||||
msgstr ""
|
||||
|
||||
msgid "Some mixed filaments rely on filaments that will not be published:"
|
||||
msgstr ""
|
||||
|
||||
#, c-format, boost-format
|
||||
msgid "Filament %d (mixed)"
|
||||
msgstr ""
|
||||
|
||||
msgid "needs"
|
||||
msgstr ""
|
||||
|
||||
msgid "not enabled"
|
||||
msgstr ""
|
||||
|
||||
msgid "material not published"
|
||||
msgstr ""
|
||||
|
||||
msgid "To publish a mixed filament, enable every filament it uses and choose Full Publish or check its Type requirement."
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish anyway"
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish 3MF..."
|
||||
msgstr ""
|
||||
|
||||
msgid "Select which settings to be published in the 3MF file"
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish 3MF Wiki"
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish 3MF Video Guide"
|
||||
msgstr ""
|
||||
|
||||
msgid "Mixed filament - published as a whole when \"Enable\" above is selected"
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish this mixed filament and enable + Full Publish its component filaments"
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish this filament slot in the 3MF file"
|
||||
msgstr ""
|
||||
|
||||
msgid "Full Publish"
|
||||
msgstr ""
|
||||
|
||||
msgid "Embed the entire filament of this slot in the 3MF file"
|
||||
msgstr ""
|
||||
|
||||
msgid "Filter non-selected"
|
||||
msgstr ""
|
||||
|
||||
#, c-format, boost-format
|
||||
msgid "Save %s as"
|
||||
msgstr "Lưu %s dưới dạng"
|
||||
@@ -22165,6 +22243,15 @@ msgstr "Thao tác này không thể hoàn tác. Tiếp tục?"
|
||||
msgid "Skipping objects."
|
||||
msgstr "Đang bỏ qua các vật thể."
|
||||
|
||||
msgid "Material Ratio"
|
||||
msgstr ""
|
||||
|
||||
msgid "Model Height"
|
||||
msgstr ""
|
||||
|
||||
msgid "Ratio"
|
||||
msgstr ""
|
||||
|
||||
# AI Translated
|
||||
msgid "Select Filament"
|
||||
msgstr "Chọn filament"
|
||||
|
||||
@@ -6,7 +6,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Slic3rPE\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-09-02 09:39-0300\n"
|
||||
"POT-Creation-Date: 2026-09-03 10:27+0800\n"
|
||||
"PO-Revision-Date: 2026-06-11 12:37-0300\n"
|
||||
"Last-Translator: Handle <mail@bysb.net>\n"
|
||||
"Language-Team: \n"
|
||||
@@ -4743,6 +4743,9 @@ msgstr ""
|
||||
"是 - 自动调整这些设置并开启旋转模式\n"
|
||||
"否 - 暂不使用旋转模式"
|
||||
|
||||
msgid "N/A"
|
||||
msgstr "不适用"
|
||||
|
||||
msgid "Printing"
|
||||
msgstr "打印中"
|
||||
|
||||
@@ -5202,9 +5205,6 @@ msgstr "无效的模式。请使用 N、N#K 或逗号分隔的列表(每个条
|
||||
msgid "Invalid format. Expected vector format: \"%1%\""
|
||||
msgstr "无效格式,应该是\"%1%\"这种数组格式"
|
||||
|
||||
msgid "N/A"
|
||||
msgstr "不适用"
|
||||
|
||||
# AI Translated
|
||||
msgid "System agents"
|
||||
msgstr "系统代理"
|
||||
@@ -6159,6 +6159,12 @@ msgstr "项目另存为"
|
||||
msgid "Save current project as"
|
||||
msgstr "项目另存为"
|
||||
|
||||
msgid "Publish 3MF"
|
||||
msgstr ""
|
||||
|
||||
msgid "Export a 3MF file with the selected settings embedded"
|
||||
msgstr ""
|
||||
|
||||
msgid "Import 3MF/STL/STEP/SVG/OBJ/AMF"
|
||||
msgstr "导入 3MF/STL/STEP/SVG/OBJ/AMF"
|
||||
|
||||
@@ -7949,6 +7955,12 @@ msgstr "请确认这些预设中的G-codes是否安全,以防止对机器造
|
||||
msgid "Customized Preset"
|
||||
msgstr "自定义的预设"
|
||||
|
||||
msgid "Some published settings could not be applied:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Some filament slots were changed:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Component name(s) inside step file not in UTF-8 format!"
|
||||
msgstr "STEP 文件中的部件名称不是 UTF-8 格式!"
|
||||
|
||||
@@ -8354,6 +8366,17 @@ msgstr "切片文件另存为:"
|
||||
msgid "The file %s has been sent to the printer's storage space and can be viewed on the printer."
|
||||
msgstr "文件%s已经发送到打印机的存储空间,可以在打印机上浏览。"
|
||||
|
||||
msgid "Publish 3MF file as:"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"Failed to export the published 3MF file.\n"
|
||||
"Please check whether the folder exists online or if other programs have the file open."
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish"
|
||||
msgstr "发布"
|
||||
|
||||
msgid "The nozzle type is not set. Please set the nozzle and try again."
|
||||
msgstr "未设置喷嘴类型。请设置喷嘴并重试。"
|
||||
|
||||
@@ -9489,9 +9512,6 @@ msgstr "跳转到发布页面"
|
||||
msgid "Note: The preparation may take several minutes. Please be patient."
|
||||
msgstr "提示:发布前需要一些准备时间,请耐心等待。"
|
||||
|
||||
msgid "Publish"
|
||||
msgstr "发布"
|
||||
|
||||
msgid "Publish was canceled"
|
||||
msgstr "发布已取消"
|
||||
|
||||
@@ -9507,6 +9527,64 @@ msgstr "正在上传数据"
|
||||
msgid "Jump to webpage"
|
||||
msgstr "跳转到网页"
|
||||
|
||||
msgid "Material"
|
||||
msgstr ""
|
||||
|
||||
msgid "Mixed filament"
|
||||
msgstr ""
|
||||
|
||||
msgid "Some mixed filaments rely on filaments that will not be published:"
|
||||
msgstr ""
|
||||
|
||||
#, c-format, boost-format
|
||||
msgid "Filament %d (mixed)"
|
||||
msgstr ""
|
||||
|
||||
msgid "needs"
|
||||
msgstr ""
|
||||
|
||||
msgid "not enabled"
|
||||
msgstr ""
|
||||
|
||||
msgid "material not published"
|
||||
msgstr ""
|
||||
|
||||
msgid "To publish a mixed filament, enable every filament it uses and choose Full Publish or check its Type requirement."
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish anyway"
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish 3MF..."
|
||||
msgstr ""
|
||||
|
||||
msgid "Select which settings to be published in the 3MF file"
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish 3MF Wiki"
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish 3MF Video Guide"
|
||||
msgstr ""
|
||||
|
||||
msgid "Mixed filament - published as a whole when \"Enable\" above is selected"
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish this mixed filament and enable + Full Publish its component filaments"
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish this filament slot in the 3MF file"
|
||||
msgstr ""
|
||||
|
||||
msgid "Full Publish"
|
||||
msgstr ""
|
||||
|
||||
msgid "Embed the entire filament of this slot in the 3MF file"
|
||||
msgstr ""
|
||||
|
||||
msgid "Filter non-selected"
|
||||
msgstr ""
|
||||
|
||||
#, c-format, boost-format
|
||||
msgid "Save %s as"
|
||||
msgstr "另存%s为"
|
||||
@@ -20639,6 +20717,15 @@ msgstr "此操作无法撤消。继续?"
|
||||
msgid "Skipping objects."
|
||||
msgstr "跳过对象。"
|
||||
|
||||
msgid "Material Ratio"
|
||||
msgstr ""
|
||||
|
||||
msgid "Model Height"
|
||||
msgstr ""
|
||||
|
||||
msgid "Ratio"
|
||||
msgstr ""
|
||||
|
||||
msgid "Select Filament"
|
||||
msgstr "选择耗材"
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Orca Slicer\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-09-02 09:39-0300\n"
|
||||
"POT-Creation-Date: 2026-09-03 10:27+0800\n"
|
||||
"PO-Revision-Date: 2025-11-28 13:48-0600\n"
|
||||
"Last-Translator: tntchn <15895303+tntchn@users.noreply.github.com>\n"
|
||||
"Language-Team: \n"
|
||||
@@ -4872,6 +4872,9 @@ msgstr ""
|
||||
"是 - 自動調整這些設定並啟用花瓶模式\n"
|
||||
"否 - 不使用花瓶模式"
|
||||
|
||||
msgid "N/A"
|
||||
msgstr "不適用"
|
||||
|
||||
msgid "Printing"
|
||||
msgstr "列印中"
|
||||
|
||||
@@ -5331,9 +5334,6 @@ msgstr "無效的格式。請使用 N、N#K 或逗號分隔的清單,每個項
|
||||
msgid "Invalid format. Expected vector format: \"%1%\""
|
||||
msgstr "無效格式,應該是「%1%」這種格式"
|
||||
|
||||
msgid "N/A"
|
||||
msgstr "不適用"
|
||||
|
||||
# AI Translated
|
||||
msgid "System agents"
|
||||
msgstr "系統代理程式"
|
||||
@@ -6289,6 +6289,12 @@ msgstr "另存專案為"
|
||||
msgid "Save current project as"
|
||||
msgstr "將目前專案另存為"
|
||||
|
||||
msgid "Publish 3MF"
|
||||
msgstr ""
|
||||
|
||||
msgid "Export a 3MF file with the selected settings embedded"
|
||||
msgstr ""
|
||||
|
||||
msgid "Import 3MF/STL/STEP/SVG/OBJ/AMF"
|
||||
msgstr "匯入 3MF/STL/STEP/SVG/OBJ/AMF"
|
||||
|
||||
@@ -8110,6 +8116,12 @@ msgstr "請確認這些預設中的 G-code 是安全的,以防止對列印裝
|
||||
msgid "Customized Preset"
|
||||
msgstr "自訂預設"
|
||||
|
||||
msgid "Some published settings could not be applied:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Some filament slots were changed:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Component name(s) inside step file not in UTF-8 format!"
|
||||
msgstr "STEP 檔案內部元件的名稱不是 UTF-8 格式!"
|
||||
|
||||
@@ -8520,6 +8532,17 @@ msgstr "切片檔案另存為:"
|
||||
msgid "The file %s has been sent to the printer's storage space and can be viewed on the printer."
|
||||
msgstr "檔案 %s 已經傳送到列印裝置的儲存空間,可以在列印裝置上瀏覽。"
|
||||
|
||||
msgid "Publish 3MF file as:"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"Failed to export the published 3MF file.\n"
|
||||
"Please check whether the folder exists online or if other programs have the file open."
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish"
|
||||
msgstr "發布"
|
||||
|
||||
msgid "The nozzle type is not set. Please set the nozzle and try again."
|
||||
msgstr "噴嘴類型尚未設定。請設定噴嘴後再試一次。"
|
||||
|
||||
@@ -9662,9 +9685,6 @@ msgstr "發布頁面"
|
||||
msgid "Note: The preparation may take several minutes. Please be patient."
|
||||
msgstr "提示:發布前需要一些準備時間,請耐心等待。"
|
||||
|
||||
msgid "Publish"
|
||||
msgstr "發布"
|
||||
|
||||
msgid "Publish was canceled"
|
||||
msgstr "發布已取消"
|
||||
|
||||
@@ -9680,6 +9700,64 @@ msgstr "正在上傳資料"
|
||||
msgid "Jump to webpage"
|
||||
msgstr "跳至網頁"
|
||||
|
||||
msgid "Material"
|
||||
msgstr ""
|
||||
|
||||
msgid "Mixed filament"
|
||||
msgstr ""
|
||||
|
||||
msgid "Some mixed filaments rely on filaments that will not be published:"
|
||||
msgstr ""
|
||||
|
||||
#, c-format, boost-format
|
||||
msgid "Filament %d (mixed)"
|
||||
msgstr ""
|
||||
|
||||
msgid "needs"
|
||||
msgstr ""
|
||||
|
||||
msgid "not enabled"
|
||||
msgstr ""
|
||||
|
||||
msgid "material not published"
|
||||
msgstr ""
|
||||
|
||||
msgid "To publish a mixed filament, enable every filament it uses and choose Full Publish or check its Type requirement."
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish anyway"
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish 3MF..."
|
||||
msgstr ""
|
||||
|
||||
msgid "Select which settings to be published in the 3MF file"
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish 3MF Wiki"
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish 3MF Video Guide"
|
||||
msgstr ""
|
||||
|
||||
msgid "Mixed filament - published as a whole when \"Enable\" above is selected"
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish this mixed filament and enable + Full Publish its component filaments"
|
||||
msgstr ""
|
||||
|
||||
msgid "Publish this filament slot in the 3MF file"
|
||||
msgstr ""
|
||||
|
||||
msgid "Full Publish"
|
||||
msgstr ""
|
||||
|
||||
msgid "Embed the entire filament of this slot in the 3MF file"
|
||||
msgstr ""
|
||||
|
||||
msgid "Filter non-selected"
|
||||
msgstr ""
|
||||
|
||||
#, c-format, boost-format
|
||||
msgid "Save %s as"
|
||||
msgstr "另存 %s 為"
|
||||
@@ -20832,6 +20910,15 @@ msgstr "此操作無法撤消。繼續?"
|
||||
msgid "Skipping objects."
|
||||
msgstr "跳過物件。"
|
||||
|
||||
msgid "Material Ratio"
|
||||
msgstr ""
|
||||
|
||||
msgid "Model Height"
|
||||
msgstr ""
|
||||
|
||||
msgid "Ratio"
|
||||
msgstr ""
|
||||
|
||||
msgid "Select Filament"
|
||||
msgstr "選擇線材"
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M1.5,8 14.5,1.5 7.5,14.5 6.3,9.7 Z" style="fill:none;stroke:gray;stroke-linecap:round;stroke-linejoin:round"/></svg>
|
||||
|
After Width: | Height: | Size: 209 B |
@@ -415,7 +415,9 @@ img.ModelThumbnail {
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
border-radius: 4px;
|
||||
background-color: inherit;
|
||||
border-color: var(--border-color);
|
||||
background-color: var(--bg-color);
|
||||
color: var(--fg-color-text);
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 200px;
|
||||
@@ -423,13 +425,17 @@ img.ModelThumbnail {
|
||||
}
|
||||
|
||||
#NoticeBar {
|
||||
background-color:#00f0d8;
|
||||
background-color: var(--main-color);
|
||||
height: 40px;
|
||||
line-height: 40px;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#NoticeBar.notice-error {
|
||||
background-color: var(--button-bg-alert);
|
||||
}
|
||||
|
||||
#NoticeContent {
|
||||
padding: 4mm 10mm;
|
||||
}
|
||||
|
||||
@@ -37,6 +37,34 @@ function HandleStudio( pVal )
|
||||
{
|
||||
HandleModelList(pVal['response']);
|
||||
}
|
||||
else if(strCmd=='check_new_printers_result')
|
||||
{
|
||||
let button = document.getElementById("CheckNewPrintersBtn");
|
||||
if (button) {
|
||||
button.style.pointerEvents = "auto";
|
||||
button.style.opacity = "1";
|
||||
}
|
||||
|
||||
let noticeBar = document.getElementById("NoticeBar");
|
||||
let noticeText = document.getElementById("NoticeText");
|
||||
let hasError = pVal.hasOwnProperty("error");
|
||||
noticeBar.classList.toggle("notice-error", hasError);
|
||||
if (hasError) {
|
||||
noticeBar.textContent = "Error";
|
||||
noticeText.textContent = pVal["error"];
|
||||
} else if (pVal["vendors"] && pVal["vendors"].length > 0) {
|
||||
noticeBar.textContent = "New printers found";
|
||||
noticeText.textContent = "New printer vendors installed: " + pVal["vendors"].join(", ");
|
||||
} else if (pVal["declined"]) {
|
||||
noticeBar.textContent = "Information";
|
||||
noticeText.textContent = "New printer vendors were found, but installation was cancelled.";
|
||||
} else {
|
||||
noticeBar.textContent = "Information";
|
||||
noticeText.textContent = "No new printers found.";
|
||||
}
|
||||
|
||||
ShowNotice(1);
|
||||
}
|
||||
}
|
||||
|
||||
function HandleModelList( pVal )
|
||||
@@ -78,9 +106,13 @@ function HandleModelList( pVal )
|
||||
}
|
||||
|
||||
//Update Nozzel Html Append
|
||||
// ORCA: HandleModelList can now be called more than once per dialog (e.g. after a new vendor
|
||||
// is installed via "check for new printers"). pModel always holds the full, current list, so
|
||||
// clear each vendor's printer area before repopulating instead of appending on top of a
|
||||
// previous render.
|
||||
for( let key in ModelHtml )
|
||||
{
|
||||
$(".OneVendorBlock[vendor='"+key+"'] .PrinterArea").append( ModelHtml[key] );
|
||||
$(".OneVendorBlock[vendor='"+key+"'] .PrinterArea").empty().append( ModelHtml[key] );
|
||||
}
|
||||
|
||||
//Update Checkbox
|
||||
@@ -343,6 +375,9 @@ function OnExit()
|
||||
let nTotal=ModelSelect.length;
|
||||
|
||||
if( nTotal==0 ) {
|
||||
let noticeBar = document.getElementById("NoticeBar");
|
||||
noticeBar.classList.add("notice-error");
|
||||
noticeBar.textContent = "Error";
|
||||
ShowNotice(1);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -36,6 +36,22 @@ function ConfirmSelect()
|
||||
}
|
||||
}
|
||||
|
||||
function CheckForNewPrinters()
|
||||
{
|
||||
var tSend={};
|
||||
tSend['sequence_id']=Math.round(new Date() / 1000);
|
||||
tSend['command']="check_for_new_printers";
|
||||
tSend['data']={};
|
||||
|
||||
var button = document.getElementById("CheckNewPrintersBtn");
|
||||
if (button) {
|
||||
button.style.pointerEvents = "none";
|
||||
button.style.opacity = "0.6";
|
||||
}
|
||||
|
||||
SendWXMessage( JSON.stringify(tSend) );
|
||||
}
|
||||
|
||||
function CreateNewPrinter()
|
||||
{
|
||||
var tSend={};
|
||||
|
||||
@@ -82,7 +82,8 @@
|
||||
</div>
|
||||
<div id="AcceptArea">
|
||||
<div class="ButtonStyleRegular ButtonTypeChoice trans" id="CreateBtn" onclick="CreateNewPrinter()">Create</div>
|
||||
<div class="ButtonStyleConfirm ButtonTypeChoice trans" tid="t39" id="AcceptBtn" onclick="ConfirmSelect()">Confirm</div>
|
||||
<div class="ButtonStyleRegular ButtonTypeChoice" id="CheckNewPrintersBtn" onclick="CheckForNewPrinters()">Check for new printers</div>
|
||||
<div class="ButtonStyleConfirm ButtonTypeChoice trans" tid="t39" id="AcceptBtn" onclick="ConfirmSelect()">Confirm</div>
|
||||
<div class="ButtonStyleRegular ButtonTypeChoice trans" tid="t38" id="PreBtn" onclick="CancelSelect()">Cancel</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -716,7 +716,8 @@ body
|
||||
background-color: #E4E4E4;
|
||||
border-radius: 8px;
|
||||
width: 184px;
|
||||
height: 184px;
|
||||
height: 184px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.FileItem img
|
||||
@@ -728,6 +729,19 @@ body
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.FileImg .FileLogoBadge
|
||||
{
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
left: 8px;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
padding: 2px;
|
||||
box-sizing: border-box;
|
||||
background-color: rgba(255,255,255,0.85);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.FileName
|
||||
{
|
||||
white-space: nowrap;
|
||||
@@ -736,6 +750,35 @@ body
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.FileNamePack
|
||||
{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.FileNamePack .FileName
|
||||
{
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.FilePublishedBadge
|
||||
{
|
||||
flex: 0 0 auto;
|
||||
margin-right: 4px;
|
||||
padding: 0 4px;
|
||||
height: 16px;
|
||||
line-height: 16px;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
color: #FFFFFF;
|
||||
background-color: #009688;
|
||||
border-radius: 2px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.FileDate
|
||||
{
|
||||
color: #A8A8A8;
|
||||
|
||||
@@ -305,15 +305,19 @@ function ShowRecentFileList( pList )
|
||||
let sImg=OneFile["image"] || sImages[sPath];
|
||||
let sTime=OneFile['time'];
|
||||
let sName=OneFile['project_name'];
|
||||
let sPublished=OneFile['published'] == '1';
|
||||
sImages[sPath] = sImg;
|
||||
|
||||
//let index=sPath.lastIndexOf('\\')>0?sPath.lastIndexOf('\\'):sPath.lastIndexOf('\/');
|
||||
//let sShortName=sPath.substring(index+1,sPath.length);
|
||||
|
||||
let sBadge=sPublished? '<span class="FilePublishedBadge">PUB</span>':'';
|
||||
let sLogoBadge=sPublished? '<img class="FileLogoBadge" src="../../images/OrcaSlicer_gradient_circle.svg" alt="" />':'';
|
||||
|
||||
let TmpHtml='<div class="FileItem" fpath="'+sPath+'" >'+
|
||||
'<a class="FileTip" title="'+sPath+'"></a>'+
|
||||
'<div class="FileImg" ><img src="'+sImg+'" onerror="this.onerror=null;this.src=\'img/d.png\';" alt="No Image" /></div>'+
|
||||
'<div class="FileName TextS1">'+sName+'</div>'+
|
||||
'<div class="FileImg" ><img src="'+sImg+'" onerror="this.onerror=null;this.src=\'img/d.png\';" alt="No Image" />'+sLogoBadge+'</div>'+
|
||||
'<div class="FileNamePack">'+sBadge+'<div class="FileName TextS1">'+sName+'</div></div>'+
|
||||
'<div class="FileDate">'+sTime+'</div>'+
|
||||
'</div>';
|
||||
|
||||
|
||||
@@ -42,6 +42,9 @@ namespace Slic3r {
|
||||
|
||||
static const std::string VERSION_CHECK_URL = "https://check-version.orcaslicer.com/latest";
|
||||
static const std::string PROFILE_UPDATE_URL = "https://check-version.orcaslicer.com/profile";
|
||||
|
||||
constexpr const char* CONFIG_ORCA_UPDATER_URL = "orca_updater_url";
|
||||
|
||||
static const std::string MODELS_STR = "models";
|
||||
|
||||
const std::string AppConfig::SECTION_FILAMENTS = "filaments";
|
||||
@@ -635,6 +638,11 @@ void AppConfig::set_defaults()
|
||||
set_bool("use_printer_agents", false);
|
||||
}
|
||||
|
||||
if (get("enable_ota").empty())
|
||||
{
|
||||
set_bool("enable_ota", false);
|
||||
}
|
||||
|
||||
// Remove legacy window positions/sizes
|
||||
erase("app", "main_frame_maximized");
|
||||
erase("app", "main_frame_pos");
|
||||
@@ -1815,7 +1823,10 @@ std::string AppConfig::version_check_url() const
|
||||
|
||||
std::string AppConfig::profile_update_url() const
|
||||
{
|
||||
return PROFILE_UPDATE_URL;
|
||||
std::string orca_updater_url = get(CONFIG_ORCA_UPDATER_URL);
|
||||
if (orca_updater_url.empty())
|
||||
return PROFILE_UPDATE_URL;
|
||||
return orca_updater_url;
|
||||
}
|
||||
|
||||
bool AppConfig::exists()
|
||||
|
||||
@@ -372,6 +372,8 @@ set(lisbslic3r_sources
|
||||
Preset.hpp
|
||||
PrincipalComponents2D.cpp
|
||||
PrincipalComponents2D.hpp
|
||||
PublishSettings.cpp
|
||||
PublishSettings.hpp
|
||||
PrintApply.cpp
|
||||
PrintBase.cpp
|
||||
PrintBase.hpp
|
||||
|
||||
@@ -642,6 +642,11 @@ bool bbs_is_valid_object_type(const std::string& type)
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
bool is_published_3mf_flag(const std::string &value)
|
||||
{
|
||||
return value == "1";
|
||||
}
|
||||
|
||||
void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
{
|
||||
if (!result) return;
|
||||
@@ -1178,6 +1183,20 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
// add backup & restore logic
|
||||
bool _load_model_from_file(std::string filename, Model& model, PlateDataPtrs& plate_data_list, std::vector<Preset*>& project_presets, DynamicPrintConfig& config, ConfigSubstitutionContext& config_substitutions, Import3mfProgressFn proFn = nullptr,
|
||||
BBLProject* project = nullptr, int plate_id = 0);
|
||||
|
||||
// A minimal published 3MF carries no slicer tags (any tag would make old receivers show
|
||||
// a baked-in, wrong "old version" popup on their geometry-only fallback), so it
|
||||
// classifies as From_Other. It is still a fully structured OrcaSlicer file though:
|
||||
// identified by its own metadata, it keeps BBS-grade geometry handling (no instance
|
||||
// splitting, no transform baking, no renaming) in this build. Old receivers without the
|
||||
// publish feature don't know the metadata and take their third-party geometry path.
|
||||
// Reads the parse-time metadata: the model XML carries it before its resources, while
|
||||
// m_model->model_info is only filled in after the whole XML has been parsed.
|
||||
bool _is_published_3mf() const {
|
||||
const auto it = this->model_info.metadata_items.find(ORCA_PUBLISHED_TAG);
|
||||
return it != this->model_info.metadata_items.end() && is_published_3mf_flag(it->second);
|
||||
}
|
||||
|
||||
bool _is_svg_shape_file(const std::string &filename) const;
|
||||
bool _extract_from_archive(mz_zip_archive& archive, std::string const & path, std::function<bool (mz_zip_archive& archive, const mz_zip_archive_file_stat& stat)>, bool restore = false);
|
||||
bool _extract_xml_from_archive(mz_zip_archive& archive, std::string const & path, XML_StartElementHandler start_handler, XML_EndElementHandler end_handler);
|
||||
@@ -2002,7 +2021,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
|
||||
lock.close();
|
||||
|
||||
if (!m_is_bbl_3mf) {
|
||||
if (!m_is_bbl_3mf && !_is_published_3mf()) {
|
||||
// if the 3mf was not produced by OrcaSlicer and there is more than one instance,
|
||||
// split the object in as many objects as instances
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ":" << __LINE__ << boost::format(", found 3mf from other vendor, split as instance");
|
||||
@@ -3566,7 +3585,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
m_index_paths.insert({ object.first.second, object.first.first});
|
||||
}
|
||||
|
||||
if (!m_is_bbl_3mf) {
|
||||
if (!m_is_bbl_3mf && !_is_published_3mf()) {
|
||||
// if the 3mf was not produced by OrcaSlicer and there is only one object,
|
||||
// set the object name to match the filename
|
||||
if (m_model->objects.size() == 1)
|
||||
@@ -5289,7 +5308,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
|
||||
TriangleMesh triangle_mesh(std::move(its), volume_data.mesh_stats);
|
||||
|
||||
if (!m_is_bbl_3mf) {
|
||||
if (!m_is_bbl_3mf && !_is_published_3mf()) {
|
||||
// if the 3mf was not produced by OrcaSlicer and there is only one instance,
|
||||
// bake the transformation into the geometry to allow the reload from disk command
|
||||
// to work properly
|
||||
@@ -5935,6 +5954,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
bool m_save_gcode { false }; // whether to save gcode for normal save
|
||||
bool m_skip_model { false }; // skip model when exporting .gcode.3mf
|
||||
bool m_skip_auxiliary { false }; // skip normal axuiliary files
|
||||
bool m_minimal_published { false }; // published 3MF: omit the project config, the embedded preset files and the slicer tags
|
||||
bool m_use_loaded_id { false }; // whether to use loaded id for identify_id
|
||||
bool m_share_mesh { false }; // whether to share mesh between objects
|
||||
std::string m_thumbnail_middle = PRINTER_THUMBNAIL_MIDDLE_FILE;
|
||||
@@ -6034,6 +6054,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
m_skip_auxiliary = store_params.strategy & SaveStrategy::SkipAuxiliary;
|
||||
m_share_mesh = store_params.strategy & SaveStrategy::ShareMesh;
|
||||
m_from_backup_save = store_params.strategy & SaveStrategy::Backup;
|
||||
m_minimal_published = store_params.strategy & SaveStrategy::MinimalPublished;
|
||||
|
||||
m_use_loaded_id = store_params.strategy & SaveStrategy::UseLoadedId;
|
||||
|
||||
@@ -6443,7 +6464,10 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
|
||||
// Adds slic3r print config file ("Metadata/Slic3r_PE.config").
|
||||
// This file contains the content of FullPrintConfig / SLAFullPrintConfig.
|
||||
if (config != nullptr) {
|
||||
// Omitted for minimal published 3MF: OrcaSlicer versions without the publish feature
|
||||
// then fall back to importing the geometry only, and new versions read the published
|
||||
// payload from the model metadata instead.
|
||||
if (config != nullptr && !m_minimal_published) {
|
||||
// BBS: change to json format
|
||||
// if (!_add_print_config_file_to_archive(archive, *config)) {
|
||||
if (!_add_project_config_file_to_archive(archive, *config, model)) { return false; }
|
||||
@@ -6456,8 +6480,8 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
if (cb_cancel) return false;
|
||||
}
|
||||
|
||||
// BBS: add project config
|
||||
if (project_presets.size() > 0) {
|
||||
// BBS: add project config (omitted for minimal published 3MF)
|
||||
if (!m_minimal_published && project_presets.size() > 0) {
|
||||
// BBS: add project embedded preset files
|
||||
_add_project_embedded_presets_to_archive(archive, model, project_presets);
|
||||
|
||||
@@ -6929,10 +6953,31 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
// Orca: PRIVACY: do not store creation & modification date in 3mf
|
||||
metadata_item_map[BBL_CREATION_DATE_TAG] = "";
|
||||
metadata_item_map[BBL_MODIFICATION_TAG] = "";
|
||||
// Orca: Write the BambuStudio compatibility version string using SLIC3R_VERSION
|
||||
metadata_item_map[BBL_APPLICATION_TAG] = (boost::format("%1%-%2%") % "BambuStudio" % SLIC3R_VERSION).str();
|
||||
// Orca: Write the BambuStudio compatibility version string using SLIC3R_VERSION.
|
||||
// A minimal published 3MF writes no slicer tags at all: any tag would route old
|
||||
// receivers onto a geometry-only fallback whose baked-in popup misreports the
|
||||
// file ("old OrcaSlicer version" / "BambuStudio"), while tag-less files classify
|
||||
// as From_Other and import the geometry silently.
|
||||
if (m_minimal_published) {
|
||||
// metadata_item_map is seeded from the input file's metadata_items above, so a
|
||||
// project opened from a regular Orca/BBS 3MF still carries the slicer-identifying
|
||||
// tags it came with. Erase every one of them - not just the two most common -
|
||||
// so a published 3MF is fully tag-less: old receivers classify it as From_Other
|
||||
// and import the geometry silently instead of showing a baked-in "old version"
|
||||
// popup, and no version marker survives to seed a later re-save.
|
||||
metadata_item_map.erase(BBL_APPLICATION_TAG);
|
||||
metadata_item_map.erase(ORCASLICER_TAG);
|
||||
metadata_item_map.erase(BBS_3MF_VERSION);
|
||||
metadata_item_map.erase(BBS_3MF_VERSION1);
|
||||
} else {
|
||||
metadata_item_map[BBL_APPLICATION_TAG] = (boost::format("%1%-%2%") % "BambuStudio" % SLIC3R_VERSION).str();
|
||||
}
|
||||
}
|
||||
metadata_item_map[BBS_3MF_VERSION] = std::to_string(VERSION_BBS_3MF);
|
||||
// The Bambu 3MF version marker is part of the slicer identity: omit it for a minimal
|
||||
// published file along with the tags erased above (skipping the overwrite alone would
|
||||
// leave the value the source file seeded into metadata_item_map).
|
||||
if (!m_minimal_published)
|
||||
metadata_item_map[BBS_3MF_VERSION] = std::to_string(VERSION_BBS_3MF);
|
||||
|
||||
if (!model.mk_name.empty()) {
|
||||
metadata_item_map[BBL_MAKERLAB_TAG] = xml_escape(model.mk_name);
|
||||
@@ -6955,7 +7000,11 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
BOOST_LOG_TRIVIAL(info) << "bbs_3mf: save key= " << item.first << ", value = " << item.second;
|
||||
stream << " <" << METADATA_TAG << " name=\"" << item.first << "\">"
|
||||
<< xml_escape(item.second) << "</" << METADATA_TAG << ">\n";
|
||||
if (item.first == BBL_APPLICATION_TAG) {
|
||||
if (item.first == BBL_APPLICATION_TAG && !m_minimal_published) {
|
||||
// The OrcaSlicer tag is only written for files that carry the Application
|
||||
// tag, which a minimal published 3MF erases (see the map assignment above):
|
||||
// the explicit !m_minimal_published guard keeps the tag-less guarantee from
|
||||
// depending on that erase happening to run first.
|
||||
stream << " <" << METADATA_TAG << " name=\"" << ORCASLICER_TAG << "\">"
|
||||
<< xml_escape(SoftFever_VERSION) << "</" << METADATA_TAG << ">\n";
|
||||
}
|
||||
@@ -9134,6 +9183,126 @@ std::string bbs_3mf_get_thumbnail(const char *path)
|
||||
return data;
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
// Parses just the model-file <metadata> elements, mirroring the importer's
|
||||
// _handle_start_metadata/_handle_end_metadata (attribute-order independent, entity-unescaped,
|
||||
// whitespace tolerant). Stops the parser as soon as the published flag node is read so the
|
||||
// geometry/resources that follow are skipped, which keeps the per-file cost small.
|
||||
struct PublishedXmlProbe
|
||||
{
|
||||
XML_Parser parser{nullptr};
|
||||
bool in_metadata{false};
|
||||
bool found{false};
|
||||
bool published{false};
|
||||
std::string curr_name;
|
||||
std::string curr_value;
|
||||
|
||||
static std::string attribute(const char** attrs, const char* key)
|
||||
{
|
||||
if (attrs == nullptr)
|
||||
return std::string();
|
||||
// expat hands the attrs as a NULL-terminated {name, value, ...} array.
|
||||
for (unsigned int a = 0; attrs[a] != nullptr; a += 2)
|
||||
if (::strcmp(attrs[a], key) == 0 && attrs[a + 1] != nullptr)
|
||||
return attrs[a + 1];
|
||||
return std::string();
|
||||
}
|
||||
|
||||
static void XMLCALL start(void* user_data, const char* name, const char** attrs)
|
||||
{
|
||||
auto* self = static_cast<PublishedXmlProbe*>(user_data);
|
||||
if (::strcmp(name, METADATA_TAG) == 0) {
|
||||
self->in_metadata = true;
|
||||
self->curr_name = attribute(attrs, NAME_ATTR);
|
||||
self->curr_value.clear();
|
||||
} else {
|
||||
self->in_metadata = false;
|
||||
}
|
||||
}
|
||||
|
||||
static void XMLCALL characters(void* user_data, const XML_Char* s, int len)
|
||||
{
|
||||
auto* self = static_cast<PublishedXmlProbe*>(user_data);
|
||||
if (self->in_metadata)
|
||||
self->curr_value.append(s, len);
|
||||
}
|
||||
|
||||
static void XMLCALL end(void* user_data, const char* name)
|
||||
{
|
||||
auto* self = static_cast<PublishedXmlProbe*>(user_data);
|
||||
if (!self->in_metadata || ::strcmp(name, METADATA_TAG) != 0)
|
||||
return;
|
||||
self->in_metadata = false;
|
||||
if (self->curr_name == ORCA_PUBLISHED_TAG) {
|
||||
self->published = is_published_3mf_flag(xml_unescape(self->curr_value));
|
||||
self->found = true;
|
||||
if (self->parser != nullptr)
|
||||
XML_StopParser(self->parser, false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
bool bbs_3mf_is_published(const std::string &path)
|
||||
{
|
||||
mz_zip_archive archive;
|
||||
mz_zip_zero_struct(&archive);
|
||||
|
||||
struct close_lock
|
||||
{
|
||||
mz_zip_archive *archive;
|
||||
void close()
|
||||
{
|
||||
if (archive) {
|
||||
close_zip_reader(archive);
|
||||
archive = nullptr;
|
||||
}
|
||||
}
|
||||
~close_lock() { close(); }
|
||||
} lock{&archive};
|
||||
|
||||
if (!open_zip_reader(&archive, path))
|
||||
return false;
|
||||
|
||||
// Read just the model XML (the metadata node sits before the resources, so the probe below
|
||||
// stops early) rather than by a raw substring match; no geometry parsing.
|
||||
int index = mz_zip_reader_locate_file(&archive, MODEL_FILE.c_str(), nullptr, 0);
|
||||
if (index < 0)
|
||||
return false;
|
||||
mz_zip_archive_file_stat stat;
|
||||
if (!mz_zip_reader_file_stat(&archive, index, &stat))
|
||||
return false;
|
||||
std::string xml(stat.m_uncomp_size, '\0');
|
||||
if (!mz_zip_reader_extract_to_mem(&archive, index, xml.data(), xml.size(), 0))
|
||||
return false;
|
||||
|
||||
XML_Parser parser = XML_ParserCreate(nullptr);
|
||||
if (parser == nullptr)
|
||||
return false;
|
||||
|
||||
PublishedXmlProbe probe;
|
||||
probe.parser = parser;
|
||||
XML_SetUserData(parser, &probe);
|
||||
XML_SetElementHandler(parser, PublishedXmlProbe::start, PublishedXmlProbe::end);
|
||||
XML_SetCharacterDataHandler(parser, PublishedXmlProbe::characters);
|
||||
// Never resolve external entities from a file we are only probing.
|
||||
XML_SetExternalEntityRefHandler(parser, nullptr);
|
||||
XML_SetEntityDeclHandler(parser, nullptr);
|
||||
|
||||
const XML_Status status = XML_Parse(parser, xml.data(), static_cast<int>(xml.size()), 1);
|
||||
// XML_StopParser(parser, false) from the end handler makes XML_Parse return
|
||||
// XML_STATUS_ERROR with XML_ERROR_ABORTED - treat that as success (we stopped on the flag).
|
||||
const bool parse_ok = (status == XML_STATUS_OK) ||
|
||||
(XML_GetErrorCode(parser) == XML_ERROR_ABORTED && probe.found);
|
||||
XML_ParserFree(parser);
|
||||
|
||||
if (!parse_ok)
|
||||
return false;
|
||||
return probe.published;
|
||||
}
|
||||
|
||||
bool load_gcode_3mf_from_stream(std::istream &data, DynamicPrintConfig *config, Model *model, PlateDataPtrs *plate_data_list, Semver *file_version)
|
||||
{
|
||||
CNumericLocalesSetter locales_setter;
|
||||
|
||||
@@ -159,12 +159,28 @@ enum class SaveStrategy
|
||||
SkipAuxiliary = 1 << 9,
|
||||
UseLoadedId = 1 << 10,
|
||||
ShareMesh = 1 << 11,
|
||||
// Keep this separate from SplitModel, which uses the 0x1000 bit as part of its
|
||||
// production-extension value.
|
||||
MinimalPublished = 1 << 13,
|
||||
|
||||
SplitModel = 0x1000 | ProductionExt,
|
||||
Encrypted = SecureContentExt | SplitModel,
|
||||
Backup = 0x10000 | WithGcode | Silence | SkipStatic | SplitModel,
|
||||
};
|
||||
|
||||
// Model metadata keys of a "published" 3MF (see MinimalPublished): the flag marks a minimal,
|
||||
// tag-less publish export, the others carry the author-selected settings payload. Namespaced
|
||||
// with the "orca_published" prefix because metadata_items round-trips verbatim through other
|
||||
// slicers, where a bare "published" key could collide.
|
||||
inline constexpr const char *ORCA_PUBLISHED_TAG = "orca_published";
|
||||
inline constexpr const char *ORCA_PUBLISHED_KEYS_TAG = "orca_published_keys";
|
||||
inline constexpr const char *ORCA_PUBLISHED_MATERIAL_TAG = "orca_published_material_keys";
|
||||
inline constexpr const char *ORCA_PUBLISHED_CONFIG_TAG = "orca_published_config";
|
||||
|
||||
// Published files are produced with "1". The importer and the GUI loader both gate on this
|
||||
// exact value, so a "0"/"false"/unknown value is rejected consistently.
|
||||
bool is_published_3mf_flag(const std::string &value);
|
||||
|
||||
inline SaveStrategy operator | (SaveStrategy lhs, SaveStrategy rhs)
|
||||
{
|
||||
using T = std::underlying_type_t <SaveStrategy>;
|
||||
@@ -277,6 +293,9 @@ extern bool load_bbs_3mf(const char* path, DynamicPrintConfig* config, ConfigSub
|
||||
|
||||
extern std::string bbs_3mf_get_thumbnail(const char * path);
|
||||
|
||||
// Lightweight check: does this 3mf carry the "published" (orca_published == "1") marker? Only reads the 3D/3dmodel.model metadata node
|
||||
extern bool bbs_3mf_is_published(const std::string &path);
|
||||
|
||||
extern bool load_gcode_3mf_from_stream(std::istream & data, DynamicPrintConfig* config, Model* model, PlateDataPtrs* plate_data_list,
|
||||
Semver* file_version);
|
||||
|
||||
|
||||
@@ -3598,6 +3598,15 @@ void FacetsAnnotation::shift_states_above(const ModelVolume &mv, EnforcerBlocker
|
||||
this->set(selector);
|
||||
}
|
||||
|
||||
void FacetsAnnotation::remap_states(const ModelVolume &mv, const EnforcerBlockerStateMap &state_map)
|
||||
{
|
||||
if (empty()) return;
|
||||
TriangleSelector selector(mv.mesh());
|
||||
selector.deserialize(m_data, false);
|
||||
selector.remap_triangle_state(state_map);
|
||||
this->set(selector);
|
||||
}
|
||||
|
||||
void FacetsAnnotation::set_enforcer_block_type_limit(const ModelVolume &mv,
|
||||
EnforcerBlockerType max_type,
|
||||
EnforcerBlockerType to_delete_filament,
|
||||
@@ -3862,6 +3871,43 @@ bool model_has_advanced_features(const Model &model)
|
||||
return false;
|
||||
}
|
||||
|
||||
void remap_model_filament_slots(Model &model, const std::map<int, int> &slot_relocations)
|
||||
{
|
||||
if (slot_relocations.empty())
|
||||
return;
|
||||
|
||||
// Paint states and the object/volume "extruder" configs store one-based slot numbers
|
||||
// (see Sidebar::on_action_add_filament's insertion remap for the same encoding).
|
||||
std::map<int, int> one_based_slots;
|
||||
for (const auto &[from, to] : slot_relocations)
|
||||
one_based_slots.emplace(from + 1, to + 1);
|
||||
|
||||
EnforcerBlockerStateMap paint_state_map;
|
||||
for (size_t state = 0; state < paint_state_map.size(); ++state)
|
||||
paint_state_map[state] = EnforcerBlockerType(state);
|
||||
for (const auto &[one_based_from, one_based_to] : one_based_slots) {
|
||||
assert(one_based_from >= 0 && size_t(one_based_from) < paint_state_map.size());
|
||||
assert(one_based_to > 0 && size_t(one_based_to) < paint_state_map.size());
|
||||
paint_state_map[size_t(one_based_from)] = EnforcerBlockerType(one_based_to);
|
||||
}
|
||||
|
||||
auto remap_extruder_config = [&one_based_slots](ModelConfig &config) -> bool {
|
||||
const auto it = config.has("extruder") ? one_based_slots.find(config.extruder()) : one_based_slots.end();
|
||||
if (it == one_based_slots.end())
|
||||
return false;
|
||||
config.set("extruder", it->second);
|
||||
return true;
|
||||
};
|
||||
|
||||
for (ModelObject *object : model.objects) {
|
||||
remap_extruder_config(object->config);
|
||||
for (ModelVolume *volume : object->volumes) {
|
||||
remap_extruder_config(volume->config);
|
||||
volume->mmu_segmentation_facets.remap_states(*volume, paint_state_map);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#ifndef NDEBUG
|
||||
// Verify whether the IDs of Model / ModelObject / ModelVolume / ModelInstance / ModelMaterial are valid and unique.
|
||||
void check_model_ids_validity(const Model &model)
|
||||
|
||||
@@ -745,6 +745,10 @@ public:
|
||||
// Shift painted filament indices >= threshold by delta. Used when a physical filament is
|
||||
// inserted ahead of existing slots (mixed-color slots are kept at the end of the list).
|
||||
void shift_states_above(const ModelVolume &mv, EnforcerBlockerType threshold, int delta);
|
||||
// Relabel painted filament indices according to state_map (old state value -> new state
|
||||
// value; untouched states keep their identity). Used when published-3MF import relocates
|
||||
// mixed-filament definitions onto new slot numbers.
|
||||
void remap_states(const ModelVolume &mv, const EnforcerBlockerStateMap &state_map);
|
||||
indexed_triangle_set get_facets_strict(const ModelVolume& mv, EnforcerBlockerType type) const;
|
||||
bool has_facets(const ModelVolume& mv, EnforcerBlockerType type) const;
|
||||
bool empty() const { return m_data.triangles_to_split.empty(); }
|
||||
@@ -1790,6 +1794,13 @@ bool model_has_multi_part_objects(const Model &model);
|
||||
// If the model has advanced features, then it cannot be processed in simple mode.
|
||||
bool model_has_advanced_features(const Model &model);
|
||||
|
||||
// Remap the model's filament-slot references after a published-3MF import relocated
|
||||
// mixed-filament definitions onto new slot numbers: object/volume "extruder" configs and
|
||||
// multi-material color-painting states (paint state stores the one-based slot number).
|
||||
// slot_relocations maps the author's zero-based slot number to its final zero-based slot;
|
||||
// entries are applied simultaneously (no chained lookups), untouched slots keep everything.
|
||||
void remap_model_filament_slots(Model &model, const std::map<int, int> &slot_relocations);
|
||||
|
||||
#ifndef NDEBUG
|
||||
// Verify whether the IDs of Model / ModelObject / ModelVolume / ModelInstance / ModelMaterial are valid and unique.
|
||||
void check_model_ids_validity(const Model &model);
|
||||
|
||||
@@ -3062,6 +3062,75 @@ void PresetCollection::save_current_preset(const std::string &new_name, bool det
|
||||
this->get_selected_preset().save(nullptr);
|
||||
}
|
||||
|
||||
// A detached standalone preset for the Full Publish receiver: create a user preset holding
|
||||
// the full resolved filament config (no inheritance, no vendor/alias links), parentless.
|
||||
// Note: universal printer compatibility is not enforced here - callers apply
|
||||
// make_publish_universal() to the config before handing it over when they need it.
|
||||
// Mirrors save_current_preset(detach=true)'s creation branch but does not force-select or
|
||||
// diff against a parent; the caller decides whether to select it.
|
||||
// The published entry's filament_id is forwarded so user bases keep their stable
|
||||
// material grouping (get_filament_presets() groups user bases by filament_id).
|
||||
// The copy is a project-embedded preset: it lives inside the loaded project only
|
||||
// (serialized into the saved .3mf, restored by load_project_embedded_presets) and
|
||||
// never touches the user's library directory; Preset::save() early-returns for
|
||||
// embedded presets, so persistence is skipped here too.
|
||||
// Returns the final (uniquified) name; on collision "<base>" -> "<base> (Published)" ->
|
||||
// "<base> (Published 2)" ...
|
||||
std::string PresetCollection::add_detached_preset(const std::string &name_base, DynamicPrintConfig config,
|
||||
const std::string &filament_id)
|
||||
{
|
||||
if (name_base.empty())
|
||||
return std::string();
|
||||
Preset stored(m_type, name_base);
|
||||
stored.config = std::move(config);
|
||||
stored.filament_id = filament_id;
|
||||
|
||||
// Uniquify verbatim; only on collision append " (Published)" then " (Published 2)".
|
||||
const std::string base_name = name_base;
|
||||
std::string final_name = base_name;
|
||||
auto exists = [this](const std::string &candidate) -> bool {
|
||||
const auto it = this->find_preset_internal(candidate);
|
||||
return it != m_presets.end() && it->name == candidate;
|
||||
};
|
||||
if (exists(final_name)) {
|
||||
final_name = base_name + " (Published)";
|
||||
for (int i = 2; exists(final_name); ++i)
|
||||
final_name = base_name + " (Published " + std::to_string(i) + ")";
|
||||
}
|
||||
|
||||
// Creation branch of save_current_preset(detach=true), without its selection side
|
||||
// effects or project-embedded path.
|
||||
lock();
|
||||
const auto it = this->find_preset_internal(final_name);
|
||||
if (m_presets.begin() + m_idx_selected >= it)
|
||||
++m_idx_selected;
|
||||
Preset &preset = *m_presets.insert(it, stored);
|
||||
preset.name = final_name;
|
||||
preset.vendor = nullptr;
|
||||
preset.alias.clear();
|
||||
preset.renamed_from.clear();
|
||||
preset.m_excluded_from.clear();
|
||||
preset.setting_id.clear();
|
||||
preset.inherits().clear();
|
||||
preset.version = Semver::parse(SoftFever_VERSION).value_or(Semver());
|
||||
preset.is_default = false;
|
||||
preset.is_system = false;
|
||||
preset.is_external = false;
|
||||
preset.bundle_id.clear();
|
||||
preset.file = this->path_for_preset(preset);
|
||||
preset.is_visible = true;
|
||||
preset.is_project_embedded = true;
|
||||
if (m_type == Preset::TYPE_PRINT)
|
||||
preset.config.option<ConfigOptionString>("print_settings_id", true)->value = final_name;
|
||||
else if (m_type == Preset::TYPE_FILAMENT)
|
||||
preset.config.option<ConfigOptionStrings>("filament_settings_id", true)->values[0] = final_name;
|
||||
else if (m_type == Preset::TYPE_PRINTER)
|
||||
preset.config.option<ConfigOptionString>("printer_settings_id", true)->value = final_name;
|
||||
unlock();
|
||||
|
||||
return final_name;
|
||||
}
|
||||
|
||||
bool PresetCollection::delete_current_preset()
|
||||
{
|
||||
Preset &selected = this->get_selected_preset();
|
||||
|
||||
@@ -631,6 +631,22 @@ public:
|
||||
// All presets are marked as not modified and the new preset is activated.
|
||||
//BBS: add project embedded preset logic
|
||||
void save_current_preset(const std::string &new_name, bool detach = false, bool save_to_project = false, Preset* _curr_preset = nullptr);
|
||||
// Insert a standalone user preset holding the full resolved config (no inheritance,
|
||||
// no vendor links): the libslic3r equivalent of "Detach from parent". Takes a
|
||||
// resolved config, clears parent/vendor/alias metadata, stamps filament_settings_id.
|
||||
// Unlike save_current_preset it does not force-select or diff against a parent.
|
||||
// Used by the published-3MF Full Publish path. The optional filament_id seeds the
|
||||
// preset's stable material grouping (get_filament_presets groups user bases by
|
||||
// filament_id); the published entry's filament_id is forwarded so the copy keeps
|
||||
// the author's grouping.
|
||||
// The copy is a project-embedded preset ("Preset Inside Project"): it lives inside
|
||||
// the loaded project only, is serialized into the saved .3mf via
|
||||
// get_current_project_embedded_presets(), and is never written to the user's
|
||||
// library directory.
|
||||
// Returns the final (uniquified) name; on collision the suffix rule is:
|
||||
// "<base>" -> "<base> (Published)" -> "<base> (Published 2)" ...
|
||||
std::string add_detached_preset(const std::string &name_base, DynamicPrintConfig config,
|
||||
const std::string &filament_id = std::string());
|
||||
|
||||
// Delete the current preset, activate the first visible preset.
|
||||
// returns true if the preset was deleted successfully.
|
||||
|
||||
+1437
-145
File diff suppressed because it is too large
Load Diff
@@ -4,9 +4,11 @@
|
||||
#include "Preset.hpp"
|
||||
#include "PresetCacheFormat.hpp"
|
||||
#include "AppConfig.hpp"
|
||||
#include "PublishSettings.hpp"
|
||||
#include "enum_bitmask.hpp"
|
||||
|
||||
#include <memory>
|
||||
#include <map>
|
||||
#include <set>
|
||||
#include <shared_mutex>
|
||||
#include <unordered_map>
|
||||
@@ -168,6 +170,30 @@ struct PresetBundleMetadata
|
||||
}
|
||||
};
|
||||
|
||||
// A "published" 3MF project: keeps the user's currently-selected presets and overlays only the
|
||||
// author-selected published keys onto the edited presets.
|
||||
struct PublishedConfig
|
||||
{
|
||||
bool published = false;
|
||||
std::vector<std::string> published_keys;
|
||||
// Per-slot published material keys, applied positionally (author slot N -> receiver slot N).
|
||||
// Partial entries are gated by the author's optional type requirement and written onto the
|
||||
// slot's stored preset in place; full entries instead detach (see PublishedMaterialEntry in
|
||||
// PublishSettings.hpp).
|
||||
std::vector<PublishedMaterialEntry> material_keys;
|
||||
// Keys that could not be applied (missing on the user's machine or vector size mismatch),
|
||||
// filled in by load_config_file_config for notification purposes.
|
||||
std::vector<std::string> skipped_keys;
|
||||
// Human-readable notices of the slot material replacements performed while loading a
|
||||
// published project, for the load notification.
|
||||
std::vector<std::string> material_replacements;
|
||||
// Mixed-filament entries that had to be moved off their authored slot on load (a real,
|
||||
// physical filament occupied it): maps the author's zero-based slot number to its final
|
||||
// zero-based slot. Consumers (e.g. model extruder/color-painting remapping) use this to
|
||||
// keep geometry references pointing at the relocated definitions.
|
||||
std::map<int, int> mixed_slot_relocations;
|
||||
};
|
||||
|
||||
// Bundle of Print + Filament + Printer presets.
|
||||
class PresetBundle
|
||||
{
|
||||
@@ -464,8 +490,8 @@ public:
|
||||
|
||||
// Load configuration that comes from a model file containing configuration, such as 3MF et al.
|
||||
// This method is called by the Plater.
|
||||
void load_config_model(const std::string &name, DynamicPrintConfig config, Semver file_version = Semver())
|
||||
{ this->load_config_file_config(name, true, std::move(config), file_version); }
|
||||
void load_config_model(const std::string &name, DynamicPrintConfig config, Semver file_version = Semver(), PublishedConfig *published_config = nullptr)
|
||||
{ this->load_config_file_config(name, true, std::move(config), file_version, false, published_config); }
|
||||
|
||||
// Load an external config file containing the print, filament and printer presets.
|
||||
// Instead of a config file, a G-code may be loaded containing the full set of parameters.
|
||||
@@ -646,7 +672,7 @@ private:
|
||||
// Load print, filament & printer presets from a config. If it is an external config, then the name is extracted from the external path.
|
||||
// and the external config is just referenced, not stored into user profile directory.
|
||||
// If it is not an external config, then the config will be stored into the user profile directory.
|
||||
void load_config_file_config(const std::string &name_or_path, bool is_external, DynamicPrintConfig &&config, Semver file_version = Semver(), bool selected = false);
|
||||
void load_config_file_config(const std::string &name_or_path, bool is_external, DynamicPrintConfig &&config, Semver file_version = Semver(), bool selected = false, PublishedConfig *published_config = nullptr);
|
||||
/*ConfigSubstitutions load_config_file_config_bundle(
|
||||
const std::string &path, const boost::property_tree::ptree &tree, ForwardCompatibilitySubstitutionRule compatibility_rule);*/
|
||||
|
||||
|
||||
@@ -0,0 +1,315 @@
|
||||
#include "PublishSettings.hpp"
|
||||
|
||||
#include "PresetBundle.hpp"
|
||||
#include "Preset.hpp"
|
||||
#include "PrintConfig.hpp"
|
||||
#include "MaterialType.hpp"
|
||||
|
||||
#include <boost/log/trivial.hpp>
|
||||
#include <boost/algorithm/string/trim.hpp>
|
||||
|
||||
#include <map>
|
||||
#include <set>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
std::string publish_base_key(const std::string &key)
|
||||
{
|
||||
const size_t pos = key.find('#');
|
||||
return pos == std::string::npos ? key : key.substr(0, pos);
|
||||
}
|
||||
|
||||
// Parse the trailing "#N" variant index ("retraction_length#2" -> 2). Returns -1 when the key
|
||||
// carries no '#' separator or its suffix is malformed; mirrors the importer's strict parse
|
||||
// (PresetBundle.cpp) so the export side rejects the same variants the receiver would skip.
|
||||
static int publish_variant_index(const std::string &key, const std::string &base_key)
|
||||
{
|
||||
if (key.size() <= base_key.size() || key.compare(0, base_key.size(), base_key) != 0 || key[base_key.size()] != '#')
|
||||
return -1;
|
||||
const std::string suffix = key.substr(base_key.size() + 1);
|
||||
if (suffix.empty())
|
||||
return -1;
|
||||
int idx = 0;
|
||||
for (const char c : suffix) {
|
||||
if (c < '0' || c > '9')
|
||||
return -1;
|
||||
idx = idx * 10 + (c - '0');
|
||||
if (idx > 1000000) // overflow guard; real vector sizes are tiny
|
||||
return -1;
|
||||
}
|
||||
return idx;
|
||||
}
|
||||
|
||||
std::string normalize_filament_type(const std::string& type)
|
||||
{
|
||||
if (type.empty())
|
||||
return type;
|
||||
if (MaterialType::find(type) != nullptr)
|
||||
return type;
|
||||
// "PLA High Speed" -> "PLA": strip a space-separated modifier, but keep dash-separated
|
||||
// types like "PA-CF" / "PETG-CF" intact (they are distinct materials, not modifiers).
|
||||
const size_t sep = type.find(' ');
|
||||
if (sep != std::string::npos) {
|
||||
const std::string base = type.substr(0, sep);
|
||||
if (MaterialType::find(base) != nullptr)
|
||||
return base;
|
||||
}
|
||||
return type;
|
||||
}
|
||||
|
||||
void make_publish_universal(DynamicPrintConfig &config)
|
||||
{
|
||||
// Lists: empty => compatible with every printer / every print preset. Conditions:
|
||||
// empty so a leftover expression left behind by the baseline clone can never
|
||||
// re-narrow the match (see is_compatible_with_printer, Preset.cpp:840). All four
|
||||
// keys exist on filament presets; nil-guard for hand-crafted future schemas.
|
||||
if (auto *opt = config.opt<ConfigOptionStrings>("compatible_printers", false))
|
||||
opt->values.clear();
|
||||
if (auto *opt = config.opt<ConfigOptionStrings>("compatible_prints", false))
|
||||
opt->values.clear();
|
||||
if (auto *opt = config.opt<ConfigOptionString>("compatible_printers_condition", false))
|
||||
opt->value.clear();
|
||||
if (auto *opt = config.opt<ConfigOptionString>("compatible_prints_condition", false))
|
||||
opt->value.clear();
|
||||
}
|
||||
|
||||
std::string publish_material_base_name(const std::string &preset_name)
|
||||
{
|
||||
if (preset_name.empty())
|
||||
return preset_name;
|
||||
const size_t at = preset_name.find('@');
|
||||
std::string base = (at == std::string::npos) ? preset_name : preset_name.substr(0, at);
|
||||
boost::trim_right(base);
|
||||
return base;
|
||||
}
|
||||
|
||||
const std::set<std::string>& publish_structural_keys()
|
||||
{
|
||||
// Non-publishable keys: the *_settings_id keys are also in PresetCollection::skipped_in_dirty
|
||||
// (Preset.cpp) / stripped from configs (profile_print_params_same); publishing them would
|
||||
// rewrite the user's preset inheritance/structure.
|
||||
static const std::set<std::string> structural_keys = {
|
||||
"printer_settings_id", "filament_settings_id", "print_settings_id",
|
||||
"sla_print_settings_id", "sla_material_settings_id",
|
||||
"compatible_printers", "compatible_prints",
|
||||
"compatible_printers_condition", "compatible_prints_condition",
|
||||
"default_filament_profile", "default_print_profile",
|
||||
"default_sla_print_profile", "default_sla_material_profile",
|
||||
"extruder_count", "bed_shape",
|
||||
"inherits", "inherits_group",
|
||||
"printer_technology", "printer_model", "printer_variant",
|
||||
"physical_printer_settings_id", "filament_ids",
|
||||
"different_settings_to_system"
|
||||
};
|
||||
return structural_keys;
|
||||
}
|
||||
|
||||
const std::set<std::string>& publish_mixed_keys()
|
||||
{
|
||||
// Must match PresetBundle's s_project_options mixed-color group (PresetBundle.cpp): these
|
||||
// are project-level parallel per-slot arrays, not filament-preset options, so the import
|
||||
// material pass applies them into project_config instead of a filament preset config.
|
||||
static const std::set<std::string> mixed_keys = {
|
||||
"filament_is_mixed",
|
||||
"filament_mixed_components",
|
||||
"filament_mixed_sublayer_ratios",
|
||||
"filament_mixed_gradient",
|
||||
"filament_mixed_gradient_range",
|
||||
"filament_mixed_gradient_curve",
|
||||
"filament_mixed_gradient_per_part"
|
||||
};
|
||||
return mixed_keys;
|
||||
}
|
||||
|
||||
// The printer tab's "Retraction" optgroup (TabPrinter::build_fff, Tab.cpp), in tab order.
|
||||
// KEEP IN SYNC with that optgroup: the published-3MF printer allowlist is built from these
|
||||
// lists, so any key shown there must be publishable here (and vice versa).
|
||||
const std::vector<PublishablePrinterOption>& publishable_printer_retraction_options()
|
||||
{
|
||||
static const std::vector<PublishablePrinterOption> options = {
|
||||
{ "retraction_length", "printer_extruder_retraction#length" },
|
||||
{ "retract_restart_extra", "printer_extruder_retraction#extra-length-on-restart" },
|
||||
{ "retraction_speed", "printer_extruder_retraction#retraction-speed" },
|
||||
{ "deretraction_speed", "printer_extruder_retraction#deretraction-speed" },
|
||||
{ "retraction_minimum_travel", "printer_extruder_retraction#travel-distance-threshold" },
|
||||
{ "retract_when_changing_layer", "printer_extruder_retraction#retract-on-layer-change" },
|
||||
{ "wipe", "printer_extruder_retraction#wipe-while-retracting" },
|
||||
{ "wipe_distance", "printer_extruder_retraction#wipe-distance" },
|
||||
{ "retract_before_wipe", "printer_extruder_retraction#retract-amount-before-wipe" },
|
||||
{ "retract_after_wipe", "printer_extruder_retraction#retract-amount-after-wipe" },
|
||||
};
|
||||
return options;
|
||||
}
|
||||
|
||||
// The printer tab's "Z-Hop" optgroup (TabPrinter::build_fff, Tab.cpp), in tab order. KEEP IN
|
||||
// SYNC with that optgroup, same as publishable_printer_retraction_options().
|
||||
const std::vector<PublishablePrinterOption>& publishable_printer_z_hop_options()
|
||||
{
|
||||
static const std::vector<PublishablePrinterOption> options = {
|
||||
{ "retract_lift_enforce", "printer_extruder_z_hop#on-surfaces" },
|
||||
{ "z_hop_types", "printer_extruder_z_hop#z-hop-type" },
|
||||
{ "z_hop", "printer_extruder_z_hop#z-hop-height" },
|
||||
{ "travel_slope", "printer_extruder_z_hop#traveling-angle" },
|
||||
{ "retract_lift_above", "printer_extruder_z_hop#only-lift-z-above" },
|
||||
{ "retract_lift_below", "printer_extruder_z_hop#only-lift-z-below" },
|
||||
};
|
||||
return options;
|
||||
}
|
||||
|
||||
const std::set<std::string>& publishable_printer_keys()
|
||||
{
|
||||
// Union of the two optgroups; "Retraction when switching material" keys are excluded
|
||||
// (toolchange retraction is device/profile territory, not a publishable behavior tweak).
|
||||
static const std::set<std::string> printer_keys = [] {
|
||||
std::set<std::string> keys;
|
||||
for (const PublishablePrinterOption &opt : publishable_printer_retraction_options())
|
||||
keys.insert(opt.key);
|
||||
for (const PublishablePrinterOption &opt : publishable_printer_z_hop_options())
|
||||
keys.insert(opt.key);
|
||||
return keys;
|
||||
}();
|
||||
return printer_keys;
|
||||
}
|
||||
|
||||
std::vector<std::string> collect_dirty_settings_keys(const PresetBundle& bundle)
|
||||
{
|
||||
std::set<std::string> keys;
|
||||
|
||||
// Union the dirty keys of each collection's edited preset (filaments may span multiple
|
||||
// slots); feeds only the Publish dialog's pre-check.
|
||||
for (const std::string& key : bundle.prints.current_dirty_options(true))
|
||||
keys.insert(key);
|
||||
for (const std::string& key : bundle.printers.current_dirty_options(true))
|
||||
keys.insert(key);
|
||||
for (const std::string& key : bundle.filaments.current_dirty_options(true))
|
||||
keys.insert(key);
|
||||
|
||||
return std::vector<std::string>(keys.begin(), keys.end());
|
||||
}
|
||||
|
||||
DynamicPrintConfig filter_published_config(
|
||||
const DynamicPrintConfig &full_config,
|
||||
const std::vector<std::string> &published_keys,
|
||||
const std::vector<PublishedMaterialEntry> &material_keys)
|
||||
{
|
||||
DynamicPrintConfig filtered;
|
||||
|
||||
std::set<std::string> base_keys_to_include;
|
||||
// Never masked (whole-vector serialization): identity, plate geometry, process keys and
|
||||
// printer keys without a "#N" variant.
|
||||
std::set<std::string> mask_exempt_keys;
|
||||
// Material entries: base key -> author slots whose values must survive; other slots are
|
||||
// masked to their defaults so a publish (partial or full) does not leak unrelated slot
|
||||
// data.
|
||||
std::map<std::string, std::set<int>> slot_mask_map;
|
||||
|
||||
// 1. Mandatory material identity & slot count keys for 3MF validation/normalization
|
||||
// (filament_ids: exported for validation, denylisted on apply - see publish_structural_keys).
|
||||
static const std::vector<std::string> s_material_identity_keys = {
|
||||
"filament_colour",
|
||||
"filament_type",
|
||||
"filament_vendor",
|
||||
"filament_ids",
|
||||
"filament_diameter",
|
||||
"filament_self_index",
|
||||
"filament_extruder_variant"
|
||||
};
|
||||
for (const std::string &key : s_material_identity_keys) {
|
||||
base_keys_to_include.insert(key);
|
||||
mask_exempt_keys.insert(key);
|
||||
}
|
||||
|
||||
// 2. Published plate / bed geometry keys (wipe tower positioning)
|
||||
static const std::vector<std::string> s_plate_geometry_keys = {
|
||||
"wipe_tower_x",
|
||||
"wipe_tower_y",
|
||||
"wipe_tower_rotation_angle"
|
||||
};
|
||||
for (const std::string &key : s_plate_geometry_keys) {
|
||||
base_keys_to_include.insert(key);
|
||||
mask_exempt_keys.insert(key);
|
||||
}
|
||||
|
||||
// 3. Process and printer published keys. Printer per-extruder keys carry a "#N" variant
|
||||
// (e.g. retraction_length#2): mask the base to the author's extruder index so a partial
|
||||
// publish does not serialize every extruder's value (same slot-masking as the material side).
|
||||
const std::set<std::string> &printer_keys = publishable_printer_keys();
|
||||
for (const std::string &key : published_keys) {
|
||||
const std::string base_key = publish_base_key(key);
|
||||
if (base_key.empty())
|
||||
continue;
|
||||
base_keys_to_include.insert(base_key);
|
||||
if (printer_keys.count(base_key) != 0) {
|
||||
const int variant_idx = publish_variant_index(key, base_key);
|
||||
if (variant_idx >= 0)
|
||||
slot_mask_map[base_key].insert(variant_idx);
|
||||
else
|
||||
mask_exempt_keys.insert(base_key); // bare printer key or malformed variant: whole vector
|
||||
} else {
|
||||
mask_exempt_keys.insert(base_key); // process key: whole vector
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Material-specific published keys. Both partial (entry.keys) and full-publish
|
||||
// (entry.full_keys) entries mask to the author's slot on export (see the copy loop below);
|
||||
// slot-less entries (hand-crafted files) stay unmasked (whole vector).
|
||||
for (const PublishedMaterialEntry &entry : material_keys) {
|
||||
for (const std::string &key : entry.keys) {
|
||||
const std::string base_key = publish_base_key(key);
|
||||
if (base_key.empty())
|
||||
continue;
|
||||
base_keys_to_include.insert(base_key);
|
||||
if (entry.slot >= 0)
|
||||
slot_mask_map[base_key].insert(entry.slot);
|
||||
}
|
||||
for (const std::string &key : entry.full_keys) {
|
||||
const std::string base_key = publish_base_key(key);
|
||||
if (base_key.empty())
|
||||
continue;
|
||||
base_keys_to_include.insert(base_key);
|
||||
if (entry.slot >= 0)
|
||||
slot_mask_map[base_key].insert(entry.slot);
|
||||
}
|
||||
}
|
||||
|
||||
// Masking restores every non-published slot of a vector option with the option default, so
|
||||
// a partial publish does not leak unrelated slot data. can_mask_slots reports whether a key
|
||||
// is maskable at all (vector option plus a registered default of the same type); an
|
||||
// unmaskable key is dropped from the payload entirely instead of shipping the author's
|
||||
// whole vector.
|
||||
auto can_mask_slots = [](const ConfigOption &opt, const ConfigOptionDef *def) -> bool {
|
||||
if (def == nullptr || !def->default_value || def->default_value->type() != opt.type())
|
||||
return false;
|
||||
const auto *vec = dynamic_cast<const ConfigOptionVectorBase *>(&opt);
|
||||
const auto *default_vec = dynamic_cast<const ConfigOptionVectorBase *>(def->default_value.get());
|
||||
return vec != nullptr && vec->size() > 0 && default_vec != nullptr && !default_vec->empty();
|
||||
};
|
||||
auto mask_slots = [](ConfigOption &opt, const ConfigOptionDef *def, const std::set<int> &keep_slots) {
|
||||
auto *vec = dynamic_cast<ConfigOptionVectorBase*>(&opt);
|
||||
for (size_t idx = 0; idx < vec->size(); ++idx)
|
||||
if (keep_slots.count(static_cast<int>(idx)) == 0)
|
||||
vec->set_at(def->default_value.get(), idx, 0);
|
||||
};
|
||||
|
||||
// Copy the selected options from full_config into the filtered config.
|
||||
for (const std::string &key : base_keys_to_include) {
|
||||
const ConfigOption *opt = full_config.option(key);
|
||||
if (opt == nullptr)
|
||||
continue;
|
||||
const auto mask_it = slot_mask_map.find(key);
|
||||
const bool needs_masking = mask_exempt_keys.count(key) == 0 && mask_it != slot_mask_map.end() && !mask_it->second.empty();
|
||||
if (needs_masking && !can_mask_slots(*opt, print_config_def.get(key))) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "publish: dropping unmaskable key \"" << key
|
||||
<< "\" from the published payload (no usable option default)";
|
||||
continue;
|
||||
}
|
||||
ConfigOption *cloned = opt->clone();
|
||||
if (needs_masking)
|
||||
mask_slots(*cloned, print_config_def.get(key), mask_it->second);
|
||||
filtered.set_key_value(key, cloned);
|
||||
}
|
||||
|
||||
return filtered;
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -0,0 +1,99 @@
|
||||
#pragma once
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace Slic3r {
|
||||
class PresetBundle;
|
||||
|
||||
// Strip a trailing "#N" variant suffix ("retraction_length#2" -> "retraction_length").
|
||||
std::string publish_base_key(const std::string &key);
|
||||
|
||||
// Structural keys that are never applied onto the receiver's presets when loading a published
|
||||
// 3MF (single source of truth for the denylist); applying them would rewrite the user's preset
|
||||
// inheritance/structure. filament_ids is still exported via the identity list (3MF validation
|
||||
// needs it) - exported, never applied.
|
||||
const std::set<std::string>& publish_structural_keys();
|
||||
|
||||
// The mixed-color filament project keys (parallel per-slot arrays, see PresetBundle's
|
||||
// s_project_options). Import applies them into project_config, not a filament preset.
|
||||
const std::set<std::string>& publish_mixed_keys();
|
||||
|
||||
// One row of the printer tab's "Retraction" / "Z-Hop" optgroups (config key + tab icon id).
|
||||
struct PublishablePrinterOption {
|
||||
const char *key; // config key, e.g. "retraction_length"
|
||||
const char *icon; // tab icon id, e.g. "printer_extruder_retraction#length"
|
||||
};
|
||||
|
||||
// The printer tab's "Retraction" / "Z-Hop" optgroup options, in tab order.
|
||||
const std::vector<PublishablePrinterOption>& publishable_printer_retraction_options();
|
||||
const std::vector<PublishablePrinterOption>& publishable_printer_z_hop_options();
|
||||
|
||||
// Union of the two optgroup option lists; printer keys apply on import only if their base
|
||||
// key is in this allowlist.
|
||||
const std::set<std::string>& publishable_printer_keys();
|
||||
|
||||
// Union of setting keys differing from the base/system preset across the current print,
|
||||
// printer and filament presets (feeds the Publish dialog's pre-check).
|
||||
std::vector<std::string> collect_dirty_settings_keys(const PresetBundle& bundle);
|
||||
|
||||
// Per-slot published material keys, applied positionally (author slot N -> receiver slot N).
|
||||
// The identity fields drive the created copy's naming and grouping on Full entries, the
|
||||
// notification labels, and the partial type gate (publish_type) is the author's explicit
|
||||
// opt-in for requiring a material type.
|
||||
struct PublishedMaterialEntry {
|
||||
std::string filament_type; // material family, e.g. "PLA" (may be empty)
|
||||
std::string filament_vendor; // e.g. "Generic", "Bambu" (may be empty)
|
||||
std::string filament_id; // stable material id, e.g. "GFL99" (may be empty)
|
||||
// Unique preset id of the author's slot preset (e.g. Orca Filament Library "setting_id").
|
||||
// Not matched against the receiver's library; carried so identical Full entries within one
|
||||
// load share one created instance (within-load dedup key).
|
||||
std::string setting_id;
|
||||
// Canonical name of the author's slot preset (e.g. "Generic PLA @System"). On Full import
|
||||
// it names the created copy after its "@variant" tail is stripped; never matched against
|
||||
// the receiver's library.
|
||||
std::string preset_name;
|
||||
// 0-based author filament slot; -1 (hand-crafted files) is skipped.
|
||||
int slot{-1};
|
||||
std::vector<std::string> keys;
|
||||
// "Full Publish": the whole filament preset (full_keys) is published. On the receiver Full
|
||||
// Publish always creates a standalone parentless copy (libslic3r's "Detach from parent"),
|
||||
// universally compatible and project-embedded only - never written to the user's library.
|
||||
// Identical Full entries within one load share one created instance (within-load dedup).
|
||||
bool full{false};
|
||||
// All non-structural filament keys of the author's slot preset; values travel in the file
|
||||
// config, masked to the author's slot index.
|
||||
std::vector<std::string> full_keys;
|
||||
// Vendor-agnostic (MaterialType) filament type the author requires for this slot; on a
|
||||
// partial entry's mismatch the slot is replaced with a same-type filament. Full entries
|
||||
// consult no gate.
|
||||
bool publish_type{false};
|
||||
std::string publish_type_value;
|
||||
// Required filament colour, applied on load regardless of the type match.
|
||||
bool publish_color{false};
|
||||
std::string color;
|
||||
// Import-side only, never serialized: the authored slot sits past the receiver's physical
|
||||
// capacity, so the entry is appended as an empty mixed-filament placeholder (virtual tail
|
||||
// slot; the GUI flags it for the user to assign components).
|
||||
bool mixed_placeholder{false};
|
||||
};
|
||||
|
||||
// "PLA High Speed" -> "PLA" (strip a space modifier); dash types like "PA-CF" are kept intact.
|
||||
std::string normalize_filament_type(const std::string& type);
|
||||
|
||||
class DynamicPrintConfig;
|
||||
// Clear the compatibility lists/conditions on a filament config so it is universally
|
||||
// compatible once detached (empty lists + empty conditions = compatible with everything).
|
||||
void make_publish_universal(DynamicPrintConfig &config);
|
||||
|
||||
// Naming base for a detached published-material copy: "Generic PLA @System" -> "Generic PLA"
|
||||
// (truncate/right-trim at the first '@' tail). Empty result means "fall back to identity".
|
||||
std::string publish_material_base_name(const std::string &preset_name);
|
||||
|
||||
// Minimal DynamicPrintConfig for a published 3MF export: only the selected published keys,
|
||||
// material keys, identity fields and plate geometry keys.
|
||||
DynamicPrintConfig filter_published_config(
|
||||
const DynamicPrintConfig &full_config,
|
||||
const std::vector<std::string> &published_keys,
|
||||
const std::vector<PublishedMaterialEntry> &material_keys);
|
||||
}
|
||||
@@ -97,6 +97,8 @@ set(SLIC3R_GUI_SOURCES
|
||||
GUI/CloneDialog.hpp
|
||||
GUI/ConfigManipulation.cpp
|
||||
GUI/ConfigManipulation.hpp
|
||||
GUI/ConfigValueFormatter.cpp
|
||||
GUI/ConfigValueFormatter.hpp
|
||||
GUI/ConfigWizard.cpp
|
||||
GUI/ConfigWizard.hpp
|
||||
GUI/ConfigWizard_private.hpp
|
||||
@@ -453,6 +455,8 @@ set(SLIC3R_GUI_SOURCES
|
||||
GUI/Project.hpp
|
||||
GUI/PublishDialog.cpp
|
||||
GUI/PublishDialog.hpp
|
||||
GUI/PublishSettingsDialog.cpp
|
||||
GUI/PublishSettingsDialog.hpp
|
||||
GUI/PurgeModeDialog.cpp
|
||||
GUI/PurgeModeDialog.hpp
|
||||
GUI/RammingChart.cpp
|
||||
|
||||
@@ -0,0 +1,245 @@
|
||||
#include "ConfigValueFormatter.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdlib>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <boost/algorithm/string.hpp>
|
||||
#include <boost/format.hpp>
|
||||
|
||||
#include "libslic3r/Config.hpp"
|
||||
#include "libslic3r/PrintConfig.hpp"
|
||||
|
||||
#include "I18N.hpp"
|
||||
#include "GUI.hpp"
|
||||
#include "Field.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
namespace GUI {
|
||||
|
||||
std::string get_pure_opt_key(const std::string& opt_key)
|
||||
{
|
||||
std::string pure_key = opt_key;
|
||||
const int pos = pure_key.find("#");
|
||||
if (pos > 0)
|
||||
boost::erase_tail(pure_key, pure_key.size() - pos);
|
||||
return pure_key;
|
||||
}
|
||||
|
||||
wxString get_string_from_enum(const std::string& opt_key, const DynamicPrintConfig& config, bool is_infill, int idx)
|
||||
{
|
||||
const ConfigOptionDef& def = config.def()->options.at(opt_key);
|
||||
const std::vector<std::string>& names = def.enum_labels;//ConfigOptionEnum<T>::get_enum_names();
|
||||
int val = 0;
|
||||
|
||||
if (idx >= 0)
|
||||
val = dynamic_cast<const ConfigOptionInts*>(config.option(opt_key))->get_at(idx);
|
||||
else
|
||||
val = config.option(opt_key)->getInt();
|
||||
|
||||
// Each infill doesn't use all list of infill declared in PrintConfig.hpp.
|
||||
// So we should "convert" val to the correct one
|
||||
if (is_infill) {
|
||||
for (auto key_val : *def.enum_keys_map)
|
||||
if (int(key_val.second) == val) {
|
||||
auto it = std::find(def.enum_values.begin(), def.enum_values.end(), key_val.first);
|
||||
if (it == def.enum_values.end())
|
||||
return "";
|
||||
return from_u8(_utf8(names[it - def.enum_values.begin()]));
|
||||
}
|
||||
return _L("Undefined");
|
||||
}
|
||||
return from_u8(_utf8(names[val]));
|
||||
}
|
||||
|
||||
wxString get_full_label(const std::string& opt_key, const DynamicPrintConfig& config)
|
||||
{
|
||||
const std::string pure_key = get_pure_opt_key(opt_key);
|
||||
auto option = config.option(pure_key);
|
||||
|
||||
if (!option || option->is_nil())
|
||||
return _L("N/A");
|
||||
|
||||
const ConfigOptionDef* opt = config.def()->get(pure_key);
|
||||
return opt->full_label.empty() ? opt->label : opt->full_label;
|
||||
}
|
||||
|
||||
wxString get_string_value(const std::string& opt_key, const DynamicPrintConfig& config)
|
||||
{
|
||||
int orig_opt_idx = -1;
|
||||
int opt_idx = -1;
|
||||
int pos = opt_key.find("#");
|
||||
std::string temp_str = opt_key;
|
||||
if (pos > 0) {
|
||||
boost::erase_head(temp_str, pos + 1);
|
||||
orig_opt_idx = std::atoi(temp_str.c_str());
|
||||
}
|
||||
opt_idx = orig_opt_idx >= 0 ? orig_opt_idx : 0;
|
||||
const std::string pure_key = get_pure_opt_key(opt_key);
|
||||
auto option = config.option(pure_key);
|
||||
if (!option) {
|
||||
return _L("N/A");
|
||||
}
|
||||
auto opt_vector = dynamic_cast<const ConfigOptionVectorBase *>(option);
|
||||
|
||||
if ((option->is_scalar() && option->is_nil()) ||
|
||||
(option->is_vector() && opt_vector && opt_idx >= 0 && opt_idx < opt_vector->size() && opt_vector->is_nil(opt_idx)))
|
||||
return _L("N/A");
|
||||
|
||||
wxString out;
|
||||
|
||||
const ConfigOptionDef* opt = config.def()->get(pure_key);
|
||||
bool is_nullable = opt->nullable;
|
||||
|
||||
switch (opt->type) {
|
||||
case coInt:
|
||||
return from_u8((boost::format("%1%") % config.opt_int(pure_key)).str());
|
||||
case coInts: {
|
||||
if (is_nullable) {
|
||||
auto values = config.opt<ConfigOptionIntsNullable>(pure_key);
|
||||
if (opt_idx < values->size())
|
||||
return from_u8((boost::format("%1%") % values->get_at(opt_idx)).str());
|
||||
}
|
||||
else {
|
||||
auto values = config.opt<ConfigOptionInts>(pure_key);
|
||||
if (orig_opt_idx >= 0 && orig_opt_idx < values->size()) {
|
||||
return from_u8((boost::format("%1%") % values->get_at(opt_idx)).str());
|
||||
}
|
||||
else {
|
||||
std::string value_str;
|
||||
for (int i = 0; i < values->size(); i++) {
|
||||
value_str += std::to_string(values->get_at(i));
|
||||
if (i != values->size() - 1) {
|
||||
value_str += ",";
|
||||
}
|
||||
}
|
||||
return from_u8(value_str);
|
||||
}
|
||||
}
|
||||
return _L("Undefined");
|
||||
}
|
||||
case coBool:
|
||||
return config.opt_bool(pure_key) ? "true" : "false";
|
||||
case coBools: {
|
||||
if (is_nullable) {
|
||||
auto values = config.opt<ConfigOptionBoolsNullable>(pure_key);
|
||||
if (opt_idx < values->size())
|
||||
return values->get_at(opt_idx) ? "true" : "false";
|
||||
}
|
||||
else {
|
||||
auto values = config.opt<ConfigOptionBools>(pure_key);
|
||||
if (opt_idx < values->size())
|
||||
return values->get_at(opt_idx) ? "true" : "false";
|
||||
}
|
||||
return _L("Undefined");
|
||||
}
|
||||
case coPercent:
|
||||
return from_u8((boost::format("%1%%%") % int(config.optptr(pure_key)->getFloat())).str());
|
||||
case coPercents: {
|
||||
if (is_nullable) {
|
||||
auto values = config.opt<ConfigOptionPercentsNullable>(pure_key);
|
||||
if (opt_idx < values->size())
|
||||
return from_u8((boost::format("%1%%%") % values->get_at(opt_idx)).str());
|
||||
}
|
||||
else {
|
||||
auto values = config.opt<ConfigOptionPercents>(pure_key);
|
||||
if (opt_idx < values->size())
|
||||
return from_u8((boost::format("%1%%%") % values->get_at(opt_idx)).str());
|
||||
}
|
||||
return _L("Undefined");
|
||||
}
|
||||
case coFloat:
|
||||
return double_to_string(config.opt_float(pure_key));
|
||||
case coFloats: {
|
||||
if (is_nullable) {
|
||||
auto values = config.opt<ConfigOptionFloatsNullable>(pure_key);
|
||||
if (opt_idx < values->size())
|
||||
return double_to_string(values->get_at(opt_idx));
|
||||
}
|
||||
else {
|
||||
auto values = config.opt<ConfigOptionFloats>(pure_key);
|
||||
if (values && opt_idx < values->size())
|
||||
return double_to_string(values->get_at(opt_idx));
|
||||
}
|
||||
return _L("Undefined");
|
||||
}
|
||||
case coString:
|
||||
return from_u8(config.opt_string(pure_key));
|
||||
case coStrings: {
|
||||
const ConfigOptionStrings* strings = config.opt<ConfigOptionStrings>(pure_key);
|
||||
if (strings) {
|
||||
if (pure_key == "compatible_printers" || pure_key == "compatible_prints") {
|
||||
if (strings->empty())
|
||||
return _L("All");
|
||||
for (size_t id = 0; id < strings->size(); id++)
|
||||
out += from_u8(strings->get_at(id)) + "\n";
|
||||
out.RemoveLast(1);
|
||||
return out;
|
||||
}
|
||||
if (!strings->empty() && opt_idx < strings->values.size())
|
||||
return from_u8(strings->get_at(opt_idx));
|
||||
}
|
||||
break;
|
||||
}
|
||||
case coFloatOrPercent: {
|
||||
const ConfigOptionFloatOrPercent* opt = config.opt<ConfigOptionFloatOrPercent>(pure_key);
|
||||
if (opt)
|
||||
out = double_to_string(opt->value) + (opt->percent ? "%" : "");
|
||||
return out;
|
||||
}
|
||||
case coEnum: {
|
||||
return get_string_from_enum(pure_key, config,
|
||||
pure_key == "top_surface_pattern" ||
|
||||
pure_key == "bottom_surface_pattern" ||
|
||||
pure_key == "internal_solid_infill_pattern" ||
|
||||
pure_key == "sparse_infill_pattern" ||
|
||||
pure_key == "ironing_pattern" ||
|
||||
pure_key == "support_ironing_pattern" ||
|
||||
pure_key == "support_pattern" ||
|
||||
pure_key == "support_interface_pattern")
|
||||
;
|
||||
}
|
||||
case coEnums: {
|
||||
return get_string_from_enum(pure_key, config,
|
||||
pure_key == "top_surface_pattern" ||
|
||||
pure_key == "bottom_surface_pattern" ||
|
||||
pure_key == "internal_solid_infill_pattern" ||
|
||||
pure_key == "sparse_infill_pattern" ||
|
||||
pure_key == "ironing_pattern" ||
|
||||
pure_key == "support_ironing_pattern" ||
|
||||
pure_key == "support_pattern" ||
|
||||
pure_key == "support_interface_pattern"
|
||||
, opt_idx);
|
||||
}
|
||||
case coPoint: {
|
||||
Vec2d val = config.opt<ConfigOptionPoint>(pure_key)->value;
|
||||
return from_u8((boost::format("[%1%]") % ConfigOptionPoint(val).serialize()).str());
|
||||
}
|
||||
case coPoints: {
|
||||
//BBS: add bed_exclude_area
|
||||
if (pure_key == "printable_area" || pure_key == "thumbnails") {
|
||||
ConfigOptionPoints points = *config.option<ConfigOptionPoints>(pure_key);
|
||||
//BuildVolume build_volume = {points.values, 0.};
|
||||
return get_thumbnails_string(points.values);
|
||||
}
|
||||
else if (pure_key == "bed_exclude_area") {
|
||||
return get_thumbnails_string(config.option<ConfigOptionPoints>(pure_key)->values);
|
||||
}
|
||||
else if (pure_key == "head_wrap_detect_zone") {
|
||||
return get_thumbnails_string(config.option<ConfigOptionPoints>(pure_key)->values);
|
||||
}
|
||||
else if (pure_key == "wrapping_exclude_area") {
|
||||
return get_thumbnails_string(config.option<ConfigOptionPoints>(pure_key)->values);
|
||||
}
|
||||
Vec2d val = config.opt<ConfigOptionPoints>(pure_key)->get_at(opt_idx);
|
||||
return from_u8((boost::format("[%1%]") % ConfigOptionPoint(val).serialize()).str());
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
} // namespace GUI
|
||||
} // namespace Slic3r
|
||||
@@ -0,0 +1,26 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
#include <wx/string.h>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
class DynamicPrintConfig;
|
||||
|
||||
namespace GUI {
|
||||
|
||||
// Human-readable value of opt_key (may carry a "#<index>" suffix) in config.
|
||||
wxString get_string_value(const std::string& opt_key, const DynamicPrintConfig& config);
|
||||
|
||||
// Full label of opt_key; "N/A" when the option is not set.
|
||||
wxString get_full_label(const std::string& opt_key, const DynamicPrintConfig& config);
|
||||
|
||||
// Strip the "#<index>" suffix (if any) from the option key.
|
||||
std::string get_pure_opt_key(const std::string& opt_key);
|
||||
|
||||
// Localized label of the currently selected value of an enum option.
|
||||
wxString get_string_from_enum(const std::string& opt_key, const DynamicPrintConfig& config, bool is_infill = false, int idx = -1);
|
||||
|
||||
} // namespace GUI
|
||||
} // namespace Slic3r
|
||||
@@ -1,16 +1,66 @@
|
||||
#include <wx/dcmemory.h>
|
||||
#include <wx/dcgraph.h>
|
||||
#include <wx/graphics.h>
|
||||
#include <wx/settings.h>
|
||||
#include <wx/window.h>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <map>
|
||||
#include <numeric>
|
||||
#include <string>
|
||||
#include <tuple>
|
||||
|
||||
#include "EncodedFilament.hpp"
|
||||
#include "FilamentBitmapUtils.hpp"
|
||||
#include "GUI_App.hpp"
|
||||
#include "GuiColor.hpp"
|
||||
#include "I18N.hpp"
|
||||
#include "Widgets/Label.hpp"
|
||||
#include "Widgets/StateColor.hpp"
|
||||
#include "libslic3r/FilamentMixer.hpp"
|
||||
#include "libslic3r/PrintConfig.hpp"
|
||||
|
||||
namespace Slic3r { namespace GUI {
|
||||
|
||||
// Barycentric utilities for a ternary (triangle) ratio picker.
|
||||
double tri_signed_area2(TriPoint a, TriPoint b, TriPoint c)
|
||||
{
|
||||
return (b.x - a.x) * (c.y - a.y) - (c.x - a.x) * (b.y - a.y);
|
||||
}
|
||||
|
||||
bool tri_contains(TriPoint p, TriPoint v0, TriPoint v1, TriPoint v2)
|
||||
{
|
||||
double total = tri_signed_area2(v0, v1, v2);
|
||||
if (std::abs(total) < 1e-9) return false;
|
||||
double s0 = tri_signed_area2(p, v1, v2) / total;
|
||||
double s1 = tri_signed_area2(v0, p, v2) / total;
|
||||
double s2 = 1.0 - s0 - s1;
|
||||
return s0 >= -0.001 && s1 >= -0.001 && s2 >= -0.001;
|
||||
}
|
||||
|
||||
void tri_barycentric(TriPoint p, TriPoint v0, TriPoint v1, TriPoint v2,
|
||||
double& w0, double& w1, double& w2)
|
||||
{
|
||||
double total = std::abs(tri_signed_area2(v0, v1, v2));
|
||||
if (total < 1e-9) { w0 = w1 = w2 = 1.0 / 3.0; return; }
|
||||
w0 = std::abs(tri_signed_area2(p, v1, v2)) / total;
|
||||
w1 = std::abs(tri_signed_area2(v0, p, v2)) / total;
|
||||
w2 = 1.0 - w0 - w1;
|
||||
w0 = std::clamp(w0, 0.0, 1.0);
|
||||
w1 = std::clamp(w1, 0.0, 1.0);
|
||||
w2 = std::clamp(w2, 0.0, 1.0);
|
||||
double s = w0 + w1 + w2;
|
||||
if (s > 0) { w0 /= s; w1 /= s; w2 /= s; }
|
||||
}
|
||||
|
||||
TriPoint tri_clamp(TriPoint p, TriPoint v0, TriPoint v1, TriPoint v2)
|
||||
{
|
||||
double w0, w1, w2;
|
||||
tri_barycentric(p, v0, v1, v2, w0, w1, w2);
|
||||
return {w0 * v0.x + w1 * v1.x + w2 * v2.x,
|
||||
w0 * v0.y + w1 * v1.y + w2 * v2.y};
|
||||
}
|
||||
|
||||
void fill_gradient_rect_east(wxDC& dc, const wxRect& rect, const wxColour& from, const wxColour& to)
|
||||
{
|
||||
if (rect.width <= 0 || rect.height <= 0) return;
|
||||
@@ -73,7 +123,7 @@ std::vector<wxColour> sample_gradient_ramp(const wxColour& first,
|
||||
// Resolve the curve a gradient slot is sampled with, mirroring the slicer's fallback in
|
||||
// ToolOrdering: a custom curve wins, otherwise a straight line between gradient_range's
|
||||
// endpoints, otherwise the 0.10 -> 0.90 default.
|
||||
static Slic3r::GradientCurve mixed_gradient_curve(const Slic3r::DynamicPrintConfig& cfg, size_t slot)
|
||||
Slic3r::GradientCurve mixed_gradient_curve(const Slic3r::DynamicPrintConfig& cfg, size_t slot)
|
||||
{
|
||||
const auto* curve_opt = cfg.option<ConfigOptionStrings>("filament_mixed_gradient_curve");
|
||||
if (curve_opt && slot < curve_opt->values.size() && !curve_opt->values[slot].empty()) {
|
||||
@@ -449,4 +499,378 @@ void recompute_mixed_slot_colors(std::vector<wxColour>& colors,
|
||||
}
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
// Layout ratios of the gradient plot rect, copied from GradientCurveEditor so the read-only
|
||||
// preview and the interactive editor stay pixel-identical. Plot rect is square 1:1; the
|
||||
// right/bottom margins host the axis arrows and labels.
|
||||
constexpr double kPlotLeftRatio = 0.0316;
|
||||
constexpr double kPlotRightRatio = 0.6766;
|
||||
constexpr double kPlotTopRatio = 0.1529;
|
||||
constexpr double kPlotBottomRatio = 0.8474;
|
||||
constexpr int kGridDivisions = 9; // 10 grid lines including the outer borders.
|
||||
constexpr int kStrokeAxis = 2; // axis line width (px, no DPI scaling)
|
||||
constexpr int kAxisArrowHalf = 5; // half-base of the axis arrow triangle (DIP)
|
||||
constexpr int kAxisArrowLen = 10; // length of the axis arrow triangle (DIP)
|
||||
constexpr int kPointRadius = 4; // anchor outer radius (DIP)
|
||||
constexpr float kBgSimilarThreshold = 15.0f;
|
||||
constexpr int kOutlineExtraDip = 2;
|
||||
constexpr double kTriangleMarginDip = 20.0;
|
||||
|
||||
// Quadratic blend that never goes out of gamut, matching MixedFilamentDialog::blend_colors.
|
||||
wxColour lerp_blend(const wxColour& a, const wxColour& b, double ratio_a)
|
||||
{
|
||||
unsigned char r, g, bl;
|
||||
Slic3r::filament_mixer_lerp(a.Red(), a.Green(), a.Blue(),
|
||||
b.Red(), b.Green(), b.Blue(),
|
||||
static_cast<float>(1.0 - ratio_a), &r, &g, &bl);
|
||||
return wxColour(r, g, bl);
|
||||
}
|
||||
|
||||
// DIP conversion for these free functions: unlike the wxWindow member FromDIP, it needs the
|
||||
// window parameter explicitly; nullptr picks the app's default DPI like the Publish dialog does.
|
||||
int dip_px(int v) { return wxWindow::FromDIP(v, nullptr); }
|
||||
|
||||
} // namespace
|
||||
|
||||
wxRect mixed_gradient_plot_rect(const wxSize& sz)
|
||||
{
|
||||
const int x = static_cast<int>(std::lround(sz.x * kPlotLeftRatio));
|
||||
const int y = static_cast<int>(std::lround(sz.y * kPlotTopRatio));
|
||||
const int x2 = static_cast<int>(std::lround(sz.x * kPlotRightRatio));
|
||||
const int y2 = static_cast<int>(std::lround(sz.y * kPlotBottomRatio));
|
||||
const int side = std::max(1, std::min(x2 - x, y2 - y));
|
||||
return wxRect(x, y, side, side);
|
||||
}
|
||||
|
||||
void draw_mixed_gradient_plot(wxDC& raw_dc, const wxSize& canvas,
|
||||
const std::vector<MixedGradientCurve>& curves,
|
||||
const std::vector<wxPoint2DDouble>& anchors,
|
||||
const MixedGradientTheme& theme)
|
||||
{
|
||||
// Draw into an internal opaque buffer so wxGCDC text/curves anti-alias against a solid
|
||||
// background (never a transparent one), then blit the finished image onto the caller's
|
||||
// buffered paint DC. wxGCDC cannot wrap a generic wxDC&, so the buffer is always a
|
||||
// wxMemoryDC -- the one type wxGCDC accepts on every platform.
|
||||
if (canvas.x <= 0 || canvas.y <= 0)
|
||||
return;
|
||||
const wxRect rc = mixed_gradient_plot_rect(canvas);
|
||||
if (rc.width <= 0 || rc.height <= 0)
|
||||
return;
|
||||
|
||||
wxBitmap buf(canvas);
|
||||
wxMemoryDC memdc(buf);
|
||||
memdc.SetBackground(wxBrush(theme.background));
|
||||
memdc.Clear();
|
||||
wxGCDC dc(memdc);
|
||||
wxGraphicsContext* gc = dc.GetGraphicsContext();
|
||||
|
||||
// 10x10 light grid (10 lines including outer borders, 9 equal divisions).
|
||||
dc.SetPen(wxPen(theme.grid, 1));
|
||||
for (int i = 0; i <= kGridDivisions; ++i) {
|
||||
const int x = rc.x + rc.width * i / kGridDivisions;
|
||||
const int y = rc.y + rc.height * i / kGridDivisions;
|
||||
dc.DrawLine(x, rc.y, x, rc.y + rc.height);
|
||||
dc.DrawLine(rc.x, y, rc.x + rc.width, y);
|
||||
}
|
||||
|
||||
// Set the label font first so text width measurements drive arrow / label placement.
|
||||
wxFont label_font = wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT);
|
||||
label_font.SetPointSize(std::max(7, label_font.GetPointSize() - 1));
|
||||
dc.SetFont(label_font);
|
||||
|
||||
const wxString axis_y_title = _L("Material Ratio");
|
||||
const wxString axis_x_title = _L("Model Height");
|
||||
const wxString pct_text = wxT("100%");
|
||||
const wxSize x_title_sz = dc.GetTextExtent(axis_x_title);
|
||||
const wxSize y_title_sz = dc.GetTextExtent(axis_y_title);
|
||||
|
||||
wxFont strong_font = label_font;
|
||||
strong_font.SetWeight(wxFONTWEIGHT_SEMIBOLD);
|
||||
dc.SetFont(strong_font);
|
||||
const wxSize pct_text_sz = dc.GetTextExtent(pct_text);
|
||||
dc.SetFont(label_font);
|
||||
|
||||
// Axes (grey 700) with filled triangle arrows. Y-axis extends above the plot top to the
|
||||
// canvas top edge; X-axis extends past the plot right toward the canvas right edge.
|
||||
const int arrow_half = dip_px(kAxisArrowHalf);
|
||||
const int arrow_len = dip_px(kAxisArrowLen);
|
||||
dc.SetPen(wxPen(theme.axis, kStrokeAxis));
|
||||
dc.SetBrush(wxBrush(theme.axis));
|
||||
|
||||
const int y_axis_x = rc.x;
|
||||
const int y_title_pct_gap = dip_px(1);
|
||||
const int y_title_bottom_pad = dip_px(2);
|
||||
const int y_title_y = std::max(0, rc.y - y_title_sz.y - y_title_pct_gap - pct_text_sz.y - y_title_bottom_pad);
|
||||
const int y_arrow_tip_y = y_title_y;
|
||||
const int y_arrow_ty = y_arrow_tip_y + arrow_len;
|
||||
dc.DrawLine(y_axis_x, y_arrow_ty, y_axis_x, rc.y + rc.height);
|
||||
{
|
||||
wxPoint tri[3] = {
|
||||
wxPoint(y_axis_x, y_arrow_tip_y),
|
||||
wxPoint(y_axis_x - arrow_half, y_arrow_ty),
|
||||
wxPoint(y_axis_x + arrow_half, y_arrow_ty),
|
||||
};
|
||||
dc.DrawPolygon(3, tri);
|
||||
}
|
||||
|
||||
const int x_axis_y = rc.y + rc.height;
|
||||
const int x_label_gap = dip_px(4);
|
||||
const int x_edge_pad = dip_px(6);
|
||||
const int x_arrow_ideal = rc.x + rc.width + dip_px(10);
|
||||
const int x_arrow_max = canvas.x - x_title_sz.x - x_label_gap - x_edge_pad - arrow_len;
|
||||
const int x_arrow_tx = std::max(rc.x + rc.width + arrow_len, std::min(x_arrow_ideal, x_arrow_max));
|
||||
const int x_arrow_tip_x = x_arrow_tx + arrow_len;
|
||||
const int x_title_x = x_arrow_tip_x + x_label_gap;
|
||||
dc.DrawLine(rc.x, x_axis_y, x_arrow_tx, x_axis_y);
|
||||
{
|
||||
wxPoint tri[3] = {
|
||||
wxPoint(x_arrow_tip_x, x_axis_y),
|
||||
wxPoint(x_arrow_tx, x_axis_y - arrow_half),
|
||||
wxPoint(x_arrow_tx, x_axis_y + arrow_half),
|
||||
};
|
||||
dc.DrawPolygon(3, tri);
|
||||
}
|
||||
|
||||
// Labels: "Material Ratio" and the leading "100%" share the same left x; the trailing
|
||||
// "Model Height" follows the X-axis arrow tip (already clamped to make room).
|
||||
const int label_left_x = y_axis_x + dip_px(10);
|
||||
dc.SetTextForeground(theme.label);
|
||||
dc.DrawText(axis_y_title, label_left_x, y_title_y);
|
||||
|
||||
dc.SetFont(strong_font);
|
||||
dc.SetTextForeground(theme.label_strong);
|
||||
dc.DrawText(pct_text, label_left_x, y_title_y + y_title_sz.y + y_title_pct_gap);
|
||||
|
||||
dc.DrawText(pct_text, rc.x + rc.width - pct_text_sz.x, x_axis_y);
|
||||
dc.SetFont(label_font);
|
||||
dc.SetTextForeground(theme.label);
|
||||
dc.DrawText(axis_x_title, x_title_x, x_axis_y - x_title_sz.y / 2);
|
||||
|
||||
if (!gc)
|
||||
return;
|
||||
|
||||
// Outline only when the curve colour is perceptually close to the background; otherwise the
|
||||
// plain filament colour reads fine and the extra stroke would look heavy.
|
||||
auto needs_outline = [&](const wxColour& c) {
|
||||
return calc_color_distance(c, theme.background) < kBgSimilarThreshold;
|
||||
};
|
||||
|
||||
// Only the geometry goes through the graphics context: dc.DrawLines() takes integer wxPoint
|
||||
// and would quantize the curve back to whole pixels. The pen is still set on the dc, which
|
||||
// forwards it here while keeping its own cached state in sync for later dc drawing.
|
||||
auto draw_polyline = [&](const MixedGradientCurve& curve) {
|
||||
if (curve.points.size() < 2)
|
||||
return;
|
||||
dc.SetPen(wxPen(curve.colour, dip_px(curve.stroke_dip)));
|
||||
gc->StrokeLines(curve.points.size(), curve.points.data());
|
||||
};
|
||||
|
||||
for (const MixedGradientCurve& curve : curves) {
|
||||
if (needs_outline(curve.colour))
|
||||
draw_polyline({curve.points, theme.outline, curve.stroke_dip + kOutlineExtraDip});
|
||||
draw_polyline(curve);
|
||||
}
|
||||
|
||||
// Control points: hollow circle with axis-colour border, theme-aware fill, drawn with a
|
||||
// sub-pixel centre so the ring stays centred on the curve.
|
||||
if (!anchors.empty()) {
|
||||
const double r = dip_px(kPointRadius);
|
||||
dc.SetPen(wxPen(theme.axis, 1));
|
||||
dc.SetBrush(wxBrush(theme.point_fill));
|
||||
for (const wxPoint2DDouble& p : anchors)
|
||||
gc->DrawEllipse(p.m_x - r, p.m_y - r, r * 2, r * 2);
|
||||
}
|
||||
|
||||
memdc.SelectObject(wxNullBitmap);
|
||||
raw_dc.DrawBitmap(buf, 0, 0);
|
||||
}
|
||||
|
||||
void draw_mixed_ratio_blend_bar(wxDC& dc, const wxRect& rect, const wxColour& first,
|
||||
const wxColour& second, double second_fraction)
|
||||
{
|
||||
if (rect.width <= 0 || rect.height <= 0)
|
||||
return;
|
||||
for (int x = 0; x < rect.width; ++x) {
|
||||
const double t = rect.width > 1 ? double(x) / rect.width : 0.0;
|
||||
const wxColour c = lerp_blend(first, second, 1.0 - t);
|
||||
dc.SetPen(wxPen(c));
|
||||
dc.DrawLine(rect.x + x, rect.y, rect.x + x, rect.y + rect.height);
|
||||
}
|
||||
|
||||
// Fixed in both themes, like the triangle picker's drag handle: the divider is drawn over
|
||||
// blended filament colour, so it has to keep its contrast against data rather than chrome.
|
||||
const int div_x = rect.x + static_cast<int>(second_fraction * rect.width);
|
||||
dc.SetPen(wxPen(wxColour(80, 80, 80), dip_px(4)));
|
||||
dc.DrawLine(div_x, rect.y, div_x, rect.y + rect.height);
|
||||
dc.SetPen(wxPen(*wxWHITE, dip_px(2)));
|
||||
dc.DrawLine(div_x, rect.y, div_x, rect.y + rect.height);
|
||||
}
|
||||
|
||||
void draw_mixed_ratio_segments(wxDC& dc, const wxRect& rect, const std::vector<wxColour>& colours,
|
||||
const std::vector<double>& shares)
|
||||
{
|
||||
const size_t n = std::min(colours.size(), shares.size());
|
||||
if (n == 0 || rect.width <= 0 || rect.height <= 0)
|
||||
return;
|
||||
std::vector<double> norm = shares;
|
||||
double total = 0.0;
|
||||
for (double s : norm)
|
||||
total += s;
|
||||
if (total <= 0.0) {
|
||||
norm.assign(n, 1.0 / n);
|
||||
total = 1.0;
|
||||
}
|
||||
auto share_to_px = [&](double share_sum) { return rect.x + int(std::lround(share_sum / total * double(rect.width))); };
|
||||
int x0 = rect.x;
|
||||
std::vector<wxRect> segs(n);
|
||||
for (size_t i = 0; i < n; ++i) {
|
||||
int x1 = rect.x + rect.width;
|
||||
if (i + 1 < n)
|
||||
x1 = share_to_px(std::accumulate(norm.begin(), norm.begin() + i + 1, 0.0));
|
||||
segs[i] = wxRect(x0, rect.y, std::max(1, x1 - x0), rect.height);
|
||||
x0 = segs[i].GetRight() + 1;
|
||||
}
|
||||
for (size_t i = 0; i < n; ++i) {
|
||||
dc.SetPen(*wxTRANSPARENT_PEN);
|
||||
dc.SetBrush(wxBrush(colours[i]));
|
||||
dc.DrawRectangle(segs[i]);
|
||||
}
|
||||
dc.SetBrush(*wxTRANSPARENT_BRUSH);
|
||||
dc.SetPen(wxPen(StateColor::darkModeColorFor(wxColour("#ACACAC")), 1));
|
||||
dc.DrawRectangle(rect);
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
struct TriCacheKey
|
||||
{
|
||||
int w, h;
|
||||
int c0r, c0g, c0b, c1r, c1g, c1b, c2r, c2g, c2b;
|
||||
int bg_r, bg_g, bg_b, ol_r, ol_g, ol_b;
|
||||
bool operator<(const TriCacheKey& o) const
|
||||
{
|
||||
return std::tie(w, h, c0r, c0g, c0b, c1r, c1g, c1b, c2r, c2g, c2b, bg_r, bg_g, bg_b, ol_r, ol_g, ol_b) <
|
||||
std::tie(o.w, o.h, o.c0r, o.c0g, o.c0b, o.c1r, o.c1g, o.c1b, o.c2r, o.c2g, o.c2b, o.bg_r, o.bg_g, o.bg_b, o.ol_r, o.ol_g, o.ol_b);
|
||||
}
|
||||
};
|
||||
|
||||
std::map<TriCacheKey, wxBitmap>& tri_cache()
|
||||
{
|
||||
static std::map<TriCacheKey, wxBitmap> cache;
|
||||
return cache;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::array<TriPoint, 3> mixed_triangle_vertices(const wxSize& size, double margin_dip)
|
||||
{
|
||||
const double pw = size.GetWidth(), ph = size.GetHeight();
|
||||
const double margin = dip_px(int(margin_dip));
|
||||
const double avail = std::min(pw, ph) - 2.0 * margin;
|
||||
const double side = avail;
|
||||
const double tri_h = side * std::sqrt(3.0) / 2.0;
|
||||
const double cx = pw / 2.0;
|
||||
const double top_y = (ph - tri_h) / 2.0;
|
||||
return {{{cx, top_y}, {cx - side / 2.0, top_y + tri_h}, {cx + side / 2.0, top_y + tri_h}}};
|
||||
}
|
||||
|
||||
void draw_mixed_triangle_picker(wxDC& dc, const wxSize& size, const std::array<wxColour, 3>& colours,
|
||||
const std::array<double, 3>& weights, const MixedTriangleTheme& theme)
|
||||
{
|
||||
if (size.GetWidth() <= 0 || size.GetHeight() <= 0)
|
||||
return;
|
||||
const std::array<TriPoint, 3> v = mixed_triangle_vertices(size, kTriangleMarginDip);
|
||||
|
||||
dc.SetBrush(wxBrush(theme.background));
|
||||
dc.SetPen(*wxTRANSPARENT_PEN);
|
||||
dc.DrawRectangle(0, 0, size.GetWidth(), size.GetHeight());
|
||||
|
||||
const wxColour& c0 = colours[0];
|
||||
const wxColour& c1 = colours[1];
|
||||
const wxColour& c2 = colours[2];
|
||||
const TriCacheKey key{size.GetWidth(), size.GetHeight(),
|
||||
c0.Red(), c0.Green(), c0.Blue(),
|
||||
c1.Red(), c1.Green(), c1.Blue(),
|
||||
c2.Red(), c2.Green(), c2.Blue(),
|
||||
theme.background.Red(), theme.background.Green(), theme.background.Blue(),
|
||||
theme.outline.Red(), theme.outline.Green(), theme.outline.Blue()};
|
||||
|
||||
wxBitmap& bmp = tri_cache()[key];
|
||||
if (!bmp.IsOk()) {
|
||||
bmp = wxBitmap(size.GetWidth(), size.GetHeight(), 24);
|
||||
wxMemoryDC mdc(bmp);
|
||||
mdc.SetBrush(wxBrush(theme.background));
|
||||
mdc.SetPen(*wxTRANSPARENT_PEN);
|
||||
mdc.DrawRectangle(0, 0, size.GetWidth(), size.GetHeight());
|
||||
|
||||
const int min_y = int(std::min({v[0].y, v[1].y, v[2].y}));
|
||||
const int max_y = int(std::max({v[0].y, v[1].y, v[2].y}));
|
||||
const int min_x = int(std::min({v[0].x, v[1].x, v[2].x}));
|
||||
const int max_x = int(std::max({v[0].x, v[1].x, v[2].x}));
|
||||
for (int py = min_y; py <= max_y; ++py) {
|
||||
for (int px = min_x; px <= max_x; ++px) {
|
||||
const TriPoint p = {double(px), double(py)};
|
||||
if (!tri_contains(p, v[0], v[1], v[2]))
|
||||
continue;
|
||||
double w0, w1, w2;
|
||||
tri_barycentric(p, v[0], v[1], v[2], w0, w1, w2);
|
||||
unsigned char mr, mg, mb;
|
||||
if (w0 + w1 > 1e-6) {
|
||||
float t01 = float(w1 / (w0 + w1));
|
||||
Slic3r::filament_mixer_lerp(c0.Red(), c0.Green(), c0.Blue(), c1.Red(), c1.Green(), c1.Blue(), t01, &mr, &mg, &mb);
|
||||
Slic3r::filament_mixer_lerp(mr, mg, mb, c2.Red(), c2.Green(), c2.Blue(), float(w2), &mr, &mg, &mb);
|
||||
} else {
|
||||
mr = c2.Red(); mg = c2.Green(); mb = c2.Blue();
|
||||
}
|
||||
mdc.SetPen(wxPen(wxColour(mr, mg, mb)));
|
||||
mdc.DrawPoint(px, py);
|
||||
}
|
||||
}
|
||||
|
||||
mdc.SetPen(wxPen(theme.outline, 1));
|
||||
mdc.SetBrush(*wxTRANSPARENT_BRUSH);
|
||||
const wxPoint pts[3] = {{int(v[0].x), int(v[0].y)}, {int(v[1].x), int(v[1].y)}, {int(v[2].x), int(v[2].y)}};
|
||||
mdc.DrawPolygon(3, pts);
|
||||
mdc.SelectObject(wxNullBitmap);
|
||||
|
||||
// Keep the cache from growing without bound across DPI/size changes.
|
||||
if (tri_cache().size() > 6) {
|
||||
auto& cache = tri_cache();
|
||||
cache.erase(cache.begin());
|
||||
}
|
||||
}
|
||||
dc.DrawBitmap(bmp, 0, 0);
|
||||
|
||||
// Published-ratio marker (read-only twin of the editor's drag handle).
|
||||
const double w0 = weights[0], w1 = weights[1], w2 = weights[2];
|
||||
const int hx = int(w0 * v[0].x + w1 * v[1].x + w2 * v[2].x);
|
||||
const int hy = int(w0 * v[0].y + w1 * v[1].y + w2 * v[2].y);
|
||||
dc.SetBrush(*wxWHITE_BRUSH);
|
||||
dc.SetPen(wxPen(theme.ring, dip_px(2)));
|
||||
dc.DrawCircle(hx, hy, dip_px(5));
|
||||
}
|
||||
|
||||
void draw_mixed_triangle_labels(wxDC& dc, const wxSize& size, const std::array<double, 3>& weights,
|
||||
const MixedTriangleTheme& theme)
|
||||
{
|
||||
const std::array<TriPoint, 3> v = mixed_triangle_vertices(size, kTriangleMarginDip);
|
||||
dc.SetFont(::Label::Body_12);
|
||||
dc.SetTextForeground(theme.label);
|
||||
|
||||
// "Ratio" title, sitting above the top vertex.
|
||||
const wxString title = _L("Ratio");
|
||||
dc.DrawText(title, dip_px(2), std::max(0, int(v[0].y - dc.GetTextExtent(title).GetHeight() - dip_px(4))));
|
||||
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
const wxString text = wxString::Format("%d%%", int(std::lround(weights[i] * 100.0)));
|
||||
const wxSize tsz = dc.GetTextExtent(text);
|
||||
int lx = int(v[i].x - tsz.GetWidth() / 2.0);
|
||||
int ly = (i == 0) ? int(v[i].y - tsz.GetHeight() - dip_px(4)) : int(v[i].y + dip_px(3));
|
||||
ly = std::clamp(ly, 0, size.GetHeight() - tsz.GetHeight());
|
||||
lx = std::clamp(lx, 0, size.GetWidth() - tsz.GetWidth());
|
||||
dc.DrawText(text, lx, ly);
|
||||
}
|
||||
}
|
||||
|
||||
}} // namespace Slic3r::GUI
|
||||
@@ -5,6 +5,9 @@
|
||||
#include <wx/colour.h>
|
||||
#include <wx/dc.h>
|
||||
#include <wx/gdicmn.h>
|
||||
#include <wx/geometry.h>
|
||||
#include <wx/graphics.h>
|
||||
#include <array>
|
||||
#include <vector>
|
||||
|
||||
// Orca: forward-declare so the header is self-contained outside libslic3r_gui's
|
||||
@@ -13,6 +16,16 @@ namespace Slic3r { class DynamicPrintConfig; struct GradientCurve; }
|
||||
|
||||
namespace Slic3r { namespace GUI {
|
||||
|
||||
// Barycentric utilities for a ternary (triangle) ratio picker, shared by the mixed-filament
|
||||
// editor and the Publish dialog's read-only definition preview.
|
||||
struct TriPoint { double x, y; };
|
||||
|
||||
double tri_signed_area2(TriPoint a, TriPoint b, TriPoint c);
|
||||
bool tri_contains(TriPoint p, TriPoint v0, TriPoint v1, TriPoint v2);
|
||||
void tri_barycentric(TriPoint p, TriPoint v0, TriPoint v1, TriPoint v2,
|
||||
double& w0, double& w1, double& w2);
|
||||
TriPoint tri_clamp(TriPoint p, TriPoint v0, TriPoint v1, TriPoint v2);
|
||||
|
||||
// Fills a rect with a west->east linear gradient by drawing solid 1px columns.
|
||||
// Use instead of wxDC::GradientFillLinear, whose CoreGraphics (CGShading) backend
|
||||
// fails to render on some macOS builds; solid fills are unaffected.
|
||||
@@ -51,6 +64,12 @@ std::vector<wxColour> sample_gradient_ramp(const wxColour& first,
|
||||
// destination's height in pixels.
|
||||
std::vector<wxColour> mixed_gradient_ramp(const Slic3r::DynamicPrintConfig& cfg, size_t slot, int steps);
|
||||
|
||||
// Resolve the curve a gradient slot is sampled with: the custom curve wins when it has at
|
||||
// least two points, otherwise a straight line between gradient_range's endpoints, otherwise
|
||||
// the 0.10 -> 0.90 default. Mirrors the slicer's ToolOrdering fallback so every preview
|
||||
// agrees with what gets sliced. Always returns a two-point curve.
|
||||
Slic3r::GradientCurve mixed_gradient_curve(const Slic3r::DynamicPrintConfig& cfg, size_t slot);
|
||||
|
||||
// Fill rect with a ramp, ramp.front() along the bottom edge.
|
||||
void fill_gradient_ramp_rect(wxDC& dc, const wxRect& rect, const std::vector<wxColour>& ramp);
|
||||
|
||||
@@ -63,6 +82,81 @@ wxBitmap create_gradient_ramp_bitmap(const std::vector<wxColour>& ramp, const wx
|
||||
void recompute_mixed_slot_colors(std::vector<wxColour>& colors,
|
||||
const Slic3r::DynamicPrintConfig& cfg);
|
||||
|
||||
// --- Gradient plot (shared by GradientCurveEditor and the Publish dialog's read-only
|
||||
// preview). The plot is a square 1:1 rect laid out with the editor's ratios so both
|
||||
// render identically; curves are drawn as sub-pixel anti-aliased polylines through
|
||||
// wxGCDC so they never quantize to whole pixels.
|
||||
|
||||
// One curve of the plot: screen-space sub-pixel points already mapped into the plot
|
||||
// rect, the stroke colour and the stroke width in DIP.
|
||||
struct MixedGradientCurve
|
||||
{
|
||||
std::vector<wxPoint2DDouble> points;
|
||||
wxColour colour;
|
||||
int stroke_dip;
|
||||
};
|
||||
|
||||
// Theme tokens, resolved by the caller through StateColor::darkModeColorFor.
|
||||
struct MixedGradientTheme
|
||||
{
|
||||
wxColour background; // for near-background outline detection
|
||||
wxColour grid; // grid line
|
||||
wxColour axis; // axis + arrow fill
|
||||
wxColour label; // "Material Ratio" / "Model Height"
|
||||
wxColour label_strong; // "100%"
|
||||
wxColour outline; // near-background curve lift
|
||||
wxColour point_fill; // anchor fill
|
||||
};
|
||||
|
||||
// Square 1:1 plot rect inside `canvas`, using the editor's plot ratios.
|
||||
wxRect mixed_gradient_plot_rect(const wxSize& canvas);
|
||||
|
||||
// Draw the whole plot (grid, axes + arrowheads, axis labels, each curve with an optional
|
||||
// near-background outline, and anchor circles). `anchors` are empty when the caller has
|
||||
// none to show. `dc` is the caller's buffered paint DC; a wxGCDC is created inside so the
|
||||
// geometry gets anti-aliased.
|
||||
void draw_mixed_gradient_plot(wxDC& dc, const wxSize& canvas,
|
||||
const std::vector<MixedGradientCurve>& curves,
|
||||
const std::vector<wxPoint2DDouble>& anchors,
|
||||
const MixedGradientTheme& theme);
|
||||
|
||||
// --- Ratio bar (2-component continuous blend + divider, matching MixedFilamentDialog).
|
||||
// Colours blend first->second across the rect; the divider marks `second_fraction` of the
|
||||
// rect's width (the second component's share, 0..1).
|
||||
void draw_mixed_ratio_blend_bar(wxDC& dc, const wxRect& rect, const wxColour& first,
|
||||
const wxColour& second, double second_fraction);
|
||||
|
||||
// Fallback ratio bar for N>2 non-gradient slots: one solid segment per component,
|
||||
// widths proportional to shares. Label text (the "NN%" inside wide-enough segments) is
|
||||
// the caller's concern.
|
||||
void draw_mixed_ratio_segments(wxDC& dc, const wxRect& rect, const std::vector<wxColour>& colours,
|
||||
const std::vector<double>& shares);
|
||||
|
||||
// --- Triangle picker (3-component), shared by MixedFilamentDialog and the Publish preview.
|
||||
struct MixedTriangleTheme
|
||||
{
|
||||
wxColour background;
|
||||
wxColour outline; // triangle border
|
||||
wxColour ring; // drag-handle ring
|
||||
wxColour label; // "Ratio" title + per-vertex labels
|
||||
};
|
||||
|
||||
// The three vertices of the read-only/miniature triangle inside a `size` square panel,
|
||||
// with `margin_dip` inset. Order: top, bottom-left, bottom-right.
|
||||
std::array<TriPoint, 3> mixed_triangle_vertices(const wxSize& size, double margin_dip = 20.0);
|
||||
|
||||
// Draw background, the cached barycentric fill, the outline and the drag-handle marker.
|
||||
// `weights` are the three barycentric shares (sum 1). The "Ratio" title and per-vertex
|
||||
// percentage labels are drawn by the caller so the interactive editor can keep its own
|
||||
// live child labels while the read-only preview draws them as text.
|
||||
void draw_mixed_triangle_picker(wxDC& dc, const wxSize& size, const std::array<wxColour, 3>& colours,
|
||||
const std::array<double, 3>& weights, const MixedTriangleTheme& theme);
|
||||
|
||||
// Draw the "Ratio" title plus one "NN%" label per vertex (used by the read-only preview;
|
||||
// the interactive editor positions its own live labels instead).
|
||||
void draw_mixed_triangle_labels(wxDC& dc, const wxSize& size, const std::array<double, 3>& weights,
|
||||
const MixedTriangleTheme& theme);
|
||||
|
||||
}} // namespace Slic3r::GUI
|
||||
|
||||
#endif // slic3r_GUI_FilamentBitmapUtils_hpp_
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "GradientCurveEditor.hpp"
|
||||
#include "FilamentBitmapUtils.hpp"
|
||||
#include "GUI_App.hpp"
|
||||
#include "GuiColor.hpp"
|
||||
#include "I18N.hpp"
|
||||
@@ -19,23 +20,13 @@ namespace GUI {
|
||||
wxDEFINE_EVENT(wxEVT_GRADIENT_CURVE_CHANGED, wxCommandEvent);
|
||||
|
||||
namespace {
|
||||
// Layout ratios of the plot rect within the widget, taken from a 214 x 180 px reference drawing.
|
||||
// Plot rect occupies the upper-left region; right + bottom margins host axis arrows / labels.
|
||||
constexpr double kPlotLeftRatio = 0.0316;
|
||||
constexpr double kPlotRightRatio = 0.6766;
|
||||
constexpr double kPlotTopRatio = 0.1529;
|
||||
constexpr double kPlotBottomRatio = 0.8474;
|
||||
constexpr int kGridDivisions = 9; // 10 grid lines including the outer borders.
|
||||
|
||||
// Hit / stroke (DIP).
|
||||
// Hit / stroke (DIP). The plot-rect ratios, grid divisions, axis/arrow geometry and the
|
||||
// near-background outline threshold now live in FilamentBitmapUtils so the read-only Publish
|
||||
// preview and this editor stay pixel-identical.
|
||||
constexpr int kHitRadius = 6;
|
||||
constexpr int kCurveHitRadius = 5;
|
||||
constexpr int kPointRadius = 4; // anchor outer radius (DIP)
|
||||
constexpr int kStrokeUnselected = 2;
|
||||
constexpr int kStrokeSelected = 4;
|
||||
constexpr int kStrokeAxis = 2; // axis line width (px, no DPI scaling - matches kGridColor pen and 2DBed convention)
|
||||
constexpr int kAxisArrowHalf = 5; // half-base of the axis arrow triangle (DIP)
|
||||
constexpr int kAxisArrowLen = 10; // length of the axis arrow triangle (DIP)
|
||||
|
||||
// Light-mode design tokens. Resolved through StateColor::darkModeColorFor()
|
||||
// at paint time so the editor follows the app theme (#EEEEEE -> #4C4C55, #6B6B6B ->
|
||||
@@ -46,12 +37,6 @@ const wxColour kAxisColor (107, 107, 107); // #6B6B6B grey 700
|
||||
const wxColour kLabelMuted (107, 107, 107); // #6B6B6B grey 700
|
||||
const wxColour kLabelStrong ( 38, 46, 48); // #262E30 grey 900
|
||||
const wxColour kOutlineColor(172, 172, 172); // #ACACAC dimmed elements
|
||||
|
||||
// LAB (DeltaE76) threshold for "curve color is too close to the background": below it the curve
|
||||
// gets a subtle outline so it does not visually vanish, otherwise it is drawn plain. Looser than
|
||||
// the 5.0 of FlushPredict::is_similar_color, so a pastel pink on white still gets an outline.
|
||||
constexpr float kBgSimilarThreshold = 15.0f;
|
||||
constexpr int kOutlineExtraDip = 2;
|
||||
} // namespace
|
||||
|
||||
GradientCurveEditor::GradientCurveEditor(wxWindow* parent,
|
||||
@@ -177,15 +162,8 @@ void GradientCurveEditor::emit_changed()
|
||||
|
||||
wxRect GradientCurveEditor::plot_rect() const
|
||||
{
|
||||
const wxSize sz = GetClientSize();
|
||||
const int x = static_cast<int>(std::lround(sz.x * kPlotLeftRatio));
|
||||
const int y = static_cast<int>(std::lround(sz.y * kPlotTopRatio));
|
||||
const int x2 = static_cast<int>(std::lround(sz.x * kPlotRightRatio));
|
||||
const int y2 = static_cast<int>(std::lround(sz.y * kPlotBottomRatio));
|
||||
// Force square 1:1 so X/Y axes share the same scale and grid cells stay square. Anchor at
|
||||
// the top-left so the "100%" labels on the bottom/right still align with the plot edges.
|
||||
const int side = std::max(1, std::min(x2 - x, y2 - y));
|
||||
return wxRect(x, y, side, side);
|
||||
// Square 1:1 plot, shared with the Publish dialog's read-only preview.
|
||||
return mixed_gradient_plot_rect(GetClientSize());
|
||||
}
|
||||
|
||||
wxPoint2DDouble GradientCurveEditor::data_to_px_f(double x, double y) const
|
||||
@@ -330,171 +308,52 @@ void GradientCurveEditor::on_paint(wxPaintEvent& /*evt*/)
|
||||
raw_dc.SetBackground(wxBrush(bg));
|
||||
raw_dc.Clear();
|
||||
|
||||
// Render through wxGCDC so curves, arrows and anchor circles get anti-aliased; the buffered
|
||||
// DC is the actual back buffer that gets blitted to the window.
|
||||
wxGCDC dc(raw_dc);
|
||||
// The curve and its anchors are drawn straight on the graphics context so their
|
||||
// coordinates stay sub-pixel accurate (see data_to_px_f).
|
||||
wxGraphicsContext* gc = dc.GetGraphicsContext();
|
||||
|
||||
const wxRect rc = plot_rect();
|
||||
if (rc.width <= 0 || rc.height <= 0)
|
||||
return;
|
||||
|
||||
// 10x10 light grid (10 lines including outer borders, 9 equal divisions).
|
||||
dc.SetPen(wxPen(grid_color, 1));
|
||||
for (int i = 0; i <= kGridDivisions; ++i) {
|
||||
const int x = rc.x + rc.width * i / kGridDivisions;
|
||||
const int y = rc.y + rc.height * i / kGridDivisions;
|
||||
dc.DrawLine(x, rc.y, x, rc.y + rc.height);
|
||||
dc.DrawLine(rc.x, y, rc.x + rc.width, y);
|
||||
}
|
||||
|
||||
// Set the label font first so text width measurements drive arrow / label placement.
|
||||
wxFont label_font = wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT);
|
||||
label_font.SetPointSize(std::max(7, label_font.GetPointSize() - 1));
|
||||
dc.SetFont(label_font);
|
||||
|
||||
const wxString axis_y_title = _L("Material Ratio");
|
||||
const wxString axis_x_title = _L("Model Height");
|
||||
const wxString pct_text = wxT("100%");
|
||||
const wxSize x_title_sz = dc.GetTextExtent(axis_x_title);
|
||||
const wxSize y_title_sz = dc.GetTextExtent(axis_y_title);
|
||||
|
||||
wxFont strong_font = label_font;
|
||||
strong_font.SetWeight(wxFONTWEIGHT_SEMIBOLD);
|
||||
dc.SetFont(strong_font);
|
||||
const wxSize pct_text_sz = dc.GetTextExtent(pct_text);
|
||||
dc.SetFont(label_font);
|
||||
|
||||
// Axes (grey 700) with filled triangle arrows. Y-axis extends above the plot top to the
|
||||
// canvas top edge; X-axis extends past the plot right toward the canvas right edge.
|
||||
const int arrow_half = FromDIP(kAxisArrowHalf);
|
||||
const int arrow_len = FromDIP(kAxisArrowLen);
|
||||
const wxSize sz = GetClientSize();
|
||||
dc.SetPen(wxPen(axis_color, kStrokeAxis));
|
||||
dc.SetBrush(wxBrush(axis_color));
|
||||
|
||||
// Y-axis: vertical line at plot_left, from arrow tip near canvas top down to plot bottom.
|
||||
const int y_axis_x = rc.x;
|
||||
const int y_title_pct_gap = FromDIP(1);
|
||||
const int y_title_bottom_pad = FromDIP(2);
|
||||
const int y_title_y = std::max(0, rc.y - y_title_sz.y - y_title_pct_gap - pct_text_sz.y - y_title_bottom_pad);
|
||||
const int y_arrow_tip_y = y_title_y;
|
||||
const int y_arrow_ty = y_arrow_tip_y + arrow_len;
|
||||
dc.DrawLine(y_axis_x, y_arrow_ty, y_axis_x, rc.y + rc.height);
|
||||
{
|
||||
wxPoint tri[3] = {
|
||||
wxPoint(y_axis_x, y_arrow_tip_y),
|
||||
wxPoint(y_axis_x - arrow_half, y_arrow_ty),
|
||||
wxPoint(y_axis_x + arrow_half, y_arrow_ty),
|
||||
// Render the plot (grid, axes, labels, curves, anchors) through the shared painter so the
|
||||
// interactive editor and the Publish dialog's read-only preview stay pixel-identical. The
|
||||
// curves are handed over as sub-pixel polylines and anti-alias inside the helper.
|
||||
std::vector<MixedGradientCurve> curves;
|
||||
std::vector<wxPoint2DDouble> anchors;
|
||||
if (m_points.size() >= 2) {
|
||||
auto color_for_curve = [&](int curve_idx) -> wxColour {
|
||||
wxColour c = (curve_idx == 0) ? m_color_low : m_color_high;
|
||||
// Transparent filaments (alpha == 0, e.g. #FFFFFF00) would be invisible.
|
||||
if (c.Alpha() == 0)
|
||||
c.Set(c.Red(), c.Green(), c.Blue(), 150);
|
||||
return c;
|
||||
};
|
||||
dc.DrawPolygon(3, tri);
|
||||
}
|
||||
|
||||
// X-axis arrow tip: stays just past the plot ideally, but is clamped so the trailing
|
||||
// "Material Ratio" label still fits inside the canvas without overlapping the arrow.
|
||||
const int x_axis_y = rc.y + rc.height;
|
||||
const int x_label_gap = FromDIP(4);
|
||||
const int x_edge_pad = FromDIP(6);
|
||||
const int x_arrow_ideal = rc.x + rc.width + FromDIP(10);
|
||||
const int x_arrow_max = sz.x - x_title_sz.x - x_label_gap - x_edge_pad - arrow_len;
|
||||
const int x_arrow_tx = std::max(rc.x + rc.width + arrow_len,
|
||||
std::min(x_arrow_ideal, x_arrow_max));
|
||||
const int x_arrow_tip_x = x_arrow_tx + arrow_len;
|
||||
const int x_title_x = x_arrow_tip_x + x_label_gap;
|
||||
dc.DrawLine(rc.x, x_axis_y, x_arrow_tx, x_axis_y);
|
||||
{
|
||||
wxPoint tri[3] = {
|
||||
wxPoint(x_arrow_tip_x, x_axis_y),
|
||||
wxPoint(x_arrow_tx, x_axis_y - arrow_half),
|
||||
wxPoint(x_arrow_tx, x_axis_y + arrow_half),
|
||||
auto build_polyline = [&](int curve_idx) -> std::vector<wxPoint2DDouble> {
|
||||
const int samples = std::max(128, plot_rect().width * 2);
|
||||
std::vector<wxPoint2DDouble> poly;
|
||||
poly.reserve(samples + 1);
|
||||
for (int s = 0; s <= samples; ++s) {
|
||||
const double x = double(s) / samples;
|
||||
const double y0 = sample_curve_y(x);
|
||||
const double vy = to_visual_y(curve_idx, y0);
|
||||
poly.push_back(data_to_px_f(x, vy));
|
||||
}
|
||||
return poly;
|
||||
};
|
||||
dc.DrawPolygon(3, tri);
|
||||
}
|
||||
|
||||
// Labels.
|
||||
// "Model Height" and "100%" share the same left x; the gap is larger than the
|
||||
// axis-arrow half-base so the text never visually touches the Y-axis arrow.
|
||||
const int label_left_x = y_axis_x + FromDIP(10);
|
||||
dc.SetTextForeground(label_muted);
|
||||
dc.DrawText(axis_y_title, label_left_x, y_title_y);
|
||||
|
||||
dc.SetFont(strong_font);
|
||||
dc.SetTextForeground(label_strong);
|
||||
dc.DrawText(pct_text, label_left_x, y_title_y + y_title_sz.y + y_title_pct_gap);
|
||||
|
||||
// Bottom-right "100%" sits under the right end of the plot; "Material Ratio" follows the
|
||||
// X-axis arrow tip (placement was already clamped above to leave room).
|
||||
dc.DrawText(pct_text, rc.x + rc.width - pct_text_sz.x, x_axis_y);
|
||||
dc.SetFont(label_font);
|
||||
dc.SetTextForeground(label_muted);
|
||||
dc.DrawText(axis_x_title, x_title_x, x_axis_y - x_title_sz.y / 2);
|
||||
|
||||
if (m_points.size() < 2 || !gc)
|
||||
return;
|
||||
|
||||
auto color_for_curve = [&](int curve_idx) -> wxColour {
|
||||
wxColour c = (curve_idx == 0) ? m_color_low : m_color_high;
|
||||
// Transparent filaments (alpha == 0, e.g. #FFFFFF00) would be invisible.
|
||||
// Lift alpha so the curve stays visible while still hinting at transparency.
|
||||
if (c.Alpha() == 0)
|
||||
c.Set(c.Red(), c.Green(), c.Blue(), 150);
|
||||
return c;
|
||||
};
|
||||
|
||||
auto build_polyline = [&](int curve_idx) -> std::vector<wxPoint2DDouble> {
|
||||
const int samples = std::max(128, rc.width * 2);
|
||||
std::vector<wxPoint2DDouble> poly;
|
||||
poly.reserve(samples + 1);
|
||||
for (int s = 0; s <= samples; ++s) {
|
||||
const double x = double(s) / samples;
|
||||
const double y0 = sample_curve_y(x);
|
||||
const double vy = to_visual_y(curve_idx, y0);
|
||||
poly.push_back(data_to_px_f(x, vy));
|
||||
// Draw unselected first so the selected curve sits on top.
|
||||
const int other = 1 - m_selected_curve;
|
||||
for (const int idx : {other, m_selected_curve}) {
|
||||
std::vector<wxPoint2DDouble> pts = build_polyline(idx);
|
||||
if (pts.empty())
|
||||
continue;
|
||||
curves.push_back({std::move(pts), color_for_curve(idx), idx == m_selected_curve ? kStrokeSelected : kStrokeUnselected});
|
||||
}
|
||||
return poly;
|
||||
};
|
||||
|
||||
// Only the geometry goes through the graphics context: dc.DrawLines() takes integer wxPoint
|
||||
// and would quantize the curve back to whole pixels. The pen is still set on the dc, which
|
||||
// forwards it here while keeping its own cached state in sync for later dc drawing.
|
||||
auto draw_polyline = [&](const std::vector<wxPoint2DDouble>& poly, const wxColour& col, int stroke_dip) {
|
||||
dc.SetPen(wxPen(col, FromDIP(stroke_dip)));
|
||||
gc->StrokeLines(poly.size(), poly.data());
|
||||
};
|
||||
|
||||
// Outline only when the curve color is perceptually close to the background; otherwise
|
||||
// the plain filament color reads fine and the extra stroke would look heavy.
|
||||
auto needs_outline = [&](const wxColour& c) {
|
||||
return calc_color_distance(c, bg) < kBgSimilarThreshold;
|
||||
};
|
||||
|
||||
auto draw_one = [&](int curve_idx, int stroke_dip) {
|
||||
const auto poly = build_polyline(curve_idx);
|
||||
const wxColour col = color_for_curve(curve_idx);
|
||||
if (needs_outline(col))
|
||||
draw_polyline(poly, outline_color, stroke_dip + kOutlineExtraDip);
|
||||
draw_polyline(poly, col, stroke_dip);
|
||||
};
|
||||
|
||||
// Draw unselected first so the selected curve sits on top.
|
||||
const int other = 1 - m_selected_curve;
|
||||
draw_one(other, kStrokeUnselected);
|
||||
draw_one(m_selected_curve, kStrokeSelected);
|
||||
|
||||
// Control points (selected curve only): hollow circle with axis-color border, theme-aware fill.
|
||||
// Drawn on the graphics context with a sub-pixel center so the ring stays centered on the
|
||||
// curve instead of drifting up to half a pixel off it; pen and brush go through the dc for
|
||||
// the same reason as in draw_polyline above.
|
||||
const double r = FromDIP(kPointRadius);
|
||||
dc.SetPen(wxPen(axis_color, 1));
|
||||
dc.SetBrush(wxBrush(point_fill));
|
||||
for (size_t i = 0; i < m_points.size(); ++i) {
|
||||
const double vy = to_visual_y(m_selected_curve, m_points[i].y);
|
||||
const wxPoint2DDouble p = data_to_px_f(m_points[i].x, vy);
|
||||
gc->DrawEllipse(p.m_x - r, p.m_y - r, r * 2, r * 2);
|
||||
// Control points (selected curve only).
|
||||
anchors.reserve(m_points.size());
|
||||
for (size_t i = 0; i < m_points.size(); ++i) {
|
||||
const double vy = to_visual_y(m_selected_curve, m_points[i].y);
|
||||
anchors.push_back(data_to_px_f(m_points[i].x, vy));
|
||||
}
|
||||
}
|
||||
|
||||
const MixedGradientTheme theme{bg, grid_color, axis_color, label_muted, label_strong, outline_color, point_fill};
|
||||
draw_mixed_gradient_plot(raw_dc, GetClientSize(), curves, anchors, theme);
|
||||
}
|
||||
|
||||
void GradientCurveEditor::on_left_down(wxMouseEvent& evt)
|
||||
|
||||
@@ -174,6 +174,7 @@ void KBShortcutsDialog::fill_shortcuts()
|
||||
{ ctrl + "O", L("Open Project") },
|
||||
{ ctrl + "S", L("Save Project") },
|
||||
{ ctrl + shift + "S", L("Save Project as")},
|
||||
{ ctrl + shift + "E", L("Publish 3MF") },
|
||||
// File>Import
|
||||
{ ctrl + "I", L("Import geometry data from STL/STEP/3MF/OBJ/AMF files") },
|
||||
// File>Export
|
||||
|
||||
@@ -40,7 +40,6 @@
|
||||
#include "Plater.hpp"
|
||||
#include "WebViewDialog.hpp"
|
||||
#include "../Utils/Process.hpp"
|
||||
#include "format.hpp"
|
||||
// BBS
|
||||
#include "PartPlate.hpp"
|
||||
#include "Preferences.hpp"
|
||||
@@ -51,11 +50,9 @@
|
||||
#include "../Utils/NetworkAgentFactory.hpp"
|
||||
#include "../Utils/PrintHost.hpp"
|
||||
|
||||
#include <fstream>
|
||||
#include <string_view>
|
||||
|
||||
#include "GUI_App.hpp"
|
||||
#include "UnsavedChangesDialog.hpp"
|
||||
#include "PublishSettingsDialog.hpp"
|
||||
#include "MsgDialog.hpp"
|
||||
#include "Notebook.hpp"
|
||||
#include "GUI_Factories.hpp"
|
||||
@@ -778,6 +775,10 @@ DPIFrame(NULL, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, BORDERLESS_FRAME_
|
||||
if (m_plater) { m_plater->add_file(); }
|
||||
return;
|
||||
}
|
||||
if (evt.CmdDown() && evt.ShiftDown() && evt.GetKeyCode() == 'E') {
|
||||
if (can_export_model()) publish_project();
|
||||
return;
|
||||
}
|
||||
evt.Skip();
|
||||
});
|
||||
|
||||
@@ -1774,6 +1775,22 @@ bool MainFrame::save_project_as(const wxString& filename)
|
||||
return ret;
|
||||
}
|
||||
|
||||
void MainFrame::publish_project()
|
||||
{
|
||||
if (m_plater == nullptr)
|
||||
return;
|
||||
// Seed the dialog from the session selection (a remembered state or a freshly loaded
|
||||
// published 3MF); a null pointer means "fresh", keeping the dirty defaults.
|
||||
std::vector<std::string> pending_keys;
|
||||
std::vector<Slic3r::PublishedMaterialEntry> pending_material;
|
||||
const bool has_prior = m_plater->get_pending_published(pending_keys, pending_material);
|
||||
PublishSettingsDialog dlg(this, has_prior ? &pending_keys : nullptr, has_prior ? &pending_material : nullptr);
|
||||
if (dlg.ShowModal() != wxID_OK)
|
||||
return;
|
||||
m_plater->set_pending_published(dlg.GetPublishedKeys(), dlg.GetPublishedMaterialKeys());
|
||||
m_plater->export_published_3mf(dlg.GetPublishedKeys(), dlg.GetPublishedMaterialKeys());
|
||||
}
|
||||
|
||||
bool MainFrame::can_upload() const
|
||||
{
|
||||
return true;
|
||||
@@ -2869,6 +2886,20 @@ void MainFrame::init_menubar_as_editor()
|
||||
[this](){return m_plater != nullptr && can_save_as(); }, this);
|
||||
#endif
|
||||
|
||||
// BBS: publish
|
||||
fileMenu->AppendSeparator();
|
||||
auto publish_handler = [this](wxCommandEvent&) { publish_project(); };
|
||||
|
||||
#ifndef __APPLE__
|
||||
append_menu_item(fileMenu, wxID_ANY, _L("Publish 3MF") + dots + "\t" + ctrl + shift + "E", _L("Export a 3MF file with the selected settings embedded"),
|
||||
publish_handler, "menu_publish", nullptr,
|
||||
[this](){return can_export_model(); }, this);
|
||||
#else
|
||||
append_menu_item(fileMenu, wxID_ANY, _L("Publish 3MF") + dots + "\t" + ctrl + shift + "E", _L("Export a 3MF file with the selected settings embedded"),
|
||||
publish_handler, "", nullptr,
|
||||
[this](){return can_export_model(); }, this);
|
||||
#endif
|
||||
|
||||
|
||||
fileMenu->AppendSeparator();
|
||||
|
||||
@@ -4188,15 +4219,23 @@ std::wstring MainFrame::FileHistory::GetThumbnailUrl(int index) const
|
||||
return wss.str();
|
||||
}
|
||||
|
||||
bool MainFrame::FileHistory::GetPublished(int index) const
|
||||
{
|
||||
return index >= 0 && index < static_cast<int>(m_published_files.size()) && m_published_files[index];
|
||||
}
|
||||
|
||||
void MainFrame::FileHistory::AddFileToHistory(const wxString &file)
|
||||
{
|
||||
if (this->m_fileMaxFiles == 0)
|
||||
return;
|
||||
wxFileHistory::AddFileToHistory(file);
|
||||
if (m_load_called)
|
||||
if (m_load_called) {
|
||||
m_thumbnails.push_front(bbs_3mf_get_thumbnail(into_u8(file).c_str()));
|
||||
else
|
||||
m_published_files.push_front(bbs_3mf_is_published(into_u8(file)));
|
||||
} else {
|
||||
m_thumbnails.push_front("");
|
||||
m_published_files.push_front(false);
|
||||
}
|
||||
}
|
||||
|
||||
void MainFrame::FileHistory::RemoveFileFromHistory(size_t i)
|
||||
@@ -4205,6 +4244,7 @@ void MainFrame::FileHistory::RemoveFileFromHistory(size_t i)
|
||||
return;
|
||||
wxFileHistory::RemoveFileFromHistory(i);
|
||||
m_thumbnails.erase(m_thumbnails.begin() + i);
|
||||
m_published_files.erase(m_published_files.begin() + i);
|
||||
}
|
||||
|
||||
size_t MainFrame::FileHistory::FindFileInHistory(const wxString & file)
|
||||
@@ -4220,6 +4260,7 @@ void MainFrame::FileHistory::LoadThumbnails()
|
||||
if (!thumbnail.empty()) {
|
||||
m_thumbnails[i] = thumbnail;
|
||||
}
|
||||
m_published_files[i] = bbs_3mf_is_published(into_u8(GetHistoryFile(i)));
|
||||
}
|
||||
});
|
||||
m_load_called = true;
|
||||
@@ -4240,6 +4281,7 @@ void MainFrame::get_recent_projects(boost::property_tree::wptree &tree, int imag
|
||||
std::wstring proj = m_recent_projects.GetHistoryFile(i).ToStdWstring();
|
||||
item.put(L"project_name", proj.substr(proj.find_last_of(L"/\\") + 1));
|
||||
item.put(L"path", proj);
|
||||
item.put(L"published", m_recent_projects.GetPublished(i) ? L"1" : L"0");
|
||||
boost::system::error_code ec;
|
||||
std::time_t t = boost::filesystem::last_write_time(proj, ec);
|
||||
if (!ec) {
|
||||
|
||||
@@ -192,6 +192,7 @@ class MainFrame : public DPIFrame
|
||||
{
|
||||
FileHistory(int max) : wxFileHistory(max) {}
|
||||
std::wstring GetThumbnailUrl(int index) const;
|
||||
bool GetPublished(int index) const;
|
||||
|
||||
virtual void AddFileToHistory(const wxString &file);
|
||||
virtual void RemoveFileFromHistory(size_t i);
|
||||
@@ -202,6 +203,7 @@ class MainFrame : public DPIFrame
|
||||
void SetMaxFiles(int max);
|
||||
private:
|
||||
std::deque<std::string> m_thumbnails;
|
||||
std::deque<bool> m_published_files; // parallel to m_thumbnails: is it a published 3mf?
|
||||
bool m_load_called = false;
|
||||
};
|
||||
|
||||
@@ -355,6 +357,8 @@ public:
|
||||
bool can_upload() const;
|
||||
void save_project();
|
||||
bool save_project_as(const wxString& filename = wxString());
|
||||
// Open the Publish dialog and export the selected settings as a published 3MF.
|
||||
void publish_project();
|
||||
|
||||
void add_to_recent_projects(const wxString& filename);
|
||||
void get_recent_projects(boost::property_tree::wptree &tree, int images);
|
||||
|
||||
@@ -845,23 +845,8 @@ wxBoxSizer* MixedFilamentDialog::create_ratio_slider()
|
||||
m_ratio_bar->Bind(wxEVT_PAINT, [this](wxPaintEvent&) {
|
||||
wxBufferedPaintDC dc(m_ratio_bar);
|
||||
wxSize sz = m_ratio_bar->GetClientSize();
|
||||
|
||||
wxColour col_a = comp_colour(0), col_b = comp_colour(1);
|
||||
|
||||
for (int x = 0; x < sz.GetWidth(); ++x) {
|
||||
double t = (double)x / sz.GetWidth();
|
||||
wxColour c = blend_colors(col_a, col_b, 1.0 - t);
|
||||
dc.SetPen(wxPen(c));
|
||||
dc.DrawLine(x, 0, x, sz.GetHeight());
|
||||
}
|
||||
|
||||
int div_x = (int)(ratio(1) / 100.0 * sz.GetWidth());
|
||||
// Fixed in both themes, like the triangle picker's drag handle: the divider is drawn over
|
||||
// blended filament colour, so it has to keep its contrast against data rather than chrome.
|
||||
dc.SetPen(wxPen(wxColour(80, 80, 80), FromDIP(4)));
|
||||
dc.DrawLine(div_x, 0, div_x, sz.GetHeight());
|
||||
dc.SetPen(wxPen(*wxWHITE, FromDIP(2)));
|
||||
dc.DrawLine(div_x, 0, div_x, sz.GetHeight());
|
||||
draw_mixed_ratio_blend_bar(dc, wxRect(0, 0, sz.GetWidth(), sz.GetHeight()),
|
||||
comp_colour(0), comp_colour(1), ratio(1) / 100.0);
|
||||
});
|
||||
|
||||
m_ratio_bar->Bind(wxEVT_LEFT_DOWN, [this](wxMouseEvent& e) {
|
||||
@@ -917,52 +902,8 @@ wxBoxSizer* MixedFilamentDialog::create_ratio_slider()
|
||||
}
|
||||
|
||||
// ---- Triangle (ternary) ratio picker ----
|
||||
|
||||
// Barycentric coordinate utilities
|
||||
struct TriPoint { double x, y; };
|
||||
|
||||
static double tri_signed_area2(TriPoint a, TriPoint b, TriPoint c)
|
||||
{
|
||||
return (b.x - a.x) * (c.y - a.y) - (c.x - a.x) * (b.y - a.y);
|
||||
}
|
||||
|
||||
static bool tri_contains(TriPoint p, TriPoint v0, TriPoint v1, TriPoint v2)
|
||||
{
|
||||
double total = tri_signed_area2(v0, v1, v2);
|
||||
if (std::abs(total) < 1e-9) return false;
|
||||
double s0 = tri_signed_area2(p, v1, v2) / total;
|
||||
double s1 = tri_signed_area2(v0, p, v2) / total;
|
||||
double s2 = 1.0 - s0 - s1;
|
||||
return s0 >= -0.001 && s1 >= -0.001 && s2 >= -0.001;
|
||||
}
|
||||
|
||||
static void tri_barycentric(TriPoint p, TriPoint v0, TriPoint v1, TriPoint v2,
|
||||
double& w0, double& w1, double& w2)
|
||||
{
|
||||
double total = std::abs(tri_signed_area2(v0, v1, v2));
|
||||
if (total < 1e-9) { w0 = w1 = w2 = 1.0 / 3.0; return; }
|
||||
w0 = std::abs(tri_signed_area2(p, v1, v2)) / total;
|
||||
w1 = std::abs(tri_signed_area2(v0, p, v2)) / total;
|
||||
w2 = 1.0 - w0 - w1;
|
||||
w0 = std::clamp(w0, 0.0, 1.0);
|
||||
w1 = std::clamp(w1, 0.0, 1.0);
|
||||
w2 = std::clamp(w2, 0.0, 1.0);
|
||||
double s = w0 + w1 + w2;
|
||||
if (s > 0) { w0 /= s; w1 /= s; w2 /= s; }
|
||||
}
|
||||
|
||||
static TriPoint tri_clamp(TriPoint p, TriPoint v0, TriPoint v1, TriPoint v2)
|
||||
{
|
||||
double w0, w1, w2;
|
||||
tri_barycentric(p, v0, v1, v2, w0, w1, w2);
|
||||
w0 = std::clamp(w0, 0.0, 1.0);
|
||||
w1 = std::clamp(w1, 0.0, 1.0);
|
||||
w2 = std::clamp(w2, 0.0, 1.0);
|
||||
double s = w0 + w1 + w2;
|
||||
if (s > 0) { w0 /= s; w1 /= s; w2 /= s; }
|
||||
return {w0 * v0.x + w1 * v1.x + w2 * v2.x,
|
||||
w0 * v0.y + w1 * v1.y + w2 * v2.y};
|
||||
}
|
||||
// The barycentric utilities (TriPoint, tri_contains, tri_barycentric, tri_clamp) live in
|
||||
// FilamentBitmapUtils so the Publish dialog can mirror this picker read-only.
|
||||
|
||||
wxBoxSizer* MixedFilamentDialog::create_triangle_picker()
|
||||
{
|
||||
@@ -996,72 +937,15 @@ wxBoxSizer* MixedFilamentDialog::create_triangle_picker()
|
||||
wxSize sz = m_triangle_panel->GetClientSize();
|
||||
auto [v0, v1, v2] = get_vertices();
|
||||
|
||||
wxColour tri_bg = StateColor::darkModeColorFor(*wxWHITE);
|
||||
dc.SetBrush(wxBrush(tri_bg));
|
||||
dc.SetPen(*wxTRANSPARENT_PEN);
|
||||
dc.DrawRectangle(0, 0, sz.GetWidth(), sz.GetHeight());
|
||||
|
||||
wxColour c0 = comp_colour(0), c1 = comp_colour(1), c2 = comp_colour(2);
|
||||
|
||||
const bool cache_valid = m_tri_cache_bmp.IsOk() &&
|
||||
m_tri_cache_size == sz &&
|
||||
m_tri_cache_c0 == c0 && m_tri_cache_c1 == c1 && m_tri_cache_c2 == c2;
|
||||
|
||||
if (!cache_valid) {
|
||||
int min_y = (int)std::min({v0.y, v1.y, v2.y});
|
||||
int max_y = (int)std::max({v0.y, v1.y, v2.y});
|
||||
int min_x = (int)std::min({v0.x, v1.x, v2.x});
|
||||
int max_x = (int)std::max({v0.x, v1.x, v2.x});
|
||||
|
||||
m_tri_cache_bmp = wxBitmap(sz.GetWidth(), sz.GetHeight(), 24);
|
||||
wxMemoryDC mdc(m_tri_cache_bmp);
|
||||
mdc.SetBrush(wxBrush(tri_bg));
|
||||
mdc.SetPen(*wxTRANSPARENT_PEN);
|
||||
mdc.DrawRectangle(0, 0, sz.GetWidth(), sz.GetHeight());
|
||||
|
||||
for (int py = min_y; py <= max_y; ++py) {
|
||||
for (int px = min_x; px <= max_x; ++px) {
|
||||
TriPoint p = {(double)px, (double)py};
|
||||
if (!tri_contains(p, v0, v1, v2)) continue;
|
||||
double w0, w1, w2;
|
||||
tri_barycentric(p, v0, v1, v2, w0, w1, w2);
|
||||
unsigned char mr, mg, mb;
|
||||
if (w0 + w1 > 1e-6) {
|
||||
float t01 = static_cast<float>(w1 / (w0 + w1));
|
||||
Slic3r::filament_mixer_lerp(c0.Red(), c0.Green(), c0.Blue(),
|
||||
c1.Red(), c1.Green(), c1.Blue(),
|
||||
t01, &mr, &mg, &mb);
|
||||
float t2 = static_cast<float>(w2);
|
||||
Slic3r::filament_mixer_lerp(mr, mg, mb,
|
||||
c2.Red(), c2.Green(), c2.Blue(),
|
||||
t2, &mr, &mg, &mb);
|
||||
} else {
|
||||
mr = c2.Red(); mg = c2.Green(); mb = c2.Blue();
|
||||
}
|
||||
mdc.SetPen(wxPen(wxColour(mr, mg, mb)));
|
||||
mdc.DrawPoint(px, py);
|
||||
}
|
||||
}
|
||||
|
||||
mdc.SetPen(wxPen(StateColor::darkModeColorFor(wxColour("#CECECE")), 1));
|
||||
mdc.SetBrush(*wxTRANSPARENT_BRUSH);
|
||||
wxPoint pts[3] = {{(int)v0.x, (int)v0.y}, {(int)v1.x, (int)v1.y}, {(int)v2.x, (int)v2.y}};
|
||||
mdc.DrawPolygon(3, pts);
|
||||
|
||||
mdc.SelectObject(wxNullBitmap);
|
||||
m_tri_cache_c0 = c0; m_tri_cache_c1 = c1; m_tri_cache_c2 = c2;
|
||||
m_tri_cache_size = sz;
|
||||
}
|
||||
|
||||
dc.DrawBitmap(m_tri_cache_bmp, 0, 0);
|
||||
|
||||
// Drag handle (always redrawn on top of cached bitmap)
|
||||
double hx = m_tri_wx * v0.x + m_tri_wy * v1.x + m_tri_wz * v2.x;
|
||||
double hy = m_tri_wx * v0.y + m_tri_wy * v1.y + m_tri_wz * v2.y;
|
||||
int handle_r = FromDIP(5);
|
||||
dc.SetBrush(*wxWHITE_BRUSH);
|
||||
dc.SetPen(wxPen(wxColour("#262E30"), FromDIP(2)));
|
||||
dc.DrawCircle((int)hx, (int)hy, handle_r);
|
||||
// Draw the background, cached barycentric fill, outline and drag-handle marker through the
|
||||
// shared picker painter (same geometry the read-only Publish preview uses).
|
||||
draw_mixed_triangle_picker(dc, sz,
|
||||
{comp_colour(0), comp_colour(1), comp_colour(2)},
|
||||
{m_tri_wx, m_tri_wy, m_tri_wz},
|
||||
{StateColor::darkModeColorFor(*wxWHITE),
|
||||
StateColor::darkModeColorFor(wxColour("#CECECE")),
|
||||
StateColor::darkModeColorFor(wxColour("#262E30")),
|
||||
StateColor::darkModeColorFor(COLOR_LABEL_MUTED)});
|
||||
|
||||
if (m_result.ratios.size() >= 3) {
|
||||
dc.SetFont(::Label::Body_10);
|
||||
|
||||
@@ -169,10 +169,6 @@ private:
|
||||
// Triangle picker drag point (barycentric weights)
|
||||
double m_tri_wx{0.333}, m_tri_wy{0.333}, m_tri_wz{0.334};
|
||||
|
||||
// Cached triangle color bitmap (invalidated when colors or size change)
|
||||
wxBitmap m_tri_cache_bmp;
|
||||
wxColour m_tri_cache_c0, m_tri_cache_c1, m_tri_cache_c2;
|
||||
wxSize m_tri_cache_size;
|
||||
std::array<RatioLabelPanel*, 3> m_triangle_ratio_labels{nullptr, nullptr, nullptr};
|
||||
};
|
||||
|
||||
|
||||
@@ -3083,7 +3083,7 @@ bool NotificationManager::push_notification_data(std::unique_ptr<NotificationMan
|
||||
}
|
||||
bool retval = false;
|
||||
if (this->activate_existing(notification.get())) {
|
||||
if (m_initialized) { // ignore update action - it cant be initialized if canvas and imgui context is not ready
|
||||
if (m_initialized && m_imgui_ready) {
|
||||
if (notification->get_type() == NotificationType::SlicingWarning) {
|
||||
m_pop_notifications.back()->append(notification->get_data().ori_text);
|
||||
} else {
|
||||
@@ -3129,6 +3129,10 @@ void NotificationManager::stop_delayed_notifications_of_type(const NotificationT
|
||||
|
||||
void NotificationManager::render_notifications(GLCanvas3D &canvas, float overlay_width, float bottom_margin, float right_margin)
|
||||
{
|
||||
// Notifications render inside an ImGui frame, so the font atlas is built from this point on
|
||||
// and pushed notifications may safely measure their text.
|
||||
m_imgui_ready = true;
|
||||
|
||||
sort_notifications();
|
||||
|
||||
float bottom_up_last_y = bottom_margin; // ORCA dont scale margins
|
||||
@@ -3339,17 +3343,7 @@ size_t NotificationManager::get_notification_count() const
|
||||
void NotificationManager::bbl_show_plateinfo_notification(const std::string &text)
|
||||
{
|
||||
NotificationData data{NotificationType::BBLPlateInfo, NotificationLevel::PrintInfoNotificationLevel, BBL_NOTICE_MAX_INTERVAL, text};
|
||||
|
||||
for (std::unique_ptr<PopNotification> ¬ification : m_pop_notifications) {
|
||||
if (notification->get_type() == NotificationType::BBLPlateInfo) {
|
||||
notification->reinit();
|
||||
notification->update(data);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
auto notification = std::make_unique<NotificationManager::PopNotification>(data, m_id_provider, m_evt_handler);
|
||||
push_notification_data(std::move(notification), 0);
|
||||
push_notification_data(data, 0);
|
||||
}
|
||||
|
||||
void NotificationManager::bbl_close_3mf_warn_notification()
|
||||
@@ -3360,20 +3354,10 @@ void NotificationManager::bbl_close_3mf_warn_notification()
|
||||
}
|
||||
}
|
||||
|
||||
void NotificationManager::bbl_show_3mf_warn_notification(const std::string &text)
|
||||
void NotificationManager::bbl_show_3mf_warn_notification(const std::string &text, NotificationLevel level)
|
||||
{
|
||||
NotificationData data{NotificationType::BBL3MFInfo, NotificationLevel::ErrorNotificationLevel, BBL_NOTICE_MAX_INTERVAL, text};
|
||||
|
||||
for (std::unique_ptr<PopNotification> ¬ification : m_pop_notifications) {
|
||||
if (notification->get_type() == NotificationType::BBL3MFInfo) {
|
||||
notification->reinit();
|
||||
notification->update(data);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
auto notification = std::make_unique<NotificationManager::PopNotification>(data, m_id_provider, m_evt_handler);
|
||||
push_notification_data(std::move(notification), 0);
|
||||
NotificationData data{NotificationType::BBL3MFInfo, level, BBL_NOTICE_MAX_INTERVAL, text};
|
||||
push_notification_data(data, 0);
|
||||
}
|
||||
|
||||
void NotificationManager::bbl_close_plateinfo_notification()
|
||||
@@ -3388,17 +3372,7 @@ void NotificationManager::bbl_close_plateinfo_notification()
|
||||
void NotificationManager::bbl_show_preview_only_notification(const std::string &text)
|
||||
{
|
||||
NotificationData data{NotificationType::BBLPreviewOnlyMode, NotificationLevel::WarningNotificationLevel, 0, text};
|
||||
|
||||
for (std::unique_ptr<PopNotification> ¬ification : m_pop_notifications) {
|
||||
if (notification->get_type() == NotificationType::BBLPreviewOnlyMode) {
|
||||
notification->reinit();
|
||||
notification->update(data);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
auto notification = std::make_unique<NotificationManager::PopNotification>(data, m_id_provider, m_evt_handler);
|
||||
push_notification_data(std::move(notification), 0);
|
||||
push_notification_data(data, 0);
|
||||
}
|
||||
|
||||
void NotificationManager::bbl_close_preview_only_notification()
|
||||
|
||||
@@ -378,7 +378,9 @@ public:
|
||||
void bbl_close_plateinfo_notification();
|
||||
|
||||
//BBS-- 3mf warning
|
||||
void bbl_show_3mf_warn_notification(const std::string &text);
|
||||
// level defaults to the historical error styling; callers reporting informational
|
||||
// 3MF load notices (published settings) pass WarningNotificationLevel instead.
|
||||
void bbl_show_3mf_warn_notification(const std::string &text, NotificationLevel level = NotificationLevel::ErrorNotificationLevel);
|
||||
void bbl_close_3mf_warn_notification();
|
||||
|
||||
//BBS--preview only mode
|
||||
@@ -1052,6 +1054,11 @@ private:
|
||||
bool m_is_dark = false;
|
||||
// set by init(), until false notifications are only added not updated and frame is not requested after push
|
||||
bool m_initialized{ false };
|
||||
// set by render_notifications() on the first rendered frame. m_initialized only proves the
|
||||
// manager exists, not that the ImGui context can measure text: the font atlas is built lazily
|
||||
// in ImGuiWrapper::new_frame() on the first GL render, so updating a notification before that
|
||||
// (PopNotification::init -> count_spaces -> ImGui::CalcTextSize) dereferences a null font.
|
||||
bool m_imgui_ready{ false };
|
||||
// Target for wxWidgets events sent by clicking on the hyperlink available at some notifications.
|
||||
wxEvtHandler* m_evt_handler;
|
||||
// Cache of IDs to identify and reuse ImGUI windows.
|
||||
@@ -1076,7 +1083,10 @@ private:
|
||||
NotificationType::ProgressBar,
|
||||
NotificationType::PrintHostUpload,
|
||||
NotificationType::SimplifySuggestion,
|
||||
NotificationType::ValidateWarning
|
||||
NotificationType::ValidateWarning,
|
||||
// A published file load can produce several distinct 3MF warnings (invalid values,
|
||||
// skipped settings, changed slots); let them stack rather than clobber each other.
|
||||
NotificationType::BBL3MFInfo
|
||||
};
|
||||
//prepared (basic) notifications
|
||||
// non-static so its not loaded too early. If static, the translations wont load correctly.
|
||||
|
||||
+399
-38
@@ -1,12 +1,10 @@
|
||||
#include "Plater.hpp"
|
||||
#include "../Utils/NetworkAgent.hpp"
|
||||
#include "../Utils/NetworkAgentFactory.hpp"
|
||||
#include "libslic3r/Config.hpp"
|
||||
#include "libslic3r_version.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <numeric>
|
||||
#include <limits>
|
||||
#include <optional>
|
||||
@@ -16,8 +14,6 @@
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <regex>
|
||||
#include <future>
|
||||
#include <thread>
|
||||
#include <atomic>
|
||||
#include <mutex>
|
||||
#include <boost/algorithm/string.hpp>
|
||||
@@ -69,7 +65,6 @@
|
||||
#include "libslic3r/Format/bbs_3mf.hpp"
|
||||
#include "libslic3r/GCode/ThumbnailData.hpp"
|
||||
#include "libslic3r/Model.hpp"
|
||||
#include "libslic3r/SLA/Hollowing.hpp"
|
||||
#include "libslic3r/SLA/SupportPoint.hpp"
|
||||
#include "libslic3r/SLA/ReprojectPointsOnMesh.hpp"
|
||||
#include "libslic3r/Polygon.hpp"
|
||||
@@ -78,16 +73,15 @@
|
||||
#include "libslic3r/SLAPrint.hpp"
|
||||
#include "libslic3r/Utils.hpp"
|
||||
#include "libslic3r/PresetBundle.hpp"
|
||||
#include "libslic3r/PublishSettings.hpp"
|
||||
#include "slic3r/Utils/CrealityPrint.hpp"
|
||||
#include "libslic3r/ClipperUtils.hpp"
|
||||
#include "libslic3r/ObjColorUtils.hpp"
|
||||
// For stl export
|
||||
#include "libslic3r/CSGMesh/ModelToCSGMesh.hpp"
|
||||
#include "libslic3r/CSGMesh/PerformCSGMeshBooleans.hpp"
|
||||
|
||||
#include "GUI.hpp"
|
||||
#include "GUI_App.hpp"
|
||||
#include "GuiColor.hpp"
|
||||
#include "GUI_ObjectList.hpp"
|
||||
#ifdef __WXGTK__
|
||||
#include "LinuxDisplayBackend.hpp"
|
||||
@@ -123,7 +117,6 @@
|
||||
#include "SendMultiMachinePage.hpp"
|
||||
#include "SendToPrinter.hpp"
|
||||
#include "PublishDialog.hpp"
|
||||
#include "ModelMall.hpp"
|
||||
#include "ConfigWizard.hpp"
|
||||
#include "SyncAmsInfoDialog.hpp"
|
||||
#include "../Utils/ASCIIFolding.hpp"
|
||||
@@ -149,7 +142,6 @@
|
||||
#include "ParamsDialog.hpp"
|
||||
#include "ImageDPIFrame.hpp"
|
||||
#include "Widgets/Label.hpp"
|
||||
#include "Widgets/RoundedRectangle.hpp"
|
||||
#include "Widgets/RadioGroup.hpp"
|
||||
#include "Widgets/CheckBox.hpp"
|
||||
#include "Widgets/Button.hpp"
|
||||
@@ -6712,6 +6704,12 @@ struct Plater::priv
|
||||
SendToPrinterDialog* m_send_to_sdcard_dlg = nullptr;
|
||||
PublishDialog *m_publish_dlg = nullptr;
|
||||
|
||||
// Session-level stash of the last published selection. Written on publish and on
|
||||
// loading a published 3MF; read when the Publish dialog is opened.
|
||||
bool m_has_pending_published{false};
|
||||
std::vector<std::string> m_pending_published_keys;
|
||||
std::vector<Slic3r::PublishedMaterialEntry> m_pending_material_keys;
|
||||
|
||||
// Data
|
||||
Slic3r::DynamicPrintConfig *config; // FIXME: leak?
|
||||
Slic3r::Print fff_print;
|
||||
@@ -6936,7 +6934,10 @@ struct Plater::priv
|
||||
|
||||
// BBS: backup & restore
|
||||
using LoadProgressCallback = std::function<bool(int, const wxString&)>;
|
||||
std::vector<size_t> load_files(const std::vector<fs::path>& input_files, LoadStrategy strategy, bool ask_multi = false);
|
||||
std::vector<size_t> load_files(const std::vector<fs::path>& input_files,
|
||||
LoadStrategy strategy,
|
||||
bool ask_multi = false,
|
||||
bool* published_out = nullptr);
|
||||
std::vector<size_t> load_model_objects(const ModelObjectPtrs& model_objects, bool allow_negative_z = false, bool split_object = false, bool auto_drop = true);
|
||||
|
||||
// Texture-to-color import: a mesh loaded with UVs + a texture map gets its faces clustered
|
||||
@@ -6964,7 +6965,7 @@ struct Plater::priv
|
||||
std::function<bool()> cancel_callback = {});
|
||||
|
||||
fs::path get_export_file_path(GUI::FileType file_type);
|
||||
wxString get_export_file(GUI::FileType file_type);
|
||||
wxString get_export_file(GUI::FileType file_type, const wxString& title = {}, bool published = false);
|
||||
|
||||
// BBS
|
||||
void load_auxiliary_files();
|
||||
@@ -8268,7 +8269,10 @@ void read_binary_stl(const std::string& filename, std::string& model_id, std::st
|
||||
}
|
||||
|
||||
// BBS: backup & restore
|
||||
std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_files, LoadStrategy strategy, bool ask_multi)
|
||||
std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_files,
|
||||
LoadStrategy strategy,
|
||||
bool ask_multi,
|
||||
bool* published_out)
|
||||
{
|
||||
std::vector<size_t> empty_result;
|
||||
bool dlg_cont = true;
|
||||
@@ -8348,6 +8352,7 @@ std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_
|
||||
const float INPUT_FILES_RATIO = 0.7;
|
||||
const float INIT_MODEL_RATIO = 0.75;
|
||||
const float CENTER_AROUND_ORIGIN_RATIO = 0.8;
|
||||
|
||||
const float LOAD_MODEL_RATIO = 0.9;
|
||||
|
||||
for (size_t i = 0; i < input_files.size(); ++i) {
|
||||
@@ -8388,6 +8393,11 @@ std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_
|
||||
DynamicPrintConfig config;
|
||||
Semver file_version;
|
||||
En3mfType en_3mf_file_type = En3mfType::From_BBS;
|
||||
// BBS: a "published" 3MF carries a flag plus the author-selected setting keys;
|
||||
// on load keep the user's current presets and overlay only those keys. Declared
|
||||
// here (outside the config block below) so it stays alive for the embedded-preset
|
||||
// gate, the metadata strip and the preset overlay after the block closes.
|
||||
PublishedConfig published_config;
|
||||
{
|
||||
DynamicPrintConfig config_loaded;
|
||||
|
||||
@@ -8415,6 +8425,107 @@ std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_
|
||||
<< boost::format(", plate_data.size %1%, project_preset.size %2%, is_bbs_or_orca_3mf %3%, file_version %4% \n") % plate_data.size() %
|
||||
project_presets.size() % (en_3mf_file_type == En3mfType::From_BBS || en_3mf_file_type == En3mfType::From_Orca) % file_version.to_string();
|
||||
|
||||
// BBS: a "published" 3MF carries a flag plus the author-selected setting keys;
|
||||
// on load keep the user's current presets and overlay only those keys. Parsed
|
||||
// here (before the version/fallback chain below) because a published file has
|
||||
// no project_settings.config: its values travel in the published_config
|
||||
// metadata payload, which must fill config_loaded before the chain decides
|
||||
// whether to import geometry only.
|
||||
if (model.model_info != nullptr) {
|
||||
auto published_it = model.model_info->metadata_items.find(ORCA_PUBLISHED_TAG);
|
||||
if (published_it != model.model_info->metadata_items.end() && is_published_3mf_flag(published_it->second)) {
|
||||
published_config.published = true;
|
||||
auto keys_it = model.model_info->metadata_items.find(ORCA_PUBLISHED_KEYS_TAG);
|
||||
if (keys_it != model.model_info->metadata_items.end()) {
|
||||
try {
|
||||
auto j = nlohmann::json::parse(keys_it->second);
|
||||
if (j.is_array())
|
||||
for (const auto& k : j)
|
||||
if (k.is_string())
|
||||
published_config.published_keys.emplace_back(k.get<std::string>());
|
||||
} catch (...) {
|
||||
// Ignore malformed published_keys; the project still loads normally.
|
||||
}
|
||||
}
|
||||
|
||||
auto material_keys_it = model.model_info->metadata_items.find(ORCA_PUBLISHED_MATERIAL_TAG);
|
||||
if (material_keys_it != model.model_info->metadata_items.end()) {
|
||||
try {
|
||||
auto jm = nlohmann::json::parse(material_keys_it->second);
|
||||
if (jm.is_array())
|
||||
for (const auto& m : jm) {
|
||||
try {
|
||||
// Malformed entries are isolated so one bad item
|
||||
// cannot discard valid entries that follow it.
|
||||
if (!m.is_object())
|
||||
continue;
|
||||
PublishedMaterialEntry entry;
|
||||
const auto mat_it = m.find("material");
|
||||
if (mat_it != m.end() && mat_it->is_object()) {
|
||||
const auto& mat = *mat_it;
|
||||
if (mat.contains("filament_type") && mat["filament_type"].is_string())
|
||||
entry.filament_type = mat["filament_type"].get<std::string>();
|
||||
if (mat.contains("filament_vendor") && mat["filament_vendor"].is_string())
|
||||
entry.filament_vendor = mat["filament_vendor"].get<std::string>();
|
||||
if (mat.contains("filament_id") && mat["filament_id"].is_string())
|
||||
entry.filament_id = mat["filament_id"].get<std::string>();
|
||||
if (mat.contains("setting_id") && mat["setting_id"].is_string())
|
||||
entry.setting_id = mat["setting_id"].get<std::string>();
|
||||
if (mat.contains("name") && mat["name"].is_string())
|
||||
entry.preset_name = mat["name"].get<std::string>();
|
||||
}
|
||||
if (m.contains("slot") && m["slot"].is_number_integer())
|
||||
entry.slot = m["slot"].get<int>();
|
||||
const auto entry_keys_it = m.find("keys");
|
||||
if (entry_keys_it != m.end() && entry_keys_it->is_array())
|
||||
for (const auto& k : *entry_keys_it)
|
||||
if (k.is_string())
|
||||
entry.keys.emplace_back(k.get<std::string>());
|
||||
// Fields always written by the current exporter.
|
||||
if (m.contains("full") && m["full"].is_boolean())
|
||||
entry.full = m["full"].get<bool>();
|
||||
const auto entry_full_keys_it = m.find("full_keys");
|
||||
if (entry_full_keys_it != m.end() && entry_full_keys_it->is_array())
|
||||
for (const auto& k : *entry_full_keys_it)
|
||||
if (k.is_string())
|
||||
entry.full_keys.emplace_back(k.get<std::string>());
|
||||
if (m.contains("publish_type") && m["publish_type"].is_boolean())
|
||||
entry.publish_type = m["publish_type"].get<bool>();
|
||||
if (m.contains("type") && m["type"].is_string())
|
||||
entry.publish_type_value = m["type"].get<std::string>();
|
||||
if (m.contains("publish_color") && m["publish_color"].is_boolean())
|
||||
entry.publish_color = m["publish_color"].get<bool>();
|
||||
if (m.contains("color") && m["color"].is_string())
|
||||
entry.color = m["color"].get<std::string>();
|
||||
published_config.material_keys.emplace_back(std::move(entry));
|
||||
} catch (const nlohmann::json::exception&) {
|
||||
// Ignore only this malformed material entry.
|
||||
}
|
||||
}
|
||||
} catch (const nlohmann::json::exception&) {
|
||||
// Ignore malformed published_material_keys; the project still loads normally.
|
||||
}
|
||||
}
|
||||
|
||||
// Rebuild the published values from the metadata payload: a published
|
||||
// file carries no project_settings.config, so config_loaded is filled
|
||||
// from here; a missing or malformed payload leaves it empty and the
|
||||
// fallback chain below imports the geometry only.
|
||||
auto payload_it = model.model_info->metadata_items.find(ORCA_PUBLISHED_CONFIG_TAG);
|
||||
if (payload_it != model.model_info->metadata_items.end()) {
|
||||
try {
|
||||
ConfigSubstitutions payload_substitutions =
|
||||
config_loaded.load_from_ini_string(payload_it->second, ForwardCompatibilitySubstitutionRule::Enable);
|
||||
config_substitutions.substitutions.insert(config_substitutions.substitutions.end(),
|
||||
std::make_move_iterator(payload_substitutions.begin()),
|
||||
std::make_move_iterator(payload_substitutions.end()));
|
||||
} catch (...) {
|
||||
// Ignore malformed published_config; the project still loads normally.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 1. add extruder for prusa model if the number of existing extruders is not enough
|
||||
// 2. add extruder for BBS or Other model if only import geometry
|
||||
if (en_3mf_file_type == En3mfType::From_Prusa || (load_model && !load_config)) {
|
||||
@@ -8578,7 +8689,7 @@ std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_
|
||||
text += "\n";
|
||||
log_and_show_3mf_info(text, bambu_project_title);
|
||||
}
|
||||
} else if (load_config) {
|
||||
} else if (load_config && !published_config.published) {
|
||||
// BambuStudio version is older or same as our SLIC3R_VERSION
|
||||
wxString text = _L("The 3MF was created by BambuStudio. Some settings may differ from OrcaSlicer.");
|
||||
log_and_show_3mf_info(text, bambu_project_title);
|
||||
@@ -8638,7 +8749,9 @@ std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_
|
||||
}
|
||||
|
||||
Semver old_version(1, 5, 9);
|
||||
if ((en_3mf_file_type == En3mfType::From_BBS || en_3mf_file_type == En3mfType::From_Orca) && (file_version < old_version) && load_model && load_config && !config_loaded.empty()) {
|
||||
// A published 3MF has no project config to migrate: skip the old-version
|
||||
// translations even if a slicer tag slipped through classification.
|
||||
if ((en_3mf_file_type == En3mfType::From_BBS || en_3mf_file_type == En3mfType::From_Orca) && (file_version < old_version) && !published_config.published && load_model && load_config && !config_loaded.empty()) {
|
||||
translate_old = true;
|
||||
partplate_list.get_plate_size(current_width, current_depth, current_height);
|
||||
}
|
||||
@@ -8660,8 +8773,10 @@ std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_
|
||||
}
|
||||
}
|
||||
|
||||
// BBS:: project embedded presets
|
||||
if ((project_presets.size() > 0) && load_config) {
|
||||
// BBS:: project embedded presets (skipped for published projects: the author's
|
||||
// embedded presets must not pollute the receiver's library, the overlay applies
|
||||
// the published keys to the receiver's own presets instead).
|
||||
if ((project_presets.size() > 0) && load_config && !published_config.published) {
|
||||
// load project embedded presets
|
||||
PresetsConfigSubstitutions preset_substitutions;
|
||||
PresetBundle & preset_bundle = *wxGetApp().preset_bundle;
|
||||
@@ -8717,6 +8832,19 @@ std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_
|
||||
}
|
||||
}
|
||||
|
||||
// BBS: a "published" 3MF loads as a new project: its path must not become the
|
||||
// project filename (Save/Ctrl-S would overwrite the shared file), and the
|
||||
// published metadata is consumed above and stripped so a later save is a normal
|
||||
// unpublished 3MF.
|
||||
if (published_out != nullptr && published_config.published)
|
||||
*published_out = true;
|
||||
if (published_config.published && load_config && this->model.model_info != nullptr) {
|
||||
this->model.model_info->metadata_items.erase(ORCA_PUBLISHED_TAG);
|
||||
this->model.model_info->metadata_items.erase(ORCA_PUBLISHED_KEYS_TAG);
|
||||
this->model.model_info->metadata_items.erase(ORCA_PUBLISHED_MATERIAL_TAG);
|
||||
this->model.model_info->metadata_items.erase(ORCA_PUBLISHED_CONFIG_TAG);
|
||||
}
|
||||
|
||||
if (load_config) {
|
||||
if (!config.empty()) {
|
||||
Preset::normalize(config);
|
||||
@@ -8725,7 +8853,7 @@ std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_
|
||||
{
|
||||
// BBS: modify the prime tower params for old version file
|
||||
Semver old_version3(2, 0, 0);
|
||||
if ((en_3mf_file_type == En3mfType::From_BBS || en_3mf_file_type == En3mfType::From_Orca) && file_version < old_version3) {
|
||||
if ((en_3mf_file_type == En3mfType::From_BBS || en_3mf_file_type == En3mfType::From_Orca) && !published_config.published && file_version < old_version3) {
|
||||
double old_filament_prime_volume = 0.;
|
||||
int filament_count = 0;
|
||||
{
|
||||
@@ -8775,7 +8903,7 @@ std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_
|
||||
}
|
||||
|
||||
auto choise = wxGetApp().app_config->get("no_warn_when_modified_gcodes");
|
||||
if (choise.empty() || choise != "true") {
|
||||
if (!published_config.published && (choise.empty() || choise != "true")) {
|
||||
// BBS: first validate the printer
|
||||
// validate the system profiles
|
||||
std::set<std::string> modified_gcodes;
|
||||
@@ -8820,12 +8948,56 @@ std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_
|
||||
if (wipe_tower_y_opt)
|
||||
file_wipe_tower_y = *wipe_tower_y_opt;
|
||||
|
||||
// Convert the printer's filament ids to Orca ids before loading.
|
||||
if (auto* agent = wxGetApp().getAgent()) {
|
||||
if (auto* ids = config.opt<ConfigOptionStrings>("filament_ids"))
|
||||
for (std::string& id : ids->values)
|
||||
id = agent->to_orca_filament_id(id);
|
||||
}
|
||||
preset_bundle->load_config_model(filename.string(), std::move(config), file_version);
|
||||
preset_bundle->load_config_model(filename.string(), std::move(config), file_version, &published_config);
|
||||
|
||||
// Mixed-filament definitions that collided with one of the
|
||||
// receiver's real slots were relocated during the preset load.
|
||||
// Re-point the freshly parsed model's extruder references and
|
||||
// color painting from the author's slot numbers to where each
|
||||
// definition landed, so volumes colored with a mix follow it.
|
||||
// Runs before the objects are handed over to the plater below.
|
||||
if (load_model && !published_config.mixed_slot_relocations.empty())
|
||||
Slic3r::remap_model_filament_slots(model, published_config.mixed_slot_relocations);
|
||||
|
||||
// BBS: notify the user about published settings that could not be applied.
|
||||
if (!published_config.skipped_keys.empty()) {
|
||||
NotificationManager* notify_manager = q->get_notification_manager();
|
||||
std::string message = _u8L("Some published settings could not be applied:");
|
||||
for (const std::string& key : published_config.skipped_keys)
|
||||
message += "\n-" + key;
|
||||
// Informational: the load succeeded, these keys were skipped.
|
||||
notify_manager
|
||||
->bbl_show_3mf_warn_notification(message,
|
||||
NotificationManager::NotificationLevel::WarningNotificationLevel);
|
||||
}
|
||||
|
||||
// BBS: notify the user about slot materials that were replaced while
|
||||
// loading a published project (type mismatch / no same-type match).
|
||||
if (!published_config.material_replacements.empty()) {
|
||||
NotificationManager* notify_manager = q->get_notification_manager();
|
||||
std::string message = _u8L("Some filament slots were changed:");
|
||||
for (const std::string& replacement : published_config.material_replacements)
|
||||
message += "\n-" + replacement;
|
||||
// Informational: the load succeeded, the slots were adapted.
|
||||
notify_manager
|
||||
->bbl_show_3mf_warn_notification(message,
|
||||
NotificationManager::NotificationLevel::WarningNotificationLevel);
|
||||
}
|
||||
|
||||
// Remember the imported published selection so the Publish dialog is
|
||||
// pre-seeded with the file's settings. Stored after the load so any
|
||||
// per-slot relocations are already reflected in material_keys.
|
||||
if (published_config.published) {
|
||||
this->m_has_pending_published = true;
|
||||
this->m_pending_published_keys = published_config.published_keys;
|
||||
this->m_pending_material_keys = published_config.material_keys;
|
||||
}
|
||||
|
||||
ConfigOption* bed_type_opt = preset_bundle->project_config.option("curr_bed_type");
|
||||
if (bed_type_opt != nullptr) {
|
||||
@@ -8966,7 +9138,13 @@ std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_
|
||||
dynamic_map->value = false;
|
||||
}
|
||||
// Update filament combobox after loading config
|
||||
wxGetApp().plater()->sidebar().update_presets(Preset::TYPE_FILAMENT);
|
||||
if (published_config.published) {
|
||||
q->update_filament_colors_in_full_config();
|
||||
wxGetApp().plater()->sidebar().update_all_preset_comboboxes();
|
||||
wxGetApp().plater()->sidebar().update_dynamic_filament_list();
|
||||
} else {
|
||||
wxGetApp().plater()->sidebar().update_presets(Preset::TYPE_FILAMENT);
|
||||
}
|
||||
// The loaded project supplies nozzle_volume_type; refresh the sidebar
|
||||
// nozzle-count badges against it.
|
||||
if (auto *nozzle_volumes = wxGetApp().preset_bundle->project_config.option<ConfigOptionEnumsGeneric>("nozzle_volume_type")) {
|
||||
@@ -9719,7 +9897,7 @@ fs::path Plater::priv::get_export_file_path(GUI::FileType file_type)
|
||||
return output_file;
|
||||
}
|
||||
|
||||
wxString Plater::priv::get_export_file(GUI::FileType file_type)
|
||||
wxString Plater::priv::get_export_file(GUI::FileType file_type, const wxString& title, bool published)
|
||||
{
|
||||
wxString wildcard;
|
||||
switch (file_type) {
|
||||
@@ -9761,8 +9939,11 @@ wxString Plater::priv::get_export_file(GUI::FileType file_type)
|
||||
}
|
||||
case FT_3MF:
|
||||
{
|
||||
output_file.replace_extension("3mf");
|
||||
dlg_title = _L("Save file as");
|
||||
// A published export is suggested as "<name>.published.3mf" so the role is visible in the
|
||||
// dialog and in the recent-files list. This is only a pre-filled suggestion; the user's
|
||||
// typed filename wins, keeping a plain ".3mf" output fully valid.
|
||||
output_file.replace_extension(published ? "published.3mf" : "3mf");
|
||||
dlg_title = title.empty() ? _L("Save file as") : title;
|
||||
break;
|
||||
}
|
||||
case FT_OBJ:
|
||||
@@ -10000,6 +10181,11 @@ void Plater::priv::reset(bool apply_presets_change)
|
||||
|
||||
clear_warnings();
|
||||
|
||||
// A new project must not inherit the previous project's published selection (Feature A/B).
|
||||
m_has_pending_published = false;
|
||||
m_pending_published_keys.clear();
|
||||
m_pending_material_keys.clear();
|
||||
|
||||
set_project_filename("");
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << __LINE__ << " call set_project_filename: empty";
|
||||
|
||||
@@ -15091,14 +15277,15 @@ void Plater::load_project(wxString const& filename2,
|
||||
if (strategy & LoadStrategy::Restore)
|
||||
input_paths.push_back(into_u8(originfile));
|
||||
|
||||
std::vector<size_t> res = load_files(input_paths, strategy);
|
||||
bool loaded_published = false;
|
||||
std::vector<size_t> res = load_files(input_paths, strategy, false, &loaded_published);
|
||||
|
||||
reset_project_dirty_initial_presets();
|
||||
update_project_dirty_from_presets();
|
||||
wxGetApp().preset_bundle->export_selections(*wxGetApp().app_config);
|
||||
|
||||
// if res is empty no data has been loaded
|
||||
if (!res.empty() && (load_restore || !(strategy & LoadStrategy::Silence))) {
|
||||
if (!res.empty() && !loaded_published && (load_restore || !(strategy & LoadStrategy::Silence))) {
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << __LINE__ << " call set_project_filename: " << (load_restore ? originfile : filename);
|
||||
p->set_project_filename(load_restore ? originfile : filename);
|
||||
if (load_restore && originfile.IsEmpty()) {
|
||||
@@ -15109,6 +15296,15 @@ void Plater::load_project(wxString const& filename2,
|
||||
if (using_exported_file()) {
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << __LINE__ << " using ecported set project filename: " << filename;
|
||||
p->set_project_filename(filename);
|
||||
} else if (loaded_published && !res.empty()) {
|
||||
// A "published" 3MF loads as a new project: its path must not become the project
|
||||
// filename (Save/Ctrl-S prompts for a destination instead of overwriting it);
|
||||
// reset() already cleared the project name, so restore the default title and keep
|
||||
// the file in recents. Only on a successful load (res not empty): a failed or
|
||||
// cancelled load must not pollute "Recently opened".
|
||||
p->set_project_name(_L("Untitled"));
|
||||
if (!filename.IsEmpty())
|
||||
wxGetApp().mainframe->add_to_recent_projects(filename);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -16766,22 +16962,12 @@ void Plater::force_update_all_plate_thumbnails()
|
||||
}
|
||||
|
||||
// BBS: backup
|
||||
std::vector<size_t> Plater::load_files(const std::vector<fs::path>& input_files, LoadStrategy strategy, bool ask_multi) {
|
||||
std::vector<size_t> Plater::load_files(const std::vector<fs::path>& input_files, LoadStrategy strategy, bool ask_multi, bool* published_out) {
|
||||
//BBS: wish to reset state when load a new file
|
||||
p->m_slice_all_only_has_gcode = false;
|
||||
//BBS: wish to reset all plates stats item selected state when load a new file
|
||||
p->preview->get_canvas3d()->reset_select_plate_toolbar_selection();
|
||||
return p->load_files(input_files, strategy, ask_multi);
|
||||
}
|
||||
|
||||
// To be called when providing a list of files to the GUI slic3r on command line.
|
||||
std::vector<size_t> Plater::load_files(const std::vector<std::string>& input_files, LoadStrategy strategy, bool ask_multi)
|
||||
{
|
||||
std::vector<fs::path> paths;
|
||||
paths.reserve(input_files.size());
|
||||
for (const std::string& path : input_files)
|
||||
paths.emplace_back(path);
|
||||
return p->load_files(paths, strategy, ask_multi);
|
||||
return p->load_files(input_files, strategy, ask_multi, published_out);
|
||||
}
|
||||
|
||||
bool Plater::preview_zip_archive(const boost::filesystem::path& archive_path)
|
||||
@@ -18010,7 +18196,6 @@ void Plater::send_gcode_finish(wxString name)
|
||||
auto out_str = GUI::format(_L("The file %s has been sent to the printer's storage space and can be viewed on the printer."), name);
|
||||
p->notification_manager->push_exporting_finished_notification(out_str, "", false);
|
||||
}
|
||||
|
||||
void Plater::export_core_3mf()
|
||||
{
|
||||
wxString path = p->get_export_file(FT_3MF);
|
||||
@@ -18019,6 +18204,182 @@ void Plater::export_core_3mf()
|
||||
export_3mf(path_u8, SaveStrategy::Silence);
|
||||
}
|
||||
|
||||
// Export the current project as a "published" 3MF: a pure export that never touches the
|
||||
// project's file name, dirty state, backup path or title, and attaches the published metadata
|
||||
// to the model only for the duration of the export (a later Save Project is a normal 3MF).
|
||||
int Plater::export_published_3mf(const std::vector<std::string>& published_keys,
|
||||
const std::vector<Slic3r::PublishedMaterialEntry>& material_keys)
|
||||
{
|
||||
wxString path = p->get_export_file(FT_3MF, _L("Publish 3MF file as:"), true);
|
||||
if (path.empty() || path == "<cancel>")
|
||||
return wxID_CANCEL;
|
||||
|
||||
nlohmann::json j = nlohmann::json::array();
|
||||
for (const std::string& key : published_keys)
|
||||
j.push_back(key);
|
||||
nlohmann::json jm = nlohmann::json::array();
|
||||
for (const Slic3r::PublishedMaterialEntry& e : material_keys)
|
||||
jm.push_back({{"material",
|
||||
{{"filament_type", e.filament_type},
|
||||
{"filament_vendor", e.filament_vendor},
|
||||
{"filament_id", e.filament_id},
|
||||
{"setting_id", e.setting_id},
|
||||
{"name", e.preset_name}}},
|
||||
{"slot", e.slot},
|
||||
{"keys", e.keys},
|
||||
{"full", e.full},
|
||||
{"full_keys", e.full_keys},
|
||||
{"publish_type", e.publish_type},
|
||||
{"type", e.publish_type_value},
|
||||
{"publish_color", e.publish_color},
|
||||
{"color", e.color}});
|
||||
|
||||
Model& model = this->model();
|
||||
// Save the previous metadata so it can be restored after the export, keeping the in-memory
|
||||
// project pristine (the published flag lives only in the exported file).
|
||||
const bool had_model_info = (model.model_info != nullptr);
|
||||
const bool had_published = had_model_info &&
|
||||
(model.model_info->metadata_items.find(ORCA_PUBLISHED_TAG) != model.model_info->metadata_items.end());
|
||||
const bool had_published_keys = had_model_info && (model.model_info->metadata_items.find(ORCA_PUBLISHED_KEYS_TAG) !=
|
||||
model.model_info->metadata_items.end());
|
||||
const bool had_material_keys = had_model_info && (model.model_info->metadata_items.find(ORCA_PUBLISHED_MATERIAL_TAG) !=
|
||||
model.model_info->metadata_items.end());
|
||||
const bool had_payload = had_model_info &&
|
||||
(model.model_info->metadata_items.find(ORCA_PUBLISHED_CONFIG_TAG) != model.model_info->metadata_items.end());
|
||||
const std::string prev_published = had_published ? model.model_info->metadata_items.at(ORCA_PUBLISHED_TAG) : std::string();
|
||||
const std::string prev_published_keys = had_published_keys ? model.model_info->metadata_items.at(ORCA_PUBLISHED_KEYS_TAG) :
|
||||
std::string();
|
||||
const std::string prev_material_keys = had_material_keys ? model.model_info->metadata_items.at(ORCA_PUBLISHED_MATERIAL_TAG) :
|
||||
std::string();
|
||||
const std::string prev_payload = had_payload ? model.model_info->metadata_items.at(ORCA_PUBLISHED_CONFIG_TAG) : std::string();
|
||||
|
||||
// export_3mf() assigns archive paths to previously unsaved SVGs. Preserve those fields too,
|
||||
// otherwise a publish changes what a later normal project save writes.
|
||||
std::vector<std::pair<std::string*, std::string>> previous_svg_paths;
|
||||
for (ModelObject* object : model.objects)
|
||||
for (ModelVolume* volume : object->volumes)
|
||||
if (volume != nullptr && volume->emboss_shape.has_value() && volume->emboss_shape->svg_file.has_value()) {
|
||||
std::string* path_in_3mf = &volume->emboss_shape->svg_file->path_in_3mf;
|
||||
previous_svg_paths.emplace_back(path_in_3mf, *path_in_3mf);
|
||||
}
|
||||
|
||||
auto restore_temporary_state = [&]() {
|
||||
for (const auto& [path_in_3mf, previous_path] : previous_svg_paths)
|
||||
*path_in_3mf = previous_path;
|
||||
|
||||
if (!had_model_info) {
|
||||
model.model_info = nullptr;
|
||||
} else {
|
||||
if (had_published)
|
||||
model.model_info->metadata_items[ORCA_PUBLISHED_TAG] = prev_published;
|
||||
else
|
||||
model.model_info->metadata_items.erase(ORCA_PUBLISHED_TAG);
|
||||
if (had_published_keys)
|
||||
model.model_info->metadata_items[ORCA_PUBLISHED_KEYS_TAG] = prev_published_keys;
|
||||
else
|
||||
model.model_info->metadata_items.erase(ORCA_PUBLISHED_KEYS_TAG);
|
||||
if (had_material_keys)
|
||||
model.model_info->metadata_items[ORCA_PUBLISHED_MATERIAL_TAG] = prev_material_keys;
|
||||
else
|
||||
model.model_info->metadata_items.erase(ORCA_PUBLISHED_MATERIAL_TAG);
|
||||
if (had_payload)
|
||||
model.model_info->metadata_items[ORCA_PUBLISHED_CONFIG_TAG] = prev_payload;
|
||||
else
|
||||
model.model_info->metadata_items.erase(ORCA_PUBLISHED_CONFIG_TAG);
|
||||
}
|
||||
};
|
||||
bool state_restored = false;
|
||||
auto restore_now = [&]() {
|
||||
if (state_restored)
|
||||
return;
|
||||
restore_temporary_state();
|
||||
state_restored = true;
|
||||
};
|
||||
ScopeGuard restore_guard(restore_now);
|
||||
|
||||
if (model.model_info == nullptr)
|
||||
model.model_info = std::make_shared<ModelInfo>();
|
||||
model.model_info->metadata_items[ORCA_PUBLISHED_TAG] = "1";
|
||||
model.model_info->metadata_items[ORCA_PUBLISHED_KEYS_TAG] = j.dump();
|
||||
model.model_info->metadata_items[ORCA_PUBLISHED_MATERIAL_TAG] = jm.dump();
|
||||
|
||||
int ret = -1;
|
||||
try {
|
||||
// Minimal published export: filter full_config to the published keys, material keys,
|
||||
// identity fields and plate geometry keys, and omit the project config file, the
|
||||
// project-embedded preset dumps and the OrcaSlicer version tag from the archive. The
|
||||
// filtered values are serialized into the published_config metadata payload instead, so
|
||||
// OrcaSlicer versions without the publish feature fall back to importing the geometry only
|
||||
// (keeping the receiver's presets) while new versions rebuild the config from the payload.
|
||||
DynamicPrintConfig full_cfg = wxGetApp().preset_bundle->full_config_secure();
|
||||
DynamicPrintConfig filtered_cfg = filter_published_config(full_cfg, published_keys, material_keys);
|
||||
std::string payload;
|
||||
for (const std::string& key : filtered_cfg.keys()) {
|
||||
// A value containing a newline would break the INI written below (read_ini throws),
|
||||
// so load_from_ini_string discards the whole settings block on import. Skip such
|
||||
// keys instead of silently dropping every setting.
|
||||
std::string value = filtered_cfg.opt_serialize(key);
|
||||
if (value.find('\n') != std::string::npos) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "publish: dropping key \"" << key
|
||||
<< "\" from the published payload (value contains a newline)";
|
||||
continue;
|
||||
}
|
||||
payload += key + " = " + value + "\n";
|
||||
}
|
||||
model.model_info->metadata_items[ORCA_PUBLISHED_CONFIG_TAG] = std::move(payload);
|
||||
|
||||
// Same file layout as save_project(), plus Silence (so export_3mf does not set the project
|
||||
// filename on success, keeping this a pure export like export_core_3mf()) and MinimalPublished.
|
||||
auto save_strategy = SaveStrategy::SplitModel | SaveStrategy::ShareMesh | SaveStrategy::Silence | SaveStrategy::MinimalPublished;
|
||||
bool full_pathnames = wxGetApp().app_config->get_bool("export_sources_full_pathnames");
|
||||
if (full_pathnames)
|
||||
save_strategy = save_strategy | SaveStrategy::FullPathSources;
|
||||
ret = export_3mf(into_path(path), save_strategy, -1, nullptr);
|
||||
} catch (...) {
|
||||
restore_now();
|
||||
MessageDialog(this,
|
||||
_L("Failed to export the published 3MF file.\nPlease check whether the folder exists online or if other programs "
|
||||
"have the file open."),
|
||||
_L("Publish"), wxOK | wxICON_WARNING)
|
||||
.ShowModal();
|
||||
return wxID_CANCEL;
|
||||
}
|
||||
|
||||
if (ret < 0) {
|
||||
restore_now();
|
||||
MessageDialog(this,
|
||||
_L("Failed to export the published 3MF file.\nPlease check whether the folder exists online or if other programs "
|
||||
"have the file open."),
|
||||
_L("Publish"), wxOK | wxICON_WARNING)
|
||||
.ShowModal();
|
||||
return wxID_CANCEL;
|
||||
}
|
||||
restore_now();
|
||||
|
||||
// Register the exported file in the "Recently opened" list
|
||||
wxGetApp().mainframe->add_to_recent_projects(path);
|
||||
|
||||
return wxID_YES;
|
||||
}
|
||||
|
||||
bool Plater::get_pending_published(std::vector<std::string>& out_keys,
|
||||
std::vector<Slic3r::PublishedMaterialEntry>& out_material) const
|
||||
{
|
||||
if (!p->m_has_pending_published)
|
||||
return false;
|
||||
out_keys = p->m_pending_published_keys;
|
||||
out_material = p->m_pending_material_keys;
|
||||
return true;
|
||||
}
|
||||
|
||||
void Plater::set_pending_published(const std::vector<std::string>& published_keys,
|
||||
const std::vector<Slic3r::PublishedMaterialEntry>& material_keys)
|
||||
{
|
||||
p->m_has_pending_published = true;
|
||||
p->m_pending_published_keys = published_keys;
|
||||
p->m_pending_material_keys = material_keys;
|
||||
}
|
||||
|
||||
Preset *get_printer_preset(const MachineObject *obj)
|
||||
{
|
||||
if (!obj)
|
||||
|
||||
@@ -407,9 +407,7 @@ public:
|
||||
bool preview_zip_archive(const boost::filesystem::path& archive_path);
|
||||
|
||||
// BBS: restore
|
||||
std::vector<size_t> load_files(const std::vector<boost::filesystem::path>& input_files, LoadStrategy strategy = LoadStrategy::LoadModel | LoadStrategy::LoadConfig, bool ask_multi = false);
|
||||
// To be called when providing a list of files to the GUI slic3r on command line.
|
||||
std::vector<size_t> load_files(const std::vector<std::string>& input_files, LoadStrategy strategy = LoadStrategy::LoadModel | LoadStrategy::LoadConfig, bool ask_multi = false);
|
||||
std::vector<size_t> load_files(const std::vector<boost::filesystem::path>& input_files, LoadStrategy strategy = LoadStrategy::LoadModel | LoadStrategy::LoadConfig, bool ask_multi = false, bool* published_out = nullptr);
|
||||
// to be called on drag and drop
|
||||
bool load_files(const wxArrayString& filenames);
|
||||
|
||||
@@ -519,6 +517,13 @@ public:
|
||||
void export_gcode_3mf(bool export_all = false);
|
||||
void send_gcode_finish(wxString name);
|
||||
void export_core_3mf();
|
||||
// Export a "published" 3MF embedding the author-selected settings in the file metadata; a
|
||||
// pure export that leaves the in-memory project untouched.
|
||||
int export_published_3mf(const std::vector<std::string>& published_keys, const std::vector<Slic3r::PublishedMaterialEntry>& material_keys);
|
||||
// Session-level stash of the last published selection, seeded into the Publish dialog on
|
||||
// open and written on publish or on loading a published 3MF
|
||||
bool get_pending_published(std::vector<std::string>& out_keys, std::vector<Slic3r::PublishedMaterialEntry>& out_material) const;
|
||||
void set_pending_published(const std::vector<std::string>& published_keys, const std::vector<Slic3r::PublishedMaterialEntry>& material_keys);
|
||||
static TriangleMesh combine_mesh_fff(const ModelObject& mo, int instance_id, std::function<void(const std::string&)> notify_func = {});
|
||||
void export_stl(bool extended = false, bool selection_only = false, bool multi_stls = false, FileType file_type = FT_STL);
|
||||
//BBS: remove amf
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,286 @@
|
||||
#pragma once
|
||||
|
||||
#include "GUI_Utils.hpp"
|
||||
#include "wxExtensions.hpp"
|
||||
#include "Widgets/TabCtrl.hpp"
|
||||
|
||||
#include "libslic3r/PublishSettings.hpp"
|
||||
|
||||
#include <wx/wx.h>
|
||||
#include <wx/colour.h>
|
||||
#include <wx/scrolwin.h>
|
||||
#include <wx/menu.h>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
|
||||
// Forward declarations (all are global classes, see Widgets/TextInput.hpp and
|
||||
// Widgets/StaticLine.hpp).
|
||||
class TextInput;
|
||||
class StaticLine;
|
||||
|
||||
namespace Slic3r { namespace GUI {
|
||||
|
||||
struct PublishMaterialIdentity
|
||||
{
|
||||
std::string type;
|
||||
std::string vendor;
|
||||
std::string id;
|
||||
};
|
||||
|
||||
// One unmet dependency of an enabled mixed-filament slot: the component filament the mix uses
|
||||
// would ship without its material (either the component slot is not enabled at all, or it is
|
||||
// enabled with neither "Full Publish" nor the "Type" requirement checked). Slots are 0-based.
|
||||
struct MixedDependencyIssue
|
||||
{
|
||||
enum class Reason { Disabled, MaterialNotPublished };
|
||||
size_t mixed_slot{0};
|
||||
size_t component_slot{0};
|
||||
Reason reason{Reason::Disabled};
|
||||
};
|
||||
|
||||
// Dialog letting a model author select which settings get embedded in a 3MF. Nested tab layout
|
||||
// mirroring the Process settings (Printer / Filament / Process outer tabs, category or material
|
||||
// tabs inside each). Dirty settings are pre-checked and shown bold; on OK the print rows become
|
||||
// "orca_published_keys" and the material rows become "orca_published_material_keys".
|
||||
class PublishSettingsDialog : public DPIDialog
|
||||
{
|
||||
public:
|
||||
// Optional published selection (Feature A/B): when the caller supplies one (either a
|
||||
// remembered session selection or the payload of a freshly loaded published 3MF) the dialog
|
||||
// is seeded from it, overriding the dirty-default pre-check. A non-null pointer to an empty
|
||||
// selection means "publish nothing" (an intentional empty state); a null pointer means "no
|
||||
// remembered selection" (keep the dirty defaults).
|
||||
PublishSettingsDialog(wxWindow* parent = nullptr,
|
||||
const std::vector<std::string>* published_keys = nullptr,
|
||||
const std::vector<Slic3r::PublishedMaterialEntry>* material_keys = nullptr);
|
||||
~PublishSettingsDialog();
|
||||
|
||||
// The selected print/printer setting keys (in display order); printer keys carry a '#N'
|
||||
// per-extruder suffix.
|
||||
std::vector<std::string> GetPublishedKeys() const;
|
||||
|
||||
// The selected keys grouped per material section (base keys, no '#N' suffix).
|
||||
std::vector<Slic3r::PublishedMaterialEntry> GetPublishedMaterialKeys() const;
|
||||
|
||||
protected:
|
||||
void on_dpi_changed(const wxRect& suggested_rect) override;
|
||||
void on_sys_color_changed() override;
|
||||
|
||||
private:
|
||||
void fit_to_content();
|
||||
void refresh_mixed_tab_bitmaps();
|
||||
|
||||
// Which part of the settings the row/category came from.
|
||||
enum class Section { Print, Printer, Material };
|
||||
|
||||
// One selectable setting row: a checkbox (setting name) plus a value label and an optional
|
||||
// grey unit label. key is the full config key, possibly with a "#N" variant suffix
|
||||
// (print/printer rows); material rows carry the base key.
|
||||
enum class RowKind {
|
||||
Setting, // a regular setting key
|
||||
Color, // material colour requirement (filament_colour)
|
||||
Type, // material type requirement (read-only text)
|
||||
};
|
||||
struct Row
|
||||
{
|
||||
std::string key;
|
||||
wxString category;
|
||||
wxString subcategory;
|
||||
wxString label;
|
||||
wxString value;
|
||||
wxString unit;
|
||||
wxString section_title; // outer tab title, for filter matching
|
||||
Section section{Section::Print};
|
||||
RowKind kind{RowKind::Setting};
|
||||
size_t outer_index{0};
|
||||
size_t inner_index{0};
|
||||
bool dirty{false}; // matches a dirty base key: pre-checked + bold
|
||||
bool matches_filter{false}; // survives the active filter (computed by apply_filter)
|
||||
wxCheckBox* check{nullptr};
|
||||
wxStaticText* value_label{nullptr};
|
||||
wxStaticText* unit_label{nullptr};
|
||||
wxStaticBitmap* color_chip{nullptr}; // Color rows only; swatch next to the value
|
||||
wxSizerItem* item{nullptr}; // sizer item of this row's h-sizer in its tab list sizer
|
||||
};
|
||||
|
||||
// An optgroup heading. Rows store indices into m_rows.
|
||||
struct Subcategory
|
||||
{
|
||||
wxString title;
|
||||
::StaticLine* header{nullptr}; // null when the title is empty
|
||||
wxSizerItem* item{nullptr};
|
||||
std::vector<size_t> rows;
|
||||
};
|
||||
|
||||
// An inner TabCtrl page with its material controls and content.
|
||||
struct Category
|
||||
{
|
||||
wxString title;
|
||||
Section section{Section::Print};
|
||||
size_t group{0}; // index into m_sections / outer page
|
||||
size_t source_index{0}; // stable source page or material slot index
|
||||
wxPanel* page{nullptr};
|
||||
wxScrolledWindow* scroll{nullptr};
|
||||
wxBoxSizer* list_sizer{nullptr};
|
||||
wxStaticText* info{nullptr};
|
||||
wxPoint scroll_pos{0, 0};
|
||||
wxStaticBitmap* filament_color_chip{nullptr};
|
||||
wxStaticText* title_label{nullptr}; // material title (static text; Full Publish carries the label elsewhere)
|
||||
// "Enable": while unchecked nothing of this slot is exported and everything below the
|
||||
// header row is hidden. For physical slots the Full Publish toggle sits on a second
|
||||
// line (full_line_item) visible only when enabled; for mixed slots Enable alone implies
|
||||
// publishing the mix definition, so no Full Publish widget exists at all.
|
||||
wxCheckBox* enable_check{nullptr};
|
||||
wxSizerItem* full_line_item{nullptr}; // sizer item of the Full Publish line (physical slots only)
|
||||
// "Full Publish": while checked, the whole slot preset is serialized and its rows
|
||||
// (incl. Color/Type) are disabled.
|
||||
wxCheckBox* full_check{nullptr};
|
||||
// True for a mixed-color filament slot: no Material/Retraction rows; Enable publishes
|
||||
// the slot's gradient/ratio definition as a whole.
|
||||
bool is_mixed{false};
|
||||
// Material identity, only for Section::Material categories.
|
||||
std::string filament_type;
|
||||
std::string filament_vendor;
|
||||
std::string filament_id;
|
||||
// The author's 0-based filament slot this material section represents.
|
||||
size_t filament_slot{0};
|
||||
std::vector<Subcategory> subs;
|
||||
std::vector<size_t> rows; // flattened rows of this category
|
||||
};
|
||||
|
||||
// Frozen snapshot of a mixed filament slot's definition for the read-only visualization
|
||||
// painted on the slot's page. Plain data only: the paint handler must never touch the
|
||||
// config. For gradient slots the curve is pre-sampled (t, ratio) pairs, where ratio is the
|
||||
// first component's share over model height; anchors carry the raw control points.
|
||||
struct MixedVisualSpec
|
||||
{
|
||||
bool valid{false};
|
||||
bool is_gradient{false};
|
||||
std::vector<wxColour> component_colours; // colour per component, in config order
|
||||
std::vector<double> ratios; // sublayer shares summing to ~1 (non-gradient)
|
||||
std::vector<double> tri_weights; // 3-component mixes: barycentric shares
|
||||
std::vector<std::pair<double, double>> gradient_samples;
|
||||
std::vector<std::pair<double, double>> gradient_anchors;
|
||||
};
|
||||
|
||||
// One outer TabCtrl page. Category entries are its inner tabs.
|
||||
struct SectionGroup
|
||||
{
|
||||
wxString title; // _L("Printer") / _L("Filament") / _L("Process")
|
||||
Section kind{Section::Print}; // maps 1:1 to the display group
|
||||
std::string icon_name; // "printer" / "filament" / "process"
|
||||
ScalableBitmap icon_bmp; // tab icon next to the title; rescaled on DPI change
|
||||
wxPanel* page{nullptr};
|
||||
TabCtrl* tabs{nullptr};
|
||||
// Second tab strip, below the main one, listing only the mixed-color filament slots.
|
||||
// Present on the Material section only (null elsewhere).
|
||||
TabCtrl* mixed_tabs{nullptr};
|
||||
wxPanel* page_host{nullptr};
|
||||
wxBoxSizer* page_host_sizer{nullptr};
|
||||
int selected_inner{-1};
|
||||
// Selected mixed tab (index into mixed_categories), valid while a mixed slot page is shown.
|
||||
int selected_mixed{-1};
|
||||
std::vector<size_t> categories; // indices into m_categories (physical slots)
|
||||
std::vector<size_t> mixed_categories; // indices into m_categories (mixed slots)
|
||||
};
|
||||
|
||||
void build_option_model();
|
||||
// Seed the dialog from a published selection (print/printer keys + per-slot material keys):
|
||||
// the supplied selection is authoritative - it is applied after the dirty pre-check and
|
||||
// overrides it, so deselected dirty keys stay off. Rows/slots not present in the selection
|
||||
// are left unselected. Unknown or out-of-range entries are skipped gracefully.
|
||||
void apply_selection(const std::vector<std::string>& published_keys,
|
||||
const std::vector<Slic3r::PublishedMaterialEntry>& material_keys);
|
||||
// Frozen snapshot of a mixed slot's definition for the page visualization, resolved from
|
||||
// the full config once at dialog-build time. Gradient slots pre-sample exactly what the
|
||||
// slicer will print: the custom curve wins over the gradient_range endpoints over the
|
||||
// 0.10 -> 0.90 default (the resolution FilamentBitmapUtils::mixed_gradient_curve mirrors).
|
||||
static MixedVisualSpec make_mixed_visual_spec(const Slic3r::DynamicPrintConfig& full, size_t slot);
|
||||
void apply_filter(const wxString& filter_text);
|
||||
// Menu-only pseudo filters: show only the checked ("Filter selected") or only the
|
||||
// unchecked ("Filter non-selected") rows. The search box keeps the user's text.
|
||||
void apply_pseudo_filter(bool selected_only);
|
||||
// Recompute row matches and visibility for the active filter mode. filter is the lowered
|
||||
// search text; it is ignored by the pseudo modes.
|
||||
void refresh_filter(const wxString& filter);
|
||||
void select_all(bool value);
|
||||
void select_visible(bool value);
|
||||
void show_menu(wxMouseEvent& evt);
|
||||
void set_row_bold(Row& row, bool bold);
|
||||
// "Full Publish" toggled: disables/enables the material's rows.
|
||||
void on_full_toggle(size_t category_index);
|
||||
// "Enable" toggled on a material slot: reveals/hides everything below the header and, for a
|
||||
// mixed slot, auto-selects its component filaments' "Enable" + "Full Publish" toggles.
|
||||
void on_enable_toggle(size_t category_index);
|
||||
// Whether a category currently publishes something, driving its tab's indicator dot.
|
||||
// Print/Printer: any row checked. Material: the slot's "Enable" is on.
|
||||
bool category_has_selection(const Category& cat) const;
|
||||
// Recompute the indicator dot on every outer/inner tab from the current selection state.
|
||||
void refresh_tab_indicators();
|
||||
// Unmet dependencies of enabled mixed-filament slots, one record per (mix, component) pair:
|
||||
// "Enable" not checked on the component, or enabled with neither "Full Publish" nor the
|
||||
// "Type" requirement row checked. Colour is deliberately ignored (the receiver renders the
|
||||
// mix from its own components' colours). Sorted by mixed slot, then component slot.
|
||||
std::vector<MixedDependencyIssue> unpublished_mixed_components() const;
|
||||
// Read-only visualization of a mixed slot's definition (a stacked ratio bar, or the
|
||||
// Material Ratio vs Model Height graph for a gradient), inserted above the info hint
|
||||
// inside the category's scroll area.
|
||||
void add_mixed_visual(size_t category_index, const MixedVisualSpec& spec);
|
||||
// Return/create the fixed outer page for a Section kind.
|
||||
size_t section_group_for(Section kind);
|
||||
size_t category_index_for(const wxString& title,
|
||||
Section section,
|
||||
size_t group,
|
||||
size_t source_index,
|
||||
const PublishMaterialIdentity& identity = PublishMaterialIdentity(),
|
||||
bool is_mixed = false);
|
||||
size_t subcategory_index_for(size_t category_index, const wxString& title, const wxString& icon);
|
||||
void add_row_ui(const std::string& key,
|
||||
const wxString& label,
|
||||
const wxString& value,
|
||||
const wxString& unit,
|
||||
size_t category_index,
|
||||
size_t subcategory_index,
|
||||
RowKind kind = RowKind::Setting);
|
||||
// The non-structural filament keys of a slot's preset, for a "Full Publish" entry.
|
||||
std::vector<std::string> full_keys_for_slot() const;
|
||||
void save_scroll_position(Category& category);
|
||||
void show_outer_page(size_t section_index);
|
||||
void show_inner_page(size_t section_index, int inner_index);
|
||||
void show_mixed_page(size_t section_index, int mixed_index);
|
||||
void on_outer_tab_changed(wxCommandEvent& event);
|
||||
void on_inner_tab_changed(size_t section_index, wxCommandEvent& event);
|
||||
void on_mixed_tab_changed(size_t section_index, wxCommandEvent& event);
|
||||
bool row_is_visible(const Row& row) const;
|
||||
void apply_visibility();
|
||||
void bind_tab_events();
|
||||
|
||||
TabCtrl* m_outer_tabs{nullptr};
|
||||
wxPanel* m_outer_host{nullptr};
|
||||
wxBoxSizer* m_outer_host_sizer{nullptr};
|
||||
int m_selected_outer{-1};
|
||||
wxBoxSizer* m_fb_sizer{nullptr}; // "All"/"None" buttons sizer
|
||||
// Active filter mode: free text from the search box, or one of the menu's pseudo filters.
|
||||
enum class FilterMode { Text, SelectedOnly, UnselectedOnly };
|
||||
FilterMode m_filter_mode{FilterMode::Text};
|
||||
TextInput* m_filter_box{nullptr};
|
||||
wxTextCtrl* m_filter_ctrl{nullptr};
|
||||
wxStaticBitmap* m_menu_button{nullptr};
|
||||
// Shown while a pseudo filter is active (the search box keeps the user's text, so the chip
|
||||
// carries the visible state); clicking it returns to text filtering.
|
||||
wxStaticText* m_pseudo_chip{nullptr};
|
||||
wxString m_info_nonsel;
|
||||
wxString m_info_allsel;
|
||||
wxString m_info_empty;
|
||||
wxString m_info_mix; // body hint shown for a mixed slot (published as a whole)
|
||||
|
||||
ScalableBitmap m_search;
|
||||
ScalableBitmap m_menu;
|
||||
|
||||
std::vector<Row> m_rows;
|
||||
std::vector<Category> m_categories;
|
||||
std::vector<SectionGroup> m_sections;
|
||||
};
|
||||
|
||||
}} // namespace Slic3r::GUI
|
||||
+8
-17
@@ -7,6 +7,7 @@
|
||||
#include "libslic3r/FilamentMixer.hpp"
|
||||
#include "libslic3r/Utils.hpp"
|
||||
#include "libslic3r/Model.hpp"
|
||||
#include "libslic3r/PublishSettings.hpp"
|
||||
#include "libslic3r/GCode/GCodeProcessor.hpp"
|
||||
|
||||
#include "Search.hpp"
|
||||
@@ -5699,26 +5700,16 @@ if (is_marlin_flavor)
|
||||
optgroup->append_single_option_line("extruder_offset", "printer_extruder_basic_information#extruder-offset-position", extruder_idx);
|
||||
|
||||
//BBS: don't show retract related config menu in machine page
|
||||
// These optgroups are built from publishable_printer_retraction/z_hop_options() so the
|
||||
// published-3MF printer allowlist (their union in libslic3r/PublishSettings.hpp) can
|
||||
// never drift from what the machine page actually shows.
|
||||
optgroup = page->new_optgroup(L("Retraction"), L"param_retraction");
|
||||
optgroup->append_single_option_line("retraction_length", "printer_extruder_retraction#length", extruder_idx);
|
||||
optgroup->append_single_option_line("retract_restart_extra", "printer_extruder_retraction#extra-length-on-restart", extruder_idx);
|
||||
optgroup->append_single_option_line("retraction_speed", "printer_extruder_retraction#retraction-speed", extruder_idx);
|
||||
optgroup->append_single_option_line("deretraction_speed", "printer_extruder_retraction#deretraction-speed", extruder_idx);
|
||||
optgroup->append_single_option_line("retraction_minimum_travel", "printer_extruder_retraction#travel-distance-threshold", extruder_idx);
|
||||
optgroup->append_single_option_line("retract_when_changing_layer", "printer_extruder_retraction#retract-on-layer-change", extruder_idx);
|
||||
optgroup->append_single_option_line("wipe", "printer_extruder_retraction#wipe-while-retracting", extruder_idx);
|
||||
optgroup->append_single_option_line("wipe_distance", "printer_extruder_retraction#wipe-distance", extruder_idx);
|
||||
optgroup->append_single_option_line("retract_before_wipe", "printer_extruder_retraction#retract-amount-before-wipe", extruder_idx);
|
||||
// Orca
|
||||
optgroup->append_single_option_line("retract_after_wipe", "printer_extruder_retraction#retract-amount-after-wipe", extruder_idx);
|
||||
for (const PublishablePrinterOption& opt : publishable_printer_retraction_options())
|
||||
optgroup->append_single_option_line(opt.key, opt.icon, extruder_idx);
|
||||
|
||||
optgroup = page->new_optgroup(L("Z-Hop"), L"param_extruder_lift_enforcement");
|
||||
optgroup->append_single_option_line("retract_lift_enforce", "printer_extruder_z_hop#on-surfaces", extruder_idx);
|
||||
optgroup->append_single_option_line("z_hop_types", "printer_extruder_z_hop#z-hop-type", extruder_idx);
|
||||
optgroup->append_single_option_line("z_hop", "printer_extruder_z_hop#z-hop-height", extruder_idx);
|
||||
optgroup->append_single_option_line("travel_slope", "printer_extruder_z_hop#traveling-angle", extruder_idx);
|
||||
optgroup->append_single_option_line("retract_lift_above", "printer_extruder_z_hop#only-lift-z-above", extruder_idx);
|
||||
optgroup->append_single_option_line("retract_lift_below", "printer_extruder_z_hop#only-lift-z-below", extruder_idx);
|
||||
for (const PublishablePrinterOption& opt : publishable_printer_z_hop_options())
|
||||
optgroup->append_single_option_line(opt.key, opt.icon, extruder_idx);
|
||||
|
||||
optgroup = page->new_optgroup(L("Retraction when switching material"), L"param_retraction_material_change");
|
||||
optgroup->append_single_option_line("retract_length_toolchange", "printer_extruder_retraction#retraction-when-switching-materials", extruder_idx);
|
||||
|
||||
@@ -30,7 +30,6 @@
|
||||
#include <memory>
|
||||
|
||||
//#include "BedShapeDialog.hpp"
|
||||
#include "Event.hpp"
|
||||
#include "wxExtensions.hpp"
|
||||
#include "ConfigManipulation.hpp"
|
||||
#include "OptionsGroup.hpp"
|
||||
@@ -38,7 +37,6 @@
|
||||
//BBS: GUI refactor
|
||||
#include "Notebook.hpp"
|
||||
#include "ParamsPanel.hpp"
|
||||
#include "Widgets/RoundedRectangle.hpp"
|
||||
#include "Widgets/TextInput.hpp"
|
||||
#include "Widgets/CheckBox.hpp" // ORCA
|
||||
|
||||
@@ -472,6 +470,7 @@ protected:
|
||||
std::string m_last_sparse_infill_rotate_template_value;
|
||||
ConfigManipulation get_config_manipulation();
|
||||
friend class EditGCodeDialog;
|
||||
friend class PublishSettingsDialog;
|
||||
};
|
||||
|
||||
class TabPrint : public Tab
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#include "libslic3r/PresetBundle.hpp"
|
||||
#include "libslic3r/Color.hpp"
|
||||
#include "format.hpp"
|
||||
#include "ConfigValueFormatter.hpp"
|
||||
#include "GUI_App.hpp"
|
||||
#include "Plater.hpp"
|
||||
#include "Tab.hpp"
|
||||
@@ -22,7 +23,6 @@
|
||||
#include "MsgDialog.hpp"
|
||||
|
||||
#include "PresetComboBoxes.hpp"
|
||||
#include "Widgets/RoundedRectangle.hpp"
|
||||
#include "Widgets/CheckBox.hpp"
|
||||
#include "Widgets/DialogButtons.hpp"
|
||||
#include "Widgets/HyperLink.hpp"
|
||||
@@ -571,14 +571,6 @@ void DiffModel::Clear()
|
||||
}
|
||||
|
||||
|
||||
static std::string get_pure_opt_key(std::string opt_key)
|
||||
{
|
||||
const int pos = opt_key.find("#");
|
||||
if (pos > 0)
|
||||
boost::erase_tail(opt_key, opt_key.size() - pos);
|
||||
return opt_key;
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// DiffViewCtrl
|
||||
// ----------------------------------------------------------------------------
|
||||
@@ -1212,32 +1204,6 @@ bool UnsavedChangesDialog::save(PresetCollection* dependent_presets, bool show_s
|
||||
return true;
|
||||
}
|
||||
|
||||
wxString get_string_from_enum(const std::string& opt_key, const DynamicPrintConfig& config, bool is_infill = false, int idx = -1)
|
||||
{
|
||||
const ConfigOptionDef& def = config.def()->options.at(opt_key);
|
||||
const std::vector<std::string>& names = def.enum_labels;//ConfigOptionEnum<T>::get_enum_names();
|
||||
int val = 0;
|
||||
|
||||
if (idx >= 0)
|
||||
val = dynamic_cast<const ConfigOptionInts*>(config.option(opt_key))->get_at(idx);
|
||||
else
|
||||
val = config.option(opt_key)->getInt();
|
||||
|
||||
// Each infill doesn't use all list of infill declared in PrintConfig.hpp.
|
||||
// So we should "convert" val to the correct one
|
||||
if (is_infill) {
|
||||
for (auto key_val : *def.enum_keys_map)
|
||||
if (int(key_val.second) == val) {
|
||||
auto it = std::find(def.enum_values.begin(), def.enum_values.end(), key_val.first);
|
||||
if (it == def.enum_values.end())
|
||||
return "";
|
||||
return from_u8(_utf8(names[it - def.enum_values.begin()]));
|
||||
}
|
||||
return _L("Undefined");
|
||||
}
|
||||
return from_u8(_utf8(names[val]));
|
||||
}
|
||||
|
||||
// BBS
|
||||
#if 0
|
||||
static size_t get_id_from_opt_key(std::string opt_key)
|
||||
@@ -1251,194 +1217,6 @@ static size_t get_id_from_opt_key(std::string opt_key)
|
||||
}
|
||||
#endif
|
||||
|
||||
static wxString get_full_label(std::string opt_key, const DynamicPrintConfig& config)
|
||||
{
|
||||
opt_key = get_pure_opt_key(opt_key);
|
||||
auto option = config.option(opt_key);
|
||||
|
||||
if (!option || option->is_nil())
|
||||
return _L("N/A");
|
||||
|
||||
const ConfigOptionDef* opt = config.def()->get(opt_key);
|
||||
return opt->full_label.empty() ? opt->label : opt->full_label;
|
||||
}
|
||||
|
||||
static wxString get_string_value(std::string opt_key, const DynamicPrintConfig& config)
|
||||
{
|
||||
int orig_opt_idx = -1;
|
||||
int opt_idx = -1;
|
||||
int pos = opt_key.find("#");
|
||||
std::string temp_str = opt_key;
|
||||
if (pos > 0) {
|
||||
boost::erase_head(temp_str, pos + 1);
|
||||
orig_opt_idx = static_cast<size_t>(atoi(temp_str.c_str()));
|
||||
}
|
||||
opt_idx = orig_opt_idx >= 0 ? orig_opt_idx : 0;
|
||||
opt_key = get_pure_opt_key(opt_key);
|
||||
auto option = config.option(opt_key);
|
||||
if (!option) {
|
||||
return _L("N/A");
|
||||
}
|
||||
auto opt_vector = dynamic_cast<const ConfigOptionVectorBase *>(option);
|
||||
|
||||
if ((option->is_scalar() && config.option(opt_key)->is_nil()) ||
|
||||
(option->is_vector() && opt_vector && opt_idx >= 0 && opt_idx < opt_vector->size() && opt_vector->is_nil(opt_idx)))
|
||||
return _L("N/A");
|
||||
|
||||
wxString out;
|
||||
|
||||
const ConfigOptionDef* opt = config.def()->get(opt_key);
|
||||
bool is_nullable = opt->nullable;
|
||||
|
||||
switch (opt->type) {
|
||||
case coInt:
|
||||
return from_u8((boost::format("%1%") % config.opt_int(opt_key)).str());
|
||||
case coInts: {
|
||||
if (is_nullable) {
|
||||
auto values = config.opt<ConfigOptionIntsNullable>(opt_key);
|
||||
if (opt_idx < values->size())
|
||||
return from_u8((boost::format("%1%") % values->get_at(opt_idx)).str());
|
||||
}
|
||||
else {
|
||||
auto values = config.opt<ConfigOptionInts>(opt_key);
|
||||
if (orig_opt_idx >= 0 && orig_opt_idx < values->size()) {
|
||||
return from_u8((boost::format("%1%") % values->get_at(opt_idx)).str());
|
||||
}
|
||||
else {
|
||||
std::string value_str;
|
||||
for (int i = 0; i < values->size(); i++) {
|
||||
value_str += std::to_string(values->get_at(i));
|
||||
if (i != values->size() - 1) {
|
||||
value_str += ",";
|
||||
}
|
||||
}
|
||||
return from_u8(value_str);
|
||||
}
|
||||
}
|
||||
return _L("Undefined");
|
||||
}
|
||||
case coBool:
|
||||
return config.opt_bool(opt_key) ? "true" : "false";
|
||||
case coBools: {
|
||||
if (is_nullable) {
|
||||
auto values = config.opt<ConfigOptionBoolsNullable>(opt_key);
|
||||
if (opt_idx < values->size())
|
||||
return values->get_at(opt_idx) ? "true" : "false";
|
||||
}
|
||||
else {
|
||||
auto values = config.opt<ConfigOptionBools>(opt_key);
|
||||
if (opt_idx < values->size())
|
||||
return values->get_at(opt_idx) ? "true" : "false";
|
||||
}
|
||||
return _L("Undefined");
|
||||
}
|
||||
case coPercent:
|
||||
return from_u8((boost::format("%1%%%") % int(config.optptr(opt_key)->getFloat())).str());
|
||||
case coPercents: {
|
||||
if (is_nullable) {
|
||||
auto values = config.opt<ConfigOptionPercentsNullable>(opt_key);
|
||||
if (opt_idx < values->size())
|
||||
return from_u8((boost::format("%1%%%") % values->get_at(opt_idx)).str());
|
||||
}
|
||||
else {
|
||||
auto values = config.opt<ConfigOptionPercents>(opt_key);
|
||||
if (opt_idx < values->size())
|
||||
return from_u8((boost::format("%1%%%") % values->get_at(opt_idx)).str());
|
||||
}
|
||||
return _L("Undefined");
|
||||
}
|
||||
case coFloat:
|
||||
return double_to_string(config.opt_float(opt_key));
|
||||
case coFloats: {
|
||||
if (is_nullable) {
|
||||
auto values = config.opt<ConfigOptionFloatsNullable>(opt_key);
|
||||
if (opt_idx < values->size())
|
||||
return double_to_string(values->get_at(opt_idx));
|
||||
}
|
||||
else {
|
||||
auto values = config.opt<ConfigOptionFloats>(opt_key);
|
||||
if (values && opt_idx < values->size())
|
||||
return double_to_string(values->get_at(opt_idx));
|
||||
}
|
||||
return _L("Undefined");
|
||||
}
|
||||
case coString:
|
||||
return from_u8(config.opt_string(opt_key));
|
||||
case coStrings: {
|
||||
const ConfigOptionStrings* strings = config.opt<ConfigOptionStrings>(opt_key);
|
||||
if (strings) {
|
||||
if (opt_key == "compatible_printers" || opt_key == "compatible_prints") {
|
||||
if (strings->empty())
|
||||
return _L("All");
|
||||
for (size_t id = 0; id < strings->size(); id++)
|
||||
out += from_u8(strings->get_at(id)) + "\n";
|
||||
out.RemoveLast(1);
|
||||
return out;
|
||||
}
|
||||
if (!strings->empty() && opt_idx < strings->values.size())
|
||||
return from_u8(strings->get_at(opt_idx));
|
||||
}
|
||||
break;
|
||||
}
|
||||
case coFloatOrPercent: {
|
||||
const ConfigOptionFloatOrPercent* opt = config.opt<ConfigOptionFloatOrPercent>(opt_key);
|
||||
if (opt)
|
||||
out = double_to_string(opt->value) + (opt->percent ? "%" : "");
|
||||
return out;
|
||||
}
|
||||
case coEnum: {
|
||||
return get_string_from_enum(opt_key, config,
|
||||
opt_key == "top_surface_pattern" ||
|
||||
opt_key == "bottom_surface_pattern" ||
|
||||
opt_key == "internal_solid_infill_pattern" ||
|
||||
opt_key == "sparse_infill_pattern" ||
|
||||
opt_key == "ironing_pattern" ||
|
||||
opt_key == "support_ironing_pattern" ||
|
||||
opt_key == "support_pattern" ||
|
||||
opt_key == "support_interface_pattern")
|
||||
;
|
||||
}
|
||||
case coEnums: {
|
||||
return get_string_from_enum(opt_key, config,
|
||||
opt_key == "top_surface_pattern" ||
|
||||
opt_key == "bottom_surface_pattern" ||
|
||||
opt_key == "internal_solid_infill_pattern" ||
|
||||
opt_key == "sparse_infill_pattern" ||
|
||||
opt_key == "ironing_pattern" ||
|
||||
opt_key == "support_ironing_pattern" ||
|
||||
opt_key == "support_pattern" ||
|
||||
opt_key == "support_interface_pattern"
|
||||
, opt_idx);
|
||||
}
|
||||
case coPoint: {
|
||||
Vec2d val = config.opt<ConfigOptionPoint>(opt_key)->value;
|
||||
return from_u8((boost::format("[%1%]") % ConfigOptionPoint(val).serialize()).str());
|
||||
}
|
||||
case coPoints: {
|
||||
//BBS: add bed_exclude_area
|
||||
if (opt_key == "printable_area" || opt_key == "thumbnails") {
|
||||
ConfigOptionPoints points = *config.option<ConfigOptionPoints>(opt_key);
|
||||
//BuildVolume build_volume = {points.values, 0.};
|
||||
return get_thumbnails_string(points.values);
|
||||
}
|
||||
else if (opt_key == "bed_exclude_area") {
|
||||
return get_thumbnails_string(config.option<ConfigOptionPoints>(opt_key)->values);
|
||||
}
|
||||
else if (opt_key == "head_wrap_detect_zone") {
|
||||
return get_thumbnails_string(config.option<ConfigOptionPoints>(opt_key)->values);
|
||||
}
|
||||
else if (opt_key == "wrapping_exclude_area") {
|
||||
return get_thumbnails_string(config.option<ConfigOptionPoints>(opt_key)->values);
|
||||
}
|
||||
Vec2d val = config.opt<ConfigOptionPoints>(opt_key)->get_at(opt_idx);
|
||||
return from_u8((boost::format("[%1%]") % ConfigOptionPoint(val).serialize()).str());
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
void UnsavedChangesDialog::update(Preset::Type type, PresetCollection* dependent_presets, const std::string& new_selected_preset, const wxString& header)
|
||||
{
|
||||
PresetCollection* presets = dependent_presets;
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
#include <wx/textdlg.h>
|
||||
|
||||
#include <wx/wx.h>
|
||||
#include <wx/weakref.h>
|
||||
#include <wx/display.h>
|
||||
#include <wx/fileconf.h>
|
||||
#include <wx/file.h>
|
||||
@@ -562,6 +563,78 @@ void GuideFrame::OnScriptMessage(wxWebViewEvent &evt)
|
||||
m_ProfileJson["filament"][fName]["selected"] = 1;
|
||||
}
|
||||
}
|
||||
else if (strCmd == "check_for_new_printers") {
|
||||
json response = json::object();
|
||||
response["command"] = "check_new_printers_result";
|
||||
// Guide pages currently send sequence_id as a number, while older
|
||||
// pages may send it as a string. Preserve the value without
|
||||
// forcing either representation.
|
||||
if (j.contains("sequence_id"))
|
||||
response["sequence_id"] = j["sequence_id"];
|
||||
else
|
||||
response["sequence_id"] = "";
|
||||
|
||||
if (!m_MainPtr->preset_updater) {
|
||||
response["error"] = "Printer update service is unavailable.";
|
||||
wxString strJS = wxString::Format("HandleStudio(%s)", response.dump(-1, ' ', true));
|
||||
wxGetApp().CallAfter([this, strJS] { RunScript(strJS); });
|
||||
} else {
|
||||
// Orca: enumerate vendors directly from disk rather than from m_ProfileJson["model"]
|
||||
// — a vendor with no machine models (e.g. a filament-only bundle, or a test fixture
|
||||
// like "test123" with an empty machine_model_list) never gets a "vendor" entry
|
||||
// pushed into "model" by LoadProfileFamily(), so it would be invisible to the
|
||||
// request body and get endlessly re-offered by the server. Scan both the system dir
|
||||
// (already-installed vendors) and the bundled resources dir (shipped-but-not-yet-
|
||||
// installed vendors), same as LoadProfileData() does when building loaded_vendors.
|
||||
std::set<std::string> system_vendors;
|
||||
for (const auto& dir : {vendor_dir, rsrc_vendor_dir}) {
|
||||
if (!boost::filesystem::exists(dir))
|
||||
continue;
|
||||
for (const auto& entry : boost::filesystem::directory_iterator(dir)) {
|
||||
if (!boost::filesystem::is_directory(entry) && boost::iequals(entry.path().extension().string(), ".json"))
|
||||
system_vendors.insert(entry.path().stem().string());
|
||||
}
|
||||
}
|
||||
// Orca: check_new_vendors() is async (network + confirmation dialog + download
|
||||
// all happen off the calling thread apart from the dialog itself); guard against
|
||||
// this dialog being closed before the callback fires.
|
||||
wxWeakRef<GuideFrame> weak_this(this);
|
||||
try {
|
||||
m_MainPtr->preset_updater->check_new_vendors(
|
||||
system_vendors, [weak_this, response](std::vector<std::string> installed_vendors, bool declined) mutable {
|
||||
if (!weak_this)
|
||||
return;
|
||||
|
||||
// Orca: append the newly installed vendor(s) into the in-memory
|
||||
// profile data (instead of a full LoadProfileData() rescan of every
|
||||
// vendor) and push the refreshed list to the webview, the same way
|
||||
// request_userguide_profile does, so the printer list picks them up
|
||||
// without needing to reopen the guide.
|
||||
for (const auto& vendor_id : installed_vendors) {
|
||||
weak_this->LoadProfileFamily(vendor_id, (weak_this->vendor_dir / (vendor_id + ".json")).string());
|
||||
}
|
||||
if (!installed_vendors.empty()) {
|
||||
json profile_response = json::object();
|
||||
profile_response["command"] = "response_userguide_profile";
|
||||
profile_response["sequence_id"] = "10001";
|
||||
profile_response["response"] = weak_this->m_ProfileJson;
|
||||
wxString profileJS = wxString::Format("HandleStudio(%s)", profile_response.dump(-1, ' ', true));
|
||||
weak_this->RunScript(profileJS);
|
||||
}
|
||||
|
||||
response["vendors"] = installed_vendors;
|
||||
response["declined"] = declined;
|
||||
wxString strJS = wxString::Format("HandleStudio(%s)", response.dump(-1, ' ', true));
|
||||
weak_this->RunScript(strJS);
|
||||
});
|
||||
} catch (const std::exception &e) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "Failed to check for new printers: " << e.what();
|
||||
response["error"] = "Failed to check for new printers.";
|
||||
wxString strJS = wxString::Format("HandleStudio(%s)", response.dump(-1, ' ', true));
|
||||
wxGetApp().CallAfter([this, strJS] { RunScript(strJS); });
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (strCmd == "user_guide_finish") {
|
||||
SaveProfile();
|
||||
|
||||
@@ -1644,13 +1717,15 @@ int GuideFrame::LoadProfileFamily(std::string strVendor, std::string strFilePath
|
||||
OneModel["materials"] = pm["default_materials"];
|
||||
|
||||
// wxString strCoverPath = wxString::Format("%s\\%s\\%s_cover.png", strFolder, strVendor, std::string(s1.mb_str()));
|
||||
std::string cover_file = s1 + "_cover.png";
|
||||
boost::filesystem::path cover_path = boost::filesystem::absolute(boost::filesystem::path(resources_dir()) / "/profiles/" / strVendor / cover_file).make_preferred();
|
||||
std::string cover_file = s1 + "_cover.png";
|
||||
boost::filesystem::path cover_path = boost::filesystem::absolute(vendor_dir / cover_file).make_preferred();
|
||||
BOOST_LOG_TRIVIAL(info) << "[WebGuideDialog] " << cover_path;
|
||||
if (!boost::filesystem::exists(cover_path)) {
|
||||
cover_path =
|
||||
(boost::filesystem::absolute(boost::filesystem::path(resources_dir()) / "/web/image/printer/") /
|
||||
cover_file)
|
||||
.make_preferred();
|
||||
cover_path = boost::filesystem::absolute(boost::filesystem::path(resources_dir()) / "/profiles/" / strVendor / cover_file)
|
||||
.make_preferred();
|
||||
if (!boost::filesystem::exists(cover_path))
|
||||
cover_path = (boost::filesystem::absolute(boost::filesystem::path(resources_dir()) / "/web/image/printer/") / cover_file)
|
||||
.make_preferred();
|
||||
}
|
||||
OneModel["cover"] = cover_path.string();
|
||||
|
||||
|
||||
@@ -160,6 +160,15 @@ bool Button::GetValue() const { return state_handler.states() & StateHandler::Ch
|
||||
|
||||
void Button::SetCenter(bool isCenter) { this->isCenter = isCenter; }
|
||||
|
||||
void Button::SetIndicator(bool on)
|
||||
{
|
||||
if (m_show_indicator == on)
|
||||
return;
|
||||
m_show_indicator = on;
|
||||
messureSize();
|
||||
Refresh();
|
||||
}
|
||||
|
||||
void Button::SetVertical(bool vertical)
|
||||
{
|
||||
this->vertical = vertical;
|
||||
@@ -313,6 +322,13 @@ void Button::render(wxDC& dc)
|
||||
szContent.x -= d;
|
||||
}
|
||||
}
|
||||
if (m_show_indicator) {
|
||||
const int dot = FromDIP(6);
|
||||
if (vertical)
|
||||
szContent.y += dot + FromDIP(6);
|
||||
else
|
||||
szContent.x += dot + FromDIP(6);
|
||||
}
|
||||
// move to center
|
||||
wxRect rcContent = {{0, 0}, size};
|
||||
if (isCenter) {
|
||||
@@ -354,6 +370,17 @@ void Button::render(wxDC& dc)
|
||||
#endif
|
||||
dc.DrawText(text, pt);
|
||||
}
|
||||
if (m_show_indicator) {
|
||||
const int dot = FromDIP(6); // diameter
|
||||
wxPoint dot_pt;
|
||||
dot_pt.x = pt.x + (text.IsEmpty() ? 0 : textSize.x) + FromDIP(6) + dot / 2;
|
||||
// Centre on the content vertically; a bitmap-only (empty-label) tab has no text row.
|
||||
dot_pt.y = text.IsEmpty() ? rcContent.y + rcContent.height / 2 : pt.y + textSize.y / 2;
|
||||
const wxColour c = StateColor::darkModeColorFor(m_indicator_color);
|
||||
dc.SetBrush(wxBrush(c));
|
||||
dc.SetPen(wxPen(c));
|
||||
dc.DrawCircle(dot_pt, dot / 2);
|
||||
}
|
||||
}
|
||||
|
||||
void Button::messureSize()
|
||||
@@ -380,6 +407,14 @@ void Button::messureSize()
|
||||
szContent.y = szIcon.y;
|
||||
}
|
||||
}
|
||||
if (m_show_indicator) {
|
||||
// Indicator dot sits to the right of the label: its diameter plus the gap from the text.
|
||||
const int dot = FromDIP(6);
|
||||
if (vertical)
|
||||
szContent.y += dot + FromDIP(6);
|
||||
else
|
||||
szContent.x += dot + FromDIP(6);
|
||||
}
|
||||
wxSize size = szContent + paddingSize * 2;
|
||||
if (minSize.GetHeight() > 0)
|
||||
size.SetHeight(minSize.GetHeight());
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "../wxExtensions.hpp"
|
||||
#include "StaticBox.hpp"
|
||||
#include <wx/tipwin.h>
|
||||
#include <wx/colour.h>
|
||||
|
||||
class ButtonProps
|
||||
{
|
||||
@@ -45,6 +46,8 @@ class Button : public StaticBox
|
||||
bool canFocus = true;
|
||||
bool isCenter = true;
|
||||
bool vertical = false;
|
||||
bool m_show_indicator = false;
|
||||
wxColour m_indicator_color = wxColour("#009688");
|
||||
|
||||
static const int buttonWidth = 200;
|
||||
static const int buttonHeight = 50;
|
||||
@@ -78,6 +81,10 @@ public:
|
||||
|
||||
void SetSelected(bool selected = true) { m_selected = selected; }
|
||||
|
||||
// Show a small coloured dot to the right of the label (used by TabCtrl tabs to flag that
|
||||
// the tab's category has a selected/toggled setting).
|
||||
void SetIndicator(bool on);
|
||||
|
||||
// Only meant to be used by inspector, not public API
|
||||
ButtonStyle GetStyle() const { return m_style; }
|
||||
ButtonType GetType() const { return m_type; }
|
||||
|
||||
@@ -7,6 +7,7 @@ BEGIN_EVENT_TABLE(StaticBox, wxWindow)
|
||||
|
||||
// catch paint events
|
||||
//EVT_ERASE_BACKGROUND(StaticBox::eraseEvent)
|
||||
EVT_SIZE(StaticBox::sizeEvent)
|
||||
EVT_PAINT(StaticBox::paintEvent)
|
||||
|
||||
END_EVENT_TABLE()
|
||||
@@ -140,6 +141,12 @@ void StaticBox::eraseEvent(wxEraseEvent& evt)
|
||||
#endif
|
||||
}
|
||||
|
||||
void StaticBox::sizeEvent(wxSizeEvent& evt)
|
||||
{
|
||||
Refresh();
|
||||
evt.Skip();
|
||||
}
|
||||
|
||||
void StaticBox::paintEvent(wxPaintEvent& evt)
|
||||
{
|
||||
// depending on your system you may need to look at double-buffered dcs
|
||||
|
||||
@@ -46,6 +46,8 @@ public:
|
||||
protected:
|
||||
void eraseEvent(wxEraseEvent& evt);
|
||||
|
||||
void sizeEvent(wxSizeEvent& evt);
|
||||
|
||||
void paintEvent(wxPaintEvent& evt);
|
||||
|
||||
void render(wxDC& dc);
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
#include <wx/dc.h>
|
||||
|
||||
wxDEFINE_EVENT( wxEVT_TAB_SEL_CHANGING, wxCommandEvent );
|
||||
wxDEFINE_EVENT( wxEVT_TAB_SEL_CHANGED, wxCommandEvent );
|
||||
wxDEFINE_EVENT(wxEVT_TAB_SEL_CHANGING, wxCommandEvent);
|
||||
wxDEFINE_EVENT(wxEVT_TAB_SEL_CHANGED, wxCommandEvent);
|
||||
|
||||
BEGIN_EVENT_TABLE(TabCtrl, StaticBox)
|
||||
|
||||
@@ -22,11 +22,7 @@ END_EVENT_TABLE()
|
||||
#define TAB_BUTTON_PADDING_Y 2
|
||||
#define TAB_BUTTON_PADDING TAB_BUTTON_PADDING_X, TAB_BUTTON_PADDING_Y
|
||||
|
||||
TabCtrl::TabCtrl(wxWindow * parent,
|
||||
wxWindowID id,
|
||||
const wxPoint & pos,
|
||||
const wxSize & size,
|
||||
long style)
|
||||
TabCtrl::TabCtrl(wxWindow* parent, wxWindowID id, const wxPoint& pos, const wxSize& size, long style)
|
||||
: StaticBox(parent, id, pos, size, style)
|
||||
{
|
||||
#if 0
|
||||
@@ -42,14 +38,11 @@ TabCtrl::TabCtrl(wxWindow * parent,
|
||||
hsizer->Add(sizer, 0, wxEXPAND | wxBOTTOM, border_width * 4);
|
||||
SetSizer(hsizer);
|
||||
Bind(wxEVT_COMMAND_BUTTON_CLICKED, &TabCtrl::buttonClicked, this);
|
||||
//wxString reason;
|
||||
//IsTransparentBackgroundSupported(&reason);
|
||||
// wxString reason;
|
||||
// IsTransparentBackgroundSupported(&reason);
|
||||
}
|
||||
|
||||
TabCtrl::~TabCtrl()
|
||||
{
|
||||
delete images;
|
||||
}
|
||||
TabCtrl::~TabCtrl() { delete images; }
|
||||
|
||||
int TabCtrl::GetSelection() const { return sel; }
|
||||
|
||||
@@ -75,15 +68,13 @@ void TabCtrl::SelectItem(int item)
|
||||
Refresh();
|
||||
}
|
||||
|
||||
void TabCtrl::Unselect()
|
||||
{
|
||||
SelectItem(-1);
|
||||
}
|
||||
void TabCtrl::Unselect() { SelectItem(-1); }
|
||||
|
||||
void TabCtrl::Rescale()
|
||||
{
|
||||
for (auto & b : btns)
|
||||
for (auto& b : btns)
|
||||
b->Rescale();
|
||||
relayout();
|
||||
}
|
||||
|
||||
bool TabCtrl::SetFont(wxFont const& font)
|
||||
@@ -95,28 +86,32 @@ bool TabCtrl::SetFont(wxFont const& font)
|
||||
return true;
|
||||
}
|
||||
|
||||
int TabCtrl::AppendItem(const wxString &item,
|
||||
int image, int selImage,
|
||||
void * clientData)
|
||||
int TabCtrl::AppendItem(const wxString& item, int image, int selImage, void* clientData)
|
||||
{
|
||||
Button * btn = new Button();
|
||||
Button* btn = new Button();
|
||||
btn->Create(this, item, "", wxBORDER_NONE);
|
||||
btn->SetFont(GetFont());
|
||||
btn->SetTextColor(StateColor(
|
||||
std::make_pair(0x6B6B6C, (int) StateColor::NotChecked),
|
||||
std::make_pair(*wxLIGHT_GREY, (int) StateColor::Normal)));
|
||||
btn->SetTextColor(
|
||||
StateColor(std::make_pair(0x6B6B6C, (int) StateColor::NotChecked), std::make_pair(wxColour("#262E30"), (int) StateColor::Normal)));
|
||||
btn->SetBackgroundColor(StateColor());
|
||||
btn->SetCornerRadius(0);
|
||||
btn->SetPaddingSize({TAB_BUTTON_PADDING});
|
||||
btns.push_back(btn);
|
||||
if (btns.size() > 1)
|
||||
sizer->GetItem(sizer->GetItemCount() - 1)->SetMinSize({0, 0});
|
||||
sizer->Add(btn, 0, wxALIGN_CENTER_VERTICAL | wxLEFT | wxRIGHT, TAB_BUTTON_SPACE);
|
||||
sizer->Add(btn, 0, wxALIGN_CENTER_VERTICAL | wxLEFT | wxRIGHT, item_space);
|
||||
sizer->AddStretchSpacer(1);
|
||||
relayout();
|
||||
return btns.size() - 1;
|
||||
}
|
||||
|
||||
int TabCtrl::AppendItem(const wxString& item, const wxBitmap& bitmap, void* clientData)
|
||||
{
|
||||
const int index = AppendItem(item, -1, -1, clientData);
|
||||
SetItemBitmap(index, bitmap);
|
||||
return index;
|
||||
}
|
||||
|
||||
bool TabCtrl::DeleteItem(int item)
|
||||
{
|
||||
if (item < 0 || item >= btns.size()) {
|
||||
@@ -136,7 +131,7 @@ bool TabCtrl::DeleteItem(int item)
|
||||
sizer->GetItem(sizer->GetItemCount() - 1)->SetMinSize({0, 0});
|
||||
|
||||
if (selection_changed) {
|
||||
sel--; // `relayout()` uses `sel` so we need to update this before calling `relayout()`
|
||||
sel--; // `relayout()` uses `sel` so we need to update this before calling `relayout()`
|
||||
}
|
||||
relayout();
|
||||
if (selection_changed) {
|
||||
@@ -159,75 +154,87 @@ void TabCtrl::DeleteAllItems()
|
||||
|
||||
unsigned int TabCtrl::GetCount() const { return btns.size(); }
|
||||
|
||||
wxString TabCtrl::GetItemText(unsigned int item) const
|
||||
wxString TabCtrl::GetItemText(unsigned int item) const { return item < btns.size() ? btns[item]->GetLabel() : wxString{}; }
|
||||
|
||||
void TabCtrl::SetItemText(unsigned int item, wxString const& value)
|
||||
{
|
||||
return item < btns.size() ? btns[item]->GetLabel() : wxString{};
|
||||
if (item >= btns.size())
|
||||
return;
|
||||
btns[item]->SetLabel(value);
|
||||
}
|
||||
|
||||
void TabCtrl::SetItemText(unsigned int item, wxString const &value)
|
||||
void TabCtrl::SetItemBitmap(unsigned int item, const wxBitmap& bitmap)
|
||||
{
|
||||
if (item >= btns.size()) return;
|
||||
btns[item]->SetLabel(value);
|
||||
if (item >= btns.size())
|
||||
return;
|
||||
btns[item]->SetIcon(bitmap);
|
||||
relayout();
|
||||
}
|
||||
|
||||
void TabCtrl::SetItemIndicator(unsigned int item, bool on)
|
||||
{
|
||||
if (item >= btns.size())
|
||||
return;
|
||||
btns[item]->SetIndicator(on);
|
||||
relayout();
|
||||
}
|
||||
|
||||
bool TabCtrl::GetItemBold(unsigned int item) const
|
||||
{
|
||||
if (item >= btns.size()) return false;
|
||||
if (item >= btns.size())
|
||||
return false;
|
||||
return btns[item]->GetFont() == bold;
|
||||
}
|
||||
|
||||
void TabCtrl::SetItemBold(unsigned int item, bool bold)
|
||||
{
|
||||
if (item >= btns.size()) return;
|
||||
if (item >= btns.size())
|
||||
return;
|
||||
btns[item]->SetFont(bold ? this->bold : GetFont());
|
||||
btns[item]->Rescale();
|
||||
}
|
||||
|
||||
void* TabCtrl::GetItemData(unsigned int item) const
|
||||
{
|
||||
if (item >= btns.size()) return nullptr;
|
||||
if (item >= btns.size())
|
||||
return nullptr;
|
||||
return btns[item]->GetClientData();
|
||||
}
|
||||
|
||||
void TabCtrl::SetItemData(unsigned int item, void* clientData)
|
||||
{
|
||||
if (item >= btns.size()) return;
|
||||
if (item >= btns.size())
|
||||
return;
|
||||
btns[item]->SetClientData(clientData);
|
||||
}
|
||||
|
||||
void TabCtrl::AssignImageList(wxImageList* imageList)
|
||||
{
|
||||
if (images == imageList) return;
|
||||
if (images == imageList)
|
||||
return;
|
||||
delete images;
|
||||
images = imageList;
|
||||
}
|
||||
|
||||
void TabCtrl::SetItemTextColour(unsigned int item, const StateColor &col)
|
||||
void TabCtrl::SetItemTextColour(unsigned int item, const StateColor& col)
|
||||
{
|
||||
if (item >= btns.size()) return;
|
||||
if (item >= btns.size())
|
||||
return;
|
||||
btns[item]->SetTextColor(col);
|
||||
}
|
||||
|
||||
int TabCtrl::GetFirstVisibleItem() const
|
||||
{
|
||||
return btns.size() == 0 ? -1 : 0;
|
||||
}
|
||||
int TabCtrl::GetFirstVisibleItem() const { return btns.size() == 0 ? -1 : 0; }
|
||||
|
||||
int TabCtrl::GetNextVisible(int item) const
|
||||
{
|
||||
return ++item < btns.size() ? item : -1;
|
||||
}
|
||||
int TabCtrl::GetNextVisible(int item) const { return ++item < btns.size() ? item : -1; }
|
||||
|
||||
bool TabCtrl::IsVisible(unsigned int item) const
|
||||
{
|
||||
return true;
|
||||
}
|
||||
bool TabCtrl::IsVisible(unsigned int item) const { return true; }
|
||||
|
||||
void TabCtrl::DoSetSize(int x, int y, int width, int height, int sizeFlags)
|
||||
{
|
||||
auto size = GetSize();
|
||||
wxWindow::DoSetSize(x, y, width, height, sizeFlags);
|
||||
if (size == GetSize()) return;
|
||||
if (size == GetSize())
|
||||
return;
|
||||
relayout();
|
||||
}
|
||||
|
||||
@@ -235,7 +242,9 @@ void TabCtrl::DoSetSize(int x, int y, int width, int height, int sizeFlags)
|
||||
|
||||
WXLRESULT TabCtrl::MSWWindowProc(WXUINT nMsg, WXWPARAM wParam, WXLPARAM lParam)
|
||||
{
|
||||
if (nMsg == WM_GETDLGCODE) { return DLGC_WANTARROWS; }
|
||||
if (nMsg == WM_GETDLGCODE) {
|
||||
return DLGC_WANTARROWS;
|
||||
}
|
||||
return wxWindow::MSWWindowProc(nMsg, wParam, lParam);
|
||||
}
|
||||
|
||||
@@ -244,15 +253,15 @@ WXLRESULT TabCtrl::MSWWindowProc(WXUINT nMsg, WXWPARAM wParam, WXLPARAM lParam)
|
||||
void TabCtrl::relayout()
|
||||
{
|
||||
int offset = 10;
|
||||
int item = sel + 1;
|
||||
int first = 0;
|
||||
int item = sel + 1;
|
||||
int first = 0;
|
||||
for (int i = 0; i < item; ++i)
|
||||
offset += btns[i]->GetMinSize().x + TAB_BUTTON_SPACE * 2;
|
||||
offset += btns[i]->GetMinSize().x + item_space * 2;
|
||||
if (item < btns.size())
|
||||
offset += btns[item]->GetMinSize().x + TAB_BUTTON_SPACE * 2;
|
||||
int width = GetSize().x;
|
||||
offset += btns[item]->GetMinSize().x + item_space * 2;
|
||||
int width = GetSize().x;
|
||||
for (int i = 0; i < btns.size(); ++i) {
|
||||
auto size = btns[i]->GetMinSize().x + TAB_BUTTON_SPACE * 2;
|
||||
auto size = btns[i]->GetMinSize().x + item_space * 2;
|
||||
if (i < sel && offset > width) {
|
||||
sizer->Show(i * 2 + 1, false);
|
||||
sizer->Show(i * 2 + 2, false);
|
||||
@@ -273,14 +282,32 @@ void TabCtrl::relayout()
|
||||
sizer->GetItem(i * 2 + 2)->SetMinSize({0, 0});
|
||||
}
|
||||
if (item >= btns.size())
|
||||
-- item;
|
||||
--item;
|
||||
// Keep spacing 2 ~ 10 TAB_BUTTON_SPACE
|
||||
int b = GetSize().x - offset - 10 - (item + 1 - first) * TAB_BUTTON_SPACE * 8;
|
||||
int b = GetSize().x - offset - 10 - (item + 1 - first) * item_space * 8;
|
||||
sizer->GetItem(item * 2 + 2)->SetMinSize({b > 0 ? b : 0, 0});
|
||||
Layout();
|
||||
}
|
||||
|
||||
void TabCtrl::buttonClicked(wxCommandEvent &event)
|
||||
void TabCtrl::SetItemSpace(int space)
|
||||
{
|
||||
if (space < 0 || space == item_space)
|
||||
return;
|
||||
item_space = space;
|
||||
relayout();
|
||||
Refresh();
|
||||
}
|
||||
|
||||
int TabCtrl::GetFullSize() const
|
||||
{
|
||||
// Mirrors relayout(): a 10px leading spacer plus every button's min width and spacing.
|
||||
int width = 10;
|
||||
for (const Button* btn : btns)
|
||||
width += btn->GetMinSize().x + item_space * 2;
|
||||
return width;
|
||||
}
|
||||
|
||||
void TabCtrl::buttonClicked(wxCommandEvent& event)
|
||||
{
|
||||
SetFocus();
|
||||
auto btn = event.GetEventObject();
|
||||
@@ -288,7 +315,7 @@ void TabCtrl::buttonClicked(wxCommandEvent &event)
|
||||
SelectItem(iter == btns.end() ? -1 : iter - btns.begin());
|
||||
}
|
||||
|
||||
void TabCtrl::keyDown(wxKeyEvent &event)
|
||||
void TabCtrl::keyDown(wxKeyEvent& event)
|
||||
{
|
||||
switch (event.GetKeyCode()) {
|
||||
case WXK_UP:
|
||||
@@ -307,11 +334,13 @@ void TabCtrl::keyDown(wxKeyEvent &event)
|
||||
void TabCtrl::doRender(wxDC& dc)
|
||||
{
|
||||
wxSize size = GetSize();
|
||||
int states = state_handler.states();
|
||||
if (sel < 0) { return; }
|
||||
int states = state_handler.states();
|
||||
if (sel < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto x1 = btns[sel]->GetPosition().x;
|
||||
auto x2 = x1 + btns[sel]->GetSize().x;
|
||||
auto x1 = btns[sel]->GetPosition().x;
|
||||
auto x2 = x1 + btns[sel]->GetSize().x;
|
||||
const int BS2 = (1 + border_width) / 2;
|
||||
#if 0
|
||||
const int BS = border_width / 2;
|
||||
|
||||
@@ -3,32 +3,30 @@
|
||||
|
||||
#include "Button.hpp"
|
||||
|
||||
wxDECLARE_EVENT( wxEVT_TAB_SEL_CHANGING, wxCommandEvent );
|
||||
wxDECLARE_EVENT( wxEVT_TAB_SEL_CHANGED, wxCommandEvent );
|
||||
wxDECLARE_EVENT(wxEVT_TAB_SEL_CHANGING, wxCommandEvent);
|
||||
wxDECLARE_EVENT(wxEVT_TAB_SEL_CHANGED, wxCommandEvent);
|
||||
|
||||
class TabCtrl : public StaticBox
|
||||
{
|
||||
std::vector<Button*> btns;
|
||||
wxImageList* images = nullptr;
|
||||
wxBoxSizer * sizer = nullptr;
|
||||
wxBoxSizer* sizer = nullptr;
|
||||
|
||||
int sel = -1;
|
||||
wxFont bold;
|
||||
int item_space = 2; // space around each button, both sides (SetItemSpace)
|
||||
|
||||
public:
|
||||
TabCtrl(wxWindow * parent,
|
||||
wxWindowID id,
|
||||
const wxPoint & pos = wxDefaultPosition,
|
||||
const wxSize & size = wxDefaultSize,
|
||||
long style = 0);
|
||||
TabCtrl(wxWindow* parent, wxWindowID id, const wxPoint& pos = wxDefaultPosition, const wxSize& size = wxDefaultSize, long style = 0);
|
||||
|
||||
~TabCtrl();
|
||||
|
||||
public:
|
||||
virtual bool SetFont(wxFont const & font) override;
|
||||
virtual bool SetFont(wxFont const& font) override;
|
||||
|
||||
public:
|
||||
int AppendItem(const wxString &item, int image = -1, int selImage = -1, void *clientData = nullptr);
|
||||
int AppendItem(const wxString& item, int image = -1, int selImage = -1, void* clientData = nullptr);
|
||||
int AppendItem(const wxString& item, const wxBitmap& bitmap, void* clientData = nullptr);
|
||||
|
||||
bool DeleteItem(int item);
|
||||
|
||||
@@ -36,7 +34,7 @@ public:
|
||||
|
||||
unsigned int GetCount() const;
|
||||
|
||||
int GetSelection() const;
|
||||
int GetSelection() const;
|
||||
|
||||
void SelectItem(int item);
|
||||
|
||||
@@ -45,15 +43,19 @@ public:
|
||||
virtual void Rescale();
|
||||
|
||||
wxString GetItemText(unsigned int item) const;
|
||||
void SetItemText(unsigned int item, wxString const &value);
|
||||
void SetItemText(unsigned int item, wxString const& value);
|
||||
void SetItemBitmap(unsigned int item, const wxBitmap& bitmap);
|
||||
|
||||
bool GetItemBold(unsigned int item) const;
|
||||
void SetItemBold(unsigned int item, bool bold);
|
||||
// Show/hide the small "has selection" dot next to a tab's text.
|
||||
void SetItemIndicator(unsigned int item, bool on);
|
||||
|
||||
void* GetItemData(unsigned int item) const;
|
||||
void SetItemData(unsigned int item, void *clientData);
|
||||
|
||||
void AssignImageList(wxImageList *imageList);
|
||||
bool GetItemBold(unsigned int item) const;
|
||||
void SetItemBold(unsigned int item, bool bold);
|
||||
|
||||
void* GetItemData(unsigned int item) const;
|
||||
void SetItemData(unsigned int item, void* clientData);
|
||||
|
||||
void AssignImageList(wxImageList* imageList);
|
||||
|
||||
void SetItemTextColour(unsigned int item, const StateColor& col);
|
||||
|
||||
@@ -62,6 +64,12 @@ public:
|
||||
int GetNextVisible(int item) const;
|
||||
bool IsVisible(unsigned int item) const;
|
||||
|
||||
// Extra space around each tab button (in px on both sides). Defaults to the control-wide
|
||||
// standard; call before appending items so every button picks it up.
|
||||
void SetItemSpace(int space);
|
||||
|
||||
int GetFullSize() const;
|
||||
|
||||
private:
|
||||
virtual void DoSetSize(int x, int y, int width, int height, int sizeFlags = wxSIZE_AUTO) override;
|
||||
|
||||
@@ -71,10 +79,10 @@ private:
|
||||
|
||||
void relayout();
|
||||
|
||||
void buttonClicked(wxCommandEvent & event);
|
||||
void keyDown(wxKeyEvent &event);
|
||||
void buttonClicked(wxCommandEvent& event);
|
||||
void keyDown(wxKeyEvent& event);
|
||||
|
||||
void doRender(wxDC & dc) override;
|
||||
void doRender(wxDC& dc) override;
|
||||
|
||||
// some useful events
|
||||
bool sendTabCtrlEvent(bool changing = false);
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <mutex>
|
||||
#include <unordered_map>
|
||||
#include <ostream>
|
||||
#include <utility>
|
||||
@@ -29,6 +30,7 @@
|
||||
#include "libslic3r/format.hpp"
|
||||
#include "libslic3r/Utils.hpp"
|
||||
#include "libslic3r/PresetBundle.hpp"
|
||||
#include "libslic3r/PresetCacheFormat.hpp"
|
||||
#include "libslic3r_version.h"
|
||||
#include "slic3r/GUI/GUI.hpp"
|
||||
#include "slic3r/GUI/GUI_App.hpp"
|
||||
@@ -96,6 +98,8 @@ struct Update
|
||||
bool forced_update;
|
||||
//BBS: add directory support
|
||||
bool is_directory {false};
|
||||
// Orca: a vendor update may be the cache-only form.
|
||||
bool is_opc {false};
|
||||
|
||||
Update() {}
|
||||
//BBS: add directory support
|
||||
@@ -126,13 +130,24 @@ struct Update
|
||||
//BBS: add directory support
|
||||
void install() const
|
||||
{
|
||||
if (is_directory) {
|
||||
if (is_directory) {
|
||||
copy_directory_recursively(source, target, file_filter);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
copy_file_fix(source, target);
|
||||
|
||||
// A vendor must be installed in exactly one form. Remove the
|
||||
// representation that would otherwise be stale or shadow this one.
|
||||
boost::system::error_code ec;
|
||||
if (is_opc) {
|
||||
fs::remove(target.parent_path() / (vendor + ".json"), ec);
|
||||
ec.clear();
|
||||
fs::remove_all(target.parent_path() / vendor, ec);
|
||||
}
|
||||
else {
|
||||
fs::remove(target.parent_path() / (vendor + ".opc"), ec);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
friend std::ostream& operator<<(std::ostream& os, const Update &self)
|
||||
{
|
||||
@@ -180,6 +195,8 @@ struct Updates
|
||||
std::vector<Update> updates;
|
||||
};
|
||||
|
||||
static bool reload_configs_update_gui();
|
||||
|
||||
|
||||
wxDEFINE_EVENT(EVT_SLIC3R_VERSION_ONLINE, wxCommandEvent);
|
||||
wxDEFINE_EVENT(EVT_SLIC3R_EXPERIMENTAL_VERSION_ONLINE, wxCommandEvent);
|
||||
@@ -207,6 +224,10 @@ struct PresetUpdater::priv
|
||||
|
||||
// Per-vendor update checking
|
||||
std::set<std::string> checked_vendors;
|
||||
// Orca (PR #130): changelog text for each vendor, captured in memory during
|
||||
// sync_vendor_config()/check_new_vendors() instead of written beside the cache.
|
||||
std::unordered_map<std::string, std::string> vendor_changelogs;
|
||||
mutable std::mutex vendor_changelogs_mutex;
|
||||
std::vector<std::thread> vendor_check_threads;
|
||||
std::atomic<bool> vendor_check_cancel{false};
|
||||
|
||||
@@ -231,6 +252,8 @@ struct PresetUpdater::priv
|
||||
void parse_version_string(const std::string& body) const;
|
||||
void sync_resources(std::string http_url, std::map<std::string, Resource> &resources, bool check_patch = false, std::string current_version="", std::string changelog_file="");
|
||||
void sync_vendor_config(const std::string& vendor_id);
|
||||
void check_new_vendors(const std::set<std::string>& system_vendors,
|
||||
std::function<void(std::vector<std::string>, bool)> callback);
|
||||
void sync_tooltip(std::string http_url, std::string language);
|
||||
void sync_plugins(std::string http_url, std::string plugin_version);
|
||||
void sync_printer_config(std::string http_url);
|
||||
@@ -268,7 +291,7 @@ void PresetUpdater::priv::set_download_prefs(AppConfig *app_config)
|
||||
version_check_url = app_config->version_check_url();
|
||||
|
||||
auto profile_update_url = app_config->profile_update_url();
|
||||
if (!profile_update_url.empty())
|
||||
if (!profile_update_url.empty() && app_config->get_bool("enable_ota"))
|
||||
enabled_config_update = true;
|
||||
else
|
||||
enabled_config_update = false;
|
||||
@@ -671,6 +694,9 @@ void PresetUpdater::priv::sync_vendor_config(const std::string& vendor_id)
|
||||
|
||||
std::string online_version_str; // this represents the PROFILE VERSION, not ORCA VERSION
|
||||
std::string download_url_str;
|
||||
std::string changelog;
|
||||
|
||||
BOOST_LOG_TRIVIAL(info) << "[Orca Updater] fetching vendor update status from " << url;
|
||||
|
||||
Http::get(url)
|
||||
.timeout_connect(5)
|
||||
@@ -683,9 +709,12 @@ void PresetUpdater::priv::sync_vendor_config(const std::string& vendor_id)
|
||||
if (http_status != 200) return;
|
||||
try {
|
||||
json j = json::parse(body);
|
||||
BOOST_LOG_TRIVIAL(info) << "[Orca Updater] url: " << url << " returned:" << body;
|
||||
|
||||
if (j.contains("vendor_version") && j.contains("download_url")) {
|
||||
online_version_str = j["vendor_version"].get<std::string>();
|
||||
download_url_str = j["download_url"].get<std::string>();
|
||||
changelog = j.value("changelog", std::string());
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "[Orca Updater] vendor check JSON parse failed: " << e.what();
|
||||
@@ -707,6 +736,10 @@ void PresetUpdater::priv::sync_vendor_config(const std::string& vendor_id)
|
||||
boost::system::error_code ec;
|
||||
fs::remove_all(cache_profile_path / vendor_id, ec);
|
||||
fs::remove(cache_profile_path / (vendor_id + ".json"), ec);
|
||||
// Orca: the OPC cache is the vendor's whole installation in one file; clear it too.
|
||||
fs::remove(cache_profile_path / (vendor_id + ".opc"), ec);
|
||||
// Best-effort cleanup of the legacy on-disk changelog written by older builds
|
||||
// (changelogs are now kept in memory - see vendor_changelogs).
|
||||
fs::remove(cache_profile_path / (vendor_id + ".changelog"), ec);
|
||||
|
||||
// Download the zip
|
||||
@@ -735,7 +768,7 @@ void PresetUpdater::priv::sync_vendor_config(const std::string& vendor_id)
|
||||
if (!download_ok || cancel || vendor_check_cancel) return;
|
||||
|
||||
// Extract vendor profile bundles under ota/profiles. The downloaded zip contains
|
||||
// the vendor json/folder at its root.
|
||||
// either the vendor json/folder or the vendor cache at its root.
|
||||
BOOST_LOG_TRIVIAL(info) << "[Orca Updater] extracting update for " << vendor_id;
|
||||
if (!extract_file(download_file, cache_profile_path)) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "[Orca Updater] extraction failed for " << vendor_id;
|
||||
@@ -745,6 +778,27 @@ void PresetUpdater::priv::sync_vendor_config(const std::string& vendor_id)
|
||||
|
||||
if (cancel || vendor_check_cancel) return;
|
||||
|
||||
const fs::path cached_vendor_json = cache_profile_path / (vendor_id + ".json");
|
||||
const fs::path cached_vendor_folder = cache_profile_path / vendor_id;
|
||||
|
||||
const fs::path cached_vendor_opc = cache_profile_path / (vendor_id + ".opc");
|
||||
|
||||
bool is_json_update = fs::is_regular_file(cached_vendor_json) && fs::is_directory(cached_vendor_folder) && !fs::is_empty(cached_vendor_folder);
|
||||
bool is_opc_update = fs::is_regular_file(cached_vendor_opc);
|
||||
if (!is_json_update && !is_opc_update) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "[Orca Updater] rejected update for " << vendor_id
|
||||
<< ": expected " << vendor_id << ".json and a non-empty "
|
||||
<< vendor_id << " directory, or OPC update format.";
|
||||
fs::remove_all(cached_vendor_folder, ec);
|
||||
fs::remove(cached_vendor_json, ec);
|
||||
return;
|
||||
}
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(vendor_changelogs_mutex);
|
||||
vendor_changelogs[vendor_id] = std::move(changelog);
|
||||
}
|
||||
|
||||
BOOST_LOG_TRIVIAL(info) << "[Orca Updater] vendor " << vendor_id << " update cached, notifying UI";
|
||||
GUI::wxGetApp().CallAfter([] {
|
||||
GUI::wxGetApp().check_config_updates_from_updater();
|
||||
@@ -1039,6 +1093,7 @@ void PresetUpdater::priv::check_installed_vendor_profiles() const
|
||||
BOOST_LOG_TRIVIAL(info) << "[Orca Updater]:Checking whether the profile from resource is newer";
|
||||
|
||||
AppConfig *app_config = GUI::wxGetApp().app_config;
|
||||
|
||||
const auto enabled_vendors = app_config->vendors();
|
||||
|
||||
std::set<std::string> bundles;
|
||||
@@ -1147,68 +1202,110 @@ Updates PresetUpdater::priv::get_config_updates(const Semver &old_slic3r_version
|
||||
if (!fs::exists(cache_profile_path))
|
||||
return updates;
|
||||
|
||||
for (auto &dir_entry : boost::filesystem::directory_iterator(cache_profile_path)) {
|
||||
const auto &path = dir_entry.path();
|
||||
std::string file_path = path.string();
|
||||
if (is_json_file(file_path)) {
|
||||
const auto path_in_vendor = vendor_path / path.filename();
|
||||
std::string vendor_name = path.filename().string();
|
||||
// Remove the .json suffix.
|
||||
vendor_name.erase(vendor_name.size() - 5);
|
||||
auto print_in_cache = (cache_profile_path / vendor_name / PRESET_PRINT_NAME);
|
||||
auto filament_in_cache = (cache_profile_path / vendor_name / PRESET_FILAMENT_NAME);
|
||||
auto machine_in_cache = (cache_profile_path / vendor_name / PRESET_PRINTER_NAME);
|
||||
// Orca (PR #130): vendor changelogs are captured in memory during
|
||||
// sync_vendor_config()/check_new_vendors(), not written beside the cache.
|
||||
std::unordered_map<std::string, std::string> changelogs;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(vendor_changelogs_mutex);
|
||||
changelogs = vendor_changelogs;
|
||||
}
|
||||
|
||||
if (is_vendor_installed(vendor_name)
|
||||
|| fs::exists(print_in_cache)
|
||||
|| fs::exists(filament_in_cache)
|
||||
|| fs::exists(machine_in_cache)) {
|
||||
// Orca: a vendor installed as a preset cache carries its version there.
|
||||
Semver vendor_ver = installed_vendor_version(vendor_name);
|
||||
for (auto &dir_entry : boost::filesystem::directory_iterator(cache_profile_path)) {
|
||||
const auto &path = dir_entry.path();
|
||||
std::string file_path = path.string();
|
||||
const bool is_opc_file = boost::iequals(path.extension().string(), ".opc");
|
||||
if (!is_json_file(file_path) && !is_opc_file)
|
||||
continue;
|
||||
|
||||
std::map<std::string, std::string> key_values;
|
||||
std::vector<std::string> keys(3);
|
||||
Semver cache_ver;
|
||||
keys[0] = BBL_JSON_KEY_VERSION;
|
||||
keys[1] = BBL_JSON_KEY_DESCRIPTION;
|
||||
keys[2] = BBL_JSON_KEY_FORCE_UPDATE;
|
||||
get_values_from_json(file_path, keys, key_values);
|
||||
std::string description = key_values[BBL_JSON_KEY_DESCRIPTION];
|
||||
bool force_update = false;
|
||||
if (key_values.find(BBL_JSON_KEY_FORCE_UPDATE) != key_values.end())
|
||||
force_update = (key_values[BBL_JSON_KEY_FORCE_UPDATE] == "1")?true:false;
|
||||
auto config_version = Semver::parse(key_values[BBL_JSON_KEY_VERSION]);
|
||||
if (config_version)
|
||||
cache_ver = *config_version;
|
||||
const std::string vendor_name = path.stem().string();
|
||||
auto print_in_cache = (cache_profile_path / vendor_name / PRESET_PRINT_NAME);
|
||||
auto filament_in_cache = (cache_profile_path / vendor_name / PRESET_FILAMENT_NAME);
|
||||
auto machine_in_cache = (cache_profile_path / vendor_name / PRESET_PRINTER_NAME);
|
||||
|
||||
std::string changelog;
|
||||
std::string changelog_file = (cache_profile_path / (vendor_name + ".changelog")).string();
|
||||
boost::nowide::ifstream ifs(changelog_file);
|
||||
if (ifs) {
|
||||
std::ostringstream oss;
|
||||
oss<< ifs.rdbuf();
|
||||
changelog = oss.str();
|
||||
ifs.close();
|
||||
}
|
||||
// Orca (PR #130): a JSON cache entry is only meaningful next to a non-empty
|
||||
// <vendor>/ preset directory; a stray or half-downloaded <vendor>.json is
|
||||
// skipped. An .opc cache is a single self-contained file (validated below),
|
||||
// so this check does not apply to it.
|
||||
if (!is_opc_file) {
|
||||
const auto vendor_folder_in_cache = cache_profile_path / vendor_name;
|
||||
if (!fs::is_regular_file(path) || !fs::is_directory(vendor_folder_in_cache) ||
|
||||
fs::is_empty(vendor_folder_in_cache)) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "[Orca Updater]:ignoring invalid cached update for "
|
||||
<< vendor_name << ": expected " << vendor_name
|
||||
<< ".json and a non-empty " << vendor_name << " directory";
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (vendor_ver < cache_ver) {
|
||||
BOOST_LOG_TRIVIAL(info) << "[Orca Updater]:need to update settings from " << vendor_ver.to_string()
|
||||
<< " to newer version " << cache_ver.to_string() << ", app version " << SLIC3R_VERSION;
|
||||
Version version;
|
||||
version.config_version = cache_ver;
|
||||
version.comment = description;
|
||||
// Orca: update vendor.json
|
||||
updates.updates.emplace_back(std::move(file_path), path_in_vendor.string(), std::move(version), vendor_name, changelog, "", force_update, false);
|
||||
//Orca: update vendor folder
|
||||
updates.updates.emplace_back(cache_profile_path / vendor_name, vendor_path / vendor_name, Version(), vendor_name, "", "", force_update, true);
|
||||
} else {
|
||||
BOOST_LOG_TRIVIAL(info) << "[Orca Updater]:cached settings for " << vendor_name
|
||||
<< " are not newer than installed version, installed " << vendor_ver.to_string()
|
||||
<< ", cached " << cache_ver.to_string();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (is_vendor_installed(vendor_name)
|
||||
|| is_opc_file
|
||||
|| fs::exists(print_in_cache)
|
||||
|| fs::exists(filament_in_cache)
|
||||
|| fs::exists(machine_in_cache)) {
|
||||
// Orca: a vendor installed as a preset cache carries its version there.
|
||||
Semver vendor_ver = installed_vendor_version(vendor_name);
|
||||
|
||||
Semver cache_ver;
|
||||
std::string description;
|
||||
bool force_update = false;
|
||||
if (is_opc_file) {
|
||||
cache_ver = VendorCacheFile::usable_version(file_path, vendor_name);
|
||||
if (!cache_ver.valid()) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "[Orca Updater]:ignoring unreadable vendor cache " << file_path;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
else {
|
||||
std::map<std::string, std::string> key_values;
|
||||
std::vector<std::string> keys(3);
|
||||
keys[0] = BBL_JSON_KEY_VERSION;
|
||||
keys[1] = BBL_JSON_KEY_DESCRIPTION;
|
||||
keys[2] = BBL_JSON_KEY_FORCE_UPDATE;
|
||||
get_values_from_json(file_path, keys, key_values);
|
||||
description = key_values[BBL_JSON_KEY_DESCRIPTION];
|
||||
if (key_values.find(BBL_JSON_KEY_FORCE_UPDATE) != key_values.end())
|
||||
force_update = (key_values[BBL_JSON_KEY_FORCE_UPDATE] == "1")?true:false;
|
||||
auto config_version = Semver::parse(key_values[BBL_JSON_KEY_VERSION]);
|
||||
if (config_version)
|
||||
cache_ver = *config_version;
|
||||
}
|
||||
|
||||
// Orca (PR #130): changelog for this vendor was captured in memory at sync time.
|
||||
const auto changelog_it = changelogs.find(vendor_name);
|
||||
std::string changelog = changelog_it != changelogs.end() ? changelog_it->second : std::string();
|
||||
|
||||
if (vendor_ver < cache_ver) {
|
||||
BOOST_LOG_TRIVIAL(info) << "[Orca Updater]:need to update settings from " << vendor_ver.to_string()
|
||||
<< " to newer version " << cache_ver.to_string() << ", app version " << SLIC3R_VERSION;
|
||||
Version version;
|
||||
version.config_version = cache_ver;
|
||||
version.comment = description;
|
||||
if (is_opc_file) {
|
||||
// A cache contains the vendor profile and all presets.
|
||||
// Install it directly; Update::install removes any
|
||||
// superseded JSON representation.
|
||||
auto &update = updates.updates.emplace_back(
|
||||
std::move(file_path), vendor_path / (vendor_name + ".opc"),
|
||||
std::move(version), vendor_name, changelog, "", force_update, false);
|
||||
update.is_opc = true;
|
||||
}
|
||||
else {
|
||||
// JSON profile and its preset directory are installed
|
||||
// separately, as before.
|
||||
updates.updates.emplace_back(std::move(file_path),
|
||||
vendor_path / (vendor_name + ".json"), std::move(version),
|
||||
vendor_name, changelog, "", force_update, false);
|
||||
updates.updates.emplace_back(cache_profile_path / vendor_name,
|
||||
vendor_path / vendor_name, Version(), vendor_name,
|
||||
"", "", force_update, true);
|
||||
}
|
||||
} else {
|
||||
BOOST_LOG_TRIVIAL(info) << "[Orca Updater]:cached settings for " << vendor_name
|
||||
<< " are not newer than installed version, installed " << vendor_ver.to_string()
|
||||
<< ", cached " << cache_ver.to_string();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return updates;
|
||||
}
|
||||
@@ -1317,6 +1414,9 @@ void PresetUpdater::sync(std::string http_url, std::string language, std::string
|
||||
// after the startup printer preset has been restored.
|
||||
this->p->sync_plugins(http_url, plugin_version);
|
||||
this->p->sync_printer_config(http_url);
|
||||
// Orca (PR #130): the filament library is always installed, so refresh it
|
||||
// from the updater on every startup sync rather than deferring to check_vendor_update().
|
||||
this->p->sync_vendor_config(PresetBundle::ORCA_FILAMENT_LIBRARY);
|
||||
//if (p->cancel)
|
||||
// return;
|
||||
//remove the tooltip currently
|
||||
@@ -1349,6 +1449,302 @@ void PresetUpdater::check_vendor_update(const std::string& vendor_id)
|
||||
});
|
||||
}
|
||||
|
||||
// Orca: ask the server which vendors from `system_vendors` have a profile bundle available that
|
||||
// isn't installed yet (or is newer than what's installed). Request body maps vendor id -> currently
|
||||
// installed profile version (unknown/not-yet-installed vendors report "0.0.0"). Response maps
|
||||
// vendor id -> {version, download_url, changelog} for each vendor the server has an update for.
|
||||
// Any such vendor is downloaded and cached under ota/profiles the same way sync_vendor_config()
|
||||
// does; the caller's check_config_updates_from_updater() -> get_config_updates()/perform_updates()
|
||||
// flow then installs the cached profiles into data_dir()/system.
|
||||
//
|
||||
// Mirrors check_vendor_update()/sync_vendor_config(): the network query and the download/extract
|
||||
// work run on a background thread (vendor_check_threads), never on the calling (UI) thread. Only
|
||||
// the confirmation dialog (which must run on the UI thread) and the final callback are marshaled
|
||||
// back via CallAfter().
|
||||
void PresetUpdater::priv::check_new_vendors(const std::set<std::string>& system_vendors,
|
||||
std::function<void(std::vector<std::string>, bool)> callback)
|
||||
{
|
||||
vendor_check_threads.emplace_back([this, system_vendors, callback]() {
|
||||
AppConfig* app_config = GUI::wxGetApp().app_config;
|
||||
std::string url = app_config->profile_update_url() + "/new?orcaslicer_version=" + Http::url_encode(SoftFever_VERSION);
|
||||
|
||||
auto check_cancel = [this](Http::Progress, bool& cancel_http) {
|
||||
if (cancel || vendor_check_cancel)
|
||||
cancel_http = true;
|
||||
};
|
||||
|
||||
json request_body = json::object();
|
||||
BOOST_LOG_TRIVIAL(info) << "[Orca Updater] checking new vendors for:";
|
||||
for (const auto& vendor_id : system_vendors) {
|
||||
// Orca: installed_vendor_version() reads whichever form the vendor is
|
||||
// installed as - the .json profile or the .opc preset cache stamp -
|
||||
// so a cache-only vendor is not reported as version 0.0.0 and then
|
||||
// endlessly re-offered by the server.
|
||||
Semver installed_ver = installed_vendor_version(vendor_id);
|
||||
request_body[vendor_id] = installed_ver.to_string();
|
||||
BOOST_LOG_TRIVIAL(info) << vendor_id << " (installed version " << installed_ver.to_string() << ")";
|
||||
}
|
||||
BOOST_LOG_TRIVIAL(info) << "[Orca Updater] new vendor check request url: " << url;
|
||||
BOOST_LOG_TRIVIAL(info) << "[Orca Updater] new vendor check request body: " << request_body.dump(2);
|
||||
|
||||
json response_json;
|
||||
bool got_response = false;
|
||||
|
||||
auto post = Http::post(url);
|
||||
|
||||
post.timeout_connect(5);
|
||||
post.on_progress(check_cancel);
|
||||
post.header("Content-Type", "application/json");
|
||||
post.on_error([](std::string body, std::string error, unsigned http_status) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "[Orca Updater] new vendor check HTTP error: " << error;
|
||||
})
|
||||
.on_complete([&response_json, &got_response](std::string body, unsigned http_status) {
|
||||
if (http_status != 200)
|
||||
return;
|
||||
try {
|
||||
json j = json::parse(body);
|
||||
if (j.is_object()) {
|
||||
response_json = std::move(j);
|
||||
got_response = true;
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "[Orca Updater] new vendor check JSON parse failed: " << e.what();
|
||||
}
|
||||
});
|
||||
|
||||
post.set_post_body(request_body.dump());
|
||||
post.perform_sync();
|
||||
|
||||
if (cancel || vendor_check_cancel)
|
||||
return;
|
||||
|
||||
if (!got_response) {
|
||||
GUI::wxGetApp().CallAfter([callback]() { callback({}, false); });
|
||||
return;
|
||||
}
|
||||
|
||||
// Collect candidates before touching the filesystem or network again.
|
||||
struct NewVendorCandidate
|
||||
{
|
||||
std::string vendor_id;
|
||||
Semver version;
|
||||
std::string download_url;
|
||||
std::string changelog;
|
||||
};
|
||||
std::vector<NewVendorCandidate> candidates;
|
||||
for (auto it = response_json.begin(); it != response_json.end(); ++it) {
|
||||
const json& entry = it.value();
|
||||
if (!entry.is_object())
|
||||
continue;
|
||||
std::string download_url_str = entry.value("download_url", std::string());
|
||||
if (download_url_str.empty())
|
||||
continue;
|
||||
|
||||
NewVendorCandidate candidate;
|
||||
candidate.vendor_id = it.key();
|
||||
auto parsed_ver = Semver::parse(entry.value("version", std::string()));
|
||||
candidate.version = parsed_ver ? *parsed_ver : Semver();
|
||||
candidate.download_url = std::move(download_url_str);
|
||||
candidate.changelog = entry.value("changelog", std::string());
|
||||
candidates.push_back(std::move(candidate));
|
||||
}
|
||||
|
||||
if (candidates.empty()) {
|
||||
GUI::wxGetApp().CallAfter([callback]() { callback({}, false); });
|
||||
return;
|
||||
}
|
||||
|
||||
// Orca: the confirmation dialog must run on the UI thread; if confirmed, the actual
|
||||
// download/install work is dispatched back onto a new background thread from there,
|
||||
// same as check_vendor_update() does for a single vendor.
|
||||
GUI::wxGetApp().CallAfter([this, candidates, callback]() {
|
||||
std::vector<GUI::MsgUpdateConfig::Update> updates_msg;
|
||||
for (const auto& candidate : candidates)
|
||||
updates_msg.emplace_back(candidate.vendor_id, candidate.version, std::string(), candidate.changelog);
|
||||
|
||||
GUI::MsgUpdateConfig dlg(updates_msg);
|
||||
if (dlg.ShowModal() != wxID_OK) {
|
||||
BOOST_LOG_TRIVIAL(info) << "[Orca Updater] user declined installing new vendors";
|
||||
callback({}, true);
|
||||
return;
|
||||
}
|
||||
|
||||
// Orca: the actual download runs on a background thread below (so it doesn't block the
|
||||
// UI), but that also means nothing visibly happens for the several seconds it can take
|
||||
// (longer still if a retry kicks in) — push a notification so it's clear work is
|
||||
// ongoing rather than looking hung.
|
||||
{
|
||||
std::string vendor_list;
|
||||
for (const auto& candidate : candidates) {
|
||||
if (!vendor_list.empty())
|
||||
vendor_list += ", ";
|
||||
vendor_list += candidate.vendor_id;
|
||||
}
|
||||
GUI::wxGetApp().plater()->get_notification_manager()->push_notification(
|
||||
_u8L("Downloading new vendor profile(s): ") + vendor_list + _u8L("..."));
|
||||
}
|
||||
|
||||
vendor_check_threads.emplace_back([this, candidates, callback]() {
|
||||
auto check_cancel = [this](Http::Progress, bool& cancel_http) {
|
||||
if (cancel || vendor_check_cancel)
|
||||
cancel_http = true;
|
||||
};
|
||||
|
||||
std::vector<std::string> new_vendor_ids;
|
||||
std::vector<std::string> failed_vendor_ids;
|
||||
auto cache_profile_path = cache_path / "profiles";
|
||||
fs::create_directories(cache_profile_path);
|
||||
boost::system::error_code ec;
|
||||
|
||||
for (const auto& candidate : candidates) {
|
||||
if (cancel || vendor_check_cancel)
|
||||
break;
|
||||
|
||||
const std::string& vendor_id = candidate.vendor_id;
|
||||
const std::string& download_url_str = candidate.download_url;
|
||||
std::string changelog = candidate.changelog;
|
||||
|
||||
BOOST_LOG_TRIVIAL(info) << "[Orca Updater] downloading new vendor " << vendor_id << " version " << candidate.version.to_string();
|
||||
|
||||
// Clear only this vendor's cached data, same as sync_vendor_config().
|
||||
fs::remove_all(cache_profile_path / vendor_id, ec);
|
||||
fs::remove(cache_profile_path / (vendor_id + ".json"), ec);
|
||||
|
||||
fs::path download_file = cache_path / (vendor_id + TMP_EXTENSION);
|
||||
bool download_ok = false;
|
||||
|
||||
// Orca: same retry pattern as Plater.cpp's project download — a single-shot
|
||||
// 5s connect timeout against GitHub's redirect chain is prone to transient
|
||||
// failures (DNS/connect hiccups) that succeed a moment later, so retry a few
|
||||
// times before giving up rather than failing the whole vendor on one blip.
|
||||
int retry_count = 0;
|
||||
const int max_retries = 3;
|
||||
bool keep_trying = true;
|
||||
while (keep_trying && retry_count < max_retries) {
|
||||
retry_count++;
|
||||
Http::get(download_url_str)
|
||||
.timeout_connect(5)
|
||||
.on_progress(check_cancel)
|
||||
.on_error([&vendor_id, &retry_count, max_retries](std::string body, std::string error, unsigned http_status) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "[Orca Updater] download failed for new vendor " << vendor_id
|
||||
<< " (attempt " << retry_count << "/" << max_retries << "): " << error;
|
||||
})
|
||||
.on_complete([&](std::string body, unsigned http_status) {
|
||||
if (http_status != 200)
|
||||
return;
|
||||
fs::fstream file(download_file, std::ios::out | std::ios::binary | std::ios::trunc);
|
||||
if (!file.good())
|
||||
return;
|
||||
file.write(body.c_str(), body.size());
|
||||
file.close();
|
||||
if (file.good())
|
||||
download_ok = true;
|
||||
})
|
||||
.perform_sync();
|
||||
|
||||
keep_trying = !download_ok && !(cancel || vendor_check_cancel);
|
||||
}
|
||||
|
||||
if (!download_ok || cancel || vendor_check_cancel) {
|
||||
if (!download_ok)
|
||||
failed_vendor_ids.push_back(vendor_id);
|
||||
continue;
|
||||
}
|
||||
|
||||
BOOST_LOG_TRIVIAL(info) << "[Orca Updater] extracting new vendor " << vendor_id;
|
||||
if (!extract_file(download_file, cache_profile_path)) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "[Orca Updater] extraction failed for new vendor " << vendor_id;
|
||||
fs::remove(download_file, ec);
|
||||
failed_vendor_ids.push_back(vendor_id);
|
||||
continue;
|
||||
}
|
||||
fs::remove(download_file, ec);
|
||||
|
||||
const fs::path cached_vendor_json = cache_profile_path / (vendor_id + ".json");
|
||||
const fs::path cached_vendor_folder = cache_profile_path / vendor_id;
|
||||
if (!fs::is_regular_file(cached_vendor_json) || !fs::is_directory(cached_vendor_folder) ||
|
||||
fs::is_empty(cached_vendor_folder)) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "[Orca Updater] rejected new vendor " << vendor_id << ": expected " << vendor_id
|
||||
<< ".json and a non-empty " << vendor_id << " directory";
|
||||
fs::remove_all(cached_vendor_folder, ec);
|
||||
fs::remove(cached_vendor_json, ec);
|
||||
failed_vendor_ids.push_back(vendor_id);
|
||||
continue;
|
||||
}
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(vendor_changelogs_mutex);
|
||||
vendor_changelogs[vendor_id] = std::move(changelog);
|
||||
}
|
||||
|
||||
new_vendor_ids.push_back(vendor_id);
|
||||
}
|
||||
|
||||
if (!new_vendor_ids.empty()) {
|
||||
// Orca: the user already confirmed via the dialog above, so install right away
|
||||
// instead of routing through check_config_updates_from_updater(), which only
|
||||
// queues a passive notification (meant for the silent background per-vendor
|
||||
// check) requiring yet another click + confirmation before anything is copied
|
||||
// into data_dir()/system.
|
||||
GUI::wxGetApp().CallAfter([this, new_vendor_ids] {
|
||||
AppConfig* app_config = GUI::wxGetApp().app_config;
|
||||
Updates updates = get_config_updates(app_config->orig_version());
|
||||
|
||||
// Only install the vendors just confirmed; leave any other unrelated
|
||||
// pending cached update (from a background sync_vendor_config()) alone,
|
||||
// still gated behind its own notification/confirmation.
|
||||
std::set<std::string> confirmed(new_vendor_ids.begin(), new_vendor_ids.end());
|
||||
Updates filtered;
|
||||
for (auto& update : updates.updates)
|
||||
if (confirmed.count(update.vendor))
|
||||
filtered.updates.push_back(std::move(update));
|
||||
|
||||
if (filtered.updates.empty()) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "[Orca Updater] new vendors cached but no updates detected";
|
||||
return;
|
||||
}
|
||||
|
||||
if (!perform_updates(std::move(filtered)) || !reload_configs_update_gui()) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "[Orca Updater] failed to install new vendors";
|
||||
return;
|
||||
}
|
||||
|
||||
BOOST_LOG_TRIVIAL(info) << "[Orca Updater] new vendors installed";
|
||||
for (const auto& vendor_id : new_vendor_ids) {
|
||||
Semver cur_ver = GUI::wxGetApp().preset_bundle->get_vendor_profile_version(vendor_id);
|
||||
GUI::wxGetApp().plater()->get_notification_manager()->push_notification(
|
||||
GUI::NotificationType::PresetUpdateFinished,
|
||||
GUI::NotificationManager::NotificationLevel::ImportantNotificationLevel,
|
||||
_u8L("Configuration package: ") + vendor_id + _u8L(" updated to ") + cur_ver.to_string());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!failed_vendor_ids.empty()) {
|
||||
GUI::wxGetApp().CallAfter([failed_vendor_ids] {
|
||||
std::string vendor_list;
|
||||
for (const auto& vendor_id : failed_vendor_ids) {
|
||||
if (!vendor_list.empty())
|
||||
vendor_list += ", ";
|
||||
vendor_list += vendor_id;
|
||||
}
|
||||
GUI::wxGetApp().plater()->get_notification_manager()->push_notification(
|
||||
_u8L("Failed to download vendor profile(s): ") + vendor_list);
|
||||
});
|
||||
}
|
||||
|
||||
GUI::wxGetApp().CallAfter([callback, new_vendor_ids]() { callback(new_vendor_ids, false); });
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void PresetUpdater::check_new_vendors(const std::set<std::string>& system_vendors,
|
||||
std::function<void(std::vector<std::string>, bool)> callback)
|
||||
{
|
||||
p->check_new_vendors(system_vendors, std::move(callback));
|
||||
}
|
||||
|
||||
void PresetUpdater::slic3r_update_notify()
|
||||
{
|
||||
if (! p->enabled_version_check)
|
||||
@@ -1370,10 +1766,9 @@ static bool reload_configs_update_gui()
|
||||
GUI::wxGetApp().load_current_presets();
|
||||
GUI::wxGetApp().plater()->set_bed_shape();
|
||||
|
||||
return true;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
PresetUpdater::UpdateResult PresetUpdater::config_update(const Semver& old_slic3r_version, UpdateParams params) const
|
||||
{
|
||||
if (! p->enabled_config_update) { return R_NOOP; }
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
#ifndef slic3r_PresetUpdate_hpp_
|
||||
#define slic3r_PresetUpdate_hpp_
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <set>
|
||||
#include <vector>
|
||||
|
||||
#include <wx/event.h>
|
||||
@@ -59,6 +61,12 @@ public:
|
||||
void on_update_notification_confirm();
|
||||
void do_printer_config_update();
|
||||
void check_vendor_update(const std::string& vendor_id);
|
||||
// Orca: async, mirrors check_vendor_update()/sync_vendor_config() — the network query and any
|
||||
// download/install work happen on a background thread; only the confirmation dialog runs on
|
||||
// the UI thread. `callback` is invoked on the UI thread with the ids of vendors that were
|
||||
// installed (empty if none were found, or the user declined) and whether the user declined.
|
||||
void check_new_vendors(const std::set<std::string>& system_vendors,
|
||||
std::function<void(std::vector<std::string> installed_vendors, bool declined)> callback);
|
||||
|
||||
bool version_check_enabled() const;
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
|
||||
#include "libslic3r/Model.hpp"
|
||||
#include "libslic3r/TriangleSelector.hpp"
|
||||
#include "libslic3r/Format/3mf.hpp"
|
||||
#include "libslic3r/Format/bbs_3mf.hpp"
|
||||
#include "libslic3r/Format/STL.hpp"
|
||||
@@ -9,9 +8,12 @@
|
||||
#include "libslic3r/Preset.hpp"
|
||||
#include "libslic3r/MultiNozzleUtils.hpp"
|
||||
#include "libslic3r/ProjectTask.hpp"
|
||||
#include "libslic3r/PublishSettings.hpp"
|
||||
|
||||
#include "test_utils.hpp"
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include <boost/filesystem/operations.hpp>
|
||||
|
||||
#include <catch2/catch_tostring.hpp>
|
||||
@@ -499,7 +501,6 @@ SCENARIO("Nozzle-group metadata .3mf round-trip", "[3mf][MultiNozzle]") {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// A mixed-color filament occupies an ordinary filament slot, and painting with it stores an
|
||||
// ordinary extruder state: a project saved by BambuStudio encodes filament 5 of a 5-slot setup
|
||||
// as paint state 5, with the mix described by the parallel filament_mixed_* project arrays.
|
||||
@@ -590,3 +591,639 @@ SCENARIO("Mixed-color filament setup and painting round-trip through a .3mf", "[
|
||||
}
|
||||
}
|
||||
}
|
||||
// Locks the serialization contract of the "Publish" metadata: the orca_published flag and the
|
||||
// orca_published_keys JSON array in model.model_info->metadata_items must survive a store_bbs_3mf ->
|
||||
// load_bbs_3mf round-trip unchanged. (The full preset-preservation behavior is exercised
|
||||
// headlessly in test_preset_bundle_loading.cpp.)
|
||||
SCENARIO("Published 3MF round-trips the published flag and published_keys metadata", "[3mf]") {
|
||||
GIVEN("a model carrying published metadata") {
|
||||
Model model;
|
||||
std::string src_file = std::string(TEST_DATA_DIR) + "/test_3mf/Prusa.stl";
|
||||
REQUIRE(load_stl(src_file.c_str(), &model));
|
||||
model.add_default_instances();
|
||||
|
||||
model.model_info = std::make_shared<ModelInfo>();
|
||||
model.model_info->metadata_items[ORCA_PUBLISHED_TAG] = "1";
|
||||
model.model_info->metadata_items[ORCA_PUBLISHED_KEYS_TAG] = R"(["layer_height","wall_thickness"])";
|
||||
|
||||
// store_bbs_3mf stages project_settings.config through the model's backup path; point
|
||||
// it at a writable temp dir (the default lives under a read-only root in CI).
|
||||
ScopedTemporaryDir backup_dir("orca_pub");
|
||||
model.set_backup_path(backup_dir.string());
|
||||
|
||||
WHEN("stored to and reloaded from a .3mf") {
|
||||
ScopedTemporaryFile temp(".3mf");
|
||||
const std::string test_file = temp.string();
|
||||
|
||||
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
|
||||
StoreParams store_params;
|
||||
store_params.path = test_file.c_str();
|
||||
store_params.model = &model;
|
||||
store_params.config = &config;
|
||||
store_params.strategy = SaveStrategy::Zip64 | SaveStrategy::Silence;
|
||||
REQUIRE(store_bbs_3mf(store_params));
|
||||
|
||||
Model dst_model;
|
||||
DynamicPrintConfig dst_config;
|
||||
ConfigSubstitutionContext ctxt{ ForwardCompatibilitySubstitutionRule::Enable };
|
||||
PlateDataPtrs dst_plates;
|
||||
std::vector<Preset*> project_presets;
|
||||
bool is_bbl_3mf = false, is_orca_3mf = false;
|
||||
Semver file_version;
|
||||
bool loaded = load_bbs_3mf(test_file.c_str(), &dst_config, &ctxt, &dst_model, &dst_plates,
|
||||
&project_presets, &is_bbl_3mf, &is_orca_3mf, &file_version, nullptr,
|
||||
LoadStrategy::LoadModel | LoadStrategy::LoadConfig);
|
||||
THEN("the published metadata round-trips unchanged") {
|
||||
REQUIRE(loaded);
|
||||
REQUIRE(dst_model.model_info != nullptr);
|
||||
REQUIRE(dst_model.model_info->metadata_items[ORCA_PUBLISHED_TAG] == "1");
|
||||
REQUIRE(dst_model.model_info->metadata_items[ORCA_PUBLISHED_KEYS_TAG] == R"(["layer_height","wall_thickness"])");
|
||||
|
||||
// The orca_published_keys value is a JSON array of setting keys; it must parse back to
|
||||
// the same keys that were selected.
|
||||
nlohmann::json keys = nlohmann::json::parse(dst_model.model_info->metadata_items[ORCA_PUBLISHED_KEYS_TAG]);
|
||||
REQUIRE(keys.is_array());
|
||||
REQUIRE(keys.size() == 2);
|
||||
REQUIRE(keys[0] == "layer_height");
|
||||
REQUIRE(keys[1] == "wall_thickness");
|
||||
}
|
||||
release_PlateData_list(dst_plates);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A normal 3MF (no Publish metadata) must load identically: the loader must not fabricate a
|
||||
// "orca_published" flag or orca_published_keys for files that never carried them.
|
||||
SCENARIO("Legacy 3MF without published metadata loads unchanged", "[3mf]") {
|
||||
GIVEN("a model without any published metadata") {
|
||||
Model model;
|
||||
std::string src_file = std::string(TEST_DATA_DIR) + "/test_3mf/Prusa.stl";
|
||||
REQUIRE(load_stl(src_file.c_str(), &model));
|
||||
model.add_default_instances();
|
||||
|
||||
ScopedTemporaryDir backup_dir("orca_legacy");
|
||||
model.set_backup_path(backup_dir.string());
|
||||
|
||||
WHEN("stored to and reloaded from a .3mf") {
|
||||
ScopedTemporaryFile temp(".3mf");
|
||||
const std::string test_file = temp.string();
|
||||
|
||||
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
|
||||
StoreParams store_params;
|
||||
store_params.path = test_file.c_str();
|
||||
store_params.model = &model;
|
||||
store_params.config = &config;
|
||||
store_params.strategy = SaveStrategy::Zip64 | SaveStrategy::Silence;
|
||||
REQUIRE(store_bbs_3mf(store_params));
|
||||
|
||||
Model dst_model;
|
||||
DynamicPrintConfig dst_config;
|
||||
ConfigSubstitutionContext ctxt{ ForwardCompatibilitySubstitutionRule::Enable };
|
||||
PlateDataPtrs dst_plates;
|
||||
std::vector<Preset*> project_presets;
|
||||
bool is_bbl_3mf = false, is_orca_3mf = false;
|
||||
Semver file_version;
|
||||
bool loaded = load_bbs_3mf(test_file.c_str(), &dst_config, &ctxt, &dst_model, &dst_plates,
|
||||
&project_presets, &is_bbl_3mf, &is_orca_3mf, &file_version, nullptr,
|
||||
LoadStrategy::LoadModel | LoadStrategy::LoadConfig);
|
||||
THEN("no published key is fabricated") {
|
||||
REQUIRE(loaded);
|
||||
REQUIRE(dst_model.model_info != nullptr);
|
||||
REQUIRE(dst_model.model_info->metadata_items.count(ORCA_PUBLISHED_TAG) == 0);
|
||||
REQUIRE(dst_model.model_info->metadata_items.count(ORCA_PUBLISHED_KEYS_TAG) == 0);
|
||||
}
|
||||
release_PlateData_list(dst_plates);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Locks the serialization contract of the orca_published_material_keys metadata: the per-entry JSON
|
||||
// must survive a store_bbs_3mf -> load_bbs_3mf round-trip verbatim, exactly like orca_published_keys.
|
||||
SCENARIO("Published 3MF round-trips the published_material_keys metadata", "[3mf]") {
|
||||
GIVEN("a model carrying published material keys metadata") {
|
||||
Model model;
|
||||
std::string src_file = std::string(TEST_DATA_DIR) + "/test_3mf/Prusa.stl";
|
||||
REQUIRE(load_stl(src_file.c_str(), &model));
|
||||
model.add_default_instances();
|
||||
|
||||
const std::string material_keys_json =
|
||||
R"([{"material":{"filament_type":"PLA","filament_vendor":"Generic","filament_id":"GFL99"},"slot":0,"keys":["filament_retraction_length","filament_z_hop"]}])";
|
||||
|
||||
model.model_info = std::make_shared<ModelInfo>();
|
||||
model.model_info->metadata_items[ORCA_PUBLISHED_MATERIAL_TAG] = material_keys_json;
|
||||
|
||||
ScopedTemporaryDir backup_dir("orca_pub_mat");
|
||||
model.set_backup_path(backup_dir.string());
|
||||
|
||||
WHEN("stored to and reloaded from a .3mf") {
|
||||
ScopedTemporaryFile temp(".3mf");
|
||||
const std::string test_file = temp.string();
|
||||
|
||||
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
|
||||
StoreParams store_params;
|
||||
store_params.path = test_file.c_str();
|
||||
store_params.model = &model;
|
||||
store_params.config = &config;
|
||||
store_params.strategy = SaveStrategy::Zip64 | SaveStrategy::Silence;
|
||||
REQUIRE(store_bbs_3mf(store_params));
|
||||
|
||||
Model dst_model;
|
||||
DynamicPrintConfig dst_config;
|
||||
ConfigSubstitutionContext ctxt{ ForwardCompatibilitySubstitutionRule::Enable };
|
||||
PlateDataPtrs dst_plates;
|
||||
std::vector<Preset*> project_presets;
|
||||
bool is_bbl_3mf = false, is_orca_3mf = false;
|
||||
Semver file_version;
|
||||
bool loaded = load_bbs_3mf(test_file.c_str(), &dst_config, &ctxt, &dst_model, &dst_plates,
|
||||
&project_presets, &is_bbl_3mf, &is_orca_3mf, &file_version, nullptr,
|
||||
LoadStrategy::LoadModel | LoadStrategy::LoadConfig);
|
||||
THEN("the published material keys metadata round-trips unchanged") {
|
||||
REQUIRE(loaded);
|
||||
REQUIRE(dst_model.model_info != nullptr);
|
||||
REQUIRE(dst_model.model_info->metadata_items[ORCA_PUBLISHED_MATERIAL_TAG] == material_keys_json);
|
||||
|
||||
// The value must parse back to one material entry carrying the nested identity
|
||||
// object, the author slot ordinal and the key list.
|
||||
nlohmann::json entries = nlohmann::json::parse(material_keys_json);
|
||||
REQUIRE(entries.is_array());
|
||||
REQUIRE(entries.size() == 1);
|
||||
REQUIRE(entries[0]["material"]["filament_type"] == "PLA");
|
||||
REQUIRE(entries[0]["material"]["filament_vendor"] == "Generic");
|
||||
REQUIRE(entries[0]["material"]["filament_id"] == "GFL99");
|
||||
REQUIRE(entries[0]["slot"] == 0);
|
||||
REQUIRE(entries[0]["keys"].is_array());
|
||||
REQUIRE(entries[0]["keys"].size() == 2);
|
||||
REQUIRE(entries[0]["keys"][0] == "filament_retraction_length");
|
||||
}
|
||||
release_PlateData_list(dst_plates);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SCENARIO("Minimal published 3MF omits project config, preset dumps and slicer tags", "[3mf]") {
|
||||
GIVEN("a multi-instance model carrying published metadata and a published_config payload") {
|
||||
Model model;
|
||||
std::string src_file = std::string(TEST_DATA_DIR) + "/test_3mf/Prusa.stl";
|
||||
REQUIRE(load_stl(src_file.c_str(), &model));
|
||||
model.add_default_instances();
|
||||
// A second instance: tag-less third-party files get their multi-instance objects split,
|
||||
// published files must not (the loader recognizes them by their metadata).
|
||||
model.objects.front()->add_instance();
|
||||
|
||||
DynamicPrintConfig full_cfg = DynamicPrintConfig::full_print_config();
|
||||
full_cfg.set_key_value("layer_height", new ConfigOptionFloat(0.24));
|
||||
full_cfg.set_key_value("retraction_length", new ConfigOptionFloats({ 1.2 }));
|
||||
|
||||
const std::vector<std::string> published_keys = { "layer_height", "retraction_length" };
|
||||
const std::vector<PublishedMaterialEntry> material_keys = {
|
||||
{ "PLA", "Generic", "GFL99", "", "Generic PLA", 0, { "filament_retraction_length" } }
|
||||
};
|
||||
|
||||
// The payload builder keeps the published and identity keys and drops everything else.
|
||||
DynamicPrintConfig filtered_cfg = filter_published_config(full_cfg, published_keys, material_keys);
|
||||
REQUIRE(filtered_cfg.option("layer_height") != nullptr);
|
||||
REQUIRE(filtered_cfg.option("retraction_length") != nullptr);
|
||||
REQUIRE(filtered_cfg.option("filament_colour") != nullptr);
|
||||
REQUIRE(filtered_cfg.option("filament_type") != nullptr);
|
||||
REQUIRE(filtered_cfg.option("wipe_tower_x") != nullptr);
|
||||
REQUIRE(filtered_cfg.option("sparse_infill_density") == nullptr);
|
||||
REQUIRE(filtered_cfg.option("machine_start_gcode") == nullptr);
|
||||
|
||||
// Serialize the payload exactly like export_published_3mf does.
|
||||
std::string payload;
|
||||
for (const std::string &key : filtered_cfg.keys())
|
||||
payload += key + " = " + filtered_cfg.opt_serialize(key) + "\n";
|
||||
|
||||
model.model_info = std::make_shared<ModelInfo>();
|
||||
model.model_info->metadata_items[ORCA_PUBLISHED_TAG] = "1";
|
||||
model.model_info->metadata_items[ORCA_PUBLISHED_KEYS_TAG] = R"(["layer_height","retraction_length"])";
|
||||
model.model_info->metadata_items[ORCA_PUBLISHED_CONFIG_TAG] = payload;
|
||||
|
||||
ScopedTemporaryDir backup_dir("orca_min_pub");
|
||||
model.set_backup_path(backup_dir.string());
|
||||
|
||||
WHEN("stored using SaveStrategy::MinimalPublished and reloaded") {
|
||||
ScopedTemporaryFile temp(".3mf");
|
||||
const std::string test_file = temp.string();
|
||||
|
||||
// Create a fake project preset to verify MinimalPublished omits it.
|
||||
Preset preset(Preset::TYPE_PRINT, "TestPrintPreset");
|
||||
preset.config = full_cfg;
|
||||
std::vector<Preset*> project_presets = { &preset };
|
||||
|
||||
StoreParams store_params;
|
||||
store_params.path = test_file.c_str();
|
||||
store_params.model = &model;
|
||||
store_params.config = &filtered_cfg;
|
||||
store_params.project_presets = project_presets;
|
||||
store_params.strategy = SaveStrategy::Zip64 | SaveStrategy::Silence | SaveStrategy::MinimalPublished;
|
||||
REQUIRE(store_bbs_3mf(store_params));
|
||||
|
||||
Model dst_model;
|
||||
ScopedTemporaryDir loaded_backup_dir("orca_min_pub_loaded");
|
||||
dst_model.set_backup_path(loaded_backup_dir.string());
|
||||
DynamicPrintConfig dst_config;
|
||||
ConfigSubstitutionContext ctxt{ ForwardCompatibilitySubstitutionRule::Enable };
|
||||
PlateDataPtrs dst_plates;
|
||||
std::vector<Preset*> loaded_presets;
|
||||
bool is_bbl_3mf = false, is_orca_3mf = false;
|
||||
Semver file_version;
|
||||
bool loaded = load_bbs_3mf(test_file.c_str(), &dst_config, &ctxt, &dst_model, &dst_plates,
|
||||
&loaded_presets, &is_bbl_3mf, &is_orca_3mf, &file_version, nullptr,
|
||||
LoadStrategy::LoadModel | LoadStrategy::LoadConfig);
|
||||
THEN("the 3MF loads without project config or embedded presets") {
|
||||
REQUIRE(loaded);
|
||||
REQUIRE(dst_config.empty());
|
||||
REQUIRE(loaded_presets.empty());
|
||||
}
|
||||
THEN("the file carries no slicer tags and classifies as a generic 3MF") {
|
||||
REQUIRE_FALSE(is_bbl_3mf);
|
||||
REQUIRE_FALSE(is_orca_3mf);
|
||||
// No Application / OrcaSlicer tag: old receivers import the geometry silently
|
||||
// instead of showing a baked-in, wrong "old version" popup.
|
||||
REQUIRE_FALSE(file_version.valid());
|
||||
}
|
||||
THEN("the geometry keeps BBS-grade handling: instances are not split") {
|
||||
REQUIRE(dst_model.objects.size() == 1);
|
||||
REQUIRE(dst_model.objects.front()->instances.size() == 2);
|
||||
}
|
||||
THEN("the published metadata and payload round-trip unchanged") {
|
||||
REQUIRE(dst_model.model_info != nullptr);
|
||||
REQUIRE(dst_model.model_info->metadata_items[ORCA_PUBLISHED_TAG] == "1");
|
||||
REQUIRE(dst_model.model_info->metadata_items[ORCA_PUBLISHED_KEYS_TAG] == R"(["layer_height","retraction_length"])");
|
||||
REQUIRE(dst_model.model_info->metadata_items[ORCA_PUBLISHED_CONFIG_TAG] == payload);
|
||||
}
|
||||
THEN("the payload parses back to the published values") {
|
||||
DynamicPrintConfig parsed_payload;
|
||||
parsed_payload.load_from_ini_string(dst_model.model_info->metadata_items[ORCA_PUBLISHED_CONFIG_TAG], ForwardCompatibilitySubstitutionRule::Enable);
|
||||
REQUIRE(parsed_payload.option("layer_height") != nullptr);
|
||||
REQUIRE_THAT(parsed_payload.opt_float("layer_height"), Catch::Matchers::WithinAbs(0.24, 1e-6));
|
||||
REQUIRE(parsed_payload.option("retraction_length") != nullptr);
|
||||
REQUIRE_THAT(parsed_payload.opt<ConfigOptionFloats>("retraction_length")->get_at(0), Catch::Matchers::WithinAbs(1.2, 1e-6));
|
||||
}
|
||||
release_PlateData_list(dst_plates);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A minimal published 3MF must not leak the slicer tags of the source project. The exporter seeds
|
||||
// metadata_item_map from the input file's metadata_items, so re-publishing a project opened from a
|
||||
// regular Orca/BBS 3MF (the typical remix flow) must strip the Application / OrcaSlicer tags it
|
||||
// came with, otherwise old receivers route onto the baked-in "old version" popup.
|
||||
SCENARIO("MinimalPublished strips slicer tags carried by the source project", "[3mf]") {
|
||||
GIVEN("a model loaded from a regular Orca/BBS 3MF whose metadata carries the slicer tags") {
|
||||
Model model;
|
||||
std::string src_file = std::string(TEST_DATA_DIR) + "/test_3mf/Prusa.stl";
|
||||
REQUIRE(load_stl(src_file.c_str(), &model));
|
||||
model.add_default_instances();
|
||||
|
||||
model.model_info = std::make_shared<ModelInfo>();
|
||||
model.model_info->metadata_items[ORCA_PUBLISHED_TAG] = "1";
|
||||
model.model_info->metadata_items["Application"] = "BambuStudio-2.0.0";
|
||||
model.model_info->metadata_items["OrcaSlicer"] = "2.1.0";
|
||||
|
||||
ScopedTemporaryDir backup_dir("orca_strip_tags");
|
||||
model.set_backup_path(backup_dir.string());
|
||||
|
||||
WHEN("stored using SaveStrategy::MinimalPublished and reloaded") {
|
||||
ScopedTemporaryFile temp(".3mf");
|
||||
const std::string test_file = temp.string();
|
||||
|
||||
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
|
||||
StoreParams store_params;
|
||||
store_params.path = test_file.c_str();
|
||||
store_params.model = &model;
|
||||
store_params.config = &config;
|
||||
store_params.strategy = SaveStrategy::Zip64 | SaveStrategy::Silence | SaveStrategy::MinimalPublished;
|
||||
REQUIRE(store_bbs_3mf(store_params));
|
||||
|
||||
Model dst_model;
|
||||
DynamicPrintConfig dst_config;
|
||||
ConfigSubstitutionContext ctxt{ ForwardCompatibilitySubstitutionRule::Enable };
|
||||
PlateDataPtrs dst_plates;
|
||||
std::vector<Preset*> loaded_presets;
|
||||
bool is_bbl_3mf = false, is_orca_3mf = false;
|
||||
Semver file_version;
|
||||
bool loaded = load_bbs_3mf(test_file.c_str(), &dst_config, &ctxt, &dst_model, &dst_plates,
|
||||
&loaded_presets, &is_bbl_3mf, &is_orca_3mf, &file_version, nullptr,
|
||||
LoadStrategy::LoadModel | LoadStrategy::LoadConfig);
|
||||
THEN("the source slicer tags are stripped, not carried through") {
|
||||
REQUIRE(loaded);
|
||||
REQUIRE(dst_model.model_info != nullptr);
|
||||
REQUIRE(dst_model.model_info->metadata_items.count("Application") == 0);
|
||||
REQUIRE(dst_model.model_info->metadata_items.count("OrcaSlicer") == 0);
|
||||
// The published marker itself must survive.
|
||||
REQUIRE(dst_model.model_info->metadata_items[ORCA_PUBLISHED_TAG] == "1");
|
||||
}
|
||||
THEN("the file classifies as a generic 3MF without a version popup") {
|
||||
REQUIRE_FALSE(is_bbl_3mf);
|
||||
REQUIRE_FALSE(is_orca_3mf);
|
||||
REQUIRE_FALSE(file_version.valid());
|
||||
}
|
||||
release_PlateData_list(dst_plates);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// An entry masks the non-published slots to their defaults so publishing slot 1 never leaks slot
|
||||
// 0's value into the file. Both a full entry (the whole-slot key list) and a partial entry (a
|
||||
// per-slot key) go through the same masking path in filter_published_config (keys and full_keys
|
||||
// are filtered identically), so the two forms are exercised together.
|
||||
SCENARIO("Published entries mask the other slots to their defaults", "[3mf]") {
|
||||
const bool full = GENERATE(true, false);
|
||||
GIVEN("a full print configuration with two filament slots") {
|
||||
DynamicPrintConfig full_cfg = DynamicPrintConfig::full_print_config();
|
||||
full_cfg.opt<ConfigOptionFloats>("filament_diameter")->values = { 1.75, 1.75 };
|
||||
full_cfg.opt<ConfigOptionStrings>("filament_colour")->values = { "#111111", "#222222" };
|
||||
// filament_flow_ratio carries a non-empty option default (1.0) of the same type, so the
|
||||
// mask can restore it on the non-published slot.
|
||||
full_cfg.opt<ConfigOptionFloatsNullable>("filament_flow_ratio", true)->values = { 1.02, 0.98 };
|
||||
|
||||
WHEN("filtering with a published entry for slot 1") {
|
||||
PublishedMaterialEntry entry;
|
||||
entry.slot = 1;
|
||||
if (full) {
|
||||
entry.full = true;
|
||||
entry.full_keys = { "filament_flow_ratio" };
|
||||
} else {
|
||||
entry.keys = { "filament_flow_ratio" };
|
||||
}
|
||||
DynamicPrintConfig filtered_cfg = filter_published_config(full_cfg, {}, { entry });
|
||||
|
||||
THEN("the selected key is present with the author's slot value") {
|
||||
REQUIRE(filtered_cfg.option("filament_flow_ratio") != nullptr);
|
||||
REQUIRE_THAT(filtered_cfg.opt<ConfigOptionFloatsNullable>("filament_flow_ratio")->values[1], Catch::Matchers::WithinAbs(0.98, 1e-6));
|
||||
}
|
||||
THEN("the non-published slot is masked to its default") {
|
||||
REQUIRE_THAT(filtered_cfg.opt<ConfigOptionFloatsNullable>("filament_flow_ratio")->values[0], Catch::Matchers::WithinAbs(1.0, 1e-6));
|
||||
}
|
||||
THEN("the identity keys stay present") {
|
||||
REQUIRE(filtered_cfg.option("filament_colour") != nullptr);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A key needing slot masking that cannot be masked (no registered option default of the same
|
||||
// type) is dropped from the payload entirely instead of shipping the author's whole vector.
|
||||
SCENARIO("Unmaskable keys are dropped from the published payload instead of leaking", "[3mf]") {
|
||||
GIVEN("a config carrying a synthetic def-less vector key and a maskable one") {
|
||||
DynamicPrintConfig full_cfg = DynamicPrintConfig::full_print_config();
|
||||
full_cfg.opt<ConfigOptionFloats>("filament_diameter")->values = { 1.75, 1.75 };
|
||||
full_cfg.opt<ConfigOptionStrings>("filament_colour")->values = { "#111111", "#222222" };
|
||||
// Not a PrintConfig key: print_config_def has no default to mask with.
|
||||
full_cfg.set_key_value("orca_synthetic_setting", new ConfigOptionFloats({ 9.9, 8.8 }));
|
||||
full_cfg.opt<ConfigOptionFloatsNullable>("filament_flow_ratio", true)->values = { 1.02, 0.98 };
|
||||
|
||||
PublishedMaterialEntry partial_entry;
|
||||
partial_entry.slot = 1;
|
||||
partial_entry.keys = { "orca_synthetic_setting", "filament_flow_ratio" };
|
||||
|
||||
WHEN("filtering with a partial entry for slot 1") {
|
||||
DynamicPrintConfig filtered_cfg = filter_published_config(full_cfg, {}, { partial_entry });
|
||||
|
||||
THEN("the unmaskable synthetic key is not published") {
|
||||
REQUIRE(filtered_cfg.option("orca_synthetic_setting") == nullptr);
|
||||
}
|
||||
THEN("the maskable key is present, author slot kept, other slot masked") {
|
||||
REQUIRE(filtered_cfg.opt<ConfigOptionFloatsNullable>("filament_flow_ratio") != nullptr);
|
||||
REQUIRE_THAT(filtered_cfg.opt<ConfigOptionFloatsNullable>("filament_flow_ratio")->values[1], Catch::Matchers::WithinAbs(0.98, 1e-6));
|
||||
REQUIRE_THAT(filtered_cfg.opt<ConfigOptionFloatsNullable>("filament_flow_ratio")->values[0], Catch::Matchers::WithinAbs(1.0, 1e-6));
|
||||
}
|
||||
THEN("the identity keys stay present") {
|
||||
REQUIRE(filtered_cfg.option("filament_colour") != nullptr);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A per-extruder printer key carrying a "#N" variant (e.g. retraction_length#1) must not serialize
|
||||
// every extruder's value: the base is masked to the author's extruder and the other slots are
|
||||
// restored to their option default, matching the material-side slot-masking invariant. A bare
|
||||
// printer base key (no variant) keeps whole-vector serialization.
|
||||
SCENARIO("Published per-extruder printer keys mask the other extruders to their defaults", "[3mf]") {
|
||||
GIVEN("a full print configuration with three extruders carrying per-extruder retraction values") {
|
||||
DynamicPrintConfig full_cfg = DynamicPrintConfig::full_print_config();
|
||||
// Non-default values on the un-selected slots, so a leak is distinguishable from the mask
|
||||
// restoring the option default (retraction_length defaults to {0.8}).
|
||||
full_cfg.opt<ConfigOptionFloats>("retraction_length")->values = { 3.0, 1.2, 4.0 };
|
||||
|
||||
WHEN("filtering with only extruder 1's retraction_length checked") {
|
||||
DynamicPrintConfig filtered_cfg = filter_published_config(full_cfg, { "retraction_length#1" }, {});
|
||||
|
||||
THEN("the author's extruder value survives") {
|
||||
REQUIRE_THAT(filtered_cfg.opt<ConfigOptionFloats>("retraction_length")->values[1], Catch::Matchers::WithinAbs(1.2, 1e-6));
|
||||
}
|
||||
THEN("the other extruders are masked to their default") {
|
||||
REQUIRE_THAT(filtered_cfg.opt<ConfigOptionFloats>("retraction_length")->values[0], Catch::Matchers::WithinAbs(0.8, 1e-6));
|
||||
REQUIRE_THAT(filtered_cfg.opt<ConfigOptionFloats>("retraction_length")->values[2], Catch::Matchers::WithinAbs(0.8, 1e-6));
|
||||
}
|
||||
}
|
||||
WHEN("filtering the bare base key without a '#N' variant") {
|
||||
DynamicPrintConfig filtered_cfg = filter_published_config(full_cfg, { "retraction_length" }, {});
|
||||
|
||||
THEN("the whole vector is serialized unmasked") {
|
||||
REQUIRE_THAT(filtered_cfg.opt<ConfigOptionFloats>("retraction_length")->values[0], Catch::Matchers::WithinAbs(3.0, 1e-6));
|
||||
REQUIRE_THAT(filtered_cfg.opt<ConfigOptionFloats>("retraction_length")->values[1], Catch::Matchers::WithinAbs(1.2, 1e-6));
|
||||
REQUIRE_THAT(filtered_cfg.opt<ConfigOptionFloats>("retraction_length")->values[2], Catch::Matchers::WithinAbs(4.0, 1e-6));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The extended per-entry fields (full dump list, published type and colour) travel inside the
|
||||
// published_material_keys metadata and round-trip unchanged.
|
||||
SCENARIO("Published 3MF round-trips the extended material metadata", "[3mf]") {
|
||||
GIVEN("a model carrying extended published material keys metadata") {
|
||||
Model model;
|
||||
std::string src_file = std::string(TEST_DATA_DIR) + "/test_3mf/Prusa.stl";
|
||||
REQUIRE(load_stl(src_file.c_str(), &model));
|
||||
model.add_default_instances();
|
||||
|
||||
const std::string material_keys_json =
|
||||
R"([{"material":{"filament_type":"PLA","filament_vendor":"Generic","filament_id":"GFL99","setting_id":"RFs9eCKYOMUSmvZf","name":"Generic PLA Matte @System"},"slot":1,"keys":[],"full":true,"full_keys":["filament_retraction_length","filament_colour"],"publish_type":true,"type":"PLA","publish_color":false,"color":""}])";
|
||||
|
||||
model.model_info = std::make_shared<ModelInfo>();
|
||||
model.model_info->metadata_items[ORCA_PUBLISHED_MATERIAL_TAG] = material_keys_json;
|
||||
|
||||
ScopedTemporaryDir backup_dir("orca_pub_mat2");
|
||||
model.set_backup_path(backup_dir.string());
|
||||
|
||||
WHEN("stored to and reloaded from a .3mf") {
|
||||
ScopedTemporaryFile temp(".3mf");
|
||||
const std::string test_file = temp.string();
|
||||
|
||||
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
|
||||
StoreParams store_params;
|
||||
store_params.path = test_file.c_str();
|
||||
store_params.model = &model;
|
||||
store_params.config = &config;
|
||||
store_params.strategy = SaveStrategy::Zip64 | SaveStrategy::Silence;
|
||||
REQUIRE(store_bbs_3mf(store_params));
|
||||
|
||||
Model dst_model;
|
||||
DynamicPrintConfig dst_config;
|
||||
ConfigSubstitutionContext ctxt{ ForwardCompatibilitySubstitutionRule::Enable };
|
||||
PlateDataPtrs dst_plates;
|
||||
std::vector<Preset*> project_presets;
|
||||
bool is_bbl_3mf = false, is_orca_3mf = false;
|
||||
Semver file_version;
|
||||
bool loaded = load_bbs_3mf(test_file.c_str(), &dst_config, &ctxt, &dst_model, &dst_plates,
|
||||
&project_presets, &is_bbl_3mf, &is_orca_3mf, &file_version, nullptr,
|
||||
LoadStrategy::LoadModel | LoadStrategy::LoadConfig);
|
||||
THEN("the extended material metadata round-trips unchanged") {
|
||||
REQUIRE(loaded);
|
||||
REQUIRE(dst_model.model_info != nullptr);
|
||||
REQUIRE(dst_model.model_info->metadata_items[ORCA_PUBLISHED_MATERIAL_TAG] == material_keys_json);
|
||||
|
||||
// The value must parse back with every extended field intact.
|
||||
nlohmann::json entries = nlohmann::json::parse(material_keys_json);
|
||||
REQUIRE(entries.is_array());
|
||||
REQUIRE(entries.size() == 1);
|
||||
REQUIRE(entries[0]["full"].get<bool>() == true);
|
||||
REQUIRE(entries[0]["full_keys"].is_array());
|
||||
REQUIRE(entries[0]["full_keys"].size() == 2);
|
||||
REQUIRE(entries[0]["publish_type"].get<bool>() == true);
|
||||
REQUIRE(entries[0]["type"] == "PLA");
|
||||
REQUIRE(entries[0]["publish_color"].get<bool>() == false);
|
||||
}
|
||||
release_PlateData_list(dst_plates);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A published mixed filament serializes its whole definition (components, ratios, gradient)
|
||||
// masked to the author's slot: the mix slot's values survive, the non-published slots reset to
|
||||
// their defaults, so a partial publish never leaks another slot's mix data.
|
||||
SCENARIO("Published mixed-filament keys are masked to the author's slot", "[3mf]") {
|
||||
GIVEN("a full print configuration with three slots, one of them mixed") {
|
||||
DynamicPrintConfig full_cfg = DynamicPrintConfig::full_print_config();
|
||||
full_cfg.opt<ConfigOptionFloats>("filament_diameter")->values = { 1.75, 1.75, 1.75 };
|
||||
full_cfg.opt<ConfigOptionStrings>("filament_colour")->values = { "#111111", "#222222", "#333333" };
|
||||
full_cfg.opt<ConfigOptionBools>("filament_is_mixed")->values = { 0, 0, 1 };
|
||||
full_cfg.opt<ConfigOptionStrings>("filament_mixed_components")->values = { "", "", "1,2" };
|
||||
full_cfg.opt<ConfigOptionStrings>("filament_mixed_sublayer_ratios")->values = { "", "", "0.6,0.4" };
|
||||
full_cfg.opt<ConfigOptionBools>("filament_mixed_gradient")->values = { 0, 0, 1 };
|
||||
full_cfg.opt<ConfigOptionStrings>("filament_mixed_gradient_range")->values = { "", "", "0.9,0.1" };
|
||||
full_cfg.opt<ConfigOptionStrings>("filament_mixed_gradient_curve")->values = { "", "", "0,0.1|1,0.9" };
|
||||
full_cfg.opt<ConfigOptionBools>("filament_mixed_gradient_per_part")->values = { 0, 0, 1 };
|
||||
|
||||
PublishedMaterialEntry mix_entry;
|
||||
mix_entry.slot = 2;
|
||||
mix_entry.keys = {
|
||||
"filament_is_mixed", "filament_mixed_components", "filament_mixed_sublayer_ratios",
|
||||
"filament_mixed_gradient", "filament_mixed_gradient_range", "filament_mixed_gradient_curve",
|
||||
"filament_mixed_gradient_per_part"
|
||||
};
|
||||
|
||||
WHEN("filtering with a mixed entry for slot 2") {
|
||||
DynamicPrintConfig filtered_cfg = filter_published_config(full_cfg, {}, { mix_entry });
|
||||
|
||||
THEN("the author's mixed slot keeps its definition") {
|
||||
REQUIRE(filtered_cfg.option("filament_is_mixed") != nullptr);
|
||||
REQUIRE(filtered_cfg.opt<ConfigOptionBools>("filament_is_mixed")->values == std::vector<unsigned char>{ 0, 0, 1 });
|
||||
const auto& components = filtered_cfg.opt<ConfigOptionStrings>("filament_mixed_components")->values;
|
||||
REQUIRE(components.size() == 3);
|
||||
CHECK(components[2] == "1,2");
|
||||
CHECK(filtered_cfg.opt<ConfigOptionStrings>("filament_mixed_sublayer_ratios")->values[2] == "0.6,0.4");
|
||||
CHECK(filtered_cfg.opt<ConfigOptionStrings>("filament_mixed_gradient_curve")->values[2] == "0,0.1|1,0.9");
|
||||
CHECK(filtered_cfg.opt<ConfigOptionBools>("filament_mixed_gradient")->values[2]);
|
||||
CHECK(filtered_cfg.opt<ConfigOptionBools>("filament_mixed_gradient_per_part")->values[2]);
|
||||
}
|
||||
THEN("the non-published slots are masked to their defaults") {
|
||||
CHECK(filtered_cfg.opt<ConfigOptionStrings>("filament_mixed_components")->values[0] == "");
|
||||
CHECK(filtered_cfg.opt<ConfigOptionStrings>("filament_mixed_components")->values[1] == "");
|
||||
CHECK(filtered_cfg.opt<ConfigOptionBools>("filament_is_mixed")->values[0] == 0);
|
||||
CHECK(filtered_cfg.opt<ConfigOptionBools>("filament_is_mixed")->values[1] == 0);
|
||||
}
|
||||
THEN("the identity keys stay present") {
|
||||
REQUIRE(filtered_cfg.option("filament_colour") != nullptr);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The published flag is gated on the exact string "1": any other serialized value means "not
|
||||
// published", so a receiver never treats a file as published on a loose truthiness check.
|
||||
TEST_CASE("is_published_3mf_flag accepts only the literal \"1\"", "[3mf]") {
|
||||
CHECK(is_published_3mf_flag("1"));
|
||||
CHECK_FALSE(is_published_3mf_flag("0"));
|
||||
CHECK_FALSE(is_published_3mf_flag("false"));
|
||||
CHECK_FALSE(is_published_3mf_flag("true"));
|
||||
CHECK_FALSE(is_published_3mf_flag(""));
|
||||
CHECK_FALSE(is_published_3mf_flag("YES"));
|
||||
}
|
||||
|
||||
// bbs_3mf_is_published is the lightweight metadata probe used to decide whether a file was
|
||||
// produced by the publish feature (GUI "recently published" tracking). It must return true only
|
||||
// for a file whose metadata carries the flag set to "1", and false for legacy files and for a
|
||||
// file whose flag is present but not "1" (which loads as a normal, non-published 3MF).
|
||||
SCENARIO("bbs_3mf_is_published detects only genuinely published 3MFs", "[3mf]") {
|
||||
auto store_model = [](const std::string &path, const std::string &flag_value, const std::string &keys_value) {
|
||||
Model model;
|
||||
std::string src_file = std::string(TEST_DATA_DIR) + "/test_3mf/Prusa.stl";
|
||||
REQUIRE(load_stl(src_file.c_str(), &model));
|
||||
model.add_default_instances();
|
||||
model.model_info = std::make_shared<ModelInfo>();
|
||||
// An empty flag_value means "don't write the flag at all" (a legacy file).
|
||||
if (!flag_value.empty())
|
||||
model.model_info->metadata_items[ORCA_PUBLISHED_TAG] = flag_value;
|
||||
model.model_info->metadata_items[ORCA_PUBLISHED_KEYS_TAG] = keys_value;
|
||||
ScopedTemporaryDir backup_dir("orca_is_pub");
|
||||
model.set_backup_path(backup_dir.string());
|
||||
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
|
||||
StoreParams store_params;
|
||||
store_params.path = path.c_str();
|
||||
store_params.model = &model;
|
||||
store_params.config = &config;
|
||||
store_params.strategy = SaveStrategy::Zip64 | SaveStrategy::Silence;
|
||||
REQUIRE(store_bbs_3mf(store_params));
|
||||
};
|
||||
|
||||
GIVEN("a minimal published 3MF whose flag is \"1\"") {
|
||||
ScopedTemporaryFile temp(".3mf");
|
||||
store_model(temp.string(), "1", R"(["layer_height"])");
|
||||
WHEN("probed by bbs_3mf_is_published") {
|
||||
THEN("it is recognized as published") {
|
||||
CHECK(bbs_3mf_is_published(temp.string()));
|
||||
}
|
||||
}
|
||||
}
|
||||
GIVEN("a legacy 3MF without any published flag") {
|
||||
ScopedTemporaryFile temp(".3mf");
|
||||
store_model(temp.string(), "", R"(["layer_height"])");
|
||||
WHEN("probed by bbs_3mf_is_published") {
|
||||
THEN("it is not recognized as published") {
|
||||
CHECK_FALSE(bbs_3mf_is_published(temp.string()));
|
||||
}
|
||||
}
|
||||
}
|
||||
GIVEN("a 3MF carrying the flag set to \"0\"") {
|
||||
ScopedTemporaryFile temp(".3mf");
|
||||
store_model(temp.string(), "0", R"(["layer_height"])");
|
||||
WHEN("probed and loaded") {
|
||||
THEN("it is not recognized as published") {
|
||||
CHECK_FALSE(bbs_3mf_is_published(temp.string()));
|
||||
}
|
||||
THEN("it loads as a normal, non-published 3MF") {
|
||||
Model dst_model;
|
||||
DynamicPrintConfig dst_config;
|
||||
ConfigSubstitutionContext ctxt{ ForwardCompatibilitySubstitutionRule::Enable };
|
||||
PlateDataPtrs dst_plates;
|
||||
std::vector<Preset*> project_presets;
|
||||
bool is_bbl_3mf = false, is_orca_3mf = false;
|
||||
Semver file_version;
|
||||
REQUIRE(load_bbs_3mf(temp.string().c_str(), &dst_config, &ctxt, &dst_model, &dst_plates,
|
||||
&project_presets, &is_bbl_3mf, &is_orca_3mf, &file_version, nullptr,
|
||||
LoadStrategy::LoadModel | LoadStrategy::LoadConfig));
|
||||
REQUIRE(dst_model.model_info != nullptr);
|
||||
// The key is present but not "1", so nothing treats the file as published; the
|
||||
// stored keys still round-trip verbatim.
|
||||
REQUIRE(dst_model.model_info->metadata_items[ORCA_PUBLISHED_TAG] == "0");
|
||||
REQUIRE(dst_model.model_info->metadata_items[ORCA_PUBLISHED_KEYS_TAG] == R"(["layer_height"])");
|
||||
release_PlateData_list(dst_plates);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user