Bug fixes and test cases for import filament of published 3MF. Update translations

This commit is contained in:
Lam Wei Lun
2026-08-19 15:47:28 +08:00
parent 36a8811cc0
commit 1b770e8638
36 changed files with 1148 additions and 1003 deletions
+5 -2
View File
@@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-08-18 16:25+0800\n" "POT-Creation-Date: 2026-08-19 14:59+0800\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@@ -5945,7 +5945,7 @@ msgstr ""
msgid "Save current project as" msgid "Save current project as"
msgstr "" msgstr ""
msgid "Publish" msgid "Publish 3MF"
msgstr "" msgstr ""
msgid "Export a 3MF file with the selected settings embedded" msgid "Export a 3MF file with the selected settings embedded"
@@ -8043,6 +8043,9 @@ msgid ""
"Please check whether the folder exists online or if other programs have the file open." "Please check whether the folder exists online or if other programs have the file open."
msgstr "" msgstr ""
msgid "Publish"
msgstr ""
msgid "The nozzle type is not set. Please set the nozzle and try again." msgid "The nozzle type is not set. Please set the nozzle and try again."
msgstr "" msgstr ""
+6 -3
View File
@@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: \n" "Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-08-18 16:25+0800\n" "POT-Creation-Date: 2026-08-19 14:59+0800\n"
"PO-Revision-Date: 2025-03-15 10:55+0100\n" "PO-Revision-Date: 2025-03-15 10:55+0100\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: \n" "Language-Team: \n"
@@ -6406,8 +6406,8 @@ msgstr "Desa el projecte com a"
msgid "Save current project as" msgid "Save current project as"
msgstr "Desar el projecte actual com" msgstr "Desar el projecte actual com"
msgid "Publish" msgid "Publish 3MF"
msgstr "Publicar" msgstr ""
msgid "Export a 3MF file with the selected settings embedded" msgid "Export a 3MF file with the selected settings embedded"
msgstr "" msgstr ""
@@ -8632,6 +8632,9 @@ msgid ""
"Please check whether the folder exists online or if other programs have the file open." "Please check whether the folder exists online or if other programs have the file open."
msgstr "" msgstr ""
msgid "Publish"
msgstr "Publicar"
msgid "The nozzle type is not set. Please set the nozzle and try again." 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." msgstr "El tipus de broquet no està establert. Establiu el broquet i torneu-ho a provar."
+6 -3
View File
@@ -3,7 +3,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: Orca Slicer\n" "Project-Id-Version: Orca Slicer\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-08-18 16:25+0800\n" "POT-Creation-Date: 2026-08-19 14:59+0800\n"
"PO-Revision-Date: \n" "PO-Revision-Date: \n"
"Last-Translator: Jakub Hencl\n" "Last-Translator: Jakub Hencl\n"
"Language-Team: \n" "Language-Team: \n"
@@ -6367,8 +6367,8 @@ msgstr "Uložit projekt jako"
msgid "Save current project as" msgid "Save current project as"
msgstr "Uložit aktuální projekt jako" msgstr "Uložit aktuální projekt jako"
msgid "Publish" msgid "Publish 3MF"
msgstr "Publikovat" msgstr ""
msgid "Export a 3MF file with the selected settings embedded" msgid "Export a 3MF file with the selected settings embedded"
msgstr "" msgstr ""
@@ -8592,6 +8592,9 @@ msgid ""
"Please check whether the folder exists online or if other programs have the file open." "Please check whether the folder exists online or if other programs have the file open."
msgstr "" msgstr ""
msgid "Publish"
msgstr "Publikovat"
msgid "The nozzle type is not set. Please set the nozzle and try again." 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." msgstr "Typ trysky není nastaven. Nastavte prosím trysku a zkuste to znovu."
+6 -3
View File
@@ -3,7 +3,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: Orca Slicer\n" "Project-Id-Version: Orca Slicer\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-08-18 16:25+0800\n" "POT-Creation-Date: 2026-08-19 14:59+0800\n"
"PO-Revision-Date: \n" "PO-Revision-Date: \n"
"Last-Translator: Heiko Liebscher <hliebschergmail.com>\n" "Last-Translator: Heiko Liebscher <hliebschergmail.com>\n"
"Language-Team: \n" "Language-Team: \n"
@@ -6259,8 +6259,8 @@ msgstr "Projekt speichern als"
msgid "Save current project as" msgid "Save current project as"
msgstr "Aktuelles Projekt speichern als" msgstr "Aktuelles Projekt speichern als"
msgid "Publish" msgid "Publish 3MF"
msgstr "Veröffentlichen" msgstr ""
msgid "Export a 3MF file with the selected settings embedded" msgid "Export a 3MF file with the selected settings embedded"
msgstr "" msgstr ""
@@ -8464,6 +8464,9 @@ msgid ""
"Please check whether the folder exists online or if other programs have the file open." "Please check whether the folder exists online or if other programs have the file open."
msgstr "" msgstr ""
msgid "Publish"
msgstr "Veröffentlichen"
msgid "The nozzle type is not set. Please set the nozzle and try again." 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." msgstr "Die Düsenart ist nicht eingestellt. Bitte stellen Sie die Düse ein und versuchen Sie es erneut."
+5 -2
View File
@@ -3,7 +3,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: Orca Slicer\n" "Project-Id-Version: Orca Slicer\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-08-18 16:25+0800\n" "POT-Creation-Date: 2026-08-19 14:59+0800\n"
"PO-Revision-Date: 2026-06-17 15:44-0300\n" "PO-Revision-Date: 2026-06-17 15:44-0300\n"
"Last-Translator: Alexandre Folle de Menezes\n" "Last-Translator: Alexandre Folle de Menezes\n"
"Language-Team: \n" "Language-Team: \n"
@@ -5941,7 +5941,7 @@ msgstr ""
msgid "Save current project as" msgid "Save current project as"
msgstr "" msgstr ""
msgid "Publish" msgid "Publish 3MF"
msgstr "" msgstr ""
msgid "Export a 3MF file with the selected settings embedded" msgid "Export a 3MF file with the selected settings embedded"
@@ -8039,6 +8039,9 @@ msgid ""
"Please check whether the folder exists online or if other programs have the file open." "Please check whether the folder exists online or if other programs have the file open."
msgstr "" msgstr ""
msgid "Publish"
msgstr ""
msgid "The nozzle type is not set. Please set the nozzle and try again." msgid "The nozzle type is not set. Please set the nozzle and try again."
msgstr "" msgstr ""
+6 -3
View File
@@ -3,7 +3,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: Orca Slicer\n" "Project-Id-Version: Orca Slicer\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-08-18 16:25+0800\n" "POT-Creation-Date: 2026-08-19 14:59+0800\n"
"PO-Revision-Date: \n" "PO-Revision-Date: \n"
"Last-Translator: Ian A. Bassi <>\n" "Last-Translator: Ian A. Bassi <>\n"
"Language-Team: \n" "Language-Team: \n"
@@ -6116,8 +6116,8 @@ msgstr "Guardar proyecto como"
msgid "Save current project as" msgid "Save current project as"
msgstr "Guardar el proyecto actual como" msgstr "Guardar el proyecto actual como"
msgid "Publish" msgid "Publish 3MF"
msgstr "Publicar" msgstr ""
msgid "Export a 3MF file with the selected settings embedded" msgid "Export a 3MF file with the selected settings embedded"
msgstr "" msgstr ""
@@ -8267,6 +8267,9 @@ msgid ""
"Please check whether the folder exists online or if other programs have the file open." "Please check whether the folder exists online or if other programs have the file open."
msgstr "" msgstr ""
msgid "Publish"
msgstr "Publicar"
msgid "The nozzle type is not set. Please set the nozzle and try again." 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." msgstr "El tipo de boquilla no está establecido. Configure la boquilla e inténtelo de nuevo."
+6 -3
View File
@@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: \n" "Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-08-18 16:25+0800\n" "POT-Creation-Date: 2026-08-19 14:59+0800\n"
"PO-Revision-Date: 2026-07-20 13:33+0200\n" "PO-Revision-Date: 2026-07-20 13:33+0200\n"
"Last-Translator: Manu Goiogana <mgoiogana@gmail.com>\n" "Last-Translator: Manu Goiogana <mgoiogana@gmail.com>\n"
"Language-Team: \n" "Language-Team: \n"
@@ -6161,8 +6161,8 @@ msgstr "Gorde proiektua honela"
msgid "Save current project as" msgid "Save current project as"
msgstr "Gorde uneko proiektua honela" msgstr "Gorde uneko proiektua honela"
msgid "Publish" msgid "Publish 3MF"
msgstr "Argitaratu" msgstr ""
msgid "Export a 3MF file with the selected settings embedded" msgid "Export a 3MF file with the selected settings embedded"
msgstr "" msgstr ""
@@ -8334,6 +8334,9 @@ msgid ""
"Please check whether the folder exists online or if other programs have the file open." "Please check whether the folder exists online or if other programs have the file open."
msgstr "" msgstr ""
msgid "Publish"
msgstr "Argitaratu"
msgid "The nozzle type is not set. Please set the nozzle and try again." 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." msgstr "Pita mota ez dago ezarrita. Ezarri pita eta saiatu berriro."
+6 -3
View File
@@ -6,7 +6,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: Orca Slicer\n" "Project-Id-Version: Orca Slicer\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-08-18 16:25+0800\n" "POT-Creation-Date: 2026-08-19 14:59+0800\n"
"PO-Revision-Date: \n" "PO-Revision-Date: \n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Guislain Cyril, Thomas Lété\n" "Language-Team: Guislain Cyril, Thomas Lété\n"
@@ -6208,8 +6208,8 @@ msgstr "Enregistrer le projet sous"
msgid "Save current project as" msgid "Save current project as"
msgstr "Enregistrer le projet actuel sous" msgstr "Enregistrer le projet actuel sous"
msgid "Publish" msgid "Publish 3MF"
msgstr "Publier" msgstr ""
msgid "Export a 3MF file with the selected settings embedded" msgid "Export a 3MF file with the selected settings embedded"
msgstr "" msgstr ""
@@ -8390,6 +8390,9 @@ msgid ""
"Please check whether the folder exists online or if other programs have the file open." "Please check whether the folder exists online or if other programs have the file open."
msgstr "" msgstr ""
msgid "Publish"
msgstr "Publier"
msgid "The nozzle type is not set. Please set the nozzle and try again." 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." msgstr "Le type de buse n'est pas défini. Veuillez définir la buse et réessayer."
+6 -3
View File
@@ -3,7 +3,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: Orca Slicer\n" "Project-Id-Version: Orca Slicer\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-08-18 16:25+0800\n" "POT-Creation-Date: 2026-08-19 14:59+0800\n"
"Language: hu\n" "Language: hu\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
@@ -6310,8 +6310,8 @@ msgstr "Projekt mentése másként"
msgid "Save current project as" msgid "Save current project as"
msgstr "Jelenlegi projekt mentése másként" msgstr "Jelenlegi projekt mentése másként"
msgid "Publish" msgid "Publish 3MF"
msgstr "Közzététel" msgstr ""
msgid "Export a 3MF file with the selected settings embedded" msgid "Export a 3MF file with the selected settings embedded"
msgstr "" msgstr ""
@@ -8516,6 +8516,9 @@ msgid ""
"Please check whether the folder exists online or if other programs have the file open." "Please check whether the folder exists online or if other programs have the file open."
msgstr "" msgstr ""
msgid "Publish"
msgstr "Közzététel"
msgid "The nozzle type is not set. Please set the nozzle and try again." 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." msgstr "A fúvókatípus nincs beállítva. Állítsd be a fúvókát, majd próbáld újra."
+6 -3
View File
@@ -3,7 +3,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: Orca Slicer\n" "Project-Id-Version: Orca Slicer\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-08-18 16:25+0800\n" "POT-Creation-Date: 2026-08-19 14:59+0800\n"
"PO-Revision-Date: \n" "PO-Revision-Date: \n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: \n" "Language-Team: \n"
@@ -6312,8 +6312,8 @@ msgstr "Salva progetto con nome"
msgid "Save current project as" msgid "Save current project as"
msgstr "Salva progetto corrente con nome" msgstr "Salva progetto corrente con nome"
msgid "Publish" msgid "Publish 3MF"
msgstr "Pubblica" msgstr ""
msgid "Export a 3MF file with the selected settings embedded" msgid "Export a 3MF file with the selected settings embedded"
msgstr "" msgstr ""
@@ -8515,6 +8515,9 @@ msgid ""
"Please check whether the folder exists online or if other programs have the file open." "Please check whether the folder exists online or if other programs have the file open."
msgstr "" msgstr ""
msgid "Publish"
msgstr "Pubblica"
msgid "The nozzle type is not set. Please set the nozzle and try again." 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." msgstr "Il tipo di ugello non è impostato. Impostare l'ugello e riprovare."
+6 -3
View File
@@ -3,7 +3,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: Orca Slicer\n" "Project-Id-Version: Orca Slicer\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-08-18 16:25+0800\n" "POT-Creation-Date: 2026-08-19 14:59+0800\n"
"PO-Revision-Date: \n" "PO-Revision-Date: \n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: \n" "Language-Team: \n"
@@ -6321,8 +6321,8 @@ msgstr "プロジェクトを名前を付けて保存"
msgid "Save current project as" msgid "Save current project as"
msgstr "プロジェクトを名前を付けて保存" msgstr "プロジェクトを名前を付けて保存"
msgid "Publish" msgid "Publish 3MF"
msgstr "公開する" msgstr ""
msgid "Export a 3MF file with the selected settings embedded" msgid "Export a 3MF file with the selected settings embedded"
msgstr "" msgstr ""
@@ -8534,6 +8534,9 @@ msgid ""
"Please check whether the folder exists online or if other programs have the file open." "Please check whether the folder exists online or if other programs have the file open."
msgstr "" msgstr ""
msgid "Publish"
msgstr "公開する"
msgid "The nozzle type is not set. Please set the nozzle and try again." msgid "The nozzle type is not set. Please set the nozzle and try again."
msgstr "ノズルタイプが設定されていません。ノズルを設定して再試行してください。" msgstr "ノズルタイプが設定されていません。ノズルを設定して再試行してください。"
+6 -3
View File
@@ -7,7 +7,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: Orca Slicer\n" "Project-Id-Version: Orca Slicer\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-08-18 16:25+0800\n" "POT-Creation-Date: 2026-08-19 14:59+0800\n"
"PO-Revision-Date: 2025-06-02 17:12+0900\n" "PO-Revision-Date: 2025-06-02 17:12+0900\n"
"Last-Translator: crwusiz <crwusiz@gmail.com>\n" "Last-Translator: crwusiz <crwusiz@gmail.com>\n"
"Language-Team: \n" "Language-Team: \n"
@@ -6335,8 +6335,8 @@ msgstr "프로젝트 다른 이름으로 저장"
msgid "Save current project as" msgid "Save current project as"
msgstr "현재 프로젝트 다른 이름으로 저장" msgstr "현재 프로젝트 다른 이름으로 저장"
msgid "Publish" msgid "Publish 3MF"
msgstr "게시" msgstr ""
msgid "Export a 3MF file with the selected settings embedded" msgid "Export a 3MF file with the selected settings embedded"
msgstr "" msgstr ""
@@ -8567,6 +8567,9 @@ msgid ""
"Please check whether the folder exists online or if other programs have the file open." "Please check whether the folder exists online or if other programs have the file open."
msgstr "" msgstr ""
msgid "Publish"
msgstr "게시"
msgid "The nozzle type is not set. Please set the nozzle and try again." msgid "The nozzle type is not set. Please set the nozzle and try again."
msgstr "노즐 유형이 설정되지 않았습니다. 노즐을 설정하고 다시 시도하세요." msgstr "노즐 유형이 설정되지 않았습니다. 노즐을 설정하고 다시 시도하세요."
+6 -3
View File
@@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: \n" "Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-08-18 16:25+0800\n" "POT-Creation-Date: 2026-08-19 14:59+0800\n"
"PO-Revision-Date: 2026-07-02 14:13+0300\n" "PO-Revision-Date: 2026-07-02 14:13+0300\n"
"Last-Translator: Gintaras Kučinskas <sharanchius@gmail.com>\n" "Last-Translator: Gintaras Kučinskas <sharanchius@gmail.com>\n"
"Language-Team: \n" "Language-Team: \n"
@@ -6298,8 +6298,8 @@ msgstr "Įrašyti projektą kaip"
msgid "Save current project as" msgid "Save current project as"
msgstr "Įrašyti dabartinį projektą kaip" msgstr "Įrašyti dabartinį projektą kaip"
msgid "Publish" msgid "Publish 3MF"
msgstr "Talpinti" msgstr ""
msgid "Export a 3MF file with the selected settings embedded" msgid "Export a 3MF file with the selected settings embedded"
msgstr "" msgstr ""
@@ -8510,6 +8510,9 @@ msgid ""
"Please check whether the folder exists online or if other programs have the file open." "Please check whether the folder exists online or if other programs have the file open."
msgstr "" msgstr ""
msgid "Publish"
msgstr "Talpinti"
msgid "The nozzle type is not set. Please set the nozzle and try again." 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ą." msgstr "Purkštuko tipas nenustatytas. Nustatykite purkštuką ir bandykite dar kartą."
+6 -3
View File
@@ -3,7 +3,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: Orca Slicer\n" "Project-Id-Version: Orca Slicer\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-08-18 16:25+0800\n" "POT-Creation-Date: 2026-08-19 14:59+0800\n"
"PO-Revision-Date: \n" "PO-Revision-Date: \n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: \n" "Language-Team: \n"
@@ -6873,8 +6873,8 @@ msgstr "Bewaar project als"
msgid "Save current project as" msgid "Save current project as"
msgstr "Bewaar huidig project als" msgstr "Bewaar huidig project als"
msgid "Publish" msgid "Publish 3MF"
msgstr "Publiceren" msgstr ""
msgid "Export a 3MF file with the selected settings embedded" msgid "Export a 3MF file with the selected settings embedded"
msgstr "" msgstr ""
@@ -9284,6 +9284,9 @@ msgid ""
"Please check whether the folder exists online or if other programs have the file open." "Please check whether the folder exists online or if other programs have the file open."
msgstr "" msgstr ""
msgid "Publish"
msgstr "Publiceren"
# AI Translated # AI Translated
msgid "The nozzle type is not set. Please set the nozzle and try again." 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." msgstr "Het mondstuktype is niet ingesteld. Stel het mondstuk in en probeer het opnieuw."
+6 -3
View File
@@ -3,7 +3,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: OrcaSlicer 2.3.0-rc\n" "Project-Id-Version: OrcaSlicer 2.3.0-rc\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-08-18 16:25+0800\n" "POT-Creation-Date: 2026-08-19 14:59+0800\n"
"PO-Revision-Date: \n" "PO-Revision-Date: \n"
"Last-Translator: Krzysztof Morga <<tlumaczeniebs@gmail.com>>\n" "Last-Translator: Krzysztof Morga <<tlumaczeniebs@gmail.com>>\n"
"Language-Team: \n" "Language-Team: \n"
@@ -6454,8 +6454,8 @@ msgstr "Zapisz projekt jako"
msgid "Save current project as" msgid "Save current project as"
msgstr "Zapisz bieżący projekt jako" msgstr "Zapisz bieżący projekt jako"
msgid "Publish" msgid "Publish 3MF"
msgstr "Opublikuj" msgstr ""
msgid "Export a 3MF file with the selected settings embedded" msgid "Export a 3MF file with the selected settings embedded"
msgstr "" msgstr ""
@@ -8724,6 +8724,9 @@ msgid ""
"Please check whether the folder exists online or if other programs have the file open." "Please check whether the folder exists online or if other programs have the file open."
msgstr "" msgstr ""
msgid "Publish"
msgstr "Opublikuj"
msgid "The nozzle type is not set. Please set the nozzle and try again." 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." msgstr "Nie ustawiono typu dyszy Wprowadź ustawienia dyszy i spróbuj ponownie."
+6 -3
View File
@@ -3,7 +3,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: Orca Slicer\n" "Project-Id-Version: Orca Slicer\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-08-18 16:25+0800\n" "POT-Creation-Date: 2026-08-19 14:59+0800\n"
"PO-Revision-Date: 2026-07-26 11:14-0300\n" "PO-Revision-Date: 2026-07-26 11:14-0300\n"
"Last-Translator: Alexandre Folle de Menezes\n" "Last-Translator: Alexandre Folle de Menezes\n"
"Language-Team: Portuguese, Brazilian\n" "Language-Team: Portuguese, Brazilian\n"
@@ -6130,8 +6130,8 @@ msgstr "Salvar projeto como"
msgid "Save current project as" msgid "Save current project as"
msgstr "Salvar o projeto atual como" msgstr "Salvar o projeto atual como"
msgid "Publish" msgid "Publish 3MF"
msgstr "Publicar" msgstr ""
msgid "Export a 3MF file with the selected settings embedded" msgid "Export a 3MF file with the selected settings embedded"
msgstr "" msgstr ""
@@ -8290,6 +8290,9 @@ msgid ""
"Please check whether the folder exists online or if other programs have the file open." "Please check whether the folder exists online or if other programs have the file open."
msgstr "" msgstr ""
msgid "Publish"
msgstr "Publicar"
msgid "The nozzle type is not set. Please set the nozzle and try again." 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." msgstr "O tipo de bico não está configurado. Configure o bico e tente novamente."
+6 -3
View File
@@ -7,7 +7,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: OrcaSlicer V2.5.0\n" "Project-Id-Version: OrcaSlicer V2.5.0\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-08-18 16:25+0800\n" "POT-Creation-Date: 2026-08-19 14:59+0800\n"
"PO-Revision-Date: 2026-02-25 13:38+0300\n" "PO-Revision-Date: 2026-02-25 13:38+0300\n"
"Last-Translator: Felix14_v2\n" "Last-Translator: Felix14_v2\n"
"Language-Team: Felix14_v2 (ДС/ТГ: @felix14_v2, почта: aleks111001@list.ru), Andylg <andylg@yandex.ru>\n" "Language-Team: Felix14_v2 (ДС/ТГ: @felix14_v2, почта: aleks111001@list.ru), Andylg <andylg@yandex.ru>\n"
@@ -6363,8 +6363,8 @@ msgstr "Сохранить проект как"
msgid "Save current project as" msgid "Save current project as"
msgstr "Сохранить текущий проект как" msgstr "Сохранить текущий проект как"
msgid "Publish" msgid "Publish 3MF"
msgstr "Опубликовать" msgstr ""
msgid "Export a 3MF file with the selected settings embedded" msgid "Export a 3MF file with the selected settings embedded"
msgstr "" msgstr ""
@@ -8569,6 +8569,9 @@ msgid ""
"Please check whether the folder exists online or if other programs have the file open." "Please check whether the folder exists online or if other programs have the file open."
msgstr "" msgstr ""
msgid "Publish"
msgstr "Опубликовать"
msgid "The nozzle type is not set. Please set the nozzle and try again." msgid "The nozzle type is not set. Please set the nozzle and try again."
msgstr "Не указан тип сопла. Укажите его и попробуйте ещё раз." msgstr "Не указан тип сопла. Укажите его и попробуйте ещё раз."
+6 -3
View File
@@ -3,7 +3,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: Orca Slicer\n" "Project-Id-Version: Orca Slicer\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-08-18 16:25+0800\n" "POT-Creation-Date: 2026-08-19 14:59+0800\n"
"Language: sv\n" "Language: sv\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
@@ -6957,8 +6957,8 @@ msgstr "Spara Projekt som"
msgid "Save current project as" msgid "Save current project as"
msgstr "Spara nuvarande projekt som" msgstr "Spara nuvarande projekt som"
msgid "Publish" msgid "Publish 3MF"
msgstr "Publicera" msgstr ""
msgid "Export a 3MF file with the selected settings embedded" msgid "Export a 3MF file with the selected settings embedded"
msgstr "" msgstr ""
@@ -9377,6 +9377,9 @@ msgid ""
"Please check whether the folder exists online or if other programs have the file open." "Please check whether the folder exists online or if other programs have the file open."
msgstr "" msgstr ""
msgid "Publish"
msgstr "Publicera"
# AI Translated # AI Translated
msgid "The nozzle type is not set. Please set the nozzle and try again." 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." msgstr "Nozzeltypen är inte angiven. Ange nozzeln och försök igen."
+6 -3
View File
@@ -7,7 +7,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: Orca Slicer\n" "Project-Id-Version: Orca Slicer\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-08-18 16:25+0800\n" "POT-Creation-Date: 2026-08-19 14:59+0800\n"
"PO-Revision-Date: 2026-06-19 13:40+0700\n" "PO-Revision-Date: 2026-06-19 13:40+0700\n"
"Last-Translator: Icezaza\n" "Last-Translator: Icezaza\n"
"Language-Team: Thai\n" "Language-Team: Thai\n"
@@ -6289,8 +6289,8 @@ msgstr "บันทึกโปรเจกต์เป็น"
msgid "Save current project as" msgid "Save current project as"
msgstr "บันทึกโครงการปัจจุบันเป็น" msgstr "บันทึกโครงการปัจจุบันเป็น"
msgid "Publish" msgid "Publish 3MF"
msgstr "เผยแพร่" msgstr ""
msgid "Export a 3MF file with the selected settings embedded" msgid "Export a 3MF file with the selected settings embedded"
msgstr "" msgstr ""
@@ -8470,6 +8470,9 @@ msgid ""
"Please check whether the folder exists online or if other programs have the file open." "Please check whether the folder exists online or if other programs have the file open."
msgstr "" msgstr ""
msgid "Publish"
msgstr "เผยแพร่"
msgid "The nozzle type is not set. Please set the nozzle and try again." msgid "The nozzle type is not set. Please set the nozzle and try again."
msgstr "ไม่ได้ตั้งค่าประเภทหัวฉีด โปรดตั้งหัวฉีดแล้วลองอีกครั้ง" msgstr "ไม่ได้ตั้งค่าประเภทหัวฉีด โปรดตั้งหัวฉีดแล้วลองอีกครั้ง"
+6 -3
View File
@@ -3,7 +3,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: Orca Slicer\n" "Project-Id-Version: Orca Slicer\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-08-18 16:25+0800\n" "POT-Creation-Date: 2026-08-19 14:59+0800\n"
"PO-Revision-Date: 2026-08-01 20:32+0300\n" "PO-Revision-Date: 2026-08-01 20:32+0300\n"
"Last-Translator: GlauTech\n" "Last-Translator: GlauTech\n"
"Language-Team: \n" "Language-Team: \n"
@@ -6389,8 +6389,8 @@ msgstr "Projeyi farklı kaydet"
msgid "Save current project as" msgid "Save current project as"
msgstr "Mevcut projeyi farklı kaydet" msgstr "Mevcut projeyi farklı kaydet"
msgid "Publish" msgid "Publish 3MF"
msgstr "Yayınla" msgstr ""
msgid "Export a 3MF file with the selected settings embedded" msgid "Export a 3MF file with the selected settings embedded"
msgstr "" msgstr ""
@@ -8606,6 +8606,9 @@ msgid ""
"Please check whether the folder exists online or if other programs have the file open." "Please check whether the folder exists online or if other programs have the file open."
msgstr "" msgstr ""
msgid "Publish"
msgstr "Yayınla"
msgid "The nozzle type is not set. Please set the nozzle and try again." 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." msgstr "Nozul tipi ayarlanmamış. Lütfen nozulu ayarlayın ve tekrar deneyin."
+6 -3
View File
@@ -3,7 +3,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: orcaslicerua\n" "Project-Id-Version: orcaslicerua\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-08-18 16:25+0800\n" "POT-Creation-Date: 2026-08-19 14:59+0800\n"
"PO-Revision-Date: 2026-07-17 16:25+0300\n" "PO-Revision-Date: 2026-07-17 16:25+0300\n"
"Last-Translator: Andrij Mizyk <andm1zyk@proton.me>\n" "Last-Translator: Andrij Mizyk <andm1zyk@proton.me>\n"
"Language-Team: Ukrainian\n" "Language-Team: Ukrainian\n"
@@ -6326,8 +6326,8 @@ msgstr "Зберегти проєкт як"
msgid "Save current project as" msgid "Save current project as"
msgstr "Зберегти поточний проєкт як" msgstr "Зберегти поточний проєкт як"
msgid "Publish" msgid "Publish 3MF"
msgstr "Публікувати" msgstr ""
msgid "Export a 3MF file with the selected settings embedded" msgid "Export a 3MF file with the selected settings embedded"
msgstr "" msgstr ""
@@ -8580,6 +8580,9 @@ msgid ""
"Please check whether the folder exists online or if other programs have the file open." "Please check whether the folder exists online or if other programs have the file open."
msgstr "" msgstr ""
msgid "Publish"
msgstr "Публікувати"
msgid "The nozzle type is not set. Please set the nozzle and try again." msgid "The nozzle type is not set. Please set the nozzle and try again."
msgstr "Тип сопла не встановлений. Будь ласка, оберіть сопло та спробуйте ще раз." msgstr "Тип сопла не встановлений. Будь ласка, оберіть сопло та спробуйте ще раз."
+6 -3
View File
@@ -3,7 +3,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: Orca Slicer\n" "Project-Id-Version: Orca Slicer\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-08-18 16:25+0800\n" "POT-Creation-Date: 2026-08-19 14:59+0800\n"
"PO-Revision-Date: 2025-10-02 17:43+0700\n" "PO-Revision-Date: 2025-10-02 17:43+0700\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: hainguyen.ts13@gmail.com\n" "Language-Team: hainguyen.ts13@gmail.com\n"
@@ -6674,8 +6674,8 @@ msgstr "Lưu dự án thành"
msgid "Save current project as" msgid "Save current project as"
msgstr "Lưu dự án hiện tại thành" msgstr "Lưu dự án hiện tại thành"
msgid "Publish" msgid "Publish 3MF"
msgstr "Xuất bản" msgstr ""
msgid "Export a 3MF file with the selected settings embedded" msgid "Export a 3MF file with the selected settings embedded"
msgstr "" msgstr ""
@@ -9001,6 +9001,9 @@ msgid ""
"Please check whether the folder exists online or if other programs have the file open." "Please check whether the folder exists online or if other programs have the file open."
msgstr "" msgstr ""
msgid "Publish"
msgstr "Xuất bản"
# AI Translated # AI Translated
msgid "The nozzle type is not set. Please set the nozzle and try again." 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." msgstr "Chưa đặt loại đầu phun. Vui lòng đặt đầu phun rồi thử lại."
+6 -3
View File
@@ -6,7 +6,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: Slic3rPE\n" "Project-Id-Version: Slic3rPE\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-08-18 16:25+0800\n" "POT-Creation-Date: 2026-08-19 14:59+0800\n"
"PO-Revision-Date: 2026-06-11 12:37-0300\n" "PO-Revision-Date: 2026-06-11 12:37-0300\n"
"Last-Translator: Handle <mail@bysb.net>\n" "Last-Translator: Handle <mail@bysb.net>\n"
"Language-Team: \n" "Language-Team: \n"
@@ -6144,8 +6144,8 @@ msgstr "项目另存为"
msgid "Save current project as" msgid "Save current project as"
msgstr "项目另存为" msgstr "项目另存为"
msgid "Publish" msgid "Publish 3MF"
msgstr "发布" msgstr ""
msgid "Export a 3MF file with the selected settings embedded" msgid "Export a 3MF file with the selected settings embedded"
msgstr "" msgstr ""
@@ -8299,6 +8299,9 @@ msgid ""
"Please check whether the folder exists online or if other programs have the file open." "Please check whether the folder exists online or if other programs have the file open."
msgstr "" msgstr ""
msgid "Publish"
msgstr "发布"
msgid "The nozzle type is not set. Please set the nozzle and try again." msgid "The nozzle type is not set. Please set the nozzle and try again."
msgstr "未设置喷嘴类型。请设置喷嘴并重试。" msgstr "未设置喷嘴类型。请设置喷嘴并重试。"
+6 -3
View File
@@ -9,7 +9,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: Orca Slicer\n" "Project-Id-Version: Orca Slicer\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-08-18 16:25+0800\n" "POT-Creation-Date: 2026-08-19 14:59+0800\n"
"PO-Revision-Date: 2025-11-28 13:48-0600\n" "PO-Revision-Date: 2025-11-28 13:48-0600\n"
"Last-Translator: tntchn <15895303+tntchn@users.noreply.github.com>\n" "Last-Translator: tntchn <15895303+tntchn@users.noreply.github.com>\n"
"Language-Team: \n" "Language-Team: \n"
@@ -6274,8 +6274,8 @@ msgstr "另存專案為"
msgid "Save current project as" msgid "Save current project as"
msgstr "將目前專案另存為" msgstr "將目前專案另存為"
msgid "Publish" msgid "Publish 3MF"
msgstr "發布" msgstr ""
msgid "Export a 3MF file with the selected settings embedded" msgid "Export a 3MF file with the selected settings embedded"
msgstr "" msgstr ""
@@ -8465,6 +8465,9 @@ msgid ""
"Please check whether the folder exists online or if other programs have the file open." "Please check whether the folder exists online or if other programs have the file open."
msgstr "" msgstr ""
msgid "Publish"
msgstr "發布"
msgid "The nozzle type is not set. Please set the nozzle and try again." msgid "The nozzle type is not set. Please set the nozzle and try again."
msgstr "噴嘴類型尚未設定。請設定噴嘴後再試一次。" msgstr "噴嘴類型尚未設定。請設定噴嘴後再試一次。"
+216 -346
View File
@@ -4752,9 +4752,8 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool
} }
} // !is_published } // !is_published
// 4) Load the project config values (the per extruder wipe matrix etc). // Load the project config values. In published mode only the plate/bed geometry keys
// In published mode the receiver must not inherit the author's filament/purge data, // cross over (the receiver must not inherit the author's filament/purge data).
// so only the plate/bed geometry project keys are applied.
this->project_config.apply_only(config, is_published ? s_project_options_published : s_project_options); this->project_config.apply_only(config, is_published ? s_project_options_published : s_project_options);
break; break;
@@ -4774,29 +4773,23 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool
this->update_compatible(PresetSelectCompatibleType::Never); this->update_compatible(PresetSelectCompatibleType::Never);
this->update_multi_material_filament_presets(); this->update_multi_material_filament_presets();
// A "published" 3MF project overlays only the author-selected published keys onto the // A "published" 3MF project overlays the author-selected published keys onto the user's
// user's currently-selected (edited) process preset. Scalar keys are applied directly; // currently-selected (edited) process preset. Keys that cannot be applied are collected for
// vector (multi-extruder) keys are applied only when the edited preset has a matching // notification; filament-class keys in published_keys (which only the material pass knows
// vector size. Keys that cannot be applied are collected for notification; legacy // how to apply) fall through into skipped_keys.
// filament/printer keys in a published file fall through into skipped_keys.
if (is_published) { if (is_published) {
std::vector<std::string> skipped_keys; std::vector<std::string> skipped_keys;
std::set<std::string> applied_keys; std::set<std::string> applied_keys;
// Set whenever the material overlay actually modifies a receiver filament preset // Only re-select the edited filament preset when the material overlay changed
// (applied key, colour or slot replacement). Only then must the edited preset be // something: re-selecting unconditionally would discard the user's unsaved in-memory
// re-snapshotted: re-selecting unconditionally would discard the user's unsaved // filament edits when the published file touches nothing.
// in-memory filament edits when the published file touches nothing.
bool material_applied = false; bool material_applied = false;
// Structural keys must never be applied to the user's presets: doing so would // Structural keys are never applied (they would rewrite the user's preset
// rewrite their preset inheritance/structure. This is the single source of truth // inheritance/structure). Defense-in-depth: a hand-crafted 3MF could list them despite
// shared with PublishSettingsDialog.cpp (publish_structural_keys in // the dialog, so skip them here too.
// PublishSettings.hpp). Defense-in-depth: a hand-crafted 3MF could set
// published_keys to these regardless of the dialog, so skip them here too.
const std::set<std::string> &structural_keys = publish_structural_keys(); const std::set<std::string> &structural_keys = publish_structural_keys();
// The printer overlay is restricted to the publishable retraction/z-hop allowlist. // The printer overlay is restricted to the publishable retraction/z-hop allowlist;
// Printer-class keys outside it are contract-excluded: never applied and never // printer-class keys outside it are contract-excluded (never applied, never reported).
// reported as skipped (a hand-crafted 3MF listing machine_start_gcode or
// nozzle_diameter must not apply them and must not spam the warning).
const std::set<std::string> &printer_allowlist = publishable_printer_keys(); const std::set<std::string> &printer_allowlist = publishable_printer_keys();
const std::vector<std::string> &printer_options = Preset::printer_options(); const std::vector<std::string> &printer_options = Preset::printer_options();
const std::set<std::string> printer_option_set(printer_options.begin(), printer_options.end()); const std::set<std::string> printer_option_set(printer_options.begin(), printer_options.end());
@@ -4805,10 +4798,10 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool
for (const std::string &key : published_config->published_keys) { for (const std::string &key : published_config->published_keys) {
if (applied_keys.count(key) != 0) if (applied_keys.count(key) != 0)
continue; // already applied continue; // already applied
// A '#' suffix denotes a variant (per-extruder/per-filament) key; resolve the base key. // A '#' suffix denotes a variant key; resolve the base key.
const std::string base_key = key.substr(0, key.find('#')); const std::string base_key = key.substr(0, key.find('#'));
// Structural keys are intentionally never applied (not "skipped due to // Structural keys are never applied (not "skipped due to mismatch"), so bail
// mismatch"), so bail out before the applied/skipped bookkeeping. // out before the applied/skipped bookkeeping.
if (structural_keys.count(base_key) != 0) if (structural_keys.count(base_key) != 0)
continue; continue;
if (allowlist != nullptr && if (allowlist != nullptr &&
@@ -4822,23 +4815,29 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool
if (src_opt == nullptr) if (src_opt == nullptr)
continue; // key not present in the loaded config; record later continue; // key not present in the loaded config; record later
if (src_opt->is_vector()) { if (src_opt->is_vector()) {
// Vector key: apply only when the edited preset has a matching vector size.
const ConfigOption *dst_opt = target.option(base_key); const ConfigOption *dst_opt = target.option(base_key);
if (dst_opt == nullptr || !dst_opt->is_vector() || if (dst_opt == nullptr || !dst_opt->is_vector())
static_cast<const ConfigOptionVectorBase*>(src_opt)->size() != static_cast<const ConfigOptionVectorBase*>(dst_opt)->size())
continue; // cannot apply; will be reported as skipped continue; // cannot apply; will be reported as skipped
// A '#' variant index must be in range: ConfigOptionVector::set_at would // A '#N' variant key (e.g. per-extruder retraction_length#2) applies one
// otherwise resize the destination vector, corrupting the receiver's preset. // element, so the index only needs to be in range on both sides - the
// receiver may have a different extruder count than the author. Out-of-range
// indices are skipped (set_at would otherwise resize the receiver's vector).
if (key.size() > base_key.size()) { if (key.size() > base_key.size()) {
const size_t idx = static_cast<size_t>(std::atoi(key.c_str() + base_key.size() + 1)); const size_t idx = static_cast<size_t>(std::atoi(key.c_str() + base_key.size() + 1));
if (idx >= static_cast<const ConfigOptionVectorBase*>(src_opt)->size()) if (idx >= static_cast<const ConfigOptionVectorBase*>(src_opt)->size() ||
idx >= static_cast<const ConfigOptionVectorBase*>(dst_opt)->size())
continue; // out-of-range variant: cannot apply; reported as skipped continue; // out-of-range variant: cannot apply; reported as skipped
} else if (static_cast<const ConfigOptionVectorBase*>(src_opt)->size() !=
static_cast<const ConfigOptionVectorBase*>(dst_opt)->size()) {
// Whole-vector base key: the receiver must have a matching vector size,
// otherwise applying would overwrite a different number of elements.
continue; // cannot apply; will be reported as skipped
} }
target.apply_only(config, {key}, true); target.apply_only(config, {key}, true);
applied_keys.insert(key); applied_keys.insert(key);
} else { } else {
// A scalar key cannot carry a '#N' variant suffix; a hand-crafted file // A scalar key cannot carry a '#N' suffix; a hand-crafted file listing one
// listing one is reported as skipped instead of being silently marked applied. // is reported as skipped instead of being silently marked applied.
if (key.find('#') != std::string::npos) if (key.find('#') != std::string::npos)
continue; continue;
// Scalar key: apply only if present on the user's machine. // Scalar key: apply only if present on the user's machine.
@@ -4852,229 +4851,87 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool
apply_published(this->prints.get_edited_preset().config, nullptr); apply_published(this->prints.get_edited_preset().config, nullptr);
apply_published(this->printers.get_edited_preset().config, &printer_allowlist); apply_published(this->printers.get_edited_preset().config, &printer_allowlist);
// Material pass: apply the author's material-qualified keys onto the receiver's // Material pass: positional per-slot entries. The author published, per slot, either the
// matching filament presets. The file config carries the author's per-slot identity // entire filament (full) or specific keys plus optionally a curated type and/or colour.
// (filament_type / filament_vendor remain in config; filament_ids was moved into a // The receiver's slot is matched positionally against the published type:
// local earlier) and the per-slot material retraction values.
if (!published_config->material_keys.empty()) {
const ConfigOptionStrings *file_types = config.option<ConfigOptionStrings>("filament_type");
const ConfigOptionStrings *file_vendors = config.option<ConfigOptionStrings>("filament_vendor");
auto identity_matches = [](const std::string &id, const std::string &type, const std::string &vendor,
const std::string &slot_id, const std::string &slot_type, const std::string &slot_vendor) {
// When both sides carry a filament_id, equality is required; otherwise fall
// back to filament_type, with filament_vendor as an additional qualifier only
// when both sides have a non-empty vendor.
if (!id.empty() && !slot_id.empty())
return id == slot_id;
if (type.empty() || type != slot_type)
return false;
if (!vendor.empty() && !slot_vendor.empty())
return vendor == slot_vendor;
return true;
};
for (const PublishedMaterialEntry &entry : published_config->material_keys) {
// Entries using the filament-publishing-v2 features (full dump, published type or
// colour) are handled by the positional per-slot pass below; the legacy identity
// matching here applies only to files that predate those features.
if (entry.full || entry.publish_type || entry.publish_color)
continue;
// Resolve the author's source slot and its ordinal among the author slots
// carrying this entry's identity. A slotted entry (slot >= 0) names the exact
// author slot and targets the receiver's Nth matching preset (N = ordinal);
// a legacy entry (slot -1) uses the first matching author slot and applies to
// every matching receiver preset.
auto slot_identity = [&filament_ids, file_types, file_vendors](size_t slot, std::string &id, std::string &type, std::string &vendor) {
id = (slot < filament_ids.size()) ? filament_ids[slot] : std::string();
type = (file_types && slot < file_types->size()) ? file_types->get_at(slot) : std::string();
vendor = (file_vendors && slot < file_vendors->size()) ? file_vendors->get_at(slot) : std::string();
};
bool author_found = false;
size_t author_slot = 0;
size_t author_ordinal = 0;
if (entry.slot >= 0) {
// Collect every author slot carrying this identity, in slot order; the
// entry's slot must be among them, and its position is the ordinal used
// to pick the receiver's matching preset.
std::vector<size_t> matching_author_slots;
for (size_t slot = 0; slot < filament_ids.size(); ++slot) {
std::string slot_id, slot_type, slot_vendor;
slot_identity(slot, slot_id, slot_type, slot_vendor);
if (identity_matches(entry.filament_id, entry.filament_type, entry.filament_vendor,
slot_id, slot_type, slot_vendor))
matching_author_slots.emplace_back(slot);
}
const auto ordinal_it = std::find(matching_author_slots.begin(), matching_author_slots.end(), size_t(entry.slot));
if (ordinal_it != matching_author_slots.end()) {
author_slot = size_t(entry.slot);
author_ordinal = size_t(ordinal_it - matching_author_slots.begin());
author_found = true;
}
// Out of range, or the slot does not carry this identity: silent skip below.
} else {
// Legacy: the first author slot whose identity matches.
for (size_t slot = 0; slot < filament_ids.size(); ++slot) {
std::string slot_id, slot_type, slot_vendor;
slot_identity(slot, slot_id, slot_type, slot_vendor);
if (identity_matches(entry.filament_id, entry.filament_type, entry.filament_vendor,
slot_id, slot_type, slot_vendor)) {
author_slot = slot;
author_found = true;
break;
}
}
}
if (!author_found)
// No author slot carries this material: nothing to apply, nothing to report.
continue;
const std::string material_label = entry.filament_id.empty() ? entry.filament_type : entry.filament_id;
auto report_skipped = [&skipped_keys, &material_label](const std::string &key, const std::string &slot_qualifier = std::string()) {
skipped_keys.emplace_back("material:" + material_label +
(slot_qualifier.empty() ? std::string() : " " + slot_qualifier) +
" (" + key + ")");
};
// Collect the receiver's matching filament presets (distinct by preset name).
std::vector<std::string> matched_preset_names;
std::set<std::string> fallback_matched_names;
for (const std::string &preset_name : this->filament_presets) {
Preset *preset = this->filaments.find_preset(preset_name);
if (preset == nullptr)
continue;
const std::string slot_id = preset->filament_id;
// Null-guard the identity reads: a malformed user preset may lack
// filament_type / filament_vendor entirely (hand-edited preset file).
const ConfigOptionStrings *slot_types = preset->config.option<ConfigOptionStrings>("filament_type");
const ConfigOptionStrings *slot_vendors = preset->config.option<ConfigOptionStrings>("filament_vendor");
const std::string slot_type = (slot_types && !slot_types->values.empty()) ? slot_types->get_at(0) : std::string();
const std::string slot_vendor = (slot_vendors && !slot_vendors->values.empty()) ? slot_vendors->get_at(0) : std::string();
if (!identity_matches(entry.filament_id, entry.filament_type, entry.filament_vendor,
slot_id, slot_type, slot_vendor))
continue;
if (std::find(matched_preset_names.begin(), matched_preset_names.end(), preset_name) == matched_preset_names.end())
matched_preset_names.emplace_back(preset_name);
if (entry.filament_id.empty() || slot_id.empty())
fallback_matched_names.insert(preset_name);
}
if (matched_preset_names.empty()) {
// No receiver material matches this entry: report each key as skipped.
for (const std::string &key : entry.keys)
report_skipped(key);
continue;
}
if (fallback_matched_names.size() > 1) {
// The type fallback matched more than one distinct receiver preset: never
// guess which one the author meant.
for (const std::string &key : entry.keys)
report_skipped(key);
continue;
}
// Slotted entries target the receiver's matching preset at the author's
// ordinal; legacy entries apply to every matching receiver preset.
std::vector<std::string> apply_to_preset_names;
if (entry.slot >= 0) {
if (author_ordinal >= matched_preset_names.size()) {
// The receiver has fewer matching presets than the author's ordinal:
// this slot's values cannot be placed, report each key.
const std::string slot_qualifier = "slot " + std::to_string(entry.slot);
for (const std::string &key : entry.keys)
report_skipped(key, slot_qualifier);
continue;
}
apply_to_preset_names.emplace_back(matched_preset_names[author_ordinal]);
} else {
apply_to_preset_names = matched_preset_names;
}
for (const std::string &key : entry.keys) {
const std::string base_key = key.substr(0, key.find('#'));
if (structural_keys.count(base_key) != 0)
continue; // structural: silent
const ConfigOption *src_opt = config.option(base_key);
if (src_opt == nullptr || !src_opt->is_vector() ||
author_slot >= static_cast<const ConfigOptionVectorBase*>(src_opt)->size()) {
report_skipped(key);
continue;
}
for (const std::string &preset_name : apply_to_preset_names) {
Preset *preset = this->filaments.find_preset(preset_name);
if (preset == nullptr)
continue;
ConfigOption *dst_opt = preset->config.option(base_key);
// Per-slot scalar copy: the receiver's filament preset holds a single
// value per key (vector of size 1), the file holds the per-slot vector.
if (dst_opt == nullptr || !dst_opt->is_vector() ||
static_cast<const ConfigOptionVectorBase*>(dst_opt)->empty() ||
dst_opt->type() != src_opt->type()) {
report_skipped(key);
continue;
}
static_cast<ConfigOptionVectorBase*>(dst_opt)->set_at(src_opt, 0, author_slot);
material_applied = true;
}
}
}
}
// Filament-publishing-v2: positional per-slot entries. The author published, per slot,
// either the entire filament (full) or specific keys plus optionally a curated type
// and/or colour. The receiver's slot is matched positionally against the published type:
// - colour: always applied to the slot, independent of the type gate; // - colour: always applied to the slot, independent of the type gate;
// - type match: a full dump is intentionally ignored (the receiver keeps its material), // - type match: the full dump still applies wholesale (every setting, as if the slot's
// a partial entry's keys are applied as usual; // filament had been loaded from a normal save); a partial entry's keys are applied
// - type mismatch: the slot is replaced with the first visible same-type filament from // as usual;
// the receiver's library; the author's values are applied on top of it (full) or the // - type mismatch: the slot is replaced with the best visible candidate, scored by the
// published keys are applied (partial); // published identity (exact filament_id, then vendor+type, then type only); a
// - no replacement available: a full entry falls back to applying the author's values // preset no other slot references wins on equal scores, and a shared exact-material
// in-memory onto the receiver's current preset (no library import); a partial entry // preset is taken even though mutating it also affects the other slot; the author's
// keeps the receiver's material and reports its keys as skipped. // values are applied on top of it (full) or the published keys are applied (partial);
// - no replacement available: a full entry falls back to the first available visible
// preset, applying the author's values on top of it; a partial entry keeps the
// receiver's material and reports its keys as skipped.
// All applied values (colour and keys) are written onto the slot's stored preset
// directly (mutate in place): the receiver's material keeps its identity and is simply
// overridden. To keep slot-to-slot aliasing (several slots referencing one preset) from
// leaking one slot's values into another, published slots sharing a preset with another
// slot are re-pointed at distinct presets before the values are applied.
{ {
// Slot growth is tied to the author slots that carry published content (full, // Grow the receiver's slots only as far as the highest published slot (never
// type or colour): the file's total filament count is irrelevant, and a slot the // shrink, never pull filler materials for unpublished slots).
// author left unpublished must not pull a filler material into the receiver's bool has_published_entries = false;
// setup. Grow only as far as the highest published slot (never shrink, never size_t target_slots = this->filament_presets.size();
// remove the receiver's existing materials).
bool has_new_semantics = false;
size_t target_slots = this->filament_presets.size();
for (const PublishedMaterialEntry &entry : published_config->material_keys) { for (const PublishedMaterialEntry &entry : published_config->material_keys) {
if (!entry.full && !entry.publish_type && !entry.publish_color) has_published_entries = true;
continue; // legacy entry, handled above
has_new_semantics = true;
if (entry.slot >= 0) if (entry.slot >= 0)
target_slots = std::max(target_slots, size_t(entry.slot) + 1); target_slots = std::max(target_slots, size_t(entry.slot) + 1);
} }
if (has_new_semantics) { if (has_published_entries) {
// Defensive cap: never exceed the file's own filament count. // Defensive cap: never exceed the file's own filament count.
target_slots = std::min(target_slots, num_filaments); target_slots = std::min(target_slots, num_filaments);
// Slots that carry published content (full/type/colour) must reference a stored // Slots carrying published content, steering the initial preset selection of
// preset that no other slot shares: the overlay mutates stored presets in place // newly grown slots.
// (colour and keys), so a shared preset would leak one slot's published values
// into every slot that references it.
std::set<int> published_slots; std::set<int> published_slots;
for (const PublishedMaterialEntry &entry : published_config->material_keys) for (const PublishedMaterialEntry &entry : published_config->material_keys)
if ((entry.full || entry.publish_type || entry.publish_color) && entry.slot >= 0) if (entry.slot >= 0)
published_slots.insert(entry.slot); published_slots.insert(entry.slot);
std::set<std::string> used_preset_names(this->filament_presets.begin(), this->filament_presets.end()); std::set<std::string> used_preset_names(this->filament_presets.begin(), this->filament_presets.end());
// Mirror first_visible_idx()'s start index so suppressed default presets are // Mirror first_visible_idx()'s start index so suppressed default presets are
// never picked as a slot material. // never picked as a slot material.
const size_t first_candidate = this->filaments.is_default_suppressed() ? this->filaments.num_default_presets() : 0; const size_t first_candidate = this->filaments.is_default_suppressed() ? this->filaments.num_default_presets() : 0;
// Candidate preference for a published entry: exact setting_id (variant-level,
// since "Generic PLA" and "Generic PLA Matte" share filament_id), then exact
// filament_id, then vendor+type, then type only (a type-only pick may surface an
// unrelated preset, e.g. a different vendor's PLA).
auto candidate_score = [](const Preset &candidate, const PublishedMaterialEntry &entry) -> int {
if (!entry.setting_id.empty() && candidate.setting_id == entry.setting_id)
return 3;
const ConfigOptionStrings *types = candidate.config.opt<ConfigOptionStrings>("filament_type");
const ConfigOptionStrings *vendors = candidate.config.opt<ConfigOptionStrings>("filament_vendor");
const std::string type = (types != nullptr && !types->values.empty()) ? types->get_at(0) : std::string();
const std::string vendor = (vendors != nullptr && !vendors->values.empty()) ? vendors->get_at(0) : std::string();
if (!entry.filament_id.empty() && candidate.filament_id == entry.filament_id)
return 2;
if (normalize_filament_type(type) == entry.publish_type_value) {
if (!entry.filament_vendor.empty() && vendor == entry.filament_vendor)
return 1;
return 0;
}
return -1;
};
while (this->filament_presets.size() < target_slots) { while (this->filament_presets.size() < target_slots) {
const size_t new_slot_idx = this->filament_presets.size(); const size_t new_slot_idx = this->filament_presets.size();
std::string initial_preset; std::string initial_preset;
if (published_slots.count(static_cast<int>(new_slot_idx)) != 0) { if (published_slots.count(static_cast<int>(new_slot_idx)) != 0) {
// Proactively assign a distinct matching candidate preset if this slot // Prefer the best distinct candidate for the slot's published material
// carries a published type... // (exact id, then vendor+type, then type only)...
for (const PublishedMaterialEntry &entry : published_config->material_keys) { for (const PublishedMaterialEntry &entry : published_config->material_keys) {
if (entry.slot != static_cast<int>(new_slot_idx) || !entry.publish_type || entry.publish_type_value.empty()) if (entry.slot != static_cast<int>(new_slot_idx) || !entry.publish_type || entry.publish_type_value.empty())
continue; continue;
for (size_t i = 0; i < this->filaments.size(); ++i) { int best_score = -1;
for (size_t i = first_candidate; i < this->filaments.size(); ++i) {
const Preset &candidate = this->filaments.preset(i); const Preset &candidate = this->filaments.preset(i);
if (!candidate.is_visible || used_preset_names.count(candidate.name) != 0) if (!candidate.is_visible || used_preset_names.count(candidate.name) != 0)
continue; continue;
if (normalize_filament_type(candidate.config.opt_string("filament_type", 0u)) == entry.publish_type_value) { const int score = candidate_score(candidate, entry);
if (score > best_score) {
best_score = score;
initial_preset = candidate.name; initial_preset = candidate.name;
break;
} }
} }
break; break;
@@ -5091,62 +4948,65 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool
} }
} }
if (initial_preset.empty()) if (initial_preset.empty())
// Unpublished filler slot, or every visible preset is already used: repeat // Unpublished filler slot, or every visible preset is already used:
// the receiver's last preset, mirroring the "Add one filament" behaviour // repeat the receiver's last preset ("Add one filament" behaviour).
// (PresetBundle::set_num_filaments).
initial_preset = this->filament_presets.empty() ? this->filaments.first_visible().name initial_preset = this->filament_presets.empty() ? this->filaments.first_visible().name
: this->filament_presets.back(); : this->filament_presets.back();
this->filament_presets.emplace_back(initial_preset); this->filament_presets.emplace_back(initial_preset);
used_preset_names.insert(initial_preset); used_preset_names.insert(initial_preset);
} }
// Slots that were grown before this block (e.g. by update_multi_material_filament_presets // Published slots that alias another slot (multi-extruder with one filament)
// matching the extruder count) may still alias another slot; re-point them at a // get re-pointed at distinct presets: the overlay mutates stored presets in
// distinct preset. Slot 0, the receiver's own material, is never re-assigned. // place, so a shared preset would leak one slot's published values into every
// aliased slot. Slot 0 (the receiver's own material) is never re-assigned;
// when no unused candidate exists the aliasing stays (unavoidable).
auto referenced_elsewhere = [&](const std::string &preset_name, size_t except_slot) {
for (size_t s = 0; s < this->filament_presets.size(); ++s)
if (s != except_slot && this->filament_presets[s] == preset_name)
return true;
return false;
};
for (size_t slot = 1; slot < this->filament_presets.size(); ++slot) { for (size_t slot = 1; slot < this->filament_presets.size(); ++slot) {
if (published_slots.count(static_cast<int>(slot)) == 0) if (published_slots.count(static_cast<int>(slot)) == 0 ||
continue; !referenced_elsewhere(this->filament_presets[slot], slot))
bool shared = false;
for (size_t other = 0; other < this->filament_presets.size(); ++other)
if (other != slot && this->filament_presets[other] == this->filament_presets[slot]) {
shared = true;
break;
}
if (!shared)
continue; continue;
// Prefer the best distinct candidate for the slot's published material
// (exact id, then vendor+type, then type only)...
std::string replacement; std::string replacement;
int best_score = -1;
for (const PublishedMaterialEntry &entry : published_config->material_keys) { for (const PublishedMaterialEntry &entry : published_config->material_keys) {
if (entry.slot != static_cast<int>(slot) || !entry.publish_type || entry.publish_type_value.empty()) if (entry.slot != static_cast<int>(slot) || !entry.publish_type || entry.publish_type_value.empty())
continue; continue;
for (size_t i = 0; i < this->filaments.size(); ++i) { for (size_t i = first_candidate; i < this->filaments.size(); ++i) {
const Preset &candidate = this->filaments.preset(i); const Preset &candidate = this->filaments.preset(i);
if (!candidate.is_visible || used_preset_names.count(candidate.name) != 0) if (!candidate.is_visible || referenced_elsewhere(candidate.name, size_t(-1)))
continue; continue;
if (normalize_filament_type(candidate.config.opt_string("filament_type", 0u)) == entry.publish_type_value) { const int score = candidate_score(candidate, entry);
if (score > best_score) {
best_score = score;
replacement = candidate.name; replacement = candidate.name;
break;
} }
} }
break; break;
} }
// ...otherwise any distinct visible preset not referenced by another slot.
if (replacement.empty()) { if (replacement.empty()) {
for (size_t i = first_candidate; i < this->filaments.size(); ++i) { for (size_t i = first_candidate; i < this->filaments.size(); ++i) {
const Preset &candidate = this->filaments.preset(i); const Preset &candidate = this->filaments.preset(i);
if (!candidate.is_visible || used_preset_names.count(candidate.name) != 0) if (candidate.is_visible && !referenced_elsewhere(candidate.name, size_t(-1))) {
continue; replacement = candidate.name;
replacement = candidate.name; break;
break; }
} }
} }
if (replacement.empty()) if (replacement.empty())
continue; // every visible preset is used: aliasing is unavoidable continue; // every visible preset is referenced: aliasing is unavoidable
used_preset_names.erase(this->filament_presets[slot]);
this->filament_presets[slot] = replacement; this->filament_presets[slot] = replacement;
used_preset_names.insert(replacement); material_applied = true;
} }
// Mirror set_num_filaments' project_config vector handling ("Add one filament"): // Grow the per-slot colour/type/map project vectors to the new slot count and
// resize the per-slot colour/type/map vectors to the grown slot count and seed the // seed the new entries so the slots render with colours instead of blank chips
// new entries so the slots render with colours instead of blank chips. Only the // (mirrors set_num_filaments; existing values are left untouched).
// new entries are seeded; the receiver's existing values are left untouched.
ConfigOptionStrings *proj_colour = this->project_config.opt<ConfigOptionStrings>("filament_colour"); ConfigOptionStrings *proj_colour = this->project_config.opt<ConfigOptionStrings>("filament_colour");
ConfigOptionStrings *proj_multi_colour = this->project_config.opt<ConfigOptionStrings>("filament_multi_colour"); ConfigOptionStrings *proj_multi_colour = this->project_config.opt<ConfigOptionStrings>("filament_multi_colour");
ConfigOptionStrings *proj_colour_type = this->project_config.opt<ConfigOptionStrings>("filament_colour_type"); ConfigOptionStrings *proj_colour_type = this->project_config.opt<ConfigOptionStrings>("filament_colour_type");
@@ -5184,8 +5044,7 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool
if (proj_colour_type && slot < proj_colour_type->values.size()) if (proj_colour_type && slot < proj_colour_type->values.size())
proj_colour_type->values[slot] = "1"; // default colour type proj_colour_type->values[slot] = "1"; // default colour type
} }
// Rebuild the flush volumes for the grown slot count (set_num_filaments does the // Rebuild the flush volumes for the grown slot count (as set_num_filaments does).
// same; without it the matrix would stay at the receiver's old size).
this->update_multi_material_filament_presets(); this->update_multi_material_filament_presets();
auto apply_slot_keys = [&](Preset &preset, const std::vector<std::string> &slot_keys, int author_slot, auto apply_slot_keys = [&](Preset &preset, const std::vector<std::string> &slot_keys, int author_slot,
@@ -5207,23 +5066,22 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool
skipped_keys.emplace_back("material:" + material_label + " (" + key + ")"); skipped_keys.emplace_back("material:" + material_label + " (" + key + ")");
continue; continue;
} }
// Per-slot scalar copy: the receiver's filament preset holds a single // Per-slot scalar copy: the receiver preset holds one value per key
// value per key (vector of size 1), the file holds the per-slot vector. // (vector of size 1), the file holds the per-slot vector.
static_cast<ConfigOptionVectorBase*>(dst_opt)->set_at(src_opt, 0, author_slot); static_cast<ConfigOptionVectorBase*>(dst_opt)->set_at(src_opt, 0, author_slot);
material_applied = true; material_applied = true;
} }
}; };
// The slot's values are applied directly onto the slot's stored preset (mutate
// in place); the per-entry type gate below may re-point the slot first.
for (const PublishedMaterialEntry &entry : published_config->material_keys) { for (const PublishedMaterialEntry &entry : published_config->material_keys) {
if (!entry.full && !entry.publish_type && !entry.publish_color)
continue; // legacy entry, handled above
if (entry.slot < 0 || size_t(entry.slot) >= this->filament_presets.size()) if (entry.slot < 0 || size_t(entry.slot) >= this->filament_presets.size())
continue; // out of range: nothing to do for this slot continue; // out of range: nothing to do for this slot
const size_t slot = size_t(entry.slot); const size_t slot = size_t(entry.slot);
// Modify the stored preset itself (real=true), never the edited snapshot: // Resolve the stored preset itself (real=true), never the edited snapshot:
// find_preset would return &m_edited_preset for the currently selected slot, // find_preset would return &m_edited_preset for the selected slot, and the
// and the re-select at the end of this block re-snapshots from the stored // re-select at the end re-snapshots from the stored preset.
// preset, silently discarding any values applied to the snapshot.
Preset *recv = this->filaments.find_preset(this->filament_presets[slot], false, true); Preset *recv = this->filaments.find_preset(this->filament_presets[slot], false, true);
if (recv == nullptr) if (recv == nullptr)
continue; continue;
@@ -5234,83 +5092,96 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool
bool apply_slot = true; bool apply_slot = true;
if (entry.publish_type && !entry.publish_type_value.empty()) { if (entry.publish_type && !entry.publish_type_value.empty()) {
const std::string recv_type = normalize_filament_type(recv->config.opt_string("filament_type", 0u)); // Null-guard: a malformed receiver preset may lack filament_type.
if (recv_type == entry.publish_type_value) { const ConfigOptionStrings *recv_types = recv->config.opt<ConfigOptionStrings>("filament_type");
// Type match: the receiver keeps its material. A full dump is const std::string recv_type = (recv_types != nullptr && !recv_types->values.empty()) ? recv_types->get_at(0) : std::string();
// intentionally ignored for this slot; partial keys still apply. if (normalize_filament_type(recv_type) != entry.publish_type_value) {
if (entry.full) // Type mismatch: replace the slot with the best matching preset,
apply_slot = false; // scored by the published identity (exact filament_id, then
} else { // vendor+type, then type only). A preset no other slot references
// Type mismatch: replace the slot with the first visible same-type // wins on equal scores; a shared exact-material preset is taken even
// filament from the receiver's library, preferring one that no other // though mutating it also affects the other slot.
// slot references (a shared stored preset would leak this slot's auto find_best = [&](bool unreferenced_only) -> std::pair<int, std::string> {
// published values into that slot). int best_score = -1;
std::string replacement, first_same_type; std::string best_name;
for (size_t i = 0; i < this->filaments.size(); ++i) { for (size_t i = first_candidate; i < this->filaments.size(); ++i) {
const Preset &candidate = this->filaments.preset(i); const Preset &candidate = this->filaments.preset(i);
if (!candidate.is_visible) if (!candidate.is_visible)
continue; continue;
if (normalize_filament_type(candidate.config.opt_string("filament_type", 0u)) != entry.publish_type_value) const int score = candidate_score(candidate, entry);
continue; if (score <= best_score)
if (first_same_type.empty()) continue;
first_same_type = candidate.name; if (unreferenced_only) {
bool used_elsewhere = false; bool used = false;
for (size_t s = 0; s < this->filament_presets.size(); ++s) for (size_t s = 0; s < this->filament_presets.size(); ++s)
if (s != slot && this->filament_presets[s] == candidate.name) { if (s != slot && this->filament_presets[s] == candidate.name) {
used_elsewhere = true; used = true;
break; break;
}
if (used)
continue;
} }
if (!used_elsewhere) { best_score = score;
replacement = candidate.name; best_name = candidate.name;
break;
} }
return { best_score, best_name };
};
const auto [strict_score, strict_name] = find_best(true);
const auto [relaxed_score, relaxed_name] = find_best(false);
int score = strict_score;
std::string replacement = strict_name;
if (relaxed_score > strict_score) {
score = relaxed_score;
replacement = relaxed_name;
} }
if (replacement.empty())
replacement = first_same_type;
if (!replacement.empty()) { if (!replacement.empty()) {
const std::string old_name = recv->name; const std::string old_name = recv->name;
this->filament_presets[slot] = replacement; this->filament_presets[slot] = replacement;
recv = this->filaments.find_preset(replacement, false, true); recv = this->filaments.find_preset(replacement, false, true);
material_applied = true; material_applied = true;
published_config->material_replacements.emplace_back( std::string replacement_line = "slot " + std::to_string(slot) + ": " + old_name + " -> " + replacement;
"slot " + std::to_string(slot) + ": " + old_name + " -> " + replacement); // A pick that is not the exact published material is a substitute;
// an entry without identity fields cannot be judged, so it stays plain.
if (score < 2 && (!entry.filament_id.empty() || !entry.filament_vendor.empty()))
replacement_line += " (substitute: no exact material match)";
published_config->material_replacements.emplace_back(std::move(replacement_line));
} else if (entry.full) { } else if (entry.full) {
// No library match: create a temporary project-embedded custom preset // No same-type library preset: fall back to the first available
// populated with default settings and overlaid with the author's values. // visible preset, preferring one no other slot references, and
std::string custom_name = entry.publish_type_value + " (Published)"; // apply the author's full values on top of it (the dump carries
for (size_t idx = 1; this->filaments.find_preset(custom_name, false) != nullptr; ++idx) // filament_type, so the preset takes the author's type).
custom_name = entry.publish_type_value + " (Published " + std::to_string(idx) + ")"; std::string fallback;
for (size_t i = first_candidate; i < this->filaments.size(); ++i) {
// Capture the slot's current name BEFORE load_preset: the custom const Preset &candidate = this->filaments.preset(i);
// name sorts ahead of the slot's material, so the deque insertion if (!candidate.is_visible)
// relocates it and recv would dangle after the call. continue;
const std::string old_name = recv->name; if (fallback.empty())
fallback = candidate.name;
DynamicPrintConfig custom_cfg = this->filaments.default_preset_for(config).config; bool referenced = false;
// filament_type is a per-slot vector option: set it via the strings for (size_t s = 0; s < this->filament_presets.size(); ++s)
// accessor. opt_string(key, bool) would ask for the scalar if (this->filament_presets[s] == candidate.name) {
// ConfigOptionString, fail the cast and dereference nullptr. referenced = true;
if (ConfigOptionStrings *type_opt = custom_cfg.opt<ConfigOptionStrings>("filament_type", true)) { break;
if (type_opt->values.empty()) }
type_opt->values.emplace_back(); if (!referenced) {
type_opt->values[0] = entry.publish_type_value; fallback = candidate.name;
break;
}
} }
if (ConfigOptionStrings *id_opt = custom_cfg.opt<ConfigOptionStrings>("filament_settings_id", true)) if (!fallback.empty() && fallback != recv->name) {
if (!id_opt->values.empty()) const std::string old_name = recv->name;
id_opt->values[0] = custom_name; this->filament_presets[slot] = fallback;
recv = this->filaments.find_preset(fallback, false, true);
Preset &created = this->filaments.load_preset("", custom_name, std::move(custom_cfg), false, file_version); material_applied = true;
created.is_project_embedded = true; published_config->material_replacements.emplace_back(
created.is_visible = true; "slot " + std::to_string(slot) + ": " + old_name + " -> " + fallback +
" (substitute: no " + entry.publish_type_value + " available)");
this->filament_presets[slot] = custom_name; }
recv = &created; // No visible preset at all: keep the receiver's material and let
material_applied = true; // the full dump mutate it below.
published_config->material_replacements.emplace_back(
"slot " + std::to_string(slot) + ": " + old_name + " -> " + custom_name);
} else { } else {
// Partial publish with no replacement available: keep the // Partial publish with no replacement: keep the receiver's
// receiver's material and report this slot's keys as skipped. // material and report the slot's keys as skipped.
for (const std::string &key : entry.keys) for (const std::string &key : entry.keys)
skipped_keys.emplace_back("material:" + material_label + " (" + key + ")"); skipped_keys.emplace_back("material:" + material_label + " (" + key + ")");
apply_slot = false; apply_slot = false;
@@ -5318,14 +5189,15 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool
} }
} }
// Colour is slot-scoped and independent of the type gate: it is applied to // The values below are applied onto whatever stored preset the slot ended up
// whichever material ends up in the slot (original, replacement or the // on (original, type replacement or the full-publish fallback), in place.
// in-memory fallback), and synced into project_config for GUI rendering.
// Colour is slot-scoped and independent of the type gate; it is also synced
// into project_config for GUI rendering.
if (entry.publish_color && !entry.color.empty()) { if (entry.publish_color && !entry.color.empty()) {
if (recv != nullptr) { if (recv != nullptr) {
// Create the key when the target preset lacks it (e.g. a replacement // Create the key when the target preset lacks it: the colour is a
// built from the static defaults): the colour is a requirement, not // requirement, not an override.
// an optional override.
if (ConfigOptionStrings *colour = recv->config.opt<ConfigOptionStrings>("filament_colour", true)) { if (ConfigOptionStrings *colour = recv->config.opt<ConfigOptionStrings>("filament_colour", true)) {
if (colour->values.empty()) if (colour->values.empty())
colour->values.emplace_back(); colour->values.emplace_back();
@@ -5364,11 +5236,9 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool
} }
published_config->skipped_keys = std::move(skipped_keys); published_config->skipped_keys = std::move(skipped_keys);
// The material overlay above modified the filament collection presets in place, but // The material overlay mutates the collection presets in place, but the edited preset
// the edited preset (what the GUI displays) is a snapshot taken when the preset was // (what the GUI displays) is a snapshot taken when the preset was last selected.
// last selected. Re-select the first slot's filament (mirroring a normal project load) // Re-select the first slot's filament so the applied values surface in the GUI.
// so the applied values (colour, type, keys and slot replacements) surface in the GUI;
// selecting any other slot's filament afterwards snapshots its modified preset too.
if (material_applied && !this->filament_presets.empty()) if (material_applied && !this->filament_presets.empty())
this->filaments.select_preset_by_name(this->filament_presets.front(), true); this->filaments.select_preset_by_name(this->filament_presets.front(), true);
} }
+6 -7
View File
@@ -167,22 +167,21 @@ struct PresetBundleMetadata
} }
}; };
// Configuration describing a "published" 3MF project: the file carries a flag plus a list of // A "published" 3MF project: keeps the user's currently-selected presets and overlays only the
// author-selected setting keys. When loading such a project the user's currently-selected // author-selected published keys onto the edited presets.
// presets are kept and only the published keys are overlaid onto the edited presets.
struct PublishedConfig struct PublishedConfig
{ {
bool published = false; bool published = false;
std::vector<std::string> published_keys; std::vector<std::string> published_keys;
// Material-qualified published keys chosen by the author for the materials used in the // Per-slot published material keys, applied positionally (author slot N -> receiver slot N),
// project; applied on load only to the receiver's filament presets whose material // gated by the author's optional type requirement and written onto the slot's stored preset
// identity matches (see PublishedMaterialEntry in PublishSettings.hpp). // in place (see PublishedMaterialEntry in PublishSettings.hpp).
std::vector<PublishedMaterialEntry> material_keys; std::vector<PublishedMaterialEntry> material_keys;
// Keys that could not be applied (missing on the user's machine or vector size mismatch), // 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. // filled in by load_config_file_config for notification purposes.
std::vector<std::string> skipped_keys; std::vector<std::string> skipped_keys;
// Human-readable notices of the slot material replacements performed while loading a // Human-readable notices of the slot material replacements performed while loading a
// published project (e.g. "Slot 2: replaced PETG with PLA"), for the load notification. // published project, for the load notification.
std::vector<std::string> material_replacements; std::vector<std::string> material_replacements;
}; };
+15 -22
View File
@@ -30,11 +30,9 @@ std::string normalize_filament_type(const std::string& type)
const std::set<std::string>& publish_structural_keys() const std::set<std::string>& publish_structural_keys()
{ {
// Structural / non-publishable keys. The *_settings_id keys are also part of // Non-publishable keys: the *_settings_id keys are also in PresetCollection::skipped_in_dirty
// PresetCollection::skipped_in_dirty (Preset.cpp) and are excluded there too. // (Preset.cpp) / stripped from configs (profile_print_params_same); publishing them would
// This mirrors the structural keys stripped from configs in Preset.cpp // rewrite the user's preset inheritance/structure.
// (profile_print_params_same) plus other keys that must never be published
// because they would rewrite the user's preset inheritance/structure.
static const std::set<std::string> structural_keys = { static const std::set<std::string> structural_keys = {
"printer_settings_id", "filament_settings_id", "print_settings_id", "printer_settings_id", "filament_settings_id", "print_settings_id",
"sla_print_settings_id", "sla_material_settings_id", "sla_print_settings_id", "sla_material_settings_id",
@@ -88,9 +86,8 @@ const std::vector<PublishablePrinterOption>& publishable_printer_z_hop_options()
const std::set<std::string>& publishable_printer_keys() const std::set<std::string>& publishable_printer_keys()
{ {
// The union of the printer tab's "Retraction" and "Z-Hop" optgroups. The "Retraction when // Union of the two optgroups; "Retraction when switching material" keys are excluded
// switching material" keys are intentionally excluded: toolchange retraction is // (toolchange retraction is device/profile territory, not a publishable behavior tweak).
// device/profile territory, not a publishable behavior tweak.
static const std::set<std::string> printer_keys = [] { static const std::set<std::string> printer_keys = [] {
std::set<std::string> keys; std::set<std::string> keys;
for (const PublishablePrinterOption &opt : publishable_printer_retraction_options()) for (const PublishablePrinterOption &opt : publishable_printer_retraction_options())
@@ -113,9 +110,8 @@ std::vector<std::string> collect_dirty_settings_keys(const PresetBundle& bundle)
} }
}; };
// Print and printer presets each track a single edited preset; filaments may span // Union the dirty keys of each collection's edited preset (filaments may span multiple
// multiple slots (multi-material). Union the dirty keys of each collection's edited // slots); feeds only the Publish dialog's pre-check.
// preset; this feeds only the Publish dialog's pre-check.
append_dirty(bundle.prints.current_dirty_options(true)); append_dirty(bundle.prints.current_dirty_options(true));
append_dirty(bundle.printers.current_dirty_options(true)); append_dirty(bundle.printers.current_dirty_options(true));
append_dirty(bundle.filaments.current_dirty_options(true)); append_dirty(bundle.filaments.current_dirty_options(true));
@@ -131,12 +127,11 @@ DynamicPrintConfig filter_published_config(
DynamicPrintConfig filtered; DynamicPrintConfig filtered;
std::set<std::string> base_keys_to_include; std::set<std::string> base_keys_to_include;
// Base keys that must never be masked: identity, plate geometry, process/printer keys and // Never masked (whole-vector serialization): identity, plate geometry, process/printer
// partially-published material keys keep today's whole-vector serialization (all slots). // keys and partially-published material keys.
std::set<std::string> mask_exempt_keys; std::set<std::string> mask_exempt_keys;
// For keys carried only by "full" entries: base key -> author slots whose values must // "Full" entries only: base key -> author slots whose values must survive; other slots are
// survive; the other slots are masked to their defaults so a full publish does not leak // masked to their defaults so a full publish does not leak unrelated slot data.
// the author's unrelated slot data.
std::map<std::string, std::set<int>> full_slot_map; std::map<std::string, std::set<int>> full_slot_map;
// 1. Mandatory material identity & slot count keys for 3MF validation/normalization // 1. Mandatory material identity & slot count keys for 3MF validation/normalization
@@ -183,8 +178,7 @@ DynamicPrintConfig filter_published_config(
mask_exempt_keys.insert(base_key); mask_exempt_keys.insert(base_key);
} }
} }
// 4b. "Full publish" entries carry the entire slot; the values of the covered keys are // Full-publish keys: mask to the author's slot on export (see the copy loop below).
// masked to the author's slot on export (see the copy loop below).
for (const std::string &key : entry.full_keys) { for (const std::string &key : entry.full_keys) {
const std::string base_key = key.substr(0, key.find('#')); const std::string base_key = key.substr(0, key.find('#'));
if (base_key.empty()) if (base_key.empty())
@@ -195,9 +189,8 @@ DynamicPrintConfig filter_published_config(
} }
} }
// Mask a vector option's slots that are not author-published: copy the option default over // Mask non-published vector slots with the option default; keys without a default stay
// each non-published index. Keys without an option default are left unmasked (the file then // unmasked (whole vector, matching partial-publish behavior).
// carries the whole vector, matching the partial-publish behavior).
auto mask_slots = [](ConfigOption &opt, const ConfigOptionDef *def, const std::set<int> &keep_slots) { auto mask_slots = [](ConfigOption &opt, const ConfigOptionDef *def, const std::set<int> &keep_slots) {
auto *vec = dynamic_cast<ConfigOptionVectorBase*>(&opt); auto *vec = dynamic_cast<ConfigOptionVectorBase*>(&opt);
if (vec == nullptr || vec->size() == 0 || def == nullptr || !def->default_value) if (vec == nullptr || vec->size() == 0 || def == nullptr || !def->default_value)
@@ -212,7 +205,7 @@ DynamicPrintConfig filter_published_config(
vec->set_at(def->default_value.get(), idx, 0); vec->set_at(def->default_value.get(), idx, 0);
}; };
// Copy selected options from full_config into filtered config // Copy the selected options from full_config into the filtered config.
for (const std::string &key : base_keys_to_include) { for (const std::string &key : base_keys_to_include) {
if (const ConfigOption *opt = full_config.option(key)) { if (const ConfigOption *opt = full_config.option(key)) {
ConfigOption *cloned = opt->clone(); ConfigOption *cloned = opt->clone();
+27 -42
View File
@@ -6,78 +6,63 @@
namespace Slic3r { namespace Slic3r {
class PresetBundle; class PresetBundle;
// Structural / non-publishable setting keys, shared by the Publish dialog and the published-3MF // Structural keys that must never be published (single source of truth for the denylist):
// overlay path in PresetBundle::load_config_file_config. These keys must never be published // publishing them would rewrite the user's preset inheritance/structure.
// because they would rewrite the user's preset inheritance/structure. This is the single
// source of truth for the denylist.
const std::set<std::string>& publish_structural_keys(); const std::set<std::string>& publish_structural_keys();
// One option row of the printer tab's "Retraction" / "Z-Hop" optgroups (TabPrinter::build_fff, // One row of the printer tab's "Retraction" / "Z-Hop" optgroups (key + tab icon id), kept
// Tab.cpp). Key and icon id are kept together so the tab can later be migrated onto these // together so the tab can later be migrated onto these lists.
// lists; publishable_printer_keys() is their union, and the published-3MF loader/dialog must
// never accept printer keys outside it.
struct PublishablePrinterOption { struct PublishablePrinterOption {
const char *key; // config key, e.g. "retraction_length" const char *key; // config key, e.g. "retraction_length"
const char *icon; // tab icon id, e.g. "printer_extruder_retraction#length" const char *icon; // tab icon id, e.g. "printer_extruder_retraction#length"
}; };
// The printer tab's "Retraction" optgroup options, in tab order. // 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_retraction_options();
// The printer tab's "Z-Hop" optgroup options, in tab order.
const std::vector<PublishablePrinterOption>& publishable_printer_z_hop_options(); const std::vector<PublishablePrinterOption>& publishable_printer_z_hop_options();
// Printer-class retraction / z-hop keys that are publishable: the union of // Union of the two optgroup option lists; the published-3MF overlay applies printer keys only
// publishable_printer_retraction_options() and publishable_printer_z_hop_options(). The // when their base key is in this allowlist (anything else is contract-excluded).
// published-3MF overlay applies printer keys only when their base key is in this allowlist;
// any other printer-class key in a published file is contract-excluded (never applied, never
// reported as skipped).
const std::set<std::string>& publishable_printer_keys(); const std::set<std::string>& publishable_printer_keys();
// Returns the union of setting keys that differ from the base/system preset across the current // Union of setting keys differing from the base/system preset across the current print,
// print, printer and filament presets (feeds the Publish dialog's pre-check). // printer and filament presets (feeds the Publish dialog's pre-check).
std::vector<std::string> collect_dirty_settings_keys(const PresetBundle& bundle); std::vector<std::string> collect_dirty_settings_keys(const PresetBundle& bundle);
// A material-qualified set of published setting keys, chosen by the author for one of the // Per-slot published material keys, applied positionally (author slot N -> receiver slot N).
// materials used in the project. The identity fields let the receiver apply the keys only // The identity fields are carried for reference/notification labels only; the type gate
// when a matching material is selected: filament_id is the most precise (stable across // (publish_type) is the author's explicit opt-in for requiring a material type.
// machines/vendors when present, empty for user presets); filament_type + filament_vendor
// are the fallback. Keys are base keys (no "#N" variant suffix).
struct PublishedMaterialEntry { struct PublishedMaterialEntry {
std::string filament_type; // material family, e.g. "PLA" (may be empty) 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_vendor; // e.g. "Generic", "Bambu" (may be empty)
std::string filament_id; // stable material id, e.g. "GFL99" (may be empty) std::string filament_id; // stable material id, e.g. "GFL99" (may be empty)
// 0-based author filament slot this entry's values came from; -1 = legacy/unspecified // Unique preset id of the author's slot preset (e.g. Orca Filament Library "setting_id");
// (files written before the slot field). Slotted entries apply to the receiver's Nth // used on load to match the exact published variant, which filament_id alone cannot
// matching preset (N = the slot's ordinal among the author's matching slots); legacy // distinguish ("Generic PLA" and "Generic PLA Matte" share their inherited id).
// entries apply to every matching receiver preset. std::string setting_id;
// 0-based author filament slot; -1 (hand-crafted files) is skipped.
int slot{-1}; int slot{-1};
std::vector<std::string> keys; std::vector<std::string> keys;
// "Full Publish": the entire filament preset of this slot is serialized (see full_keys), // "Full Publish": serialize the whole filament preset (full_keys); the type gate then
// not just the individually selected keys. On load the type gate (publish_type_value) // decides whether the receiver keeps its material (type match) or is replaced.
// decides whether the receiver keeps its material (type match) or is replaced; a full
// entry carries no partial keys.
bool full{false}; bool full{false};
// All non-structural filament keys of the author's slot preset, present when full is true. // All non-structural filament keys of the author's slot preset; values travel in the file
// Values travel in the file config, masked to the author's slot index. // config, masked to the author's slot index.
std::vector<std::string> full_keys; std::vector<std::string> full_keys;
// Vendor-agnostic, curated (MaterialType) filament type the author requires for this slot. // Vendor-agnostic (MaterialType) filament type the author requires for this slot; on
// On load the receiver's slot material is matched against it; on mismatch the slot is // mismatch the slot is replaced with a same-type filament from the receiver's library.
// replaced with a same-type filament from the receiver's library.
bool publish_type{false}; bool publish_type{false};
std::string publish_type_value; std::string publish_type_value;
// Required filament colour for this slot, applied on load regardless of the type match. // Required filament colour, applied on load regardless of the type match.
bool publish_color{false}; bool publish_color{false};
std::string color; std::string color;
}; };
// Normalizes a filament type string against the curated MaterialType list: an exact match // "PLA High Speed" -> "PLA" (strip a space modifier); dash types like "PA-CF" are kept intact.
// wins, then the value is stripped after its first space ("PLA High Speed" -> "PLA"); a
// value still not recognized is returned unchanged. Shared by the Publish dialog's type row
// default and by the published-3MF loader's type matching.
std::string normalize_filament_type(const std::string& type); std::string normalize_filament_type(const std::string& type);
// Constructs a minimal DynamicPrintConfig for a published 3MF export containing only the // Minimal DynamicPrintConfig for a published 3MF export: only the selected published keys,
// author-selected published keys, material keys, material identity fields, and plate geometry keys. // material keys, identity fields and plate geometry keys.
class DynamicPrintConfig; class DynamicPrintConfig;
DynamicPrintConfig filter_published_config( DynamicPrintConfig filter_published_config(
const DynamicPrintConfig &full_config, const DynamicPrintConfig &full_config,
+4 -6
View File
@@ -11,18 +11,16 @@ class DynamicPrintConfig;
namespace GUI { namespace GUI {
// Return the value of the given option (identified by opt_key, which may contain // Human-readable value of opt_key (may carry a "#<index>" suffix) in config.
// a "#<index>" suffix) formatted as a human readable string.
wxString get_string_value(const std::string& opt_key, const DynamicPrintConfig& config); wxString get_string_value(const std::string& opt_key, const DynamicPrintConfig& config);
// Return the full label of the given option (identified by opt_key, which may contain // Full label of opt_key; "N/A" when the option is not set.
// a "#<index>" suffix). Returns "N/A" when the option is not set.
wxString get_full_label(const std::string& opt_key, const DynamicPrintConfig& config); wxString get_full_label(const std::string& opt_key, const DynamicPrintConfig& config);
// Strip the "#<index>" suffix (if any) from the given option key. // Strip the "#<index>" suffix (if any) from the option key.
std::string get_pure_opt_key(const std::string& opt_key); std::string get_pure_opt_key(const std::string& opt_key);
// Return the localized label of the currently selected value of an enum option. // 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); wxString get_string_from_enum(const std::string& opt_key, const DynamicPrintConfig& config, bool is_infill = false, int idx = -1);
} // namespace GUI } // namespace GUI
+2 -2
View File
@@ -2837,11 +2837,11 @@ void MainFrame::init_menubar_as_editor()
auto publish_handler = [this](wxCommandEvent&) { publish_project(); }; auto publish_handler = [this](wxCommandEvent&) { publish_project(); };
#ifndef __APPLE__ #ifndef __APPLE__
append_menu_item(fileMenu, wxID_ANY, _L("Publish") + dots + "\t" + ctrl + shift + "E", _L("Export a 3MF file with the selected settings embedded"), 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, publish_handler, "menu_publish", nullptr,
[this](){return can_export_model(); }, this); [this](){return can_export_model(); }, this);
#else #else
append_menu_item(fileMenu, wxID_ANY, _L("Publish") + dots + "\t" + ctrl + shift + "E", _L("Export a 3MF file with the selected settings embedded"), 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, publish_handler, "", nullptr,
[this](){return can_export_model(); }, this); [this](){return can_export_model(); }, this);
#endif #endif
+23 -25
View File
@@ -7208,9 +7208,8 @@ std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_
} }
} }
// BBS: a "published" 3MF project carries a flag plus a list of author-selected // BBS: a "published" 3MF carries a flag plus the author-selected setting keys;
// setting keys. When present, keep the user's currently-selected presets and // on load keep the user's current presets and overlay only those keys.
// overlay only the published keys onto the edited presets on load.
PublishedConfig published_config; PublishedConfig published_config;
if (model.model_info != nullptr) { if (model.model_info != nullptr) {
auto published_it = model.model_info->metadata_items.find("published"); auto published_it = model.model_info->metadata_items.find("published");
@@ -7249,6 +7248,8 @@ std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_
entry.filament_vendor = mat["filament_vendor"].get<std::string>(); entry.filament_vendor = mat["filament_vendor"].get<std::string>();
if (mat.contains("filament_id") && mat["filament_id"].is_string()) if (mat.contains("filament_id") && mat["filament_id"].is_string())
entry.filament_id = mat["filament_id"].get<std::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 (m.contains("slot") && m["slot"].is_number_integer()) if (m.contains("slot") && m["slot"].is_number_integer())
entry.slot = m["slot"].get<int>(); entry.slot = m["slot"].get<int>();
@@ -7257,7 +7258,7 @@ std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_
for (const auto &k : *entry_keys_it) for (const auto &k : *entry_keys_it)
if (k.is_string()) if (k.is_string())
entry.keys.emplace_back(k.get<std::string>()); entry.keys.emplace_back(k.get<std::string>());
// Filament-publishing-v2 fields; absent in legacy files. // Fields always written by the current exporter.
if (m.contains("full") && m["full"].is_boolean()) if (m.contains("full") && m["full"].is_boolean())
entry.full = m["full"].get<bool>(); entry.full = m["full"].get<bool>();
const auto entry_full_keys_it = m.find("full_keys"); const auto entry_full_keys_it = m.find("full_keys");
@@ -7282,10 +7283,10 @@ std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_
} }
} }
// BBS: a "published" 3MF behaves like a new project once loaded: the file's path // BBS: a "published" 3MF loads as a new project: its path must not become the
// must not become the project filename (Save/Ctrl-S would otherwise overwrite the // project filename (Save/Ctrl-S would overwrite the shared file), and the
// shared file), and the published metadata is consumed by the overlay above and // published metadata is consumed above and stripped so a later save is a normal
// stripped so a later save produces a normal, unpublished 3MF. // unpublished 3MF.
if (published_out != nullptr && published_config.published) if (published_out != nullptr && published_config.published)
*published_out = true; *published_out = true;
if (published_config.published && load_config && this->model.model_info != nullptr) { if (published_config.published && load_config && this->model.model_info != nullptr) {
@@ -13309,10 +13310,10 @@ void Plater::load_project(wxString const& filename2,
p->set_project_filename(filename); p->set_project_filename(filename);
} }
else if (loaded_published) { else if (loaded_published) {
// A "published" 3MF loads as a new project: the shared file's path must not become // A "published" 3MF loads as a new project: its path must not become the project
// the project filename, so Save/Ctrl-S prompts for a destination instead of // filename (Save/Ctrl-S prompts for a destination instead of overwriting it);
// overwriting the published file. reset() above already cleared the project name // reset() already cleared the project name, so restore the default title and keep
// and folder; restore the default new-project title and keep the file in recents. // the file in recents.
p->set_project_name(_L("Untitled")); p->set_project_name(_L("Untitled"));
if (!filename.IsEmpty()) if (!filename.IsEmpty())
wxGetApp().mainframe->add_to_recent_projects(filename); wxGetApp().mainframe->add_to_recent_projects(filename);
@@ -16225,10 +16226,9 @@ void Plater::export_core_3mf()
export_3mf(path_u8, SaveStrategy::Silence); export_3mf(path_u8, SaveStrategy::Silence);
} }
// Export the current project as a "published" 3MF. This is a pure export: unlike save_project(), // Export the current project as a "published" 3MF: a pure export that never touches the
// it never touches the project's file name, dirty state, backup path or title, and the // project's file name, dirty state, backup path or title, and attaches the published metadata
// published metadata is attached to the model only for the duration of the export so the // to the model only for the duration of the export (a later Save Project is a normal 3MF).
// in-memory project stays exactly as it was (a later Save Project produces a normal 3MF).
int Plater::export_published_3mf(const std::vector<std::string>& published_keys, const std::vector<Slic3r::PublishedMaterialEntry>& material_keys) 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:")); wxString path = p->get_export_file(FT_3MF, _L("Publish 3MF file as:"));
@@ -16240,14 +16240,14 @@ int Plater::export_published_3mf(const std::vector<std::string>& published_keys,
j.push_back(key); j.push_back(key);
nlohmann::json jm = nlohmann::json::array(); nlohmann::json jm = nlohmann::json::array();
for (const Slic3r::PublishedMaterialEntry& e : material_keys) 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}}}, {"slot", e.slot}, {"keys", e.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}}}, {"slot", e.slot}, {"keys", e.keys},
{"full", e.full}, {"full_keys", e.full_keys}, {"full", e.full}, {"full_keys", e.full_keys},
{"publish_type", e.publish_type}, {"type", e.publish_type_value}, {"publish_type", e.publish_type}, {"type", e.publish_type_value},
{"publish_color", e.publish_color}, {"color", e.color} }); {"publish_color", e.publish_color}, {"color", e.color} });
Model& model = this->model(); Model& model = this->model();
// Remember the previous metadata state so it can be restored after the export, keeping the // Save the previous metadata so it can be restored after the export, keeping the in-memory
// in-memory project pristine (the published flag lives only in the exported file). // project pristine (the published flag lives only in the exported file).
const bool had_model_info = (model.model_info != nullptr); const bool had_model_info = (model.model_info != nullptr);
const bool had_published = had_model_info && (model.model_info->metadata_items.find("published") != model.model_info->metadata_items.end()); const bool had_published = had_model_info && (model.model_info->metadata_items.find("published") != model.model_info->metadata_items.end());
const bool had_published_keys = had_model_info && (model.model_info->metadata_items.find("published_keys") != model.model_info->metadata_items.end()); const bool had_published_keys = had_model_info && (model.model_info->metadata_items.find("published_keys") != model.model_info->metadata_items.end());
@@ -16261,15 +16261,13 @@ int Plater::export_published_3mf(const std::vector<std::string>& published_keys,
model.model_info->metadata_items["published_keys"] = j.dump(); model.model_info->metadata_items["published_keys"] = j.dump();
model.model_info->metadata_items["published_material_keys"] = jm.dump(); model.model_info->metadata_items["published_material_keys"] = jm.dump();
// Minimal published export: filter full_config to only the published keys, material keys, // Minimal published export: filter full_config to the published keys, material keys,
// identity fields, and plate geometry keys, and omit project-embedded preset dumps. // identity fields and plate geometry keys, and omit project-embedded preset dumps.
DynamicPrintConfig full_cfg = wxGetApp().preset_bundle->full_config_secure(); DynamicPrintConfig full_cfg = wxGetApp().preset_bundle->full_config_secure();
DynamicPrintConfig filtered_cfg = filter_published_config(full_cfg, published_keys, material_keys); DynamicPrintConfig filtered_cfg = filter_published_config(full_cfg, published_keys, material_keys);
// Same file layout save_project() uses for its project files, plus SaveStrategy::Silence and SaveStrategy::MinimalPublished: // Same file layout as save_project(), plus Silence (so export_3mf does not set the project
// without it export_3mf() calls set_project_filename() on success, which would make this // filename on success, keeping this a pure export like export_core_3mf()) and MinimalPublished.
// pure export the current project file. Silence keeps the project state untouched, exactly
// like export_core_3mf().
auto save_strategy = SaveStrategy::SplitModel | SaveStrategy::ShareMesh | SaveStrategy::Silence | SaveStrategy::MinimalPublished; auto save_strategy = SaveStrategy::SplitModel | SaveStrategy::ShareMesh | SaveStrategy::Silence | SaveStrategy::MinimalPublished;
bool full_pathnames = wxGetApp().app_config->get_bool("export_sources_full_pathnames"); bool full_pathnames = wxGetApp().app_config->get_bool("export_sources_full_pathnames");
if (full_pathnames) if (full_pathnames)
+2 -3
View File
@@ -495,9 +495,8 @@ public:
void export_gcode_3mf(bool export_all = false); void export_gcode_3mf(bool export_all = false);
void send_gcode_finish(wxString name); void send_gcode_finish(wxString name);
void export_core_3mf(); void export_core_3mf();
// Export the current project as a "published" 3MF: embeds the author-selected settings // Export a "published" 3MF embedding the author-selected settings in the file metadata; a
// (published_keys / published_material_keys) into the file's metadata. A pure export: the // pure export that leaves the in-memory project untouched.
// in-memory project (filename, dirty state, model_info metadata) is left untouched.
int export_published_3mf(const std::vector<std::string>& published_keys, const std::vector<Slic3r::PublishedMaterialEntry>& material_keys); int export_published_3mf(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 = {}); 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); void export_stl(bool extended = false, bool selection_only = false, bool multi_stls = false, FileType file_type = FT_STL);
+96 -77
View File
@@ -23,8 +23,8 @@
namespace Slic3r { namespace GUI { namespace Slic3r { namespace GUI {
namespace { namespace {
// Menu ids for show_menu(). Dedicated range above the standard ids so the popup cannot // Menu ids for show_menu(): dedicated range so the popup cannot collide with application-level
// collide with application-level bindings (e.g. MainFrame's recent-files wxID_FILE1.. range). // bindings (e.g. MainFrame's recent-files wxID_FILE1.. range).
enum { enum {
kPublishSelectAll = wxID_HIGHEST + 1, kPublishSelectAll = wxID_HIGHEST + 1,
kPublishDeselectAll, kPublishDeselectAll,
@@ -49,9 +49,7 @@ PublishMaterialIdentity material_identity(size_t slot, const DynamicPrintConfig&
return identity; return identity;
} }
// "Generic PLA @System" -> "Generic PLA"; mirrors the alias derivation in // "Generic PLA @System" -> "Generic PLA"; mirrors the alias derivation in PresetBundle.cpp.
// PresetBundle::load_vendor_configs_from_json (PresetBundle.cpp) and
// PresetCollection::set_custom_preset_alias (Preset.cpp).
std::string material_display_name(const std::string& preset_name) std::string material_display_name(const std::string& preset_name)
{ {
const size_t at = preset_name.find_first_of('@'); const size_t at = preset_name.find_first_of('@');
@@ -62,8 +60,8 @@ std::string material_display_name(const std::string& preset_name)
return bare.empty() ? preset_name : bare; return bare.empty() ? preset_name : bare;
} }
// Human-readable section title for a filament slot: the resolved preset name, // Section title for a filament slot: the resolved preset name, then the filament type, then
// falling back to the filament type, then to the generic "Material". // the generic "Material".
wxString material_title(size_t slot, const PresetBundle* bundle, const DynamicPrintConfig& full) wxString material_title(size_t slot, const PresetBundle* bundle, const DynamicPrintConfig& full)
{ {
if (slot < bundle->filament_presets.size()) { if (slot < bundle->filament_presets.size()) {
@@ -165,8 +163,7 @@ PublishSettingsDialog::PublishSettingsDialog(wxWindow* parent)
auto dlg_btns = new DialogButtons(this, {"OK", "Cancel"}); auto dlg_btns = new DialogButtons(this, {"OK", "Cancel"});
dlg_btns->GetOK()->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { dlg_btns->GetOK()->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) {
// Publish is always allowed: no settings selected means a publish with // Publish is always allowed: no settings selected means no settings override.
// no settings override.
EndModal(wxID_OK); EndModal(wxID_OK);
}); });
dlg_btns->GetCANCEL()->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { EndModal(wxID_CANCEL); }); dlg_btns->GetCANCEL()->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { EndModal(wxID_CANCEL); });
@@ -183,12 +180,11 @@ PublishSettingsDialog::~PublishSettingsDialog() {}
void PublishSettingsDialog::build_option_model() void PublishSettingsDialog::build_option_model()
{ {
// Structural / non-publishable keys, shared with the published-3MF overlay // Structural / non-publishable keys, shared with the published-3MF overlay path.
// path (see libslic3r/PublishSettings.hpp).
const std::set<std::string>& denylist = publish_structural_keys(); const std::set<std::string>& denylist = publish_structural_keys();
// Base keys already added in the print/printer sections. Printer rows share // Base keys already added in the print/printer sections. Printer rows share this set:
// this set: a base key appears once (per-extruder "#N" variants collapse to // per-extruder "#N" variants collapse to the first occurrence (acceptable MVP; the
// the first occurrence - acceptable MVP; the per-extruder context is lost). // per-extruder context is lost in the UI).
std::set<std::string> added; std::set<std::string> added;
PresetBundle* bundle = wxGetApp().preset_bundle; PresetBundle* bundle = wxGetApp().preset_bundle;
@@ -198,17 +194,17 @@ void PublishSettingsDialog::build_option_model()
m_info_allsel = _L("All items selected..."); m_info_allsel = _L("All items selected...");
m_info_empty = _L("No matching items..."); m_info_empty = _L("No matching items...");
// Keep the tab order explicit: Section's enum order is Print, Printer, // Tab order differs from Section's enum order (Print, Printer, Material): the dialog
// Material, while the dialog presents Printer, Filament, Process. // presents Printer, Filament, Process.
m_sections.reserve(3); m_sections.reserve(3);
const Section tab_order[] = {Section::Printer, Section::Material, Section::Print}; const Section tab_order[] = {Section::Printer, Section::Material, Section::Print};
for (Section kind : tab_order) for (Section kind : tab_order)
section_group_for(kind); section_group_for(kind);
bind_tab_events(); bind_tab_events();
// Shared per-option label/value computation; returns false when the option // Shared per-option label/value computation; returns false when the option must be skipped
// must be skipped (denylisted / unknown / empty label). value is the pure // (denylisted / unknown / empty label). value is the stringified value; unit the translated
// stringified value; unit is the translated sidetext (may be empty). // sidetext (may be empty).
auto option_text = [&denylist, &full](const std::string& opt_id, const std::string& pure_key, wxString& label, wxString& value, auto option_text = [&denylist, &full](const std::string& opt_id, const std::string& pure_key, wxString& label, wxString& value,
wxString& unit) -> bool { wxString& unit) -> bool {
if (denylist.count(pure_key) > 0) if (denylist.count(pure_key) > 0)
@@ -224,9 +220,8 @@ void PublishSettingsDialog::build_option_model()
return true; return true;
}; };
// --- Phase 1: printer per-extruder retraction settings (displayed first, // --- Phase 1: printer per-extruder retraction settings (first, mirroring the sidebar's
// mirroring the sidebar's Printer group). The printer tab's // Printer group), from the printer tab's "Extruder"/"Extruder N" pages.
// "Extruder"/"Extruder N" pages carry the per-extruder retraction options.
{ {
size_t g = section_group_for(Section::Printer); size_t g = section_group_for(Section::Printer);
category_index_for(_L("Extruder"), Section::Printer, "custom-gcode_extruder", g, 0); category_index_for(_L("Extruder"), Section::Printer, "custom-gcode_extruder", g, 0);
@@ -238,18 +233,17 @@ void PublishSettingsDialog::build_option_model()
continue; continue;
const wxString page_title = Tab::translate_category(page->title(), tab->m_type); const wxString page_title = Tab::translate_category(page->title(), tab->m_type);
for (const ConfigOptionsGroupShp& optgroup : page->m_optgroups) { for (const ConfigOptionsGroupShp& optgroup : page->m_optgroups) {
// Allowlist on the untranslated optgroup title; the "Retraction // Allowlist on the untranslated optgroup title; the "Retraction when
// when switching material" group is intentionally skipped. // switching material" group is intentionally skipped.
if (optgroup->title != "Retraction" && optgroup->title != "Z-Hop") if (optgroup->title != "Retraction" && optgroup->title != "Z-Hop")
continue; continue;
const wxString subcategory = _(optgroup->title); const wxString subcategory = _(optgroup->title);
for (const auto& opt : optgroup->opt_map()) { for (const auto& opt : optgroup->opt_map()) {
const std::string& opt_id = opt.first; const std::string& opt_id = opt.first;
const std::string& pure_key = opt.second.first; const std::string& pure_key = opt.second.first;
// Per-extruder "#N" variants collapse to the first base key. The row stores // Per-extruder "#N" variants collapse to the first base key. The row
// the BASE key (whole-vector semantics on load: the size-guarded apply // stores the base key; GetPublishedKeys() later expands it back to one
// copies the author's full vector), while the "#0" opt_id is only used to // "#N" entry per extruder so the load side can apply per-extruder values.
// display the first extruder's value.
if (!added.insert(pure_key).second) if (!added.insert(pure_key).second)
continue; continue;
wxString label, value, unit; wxString label, value, unit;
@@ -264,8 +258,8 @@ void PublishSettingsDialog::build_option_model()
} }
} }
// --- Phase 2: per-material sections synthesized from the filament tab's // --- Phase 2: per-material sections synthesized from the filament tab's "Setting
// "Setting Overrides" page, under the Filament group. // Overrides" page, under the Filament group.
{ {
size_t g = section_group_for(Section::Material); size_t g = section_group_for(Section::Material);
Tab* filament_tab = nullptr; Tab* filament_tab = nullptr;
@@ -283,17 +277,16 @@ void PublishSettingsDialog::build_option_model()
} }
if (overrides_page != nullptr) { if (overrides_page != nullptr) {
// One section per filament slot: a 4-slot printer (e.g. 1 PLA + // One section per filament slot (a 4-slot printer shows 4 pages), each
// 3 PETG) shows 4 separate pages, each disambiguated internally by // disambiguated by its colour chip and slot identity while showing the bare name.
// its colour chip and slot identity while displaying the bare name.
for (size_t slot = 0; slot < bundle->filament_presets.size(); ++slot) { for (size_t slot = 0; slot < bundle->filament_presets.size(); ++slot) {
const PublishMaterialIdentity identity = material_identity(slot, full); const PublishMaterialIdentity identity = material_identity(slot, full);
const wxString title = material_title(slot, bundle, full); const wxString title = material_title(slot, bundle, full);
const size_t category_index = category_index_for(title, Section::Material, "custom-gcode_filament", g, slot, identity); const size_t category_index = category_index_for(title, Section::Material, "custom-gcode_filament", g, slot, identity);
// Filament-publishing-v2 rows: the author may require a filament colour and/or // Material requirement rows: an optional filament colour and/or a
// a vendor-agnostic material type for this slot. They live in their own // vendor-agnostic material type for this slot, in their own optgroup so they
// optgroup so they stay visually separated from the setting rows. // stay visually separated from the setting rows.
{ {
const size_t req_sub = subcategory_index_for(category_index, _L("Material"), "custom-gcode_filament"); const size_t req_sub = subcategory_index_for(category_index, _L("Material"), "custom-gcode_filament");
std::string hex; std::string hex;
@@ -309,18 +302,16 @@ void PublishSettingsDialog::build_option_model()
RowKind::Type); RowKind::Type);
} }
// A material section must not repeat a key; the same key may // A material section must not repeat a key; the same key may appear in other
// appear in other material sections - that is intended. // material sections - that is intended.
std::set<std::string> material_added; std::set<std::string> material_added;
for (const ConfigOptionsGroupShp& optgroup : overrides_page->m_optgroups) { for (const ConfigOptionsGroupShp& optgroup : overrides_page->m_optgroups) {
// Allowlist on the untranslated optgroup title; the // Allowlist on the untranslated optgroup title; "Ironing" is skipped.
// "Ironing" group is intentionally skipped.
if (optgroup->title != "Retraction" && optgroup->title != "Retraction when switching material") if (optgroup->title != "Retraction" && optgroup->title != "Retraction when switching material")
continue; continue;
for (const auto& opt : optgroup->opt_map()) { for (const auto& opt : optgroup->opt_map()) {
// Row keys are base keys (no "#N"): the load side // Row keys are base keys; the load side applies them positionally.
// matches the material and uses the author's slot.
const std::string& opt_id = opt.first; const std::string& opt_id = opt.first;
std::string base = opt_id.substr(0, opt_id.find('#')); std::string base = opt_id.substr(0, opt_id.find('#'));
if (!material_added.insert(base).second) if (!material_added.insert(base).second)
@@ -381,17 +372,15 @@ void PublishSettingsDialog::build_option_model()
} }
} }
// Pre-check the dirty (modified) settings and mark them bold. The base-key // Pre-check the dirty (modified) settings and mark them bold (base-key match, across all
// match covers all sections; collect_dirty_settings_keys already unions the // sections; collect_dirty_settings_keys unions the prints, printers and filaments).
// prints, printers and filaments of the bundle.
std::set<std::string> dirty_base; std::set<std::string> dirty_base;
for (const std::string& key : collect_dirty_settings_keys(*wxGetApp().preset_bundle)) { for (const std::string& key : collect_dirty_settings_keys(*wxGetApp().preset_bundle)) {
auto n = key.find('#'); auto n = key.find('#');
dirty_base.insert(n == std::string::npos ? key : key.substr(0, n)); dirty_base.insert(n == std::string::npos ? key : key.substr(0, n));
} }
for (Row& row : m_rows) { for (Row& row : m_rows) {
// The Color/Type requirement rows are not "dirty overrides": they are never // The Color/Type requirement rows are not "dirty overrides": never auto-checked.
// auto-checked by the dirty pre-check.
if (row.kind != RowKind::Setting) if (row.kind != RowKind::Setting)
continue; continue;
std::string base = row.key.substr(0, row.key.find('#')); std::string base = row.key.substr(0, row.key.find('#'));
@@ -402,8 +391,8 @@ void PublishSettingsDialog::build_option_model()
} }
} }
// Wire the "Full Publish" checkboxes: toggling one disables/enables the material's // Wire the "Full Publish" checkboxes: toggling one disables/enables the material's rows.
// rows. Bind by index so the lambda stays valid even if the vector is reallocated later. // Bind by index so the lambda stays valid even if the vector is reallocated later.
for (size_t c = 0; c < m_categories.size(); ++c) for (size_t c = 0; c < m_categories.size(); ++c)
if (m_categories[c].full_check != nullptr) if (m_categories[c].full_check != nullptr)
m_categories[c].full_check->Bind(wxEVT_CHECKBOX, [this, c](wxCommandEvent&) { on_full_toggle(c); }); m_categories[c].full_check->Bind(wxEVT_CHECKBOX, [this, c](wxCommandEvent&) { on_full_toggle(c); });
@@ -449,6 +438,7 @@ size_t PublishSettingsDialog::section_group_for(Section kind)
section.icon_name = "process"; section.icon_name = "process";
break; break;
} }
section.icon_bmp = ScalableBitmap(this, section.icon_name, 16);
constexpr long tab_style = wxTR_NO_BUTTONS | wxTR_HIDE_ROOT | wxTR_SINGLE | wxTR_NO_LINES | wxBORDER_NONE | wxWANTS_CHARS | constexpr long tab_style = wxTR_NO_BUTTONS | wxTR_HIDE_ROOT | wxTR_SINGLE | wxTR_NO_LINES | wxBORDER_NONE | wxWANTS_CHARS |
wxTR_FULL_ROW_HIGHLIGHT; wxTR_FULL_ROW_HIGHLIGHT;
@@ -466,7 +456,10 @@ size_t PublishSettingsDialog::section_group_for(Section kind)
page_sizer->Add(section.page_host, 1, wxEXPAND | wxTOP, FromDIP(4)); page_sizer->Add(section.page_host, 1, wxEXPAND | wxTOP, FromDIP(4));
section.page->SetSizer(page_sizer); section.page->SetSizer(page_sizer);
m_outer_tabs->AppendItem(section.title); if (section.icon_bmp.bmp().IsOk())
m_outer_tabs->AppendItem(section.title, section.icon_bmp.bmp());
else
m_outer_tabs->AppendItem(section.title);
m_outer_host_sizer->Add(section.page, 1, wxEXPAND); m_outer_host_sizer->Add(section.page, 1, wxEXPAND);
section.page->Hide(); section.page->Hide();
m_sections.push_back(std::move(section)); m_sections.push_back(std::move(section));
@@ -610,7 +603,7 @@ void PublishSettingsDialog::add_row_ui(const std::string& key,
auto* row_sizer = new wxBoxSizer(wxHORIZONTAL); auto* row_sizer = new wxBoxSizer(wxHORIZONTAL);
row_sizer->Add(current.check, 0, wxALIGN_CENTER_VERTICAL); row_sizer->Add(current.check, 0, wxALIGN_CENTER_VERTICAL);
// The value is read-only text (incl. the Type row: the published type is the slot's // The value is read-only text (incl. the Type row: the published type is the slot's
// normalized type, the author cannot pick a different one here). // normalized type, not author-editable).
current.value_label = new wxStaticText(category.scroll, wxID_ANY, value, wxDefaultPosition, wxDefaultSize, wxST_ELLIPSIZE_END); current.value_label = new wxStaticText(category.scroll, wxID_ANY, value, wxDefaultPosition, wxDefaultSize, wxST_ELLIPSIZE_END);
current.value_label->SetFont(Label::Body_13); current.value_label->SetFont(Label::Body_13);
current.value_label->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#262E30"))); current.value_label->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#262E30")));
@@ -643,8 +636,7 @@ void PublishSettingsDialog::on_full_toggle(size_t category_index)
void PublishSettingsDialog::set_row_bold(Row& row, bool bold) void PublishSettingsDialog::set_row_bold(Row& row, bool bold)
{ {
// Real set/clear: rebase on the dialog's body font so that clearing bold // Rebase on the dialog's body font so clearing bold restores the exact original font.
// restores the exact original font (the old CheckList::SetBold was one-way).
row.check->SetFont(bold ? Label::Body_13.Bold() : Label::Body_13); row.check->SetFont(bold ? Label::Body_13.Bold() : Label::Body_13);
} }
@@ -718,12 +710,11 @@ void PublishSettingsDialog::apply_filter(const wxString& filter_text)
Freeze(); Freeze();
wxString filter = filter_text.Lower(); wxString filter = filter_text.Lower();
// Pseudo filters (menu only): show only checked ("::sel") or only // Pseudo filters (menu only): show only checked ("::sel") or only unchecked ("::nonsel").
// unchecked ("::nonsel") rows.
const bool pseudo = (filter == "::sel" || filter == "::nonsel"); const bool pseudo = (filter == "::sel" || filter == "::nonsel");
m_fb_sizer->Show(!pseudo); m_fb_sizer->Show(!pseudo);
// Update row matches first; page and optgroup visibility is applied below. // Row matches are computed first; page and optgroup visibility is applied below.
if (pseudo) { if (pseudo) {
if (m_filter_ctrl->GetValue().Lower() != filter) { if (m_filter_ctrl->GetValue().Lower() != filter) {
m_filter_ctrl->ChangeValue(filter); m_filter_ctrl->ChangeValue(filter);
@@ -808,8 +799,7 @@ void PublishSettingsDialog::apply_visibility()
void PublishSettingsDialog::select_all(bool value) void PublishSettingsDialog::select_all(bool value)
{ {
// "All" does not auto-enable gated material sections; "None" leaves a gated // "All" skips disabled (gated) rows; "None" leaves a gated row's preserved value.
// row's preserved value untouched.
for (Row& row : m_rows) for (Row& row : m_rows)
if (row.check->IsEnabled()) if (row.check->IsEnabled())
row.check->SetValue(value); row.check->SetValue(value);
@@ -829,19 +819,18 @@ bool PublishSettingsDialog::row_is_visible(const Row& row) const
void PublishSettingsDialog::select_visible(bool value) void PublishSettingsDialog::select_visible(bool value)
{ {
wxString filter = m_filter_ctrl->GetValue().Lower(); wxString filter = m_filter_ctrl->GetValue().Lower();
// In a pseudo-filter view the rows being toggled would all disappear; // In a pseudo-filter view the rows being toggled would all disappear; drop the filter
// drop the filter afterwards so the result stays visible. // afterwards so the result stays visible.
bool clear_pseudo = (!value && filter == "::nonsel") || (value && filter == "::sel"); bool clear_pseudo = (!value && filter == "::nonsel") || (value && filter == "::sel");
// Toggle the rows that are visible under the *current* filter. // Toggle the rows visible under the *current* filter.
for (Row& row : m_rows) for (Row& row : m_rows)
if (row_is_visible(row)) if (row_is_visible(row))
row.check->SetValue(value); row.check->SetValue(value);
if (clear_pseudo) { if (clear_pseudo) {
// Note: SetValue() may fire wxEVT_TEXT on some platforms, which // Note: SetValue() may fire wxEVT_TEXT on some platforms, re-entering apply_filter() -
// re-enters apply_filter() - that is fine, the rows above were already // that is fine; the rows above were already toggled and the trailing call is idempotent.
// toggled and the trailing call below is idempotent.
m_filter_ctrl->ChangeValue(""); m_filter_ctrl->ChangeValue("");
apply_filter(""); // resync visibility and the All/None bar apply_filter(""); // resync visibility and the All/None bar
} }
@@ -895,12 +884,31 @@ void PublishSettingsDialog::show_menu(wxMouseEvent& evt)
std::vector<std::string> PublishSettingsDialog::GetPublishedKeys() const std::vector<std::string> PublishSettingsDialog::GetPublishedKeys() const
{ {
std::vector<std::string> out; std::vector<std::string> out;
// Process and printer sections both travel through published_keys (the load-side // Process and printer sections both travel through published_keys (the load-side overlay
// overlay applies process keys to the prints edited preset and the allowlisted // applies process keys to the prints edited preset and the allowlisted printer keys to the
// printer keys to the printers edited preset). Material keys use a separate API. // printers edited preset); material keys use a separate API.
for (const Row& row : m_rows) const DynamicPrintConfig full = wxGetApp().preset_bundle->full_config();
if ((row.section == Section::Print || row.section == Section::Printer) && row.check->GetValue()) for (const Row& row : m_rows) {
if ((row.section != Section::Print && row.section != Section::Printer) || !row.check->GetValue())
continue;
if (row.section == Section::Printer) {
// Printer rows store the base key (per-extruder "#N" variants collapsed during
// build). Publish every extruder element so the load side can apply per-extruder
// values even when the receiver has a different extruder count; a scalar printer
// key is published as-is.
const std::string base_key = row.key.substr(0, row.key.find('#'));
if (const ConfigOption* opt = full.option(base_key)) {
if (const auto* vec = dynamic_cast<const ConfigOptionVectorBase*>(opt)) {
for (size_t i = 0; i < vec->size(); ++i)
out.push_back(base_key + "#" + std::to_string(i));
} else {
out.push_back(base_key);
}
}
} else {
out.push_back(row.key); out.push_back(row.key);
}
}
return out; return out;
} }
@@ -915,8 +923,15 @@ std::vector<Slic3r::PublishedMaterialEntry> PublishSettingsDialog::GetPublishedM
entry.filament_vendor = cat.filament_vendor; entry.filament_vendor = cat.filament_vendor;
entry.filament_id = cat.filament_id; entry.filament_id = cat.filament_id;
entry.slot = static_cast<int>(cat.filament_slot); entry.slot = static_cast<int>(cat.filament_slot);
// "Full Publish": the entire filament preset of the slot is embedded; type and color // The author's preset id distinguishes exact variants that share filament_id
// are implicitly published, and the per-key rows are disabled and their state is ignored. // ("Generic PLA" vs "Generic PLA Matte"), so the receiver can match precisely.
PresetBundle *bundle = wxGetApp().preset_bundle;
if (bundle != nullptr && cat.filament_slot < bundle->filament_presets.size()) {
if (const Preset *preset = bundle->filaments.find_preset(bundle->filament_presets[cat.filament_slot], false, true))
entry.setting_id = preset->setting_id;
}
// "Full Publish": the whole filament preset is embedded; type and colour are implicitly
// published, and the per-key rows are disabled / their state ignored.
if (cat.full_check != nullptr && cat.full_check->GetValue()) { if (cat.full_check != nullptr && cat.full_check->GetValue()) {
entry.full = true; entry.full = true;
entry.full_keys = full_keys_for_slot(); entry.full_keys = full_keys_for_slot();
@@ -946,8 +961,7 @@ std::vector<Slic3r::PublishedMaterialEntry> PublishSettingsDialog::GetPublishedM
entry.keys.push_back(row.key); entry.keys.push_back(row.key);
} }
} }
// A material with only setting keys but none checked, or with nothing selected at all, // Nothing checked at all -> nothing to write.
// carries no information for the writer.
if (!entry.keys.empty() || entry.publish_type || entry.publish_color) if (!entry.keys.empty() || entry.publish_type || entry.publish_color)
out.push_back(std::move(entry)); out.push_back(std::move(entry));
} }
@@ -956,9 +970,9 @@ std::vector<Slic3r::PublishedMaterialEntry> PublishSettingsDialog::GetPublishedM
std::vector<std::string> PublishSettingsDialog::full_keys_for_slot() const std::vector<std::string> PublishSettingsDialog::full_keys_for_slot() const
{ {
// The canonical filament preset keys, minus the structural keys the published overlay must // The canonical filament preset keys minus the structural keys the published overlay must
// never touch (inherits, compatibility, *_settings_id, ...), plus filament_colour (not a // never touch (inherits, compatibility, *_settings_id, ...), plus filament_colour (not a
// member of Preset::filament_options). The values travel in the exported config, masked to // member of Preset::filament_options). Values travel in the exported config, masked to
// this slot, and are applied on load onto the receiver's slot. // this slot, and are applied on load onto the receiver's slot.
const std::set<std::string>& denylist = publish_structural_keys(); const std::set<std::string>& denylist = publish_structural_keys();
std::vector<std::string> keys; std::vector<std::string> keys;
@@ -971,7 +985,7 @@ std::vector<std::string> PublishSettingsDialog::full_keys_for_slot() const
void PublishSettingsDialog::on_dpi_changed(const wxRect& suggested_rect) void PublishSettingsDialog::on_dpi_changed(const wxRect& suggested_rect)
{ {
// Rescale toolbar bitmaps and icons; collapse chevrons are vector-drawn and repaint themselves. // Rescale toolbar bitmaps and icons; collapse chevrons are vector-drawn and repaint.
m_search.msw_rescale(); m_search.msw_rescale();
m_menu.msw_rescale(); m_menu.msw_rescale();
m_filter_box->SetIcon(m_search.bmp()); m_filter_box->SetIcon(m_search.bmp());
@@ -1000,8 +1014,13 @@ void PublishSettingsDialog::on_dpi_changed(const wxRect& suggested_rect)
cat.list_sizer->Layout(); cat.list_sizer->Layout();
} }
for (SectionGroup& section : m_sections) for (size_t s = 0; s < m_sections.size(); ++s) {
SectionGroup& section = m_sections[s];
section.icon_bmp.msw_rescale();
if (section.icon_bmp.bmp().IsOk())
m_outer_tabs->SetItemBitmap(s, section.icon_bmp.bmp());
section.tabs->Rescale(); section.tabs->Rescale();
}
// Refresh the per-row Color chips at the new DPI. // Refresh the per-row Color chips at the new DPI.
for (Row& row : m_rows) { for (Row& row : m_rows) {
+14 -17
View File
@@ -28,25 +28,21 @@ struct PublishMaterialIdentity
std::string id; std::string id;
}; };
// Dialog that lets a model author select which settings get embedded in a 3MF. // Dialog letting a model author select which settings get embedded in a 3MF. Nested tab layout
// Settings are grouped into the same nested custom tab layout used by the // mirroring the Process settings (Printer / Filament / Process outer tabs, category or material
// Process settings: Printer, Filament, and Process outer tabs, with category or // tabs inside each). Dirty settings are pre-checked and shown bold; on OK the print rows become
// material tabs inside each section. Optgroups are ordinary grouped headers. // "published_keys" and the material rows become "published_material_keys".
// Modified (dirty) settings are pre-checked and shown bold. On OK, the print
// rows become the "published_keys" list and the material rows become the
// per-material "published_material_keys".
class PublishSettingsDialog : public DPIDialog class PublishSettingsDialog : public DPIDialog
{ {
public: public:
PublishSettingsDialog(wxWindow* parent = nullptr); PublishSettingsDialog(wxWindow* parent = nullptr);
~PublishSettingsDialog(); ~PublishSettingsDialog();
// The selected print-section setting keys (in display order). Keys may // The selected print/printer setting keys (in display order); printer keys carry a '#N'
// contain '#'. // per-extruder suffix.
std::vector<std::string> GetPublishedKeys() const; std::vector<std::string> GetPublishedKeys() const;
// The selected keys grouped per material: one entry per material section // The selected keys grouped per material section (base keys, no '#N' suffix).
// with at least one checked key. Keys are base keys (no "#N" suffix).
std::vector<Slic3r::PublishedMaterialEntry> GetPublishedMaterialKeys() const; std::vector<Slic3r::PublishedMaterialEntry> GetPublishedMaterialKeys() const;
protected: protected:
@@ -56,9 +52,9 @@ private:
// Which part of the settings the row/category came from. // Which part of the settings the row/category came from.
enum class Section { Print, Printer, Material }; enum class Section { Print, Printer, Material };
// One selectable setting row: a checkbox (setting name) plus a value label // One selectable setting row: a checkbox (setting name) plus a value label and an optional
// and a (optional) grey unit label. key is the full config key and may carry // grey unit label. key is the full config key, possibly with a "#N" variant suffix
// a "#N" variant suffix (print/printer rows); material rows carry the base key. // (print/printer rows); material rows carry the base key.
enum class RowKind { enum class RowKind {
Setting, // a regular setting key Setting, // a regular setting key
Color, // material colour requirement (filament_colour) Color, // material colour requirement (filament_colour)
@@ -113,9 +109,9 @@ private:
ScalableBitmap icon_bmp; // scalable bitmap for DPI changes ScalableBitmap icon_bmp; // scalable bitmap for DPI changes
wxStaticBitmap* icon{nullptr}; wxStaticBitmap* icon{nullptr};
wxStaticBitmap* filament_color_chip{nullptr}; wxStaticBitmap* filament_color_chip{nullptr};
wxStaticText* title_label{nullptr}; // material title (static text, Full Publish carries the label elsewhere) wxStaticText* title_label{nullptr}; // material title (static text; Full Publish carries the label elsewhere)
// "Full Publish": serializing the entire filament preset of this slot. While checked, // "Full Publish": while checked, the whole slot preset is serialized and its rows
// the slot's rows (incl. Color/Type) are disabled. // (incl. Color/Type) are disabled.
bool full{false}; bool full{false};
wxCheckBox* full_check{nullptr}; wxCheckBox* full_check{nullptr};
// Material identity, only for Section::Material categories. // Material identity, only for Section::Material categories.
@@ -134,6 +130,7 @@ private:
wxString title; // _L("Printer") / _L("Filament") / _L("Process") wxString title; // _L("Printer") / _L("Filament") / _L("Process")
Section kind{Section::Print}; // maps 1:1 to the display group Section kind{Section::Print}; // maps 1:1 to the display group
std::string icon_name; // "printer" / "filament" / "process" std::string icon_name; // "printer" / "filament" / "process"
ScalableBitmap icon_bmp; // tab icon next to the title; rescaled on DPI change
wxPanel* page{nullptr}; wxPanel* page{nullptr};
TabCtrl* tabs{nullptr}; TabCtrl* tabs{nullptr};
wxPanel* page_host{nullptr}; wxPanel* page_host{nullptr};
+23 -28
View File
@@ -501,12 +501,10 @@ SCENARIO("Nozzle-group metadata .3mf round-trip", "[3mf][MultiNozzle]") {
} }
} }
// The "Publish" feature stores a published flag plus a JSON array of author-selected setting keys // Locks the serialization contract of the "Publish" metadata: the published flag and the
// in model.model_info->metadata_items. This locks the serialization contract: both keys must survive // published_keys JSON array in model.model_info->metadata_items must survive a store_bbs_3mf ->
// a store_bbs_3mf -> load_bbs_3mf round-trip unchanged. (The full preset-preservation behavior — // load_bbs_3mf round-trip unchanged. (The full preset-preservation behavior is exercised
// keeping the user's currently-selected presets and overlaying only the published keys onto them — // headlessly in test_preset_bundle_loading.cpp.)
// is exercised headlessly in "Published 3MF overlays only the author-selected process keys onto the
// edited preset" in test_preset_bundle_loading.cpp.)
SCENARIO("Published 3MF round-trips the published flag and published_keys metadata", "[3mf]") { SCENARIO("Published 3MF round-trips the published flag and published_keys metadata", "[3mf]") {
GIVEN("a model carrying published metadata") { GIVEN("a model carrying published metadata") {
Model model; Model model;
@@ -518,8 +516,8 @@ SCENARIO("Published 3MF round-trips the published flag and published_keys metada
model.model_info->metadata_items["published"] = "1"; model.model_info->metadata_items["published"] = "1";
model.model_info->metadata_items["published_keys"] = R"(["layer_height","wall_thickness"])"; model.model_info->metadata_items["published_keys"] = R"(["layer_height","wall_thickness"])";
// store_bbs_3mf stages Metadata/project_settings.config through the model's backup path; // store_bbs_3mf stages project_settings.config through the model's backup path; point
// point it at a writable temp dir (the default lives under a read-only root in CI). // it at a writable temp dir (the default lives under a read-only root in CI).
ScopedTemporaryDir backup_dir("orca_pub"); ScopedTemporaryDir backup_dir("orca_pub");
model.set_backup_path(backup_dir.string()); model.set_backup_path(backup_dir.string());
@@ -564,8 +562,8 @@ SCENARIO("Published 3MF round-trips the published flag and published_keys metada
} }
} }
// A project saved without the Publish metadata (i.e. a normal 3MF) must load identically: the // A normal 3MF (no Publish metadata) must load identically: the loader must not fabricate a
// loader must not fabricate a "published" flag or published_keys for files that never carried them. // "published" flag or published_keys for files that never carried them.
SCENARIO("Legacy 3MF without published metadata loads unchanged", "[3mf]") { SCENARIO("Legacy 3MF without published metadata loads unchanged", "[3mf]") {
GIVEN("a model without any published metadata") { GIVEN("a model without any published metadata") {
Model model; Model model;
@@ -610,10 +608,8 @@ SCENARIO("Legacy 3MF without published metadata loads unchanged", "[3mf]") {
} }
} }
// The "Publish" feature can also store material-qualified setting keys, one entry per material // Locks the serialization contract of the published_material_keys metadata: the per-entry JSON
// the author uses, in model.model_info->metadata_items. This locks the serialization contract // must survive a store_bbs_3mf -> load_bbs_3mf round-trip verbatim, exactly like published_keys.
// for that entry list: the JSON must survive a store_bbs_3mf -> load_bbs_3mf round-trip
// verbatim, exactly like the plain published_keys array.
SCENARIO("Published 3MF round-trips the published_material_keys metadata", "[3mf]") { SCENARIO("Published 3MF round-trips the published_material_keys metadata", "[3mf]") {
GIVEN("a model carrying published material keys metadata") { GIVEN("a model carrying published material keys metadata") {
Model model; Model model;
@@ -658,8 +654,7 @@ SCENARIO("Published 3MF round-trips the published_material_keys metadata", "[3mf
REQUIRE(dst_model.model_info->metadata_items["published_material_keys"] == material_keys_json); REQUIRE(dst_model.model_info->metadata_items["published_material_keys"] == material_keys_json);
// The value must parse back to one material entry carrying the nested identity // The value must parse back to one material entry carrying the nested identity
// object, the author slot ordinal and the key list, so the loader can match it // object, the author slot ordinal and the key list.
// to the receiver's filaments.
nlohmann::json entries = nlohmann::json::parse(material_keys_json); nlohmann::json entries = nlohmann::json::parse(material_keys_json);
REQUIRE(entries.is_array()); REQUIRE(entries.is_array());
REQUIRE(entries.size() == 1); REQUIRE(entries.size() == 1);
@@ -689,19 +684,19 @@ SCENARIO("Minimal published 3MF serialization filters config and omits embedded
const std::vector<std::string> published_keys = { "layer_height", "retraction_length" }; const std::vector<std::string> published_keys = { "layer_height", "retraction_length" };
const std::vector<PublishedMaterialEntry> material_keys = { const std::vector<PublishedMaterialEntry> material_keys = {
{ "PLA", "Generic", "GFL99", 0, { "filament_retraction_length" } } { "PLA", "Generic", "GFL99", "", 0, { "filament_retraction_length" } }
}; };
DynamicPrintConfig filtered_cfg = filter_published_config(full_cfg, published_keys, material_keys); DynamicPrintConfig filtered_cfg = filter_published_config(full_cfg, published_keys, material_keys);
// Filtered config must contain the published keys and identity keys // Filtered config keeps the published and identity keys...
REQUIRE(filtered_cfg.option("layer_height") != nullptr); REQUIRE(filtered_cfg.option("layer_height") != nullptr);
REQUIRE(filtered_cfg.option("retraction_length") != nullptr); REQUIRE(filtered_cfg.option("retraction_length") != nullptr);
REQUIRE(filtered_cfg.option("filament_colour") != nullptr); REQUIRE(filtered_cfg.option("filament_colour") != nullptr);
REQUIRE(filtered_cfg.option("filament_type") != nullptr); REQUIRE(filtered_cfg.option("filament_type") != nullptr);
REQUIRE(filtered_cfg.option("wipe_tower_x") != nullptr); REQUIRE(filtered_cfg.option("wipe_tower_x") != nullptr);
// Non-published settings should NOT be in filtered_cfg // ...and drops everything else.
REQUIRE(filtered_cfg.option("sparse_infill_density") == nullptr); REQUIRE(filtered_cfg.option("sparse_infill_density") == nullptr);
REQUIRE(filtered_cfg.option("machine_start_gcode") == nullptr); REQUIRE(filtered_cfg.option("machine_start_gcode") == nullptr);
@@ -716,7 +711,7 @@ SCENARIO("Minimal published 3MF serialization filters config and omits embedded
ScopedTemporaryFile temp(".3mf"); ScopedTemporaryFile temp(".3mf");
const std::string test_file = temp.string(); const std::string test_file = temp.string();
// Create a fake project preset to verify it gets omitted with MinimalPublished // Create a fake project preset to verify MinimalPublished omits it.
Preset preset(Preset::TYPE_PRINT, "TestPrintPreset"); Preset preset(Preset::TYPE_PRINT, "TestPrintPreset");
preset.config = full_cfg; preset.config = full_cfg;
std::vector<Preset*> project_presets = { &preset }; std::vector<Preset*> project_presets = { &preset };
@@ -753,9 +748,9 @@ SCENARIO("Minimal published 3MF serialization filters config and omits embedded
} }
} }
// Filament-publishing v2: a "full publish" entry carries the whole slot's key list. Its vector // A "full publish" entry carries the whole slot's key list: its vector options keep only the
// options keep only the author's slot value; the other slots are masked to their defaults so a // author's slot value, the other slots are masked to their defaults so a slot-1 full publish
// slot-1 full publish does not leak slot 0's data into the file. // does not leak slot 0's data into the file.
SCENARIO("Full-publish entries filter the whole slot and mask the other slots", "[3mf]") { SCENARIO("Full-publish entries filter the whole slot and mask the other slots", "[3mf]") {
GIVEN("a full print configuration with two filament slots") { GIVEN("a full print configuration with two filament slots") {
DynamicPrintConfig full_cfg = DynamicPrintConfig::full_print_config(); DynamicPrintConfig full_cfg = DynamicPrintConfig::full_print_config();
@@ -787,9 +782,9 @@ SCENARIO("Full-publish entries filter the whole slot and mask the other slots",
} }
} }
// Filament-publishing v2: the extended per-entry fields (full dump list, published type and // The extended per-entry fields (full dump list, published type and colour) travel inside the
// colour) travel inside the published_material_keys metadata and round-trip unchanged. // published_material_keys metadata and round-trip unchanged.
SCENARIO("Published 3MF round-trips the filament-publishing-v2 material metadata", "[3mf]") { SCENARIO("Published 3MF round-trips the extended material metadata", "[3mf]") {
GIVEN("a model carrying extended published material keys metadata") { GIVEN("a model carrying extended published material keys metadata") {
Model model; Model model;
std::string src_file = std::string(TEST_DATA_DIR) + "/test_3mf/Prusa.stl"; std::string src_file = std::string(TEST_DATA_DIR) + "/test_3mf/Prusa.stl";
@@ -797,7 +792,7 @@ SCENARIO("Published 3MF round-trips the filament-publishing-v2 material metadata
model.add_default_instances(); model.add_default_instances();
const std::string material_keys_json = const std::string material_keys_json =
R"([{"material":{"filament_type":"PLA","filament_vendor":"Generic","filament_id":"GFL99"},"slot":1,"keys":[],"full":true,"full_keys":["filament_retraction_length","filament_colour"],"publish_type":true,"type":"PLA","publish_color":false,"color":""}])"; R"([{"material":{"filament_type":"PLA","filament_vendor":"Generic","filament_id":"GFL99","setting_id":"RFs9eCKYOMUSmvZf"},"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 = std::make_shared<ModelInfo>();
model.model_info->metadata_items["published_material_keys"] = material_keys_json; model.model_info->metadata_items["published_material_keys"] = material_keys_json;
@@ -832,7 +827,7 @@ SCENARIO("Published 3MF round-trips the filament-publishing-v2 material metadata
REQUIRE(dst_model.model_info != nullptr); REQUIRE(dst_model.model_info != nullptr);
REQUIRE(dst_model.model_info->metadata_items["published_material_keys"] == material_keys_json); REQUIRE(dst_model.model_info->metadata_items["published_material_keys"] == material_keys_json);
// The value must parse back with every filament-publishing-v2 field intact. // The value must parse back with every extended field intact.
nlohmann::json entries = nlohmann::json::parse(material_keys_json); nlohmann::json entries = nlohmann::json::parse(material_keys_json);
REQUIRE(entries.is_array()); REQUIRE(entries.is_array());
REQUIRE(entries.size() == 1); REQUIRE(entries.size() == 1);
File diff suppressed because it is too large Load Diff