mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-12 18:31:18 +00:00
Compare commits
39
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
67eee11e25 | ||
|
|
253f90c975 | ||
|
|
0cec10748e | ||
|
|
c55f9c2251 | ||
|
|
d2675f15d1 | ||
|
|
5371c85362 | ||
|
|
a243941b72 | ||
|
|
6a1a5d51e9 | ||
|
|
e625845b34 | ||
|
|
01b340dee1 | ||
|
|
4c2a63e11e | ||
|
|
f8cf34c55b | ||
|
|
00d11a7ba0 | ||
|
|
546b9c621d | ||
|
|
7b717ffa7d | ||
|
|
f42173a19f | ||
|
|
b99a9a06f3 | ||
|
|
faa64ab089 | ||
|
|
987b1b2b63 | ||
|
|
9bd4c71a19 | ||
|
|
3c646f305c | ||
|
|
432d6be518 | ||
|
|
3e7f0c027a | ||
|
|
504a143b98 | ||
|
|
a87639ac41 | ||
|
|
ecbd03fed7 | ||
|
|
ccbb73baaf | ||
|
|
029bc7928f | ||
|
|
0a8fce0913 | ||
|
|
3942ba7005 | ||
|
|
0110572a77 | ||
|
|
2c2869ab07 | ||
|
|
bf63eb1226 | ||
|
|
38e57888a5 | ||
|
|
fc079b2e5b | ||
|
|
a88843c6f8 | ||
|
|
a13089d357 | ||
|
|
fb6c93963d | ||
|
|
7688f7a16c |
@@ -131,6 +131,13 @@
|
||||
<true/>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>${MACOSX_BUNDLE_COPYRIGHT}</string>
|
||||
<key>NSLocalNetworkUsageDescription</key>
|
||||
<string>OrcaSlicer uses your local network to discover and connect to printers.</string>
|
||||
<key>NSBonjourServices</key>
|
||||
<array>
|
||||
<string>_octoprint._tcp</string>
|
||||
<string>_http._tcp</string>
|
||||
</array>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<!-- Disable App Transport Security. Resolves https://github.com/OrcaSlicer/OrcaSlicer/issues/791 -->
|
||||
|
||||
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-10-11 16:21+0800\n"
|
||||
"POT-Creation-Date: 2026-10-12 19:00+0800\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
@@ -3999,6 +3999,15 @@ msgstr ""
|
||||
msgid "Go to Plate"
|
||||
msgstr ""
|
||||
|
||||
msgid "Device"
|
||||
msgstr ""
|
||||
|
||||
msgid "Device (Web)"
|
||||
msgstr ""
|
||||
|
||||
msgid "Multi-device"
|
||||
msgstr ""
|
||||
|
||||
msgid "AMS Materials Setting"
|
||||
msgstr ""
|
||||
|
||||
@@ -4047,6 +4056,9 @@ msgstr ""
|
||||
msgid "You need to select the material type and color first."
|
||||
msgstr ""
|
||||
|
||||
msgid "The minimum temperature cannot be greater than the maximum temperature."
|
||||
msgstr ""
|
||||
|
||||
#, possible-c-format, possible-boost-format
|
||||
msgid "Please input a valid value (K in %.1f~%.1f)"
|
||||
msgstr ""
|
||||
@@ -4055,6 +4067,12 @@ msgstr ""
|
||||
msgid "Please input a valid value (K in %.1f~%.1f, N in %.1f~%.1f)"
|
||||
msgstr ""
|
||||
|
||||
msgid "Favorite filaments"
|
||||
msgstr ""
|
||||
|
||||
msgid "All filaments"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"The nozzle flow is not set. Please set the nozzle flow rate before editing the filament.\n"
|
||||
"'Device -> Print parts'"
|
||||
@@ -5985,18 +6003,9 @@ msgstr ""
|
||||
msgid "Preview"
|
||||
msgstr ""
|
||||
|
||||
msgid "Device"
|
||||
msgstr ""
|
||||
|
||||
msgid "Multi-device"
|
||||
msgstr ""
|
||||
|
||||
msgid "Project"
|
||||
msgstr ""
|
||||
|
||||
msgid "Device (Web)"
|
||||
msgstr ""
|
||||
|
||||
msgid "Yes"
|
||||
msgstr ""
|
||||
|
||||
@@ -6670,6 +6679,22 @@ msgstr ""
|
||||
msgid "File '%s' was lost! Please download it again."
|
||||
msgstr ""
|
||||
|
||||
msgid "gcodes"
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to delete the file from printer."
|
||||
msgstr ""
|
||||
|
||||
#, possible-c-format, possible-boost-format
|
||||
msgid "Do you want to print the file '%s' from printer?"
|
||||
msgstr ""
|
||||
|
||||
msgid "Print file"
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to start the print job."
|
||||
msgstr ""
|
||||
|
||||
#, possible-c-format, possible-boost-format
|
||||
msgid ""
|
||||
"File: %s\n"
|
||||
@@ -10342,6 +10367,12 @@ msgstr ""
|
||||
msgid "Clean Up"
|
||||
msgstr ""
|
||||
|
||||
msgid "AMS filament mapping is not available for this printer. Clear the AMS mapping before printing."
|
||||
msgstr ""
|
||||
|
||||
msgid "A filament used by this print has no AMS mapping. Assign it before printing."
|
||||
msgstr ""
|
||||
|
||||
msgid "Preparing print job"
|
||||
msgstr ""
|
||||
|
||||
@@ -27207,6 +27238,9 @@ msgstr ""
|
||||
msgid "An error occurred loading %s"
|
||||
msgstr ""
|
||||
|
||||
msgid "/"
|
||||
msgstr ""
|
||||
|
||||
#, possible-boost-format
|
||||
msgid "Load %1% to %2%"
|
||||
msgstr ""
|
||||
|
||||
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: \n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-10-11 16:21+0800\n"
|
||||
"POT-Creation-Date: 2026-10-12 19:00+0800\n"
|
||||
"PO-Revision-Date: 2025-03-15 10:55+0100\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: \n"
|
||||
@@ -4375,6 +4375,16 @@ msgstr "Projectes recents"
|
||||
msgid "Go to Plate"
|
||||
msgstr "Anar a la placa"
|
||||
|
||||
msgid "Device"
|
||||
msgstr "Dispositiu"
|
||||
|
||||
# AI Translated
|
||||
msgid "Device (Web)"
|
||||
msgstr "Dispositiu (Web)"
|
||||
|
||||
msgid "Multi-device"
|
||||
msgstr "Multidispositiu"
|
||||
|
||||
msgid "AMS Materials Setting"
|
||||
msgstr "Configuració de materials AMS"
|
||||
|
||||
@@ -4423,6 +4433,9 @@ msgstr "Estàs segur que vols esborrar la informació del filament?"
|
||||
msgid "You need to select the material type and color first."
|
||||
msgstr "Primer heu de seleccionar el tipus de material i el color."
|
||||
|
||||
msgid "The minimum temperature cannot be greater than the maximum temperature."
|
||||
msgstr ""
|
||||
|
||||
#, c-format, boost-format
|
||||
msgid "Please input a valid value (K in %.1f~%.1f)"
|
||||
msgstr "Introduïu un valor vàlid (K a %.1f~%.1f)"
|
||||
@@ -4431,6 +4444,12 @@ msgstr "Introduïu un valor vàlid (K a %.1f~%.1f)"
|
||||
msgid "Please input a valid value (K in %.1f~%.1f, N in %.1f~%.1f)"
|
||||
msgstr "Introduïu un valor vàlid (K a %.1f~%.1f, N a %.1f~%.1f)"
|
||||
|
||||
msgid "Favorite filaments"
|
||||
msgstr ""
|
||||
|
||||
msgid "All filaments"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"The nozzle flow is not set. Please set the nozzle flow rate before editing the filament.\n"
|
||||
"'Device -> Print parts'"
|
||||
@@ -6506,19 +6525,9 @@ msgstr "Preparació"
|
||||
msgid "Preview"
|
||||
msgstr "Previsualització"
|
||||
|
||||
msgid "Device"
|
||||
msgstr "Dispositiu"
|
||||
|
||||
msgid "Multi-device"
|
||||
msgstr "Multidispositiu"
|
||||
|
||||
msgid "Project"
|
||||
msgstr "Projecte"
|
||||
|
||||
# AI Translated
|
||||
msgid "Device (Web)"
|
||||
msgstr "Dispositiu (Web)"
|
||||
|
||||
msgid "Yes"
|
||||
msgstr "Sí"
|
||||
|
||||
@@ -7225,6 +7234,22 @@ msgstr "El fitxer .gcode.3mf no conté dades de G-code. Lamineu-lo amb Orca Slic
|
||||
msgid "File '%s' was lost! Please download it again."
|
||||
msgstr "L'arxiu '%s' s'ha perdut! Si us plau, descarregueu-lo de nou."
|
||||
|
||||
msgid "gcodes"
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to delete the file from printer."
|
||||
msgstr ""
|
||||
|
||||
#, c-format, boost-format
|
||||
msgid "Do you want to print the file '%s' from printer?"
|
||||
msgstr ""
|
||||
|
||||
msgid "Print file"
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to start the print job."
|
||||
msgstr ""
|
||||
|
||||
#, c-format, boost-format
|
||||
msgid ""
|
||||
"File: %s\n"
|
||||
@@ -11411,6 +11436,12 @@ msgstr "Cancel·lar el timelapse i imprimir"
|
||||
msgid "Clean Up"
|
||||
msgstr "Netejar"
|
||||
|
||||
msgid "AMS filament mapping is not available for this printer. Clear the AMS mapping before printing."
|
||||
msgstr ""
|
||||
|
||||
msgid "A filament used by this print has no AMS mapping. Assign it before printing."
|
||||
msgstr ""
|
||||
|
||||
msgid "Preparing print job"
|
||||
msgstr "Preparant el treball d'impressió"
|
||||
|
||||
@@ -31518,6 +31549,9 @@ msgstr "Activar les eines de desenvolupament"
|
||||
msgid "An error occurred loading %s"
|
||||
msgstr "S'ha produït un error en carregar %s"
|
||||
|
||||
msgid "/"
|
||||
msgstr "/"
|
||||
|
||||
# AI Translated
|
||||
#, boost-format
|
||||
msgid "Load %1% to %2%"
|
||||
@@ -33585,9 +33619,6 @@ msgstr ""
|
||||
#~ msgid "An error occurred loading "
|
||||
#~ msgstr "S'ha produït un error en carregar "
|
||||
|
||||
#~ msgid "/"
|
||||
#~ msgstr "/"
|
||||
|
||||
# AI Translated
|
||||
#, c-format, boost-format
|
||||
#~ msgid "Load %s to "
|
||||
|
||||
@@ -3,7 +3,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Orca Slicer\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-10-11 16:21+0800\n"
|
||||
"POT-Creation-Date: 2026-10-12 19:00+0800\n"
|
||||
"PO-Revision-Date: \n"
|
||||
"Last-Translator: Jakub Hencl\n"
|
||||
"Language-Team: \n"
|
||||
@@ -4331,6 +4331,16 @@ msgstr "Nedávné projekty"
|
||||
msgid "Go to Plate"
|
||||
msgstr "Přejít na desku"
|
||||
|
||||
msgid "Device"
|
||||
msgstr "Zařízení"
|
||||
|
||||
# AI Translated
|
||||
msgid "Device (Web)"
|
||||
msgstr "Zařízení (Web)"
|
||||
|
||||
msgid "Multi-device"
|
||||
msgstr "Více zařízení"
|
||||
|
||||
msgid "AMS Materials Setting"
|
||||
msgstr "Nastavení materiálů AMS"
|
||||
|
||||
@@ -4381,6 +4391,9 @@ msgstr "Opravdu chcete vymazat informace o filamentu?"
|
||||
msgid "You need to select the material type and color first."
|
||||
msgstr "Nejprve musíte vybrat typ materiálu a barvu."
|
||||
|
||||
msgid "The minimum temperature cannot be greater than the maximum temperature."
|
||||
msgstr ""
|
||||
|
||||
#, c-format, boost-format
|
||||
msgid "Please input a valid value (K in %.1f~%.1f)"
|
||||
msgstr "Zadejte platnou hodnotu (K v rozsahu %.1f~%.1f)"
|
||||
@@ -4389,6 +4402,12 @@ msgstr "Zadejte platnou hodnotu (K v rozsahu %.1f~%.1f)"
|
||||
msgid "Please input a valid value (K in %.1f~%.1f, N in %.1f~%.1f)"
|
||||
msgstr "Zadejte platnou hodnotu (K v rozsahu %.1f~%.1f, N v rozsahu %.1f~%.1f)"
|
||||
|
||||
msgid "Favorite filaments"
|
||||
msgstr ""
|
||||
|
||||
msgid "All filaments"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"The nozzle flow is not set. Please set the nozzle flow rate before editing the filament.\n"
|
||||
"'Device -> Print parts'"
|
||||
@@ -6482,19 +6501,9 @@ msgstr "Připravit"
|
||||
msgid "Preview"
|
||||
msgstr "Náhled"
|
||||
|
||||
msgid "Device"
|
||||
msgstr "Zařízení"
|
||||
|
||||
msgid "Multi-device"
|
||||
msgstr "Více zařízení"
|
||||
|
||||
msgid "Project"
|
||||
msgstr "Projekt"
|
||||
|
||||
# AI Translated
|
||||
msgid "Device (Web)"
|
||||
msgstr "Zařízení (Web)"
|
||||
|
||||
msgid "Yes"
|
||||
msgstr "Ano"
|
||||
|
||||
@@ -7205,6 +7214,22 @@ msgstr "Soubor .gcode.3mf neobsahuje žádná G-code data. Prosím, rozřežte m
|
||||
msgid "File '%s' was lost! Please download it again."
|
||||
msgstr "Soubor '%s' byl ztracen! Stáhněte jej prosím znovu."
|
||||
|
||||
msgid "gcodes"
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to delete the file from printer."
|
||||
msgstr ""
|
||||
|
||||
#, c-format, boost-format
|
||||
msgid "Do you want to print the file '%s' from printer?"
|
||||
msgstr ""
|
||||
|
||||
msgid "Print file"
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to start the print job."
|
||||
msgstr ""
|
||||
|
||||
#, c-format, boost-format
|
||||
msgid ""
|
||||
"File: %s\n"
|
||||
@@ -11380,6 +11405,12 @@ msgstr "Zrušit časosběr a tisknout"
|
||||
msgid "Clean Up"
|
||||
msgstr "Vyčistit"
|
||||
|
||||
msgid "AMS filament mapping is not available for this printer. Clear the AMS mapping before printing."
|
||||
msgstr ""
|
||||
|
||||
msgid "A filament used by this print has no AMS mapping. Assign it before printing."
|
||||
msgstr ""
|
||||
|
||||
msgid "Preparing print job"
|
||||
msgstr "Příprava tiskové úlohy"
|
||||
|
||||
@@ -31532,6 +31563,10 @@ msgstr "Povolit vývojářské nástroje"
|
||||
msgid "An error occurred loading %s"
|
||||
msgstr "Při načítání %s došlo k chybě"
|
||||
|
||||
# AI Translated
|
||||
msgid "/"
|
||||
msgstr "/"
|
||||
|
||||
# AI Translated
|
||||
#, boost-format
|
||||
msgid "Load %1% to %2%"
|
||||
@@ -33601,10 +33636,6 @@ msgstr ""
|
||||
#~ msgid "An error occurred loading "
|
||||
#~ msgstr "Při načítání došlo k chybě: "
|
||||
|
||||
# AI Translated
|
||||
#~ msgid "/"
|
||||
#~ msgstr "/"
|
||||
|
||||
# AI Translated
|
||||
#, c-format, boost-format
|
||||
#~ msgid "Load %s to "
|
||||
|
||||
@@ -3,7 +3,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Orca Slicer\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-10-11 16:21+0800\n"
|
||||
"POT-Creation-Date: 2026-10-12 19:00+0800\n"
|
||||
"PO-Revision-Date: \n"
|
||||
"Last-Translator: Heiko Liebscher <hliebschergmail.com>\n"
|
||||
"Language-Team: \n"
|
||||
@@ -4246,6 +4246,16 @@ msgstr "Zuletzt verwendete Projekte"
|
||||
msgid "Go to Plate"
|
||||
msgstr "Zur Druckplatte wechseln"
|
||||
|
||||
msgid "Device"
|
||||
msgstr "Gerät"
|
||||
|
||||
# AI Translated
|
||||
msgid "Device (Web)"
|
||||
msgstr "Gerät (Web)"
|
||||
|
||||
msgid "Multi-device"
|
||||
msgstr "Multi-Gerät"
|
||||
|
||||
msgid "AMS Materials Setting"
|
||||
msgstr "AMS Material Einstellung"
|
||||
|
||||
@@ -4294,6 +4304,9 @@ msgstr "Sind Sie sicher, dass Sie die Filamentinformationen löschen möchten?"
|
||||
msgid "You need to select the material type and color first."
|
||||
msgstr "Sie müssen zuerst den Materialtyp und die Farbe auswählen."
|
||||
|
||||
msgid "The minimum temperature cannot be greater than the maximum temperature."
|
||||
msgstr ""
|
||||
|
||||
#, c-format, boost-format
|
||||
msgid "Please input a valid value (K in %.1f~%.1f)"
|
||||
msgstr "Bitte geben Sie einen gültigen Wert ein (K in %.1f~%.1f)"
|
||||
@@ -4302,6 +4315,12 @@ msgstr "Bitte geben Sie einen gültigen Wert ein (K in %.1f~%.1f)"
|
||||
msgid "Please input a valid value (K in %.1f~%.1f, N in %.1f~%.1f)"
|
||||
msgstr "Bitte geben Sie einen gültigen Wert ein (K in %.1f~%.1f, N in %.1f~%.1f)"
|
||||
|
||||
msgid "Favorite filaments"
|
||||
msgstr ""
|
||||
|
||||
msgid "All filaments"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"The nozzle flow is not set. Please set the nozzle flow rate before editing the filament.\n"
|
||||
"'Device -> Print parts'"
|
||||
@@ -6364,19 +6383,9 @@ msgstr "Vorbereiten"
|
||||
msgid "Preview"
|
||||
msgstr "Vorschau"
|
||||
|
||||
msgid "Device"
|
||||
msgstr "Gerät"
|
||||
|
||||
msgid "Multi-device"
|
||||
msgstr "Multi-Gerät"
|
||||
|
||||
msgid "Project"
|
||||
msgstr "Projekt"
|
||||
|
||||
# AI Translated
|
||||
msgid "Device (Web)"
|
||||
msgstr "Gerät (Web)"
|
||||
|
||||
msgid "Yes"
|
||||
msgstr "Ja"
|
||||
|
||||
@@ -7072,6 +7081,22 @@ msgstr "Die .gcode.3mf-Datei enthält keine G-Code-Daten. Bitte slicen Sie sie m
|
||||
msgid "File '%s' was lost! Please download it again."
|
||||
msgstr "Die Datei '%s' ist verloren gegangen! Bitte laden Sie diese erneut herunter."
|
||||
|
||||
msgid "gcodes"
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to delete the file from printer."
|
||||
msgstr ""
|
||||
|
||||
#, c-format, boost-format
|
||||
msgid "Do you want to print the file '%s' from printer?"
|
||||
msgstr ""
|
||||
|
||||
msgid "Print file"
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to start the print job."
|
||||
msgstr ""
|
||||
|
||||
#, c-format, boost-format
|
||||
msgid ""
|
||||
"File: %s\n"
|
||||
@@ -11196,6 +11221,12 @@ msgstr "Zeitraffer abbrechen & Drucken"
|
||||
msgid "Clean Up"
|
||||
msgstr "Bereinigen"
|
||||
|
||||
msgid "AMS filament mapping is not available for this printer. Clear the AMS mapping before printing."
|
||||
msgstr ""
|
||||
|
||||
msgid "A filament used by this print has no AMS mapping. Assign it before printing."
|
||||
msgstr ""
|
||||
|
||||
msgid "Preparing print job"
|
||||
msgstr "Druckauftrag vorbereiten"
|
||||
|
||||
@@ -30919,6 +30950,9 @@ msgstr "Entwicklertools aktivieren"
|
||||
msgid "An error occurred loading %s"
|
||||
msgstr "Fehler beim Laden von %s"
|
||||
|
||||
msgid "/"
|
||||
msgstr "/"
|
||||
|
||||
# AI Translated
|
||||
#, boost-format
|
||||
msgid "Load %1% to %2%"
|
||||
@@ -32980,9 +33014,6 @@ msgstr ""
|
||||
#~ msgid "An error occurred loading "
|
||||
#~ msgstr "Fehler beim Laden von "
|
||||
|
||||
#~ msgid "/"
|
||||
#~ msgstr "/"
|
||||
|
||||
# AI Translated
|
||||
#, c-format, boost-format
|
||||
#~ msgid "Load %s to "
|
||||
|
||||
@@ -3,7 +3,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Orca Slicer\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-10-11 16:21+0800\n"
|
||||
"POT-Creation-Date: 2026-10-12 19:00+0800\n"
|
||||
"PO-Revision-Date: 2026-06-17 15:44-0300\n"
|
||||
"Last-Translator: Alexandre Folle de Menezes\n"
|
||||
"Language-Team: \n"
|
||||
@@ -3995,6 +3995,15 @@ msgstr ""
|
||||
msgid "Go to Plate"
|
||||
msgstr ""
|
||||
|
||||
msgid "Device"
|
||||
msgstr ""
|
||||
|
||||
msgid "Device (Web)"
|
||||
msgstr ""
|
||||
|
||||
msgid "Multi-device"
|
||||
msgstr ""
|
||||
|
||||
msgid "AMS Materials Setting"
|
||||
msgstr ""
|
||||
|
||||
@@ -4043,6 +4052,9 @@ msgstr ""
|
||||
msgid "You need to select the material type and color first."
|
||||
msgstr ""
|
||||
|
||||
msgid "The minimum temperature cannot be greater than the maximum temperature."
|
||||
msgstr ""
|
||||
|
||||
#, c-format, boost-format
|
||||
msgid "Please input a valid value (K in %.1f~%.1f)"
|
||||
msgstr ""
|
||||
@@ -4051,6 +4063,12 @@ msgstr ""
|
||||
msgid "Please input a valid value (K in %.1f~%.1f, N in %.1f~%.1f)"
|
||||
msgstr ""
|
||||
|
||||
msgid "Favorite filaments"
|
||||
msgstr ""
|
||||
|
||||
msgid "All filaments"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"The nozzle flow is not set. Please set the nozzle flow rate before editing the filament.\n"
|
||||
"'Device -> Print parts'"
|
||||
@@ -5981,18 +5999,9 @@ msgstr ""
|
||||
msgid "Preview"
|
||||
msgstr ""
|
||||
|
||||
msgid "Device"
|
||||
msgstr ""
|
||||
|
||||
msgid "Multi-device"
|
||||
msgstr ""
|
||||
|
||||
msgid "Project"
|
||||
msgstr ""
|
||||
|
||||
msgid "Device (Web)"
|
||||
msgstr ""
|
||||
|
||||
msgid "Yes"
|
||||
msgstr ""
|
||||
|
||||
@@ -6666,6 +6675,22 @@ msgstr ""
|
||||
msgid "File '%s' was lost! Please download it again."
|
||||
msgstr ""
|
||||
|
||||
msgid "gcodes"
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to delete the file from printer."
|
||||
msgstr ""
|
||||
|
||||
#, c-format, boost-format
|
||||
msgid "Do you want to print the file '%s' from printer?"
|
||||
msgstr ""
|
||||
|
||||
msgid "Print file"
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to start the print job."
|
||||
msgstr ""
|
||||
|
||||
#, c-format, boost-format
|
||||
msgid ""
|
||||
"File: %s\n"
|
||||
@@ -10338,6 +10363,12 @@ msgstr ""
|
||||
msgid "Clean Up"
|
||||
msgstr ""
|
||||
|
||||
msgid "AMS filament mapping is not available for this printer. Clear the AMS mapping before printing."
|
||||
msgstr ""
|
||||
|
||||
msgid "A filament used by this print has no AMS mapping. Assign it before printing."
|
||||
msgstr ""
|
||||
|
||||
msgid "Preparing print job"
|
||||
msgstr ""
|
||||
|
||||
@@ -27203,6 +27234,9 @@ msgstr ""
|
||||
msgid "An error occurred loading %s"
|
||||
msgstr ""
|
||||
|
||||
msgid "/"
|
||||
msgstr ""
|
||||
|
||||
#, boost-format
|
||||
msgid "Load %1% to %2%"
|
||||
msgstr ""
|
||||
|
||||
@@ -3,7 +3,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Orca Slicer\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-10-11 16:21+0800\n"
|
||||
"POT-Creation-Date: 2026-10-12 19:00+0800\n"
|
||||
"PO-Revision-Date: \n"
|
||||
"Last-Translator: Ian A. Bassi <>\n"
|
||||
"Language-Team: \n"
|
||||
@@ -4123,6 +4123,16 @@ msgstr "Proyectos recientes"
|
||||
msgid "Go to Plate"
|
||||
msgstr "Ir a la cama"
|
||||
|
||||
msgid "Device"
|
||||
msgstr "Dispositivo"
|
||||
|
||||
# AI Translated
|
||||
msgid "Device (Web)"
|
||||
msgstr "Dispositivo (Web)"
|
||||
|
||||
msgid "Multi-device"
|
||||
msgstr "Multi-dispositivo"
|
||||
|
||||
msgid "AMS Materials Setting"
|
||||
msgstr "Ajustes de materiales AMS"
|
||||
|
||||
@@ -4173,6 +4183,9 @@ msgstr "¿Estás seguro que quieres limpiar la información de filamento?"
|
||||
msgid "You need to select the material type and color first."
|
||||
msgstr "Necesitas seleccionar el tipo y el color del material primero."
|
||||
|
||||
msgid "The minimum temperature cannot be greater than the maximum temperature."
|
||||
msgstr ""
|
||||
|
||||
#, c-format, boost-format
|
||||
msgid "Please input a valid value (K in %.1f~%.1f)"
|
||||
msgstr "Por favor, introduzca un valor válido (K in %.1f~%.1f)"
|
||||
@@ -4181,6 +4194,12 @@ msgstr "Por favor, introduzca un valor válido (K in %.1f~%.1f)"
|
||||
msgid "Please input a valid value (K in %.1f~%.1f, N in %.1f~%.1f)"
|
||||
msgstr "Introduzca un valor válido (K en %.1f~%.1f, N en %.1f~%.1f)"
|
||||
|
||||
msgid "Favorite filaments"
|
||||
msgstr ""
|
||||
|
||||
msgid "All filaments"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"The nozzle flow is not set. Please set the nozzle flow rate before editing the filament.\n"
|
||||
"'Device -> Print parts'"
|
||||
@@ -6219,19 +6238,9 @@ msgstr "Preparar"
|
||||
msgid "Preview"
|
||||
msgstr "Previsualización"
|
||||
|
||||
msgid "Device"
|
||||
msgstr "Dispositivo"
|
||||
|
||||
msgid "Multi-device"
|
||||
msgstr "Multi-dispositivo"
|
||||
|
||||
msgid "Project"
|
||||
msgstr "Proyecto"
|
||||
|
||||
# AI Translated
|
||||
msgid "Device (Web)"
|
||||
msgstr "Dispositivo (Web)"
|
||||
|
||||
msgid "Yes"
|
||||
msgstr "Sí"
|
||||
|
||||
@@ -6925,6 +6934,22 @@ msgstr "El archivo .gcode .3mf no contiene datos de G-Code. Por favor, lamine co
|
||||
msgid "File '%s' was lost! Please download it again."
|
||||
msgstr "¡El archivo '%s' se perdió!\" Por favor, vuelva a descargárlo."
|
||||
|
||||
msgid "gcodes"
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to delete the file from printer."
|
||||
msgstr ""
|
||||
|
||||
#, c-format, boost-format
|
||||
msgid "Do you want to print the file '%s' from printer?"
|
||||
msgstr ""
|
||||
|
||||
msgid "Print file"
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to start the print job."
|
||||
msgstr ""
|
||||
|
||||
#, c-format, boost-format
|
||||
msgid ""
|
||||
"File: %s\n"
|
||||
@@ -10931,6 +10956,12 @@ msgstr "Cancelar timelapse e imprimir"
|
||||
msgid "Clean Up"
|
||||
msgstr "Limpiar"
|
||||
|
||||
msgid "AMS filament mapping is not available for this printer. Clear the AMS mapping before printing."
|
||||
msgstr ""
|
||||
|
||||
msgid "A filament used by this print has no AMS mapping. Assign it before printing."
|
||||
msgstr ""
|
||||
|
||||
msgid "Preparing print job"
|
||||
msgstr "Preparando el trabajo de impresión"
|
||||
|
||||
@@ -30478,6 +30509,9 @@ msgstr "Activar herramientas de desarrollo"
|
||||
msgid "An error occurred loading %s"
|
||||
msgstr "Se produjo un error al cargar %s"
|
||||
|
||||
msgid "/"
|
||||
msgstr "/"
|
||||
|
||||
# AI Translated
|
||||
#, boost-format
|
||||
msgid "Load %1% to %2%"
|
||||
@@ -32535,9 +32569,6 @@ msgstr ""
|
||||
#~ msgid "An error occurred loading "
|
||||
#~ msgstr "Se produjo un error al cargar "
|
||||
|
||||
#~ msgid "/"
|
||||
#~ msgstr "/"
|
||||
|
||||
# AI Translated
|
||||
#, c-format, boost-format
|
||||
#~ msgid "Load %s to "
|
||||
|
||||
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: \n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-10-11 16:21+0800\n"
|
||||
"POT-Creation-Date: 2026-10-12 19:00+0800\n"
|
||||
"PO-Revision-Date: 2026-07-20 13:33+0200\n"
|
||||
"Last-Translator: Manu Goiogana <mgoiogana@gmail.com>\n"
|
||||
"Language-Team: \n"
|
||||
@@ -4165,6 +4165,16 @@ msgstr "Azken proiektuak"
|
||||
msgid "Go to Plate"
|
||||
msgstr "Joan plakara"
|
||||
|
||||
msgid "Device"
|
||||
msgstr "Gailua"
|
||||
|
||||
# AI Translated
|
||||
msgid "Device (Web)"
|
||||
msgstr "Gailua (Web)"
|
||||
|
||||
msgid "Multi-device"
|
||||
msgstr "Gailu anitz"
|
||||
|
||||
msgid "AMS Materials Setting"
|
||||
msgstr "AMS Materialen Ezarpena"
|
||||
|
||||
@@ -4216,6 +4226,9 @@ msgstr "Ziur zaude filamentuaren informazioa garbitu nahi duzula?"
|
||||
msgid "You need to select the material type and color first."
|
||||
msgstr "Lehenik eta behin, material-mota eta kolorea hautatu behar dituzu."
|
||||
|
||||
msgid "The minimum temperature cannot be greater than the maximum temperature."
|
||||
msgstr ""
|
||||
|
||||
#, c-format, boost-format
|
||||
msgid "Please input a valid value (K in %.1f~%.1f)"
|
||||
msgstr "Mesedez, sartu baliozko balio bat (K in %.1f~%.1f)"
|
||||
@@ -4224,6 +4237,12 @@ msgstr "Mesedez, sartu baliozko balio bat (K in %.1f~%.1f)"
|
||||
msgid "Please input a valid value (K in %.1f~%.1f, N in %.1f~%.1f)"
|
||||
msgstr "Mesedez, sartu baliozko balio bat (K in %.1f~%.1f, N in %.1f~%.1f)"
|
||||
|
||||
msgid "Favorite filaments"
|
||||
msgstr ""
|
||||
|
||||
msgid "All filaments"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"The nozzle flow is not set. Please set the nozzle flow rate before editing the filament.\n"
|
||||
"'Device -> Print parts'"
|
||||
@@ -6269,19 +6288,9 @@ msgstr "Prestatu"
|
||||
msgid "Preview"
|
||||
msgstr "Aurrebista"
|
||||
|
||||
msgid "Device"
|
||||
msgstr "Gailua"
|
||||
|
||||
msgid "Multi-device"
|
||||
msgstr "Gailu anitz"
|
||||
|
||||
msgid "Project"
|
||||
msgstr "Proiektua"
|
||||
|
||||
# AI Translated
|
||||
msgid "Device (Web)"
|
||||
msgstr "Gailua (Web)"
|
||||
|
||||
msgid "Yes"
|
||||
msgstr "Bai"
|
||||
|
||||
@@ -6976,6 +6985,22 @@ msgstr ".gcode.3mf fitxategiak ez du G-code daturik. Mesedez, xerratu OrcaSlicer
|
||||
msgid "File '%s' was lost! Please download it again."
|
||||
msgstr "'%s' fitxategia galdu da! Deskargatu berriro."
|
||||
|
||||
msgid "gcodes"
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to delete the file from printer."
|
||||
msgstr ""
|
||||
|
||||
#, c-format, boost-format
|
||||
msgid "Do you want to print the file '%s' from printer?"
|
||||
msgstr ""
|
||||
|
||||
msgid "Print file"
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to start the print job."
|
||||
msgstr ""
|
||||
|
||||
#, c-format, boost-format
|
||||
msgid ""
|
||||
"File: %s\n"
|
||||
@@ -11037,6 +11062,12 @@ msgstr "Utzi timelapsea eta inprimatu"
|
||||
msgid "Clean Up"
|
||||
msgstr "Garbitu"
|
||||
|
||||
msgid "AMS filament mapping is not available for this printer. Clear the AMS mapping before printing."
|
||||
msgstr ""
|
||||
|
||||
msgid "A filament used by this print has no AMS mapping. Assign it before printing."
|
||||
msgstr ""
|
||||
|
||||
msgid "Preparing print job"
|
||||
msgstr "Inprimatze-lana prestatzen"
|
||||
|
||||
@@ -30669,6 +30700,9 @@ msgstr "Gaitu garapen-tresnak"
|
||||
msgid "An error occurred loading %s"
|
||||
msgstr "Errore bat gertatu da %s kargatzean"
|
||||
|
||||
msgid "/"
|
||||
msgstr "/"
|
||||
|
||||
# AI Translated
|
||||
#, boost-format
|
||||
msgid "Load %1% to %2%"
|
||||
@@ -32731,9 +32765,6 @@ msgstr ""
|
||||
#~ msgid "An error occurred loading "
|
||||
#~ msgstr "Errore bat gertatu da hau kargatzean: "
|
||||
|
||||
#~ msgid "/"
|
||||
#~ msgstr "/"
|
||||
|
||||
# AI Translated
|
||||
#, c-format, boost-format
|
||||
#~ msgid "Load %s to "
|
||||
|
||||
@@ -6,7 +6,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Orca Slicer\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-10-11 16:21+0800\n"
|
||||
"POT-Creation-Date: 2026-10-12 19:00+0800\n"
|
||||
"PO-Revision-Date: \n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Guislain Cyril, Thomas Lété\n"
|
||||
@@ -4195,6 +4195,16 @@ msgstr "Projets récents"
|
||||
msgid "Go to Plate"
|
||||
msgstr "Aller à la plaque"
|
||||
|
||||
msgid "Device"
|
||||
msgstr "Appareil"
|
||||
|
||||
# AI Translated
|
||||
msgid "Device (Web)"
|
||||
msgstr "Appareil (Web)"
|
||||
|
||||
msgid "Multi-device"
|
||||
msgstr "Multi-appareils"
|
||||
|
||||
msgid "AMS Materials Setting"
|
||||
msgstr "Réglage des matériaux AMS"
|
||||
|
||||
@@ -4245,6 +4255,9 @@ msgstr "Voulez-vous vraiment effacer les informations du filament ?"
|
||||
msgid "You need to select the material type and color first."
|
||||
msgstr "Vous devez d'abord sélectionner le type de matériau et sa couleur."
|
||||
|
||||
msgid "The minimum temperature cannot be greater than the maximum temperature."
|
||||
msgstr ""
|
||||
|
||||
#, c-format, boost-format
|
||||
msgid "Please input a valid value (K in %.1f~%.1f)"
|
||||
msgstr "Veuillez saisir une valeur valide (K entre %.1f~%.1f)"
|
||||
@@ -4253,6 +4266,12 @@ msgstr "Veuillez saisir une valeur valide (K entre %.1f~%.1f)"
|
||||
msgid "Please input a valid value (K in %.1f~%.1f, N in %.1f~%.1f)"
|
||||
msgstr "Veuillez saisir une valeur valide (K entre %.1f~%.1f, N entre %.1f~%.1f)"
|
||||
|
||||
msgid "Favorite filaments"
|
||||
msgstr ""
|
||||
|
||||
msgid "All filaments"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"The nozzle flow is not set. Please set the nozzle flow rate before editing the filament.\n"
|
||||
"'Device -> Print parts'"
|
||||
@@ -6308,19 +6327,9 @@ msgstr "Préparer"
|
||||
msgid "Preview"
|
||||
msgstr "Aperçu"
|
||||
|
||||
msgid "Device"
|
||||
msgstr "Appareil"
|
||||
|
||||
msgid "Multi-device"
|
||||
msgstr "Multi-appareils"
|
||||
|
||||
msgid "Project"
|
||||
msgstr "Projet"
|
||||
|
||||
# AI Translated
|
||||
msgid "Device (Web)"
|
||||
msgstr "Appareil (Web)"
|
||||
|
||||
msgid "Yes"
|
||||
msgstr "Oui"
|
||||
|
||||
@@ -7015,6 +7024,22 @@ msgstr "Le fichier G-code .3mf ne contient pas de données G-code. Veuillez le d
|
||||
msgid "File '%s' was lost! Please download it again."
|
||||
msgstr "Le fichier « %s » a été perdu ! Veuillez le télécharger à nouveau."
|
||||
|
||||
msgid "gcodes"
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to delete the file from printer."
|
||||
msgstr ""
|
||||
|
||||
#, c-format, boost-format
|
||||
msgid "Do you want to print the file '%s' from printer?"
|
||||
msgstr ""
|
||||
|
||||
msgid "Print file"
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to start the print job."
|
||||
msgstr ""
|
||||
|
||||
#, c-format, boost-format
|
||||
msgid ""
|
||||
"File: %s\n"
|
||||
@@ -11053,6 +11078,12 @@ msgstr "Annuler le timelapse et imprimer"
|
||||
msgid "Clean Up"
|
||||
msgstr "Nettoyer"
|
||||
|
||||
msgid "AMS filament mapping is not available for this printer. Clear the AMS mapping before printing."
|
||||
msgstr ""
|
||||
|
||||
msgid "A filament used by this print has no AMS mapping. Assign it before printing."
|
||||
msgstr ""
|
||||
|
||||
msgid "Preparing print job"
|
||||
msgstr "Préparation du travail d'impression"
|
||||
|
||||
@@ -30415,6 +30446,9 @@ msgstr "Activer les outils de développement"
|
||||
msgid "An error occurred loading %s"
|
||||
msgstr "Une erreur s’est produite lors du chargement de %s"
|
||||
|
||||
msgid "/"
|
||||
msgstr "/"
|
||||
|
||||
# AI Translated
|
||||
#, boost-format
|
||||
msgid "Load %1% to %2%"
|
||||
@@ -32466,9 +32500,6 @@ msgstr ""
|
||||
#~ msgid "An error occurred loading "
|
||||
#~ msgstr "Une erreur s’est produite lors du chargement de "
|
||||
|
||||
#~ msgid "/"
|
||||
#~ msgstr "/"
|
||||
|
||||
# AI Translated
|
||||
#, c-format, boost-format
|
||||
#~ msgid "Load %s to "
|
||||
|
||||
@@ -3,7 +3,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Orca Slicer\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-10-11 16:21+0800\n"
|
||||
"POT-Creation-Date: 2026-10-12 19:00+0800\n"
|
||||
"Language: hu\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
@@ -4302,6 +4302,17 @@ msgstr "Legutóbbi projektek"
|
||||
msgid "Go to Plate"
|
||||
msgstr "Ugrás a tálcára"
|
||||
|
||||
msgid "Device"
|
||||
msgstr "Nyomtató"
|
||||
|
||||
# AI Translated
|
||||
msgid "Device (Web)"
|
||||
msgstr "Nyomtató (Web)"
|
||||
|
||||
# AI Translated
|
||||
msgid "Multi-device"
|
||||
msgstr "Több eszköz"
|
||||
|
||||
msgid "AMS Materials Setting"
|
||||
msgstr "AMS anyagok beállítása"
|
||||
|
||||
@@ -4354,6 +4365,9 @@ msgstr "Biztosan törölni szeretnéd a filament adatait?"
|
||||
msgid "You need to select the material type and color first."
|
||||
msgstr "Először ki kell választanod az anyagtípust és a színt."
|
||||
|
||||
msgid "The minimum temperature cannot be greater than the maximum temperature."
|
||||
msgstr ""
|
||||
|
||||
#, c-format, boost-format
|
||||
msgid "Please input a valid value (K in %.1f~%.1f)"
|
||||
msgstr "Adj meg érvényes értéket (K tartomány: %.1f~%.1f)"
|
||||
@@ -4362,6 +4376,12 @@ msgstr "Adj meg érvényes értéket (K tartomány: %.1f~%.1f)"
|
||||
msgid "Please input a valid value (K in %.1f~%.1f, N in %.1f~%.1f)"
|
||||
msgstr "Adj meg érvényes értéket (K tartomány: %.1f~%.1f, N tartomány: %.1f~%.1f)"
|
||||
|
||||
msgid "Favorite filaments"
|
||||
msgstr ""
|
||||
|
||||
msgid "All filaments"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"The nozzle flow is not set. Please set the nozzle flow rate before editing the filament.\n"
|
||||
"'Device -> Print parts'"
|
||||
@@ -6448,20 +6468,9 @@ msgstr "Előkészítés"
|
||||
msgid "Preview"
|
||||
msgstr "Előnézet"
|
||||
|
||||
msgid "Device"
|
||||
msgstr "Nyomtató"
|
||||
|
||||
# AI Translated
|
||||
msgid "Multi-device"
|
||||
msgstr "Több eszköz"
|
||||
|
||||
msgid "Project"
|
||||
msgstr "Projekt"
|
||||
|
||||
# AI Translated
|
||||
msgid "Device (Web)"
|
||||
msgstr "Nyomtató (Web)"
|
||||
|
||||
msgid "Yes"
|
||||
msgstr "Igen"
|
||||
|
||||
@@ -7167,6 +7176,22 @@ msgstr "A .gcode.3mf fájl nem tartalmaz G-code adatot. Szeleteld újra Orca Sli
|
||||
msgid "File '%s' was lost! Please download it again."
|
||||
msgstr "A(z) '%s' fájl elveszett! Kérlek, töltsd le újra."
|
||||
|
||||
msgid "gcodes"
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to delete the file from printer."
|
||||
msgstr ""
|
||||
|
||||
#, c-format, boost-format
|
||||
msgid "Do you want to print the file '%s' from printer?"
|
||||
msgstr ""
|
||||
|
||||
msgid "Print file"
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to start the print job."
|
||||
msgstr ""
|
||||
|
||||
#, c-format, boost-format
|
||||
msgid ""
|
||||
"File: %s\n"
|
||||
@@ -11329,6 +11354,12 @@ msgstr "Timelapse megszakítása és nyomtatás"
|
||||
msgid "Clean Up"
|
||||
msgstr "Tisztítás"
|
||||
|
||||
msgid "AMS filament mapping is not available for this printer. Clear the AMS mapping before printing."
|
||||
msgstr ""
|
||||
|
||||
msgid "A filament used by this print has no AMS mapping. Assign it before printing."
|
||||
msgstr ""
|
||||
|
||||
msgid "Preparing print job"
|
||||
msgstr "Nyomtatási feladat előkészítése"
|
||||
|
||||
@@ -31397,6 +31428,10 @@ msgstr "Fejlesztői eszközök engedélyezése"
|
||||
msgid "An error occurred loading %s"
|
||||
msgstr "Hiba történt a(z) %s betöltésekor"
|
||||
|
||||
# AI Translated
|
||||
msgid "/"
|
||||
msgstr "/"
|
||||
|
||||
# AI Translated
|
||||
#, boost-format
|
||||
msgid "Load %1% to %2%"
|
||||
@@ -33459,10 +33494,6 @@ msgstr ""
|
||||
#~ msgid "An error occurred loading "
|
||||
#~ msgstr "Hiba történt a betöltés során: "
|
||||
|
||||
# AI Translated
|
||||
#~ msgid "/"
|
||||
#~ msgstr "/"
|
||||
|
||||
# AI Translated
|
||||
#, c-format, boost-format
|
||||
#~ msgid "Load %s to "
|
||||
|
||||
@@ -3,7 +3,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Orca Slicer\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-10-11 16:21+0800\n"
|
||||
"POT-Creation-Date: 2026-10-12 19:00+0800\n"
|
||||
"PO-Revision-Date: \n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: \n"
|
||||
@@ -4295,6 +4295,16 @@ msgstr "Progetti recenti"
|
||||
msgid "Go to Plate"
|
||||
msgstr "Vai al piatto"
|
||||
|
||||
msgid "Device"
|
||||
msgstr "Dispositivo"
|
||||
|
||||
# AI Translated
|
||||
msgid "Device (Web)"
|
||||
msgstr "Dispositivo (Web)"
|
||||
|
||||
msgid "Multi-device"
|
||||
msgstr "Multi-dispositivo"
|
||||
|
||||
msgid "AMS Materials Setting"
|
||||
msgstr "Impostazione materiali AMS"
|
||||
|
||||
@@ -4347,6 +4357,9 @@ msgstr "Sei sicuro di voler cancellare le informazioni del filamento?"
|
||||
msgid "You need to select the material type and color first."
|
||||
msgstr "Devi prima selezionare il tipo e il colore del materiale."
|
||||
|
||||
msgid "The minimum temperature cannot be greater than the maximum temperature."
|
||||
msgstr ""
|
||||
|
||||
#, c-format, boost-format
|
||||
msgid "Please input a valid value (K in %.1f~%.1f)"
|
||||
msgstr "Si prega di immettere un valore valido (K in %.1f~%.1f)"
|
||||
@@ -4355,6 +4368,12 @@ msgstr "Si prega di immettere un valore valido (K in %.1f~%.1f)"
|
||||
msgid "Please input a valid value (K in %.1f~%.1f, N in %.1f~%.1f)"
|
||||
msgstr "Si prega di immettere un valore valido (K in %.1f~%.1f, N in %.1f~%.1f)"
|
||||
|
||||
msgid "Favorite filaments"
|
||||
msgstr ""
|
||||
|
||||
msgid "All filaments"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"The nozzle flow is not set. Please set the nozzle flow rate before editing the filament.\n"
|
||||
"'Device -> Print parts'"
|
||||
@@ -6416,19 +6435,9 @@ msgstr "Prepara"
|
||||
msgid "Preview"
|
||||
msgstr "Anteprima"
|
||||
|
||||
msgid "Device"
|
||||
msgstr "Dispositivo"
|
||||
|
||||
msgid "Multi-device"
|
||||
msgstr "Multi-dispositivo"
|
||||
|
||||
msgid "Project"
|
||||
msgstr "Progetto"
|
||||
|
||||
# AI Translated
|
||||
msgid "Device (Web)"
|
||||
msgstr "Dispositivo (Web)"
|
||||
|
||||
msgid "Yes"
|
||||
msgstr "Sì"
|
||||
|
||||
@@ -7132,6 +7141,22 @@ msgstr "Il file .gcode.3mf non contiene dati G-code. Elaboralo con OrcaSlicer ed
|
||||
msgid "File '%s' was lost! Please download it again."
|
||||
msgstr "Il file '%s' è stato perso! Si prega di scaricarlo di nuovo."
|
||||
|
||||
msgid "gcodes"
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to delete the file from printer."
|
||||
msgstr ""
|
||||
|
||||
#, c-format, boost-format
|
||||
msgid "Do you want to print the file '%s' from printer?"
|
||||
msgstr ""
|
||||
|
||||
msgid "Print file"
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to start the print job."
|
||||
msgstr ""
|
||||
|
||||
#, c-format, boost-format
|
||||
msgid ""
|
||||
"File: %s\n"
|
||||
@@ -11289,6 +11314,12 @@ msgstr "Annulla timelapse e stampa"
|
||||
msgid "Clean Up"
|
||||
msgstr "Pulisci"
|
||||
|
||||
msgid "AMS filament mapping is not available for this printer. Clear the AMS mapping before printing."
|
||||
msgstr ""
|
||||
|
||||
msgid "A filament used by this print has no AMS mapping. Assign it before printing."
|
||||
msgstr ""
|
||||
|
||||
msgid "Preparing print job"
|
||||
msgstr "Preparazione della stampa"
|
||||
|
||||
@@ -31259,6 +31290,9 @@ msgstr "Abilita strumenti per sviluppatori"
|
||||
msgid "An error occurred loading %s"
|
||||
msgstr "Si è verificato un errore durante il caricamento di %s"
|
||||
|
||||
msgid "/"
|
||||
msgstr "/"
|
||||
|
||||
# AI Translated
|
||||
#, boost-format
|
||||
msgid "Load %1% to %2%"
|
||||
@@ -33322,9 +33356,6 @@ msgstr ""
|
||||
#~ msgid "An error occurred loading "
|
||||
#~ msgstr "Si è verificato un errore durante il caricamento di "
|
||||
|
||||
#~ msgid "/"
|
||||
#~ msgstr "/"
|
||||
|
||||
# AI Translated
|
||||
#, c-format, boost-format
|
||||
#~ msgid "Load %s to "
|
||||
|
||||
@@ -3,7 +3,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Orca Slicer\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-10-11 16:21+0800\n"
|
||||
"POT-Creation-Date: 2026-10-12 19:00+0800\n"
|
||||
"PO-Revision-Date: \n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: \n"
|
||||
@@ -4301,6 +4301,17 @@ msgstr "最近のプロジェクト"
|
||||
msgid "Go to Plate"
|
||||
msgstr "プレートへ移動"
|
||||
|
||||
msgid "Device"
|
||||
msgstr "デバイス"
|
||||
|
||||
# AI Translated
|
||||
msgid "Device (Web)"
|
||||
msgstr "デバイス (Web)"
|
||||
|
||||
# AI Translated
|
||||
msgid "Multi-device"
|
||||
msgstr "マルチデバイス"
|
||||
|
||||
msgid "AMS Materials Setting"
|
||||
msgstr "AMS素材設定"
|
||||
|
||||
@@ -4349,6 +4360,9 @@ msgstr "フィラメント情報をクリアしてもよろしいですか?"
|
||||
msgid "You need to select the material type and color first."
|
||||
msgstr "まず材料タイプと色を選択してください。"
|
||||
|
||||
msgid "The minimum temperature cannot be greater than the maximum temperature."
|
||||
msgstr ""
|
||||
|
||||
#, c-format, boost-format
|
||||
msgid "Please input a valid value (K in %.1f~%.1f)"
|
||||
msgstr "有効な値を入力してください(Kは%.1f~%.1f)"
|
||||
@@ -4357,6 +4371,12 @@ msgstr "有効な値を入力してください(Kは%.1f~%.1f)"
|
||||
msgid "Please input a valid value (K in %.1f~%.1f, N in %.1f~%.1f)"
|
||||
msgstr "有効な値を入力してください(Kは%.1f~%.1f、Nは%.1f~%.1f)"
|
||||
|
||||
msgid "Favorite filaments"
|
||||
msgstr ""
|
||||
|
||||
msgid "All filaments"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"The nozzle flow is not set. Please set the nozzle flow rate before editing the filament.\n"
|
||||
"'Device -> Print parts'"
|
||||
@@ -6433,20 +6453,9 @@ msgstr "準備"
|
||||
msgid "Preview"
|
||||
msgstr "プレビュー"
|
||||
|
||||
msgid "Device"
|
||||
msgstr "デバイス"
|
||||
|
||||
# AI Translated
|
||||
msgid "Multi-device"
|
||||
msgstr "マルチデバイス"
|
||||
|
||||
msgid "Project"
|
||||
msgstr "プロジェクト"
|
||||
|
||||
# AI Translated
|
||||
msgid "Device (Web)"
|
||||
msgstr "デバイス (Web)"
|
||||
|
||||
msgid "Yes"
|
||||
msgstr "はい"
|
||||
|
||||
@@ -7147,6 +7156,22 @@ msgstr ".gcode.3mfファイルにG-codeデータが含まれていません。Or
|
||||
msgid "File '%s' was lost! Please download it again."
|
||||
msgstr "ファイル %s が見つかりませんでした、もう一度ダウンロードしてください。"
|
||||
|
||||
msgid "gcodes"
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to delete the file from printer."
|
||||
msgstr ""
|
||||
|
||||
#, c-format, boost-format
|
||||
msgid "Do you want to print the file '%s' from printer?"
|
||||
msgstr ""
|
||||
|
||||
msgid "Print file"
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to start the print job."
|
||||
msgstr ""
|
||||
|
||||
#, c-format, boost-format
|
||||
msgid ""
|
||||
"File: %s\n"
|
||||
@@ -11317,6 +11342,12 @@ msgstr "タイムラプスを中止して印刷"
|
||||
msgid "Clean Up"
|
||||
msgstr "整理する"
|
||||
|
||||
msgid "AMS filament mapping is not available for this printer. Clear the AMS mapping before printing."
|
||||
msgstr ""
|
||||
|
||||
msgid "A filament used by this print has no AMS mapping. Assign it before printing."
|
||||
msgstr ""
|
||||
|
||||
msgid "Preparing print job"
|
||||
msgstr "造形タスクを準備"
|
||||
|
||||
@@ -31779,6 +31810,9 @@ msgstr "開発者ツールを有効化"
|
||||
msgid "An error occurred loading %s"
|
||||
msgstr "%s の読み込み中にエラーが発生しました"
|
||||
|
||||
msgid "/"
|
||||
msgstr "/"
|
||||
|
||||
# AI Translated
|
||||
#, boost-format
|
||||
msgid "Load %1% to %2%"
|
||||
@@ -33882,9 +33916,6 @@ msgstr ""
|
||||
#~ msgid "An error occurred loading "
|
||||
#~ msgstr "読み込み中にエラーが発生しました: "
|
||||
|
||||
#~ msgid "/"
|
||||
#~ msgstr "/"
|
||||
|
||||
# AI Translated
|
||||
#, c-format, boost-format
|
||||
#~ msgid "Load %s to "
|
||||
|
||||
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Orca Slicer\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-10-11 16:21+0800\n"
|
||||
"POT-Creation-Date: 2026-10-12 19:00+0800\n"
|
||||
"PO-Revision-Date: 2025-06-02 17:12+0900\n"
|
||||
"Last-Translator: crwusiz <crwusiz@gmail.com>\n"
|
||||
"Language-Team: \n"
|
||||
@@ -4324,6 +4324,16 @@ msgstr "최근 프로젝트"
|
||||
msgid "Go to Plate"
|
||||
msgstr "플레이트로 이동"
|
||||
|
||||
msgid "Device"
|
||||
msgstr "장치"
|
||||
|
||||
# AI Translated
|
||||
msgid "Device (Web)"
|
||||
msgstr "장치 (웹)"
|
||||
|
||||
msgid "Multi-device"
|
||||
msgstr "멀티 디바이스"
|
||||
|
||||
msgid "AMS Materials Setting"
|
||||
msgstr "AMS 재료 설정"
|
||||
|
||||
@@ -4374,6 +4384,9 @@ msgstr "정말 필라멘트 정보를 삭제하시겠습니까?"
|
||||
msgid "You need to select the material type and color first."
|
||||
msgstr "재료 유형과 색상을 먼저 선택해야 합니다."
|
||||
|
||||
msgid "The minimum temperature cannot be greater than the maximum temperature."
|
||||
msgstr ""
|
||||
|
||||
#, c-format, boost-format
|
||||
msgid "Please input a valid value (K in %.1f~%.1f)"
|
||||
msgstr "유효한 값을 입력하세요 (%.1f~%.1f 사이의 K)"
|
||||
@@ -4382,6 +4395,12 @@ msgstr "유효한 값을 입력하세요 (%.1f~%.1f 사이의 K)"
|
||||
msgid "Please input a valid value (K in %.1f~%.1f, N in %.1f~%.1f)"
|
||||
msgstr "유효한 값을 입력하세요 (%.1f~%.1f 사이의 K, %.1f~%.1f 사이의 N)"
|
||||
|
||||
msgid "Favorite filaments"
|
||||
msgstr ""
|
||||
|
||||
msgid "All filaments"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"The nozzle flow is not set. Please set the nozzle flow rate before editing the filament.\n"
|
||||
"'Device -> Print parts'"
|
||||
@@ -6472,19 +6491,9 @@ msgstr "준비 하기"
|
||||
msgid "Preview"
|
||||
msgstr "미리 보기"
|
||||
|
||||
msgid "Device"
|
||||
msgstr "장치"
|
||||
|
||||
msgid "Multi-device"
|
||||
msgstr "멀티 디바이스"
|
||||
|
||||
msgid "Project"
|
||||
msgstr "프로젝트"
|
||||
|
||||
# AI Translated
|
||||
msgid "Device (Web)"
|
||||
msgstr "장치 (웹)"
|
||||
|
||||
msgid "Yes"
|
||||
msgstr "예"
|
||||
|
||||
@@ -7193,6 +7202,22 @@ msgstr ".gcode.3mf 파일에는 G-code 데이터가 없습니다. OrcaSlicer에
|
||||
msgid "File '%s' was lost! Please download it again."
|
||||
msgstr "'%s' 파일이 손실되었습니다! 다시 다운로드하세요."
|
||||
|
||||
msgid "gcodes"
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to delete the file from printer."
|
||||
msgstr ""
|
||||
|
||||
#, c-format, boost-format
|
||||
msgid "Do you want to print the file '%s' from printer?"
|
||||
msgstr ""
|
||||
|
||||
msgid "Print file"
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to start the print job."
|
||||
msgstr ""
|
||||
|
||||
#, c-format, boost-format
|
||||
msgid ""
|
||||
"File: %s\n"
|
||||
@@ -11459,6 +11484,12 @@ msgstr "타임랩스 취소 후 출력"
|
||||
msgid "Clean Up"
|
||||
msgstr "정리"
|
||||
|
||||
msgid "AMS filament mapping is not available for this printer. Clear the AMS mapping before printing."
|
||||
msgstr ""
|
||||
|
||||
msgid "A filament used by this print has no AMS mapping. Assign it before printing."
|
||||
msgstr ""
|
||||
|
||||
msgid "Preparing print job"
|
||||
msgstr "출력 작업 준비 중"
|
||||
|
||||
@@ -31797,6 +31828,9 @@ msgstr "개발자 도구 사용"
|
||||
msgid "An error occurred loading %s"
|
||||
msgstr "%s 로드 중 오류가 발생했습니다"
|
||||
|
||||
msgid "/"
|
||||
msgstr "/"
|
||||
|
||||
# AI Translated
|
||||
#, boost-format
|
||||
msgid "Load %1% to %2%"
|
||||
@@ -33872,9 +33906,6 @@ msgstr ""
|
||||
#~ msgid "An error occurred loading "
|
||||
#~ msgstr "로드 중 오류가 발생했습니다: "
|
||||
|
||||
#~ msgid "/"
|
||||
#~ msgstr "/"
|
||||
|
||||
# AI Translated
|
||||
#, c-format, boost-format
|
||||
#~ msgid "Load %s to "
|
||||
|
||||
@@ -147,6 +147,7 @@ src/slic3r/GUI/HMSPanel.cpp
|
||||
src/slic3r/GUI/MainFrame.cpp
|
||||
src/slic3r/GUI/MediaPlayCtrl.cpp
|
||||
src/slic3r/GUI/MediaFilePanel.cpp
|
||||
src/slic3r/GUI/OrcaFilesPanel.cpp
|
||||
src/slic3r/GUI/ImageGrid.cpp
|
||||
src/slic3r/GUI/Widgets/ImageSwitchButton.cpp
|
||||
src/slic3r/GUI/Printer/PrinterFileSystem.cpp
|
||||
|
||||
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: \n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-10-11 16:21+0800\n"
|
||||
"POT-Creation-Date: 2026-10-12 19:00+0800\n"
|
||||
"PO-Revision-Date: 2026-07-02 14:13+0300\n"
|
||||
"Last-Translator: Gintaras Kučinskas <sharanchius@gmail.com>\n"
|
||||
"Language-Team: \n"
|
||||
@@ -4291,6 +4291,16 @@ msgstr "Paskutinieji projektai"
|
||||
msgid "Go to Plate"
|
||||
msgstr "Eiti į plokštę"
|
||||
|
||||
msgid "Device"
|
||||
msgstr "Įrenginys"
|
||||
|
||||
# AI Translated
|
||||
msgid "Device (Web)"
|
||||
msgstr "Įrenginys (Web)"
|
||||
|
||||
msgid "Multi-device"
|
||||
msgstr "Kelių įrenginių valdymas (Multi-device)"
|
||||
|
||||
msgid "AMS Materials Setting"
|
||||
msgstr "AMS medžiagų nuostatos"
|
||||
|
||||
@@ -4341,6 +4351,9 @@ msgstr "Ar jūs tikrai norite išvalyti gijos informaciją?"
|
||||
msgid "You need to select the material type and color first."
|
||||
msgstr "Pradžioje jums reikia pasirinkti medžiagos tipą ir spalvą."
|
||||
|
||||
msgid "The minimum temperature cannot be greater than the maximum temperature."
|
||||
msgstr ""
|
||||
|
||||
#, c-format, boost-format
|
||||
msgid "Please input a valid value (K in %.1f~%.1f)"
|
||||
msgstr "Įveskite tinkamą reikšmę (K %.1f~%.1f)"
|
||||
@@ -4349,6 +4362,12 @@ msgstr "Įveskite tinkamą reikšmę (K %.1f~%.1f)"
|
||||
msgid "Please input a valid value (K in %.1f~%.1f, N in %.1f~%.1f)"
|
||||
msgstr "Įveskite tinkamą reikšmę (K %.1f~%.1f, N %.1f~%.1f)"
|
||||
|
||||
msgid "Favorite filaments"
|
||||
msgstr ""
|
||||
|
||||
msgid "All filaments"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"The nozzle flow is not set. Please set the nozzle flow rate before editing the filament.\n"
|
||||
"'Device -> Print parts'"
|
||||
@@ -6435,19 +6454,9 @@ msgstr "Paruošimas"
|
||||
msgid "Preview"
|
||||
msgstr "Peržiūra"
|
||||
|
||||
msgid "Device"
|
||||
msgstr "Įrenginys"
|
||||
|
||||
msgid "Multi-device"
|
||||
msgstr "Kelių įrenginių valdymas (Multi-device)"
|
||||
|
||||
msgid "Project"
|
||||
msgstr "Projektas"
|
||||
|
||||
# AI Translated
|
||||
msgid "Device (Web)"
|
||||
msgstr "Įrenginys (Web)"
|
||||
|
||||
msgid "Yes"
|
||||
msgstr "Taip"
|
||||
|
||||
@@ -7153,6 +7162,22 @@ msgstr "Failas „.gcode.3mf“ neturi G-code duomenų. Supjaustykite jį su „
|
||||
msgid "File '%s' was lost! Please download it again."
|
||||
msgstr "Failas „%s“ nerastas! Atsisiųskite jį iš naujo."
|
||||
|
||||
msgid "gcodes"
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to delete the file from printer."
|
||||
msgstr ""
|
||||
|
||||
#, c-format, boost-format
|
||||
msgid "Do you want to print the file '%s' from printer?"
|
||||
msgstr ""
|
||||
|
||||
msgid "Print file"
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to start the print job."
|
||||
msgstr ""
|
||||
|
||||
#, c-format, boost-format
|
||||
msgid ""
|
||||
"File: %s\n"
|
||||
@@ -11285,6 +11310,12 @@ msgstr "Atšaukti intervalinį fotografavimą ir spausdinti"
|
||||
msgid "Clean Up"
|
||||
msgstr "Išvalyti"
|
||||
|
||||
msgid "AMS filament mapping is not available for this printer. Clear the AMS mapping before printing."
|
||||
msgstr ""
|
||||
|
||||
msgid "A filament used by this print has no AMS mapping. Assign it before printing."
|
||||
msgstr ""
|
||||
|
||||
msgid "Preparing print job"
|
||||
msgstr "Spausdinimo užduoties rengimas"
|
||||
|
||||
@@ -12954,6 +12985,7 @@ msgstr ""
|
||||
msgid "Select filtered"
|
||||
msgstr ""
|
||||
|
||||
# AI Translated
|
||||
msgid "Clear all"
|
||||
msgstr "Išvalyti visus"
|
||||
|
||||
@@ -31132,6 +31164,10 @@ msgstr "Įjungti kūrėjo įrankius"
|
||||
msgid "An error occurred loading %s"
|
||||
msgstr "Įvyko klaida įkeliant %s"
|
||||
|
||||
# AI Translated
|
||||
msgid "/"
|
||||
msgstr "/"
|
||||
|
||||
# AI Translated
|
||||
#, boost-format
|
||||
msgid "Load %1% to %2%"
|
||||
@@ -33199,10 +33235,6 @@ msgstr ""
|
||||
#~ msgid "An error occurred loading "
|
||||
#~ msgstr "Įvyko klaida įkeliant "
|
||||
|
||||
# AI Translated
|
||||
#~ msgid "/"
|
||||
#~ msgstr "/"
|
||||
|
||||
# AI Translated
|
||||
#, c-format, boost-format
|
||||
#~ msgid "Load %s to "
|
||||
|
||||
@@ -3,7 +3,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Orca Slicer\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-10-11 16:21+0800\n"
|
||||
"POT-Creation-Date: 2026-10-12 19:00+0800\n"
|
||||
"PO-Revision-Date: \n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: \n"
|
||||
@@ -4660,6 +4660,17 @@ msgstr "Recente projecten"
|
||||
msgid "Go to Plate"
|
||||
msgstr "Naar printbed gaan"
|
||||
|
||||
msgid "Device"
|
||||
msgstr "Apparaat"
|
||||
|
||||
# AI Translated
|
||||
msgid "Device (Web)"
|
||||
msgstr "Apparaat (Web)"
|
||||
|
||||
# AI Translated
|
||||
msgid "Multi-device"
|
||||
msgstr "Meerdere apparaten"
|
||||
|
||||
msgid "AMS Materials Setting"
|
||||
msgstr "AMS Materiaal instellingen"
|
||||
|
||||
@@ -4716,6 +4727,9 @@ msgstr "Weet u zeker dat u de filamentinformatie wilt wissen?"
|
||||
msgid "You need to select the material type and color first."
|
||||
msgstr "U moet eerst het materiaaltype en de kleur selecteren."
|
||||
|
||||
msgid "The minimum temperature cannot be greater than the maximum temperature."
|
||||
msgstr ""
|
||||
|
||||
# AI Translated
|
||||
#, c-format, boost-format
|
||||
msgid "Please input a valid value (K in %.1f~%.1f)"
|
||||
@@ -4726,6 +4740,12 @@ msgstr "Voer een geldige waarde in (K tussen %.1f~%.1f)"
|
||||
msgid "Please input a valid value (K in %.1f~%.1f, N in %.1f~%.1f)"
|
||||
msgstr "Voer een geldige waarde in (K tussen %.1f~%.1f, N tussen %.1f~%.1f)"
|
||||
|
||||
msgid "Favorite filaments"
|
||||
msgstr ""
|
||||
|
||||
msgid "All filaments"
|
||||
msgstr ""
|
||||
|
||||
# AI Translated
|
||||
msgid ""
|
||||
"The nozzle flow is not set. Please set the nozzle flow rate before editing the filament.\n"
|
||||
@@ -6968,21 +6988,10 @@ msgstr "Voorbereiden"
|
||||
msgid "Preview"
|
||||
msgstr "Voorvertoning"
|
||||
|
||||
msgid "Device"
|
||||
msgstr "Apparaat"
|
||||
|
||||
# AI Translated
|
||||
msgid "Multi-device"
|
||||
msgstr "Meerdere apparaten"
|
||||
|
||||
# AI Translated
|
||||
msgid "Project"
|
||||
msgstr "Project"
|
||||
|
||||
# AI Translated
|
||||
msgid "Device (Web)"
|
||||
msgstr "Apparaat (Web)"
|
||||
|
||||
msgid "Yes"
|
||||
msgstr "Ja"
|
||||
|
||||
@@ -7731,6 +7740,22 @@ msgstr "Het .gcode.3mf-bestand bevat geen G-code-gegevens. Slice het met Orca Sl
|
||||
msgid "File '%s' was lost! Please download it again."
|
||||
msgstr "Bestand '%s' is verloren gegaan! Download het opnieuw."
|
||||
|
||||
msgid "gcodes"
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to delete the file from printer."
|
||||
msgstr ""
|
||||
|
||||
#, c-format, boost-format
|
||||
msgid "Do you want to print the file '%s' from printer?"
|
||||
msgstr ""
|
||||
|
||||
msgid "Print file"
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to start the print job."
|
||||
msgstr ""
|
||||
|
||||
#, c-format, boost-format
|
||||
msgid ""
|
||||
"File: %s\n"
|
||||
@@ -12172,6 +12197,12 @@ msgstr "Timelapse annuleren en printen"
|
||||
msgid "Clean Up"
|
||||
msgstr "Opruimen"
|
||||
|
||||
msgid "AMS filament mapping is not available for this printer. Clear the AMS mapping before printing."
|
||||
msgstr ""
|
||||
|
||||
msgid "A filament used by this print has no AMS mapping. Assign it before printing."
|
||||
msgstr ""
|
||||
|
||||
msgid "Preparing print job"
|
||||
msgstr "Print opdracht voorbereiden"
|
||||
|
||||
@@ -33323,6 +33354,9 @@ msgstr "Ontwikkelaarstools inschakelen"
|
||||
msgid "An error occurred loading %s"
|
||||
msgstr "Er is een fout opgetreden bij het laden van %s"
|
||||
|
||||
msgid "/"
|
||||
msgstr "/"
|
||||
|
||||
# AI Translated
|
||||
#, boost-format
|
||||
msgid "Load %1% to %2%"
|
||||
@@ -35456,9 +35490,6 @@ msgstr ""
|
||||
#~ msgid "An error occurred loading "
|
||||
#~ msgstr "Er is een fout opgetreden bij het laden van "
|
||||
|
||||
#~ msgid "/"
|
||||
#~ msgstr "/"
|
||||
|
||||
# AI Translated
|
||||
#, c-format, boost-format
|
||||
#~ msgid "Load %s to "
|
||||
|
||||
@@ -3,7 +3,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: OrcaSlicer 2.3.0-rc\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-10-11 16:21+0800\n"
|
||||
"POT-Creation-Date: 2026-10-12 19:00+0800\n"
|
||||
"PO-Revision-Date: \n"
|
||||
"Last-Translator: Krzysztof Morga <<tlumaczeniebs@gmail.com>>\n"
|
||||
"Language-Team: \n"
|
||||
@@ -4382,6 +4382,16 @@ msgstr "Ostatnie projekty"
|
||||
msgid "Go to Plate"
|
||||
msgstr "Przejdź do płyty"
|
||||
|
||||
msgid "Device"
|
||||
msgstr "Urządzenie"
|
||||
|
||||
# AI Translated
|
||||
msgid "Device (Web)"
|
||||
msgstr "Urządzenie (Web)"
|
||||
|
||||
msgid "Multi-device"
|
||||
msgstr "Wiele urządzeń"
|
||||
|
||||
msgid "AMS Materials Setting"
|
||||
msgstr "Ustawienia filamentów AMS"
|
||||
|
||||
@@ -4435,6 +4445,9 @@ msgstr "Czy na pewno usunąć informacje o filamentach?"
|
||||
msgid "You need to select the material type and color first."
|
||||
msgstr "Najpierw należy wybrać rodzaj i kolor filamentu."
|
||||
|
||||
msgid "The minimum temperature cannot be greater than the maximum temperature."
|
||||
msgstr ""
|
||||
|
||||
#, c-format, boost-format
|
||||
msgid "Please input a valid value (K in %.1f~%.1f)"
|
||||
msgstr "Proszę podać prawidłową wartość (K w zakresie %.1f~%.1f)"
|
||||
@@ -4443,6 +4456,12 @@ msgstr "Proszę podać prawidłową wartość (K w zakresie %.1f~%.1f)"
|
||||
msgid "Please input a valid value (K in %.1f~%.1f, N in %.1f~%.1f)"
|
||||
msgstr "Proszę podać prawidłową wartość (K w zakresie %.1f~%.1f, N in %.1f~%.1f)"
|
||||
|
||||
msgid "Favorite filaments"
|
||||
msgstr ""
|
||||
|
||||
msgid "All filaments"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"The nozzle flow is not set. Please set the nozzle flow rate before editing the filament.\n"
|
||||
"'Device -> Print parts'"
|
||||
@@ -6552,19 +6571,9 @@ msgstr "Przygotowanie"
|
||||
msgid "Preview"
|
||||
msgstr "Podgląd"
|
||||
|
||||
msgid "Device"
|
||||
msgstr "Urządzenie"
|
||||
|
||||
msgid "Multi-device"
|
||||
msgstr "Wiele urządzeń"
|
||||
|
||||
msgid "Project"
|
||||
msgstr "Projekt"
|
||||
|
||||
# AI Translated
|
||||
msgid "Device (Web)"
|
||||
msgstr "Urządzenie (Web)"
|
||||
|
||||
msgid "Yes"
|
||||
msgstr "Tak"
|
||||
|
||||
@@ -7283,6 +7292,22 @@ msgstr "Plik .gcode.3mf nie zawiera danych G-code. Proszę poddać go obróbce w
|
||||
msgid "File '%s' was lost! Please download it again."
|
||||
msgstr "Plik „%s” został utracony! Proszę pobrać go ponownie."
|
||||
|
||||
msgid "gcodes"
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to delete the file from printer."
|
||||
msgstr ""
|
||||
|
||||
#, c-format, boost-format
|
||||
msgid "Do you want to print the file '%s' from printer?"
|
||||
msgstr ""
|
||||
|
||||
msgid "Print file"
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to start the print job."
|
||||
msgstr ""
|
||||
|
||||
#, c-format, boost-format
|
||||
msgid ""
|
||||
"File: %s\n"
|
||||
@@ -11558,6 +11583,12 @@ msgstr "Anuluj timelapse i drukuj"
|
||||
msgid "Clean Up"
|
||||
msgstr "Wyczyść"
|
||||
|
||||
msgid "AMS filament mapping is not available for this printer. Clear the AMS mapping before printing."
|
||||
msgstr ""
|
||||
|
||||
msgid "A filament used by this print has no AMS mapping. Assign it before printing."
|
||||
msgstr ""
|
||||
|
||||
msgid "Preparing print job"
|
||||
msgstr "Przygotowywanie zadania do druku"
|
||||
|
||||
@@ -31778,6 +31809,9 @@ msgstr "Włącz narzędzia deweloperskie"
|
||||
msgid "An error occurred loading %s"
|
||||
msgstr "Wystąpił błąd podczas ładowania %s"
|
||||
|
||||
msgid "/"
|
||||
msgstr "/"
|
||||
|
||||
# AI Translated
|
||||
#, boost-format
|
||||
msgid "Load %1% to %2%"
|
||||
@@ -33820,9 +33854,6 @@ msgstr ""
|
||||
#~ msgid "An error occurred loading "
|
||||
#~ msgstr "Wystąpił błąd podczas ładowania"
|
||||
|
||||
#~ msgid "/"
|
||||
#~ msgstr "/"
|
||||
|
||||
# AI Translated
|
||||
#, c-format, boost-format
|
||||
#~ msgid "Load %s to "
|
||||
|
||||
@@ -3,7 +3,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Orca Slicer\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-10-11 16:21+0800\n"
|
||||
"POT-Creation-Date: 2026-10-12 19:00+0800\n"
|
||||
"PO-Revision-Date: 2026-07-26 11:14-0300\n"
|
||||
"Last-Translator: Alexandre Folle de Menezes\n"
|
||||
"Language-Team: Portuguese, Brazilian\n"
|
||||
@@ -4132,6 +4132,15 @@ msgstr "Projetos Recentes"
|
||||
msgid "Go to Plate"
|
||||
msgstr "Ir para Placa"
|
||||
|
||||
msgid "Device"
|
||||
msgstr "Dispositivo"
|
||||
|
||||
msgid "Device (Web)"
|
||||
msgstr "Dispositivo (Web)"
|
||||
|
||||
msgid "Multi-device"
|
||||
msgstr "Multi-dispositivo"
|
||||
|
||||
msgid "AMS Materials Setting"
|
||||
msgstr "Configuração de Materiais AMS"
|
||||
|
||||
@@ -4182,6 +4191,9 @@ msgstr "Tem certeza de que deseja limpar as informações do filamento?"
|
||||
msgid "You need to select the material type and color first."
|
||||
msgstr "Você precisa selecionar o tipo e a cor do material primeiro."
|
||||
|
||||
msgid "The minimum temperature cannot be greater than the maximum temperature."
|
||||
msgstr ""
|
||||
|
||||
#, c-format, boost-format
|
||||
msgid "Please input a valid value (K in %.1f~%.1f)"
|
||||
msgstr "Por favor insira um valor válido (K entre %.1f~%.1f)"
|
||||
@@ -4190,6 +4202,12 @@ msgstr "Por favor insira um valor válido (K entre %.1f~%.1f)"
|
||||
msgid "Please input a valid value (K in %.1f~%.1f, N in %.1f~%.1f)"
|
||||
msgstr "Por favor insira um valor válido (K entre %.1f~%.1f, N entre %.1f~%.1f)"
|
||||
|
||||
msgid "Favorite filaments"
|
||||
msgstr ""
|
||||
|
||||
msgid "All filaments"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"The nozzle flow is not set. Please set the nozzle flow rate before editing the filament.\n"
|
||||
"'Device -> Print parts'"
|
||||
@@ -6222,18 +6240,9 @@ msgstr "Preparar"
|
||||
msgid "Preview"
|
||||
msgstr "Pré-visualizar"
|
||||
|
||||
msgid "Device"
|
||||
msgstr "Dispositivo"
|
||||
|
||||
msgid "Multi-device"
|
||||
msgstr "Multi-dispositivo"
|
||||
|
||||
msgid "Project"
|
||||
msgstr "Projeto"
|
||||
|
||||
msgid "Device (Web)"
|
||||
msgstr "Dispositivo (Web)"
|
||||
|
||||
msgid "Yes"
|
||||
msgstr "Sim"
|
||||
|
||||
@@ -6924,6 +6933,22 @@ msgstr "O arquivo .gcode.3mf não contém dados de G-code. Por favor, fatie com
|
||||
msgid "File '%s' was lost! Please download it again."
|
||||
msgstr "O arquivo '%s' foi perdido! Por favor, baixe-o novamente."
|
||||
|
||||
msgid "gcodes"
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to delete the file from printer."
|
||||
msgstr ""
|
||||
|
||||
#, c-format, boost-format
|
||||
msgid "Do you want to print the file '%s' from printer?"
|
||||
msgstr ""
|
||||
|
||||
msgid "Print file"
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to start the print job."
|
||||
msgstr ""
|
||||
|
||||
#, c-format, boost-format
|
||||
msgid ""
|
||||
"File: %s\n"
|
||||
@@ -10873,6 +10898,12 @@ msgstr "Cancelar Timelapse e Imprimir"
|
||||
msgid "Clean Up"
|
||||
msgstr "Limpar"
|
||||
|
||||
msgid "AMS filament mapping is not available for this printer. Clear the AMS mapping before printing."
|
||||
msgstr ""
|
||||
|
||||
msgid "A filament used by this print has no AMS mapping. Assign it before printing."
|
||||
msgstr ""
|
||||
|
||||
msgid "Preparing print job"
|
||||
msgstr "Preparando trabalho de impressão"
|
||||
|
||||
@@ -30359,6 +30390,10 @@ msgstr "Ativar Ferramentas de Desenvolvedor"
|
||||
msgid "An error occurred loading %s"
|
||||
msgstr "Ocorreu um erro ao carregar %s"
|
||||
|
||||
# AI Translated
|
||||
msgid "/"
|
||||
msgstr "/"
|
||||
|
||||
# AI Translated
|
||||
#, boost-format
|
||||
msgid "Load %1% to %2%"
|
||||
@@ -32414,10 +32449,6 @@ msgstr ""
|
||||
#~ msgid "An error occurred loading "
|
||||
#~ msgstr "Ocorreu um erro ao carregar "
|
||||
|
||||
# AI Translated
|
||||
#~ msgid "/"
|
||||
#~ msgstr "/"
|
||||
|
||||
# AI Translated
|
||||
#, c-format, boost-format
|
||||
#~ msgid "Load %s to "
|
||||
|
||||
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: OrcaSlicer V2.5.0\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-10-11 16:21+0800\n"
|
||||
"POT-Creation-Date: 2026-10-12 19:00+0800\n"
|
||||
"PO-Revision-Date: 2026-02-25 13:38+0300\n"
|
||||
"Last-Translator: Felix14_v2\n"
|
||||
"Language-Team: Felix14_v2 (ДС/ТГ: @felix14_v2, почта: aleks111001@list.ru), Andylg <andylg@yandex.ru>\n"
|
||||
@@ -4267,6 +4267,16 @@ msgstr "Недавние проекты"
|
||||
msgid "Go to Plate"
|
||||
msgstr "Перейти к столу"
|
||||
|
||||
msgid "Device"
|
||||
msgstr "Принтер"
|
||||
|
||||
# AI Translated
|
||||
msgid "Device (Web)"
|
||||
msgstr "Принтер (веб)"
|
||||
|
||||
msgid "Multi-device"
|
||||
msgstr "Принтеры"
|
||||
|
||||
msgid "AMS Materials Setting"
|
||||
msgstr "Настройка материалов AMS"
|
||||
|
||||
@@ -4317,6 +4327,9 @@ msgstr "Вы действительно хотите удалить информ
|
||||
msgid "You need to select the material type and color first."
|
||||
msgstr "Сначала нужно выбрать тип материала и цвет."
|
||||
|
||||
msgid "The minimum temperature cannot be greater than the maximum temperature."
|
||||
msgstr ""
|
||||
|
||||
#, c-format, boost-format
|
||||
msgid "Please input a valid value (K in %.1f~%.1f)"
|
||||
msgstr "Пожалуйста, введите допустимое значение (K в диапазоне %.1f~%.1f)"
|
||||
@@ -4325,6 +4338,12 @@ msgstr "Пожалуйста, введите допустимое значени
|
||||
msgid "Please input a valid value (K in %.1f~%.1f, N in %.1f~%.1f)"
|
||||
msgstr "Пожалуйста, введите допустимое значение (K в диапазоне %.1f~%.1f, N в диапазоне %.1f~%.1f)"
|
||||
|
||||
msgid "Favorite filaments"
|
||||
msgstr ""
|
||||
|
||||
msgid "All filaments"
|
||||
msgstr ""
|
||||
|
||||
# Дичь какая-то бамбуковская с терминами, требуется проверка.
|
||||
msgid ""
|
||||
"The nozzle flow is not set. Please set the nozzle flow rate before editing the filament.\n"
|
||||
@@ -6465,19 +6484,9 @@ msgstr "Подготовка"
|
||||
msgid "Preview"
|
||||
msgstr "Просмотр нарезки"
|
||||
|
||||
msgid "Device"
|
||||
msgstr "Принтер"
|
||||
|
||||
msgid "Multi-device"
|
||||
msgstr "Принтеры"
|
||||
|
||||
msgid "Project"
|
||||
msgstr "Проект"
|
||||
|
||||
# AI Translated
|
||||
msgid "Device (Web)"
|
||||
msgstr "Принтер (веб)"
|
||||
|
||||
msgid "Yes"
|
||||
msgstr "Да"
|
||||
|
||||
@@ -7190,6 +7199,22 @@ msgstr "Файл .gcode.3mf не содержит данных G-code. Испо
|
||||
msgid "File '%s' was lost! Please download it again."
|
||||
msgstr "Файл '%s' потерян! Пожалуйста, загрузите его снова."
|
||||
|
||||
msgid "gcodes"
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to delete the file from printer."
|
||||
msgstr ""
|
||||
|
||||
#, c-format, boost-format
|
||||
msgid "Do you want to print the file '%s' from printer?"
|
||||
msgstr ""
|
||||
|
||||
msgid "Print file"
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to start the print job."
|
||||
msgstr ""
|
||||
|
||||
#, c-format, boost-format
|
||||
msgid ""
|
||||
"File: %s\n"
|
||||
@@ -11298,6 +11323,12 @@ msgstr "Отключить таймлапс и продолжить"
|
||||
msgid "Clean Up"
|
||||
msgstr "Очистить"
|
||||
|
||||
msgid "AMS filament mapping is not available for this printer. Clear the AMS mapping before printing."
|
||||
msgstr ""
|
||||
|
||||
msgid "A filament used by this print has no AMS mapping. Assign it before printing."
|
||||
msgstr ""
|
||||
|
||||
msgid "Preparing print job"
|
||||
msgstr "Подготовка задания на печать"
|
||||
|
||||
@@ -12968,6 +12999,7 @@ msgstr ""
|
||||
msgid "Select filtered"
|
||||
msgstr ""
|
||||
|
||||
# AI Translated
|
||||
msgid "Clear all"
|
||||
msgstr "Очистить"
|
||||
|
||||
@@ -31626,6 +31658,9 @@ msgstr "Включить инструменты разработчика"
|
||||
msgid "An error occurred loading %s"
|
||||
msgstr "Ошибка при загрузке %s"
|
||||
|
||||
msgid "/"
|
||||
msgstr "/"
|
||||
|
||||
# AI Translated
|
||||
#, boost-format
|
||||
msgid "Load %1% to %2%"
|
||||
@@ -33693,9 +33728,6 @@ msgstr ""
|
||||
#~ msgid "An error occurred loading "
|
||||
#~ msgstr "Ошибка при загрузке "
|
||||
|
||||
#~ msgid "/"
|
||||
#~ msgstr "/"
|
||||
|
||||
# AI Translated
|
||||
#, c-format, boost-format
|
||||
#~ msgid "Load %s to "
|
||||
|
||||
@@ -3,7 +3,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Orca Slicer\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-10-11 16:21+0800\n"
|
||||
"POT-Creation-Date: 2026-10-12 19:00+0800\n"
|
||||
"Language: sv\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
@@ -4728,6 +4728,17 @@ msgstr "Senaste projekt"
|
||||
msgid "Go to Plate"
|
||||
msgstr "Gå till platta"
|
||||
|
||||
msgid "Device"
|
||||
msgstr "Enhet"
|
||||
|
||||
# AI Translated
|
||||
msgid "Device (Web)"
|
||||
msgstr "Enhet (Webb)"
|
||||
|
||||
# AI Translated
|
||||
msgid "Multi-device"
|
||||
msgstr "Flera enheter"
|
||||
|
||||
msgid "AMS Materials Setting"
|
||||
msgstr "AMS Material Inställning"
|
||||
|
||||
@@ -4781,6 +4792,9 @@ msgstr "Är du säker på att du vill rensa filament informationen?"
|
||||
msgid "You need to select the material type and color first."
|
||||
msgstr "Du måste först välja materialtyp och färg."
|
||||
|
||||
msgid "The minimum temperature cannot be greater than the maximum temperature."
|
||||
msgstr ""
|
||||
|
||||
# AI Translated
|
||||
#, c-format, boost-format
|
||||
msgid "Please input a valid value (K in %.1f~%.1f)"
|
||||
@@ -4791,6 +4805,12 @@ msgstr "Ange ett giltigt värde (K mellan %.1f~%.1f)"
|
||||
msgid "Please input a valid value (K in %.1f~%.1f, N in %.1f~%.1f)"
|
||||
msgstr "Ange ett giltigt värde (K mellan %.1f~%.1f, N mellan %.1f~%.1f)"
|
||||
|
||||
msgid "Favorite filaments"
|
||||
msgstr ""
|
||||
|
||||
msgid "All filaments"
|
||||
msgstr ""
|
||||
|
||||
# AI Translated
|
||||
msgid ""
|
||||
"The nozzle flow is not set. Please set the nozzle flow rate before editing the filament.\n"
|
||||
@@ -7065,20 +7085,9 @@ msgstr "Förbered"
|
||||
msgid "Preview"
|
||||
msgstr "Förhandsvisning"
|
||||
|
||||
msgid "Device"
|
||||
msgstr "Enhet"
|
||||
|
||||
# AI Translated
|
||||
msgid "Multi-device"
|
||||
msgstr "Flera enheter"
|
||||
|
||||
msgid "Project"
|
||||
msgstr "Projekt"
|
||||
|
||||
# AI Translated
|
||||
msgid "Device (Web)"
|
||||
msgstr "Enhet (Webb)"
|
||||
|
||||
msgid "Yes"
|
||||
msgstr "Ja"
|
||||
|
||||
@@ -7825,6 +7834,22 @@ msgstr "Filen .gcode.3mf innehåller inga G-code-data. Bered den med Orca Slicer
|
||||
msgid "File '%s' was lost! Please download it again."
|
||||
msgstr "Filen '%s' förlorades! Ladda ner den igen."
|
||||
|
||||
msgid "gcodes"
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to delete the file from printer."
|
||||
msgstr ""
|
||||
|
||||
#, c-format, boost-format
|
||||
msgid "Do you want to print the file '%s' from printer?"
|
||||
msgstr ""
|
||||
|
||||
msgid "Print file"
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to start the print job."
|
||||
msgstr ""
|
||||
|
||||
# AI Translated
|
||||
#, c-format, boost-format
|
||||
msgid ""
|
||||
@@ -12320,6 +12345,12 @@ msgstr "Avbryt timelapse och skriv ut"
|
||||
msgid "Clean Up"
|
||||
msgstr "Rensa"
|
||||
|
||||
msgid "AMS filament mapping is not available for this printer. Clear the AMS mapping before printing."
|
||||
msgstr ""
|
||||
|
||||
msgid "A filament used by this print has no AMS mapping. Assign it before printing."
|
||||
msgstr ""
|
||||
|
||||
msgid "Preparing print job"
|
||||
msgstr "Förbereder utskriftsjobb"
|
||||
|
||||
@@ -33657,6 +33688,9 @@ msgstr "Aktivera utvecklarverktyg"
|
||||
msgid "An error occurred loading %s"
|
||||
msgstr "Ett fel uppstod vid inläsning av %s"
|
||||
|
||||
msgid "/"
|
||||
msgstr "/"
|
||||
|
||||
# AI Translated
|
||||
#, boost-format
|
||||
msgid "Load %1% to %2%"
|
||||
@@ -35810,9 +35844,6 @@ msgstr ""
|
||||
#~ msgid "An error occurred loading "
|
||||
#~ msgstr "Ett fel uppstod vid inläsning av "
|
||||
|
||||
#~ msgid "/"
|
||||
#~ msgstr "/"
|
||||
|
||||
# AI Translated
|
||||
#, c-format, boost-format
|
||||
#~ msgid "Load %s to "
|
||||
|
||||
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Orca Slicer\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-10-11 16:21+0800\n"
|
||||
"POT-Creation-Date: 2026-10-12 19:00+0800\n"
|
||||
"PO-Revision-Date: 2026-06-19 13:40+0700\n"
|
||||
"Last-Translator: Icezaza\n"
|
||||
"Language-Team: Thai\n"
|
||||
@@ -4279,6 +4279,16 @@ msgstr "โปรเจกต์ล่าสุด"
|
||||
msgid "Go to Plate"
|
||||
msgstr "ไปที่ฐานพิมพ์"
|
||||
|
||||
msgid "Device"
|
||||
msgstr "อุปกรณ์"
|
||||
|
||||
# AI Translated
|
||||
msgid "Device (Web)"
|
||||
msgstr "อุปกรณ์ (เว็บ)"
|
||||
|
||||
msgid "Multi-device"
|
||||
msgstr "หลายอุปกรณ์"
|
||||
|
||||
msgid "AMS Materials Setting"
|
||||
msgstr "การตั้งค่าวัสดุ AMS"
|
||||
|
||||
@@ -4329,6 +4339,9 @@ msgstr "คุณแน่ใจหรือไม่ว่าต้องกา
|
||||
msgid "You need to select the material type and color first."
|
||||
msgstr "คุณต้องเลือกประเภทวัสดุและสีก่อน"
|
||||
|
||||
msgid "The minimum temperature cannot be greater than the maximum temperature."
|
||||
msgstr ""
|
||||
|
||||
#, c-format, boost-format
|
||||
msgid "Please input a valid value (K in %.1f~%.1f)"
|
||||
msgstr "โปรดป้อนค่าที่ถูกต้อง (K อยู่ในช่วง %.1f~%.1f)"
|
||||
@@ -4337,6 +4350,12 @@ msgstr "โปรดป้อนค่าที่ถูกต้อง (K อ
|
||||
msgid "Please input a valid value (K in %.1f~%.1f, N in %.1f~%.1f)"
|
||||
msgstr "โปรดป้อนค่าที่ถูกต้อง (K ใน %.1f~%.1f, N ใน %.1f~%.1f)"
|
||||
|
||||
msgid "Favorite filaments"
|
||||
msgstr ""
|
||||
|
||||
msgid "All filaments"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"The nozzle flow is not set. Please set the nozzle flow rate before editing the filament.\n"
|
||||
"'Device -> Print parts'"
|
||||
@@ -6399,19 +6418,9 @@ msgstr "เตรียมพิมพ์"
|
||||
msgid "Preview"
|
||||
msgstr "พรีวิว"
|
||||
|
||||
msgid "Device"
|
||||
msgstr "อุปกรณ์"
|
||||
|
||||
msgid "Multi-device"
|
||||
msgstr "หลายอุปกรณ์"
|
||||
|
||||
msgid "Project"
|
||||
msgstr "โปรเจกต์"
|
||||
|
||||
# AI Translated
|
||||
msgid "Device (Web)"
|
||||
msgstr "อุปกรณ์ (เว็บ)"
|
||||
|
||||
msgid "Yes"
|
||||
msgstr "ใช่"
|
||||
|
||||
@@ -7106,6 +7115,22 @@ msgstr "ไฟล์ .gcode.3mf ไม่มีข้อมูล G-code โป
|
||||
msgid "File '%s' was lost! Please download it again."
|
||||
msgstr "ไฟล์ '%s' สูญหาย! กรุณาดาวน์โหลดอีกครั้ง."
|
||||
|
||||
msgid "gcodes"
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to delete the file from printer."
|
||||
msgstr ""
|
||||
|
||||
#, c-format, boost-format
|
||||
msgid "Do you want to print the file '%s' from printer?"
|
||||
msgstr ""
|
||||
|
||||
msgid "Print file"
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to start the print job."
|
||||
msgstr ""
|
||||
|
||||
#, c-format, boost-format
|
||||
msgid ""
|
||||
"File: %s\n"
|
||||
@@ -11215,6 +11240,12 @@ msgstr "ยกเลิกไทม์แลปส์และพิมพ์"
|
||||
msgid "Clean Up"
|
||||
msgstr "ล้าง"
|
||||
|
||||
msgid "AMS filament mapping is not available for this printer. Clear the AMS mapping before printing."
|
||||
msgstr ""
|
||||
|
||||
msgid "A filament used by this print has no AMS mapping. Assign it before printing."
|
||||
msgstr ""
|
||||
|
||||
msgid "Preparing print job"
|
||||
msgstr "กำลังเตรียมงานพิมพ์"
|
||||
|
||||
@@ -31014,6 +31045,10 @@ msgstr "เปิดใช้งานเครื่องมือนักพ
|
||||
msgid "An error occurred loading %s"
|
||||
msgstr "เกิดข้อผิดพลาดในการโหลด %s"
|
||||
|
||||
# AI Translated
|
||||
msgid "/"
|
||||
msgstr "/"
|
||||
|
||||
# AI Translated
|
||||
#, boost-format
|
||||
msgid "Load %1% to %2%"
|
||||
@@ -33075,10 +33110,6 @@ msgstr ""
|
||||
#~ msgid "An error occurred loading "
|
||||
#~ msgstr "เกิดข้อผิดพลาดในการโหลด "
|
||||
|
||||
# AI Translated
|
||||
#~ msgid "/"
|
||||
#~ msgstr "/"
|
||||
|
||||
# AI Translated
|
||||
#, c-format, boost-format
|
||||
#~ msgid "Load %s to "
|
||||
|
||||
@@ -3,7 +3,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Orca Slicer\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-10-11 16:21+0800\n"
|
||||
"POT-Creation-Date: 2026-10-12 19:00+0800\n"
|
||||
"PO-Revision-Date: 2026-08-21 23:18+0300\n"
|
||||
"Last-Translator: GlauTech\n"
|
||||
"Language-Team: \n"
|
||||
@@ -4331,6 +4331,16 @@ msgstr "Son Projeler"
|
||||
msgid "Go to Plate"
|
||||
msgstr "Plakaya Git"
|
||||
|
||||
msgid "Device"
|
||||
msgstr "Cihaz"
|
||||
|
||||
# AI Translated
|
||||
msgid "Device (Web)"
|
||||
msgstr "Cihaz (Web)"
|
||||
|
||||
msgid "Multi-device"
|
||||
msgstr "Çoklu cihaz"
|
||||
|
||||
msgid "AMS Materials Setting"
|
||||
msgstr "AMS Malzeme Ayarı"
|
||||
|
||||
@@ -4381,6 +4391,9 @@ msgstr "Filament bilgisini temizlemek istediğinizden emin misiniz?"
|
||||
msgid "You need to select the material type and color first."
|
||||
msgstr "Önce malzeme türünü ve rengini seçmeniz gerekir."
|
||||
|
||||
msgid "The minimum temperature cannot be greater than the maximum temperature."
|
||||
msgstr ""
|
||||
|
||||
#, c-format, boost-format
|
||||
msgid "Please input a valid value (K in %.1f~%.1f)"
|
||||
msgstr "Lütfen geçerli bir değer girin (K %.1f~%.1f içinde)"
|
||||
@@ -4389,6 +4402,12 @@ msgstr "Lütfen geçerli bir değer girin (K %.1f~%.1f içinde)"
|
||||
msgid "Please input a valid value (K in %.1f~%.1f, N in %.1f~%.1f)"
|
||||
msgstr "Lütfen geçerli bir değer girin (K %.1f~%.1f içinde, N %.1f~%.1f içinde)"
|
||||
|
||||
msgid "Favorite filaments"
|
||||
msgstr ""
|
||||
|
||||
msgid "All filaments"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"The nozzle flow is not set. Please set the nozzle flow rate before editing the filament.\n"
|
||||
"'Device -> Print parts'"
|
||||
@@ -6465,19 +6484,9 @@ msgstr "Hazırlık"
|
||||
msgid "Preview"
|
||||
msgstr "Ön İzleme"
|
||||
|
||||
msgid "Device"
|
||||
msgstr "Cihaz"
|
||||
|
||||
msgid "Multi-device"
|
||||
msgstr "Çoklu cihaz"
|
||||
|
||||
msgid "Project"
|
||||
msgstr "Proje"
|
||||
|
||||
# AI Translated
|
||||
msgid "Device (Web)"
|
||||
msgstr "Cihaz (Web)"
|
||||
|
||||
msgid "Yes"
|
||||
msgstr "Evet"
|
||||
|
||||
@@ -7179,6 +7188,22 @@ msgstr ".gcode.3mf dosyası hiçbir G-code verisi içermiyor. Lütfen dosyayı B
|
||||
msgid "File '%s' was lost! Please download it again."
|
||||
msgstr "'%s' dosyası kayboldu! Lütfen tekrar indirin."
|
||||
|
||||
msgid "gcodes"
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to delete the file from printer."
|
||||
msgstr ""
|
||||
|
||||
#, c-format, boost-format
|
||||
msgid "Do you want to print the file '%s' from printer?"
|
||||
msgstr ""
|
||||
|
||||
msgid "Print file"
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to start the print job."
|
||||
msgstr ""
|
||||
|
||||
#, c-format, boost-format
|
||||
msgid ""
|
||||
"File: %s\n"
|
||||
@@ -11347,6 +11372,12 @@ msgstr "Timelapse'i İptal Et ve Yazdır"
|
||||
msgid "Clean Up"
|
||||
msgstr "Temizle"
|
||||
|
||||
msgid "AMS filament mapping is not available for this printer. Clear the AMS mapping before printing."
|
||||
msgstr ""
|
||||
|
||||
msgid "A filament used by this print has no AMS mapping. Assign it before printing."
|
||||
msgstr ""
|
||||
|
||||
msgid "Preparing print job"
|
||||
msgstr "Yazdırma için hazırlanıyor"
|
||||
|
||||
@@ -31457,6 +31488,10 @@ msgstr "Geliştirici Araçlarını Etkinleştir"
|
||||
msgid "An error occurred loading %s"
|
||||
msgstr "%s yüklenirken bir hata oluştu"
|
||||
|
||||
# AI Translated
|
||||
msgid "/"
|
||||
msgstr "/"
|
||||
|
||||
# AI Translated
|
||||
#, boost-format
|
||||
msgid "Load %1% to %2%"
|
||||
@@ -33523,10 +33558,6 @@ msgstr ""
|
||||
#~ msgid "An error occurred loading "
|
||||
#~ msgstr "Yüklenirken bir hata oluştu: "
|
||||
|
||||
# AI Translated
|
||||
#~ msgid "/"
|
||||
#~ msgstr "/"
|
||||
|
||||
# AI Translated
|
||||
#, c-format, boost-format
|
||||
#~ msgid "Load %s to "
|
||||
|
||||
@@ -3,7 +3,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: orcaslicerua\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-10-11 16:21+0800\n"
|
||||
"POT-Creation-Date: 2026-10-12 19:00+0800\n"
|
||||
"PO-Revision-Date: 2026-07-17 16:25+0300\n"
|
||||
"Last-Translator: Andrij Mizyk <andm1zyk@proton.me>\n"
|
||||
"Language-Team: Ukrainian\n"
|
||||
@@ -4245,6 +4245,16 @@ msgstr "Останні проєкти"
|
||||
msgid "Go to Plate"
|
||||
msgstr "Перейти до пластини"
|
||||
|
||||
msgid "Device"
|
||||
msgstr "Пристрій"
|
||||
|
||||
# AI Translated
|
||||
msgid "Device (Web)"
|
||||
msgstr "Пристрій (Веб)"
|
||||
|
||||
msgid "Multi-device"
|
||||
msgstr "Багато пристроїв"
|
||||
|
||||
msgid "AMS Materials Setting"
|
||||
msgstr "Налаштування матеріалів AMS"
|
||||
|
||||
@@ -4295,6 +4305,9 @@ msgstr "Ви впевнені, що хочете видалити інформа
|
||||
msgid "You need to select the material type and color first."
|
||||
msgstr "Спочатку потрібно вибрати тип матеріалу та колір."
|
||||
|
||||
msgid "The minimum temperature cannot be greater than the maximum temperature."
|
||||
msgstr ""
|
||||
|
||||
#, c-format, boost-format
|
||||
msgid "Please input a valid value (K in %.1f~%.1f)"
|
||||
msgstr "Будь ласка введіть коректне значення (K в %.1f~%.1f)"
|
||||
@@ -4303,6 +4316,12 @@ msgstr "Будь ласка введіть коректне значення (K
|
||||
msgid "Please input a valid value (K in %.1f~%.1f, N in %.1f~%.1f)"
|
||||
msgstr "Будь ласка введіть коректне значення (K в %.1f~%.1f, N в %.1f~%.1f)"
|
||||
|
||||
msgid "Favorite filaments"
|
||||
msgstr ""
|
||||
|
||||
msgid "All filaments"
|
||||
msgstr ""
|
||||
|
||||
# AI Translated
|
||||
msgid ""
|
||||
"The nozzle flow is not set. Please set the nozzle flow rate before editing the filament.\n"
|
||||
@@ -6429,19 +6448,9 @@ msgstr "Підготовка"
|
||||
msgid "Preview"
|
||||
msgstr "Перегляд"
|
||||
|
||||
msgid "Device"
|
||||
msgstr "Пристрій"
|
||||
|
||||
msgid "Multi-device"
|
||||
msgstr "Багато пристроїв"
|
||||
|
||||
msgid "Project"
|
||||
msgstr "Проєкт"
|
||||
|
||||
# AI Translated
|
||||
msgid "Device (Web)"
|
||||
msgstr "Пристрій (Веб)"
|
||||
|
||||
msgid "Yes"
|
||||
msgstr "Так"
|
||||
|
||||
@@ -7148,6 +7157,22 @@ msgstr "Файл .gcode.3mf не містить даних G-code. Будь ла
|
||||
msgid "File '%s' was lost! Please download it again."
|
||||
msgstr "Файл '%s' втрачений! Завантажте його знову."
|
||||
|
||||
msgid "gcodes"
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to delete the file from printer."
|
||||
msgstr ""
|
||||
|
||||
#, c-format, boost-format
|
||||
msgid "Do you want to print the file '%s' from printer?"
|
||||
msgstr ""
|
||||
|
||||
msgid "Print file"
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to start the print job."
|
||||
msgstr ""
|
||||
|
||||
#, c-format, boost-format
|
||||
msgid ""
|
||||
"File: %s\n"
|
||||
@@ -11364,6 +11389,12 @@ msgstr "Скасувати таймлапс і друкувати"
|
||||
msgid "Clean Up"
|
||||
msgstr "Очистити"
|
||||
|
||||
msgid "AMS filament mapping is not available for this printer. Clear the AMS mapping before printing."
|
||||
msgstr ""
|
||||
|
||||
msgid "A filament used by this print has no AMS mapping. Assign it before printing."
|
||||
msgstr ""
|
||||
|
||||
msgid "Preparing print job"
|
||||
msgstr "Підготовка завдання для друку"
|
||||
|
||||
@@ -31624,6 +31655,9 @@ msgstr "Увімкнути інструменти розробника"
|
||||
msgid "An error occurred loading %s"
|
||||
msgstr "Помилка під час завантаження %s"
|
||||
|
||||
msgid "/"
|
||||
msgstr "/"
|
||||
|
||||
# AI Translated
|
||||
#, boost-format
|
||||
msgid "Load %1% to %2%"
|
||||
@@ -33718,9 +33752,6 @@ msgstr ""
|
||||
#~ msgid "An error occurred loading "
|
||||
#~ msgstr "Помилка під час завантаження "
|
||||
|
||||
#~ msgid "/"
|
||||
#~ msgstr "/"
|
||||
|
||||
# AI Translated
|
||||
#, c-format, boost-format
|
||||
#~ msgid "Load %s to "
|
||||
|
||||
@@ -3,7 +3,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Orca Slicer\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-10-11 16:21+0800\n"
|
||||
"POT-Creation-Date: 2026-10-12 19:00+0800\n"
|
||||
"PO-Revision-Date: 2025-10-02 17:43+0700\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: hainguyen.ts13@gmail.com\n"
|
||||
@@ -4503,6 +4503,16 @@ msgstr "Dự án gần đây"
|
||||
msgid "Go to Plate"
|
||||
msgstr "Đi tới plate"
|
||||
|
||||
msgid "Device"
|
||||
msgstr "Thiết bị"
|
||||
|
||||
# AI Translated
|
||||
msgid "Device (Web)"
|
||||
msgstr "Thiết bị (Web)"
|
||||
|
||||
msgid "Multi-device"
|
||||
msgstr "Nhiều thiết bị"
|
||||
|
||||
msgid "AMS Materials Setting"
|
||||
msgstr "Cài đặt vật liệu AMS"
|
||||
|
||||
@@ -4555,6 +4565,9 @@ msgstr "Bạn có chắc muốn xóa thông tin filament?"
|
||||
msgid "You need to select the material type and color first."
|
||||
msgstr "Bạn cần chọn loại vật liệu và màu trước."
|
||||
|
||||
msgid "The minimum temperature cannot be greater than the maximum temperature."
|
||||
msgstr ""
|
||||
|
||||
#, c-format, boost-format
|
||||
msgid "Please input a valid value (K in %.1f~%.1f)"
|
||||
msgstr "Vui lòng nhập giá trị hợp lệ (K trong %.1f~%.1f)"
|
||||
@@ -4563,6 +4576,12 @@ msgstr "Vui lòng nhập giá trị hợp lệ (K trong %.1f~%.1f)"
|
||||
msgid "Please input a valid value (K in %.1f~%.1f, N in %.1f~%.1f)"
|
||||
msgstr "Vui lòng nhập giá trị hợp lệ (K trong %.1f~%.1f, N trong %.1f~%.1f)"
|
||||
|
||||
msgid "Favorite filaments"
|
||||
msgstr ""
|
||||
|
||||
msgid "All filaments"
|
||||
msgstr ""
|
||||
|
||||
# AI Translated
|
||||
msgid ""
|
||||
"The nozzle flow is not set. Please set the nozzle flow rate before editing the filament.\n"
|
||||
@@ -6789,19 +6808,9 @@ msgstr "Chuẩn bị"
|
||||
msgid "Preview"
|
||||
msgstr "Xem trước"
|
||||
|
||||
msgid "Device"
|
||||
msgstr "Thiết bị"
|
||||
|
||||
msgid "Multi-device"
|
||||
msgstr "Nhiều thiết bị"
|
||||
|
||||
msgid "Project"
|
||||
msgstr "Dự án"
|
||||
|
||||
# AI Translated
|
||||
msgid "Device (Web)"
|
||||
msgstr "Thiết bị (Web)"
|
||||
|
||||
msgid "Yes"
|
||||
msgstr "Có"
|
||||
|
||||
@@ -7513,6 +7522,22 @@ msgstr "File .gcode.3mf không chứa dữ liệu G-code. Vui lòng slice với
|
||||
msgid "File '%s' was lost! Please download it again."
|
||||
msgstr "File '%s' đã bị mất! Vui lòng tải lại."
|
||||
|
||||
msgid "gcodes"
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to delete the file from printer."
|
||||
msgstr ""
|
||||
|
||||
#, c-format, boost-format
|
||||
msgid "Do you want to print the file '%s' from printer?"
|
||||
msgstr ""
|
||||
|
||||
msgid "Print file"
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to start the print job."
|
||||
msgstr ""
|
||||
|
||||
#, c-format, boost-format
|
||||
msgid ""
|
||||
"File: %s\n"
|
||||
@@ -11887,6 +11912,12 @@ msgstr "Hủy timelapse & In"
|
||||
msgid "Clean Up"
|
||||
msgstr "Dọn dẹp"
|
||||
|
||||
msgid "AMS filament mapping is not available for this printer. Clear the AMS mapping before printing."
|
||||
msgstr ""
|
||||
|
||||
msgid "A filament used by this print has no AMS mapping. Assign it before printing."
|
||||
msgstr ""
|
||||
|
||||
msgid "Preparing print job"
|
||||
msgstr "Đang chuẩn bị công việc in"
|
||||
|
||||
@@ -32295,6 +32326,10 @@ msgstr "Bật công cụ nhà phát triển"
|
||||
msgid "An error occurred loading %s"
|
||||
msgstr "Đã xảy ra lỗi khi tải %s"
|
||||
|
||||
# AI Translated
|
||||
msgid "/"
|
||||
msgstr "/"
|
||||
|
||||
# AI Translated
|
||||
#, boost-format
|
||||
msgid "Load %1% to %2%"
|
||||
@@ -34398,10 +34433,6 @@ msgstr ""
|
||||
#~ msgid "An error occurred loading "
|
||||
#~ msgstr "Đã xảy ra lỗi khi tải "
|
||||
|
||||
# AI Translated
|
||||
#~ msgid "/"
|
||||
#~ msgstr "/"
|
||||
|
||||
# AI Translated
|
||||
#, c-format, boost-format
|
||||
#~ msgid "Load %s to "
|
||||
|
||||
@@ -6,7 +6,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Slic3rPE\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-10-11 16:21+0800\n"
|
||||
"POT-Creation-Date: 2026-10-12 19:00+0800\n"
|
||||
"PO-Revision-Date: 2026-06-11 12:37-0300\n"
|
||||
"Last-Translator: Handle <mail@bysb.net>\n"
|
||||
"Language-Team: \n"
|
||||
@@ -4162,6 +4162,16 @@ msgstr "最近的项目"
|
||||
msgid "Go to Plate"
|
||||
msgstr "转到盘"
|
||||
|
||||
msgid "Device"
|
||||
msgstr "设备"
|
||||
|
||||
# AI Translated
|
||||
msgid "Device (Web)"
|
||||
msgstr "设备(网页)"
|
||||
|
||||
msgid "Multi-device"
|
||||
msgstr "多设备"
|
||||
|
||||
msgid "AMS Materials Setting"
|
||||
msgstr "AMS 材料设置"
|
||||
|
||||
@@ -4210,6 +4220,9 @@ msgstr "您确定要清除耗材丝信息吗?"
|
||||
msgid "You need to select the material type and color first."
|
||||
msgstr "您需要先选择材料类型和颜色。"
|
||||
|
||||
msgid "The minimum temperature cannot be greater than the maximum temperature."
|
||||
msgstr ""
|
||||
|
||||
#, c-format, boost-format
|
||||
msgid "Please input a valid value (K in %.1f~%.1f)"
|
||||
msgstr "请输入有效的数值(K的范围为%.1f~%.1f)"
|
||||
@@ -4218,6 +4231,12 @@ msgstr "请输入有效的数值(K的范围为%.1f~%.1f)"
|
||||
msgid "Please input a valid value (K in %.1f~%.1f, N in %.1f~%.1f)"
|
||||
msgstr "请输入有效的数值(K的范围为%.1f~%.1f,N的范围为%.1f~%.1f)"
|
||||
|
||||
msgid "Favorite filaments"
|
||||
msgstr ""
|
||||
|
||||
msgid "All filaments"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"The nozzle flow is not set. Please set the nozzle flow rate before editing the filament.\n"
|
||||
"'Device -> Print parts'"
|
||||
@@ -6298,19 +6317,9 @@ msgstr "准备"
|
||||
msgid "Preview"
|
||||
msgstr "预览"
|
||||
|
||||
msgid "Device"
|
||||
msgstr "设备"
|
||||
|
||||
msgid "Multi-device"
|
||||
msgstr "多设备"
|
||||
|
||||
msgid "Project"
|
||||
msgstr "项目"
|
||||
|
||||
# AI Translated
|
||||
msgid "Device (Web)"
|
||||
msgstr "设备(网页)"
|
||||
|
||||
msgid "Yes"
|
||||
msgstr "是"
|
||||
|
||||
@@ -7006,6 +7015,22 @@ msgstr ".gcode.3mf文件中不包含G-code数据。请使用Orca Slicer进行切
|
||||
msgid "File '%s' was lost! Please download it again."
|
||||
msgstr "文件%s丢失,请重新下载。"
|
||||
|
||||
msgid "gcodes"
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to delete the file from printer."
|
||||
msgstr ""
|
||||
|
||||
#, c-format, boost-format
|
||||
msgid "Do you want to print the file '%s' from printer?"
|
||||
msgstr ""
|
||||
|
||||
msgid "Print file"
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to start the print job."
|
||||
msgstr ""
|
||||
|
||||
#, c-format, boost-format
|
||||
msgid ""
|
||||
"File: %s\n"
|
||||
@@ -11115,6 +11140,12 @@ msgstr "取消延时摄影,继续打印"
|
||||
msgid "Clean Up"
|
||||
msgstr "清理"
|
||||
|
||||
msgid "AMS filament mapping is not available for this printer. Clear the AMS mapping before printing."
|
||||
msgstr ""
|
||||
|
||||
msgid "A filament used by this print has no AMS mapping. Assign it before printing."
|
||||
msgstr ""
|
||||
|
||||
msgid "Preparing print job"
|
||||
msgstr "正在准备打印任务"
|
||||
|
||||
@@ -12776,6 +12807,7 @@ msgstr "未勾选"
|
||||
msgid "Select filtered"
|
||||
msgstr "选择筛选项"
|
||||
|
||||
# AI Translated
|
||||
msgid "Clear all"
|
||||
msgstr "清空"
|
||||
|
||||
@@ -31147,6 +31179,10 @@ msgstr "启用开发者工具"
|
||||
msgid "An error occurred loading %s"
|
||||
msgstr "加载 %s 时出错"
|
||||
|
||||
# AI Translated
|
||||
msgid "/"
|
||||
msgstr "/"
|
||||
|
||||
# AI Translated
|
||||
#, boost-format
|
||||
msgid "Load %1% to %2%"
|
||||
@@ -33214,10 +33250,6 @@ msgstr ""
|
||||
#~ msgid "An error occurred loading "
|
||||
#~ msgstr "加载时出错: "
|
||||
|
||||
# AI Translated
|
||||
#~ msgid "/"
|
||||
#~ msgstr "/"
|
||||
|
||||
# AI Translated
|
||||
#, c-format, boost-format
|
||||
#~ msgid "Load %s to "
|
||||
|
||||
@@ -9,7 +9,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Orca Slicer\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-10-11 16:21+0800\n"
|
||||
"POT-Creation-Date: 2026-10-12 19:00+0800\n"
|
||||
"PO-Revision-Date: 2025-11-28 13:48-0600\n"
|
||||
"Last-Translator: tntchn <15895303+tntchn@users.noreply.github.com>\n"
|
||||
"Language-Team: \n"
|
||||
@@ -4237,6 +4237,16 @@ msgstr "最近的專案"
|
||||
msgid "Go to Plate"
|
||||
msgstr "前往列印板"
|
||||
|
||||
msgid "Device"
|
||||
msgstr "裝置"
|
||||
|
||||
# AI Translated
|
||||
msgid "Device (Web)"
|
||||
msgstr "裝置(網頁)"
|
||||
|
||||
msgid "Multi-device"
|
||||
msgstr "多臺裝置"
|
||||
|
||||
msgid "AMS Materials Setting"
|
||||
msgstr "AMS 線材設定"
|
||||
|
||||
@@ -4288,6 +4298,9 @@ msgstr "您確認要清除線材資訊嗎?"
|
||||
msgid "You need to select the material type and color first."
|
||||
msgstr "您需要先選擇線材類型和顏色。"
|
||||
|
||||
msgid "The minimum temperature cannot be greater than the maximum temperature."
|
||||
msgstr ""
|
||||
|
||||
#, c-format, boost-format
|
||||
msgid "Please input a valid value (K in %.1f~%.1f)"
|
||||
msgstr "請輸入有效的數值 (K於 %.1f~%.1f 之間)"
|
||||
@@ -4296,6 +4309,12 @@ msgstr "請輸入有效的數值 (K於 %.1f~%.1f 之間)"
|
||||
msgid "Please input a valid value (K in %.1f~%.1f, N in %.1f~%.1f)"
|
||||
msgstr "請輸入有效的數值 (K於 %.1f~%.1f 之間,N於 %.1f~%.1f 之間)"
|
||||
|
||||
msgid "Favorite filaments"
|
||||
msgstr ""
|
||||
|
||||
msgid "All filaments"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"The nozzle flow is not set. Please set the nozzle flow rate before editing the filament.\n"
|
||||
"'Device -> Print parts'"
|
||||
@@ -6381,19 +6400,9 @@ msgstr "準備"
|
||||
msgid "Preview"
|
||||
msgstr "預覽"
|
||||
|
||||
msgid "Device"
|
||||
msgstr "裝置"
|
||||
|
||||
msgid "Multi-device"
|
||||
msgstr "多臺裝置"
|
||||
|
||||
msgid "Project"
|
||||
msgstr "專案"
|
||||
|
||||
# AI Translated
|
||||
msgid "Device (Web)"
|
||||
msgstr "裝置(網頁)"
|
||||
|
||||
msgid "Yes"
|
||||
msgstr "是"
|
||||
|
||||
@@ -7088,6 +7097,22 @@ msgstr ".gcode.3mf 檔案不包含 G-code 資料。請使用 OrcaSlicer 切片
|
||||
msgid "File '%s' was lost! Please download it again."
|
||||
msgstr "檔案 %s 遺失,請重新下載。"
|
||||
|
||||
msgid "gcodes"
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to delete the file from printer."
|
||||
msgstr ""
|
||||
|
||||
#, c-format, boost-format
|
||||
msgid "Do you want to print the file '%s' from printer?"
|
||||
msgstr ""
|
||||
|
||||
msgid "Print file"
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to start the print job."
|
||||
msgstr ""
|
||||
|
||||
#, c-format, boost-format
|
||||
msgid ""
|
||||
"File: %s\n"
|
||||
@@ -11203,6 +11228,12 @@ msgstr "取消縮時攝影並列印"
|
||||
msgid "Clean Up"
|
||||
msgstr "清理"
|
||||
|
||||
msgid "AMS filament mapping is not available for this printer. Clear the AMS mapping before printing."
|
||||
msgstr ""
|
||||
|
||||
msgid "A filament used by this print has no AMS mapping. Assign it before printing."
|
||||
msgstr ""
|
||||
|
||||
msgid "Preparing print job"
|
||||
msgstr "正在準備列印作業"
|
||||
|
||||
@@ -12852,6 +12883,7 @@ msgstr ""
|
||||
msgid "Select filtered"
|
||||
msgstr ""
|
||||
|
||||
# AI Translated
|
||||
msgid "Clear all"
|
||||
msgstr "清空"
|
||||
|
||||
@@ -30937,6 +30969,10 @@ msgstr "啟用開發者工具"
|
||||
msgid "An error occurred loading %s"
|
||||
msgstr "載入 %s 時發生錯誤"
|
||||
|
||||
# AI Translated
|
||||
msgid "/"
|
||||
msgstr "/"
|
||||
|
||||
# AI Translated
|
||||
#, boost-format
|
||||
msgid "Load %1% to %2%"
|
||||
@@ -33014,10 +33050,6 @@ msgstr ""
|
||||
#~ msgid "An error occurred loading "
|
||||
#~ msgstr "載入時發生錯誤: "
|
||||
|
||||
# AI Translated
|
||||
#~ msgid "/"
|
||||
#~ msgstr "/"
|
||||
|
||||
# AI Translated
|
||||
#, c-format, boost-format
|
||||
#~ msgid "Load %s to "
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
<svg width="28" height="27" viewBox="0 0 28 27" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path transform="matrix(0.9 0 0 0.9 1.4 1.35)" d="M11.9888 2.01601C12.8984 0.494662 15.1024 0.494662 16.012 2.01601L18.8677 6.79237C19.1958 7.34113 19.7339 7.73201 20.3572 7.87447L25.7821 9.11446C27.5101 9.50943 28.1913 11.6056 27.0254 12.9408L23.3652 17.1327C22.9449 17.6143 22.7394 18.2469 22.7964 18.8836L23.2936 24.4263C23.4519 26.1918 21.6687 27.4873 20.0386 26.7911L14.9209 24.6056C14.3329 24.3545 13.6679 24.3545 13.0799 24.6056L7.96209 26.7911C6.33196 27.4873 4.54887 26.1918 4.70722 24.4263L5.20434 18.8836C5.26145 18.2469 5.05595 17.6143 4.63544 17.1327L0.97533 12.9408C-0.19048 11.6056 0.490599 9.50943 2.21859 9.11446L7.64364 7.87447C8.2669 7.73201 8.80494 7.34113 9.13302 6.79237L11.9888 2.01601Z" stroke="#949494" stroke-width="2.4" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 876 B |
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "Qidi",
|
||||
"version": "02.04.00.16",
|
||||
"version": "02.04.00.17",
|
||||
"force_update": "0",
|
||||
"description": "Qidi configurations",
|
||||
"machine_model_list": [
|
||||
|
||||
@@ -83,7 +83,7 @@
|
||||
"extruder_clearance_height_to_lid": "120",
|
||||
"layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nLOG_Z\nG92 E0\n",
|
||||
"machine_end_gcode": "M141 S0\nM104 S0\nM140 S0\nG1 E-3 F1800\nG0 Z{min(max_print_height, max_layer_z + 3)} F600\nG0 X0 Y0 F12000\n{if max_layer_z < max_print_height / 2}G1 Z{max_print_height / 2 + 10} F600{else}G1 Z{min(max_print_height, max_layer_z + 3)}{endif}",
|
||||
"machine_start_gcode": "PRINT_START BED=[hot_plate_temp_initial_layer] HOTEND=[nozzle_temperature_initial_layer] CHAMBER=[chamber_temperature]\nM83\nM140 S[hot_plate_temp_initial_layer]\nM104 S[nozzle_temperature_initial_layer]\nG4 P3000\nG0 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85),0)} Y{max((min(print_bed_max[1] - 5, first_layer_print_min[1] + 80) - 85), 0)} Z5 F6000\nG0 Z[initial_layer_print_height] F600\nG1 E3 F1800\nG1 X{(min(print_bed_max[0], first_layer_print_min[0] + 80))} E{85 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 Y{max((min(print_bed_max[1] - 5, first_layer_print_min[1] + 80) - 85), 0) + 2} E{2 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85), 0)} E{85 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 Y{max((min(print_bed_max[1] - 5, first_layer_print_min[1] + 80) - 85), 0) + 85} E{83 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85), 0) + 2} E{2 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 Y{max((min(print_bed_max[1] - 5, first_layer_print_min[1] + 80) - 85), 0) + 3} E{82 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85), 0) + 3} Z0\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85), 0) + 6}\nG1 Z1 F600\n",
|
||||
"machine_start_gcode": "PRINT_START BED=[bed_temperature_initial_layer_single] HOTEND=[nozzle_temperature_initial_layer] CHAMBER=[chamber_temperature]\nM83\nM140 S[bed_temperature_initial_layer_single]\nM104 S[nozzle_temperature_initial_layer]\nG4 P3000\nG0 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85),0)} Y{max((min(print_bed_max[1] - 5, first_layer_print_min[1] + 80) - 85), 0)} Z5 F6000\nG0 Z[initial_layer_print_height] F600\nG1 E3 F1800\nG1 X{(min(print_bed_max[0], first_layer_print_min[0] + 80))} E{85 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 Y{max((min(print_bed_max[1] - 5, first_layer_print_min[1] + 80) - 85), 0) + 2} E{2 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85), 0)} E{85 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 Y{max((min(print_bed_max[1] - 5, first_layer_print_min[1] + 80) - 85), 0) + 85} E{83 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85), 0) + 2} E{2 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 Y{max((min(print_bed_max[1] - 5, first_layer_print_min[1] + 80) - 85), 0) + 3} E{82 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85), 0) + 3} Z0\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85), 0) + 6}\nG1 Z1 F600\n",
|
||||
"thumbnails_format": "PNG",
|
||||
"default_filament_profile": [
|
||||
"Generic PLA @Qidi Q1 Pro 0.4 nozzle"
|
||||
|
||||
@@ -100,7 +100,7 @@
|
||||
"default_filament_profile": [
|
||||
"Generic PLA @Qidi"
|
||||
],
|
||||
"machine_start_gcode": "G28\nM140 S[hot_plate_temp_initial_layer]\nM190 S[hot_plate_temp_initial_layer]\nM109 S[nozzle_temperature_initial_layer]\nG92 E-19\nG0 Y5 Z0.3 F3600\nG1 X5 E0 F2400\n",
|
||||
"machine_start_gcode": "G28\nM140 S[bed_temperature_initial_layer_single]\nM190 S[bed_temperature_initial_layer_single]\nM109 S[nozzle_temperature_initial_layer]\nG92 E-19\nG0 Y5 Z0.3 F3600\nG1 X5 E0 F2400\n",
|
||||
"machine_end_gcode": "M104 S0\nM140 S0\n;Retract the filament\nG92 E0\nG1 E-3 F300\nG28\nM84\n",
|
||||
"scan_first_layer": "0"
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
"210x210/COLPIC",
|
||||
"110x110/PNG"
|
||||
],
|
||||
"machine_start_gcode": "PRINT_START\nG28\nM141 S0\nG0 Z50 F600\nM190 S[hot_plate_temp_initial_layer]\nG28 Z\nG29; mesh bed leveling ,comment this code to close it\nG0 X0 Y0 Z50 F6000\nM141 S{overall_chamber_temperature}\nM109 S[nozzle_temperature_initial_layer]\nM106 P3 S255\nM83\nG4 P3000\nG0 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85),0)} Y{max((min(print_bed_max[1], first_layer_print_min[1] + 80) - 85),0)} Z5 F6000\nG0 Z[initial_layer_print_height] F600\nG1 E3 F1800\nG1 X{(min(print_bed_max[0], first_layer_print_min[0] + 80))} E{85 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 Y{max((min(print_bed_max[1], first_layer_print_min[1] + 80) - 85),0) + 2} E{2 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85),0)} E{85 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 Y{max((min(print_bed_max[1], first_layer_print_min[1] + 80) - 85),0) + 85} E{83 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85),0) + 2} E{2 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 Y{max((min(print_bed_max[1], first_layer_print_min[1] + 80) - 85),0) + 3} E{82 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85),0) + 12} E{-10 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 E{10 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\n",
|
||||
"machine_start_gcode": "PRINT_START\nG28\nM141 S0\nG0 Z50 F600\nM190 S[bed_temperature_initial_layer_single]\nG28 Z\nG29; mesh bed leveling ,comment this code to close it\nG0 X0 Y0 Z50 F6000\nM141 S{overall_chamber_temperature}\nM109 S[nozzle_temperature_initial_layer]\nM106 P3 S255\nM83\nG4 P3000\nG0 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85),0)} Y{max((min(print_bed_max[1], first_layer_print_min[1] + 80) - 85),0)} Z5 F6000\nG0 Z[initial_layer_print_height] F600\nG1 E3 F1800\nG1 X{(min(print_bed_max[0], first_layer_print_min[0] + 80))} E{85 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 Y{max((min(print_bed_max[1], first_layer_print_min[1] + 80) - 85),0) + 2} E{2 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85),0)} E{85 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 Y{max((min(print_bed_max[1], first_layer_print_min[1] + 80) - 85),0) + 85} E{83 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85),0) + 2} E{2 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 Y{max((min(print_bed_max[1], first_layer_print_min[1] + 80) - 85),0) + 3} E{82 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85),0) + 12} E{-10 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 E{10 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\n",
|
||||
"machine_end_gcode": "M141 S0\nM104 S0\nM140 S0\nG1 E-3 F1800\nG0 Z{min(max_print_height, max_layer_z + 3)} F600\nG0 X0 Y0 F12000\n{if max_layer_z < max_print_height / 2}G1 Z{max_print_height / 2 + 10} F600{else}G1 Z{min(max_print_height, max_layer_z + 3)}{endif}",
|
||||
"scan_first_layer": "0"
|
||||
}
|
||||
|
||||
@@ -126,6 +126,13 @@
|
||||
</array>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>10.10</string>
|
||||
<key>NSLocalNetworkUsageDescription</key>
|
||||
<string>OrcaSlicer uses your local network to discover and connect to printers.</string>
|
||||
<key>NSBonjourServices</key>
|
||||
<array>
|
||||
<string>_octoprint._tcp</string>
|
||||
<string>_http._tcp</string>
|
||||
</array>
|
||||
<key>NSPrincipalClass</key>
|
||||
<string>NSApplication</string>
|
||||
<key>NSHighResolutionCapable</key>
|
||||
|
||||
@@ -1729,8 +1729,8 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
//plate->pattern_file = it->second->pattern_file;
|
||||
plate->no_light_thumbnail_file = it->second->no_light_thumbnail_file;
|
||||
plate->top_file = it->second->top_file;
|
||||
plate->pick_file = it->second->pick_file.empty();
|
||||
plate->pattern_bbox_file = it->second->pattern_bbox_file.empty();
|
||||
plate->pick_file = it->second->pick_file;
|
||||
plate->pattern_bbox_file = it->second->pattern_bbox_file;
|
||||
plate->config = it->second->config;
|
||||
|
||||
if (!plate->thumbnail_file.empty())
|
||||
|
||||
@@ -3295,7 +3295,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato
|
||||
m_is_role_based_fan_on.fill(false);
|
||||
m_role_based_fan_marker_layer.fill(-1);
|
||||
|
||||
m_fan_mover.release();
|
||||
m_fan_mover.reset();
|
||||
m_ordering_cache.clear();
|
||||
|
||||
// Belt printer: initialize belt-specific writer via virtual hook.
|
||||
|
||||
+111
-36
@@ -4102,7 +4102,11 @@ void PresetBundle::get_ams_cobox_infos(AMSComboInfo& combox_info)
|
||||
auto ams_name = ams.opt_string("tray_name", 0u);
|
||||
auto filament_changed = !ams.has("filament_changed") || ams.opt_bool("filament_changed");
|
||||
auto filament_multi_color = ams.opt<ConfigOptionStrings>("filament_multi_colour")->values;
|
||||
if (filament_id.empty()) {
|
||||
auto filament_type = ams.opt_string("filament_type", 0u);
|
||||
auto is_placeholder = ams.has("filament_slot_placeholder") && ams.opt_bool("filament_slot_placeholder", 0u);
|
||||
// A printer-set tray carries a material type but no preset id; resolve it to the
|
||||
// matching Generic preset so it is not dropped and the combo array keeps aligning.
|
||||
if (filament_id.empty() && (is_placeholder || filament_type.empty())) {
|
||||
continue;
|
||||
}
|
||||
if (!filament_changed && this->filament_presets.size() > combox_info.ams_filament_presets.size()) {
|
||||
@@ -4112,16 +4116,48 @@ void PresetBundle::get_ams_cobox_infos(AMSComboInfo& combox_info)
|
||||
combox_info.ams_names.push_back(ams_name);
|
||||
continue;
|
||||
}
|
||||
auto iter = std::find_if(filaments.begin(), filaments.end(),
|
||||
[this, &filament_id](auto &f) { return f.is_compatible && filaments.get_preset_base(f) == &f && f.filament_id == filament_id; });
|
||||
warn_ambiguous_filament_id_match(filaments, iter, filament_id);
|
||||
auto iter = filaments.end();
|
||||
if (!filament_id.empty()) {
|
||||
iter = std::find_if(filaments.begin(), filaments.end(),
|
||||
[this, &filament_id](auto &f) { return f.is_compatible && filaments.get_preset_base(f) == &f && f.filament_id == filament_id; });
|
||||
warn_ambiguous_filament_id_match(filaments, iter, filament_id);
|
||||
}
|
||||
if (iter == filaments.end()) {
|
||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": filament_id %1% not found or system or compatible") % filament_id;
|
||||
auto filament_type = ams.opt_string("filament_type", 0u);
|
||||
if (!filament_type.empty()) {
|
||||
auto original_type = filament_type;
|
||||
filament_type = "Generic " + filament_type;
|
||||
iter = std::find_if(filaments.begin(), filaments.end(),
|
||||
[&filament_type](auto &f) { return f.is_compatible && f.is_system && boost::algorithm::starts_with(f.name, filament_type); });
|
||||
if (iter == filaments.end()) {
|
||||
// Similarity fallback: find a generic preset whose filament_type
|
||||
// appears as a whole word in the AMS type (e.g. "ASA" in "ASA Sparkle").
|
||||
auto upper_type = boost::to_upper_copy(original_type);
|
||||
auto contains_word = [](const std::string& haystack, const std::string& needle) {
|
||||
auto pos = haystack.find(needle);
|
||||
while (pos != std::string::npos) {
|
||||
bool start_ok = (pos == 0 || !std::isalnum(static_cast<unsigned char>(haystack[pos - 1])));
|
||||
bool end_ok = (pos + needle.size() >= haystack.size() ||
|
||||
!std::isalnum(static_cast<unsigned char>(haystack[pos + needle.size()])));
|
||||
if (start_ok && end_ok)
|
||||
return true;
|
||||
pos = haystack.find(needle, pos + 1);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
// Find the longest-matching preset type to prefer e.g. "PA-CF" over "PA".
|
||||
size_t best_len = 0;
|
||||
for (auto it = filaments.begin(); it != filaments.end(); ++it) {
|
||||
if (!it->is_compatible || !it->is_system || !boost::algorithm::starts_with(it->name, "Generic "))
|
||||
continue;
|
||||
auto preset_type = boost::to_upper_copy(it->config.opt_string("filament_type", 0u));
|
||||
if (preset_type.size() > best_len && contains_word(upper_type, preset_type)) {
|
||||
iter = it;
|
||||
best_len = preset_type.size();
|
||||
filament_type = "Generic " + it->config.opt_string("filament_type", 0u);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (iter == filaments.end()) {
|
||||
// Prefer old selection
|
||||
@@ -4132,7 +4168,11 @@ void PresetBundle::get_ams_cobox_infos(AMSComboInfo& combox_info)
|
||||
combox_info.ams_names.push_back(ams_name);
|
||||
continue;
|
||||
}
|
||||
iter = std::find_if(filaments.begin(), filaments.end(), [](auto &f) { return f.is_compatible && f.is_system; });
|
||||
iter = std::find_if(filaments.begin(), filaments.end(), [](auto &f) {
|
||||
return f.is_compatible && f.is_system && boost::algorithm::starts_with(f.name, "Generic ");
|
||||
});
|
||||
if (iter == filaments.end())
|
||||
iter = std::find_if(filaments.begin(), filaments.end(), [](auto &f) { return f.is_compatible && f.is_system; });
|
||||
if (iter == filaments.end())
|
||||
continue;
|
||||
}
|
||||
@@ -4170,6 +4210,7 @@ unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfi
|
||||
for (auto &entry : filament_ams_list) {
|
||||
auto & ams = entry.second;
|
||||
auto filament_id = ams.opt_string("filament_id", 0u);
|
||||
const bool printer_set_tray = filament_id.empty();
|
||||
auto filament_color = ams.opt_string("filament_colour", 0u);
|
||||
auto filament_color_type = ams.opt_string("filament_colour_type", 0u);
|
||||
auto filament_changed = !ams.has("filament_changed") || ams.opt_bool("filament_changed");
|
||||
@@ -4182,42 +4223,58 @@ unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfi
|
||||
ams_array_maps.push_back(temp);
|
||||
index++;
|
||||
if (filament_id.empty()) {
|
||||
if (use_map) {
|
||||
for (int j = maps.size() - 1; j >= 0; j--) {
|
||||
if (maps[j].slot_id == slot_id && maps[j].ams_id == ams_id) {
|
||||
maps.erase(j);
|
||||
// A tray written by the printer UI carries a material type but no
|
||||
// OrcaSlicer preset id. Resolve it to the matching Generic preset with
|
||||
// the tray's own color instead of dropping it (direct sync) or forcing
|
||||
// "Generic PLA" (mapping sync). Placeholders and typeless trays keep
|
||||
// the previous behavior.
|
||||
const auto tray_type = ams.opt_string("filament_type", 0u);
|
||||
if (is_placeholder || tray_type.empty()) {
|
||||
if (use_map) {
|
||||
for (int j = maps.size() - 1; j >= 0; j--) {
|
||||
if (maps[j].slot_id == slot_id && maps[j].ams_id == ams_id) {
|
||||
maps.erase(j);
|
||||
}
|
||||
}
|
||||
ams_filament_presets.push_back("Generic PLA");//for unknow matieral
|
||||
auto default_unknown_color = "#CECECE";
|
||||
ams_filament_colors.push_back(default_unknown_color);
|
||||
ams_filament_color_types.push_back("1");
|
||||
if (filament_multi_color.size() == 0) {
|
||||
filament_multi_color.push_back(default_unknown_color);
|
||||
}
|
||||
ams_multi_color_filment.push_back(filament_multi_color);
|
||||
} else if (is_placeholder) {
|
||||
// Orca: push placeholders to keep index alignment with ams_infos
|
||||
ams_filament_presets.push_back("");
|
||||
ams_filament_colors.push_back("");
|
||||
ams_filament_color_types.push_back("");
|
||||
ams_multi_color_filment.push_back({});
|
||||
}
|
||||
ams_filament_presets.push_back("Generic PLA");//for unknow matieral
|
||||
auto default_unknown_color = "#CECECE";
|
||||
ams_filament_colors.push_back(default_unknown_color);
|
||||
ams_filament_color_types.push_back("1");
|
||||
if (filament_multi_color.size() == 0) {
|
||||
filament_multi_color.push_back(default_unknown_color);
|
||||
}
|
||||
ams_multi_color_filment.push_back(filament_multi_color);
|
||||
} else if (is_placeholder) {
|
||||
// Orca: push placeholders to keep index alignment with ams_infos
|
||||
ams_filament_presets.push_back("");
|
||||
ams_filament_colors.push_back("");
|
||||
ams_filament_color_types.push_back("");
|
||||
ams_multi_color_filment.push_back({});
|
||||
continue;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (!filament_changed && this->filament_presets.size() > ams_filament_presets.size()) {
|
||||
ams_filament_presets.push_back(this->filament_presets[ams_filament_presets.size()]);
|
||||
ams_filament_colors.push_back(filament_color);
|
||||
ams_filament_color_types.push_back(filament_color_type);
|
||||
ams_multi_color_filment.push_back(filament_multi_color);
|
||||
ams_infos.back().valid = true;
|
||||
continue;
|
||||
}
|
||||
bool has_type = false;
|
||||
auto filament_type = ams.opt_string("filament_type", 0u);
|
||||
auto iter = std::find_if(filaments.begin(), filaments.end(), [this, &filament_id, &has_type, filament_type](auto &f) {
|
||||
has_type |= f.config.opt_string("filament_type", 0u) == filament_type;
|
||||
return f.is_compatible && filaments.get_preset_base(f) == &f && f.filament_id == filament_id; });
|
||||
warn_ambiguous_filament_id_match(filaments, iter, filament_id);
|
||||
auto iter = filaments.end();
|
||||
if (!filament_id.empty()) {
|
||||
iter = std::find_if(filaments.begin(), filaments.end(), [this, &filament_id, &has_type, filament_type](auto &f) {
|
||||
has_type |= f.config.opt_string("filament_type", 0u) == filament_type;
|
||||
return f.is_compatible && filaments.get_preset_base(f) == &f && f.filament_id == filament_id; });
|
||||
warn_ambiguous_filament_id_match(filaments, iter, filament_id);
|
||||
} else {
|
||||
// The material type is the only identity a printer-set tray carries.
|
||||
has_type = std::any_of(filaments.begin(), filaments.end(), [&filament_type](auto &f) {
|
||||
return f.is_compatible && f.config.opt_string("filament_type", 0u) == filament_type; });
|
||||
}
|
||||
if (iter == filaments.end()) {
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": filament_id %1% not found or system or compatible") % filament_id;
|
||||
if (!filament_type.empty()) {
|
||||
@@ -4257,6 +4314,7 @@ unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfi
|
||||
}
|
||||
}
|
||||
}
|
||||
const bool resolved_by_material_type = printer_set_tray && iter != filaments.end();
|
||||
if (iter == filaments.end()) {
|
||||
// Prefer old selection
|
||||
if (ams_filament_presets.size() < this->filament_presets.size()) {
|
||||
@@ -4264,6 +4322,7 @@ unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfi
|
||||
ams_filament_colors.push_back(filament_color);
|
||||
ams_filament_color_types.push_back(filament_color_type);
|
||||
ams_multi_color_filment.push_back(filament_multi_color);
|
||||
ams_infos.back().valid = true;
|
||||
unknowns.emplace_back(&ams, has_type ? L("The filament may not be compatible with the current machine settings. Generic filament presets will be used.") :
|
||||
L("The filament model is unknown. Still using the previous filament preset."));
|
||||
continue;
|
||||
@@ -4279,7 +4338,8 @@ unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfi
|
||||
if (iter == filaments.end())
|
||||
continue;
|
||||
}
|
||||
unknowns.emplace_back(&ams, boost::algorithm::starts_with(iter->name, filament_type) ?
|
||||
if (!resolved_by_material_type)
|
||||
unknowns.emplace_back(&ams, boost::algorithm::starts_with(iter->name, filament_type) ?
|
||||
(has_type ? L("The filament may not be compatible with the current machine settings. Generic filament presets will be used.") :
|
||||
L("The filament model is unknown. Generic filament presets will be used.")) :
|
||||
(has_type ? L("The filament may not be compatible with the current machine settings. A random filament preset will be used.") :
|
||||
@@ -4290,6 +4350,7 @@ unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfi
|
||||
ams_filament_colors.push_back(filament_color);
|
||||
ams_filament_color_types.push_back(filament_color_type);
|
||||
ams_multi_color_filment.push_back(filament_multi_color);
|
||||
ams_infos.back().valid = true;
|
||||
}
|
||||
if (ams_filament_presets.empty())
|
||||
return 0;
|
||||
@@ -4729,9 +4790,21 @@ std::vector<std::vector<DynamicPrintConfig>> PresetBundle::get_extruder_filament
|
||||
return filament_infos;
|
||||
}
|
||||
|
||||
// ORCA TODO: currently, this function assumes the printer name follows the pattern of "<printer_model> <nozzle_diameter>", e.g.
|
||||
// printer_type: "Bambu Lab X2D", nozzle_diameter_str: "0.4 nozzle" => printer_name: "Bambu Lab X2D 0.4 nozzle". If the printer name does
|
||||
// not follow this pattern, the function may not work correctly.
|
||||
std::string PresetBundle::get_printer_model_display_name(const std::string &model_id) const
|
||||
{
|
||||
if (model_id.empty())
|
||||
return {};
|
||||
for (const auto &vendor_entry : vendors) {
|
||||
for (const auto &model : vendor_entry.second.models) {
|
||||
if (model.model_id == model_id)
|
||||
return model.name;
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
// ORCA TODO: this assumes printer names follow "<printer_model> <nozzle_diameter>", e.g.
|
||||
// "Bambu Lab X2D 0.4 nozzle". Other naming schemes may not resolve correctly.
|
||||
std::set<std::string> PresetBundle::get_printer_names_by_printer_type_and_nozzle(const std::string &printer_type, std::string nozzle_diameter_str, bool system_only)
|
||||
{
|
||||
std::set<std::string> printer_names;
|
||||
@@ -4753,7 +4826,9 @@ std::set<std::string> PresetBundle::get_printer_names_by_printer_type_and_nozzle
|
||||
if (printer_it->name.find(nozzle_diameter_str) != std::string::npos) printer_names.insert(printer_it->name);
|
||||
}
|
||||
|
||||
assert(printer_names.size() == 1);
|
||||
// No match is normal for a connected machine the user has not installed; only an
|
||||
// ambiguous match is a bug (the caller assumes one preset per model and nozzle).
|
||||
assert(printer_names.size() <= 1);
|
||||
|
||||
for (auto& printer_name : printer_names) {
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " " << __LINE__ << " printer name: " << printer_name;
|
||||
@@ -4766,8 +4841,8 @@ std::vector<Preset *> PresetBundle::get_filament_presets_for_machine(const std::
|
||||
const std::string &nozzle_diameter_str,
|
||||
bool include_user_presets)
|
||||
{
|
||||
// Printer model plus nozzle diameter is expected to resolve to a single system printer preset;
|
||||
// get_printer_names_by_printer_type_and_nozzle asserts as much in debug builds.
|
||||
// Printer model plus nozzle diameter normally resolves to a single system printer preset.
|
||||
// Zero matches is normal for a connected machine the user never installed.
|
||||
const std::set<std::string> printer_names = get_printer_names_by_printer_type_and_nozzle(printer_type, nozzle_diameter_str);
|
||||
const Preset *printer = printer_names.empty() ? nullptr : printers.find_preset(*printer_names.begin());
|
||||
if (printer == nullptr)
|
||||
|
||||
@@ -413,6 +413,9 @@ public:
|
||||
|
||||
std::vector<std::vector<DynamicPrintConfig>> get_extruder_filament_info() const;
|
||||
|
||||
// Resolve a vendor model id (an agent-reported machine identity) to the display name
|
||||
// printer profiles use as printer_model. Empty when no loaded vendor declares it.
|
||||
std::string get_printer_model_display_name(const std::string &model_id) const;
|
||||
std::set<std::string> get_printer_names_by_printer_type_and_nozzle(const std::string &printer_type, std::string nozzle_diameter_str, bool system_only = true);
|
||||
// Orca: the root filament presets a connected machine can use, resolved with the rule the rest
|
||||
// of the app applies (is_compatible_with_printer): an empty compatible_printers means every
|
||||
|
||||
@@ -1570,61 +1570,6 @@ std::vector<TextureIsland> compute_connected_net(const PatchUnwrap &unwrap)
|
||||
return islands;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> transfer_facet_paint(const indexed_triangle_set &src_mesh,
|
||||
const TriangleSelector::TriangleSplittingData &src_paint,
|
||||
const indexed_triangle_set &dst_mesh)
|
||||
{
|
||||
if (src_mesh.indices.empty() || dst_mesh.indices.empty() || src_paint.bitstream.empty())
|
||||
return {};
|
||||
|
||||
const TriangleMesh src(src_mesh);
|
||||
TriangleSelector selector(src);
|
||||
selector.deserialize(src_paint, false);
|
||||
|
||||
// Every sub-triangle of the source, each carrying its own state - NONE included, so the soup
|
||||
// covers the whole surface and a foot point always lands on something. One soup rather than one
|
||||
// tree per state: a foot point has one nearest triangle, and that triangle's state is the answer.
|
||||
std::vector<indexed_triangle_set> per_state;
|
||||
selector.get_facets(per_state);
|
||||
indexed_triangle_set soup;
|
||||
std::vector<uint8_t> label;
|
||||
for (size_t st = 0; st < per_state.size(); ++st) {
|
||||
const indexed_triangle_set &part = per_state[st];
|
||||
if (part.indices.empty())
|
||||
continue;
|
||||
const int base = int(soup.vertices.size());
|
||||
soup.vertices.insert(soup.vertices.end(), part.vertices.begin(), part.vertices.end());
|
||||
for (const stl_triangle_vertex_indices &t : part.indices) {
|
||||
soup.indices.emplace_back(t[0] + base, t[1] + base, t[2] + base);
|
||||
label.push_back(uint8_t(st));
|
||||
}
|
||||
}
|
||||
if (soup.indices.empty())
|
||||
return {};
|
||||
|
||||
const AABBTreeIndirect::Tree3f tree =
|
||||
AABBTreeIndirect::build_aabb_tree_over_indexed_triangle_set(soup.vertices, soup.indices);
|
||||
|
||||
std::vector<uint8_t> out(dst_mesh.indices.size(), 0);
|
||||
tbb::parallel_for(tbb::blocked_range<size_t>(0, dst_mesh.indices.size()),
|
||||
[&](const tbb::blocked_range<size_t> &range) {
|
||||
for (size_t j = range.begin(); j < range.end(); ++j) {
|
||||
const stl_triangle_vertex_indices &t = dst_mesh.indices[j];
|
||||
// The centroid, not a corner: a facet takes one state, and its middle is the point that
|
||||
// is inside it whatever the triangle looks like.
|
||||
const Vec3f centroid = (dst_mesh.vertices[size_t(t[0])] + dst_mesh.vertices[size_t(t[1])] +
|
||||
dst_mesh.vertices[size_t(t[2])]) / 3.f;
|
||||
size_t hit = 0;
|
||||
Vec3f foot;
|
||||
const float d2 = AABBTreeIndirect::squared_distance_to_indexed_triangle_set(soup.vertices, soup.indices,
|
||||
tree, centroid, hit, foot);
|
||||
if (d2 >= 0.f && hit < label.size())
|
||||
out[j] = label[hit];
|
||||
}
|
||||
});
|
||||
return out;
|
||||
}
|
||||
|
||||
void average_island_scales(std::vector<TextureIsland> &islands)
|
||||
{
|
||||
if (islands.empty())
|
||||
|
||||
@@ -578,31 +578,6 @@ bool sample_layer_color(const DecodedHeightTexture &texture, const TextureDispla
|
||||
const Vec3f &patch_center = Vec3f::Zero(), const Vec3f &patch_axis = Vec3f::UnitZ(),
|
||||
const Vec2f *lscm_uv = nullptr);
|
||||
|
||||
// Carries a per-facet paint annotation - the colour paint above all - from a mesh onto the mesh a
|
||||
// bake produced from it: every triangle of `dst_mesh` takes the state painted at its own foot point on
|
||||
// `src_mesh`, i.e. on the nearest point of the surface it was built from. Returned as one
|
||||
// EnforcerBlockerType per `dst_mesh` triangle, 0 (NONE) where that foot point carried no paint.
|
||||
//
|
||||
// ModelVolume::restore_painting() cannot do this. Its remap pairs a source paint triangle with a
|
||||
// target triangle that *overlaps* it to within 0.01 mm, which is right for a remesh or a
|
||||
// simplification - they leave the surface where it was - and useless after a bake, which moves the
|
||||
// surface by the relief depth. Measured on a model displaced by a tenth of its own size, that remap
|
||||
// kept a quarter of the colour and left the rest unpainted; this keeps essentially all of it, because
|
||||
// a foot point is exactly as far away as the relief is deep and the lookup does not care how far that
|
||||
// is.
|
||||
//
|
||||
// The source's *sub*-triangles are what is looked up, not its triangles, so the part of a triangle a
|
||||
// brush stroke only clipped a corner off keeps its own state rather than rounding to the whole
|
||||
// triangle's. No distance limit is needed or wanted: the nearest point on the source surface is where
|
||||
// a baked triangle came from however far the relief carried it, and an unpainted origin reports NONE
|
||||
// on its own.
|
||||
//
|
||||
// Returns an empty vector when `src_paint` is empty or either mesh is - the caller then has nothing to
|
||||
// carry, which is not the same as "carry nothing".
|
||||
std::vector<uint8_t> transfer_facet_paint(const indexed_triangle_set &src_mesh,
|
||||
const TriangleSelector::TriangleSplittingData &src_paint,
|
||||
const indexed_triangle_set &dst_mesh);
|
||||
|
||||
// Area-weighted centroid and average normal of a layer's currently painted patch, in mesh-local
|
||||
// coordinates - the same measurements build_texture_displacement() uses to pick its dominant
|
||||
// projection axis. Used by the GUI to anchor the on-canvas "adjust texture placement" gizmo to
|
||||
|
||||
@@ -372,6 +372,10 @@ set(SLIC3R_GUI_SOURCES
|
||||
GUI/MarkdownTip.hpp
|
||||
GUI/MediaFilePanel.cpp
|
||||
GUI/MediaFilePanel.h
|
||||
GUI/OrcaFilesPanel.cpp
|
||||
GUI/OrcaFilesPanel.h
|
||||
GUI/StoragePanel.cpp
|
||||
GUI/StoragePanel.h
|
||||
GUI/MediaPlayCtrl.cpp
|
||||
GUI/MediaPlayCtrl.h
|
||||
GUI/WebRtcMediaController.cpp
|
||||
@@ -463,6 +467,11 @@ set(SLIC3R_GUI_SOURCES
|
||||
GUI/PresetHints.hpp
|
||||
GUI/PrinterCloudAuthDialog.cpp
|
||||
GUI/PrinterCloudAuthDialog.hpp
|
||||
GUI/Printer/BambuFileGridModel.cpp
|
||||
GUI/Printer/BambuFileGridModel.h
|
||||
GUI/Printer/IFileGridModel.h
|
||||
GUI/Printer/RemoteFileGridModel.cpp
|
||||
GUI/Printer/RemoteFileGridModel.h
|
||||
GUI/Printer/PrinterFileSystem.cpp
|
||||
GUI/Printer/PrinterFileSystem.h
|
||||
GUI/PrinterWebView.cpp
|
||||
@@ -776,8 +785,12 @@ set(SLIC3R_GUI_SOURCES
|
||||
Utils/ICameraSignalingChannel.hpp
|
||||
Utils/OrcaCloudServiceAgent.cpp
|
||||
Utils/OrcaCloudServiceAgent.hpp
|
||||
Utils/OrcaMqttConnection.cpp
|
||||
Utils/OrcaMqttConnection.hpp
|
||||
Utils/OrcaPrinterAgent.cpp
|
||||
Utils/OrcaPrinterAgent.hpp
|
||||
Utils/OrcaCloudSignalingChannel.cpp
|
||||
Utils/OrcaCloudSignalingChannel.hpp
|
||||
Utils/QidiPrinterAgent.cpp
|
||||
Utils/QidiPrinterAgent.hpp
|
||||
Utils/SnapmakerPrinterAgent.cpp
|
||||
@@ -916,7 +929,7 @@ target_include_directories(libslic3r_gui PRIVATE Utils ${CMAKE_CURRENT_BINARY_DI
|
||||
|
||||
if (WIN32)
|
||||
target_include_directories(libslic3r_gui SYSTEM PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../../deps/WebView2/include)
|
||||
target_link_libraries(libslic3r_gui Advapi32)
|
||||
target_link_libraries(libslic3r_gui Advapi32 Crypt32)
|
||||
endif()
|
||||
|
||||
source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} FILES ${SLIC3R_GUI_SOURCES})
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include "slic3r/GUI/DeviceCore/DevExtruderSystem.h"
|
||||
|
||||
#include "slic3r/GUI/DeviceCore/DevManager.h"
|
||||
#include "slic3r/GUI/DeviceManager.hpp"
|
||||
|
||||
#include "slic3r/GUI/MsgDialog.hpp"
|
||||
|
||||
@@ -1633,8 +1634,16 @@ int AMSDryCtrWin::update_filament_list(DevAms* dev_ams, MachineObject* obj)
|
||||
stream << std::fixed << std::setprecision(1) << obj->GetExtderSystem()->GetNozzleDiameter(extruder_id);
|
||||
std::string nozzle_diameter_str = stream.str();
|
||||
|
||||
// The connected device's model may not resolve (OrcaSonar's is optional); the
|
||||
// helper falls back to the selected profile so the list is never empty.
|
||||
const std::string filament_printer_model = resolve_filament_printer_model(obj->printer_type, preset_bundle);
|
||||
if (filament_printer_model.empty()) {
|
||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << " cannot resolve a printer model for the filament list";
|
||||
return false;
|
||||
}
|
||||
|
||||
for (Preset *filament_it : preset_bundle->get_filament_presets_for_machine(
|
||||
DevPrinterConfigUtil::get_printer_display_name(obj->printer_type), nozzle_diameter_str, obj->is_support_user_preset)) {
|
||||
filament_printer_model, nozzle_diameter_str, obj->is_support_user_preset)) {
|
||||
if (!filament_id_set.insert(filament_it->filament_id).second)
|
||||
continue;
|
||||
const std::string filament_alias = filaments.get_preset_alias(*filament_it, true);
|
||||
|
||||
@@ -4,10 +4,12 @@
|
||||
#include "GUI_App.hpp"
|
||||
#include "GUI.hpp"
|
||||
#include "slic3r/Utils/NetworkAgent.hpp"
|
||||
#include "slic3r/Utils/NetworkAgentFactory.hpp"
|
||||
#include "libslic3r/Preset.hpp"
|
||||
#include "I18N.hpp"
|
||||
#include <algorithm>
|
||||
#include <boost/log/trivial.hpp>
|
||||
#include <slic3r/GUI/DeviceManager.hpp>
|
||||
#include <string>
|
||||
#include <sstream>
|
||||
#include <ios>
|
||||
@@ -16,6 +18,7 @@
|
||||
#include <wx/checklst.h>
|
||||
#include "slic3r/GUI/GUI_Utils.hpp"
|
||||
#include "slic3r/GUI/Widgets/Button.hpp"
|
||||
#include "slic3r/GUI/Widgets/DropDown.hpp"
|
||||
#include "slic3r/GUI/Widgets/Label.hpp"
|
||||
#include <utility>
|
||||
#include "slic3r/GUI/wxExtensions.hpp"
|
||||
@@ -172,7 +175,26 @@ void AMSMaterialsSetting::create()
|
||||
|
||||
Bind(wxEVT_PAINT, &AMSMaterialsSetting::paintEvent, this);
|
||||
Bind(EVT_SELECTED_COLOR, &AMSMaterialsSetting::on_picker_color, this);
|
||||
m_comboBox_filament->Connect(wxEVT_COMMAND_COMBOBOX_SELECTED, wxCommandEventHandler(AMSMaterialsSetting::on_select_filament), NULL, this);
|
||||
m_comboBox_filament->Connect(wxEVT_COMMAND_COMBOBOX_SELECTED, wxCommandEventHandler(AMSMaterialsSetting::on_select_filament), NULL, this);
|
||||
m_comboBox_filament->Bind(EVT_DROPDOWN_ITEM_ACTION, &AMSMaterialsSetting::on_toggle_favorite_filament, this);
|
||||
|
||||
// Favorites toggled in the open list are regrouped just before it opens again, so rows never move
|
||||
// under the cursor. These handlers run ahead of ComboBox's own, which then measure and show the
|
||||
// rebuilt rows; wxEVT_COMBOBOX_DROPDOWN would be too late, as ComboBox sends it after showing the list.
|
||||
auto regroup_favorites = [this]() {
|
||||
if (!m_favorites_regroup || m_comboBox_filament->is_drop_down())
|
||||
return;
|
||||
const FilamentInfos* info = selected_filament_info();
|
||||
const std::string filament_id = info ? info->filament_id : std::string();
|
||||
rebuild_filament_dropdown_items(filament_id);
|
||||
};
|
||||
m_comboBox_filament->Bind(wxEVT_LEFT_DOWN, [regroup_favorites](wxMouseEvent& e) { regroup_favorites(); e.Skip(); });
|
||||
m_comboBox_filament->Bind(wxEVT_LEFT_DCLICK, [regroup_favorites](wxMouseEvent& e) { regroup_favorites(); e.Skip(); });
|
||||
m_comboBox_filament->Bind(wxEVT_KEY_DOWN, [regroup_favorites](wxKeyEvent& e) {
|
||||
if (e.GetKeyCode() == WXK_RETURN || e.GetKeyCode() == WXK_SPACE) // the keys that open the list
|
||||
regroup_favorites();
|
||||
e.Skip();
|
||||
});
|
||||
|
||||
m_comboBox_cali_result->Connect(wxEVT_COMMAND_COMBOBOX_SELECTED, wxCommandEventHandler(AMSMaterialsSetting::on_select_cali_result), NULL, this);
|
||||
}
|
||||
@@ -191,11 +213,9 @@ void AMSMaterialsSetting::create_panel_normal(wxWindow* parent)
|
||||
|
||||
m_sizer_filament->Add(0, 0, 0, wxEXPAND, 0);
|
||||
|
||||
#ifdef __APPLE__
|
||||
m_comboBox_filament = new wxComboBox(parent, wxID_ANY, wxEmptyString, wxDefaultPosition, AMS_MATERIALS_SETTING_COMBOX_WIDTH, 0, nullptr, wxCB_READONLY);
|
||||
#else
|
||||
m_comboBox_filament = new ::ComboBox(parent, wxID_ANY, wxEmptyString, wxDefaultPosition, AMS_MATERIALS_SETTING_COMBOX_WIDTH, 0, nullptr, wxCB_READONLY);
|
||||
#endif
|
||||
m_star_filled = ScalableBitmap(this, "score_star_light", 16);
|
||||
m_star_empty = ScalableBitmap(this, "score_star_outline", 16);
|
||||
|
||||
m_sizer_filament->Add(m_comboBox_filament, 1, wxALIGN_CENTER, 0);
|
||||
|
||||
@@ -420,6 +440,7 @@ void AMSMaterialsSetting::paintEvent(wxPaintEvent &evt)
|
||||
AMSMaterialsSetting::~AMSMaterialsSetting()
|
||||
{
|
||||
m_comboBox_filament->Disconnect(wxEVT_COMMAND_COMBOBOX_SELECTED, wxCommandEventHandler(AMSMaterialsSetting::on_select_filament), NULL, this);
|
||||
m_comboBox_filament->Unbind(EVT_DROPDOWN_ITEM_ACTION, &AMSMaterialsSetting::on_toggle_favorite_filament, this);
|
||||
m_comboBox_cali_result->Disconnect(wxEVT_COMMAND_COMBOBOX_SELECTED, wxCommandEventHandler(AMSMaterialsSetting::on_select_cali_result), NULL, this);
|
||||
}
|
||||
|
||||
@@ -477,6 +498,13 @@ void AMSMaterialsSetting::update_filament_editing(bool is_printing)
|
||||
m_button_confirm->Show(true);
|
||||
}
|
||||
|
||||
// A third-party tray owns its temp range; BBL RFID trays keep the read-only preset values.
|
||||
// Only the OrcaSonar agent accepts the temp fields, so other agents stay read-only.
|
||||
const bool is_orca_agent = obj && obj->printer_agent_id == ORCA_PRINTER_AGENT_ID;
|
||||
const bool can_edit = !is_printing || obj->is_support_filament_setting_inprinting;
|
||||
m_input_nozzle_min->Enable(m_is_third && can_edit && is_orca_agent);
|
||||
m_input_nozzle_max->Enable(m_is_third && can_edit && is_orca_agent);
|
||||
|
||||
if (!m_is_third) {
|
||||
m_tip_readonly->SetLabelText(wxEmptyString);
|
||||
m_tip_readonly->Hide();
|
||||
@@ -499,6 +527,8 @@ void AMSMaterialsSetting::update_filament_editing(bool is_printing)
|
||||
if (m_view_only) { // Orca: view-only (2D laser/cut) — lock every edit control and hide apply/reset
|
||||
m_comboBox_filament->Enable(false);
|
||||
m_comboBox_cali_result->Enable(false);
|
||||
m_input_nozzle_min->Enable(false);
|
||||
m_input_nozzle_max->Enable(false);
|
||||
m_input_k_val->Enable(false);
|
||||
m_input_n_val->Enable(false);
|
||||
m_button_confirm->Hide();
|
||||
@@ -531,10 +561,9 @@ void AMSMaterialsSetting::on_select_reset(wxCommandEvent& event) {
|
||||
std::string selected_ams_id;
|
||||
PresetBundle *preset_bundle = wxGetApp().preset_bundle;
|
||||
if (preset_bundle) {
|
||||
const FilamentInfos* selected_info = selected_filament_info();
|
||||
for (auto it = preset_bundle->filaments.begin(); it != preset_bundle->filaments.end(); it++) {
|
||||
auto filament_item = map_filament_items[m_comboBox_filament->GetValue().ToStdString()];
|
||||
std::string filament_id = filament_item.filament_id;
|
||||
if (it->filament_id.compare(filament_id) == 0) {
|
||||
if (selected_info && it->filament_id.compare(selected_info->filament_id) == 0) {
|
||||
selected_ams_id = it->filament_id;
|
||||
break;
|
||||
}
|
||||
@@ -548,7 +577,7 @@ void AMSMaterialsSetting::on_select_reset(wxCommandEvent& event) {
|
||||
}
|
||||
|
||||
// set k / n value
|
||||
if (obj->cali_version <= -1 && obj->get_printer_series() == PrinterSeries::SERIES_P1P) {
|
||||
if (obj->supports_extrusion_cali() && obj->cali_version <= -1 && obj->get_printer_series() == PrinterSeries::SERIES_P1P) {
|
||||
// set extrusion cali ratio
|
||||
int cali_tray_id = ams_id * 4 + slot_id;
|
||||
|
||||
@@ -569,7 +598,7 @@ void AMSMaterialsSetting::on_select_reset(wxCommandEvent& event) {
|
||||
}
|
||||
obj->command_extrusion_cali_set(cali_tray_id, "", "", k, n);
|
||||
}
|
||||
else {
|
||||
else if (obj->supports_extrusion_cali()) {
|
||||
PACalibIndexInfo select_index_info;
|
||||
int tray_id = ams_id * 4 + slot_id;
|
||||
if (is_virtual_tray()) {
|
||||
@@ -682,12 +711,13 @@ void AMSMaterialsSetting::on_select_ok(wxCommandEvent &event)
|
||||
ams_setting_id = "";
|
||||
|
||||
// the combobox item
|
||||
auto filament_item = map_filament_items[m_comboBox_filament->GetValue().ToStdString()];
|
||||
const FilamentInfos* selected_info = selected_filament_info();
|
||||
const std::string filament_id = selected_info ? selected_info->filament_id : std::string();
|
||||
|
||||
// check filament info (rack-gated per-nozzle blacklist evaluation)
|
||||
PresetBundle* preset_bundle = wxGetApp().preset_bundle;
|
||||
const auto& fila_check_res = sCheckFilamentInfo(preset_bundle, obj, ams_id, slot_id,
|
||||
filament_item.filament_id, ams_filament_id, ams_setting_id);
|
||||
filament_id, ams_filament_id, ams_setting_id);
|
||||
|
||||
if (const auto& prohibit_items = fila_check_res.get_items_by_action("prohibition"); !prohibit_items.empty()) {
|
||||
wxString info_msg;
|
||||
@@ -728,6 +758,21 @@ void AMSMaterialsSetting::on_select_ok(wxCommandEvent &event)
|
||||
return;
|
||||
}
|
||||
|
||||
if (nozzle_temp_min_int < FILAMENT_MIN_TEMP || nozzle_temp_min_int > FILAMENT_MAX_TEMP ||
|
||||
nozzle_temp_max_int < FILAMENT_MIN_TEMP || nozzle_temp_max_int > FILAMENT_MAX_TEMP) {
|
||||
MessageDialog msg_dlg(nullptr,
|
||||
wxString::Format(_L("The input value should be greater than %1% and less than %2%"), FILAMENT_MIN_TEMP, FILAMENT_MAX_TEMP),
|
||||
wxEmptyString, wxICON_WARNING | wxOK);
|
||||
msg_dlg.ShowModal();
|
||||
return;
|
||||
}
|
||||
if (nozzle_temp_min_int > nozzle_temp_max_int) {
|
||||
MessageDialog msg_dlg(nullptr, _L("The minimum temperature cannot be greater than the maximum temperature."),
|
||||
wxEmptyString, wxICON_WARNING | wxOK);
|
||||
msg_dlg.ShowModal();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Orca: log the tray payload this dialog hands the printer, so the filament_id resolved from the
|
||||
// dropdown selection can be checked against the tray_info_idx the AMS actually receives. A
|
||||
@@ -751,7 +796,7 @@ void AMSMaterialsSetting::on_select_ok(wxCommandEvent &event)
|
||||
wxString k_text = m_input_k_val->GetTextCtrl()->GetValue();
|
||||
wxString n_text = m_input_n_val->GetTextCtrl()->GetValue();
|
||||
|
||||
if (obj->cali_version <= -1 && (obj->get_printer_series() != PrinterSeries::SERIES_X1) && !ExtrusionCalibration::check_k_validation(k_text)) {
|
||||
if (obj->supports_extrusion_cali() && obj->cali_version <= -1 && (obj->get_printer_series() != PrinterSeries::SERIES_X1) && !ExtrusionCalibration::check_k_validation(k_text)) {
|
||||
wxString k_tips = wxString::Format(_L("Please input a valid value (K in %.1f~%.1f)"), MIN_PA_K_VALUE, MAX_PA_K_VALUE);
|
||||
wxString kn_tips = wxString::Format(_L("Please input a valid value (K in %.1f~%.1f, N in %.1f~%.1f)"), MIN_PA_K_VALUE, MAX_PA_K_VALUE, 0.6, 2.0);
|
||||
MessageDialog msg_dlg(nullptr, k_tips, wxEmptyString, wxICON_WARNING | wxOK);
|
||||
@@ -781,7 +826,7 @@ void AMSMaterialsSetting::on_select_ok(wxCommandEvent &event)
|
||||
vt_tray = VIRTUAL_TRAY_DEPUTY_ID;
|
||||
}
|
||||
|
||||
if (obj->cali_version >= 0) {
|
||||
if (obj->supports_extrusion_cali() && obj->cali_version >= 0) {
|
||||
PACalibIndexInfo select_index_info;
|
||||
select_index_info.tray_id = vt_tray;
|
||||
select_index_info.ams_id = ams_id;
|
||||
@@ -800,7 +845,7 @@ void AMSMaterialsSetting::on_select_ok(wxCommandEvent &event)
|
||||
|
||||
CalibUtils::select_PA_calib_result(select_index_info);
|
||||
}
|
||||
else {
|
||||
else if (obj->supports_extrusion_cali()) {
|
||||
obj->command_extrusion_cali_set(vt_tray, "", "", k, n);
|
||||
}
|
||||
}
|
||||
@@ -822,7 +867,7 @@ void AMSMaterialsSetting::on_select_ok(wxCommandEvent &event)
|
||||
;
|
||||
}
|
||||
|
||||
if (obj->cali_version >= 0) {
|
||||
if (obj->supports_extrusion_cali() && obj->cali_version >= 0) {
|
||||
PACalibIndexInfo select_index_info;
|
||||
select_index_info.tray_id = cali_tray_id;
|
||||
select_index_info.ams_id = ams_id;
|
||||
@@ -841,7 +886,7 @@ void AMSMaterialsSetting::on_select_ok(wxCommandEvent &event)
|
||||
|
||||
CalibUtils::select_PA_calib_result(select_index_info);
|
||||
}
|
||||
else {
|
||||
else if (obj->supports_extrusion_cali()) {
|
||||
obj->command_extrusion_cali_set(cali_tray_id, "", "", k, n);
|
||||
}
|
||||
}
|
||||
@@ -937,7 +982,12 @@ bool AMSMaterialsSetting::is_virtual_tray()
|
||||
|
||||
void AMSMaterialsSetting::update_widgets()
|
||||
{
|
||||
if (obj && obj->get_printer_series() == PrinterSeries::SERIES_X1 && obj->cali_version <= -1) {
|
||||
if (obj && !obj->supports_extrusion_cali()) {
|
||||
// No printer-side K/N records: do not offer them.
|
||||
m_panel_normal->Show();
|
||||
m_panel_kn->Hide();
|
||||
}
|
||||
else if (obj && obj->get_printer_series() == PrinterSeries::SERIES_X1 && obj->cali_version <= -1) {
|
||||
// Low version firmware does not display k value
|
||||
m_panel_kn->Hide();
|
||||
}
|
||||
@@ -986,6 +1036,133 @@ static void _collect_filament_info(const wxString& shown_name,
|
||||
query_filament_types[shown_name] = filament.config.get_filament_type();
|
||||
}
|
||||
|
||||
static const char* AMS_FAVORITE_FILAMENT_IDS_KEY = "ams_favorite_filament_ids";
|
||||
|
||||
const AMSMaterialsSetting::FilamentInfos* AMSMaterialsSetting::selected_filament_info() const
|
||||
{
|
||||
const int selection = m_comboBox_filament->GetSelection();
|
||||
if (selection < 0 || selection >= int(m_filament_list_items.size()) || !m_filament_list_items[selection].is_filament)
|
||||
return nullptr;
|
||||
return &m_filament_list_items[selection].filament;
|
||||
}
|
||||
|
||||
std::set<std::string> AMSMaterialsSetting::load_favorite_filament_ids() const
|
||||
{
|
||||
std::set<std::string> favorite_ids;
|
||||
std::vector<std::string> values;
|
||||
const std::string serialized = wxGetApp().app_config->get(AMS_FAVORITE_FILAMENT_IDS_KEY);
|
||||
if (!serialized.empty() && Slic3r::unescape_strings_cstyle(serialized, values)) {
|
||||
favorite_ids.insert(values.begin(), values.end());
|
||||
}
|
||||
return favorite_ids;
|
||||
}
|
||||
|
||||
void AMSMaterialsSetting::save_favorite_filament_ids(const std::set<std::string>& favorite_ids)
|
||||
{
|
||||
std::vector<std::string> values(favorite_ids.begin(), favorite_ids.end());
|
||||
wxGetApp().app_config->set(AMS_FAVORITE_FILAMENT_IDS_KEY, Slic3r::escape_strings_cstyle(values));
|
||||
wxGetApp().app_config->save();
|
||||
}
|
||||
|
||||
void AMSMaterialsSetting::rebuild_filament_dropdown_items(const std::string& selected_filament_id)
|
||||
{
|
||||
std::vector<DropDown::Item> items;
|
||||
m_filament_list_items.clear();
|
||||
|
||||
auto append_header = [&items, this](const wxString& label) {
|
||||
DropDown::Item item;
|
||||
item.text = label;
|
||||
item.style = DD_ITEM_STYLE_SPLIT_ITEM | DD_ITEM_STYLE_DISABLED;
|
||||
items.push_back(item);
|
||||
m_filament_list_items.push_back(FilamentListItem{});
|
||||
};
|
||||
|
||||
auto append_filament = [&items, this](const wxString& name) {
|
||||
auto info_it = map_filament_items.find(name);
|
||||
if (info_it == map_filament_items.end())
|
||||
return;
|
||||
|
||||
DropDown::Item item;
|
||||
item.text = name;
|
||||
items.push_back(item);
|
||||
m_filament_list_items.push_back(FilamentListItem{info_it->second, true});
|
||||
};
|
||||
|
||||
// Favorites are listed again at the top under their own header. Without any, the list is the
|
||||
// plain filament list.
|
||||
const std::set<std::string> favorite_ids = load_favorite_filament_ids();
|
||||
std::vector<wxString> favorites;
|
||||
for (const wxString& name : m_sorted_filament_items) {
|
||||
auto info_it = map_filament_items.find(name);
|
||||
if (info_it != map_filament_items.end() && favorite_ids.count(info_it->second.filament_id) > 0)
|
||||
favorites.push_back(name);
|
||||
}
|
||||
if (!favorites.empty()) {
|
||||
append_header(_L("Favorite filaments"));
|
||||
for (const wxString& name : favorites)
|
||||
append_filament(name);
|
||||
append_header(_L("All filaments"));
|
||||
}
|
||||
for (const wxString& name : m_sorted_filament_items)
|
||||
append_filament(name);
|
||||
|
||||
int selection_idx = -1;
|
||||
if (!selected_filament_id.empty()) {
|
||||
for (int i = 0; i < int(m_filament_list_items.size()); ++i) {
|
||||
if (m_filament_list_items[i].is_filament && m_filament_list_items[i].filament.filament_id == selected_filament_id) {
|
||||
selection_idx = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clear() first: SetItems() keeps the old selection, and SetSelection() ignores an index equal to it,
|
||||
// which would leave the previous filament's name on the combo.
|
||||
m_comboBox_filament->Clear();
|
||||
m_comboBox_filament->SetItems(items);
|
||||
update_favorite_stars();
|
||||
m_comboBox_filament->SetSelection(selection_idx);
|
||||
m_favorites_regroup = false;
|
||||
}
|
||||
|
||||
void AMSMaterialsSetting::update_favorite_stars()
|
||||
{
|
||||
const std::set<std::string> favorite_ids = load_favorite_filament_ids();
|
||||
for (size_t i = 0; i < m_filament_list_items.size(); ++i) {
|
||||
const FilamentListItem& row = m_filament_list_items[i];
|
||||
if (row.is_filament) {
|
||||
const bool favorite = favorite_ids.count(row.filament.filament_id) > 0;
|
||||
m_comboBox_filament->SetItemActionBitmap(i, favorite ? m_star_filled.bmp() : m_star_empty.bmp());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void AMSMaterialsSetting::toggle_favorite_filament(const std::string& filament_id)
|
||||
{
|
||||
if (filament_id.empty())
|
||||
return;
|
||||
|
||||
std::set<std::string> favorite_ids = load_favorite_filament_ids();
|
||||
auto iter = favorite_ids.find(filament_id);
|
||||
if (iter == favorite_ids.end())
|
||||
favorite_ids.insert(filament_id);
|
||||
else
|
||||
favorite_ids.erase(iter);
|
||||
|
||||
save_favorite_filament_ids(favorite_ids);
|
||||
}
|
||||
|
||||
void AMSMaterialsSetting::on_toggle_favorite_filament(wxCommandEvent& evt)
|
||||
{
|
||||
const int index = evt.GetInt();
|
||||
if (index < 0 || index >= int(m_filament_list_items.size()) || !m_filament_list_items[index].is_filament)
|
||||
return;
|
||||
|
||||
toggle_favorite_filament(m_filament_list_items[index].filament.filament_id);
|
||||
update_favorite_stars();
|
||||
m_favorites_regroup = true;
|
||||
}
|
||||
|
||||
void AMSMaterialsSetting::Popup(wxString filament, wxString sn, wxString temp_min, wxString temp_max, wxString k, wxString n)
|
||||
{
|
||||
if (!obj) return;
|
||||
@@ -998,14 +1175,16 @@ void AMSMaterialsSetting::Popup(wxString filament, wxString sn, wxString temp_mi
|
||||
|
||||
m_input_k_val->GetTextCtrl()->SetValue(k);
|
||||
m_input_n_val->GetTextCtrl()->SetValue(n);
|
||||
// Tray values are re-supplied by every caller; clear so a previous popup cannot leak in.
|
||||
m_input_nozzle_min->GetTextCtrl()->SetValue(wxEmptyString);
|
||||
m_input_nozzle_max->GetTextCtrl()->SetValue(wxEmptyString);
|
||||
|
||||
wxArrayString filament_items;
|
||||
wxString bambu_filament_name;
|
||||
wxString hint_filament_name; // the hint type to be selected
|
||||
std::unordered_map<wxString, wxString> query_filament_vendors;// some information for sort
|
||||
std::unordered_map<wxString, wxString> query_filament_types; //
|
||||
|
||||
std::set<std::string> filament_id_set;
|
||||
m_sorted_filament_items.clear();
|
||||
// The alias keyed map has to start empty: it is a member, so a stale alias left by an earlier
|
||||
// popup (a different printer, a different nozzle) would resolve to that printer's filament_id.
|
||||
map_filament_items.clear();
|
||||
@@ -1024,18 +1203,21 @@ void AMSMaterialsSetting::Popup(wxString filament, wxString sn, wxString temp_mi
|
||||
}
|
||||
stream << std::fixed << std::setprecision(1) << machine_diameter;
|
||||
std::string nozzle_diameter_str = stream.str();
|
||||
// OrcaSonar's model id is optional, so the device model alone can resolve to nothing;
|
||||
// the helper falls back to the selected profile so the dropdown is never empty.
|
||||
const std::string filament_printer_model = resolve_filament_printer_model(obj->printer_type, preset_bundle);
|
||||
|
||||
if (preset_bundle) {
|
||||
if (preset_bundle && !filament_printer_model.empty()) {
|
||||
BOOST_LOG_TRIVIAL(trace) << "system_preset_bundle filament number=" << preset_bundle->filaments.size();
|
||||
for (Preset *filament_it : preset_bundle->get_filament_presets_for_machine(
|
||||
DevPrinterConfigUtil::get_printer_display_name(obj->printer_type), nozzle_diameter_str, obj->is_support_user_preset)) {
|
||||
filament_printer_model, nozzle_diameter_str, obj->is_support_user_preset)) {
|
||||
if (!filament_id_set.insert(filament_it->filament_id).second)
|
||||
continue;
|
||||
const std::string alias = preset_bundle->filaments.get_preset_alias(*filament_it, true);
|
||||
if (alias.empty())
|
||||
continue;
|
||||
|
||||
filament_items.push_back(alias);
|
||||
m_sorted_filament_items.push_back(alias);
|
||||
_collect_filament_info(alias, *filament_it, query_filament_vendors, query_filament_types);
|
||||
|
||||
FilamentInfos filament_infos;
|
||||
@@ -1044,7 +1226,6 @@ void AMSMaterialsSetting::Popup(wxString filament, wxString sn, wxString temp_mi
|
||||
map_filament_items[alias] = filament_infos;
|
||||
|
||||
if (filament_it->filament_id == ams_filament_id) {
|
||||
hint_filament_name = from_u8(alias);
|
||||
bambu_filament_name = from_u8(alias);
|
||||
|
||||
|
||||
@@ -1095,6 +1276,12 @@ void AMSMaterialsSetting::Popup(wxString filament, wxString sn, wxString temp_mi
|
||||
else {
|
||||
m_comboBox_filament->Show();
|
||||
m_readonly_filament->Hide();
|
||||
// A printer-set tray carries its own temps; the preset selection only fills
|
||||
// them when the tray has none.
|
||||
if (!temp_min.IsEmpty() && temp_min != "0")
|
||||
m_input_nozzle_min->GetTextCtrl()->SetValue(temp_min);
|
||||
if (!temp_max.IsEmpty() && temp_max != "0")
|
||||
m_input_nozzle_max->GetTextCtrl()->SetValue(temp_max);
|
||||
}
|
||||
|
||||
if (obj->cali_version >= 0) {
|
||||
@@ -1215,33 +1402,22 @@ void AMSMaterialsSetting::Popup(wxString filament, wxString sn, wxString temp_mi
|
||||
return name_compare != 0 ? name_compare < 0 : left < right;
|
||||
};
|
||||
|
||||
std::sort(filament_items.begin(), filament_items.end(), _filament_sorter);
|
||||
std::sort(m_sorted_filament_items.begin(), m_sorted_filament_items.end(), _filament_sorter);
|
||||
}
|
||||
|
||||
// traverse the hint selection idx
|
||||
int selection_idx = -1;
|
||||
{
|
||||
for(int i = 0; i < filament_items.size(); i++)
|
||||
{
|
||||
if (hint_filament_name == filament_items[i])
|
||||
{
|
||||
selection_idx = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (m_sorted_filament_items.empty())
|
||||
BOOST_LOG_TRIVIAL(warning) << "ams_materials_setting: no filament presets for printer model \""
|
||||
<< filament_printer_model << "\" at nozzle " << nozzle_diameter_str;
|
||||
|
||||
m_comboBox_filament->Set(filament_items);
|
||||
m_comboBox_filament->SetSelection(selection_idx);
|
||||
rebuild_filament_dropdown_items(ams_filament_id);
|
||||
int selection_idx = m_comboBox_filament->GetSelection();
|
||||
m_comboBox_from_printer = true;
|
||||
post_select_event(selection_idx);
|
||||
|
||||
if (selection_idx < 0) {
|
||||
m_comboBox_filament->SetValue(wxEmptyString);
|
||||
}
|
||||
|
||||
// Set the flag whether to open the filament setting dialog from the device page
|
||||
m_comboBox_filament->SetClientData(new int(1));
|
||||
|
||||
update();
|
||||
Layout();
|
||||
Fit();
|
||||
@@ -1285,17 +1461,18 @@ int AMSMaterialsSetting::get_filament_variant_index(const Preset &filament, cons
|
||||
|
||||
void AMSMaterialsSetting::on_select_filament(wxCommandEvent &evt)
|
||||
{
|
||||
// Get the flag whether to open the filament setting dialog from the device page
|
||||
int* from_printer = static_cast<int*>(m_comboBox_filament->GetClientData());
|
||||
// True for the popup's own initial selection: the dialog pre-filled the tray's
|
||||
// temps, so the matching preset must not overwrite them.
|
||||
const bool initial_printer_selection = m_comboBox_from_printer;
|
||||
|
||||
m_filament_type = "";
|
||||
const FilamentInfos* selected_info = selected_filament_info();
|
||||
PresetBundle* preset_bundle = wxGetApp().preset_bundle;
|
||||
std::string filament_printer_model;
|
||||
if (preset_bundle) {
|
||||
std::ostringstream stream;
|
||||
if (obj) {
|
||||
// Defensive: this dialog is opened only from StatusPanel (BBL-only) today, so the fallback fires
|
||||
// only during the brief BBL startup window before firmware reports nozzle info. Without this,
|
||||
// the "0.0" lookup string returns an empty set and filament lookup yields no results.
|
||||
// Use the selected profile's nozzle diameter until the connected device reports one.
|
||||
float machine_diameter = obj->GetExtderSystem()->GetNozzleDiameter(0);
|
||||
if (machine_diameter == 0.0f) {
|
||||
const ConfigOption *opt = preset_bundle->printers.get_selected_preset().config.option("nozzle_diameter");
|
||||
@@ -1305,33 +1482,35 @@ void AMSMaterialsSetting::on_select_filament(wxCommandEvent &evt)
|
||||
}
|
||||
}
|
||||
stream << std::fixed << std::setprecision(1) << machine_diameter;
|
||||
filament_printer_model = resolve_filament_printer_model(obj->printer_type, preset_bundle);
|
||||
}
|
||||
std::string nozzle_diameter_str = stream.str();
|
||||
// Resolve the selection against the same list Popup() built the dropdown from, so the two
|
||||
// halves of the dialog cannot disagree about which filaments this machine can use.
|
||||
const std::string selected = m_comboBox_filament->GetValue().ToStdString();
|
||||
if (!selected.empty()) {
|
||||
const std::string filament_id = map_filament_items[selected].filament_id;
|
||||
if (selected_info && !filament_printer_model.empty()) {
|
||||
const std::string& filament_id = selected_info->filament_id;
|
||||
for (Preset *it : preset_bundle->get_filament_presets_for_machine(
|
||||
DevPrinterConfigUtil::get_printer_display_name(obj->printer_type), nozzle_diameter_str, obj->is_support_user_preset)) {
|
||||
filament_printer_model, nozzle_diameter_str, obj->is_support_user_preset)) {
|
||||
if (it->filament_id != filament_id)
|
||||
continue;
|
||||
// ) if nozzle_temperature_range is found
|
||||
const int variant_index = get_filament_variant_index(*it, nozzle_diameter_str);
|
||||
ConfigOption* opt_min = it->config.option("nozzle_temperature_range_low");
|
||||
if (opt_min) {
|
||||
ConfigOptionInts* opt_min_ints = dynamic_cast<ConfigOptionInts*>(opt_min);
|
||||
if (opt_min_ints) {
|
||||
wxString text_nozzle_temp_min = wxString::Format("%d", opt_min_ints->get_at(variant_index));
|
||||
m_input_nozzle_min->GetTextCtrl()->SetValue(text_nozzle_temp_min);
|
||||
if (!initial_printer_selection) {
|
||||
// ) if nozzle_temperature_range is found
|
||||
const int variant_index = get_filament_variant_index(*it, nozzle_diameter_str);
|
||||
ConfigOption* opt_min = it->config.option("nozzle_temperature_range_low");
|
||||
if (opt_min) {
|
||||
ConfigOptionInts* opt_min_ints = dynamic_cast<ConfigOptionInts*>(opt_min);
|
||||
if (opt_min_ints) {
|
||||
wxString text_nozzle_temp_min = wxString::Format("%d", opt_min_ints->get_at(variant_index));
|
||||
m_input_nozzle_min->GetTextCtrl()->SetValue(text_nozzle_temp_min);
|
||||
}
|
||||
}
|
||||
}
|
||||
ConfigOption* opt_max = it->config.option("nozzle_temperature_range_high");
|
||||
if (opt_max) {
|
||||
ConfigOptionInts* opt_max_ints = dynamic_cast<ConfigOptionInts*>(opt_max);
|
||||
if (opt_max_ints) {
|
||||
wxString text_nozzle_temp_max = wxString::Format("%d", opt_max_ints->get_at(variant_index));
|
||||
m_input_nozzle_max->GetTextCtrl()->SetValue(text_nozzle_temp_max);
|
||||
ConfigOption* opt_max = it->config.option("nozzle_temperature_range_high");
|
||||
if (opt_max) {
|
||||
ConfigOptionInts* opt_max_ints = dynamic_cast<ConfigOptionInts*>(opt_max);
|
||||
if (opt_max_ints) {
|
||||
wxString text_nozzle_temp_max = wxString::Format("%d", opt_max_ints->get_at(variant_index));
|
||||
m_input_nozzle_max->GetTextCtrl()->SetValue(text_nozzle_temp_max);
|
||||
}
|
||||
}
|
||||
}
|
||||
ConfigOption* opt_type = it->config.option("filament_type");
|
||||
@@ -1364,42 +1543,36 @@ void AMSMaterialsSetting::on_select_filament(wxCommandEvent &evt)
|
||||
//reset cali
|
||||
int cali_select_idx = -1;
|
||||
|
||||
if ( !this->obj || m_filament_selection < 0) {
|
||||
if ( !this->obj || m_filament_selection < 0 || !selected_info) {
|
||||
m_input_k_val->Enable(false);
|
||||
m_input_n_val->Enable(false);
|
||||
m_button_confirm->Disable(); // ORCA No need to change style
|
||||
m_comboBox_cali_result->Clear();
|
||||
m_comboBox_cali_result->SetValue(wxEmptyString);
|
||||
m_input_k_val->GetTextCtrl()->SetValue(wxEmptyString);
|
||||
m_input_n_val->GetTextCtrl()->SetValue(wxEmptyString);
|
||||
m_comboBox_filament->SetClientData(new int(0));
|
||||
if (!initial_printer_selection) {
|
||||
m_input_k_val->GetTextCtrl()->SetValue(wxEmptyString);
|
||||
m_input_n_val->GetTextCtrl()->SetValue(wxEmptyString);
|
||||
}
|
||||
m_comboBox_from_printer = false;
|
||||
return;
|
||||
}
|
||||
else {
|
||||
m_button_confirm->Enable(true); // ORCA No need to change style
|
||||
if (!m_view_only)
|
||||
m_button_confirm->Enable(true); // ORCA No need to change style
|
||||
}
|
||||
|
||||
//filament id
|
||||
ams_filament_id = "";
|
||||
ams_setting_id = "";
|
||||
|
||||
if (preset_bundle) {
|
||||
for (auto it = preset_bundle->filaments.begin(); it != preset_bundle->filaments.end(); it++) {
|
||||
auto itor = map_filament_items.find(m_comboBox_filament->GetValue().ToStdString());
|
||||
if ( itor != map_filament_items.end()) {
|
||||
ams_filament_id = itor->second.filament_id;
|
||||
ams_setting_id = itor->second.setting_id;
|
||||
break;
|
||||
}
|
||||
|
||||
if (it->alias.compare(m_comboBox_filament->GetValue().ToStdString()) == 0) {
|
||||
ams_filament_id = it->filament_id;
|
||||
ams_setting_id = it->setting_id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (selected_info) {
|
||||
ams_filament_id = selected_info->filament_id;
|
||||
ams_setting_id = selected_info->setting_id;
|
||||
}
|
||||
|
||||
if (!ams_filament_id.empty())
|
||||
m_clr_picker->is_empty(false);
|
||||
|
||||
wxArrayString items;
|
||||
m_pa_profile_items.clear();
|
||||
m_comboBox_cali_result->SetValue(wxEmptyString);
|
||||
@@ -1455,7 +1628,7 @@ void AMSMaterialsSetting::on_select_filament(wxCommandEvent &evt)
|
||||
|
||||
m_comboBox_cali_result->Set(items);
|
||||
if (ams_id == VIRTUAL_TRAY_MAIN_ID || ams_id == VIRTUAL_TRAY_DEPUTY_ID) {
|
||||
if (from_printer && (*from_printer == 1)) {
|
||||
if (initial_printer_selection) {
|
||||
for (auto slot : obj->vt_slot) {
|
||||
if (slot.id == std::to_string(ams_id))
|
||||
cali_select_idx = CalibUtils::get_selected_calib_idx(m_pa_profile_items, slot.cali_idx);
|
||||
@@ -1472,10 +1645,11 @@ void AMSMaterialsSetting::on_select_filament(wxCommandEvent &evt)
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (from_printer && (*from_printer == 1)) {
|
||||
if (initial_printer_selection) {
|
||||
DevAmsTray* selected_tray = this->obj->GetFilaSystem()->GetAmsTray(std::to_string(ams_id), std::to_string(slot_id));
|
||||
if (!selected_tray)
|
||||
{
|
||||
m_comboBox_from_printer = false;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1506,7 +1680,7 @@ void AMSMaterialsSetting::on_select_filament(wxCommandEvent &evt)
|
||||
else {
|
||||
if (!ams_filament_id.empty()) {
|
||||
//m_input_k_val->GetTextCtrl()->SetValue("0.00");
|
||||
m_input_k_val->Enable(true);
|
||||
m_input_k_val->Enable(!m_view_only);
|
||||
}
|
||||
else {
|
||||
//m_input_k_val->GetTextCtrl()->SetValue("0.00");
|
||||
@@ -1514,7 +1688,7 @@ void AMSMaterialsSetting::on_select_filament(wxCommandEvent &evt)
|
||||
}
|
||||
}
|
||||
|
||||
m_comboBox_filament->SetClientData(new int(0));
|
||||
m_comboBox_from_printer = false;
|
||||
}
|
||||
|
||||
void AMSMaterialsSetting::on_dpi_changed(const wxRect &suggested_rect)
|
||||
@@ -1529,6 +1703,11 @@ void AMSMaterialsSetting::on_dpi_changed(const wxRect &suggested_rect)
|
||||
m_button_reset->Rescale(); // ORCA
|
||||
m_button_confirm->Rescale(); // ORCA
|
||||
m_button_close->Rescale(); // ORCA
|
||||
// An open list would keep the measurements of the old stars, so close it before replacing them.
|
||||
m_comboBox_filament->CloseDropdown();
|
||||
m_star_filled.msw_rescale();
|
||||
m_star_empty.msw_rescale();
|
||||
update_favorite_stars();
|
||||
this->Refresh();
|
||||
}
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
#include "slic3r/GUI/Widgets/StateColor.hpp"
|
||||
#include <wx/panel.h>
|
||||
#include <map>
|
||||
#include <set>
|
||||
|
||||
#define AMS_MATERIALS_SETTING_DEF_COLOUR wxColour(255, 255, 255)
|
||||
#define AMS_MATERIALS_SETTING_GREY900 wxColour(38, 46, 48)
|
||||
@@ -137,7 +138,7 @@ public:
|
||||
std::string ams_filament_id;
|
||||
std::string ams_setting_id;
|
||||
|
||||
bool m_is_third;
|
||||
bool m_is_third = false;
|
||||
// Orca: view-only mode (laser/cut). When set, the dialog is inspectable but every
|
||||
// editing control is disabled and no command is sent.
|
||||
bool m_view_only = false;
|
||||
@@ -156,6 +157,7 @@ protected:
|
||||
void create_panel_kn(wxWindow* parent);
|
||||
void on_dpi_changed(const wxRect &suggested_rect) override;
|
||||
void on_select_filament(wxCommandEvent& evt);
|
||||
void on_toggle_favorite_filament(wxCommandEvent& evt);
|
||||
void on_select_cali_result(wxCommandEvent &evt);
|
||||
void on_select_ok(wxCommandEvent &event);
|
||||
void on_select_reset(wxCommandEvent &event);
|
||||
@@ -163,6 +165,11 @@ protected:
|
||||
void on_clr_picker(wxMouseEvent &event);
|
||||
bool is_virtual_tray();
|
||||
void update_widgets();
|
||||
void rebuild_filament_dropdown_items(const std::string& selected_filament_id);
|
||||
void update_favorite_stars();
|
||||
std::set<std::string> load_favorite_filament_ids() const;
|
||||
void save_favorite_filament_ids(const std::set<std::string>& favorite_ids);
|
||||
void toggle_favorite_filament(const std::string& filament_id);
|
||||
|
||||
void update_filament_editing(bool is_printing);
|
||||
// Orca: the variant index of the filament's per-variant options on the nozzle this tray feeds
|
||||
@@ -199,15 +206,13 @@ protected:
|
||||
wxStaticText* m_n_param;
|
||||
TextInput* m_input_n_val;
|
||||
int m_filament_selection;
|
||||
// True while the popup's initial, printer-driven selection is being handled; it
|
||||
// keeps that selection from overwriting the tray's own temps with preset temps.
|
||||
bool m_comboBox_from_printer = false;
|
||||
|
||||
int m_pa_cali_select_id = 0;
|
||||
|
||||
// Orca: native wxComboBox on macOS (the custom ComboBox mis-renders the filament dropdown there)
|
||||
#ifdef __APPLE__
|
||||
wxComboBox *m_comboBox_filament;
|
||||
#else
|
||||
ComboBox *m_comboBox_filament;
|
||||
#endif
|
||||
ComboBox * m_comboBox_cali_result;
|
||||
TextInput* m_readonly_filament;
|
||||
|
||||
@@ -215,7 +220,20 @@ protected:
|
||||
std::string filament_id;
|
||||
std::string setting_id;
|
||||
};
|
||||
std::map<std::string, FilamentInfos> map_filament_items;
|
||||
struct FilamentListItem {
|
||||
FilamentInfos filament;
|
||||
bool is_filament{false};
|
||||
};
|
||||
const FilamentInfos* selected_filament_info() const;
|
||||
|
||||
// Keyed by the same wxString the dropdown shows, so a row's name never has to be converted back.
|
||||
std::map<wxString, FilamentInfos> map_filament_items;
|
||||
std::vector<wxString> m_sorted_filament_items;
|
||||
std::vector<FilamentListItem> m_filament_list_items;
|
||||
ScalableBitmap m_star_filled;
|
||||
ScalableBitmap m_star_empty;
|
||||
// Favorites changed while the list was open; their rows are regrouped before it opens again.
|
||||
bool m_favorites_regroup{false};
|
||||
};
|
||||
|
||||
wxDECLARE_EVENT(EVT_SELECTED_COLOR, wxCommandEvent);
|
||||
|
||||
@@ -530,7 +530,8 @@ void AMSSetting::on_insert_material_read(wxCommandEvent &event)
|
||||
bool tray_read_opt = m_checkbox_Insert_material_auto_read->GetValue();
|
||||
bool remain_opt = m_checkbox_remain->GetValue();
|
||||
|
||||
m_obj->command_ams_user_settings(start_read_opt, tray_read_opt, remain_opt);
|
||||
if (m_obj->command_ams_user_settings(start_read_opt, tray_read_opt, remain_opt) != 0)
|
||||
UpdateByObj(m_obj);
|
||||
|
||||
m_sizer_Insert_material_tip_inline->Layout();
|
||||
Layout();
|
||||
@@ -556,7 +557,8 @@ void AMSSetting::on_starting_read(wxCommandEvent &event)
|
||||
bool tray_read_opt = m_checkbox_Insert_material_auto_read->GetValue();
|
||||
bool remain_opt = m_checkbox_remain->GetValue();
|
||||
|
||||
m_obj->command_ams_user_settings(start_read_opt, tray_read_opt, remain_opt);
|
||||
if (m_obj->command_ams_user_settings(start_read_opt, tray_read_opt, remain_opt) != 0)
|
||||
UpdateByObj(m_obj);
|
||||
|
||||
m_sizer_starting_tip_inline->Layout();
|
||||
Layout();
|
||||
@@ -570,7 +572,8 @@ void AMSSetting::on_remain(wxCommandEvent& event)
|
||||
bool start_read_opt = m_checkbox_starting_auto_read->GetValue();
|
||||
bool tray_read_opt = m_checkbox_Insert_material_auto_read->GetValue();
|
||||
bool remain_opt = m_checkbox_remain->GetValue();
|
||||
m_obj->command_ams_user_settings(start_read_opt, tray_read_opt, remain_opt);
|
||||
if (m_obj->command_ams_user_settings(start_read_opt, tray_read_opt, remain_opt) != 0)
|
||||
UpdateByObj(m_obj);
|
||||
event.Skip();
|
||||
}
|
||||
|
||||
@@ -806,4 +809,4 @@ void AMSSettingArrangeAMSOrder::OnBtnRearrangeClicked(wxCommandEvent& event)
|
||||
}
|
||||
#endif
|
||||
|
||||
}} // namespace Slic3r::GUI
|
||||
}} // namespace Slic3r::GUI
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include "Widgets/DialogButtons.hpp"
|
||||
#include "libslic3r/Config.hpp"
|
||||
#include <boost/log/trivial.hpp>
|
||||
#include <slic3r/GUI/DeviceManager.hpp>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include "libslic3r/calib.hpp"
|
||||
@@ -745,9 +746,15 @@ wxArrayString NewCalibrationHistoryDialog::get_all_filaments(const MachineObject
|
||||
std::string nozzle_diameter_str = stream.str();
|
||||
|
||||
if (preset_bundle) {
|
||||
// OrcaSonar's model id is optional; the helper falls back to the selected profile
|
||||
// so the list is never empty.
|
||||
const std::string filament_printer_model = resolve_filament_printer_model(obj->printer_type, preset_bundle);
|
||||
if (filament_printer_model.empty())
|
||||
return filament_items;
|
||||
|
||||
BOOST_LOG_TRIVIAL(trace) << "system_preset_bundle filament number=" << preset_bundle->filaments.size();
|
||||
for (Preset *filament_it : preset_bundle->get_filament_presets_for_machine(
|
||||
DevPrinterConfigUtil::get_printer_display_name(obj->printer_type), nozzle_diameter_str, obj->is_support_user_preset)) {
|
||||
filament_printer_model, nozzle_diameter_str, obj->is_support_user_preset)) {
|
||||
if (!filament_id_set.insert(filament_it->filament_id).second)
|
||||
continue;
|
||||
const std::string alias = preset_bundle->filaments.get_preset_alias(*filament_it, true);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cassert>
|
||||
@@ -8,6 +9,7 @@
|
||||
#include <cstdlib>
|
||||
#include <chrono>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <wx/app.h>
|
||||
#include <wx/colour.h>
|
||||
#include <string>
|
||||
#include <wx/string.h>
|
||||
@@ -19,6 +21,7 @@
|
||||
#include <utility>
|
||||
#include "DevFilaSystem.h"
|
||||
#include "slic3r/Utils/NetworkAgent.hpp"
|
||||
#include "slic3r/Utils/NetworkAgentFactory.hpp" // ORCA_PRINTER_AGENT_ID
|
||||
#include "DevNozzleSystem.h" // DevNozzle / DevNozzleSystem for GetNozzleFlowStringByAmsId
|
||||
|
||||
// TODO: remove this include
|
||||
@@ -61,6 +64,15 @@ void DevAmsTray::UpdateColorFromStr(const std::string& color)
|
||||
}
|
||||
}
|
||||
|
||||
void DevAmsTray::UpdateEmptyState(bool material_fields_present, bool classify_empty)
|
||||
{
|
||||
const auto is_zero_or_empty = [](const std::string& value) {
|
||||
return value.empty() || std::all_of(value.begin(), value.end(), [](char c) { return c == '0'; });
|
||||
};
|
||||
is_empty = material_fields_present && classify_empty && setting_id.empty() && m_fila_type.empty() &&
|
||||
is_zero_or_empty(color) && is_zero_or_empty(tag_uid);
|
||||
}
|
||||
|
||||
void DevAmsTray::reset()
|
||||
{
|
||||
tag_uid = "";
|
||||
@@ -82,6 +94,7 @@ void DevAmsTray::reset()
|
||||
k = 0.0f;
|
||||
n = 0.0f;
|
||||
is_bbl = false;
|
||||
is_empty = false;
|
||||
hold_count = 0;
|
||||
remain = 0;
|
||||
}
|
||||
@@ -681,7 +694,7 @@ void DevFilaSystemParser::ParseV1_0(const json& jj, MachineObject* obj, DevFilaS
|
||||
//std::string type = (*tray_it)["tray_type"].get<std::string>();
|
||||
std::string type = MachineObject::setting_id_to_type(curr_tray->setting_id, (*tray_it)["tray_type"].get<std::string>());
|
||||
// curr_tray->setting_id is our OF id; GFS00/GFS01 are the printer's own support-filament ids.
|
||||
auto* agent = GUI::wxGetApp().getAgent();
|
||||
auto* agent = wxTheApp != nullptr ? GUI::wxGetApp().getAgent() : nullptr;
|
||||
const std::string printer_filament_id = agent ? agent->from_orca_filament_id(curr_tray->setting_id) : curr_tray->setting_id;
|
||||
if (printer_filament_id == "GFS00")
|
||||
{
|
||||
@@ -781,6 +794,8 @@ void DevFilaSystemParser::ParseV1_0(const json& jj, MachineObject* obj, DevFilaS
|
||||
{
|
||||
curr_tray->remain = -1;
|
||||
}
|
||||
curr_tray->UpdateEmptyState(tray_it->contains("tray_info_idx") && tray_it->contains("tray_type"),
|
||||
obj->printer_agent_id == ORCA_PRINTER_AGENT_ID);
|
||||
// The tray objects are reused across status updates. Reset this
|
||||
// state when a previously empty slot receives a filament again.
|
||||
curr_tray->is_slot_placeholder = tray_it->contains("tray_slot_placeholder");
|
||||
|
||||
@@ -82,6 +82,7 @@ public:
|
||||
wxColour wx_color;
|
||||
bool is_bbl;
|
||||
bool is_exists = false;
|
||||
bool is_empty = false; // Explicitly reported as having no filament.
|
||||
bool is_slot_placeholder = false; // Orca: True for empty tray slots from pull-mode agents
|
||||
int hold_count = 0;
|
||||
int remain = 0; // filament remain: 0 ~ 100
|
||||
@@ -90,13 +91,15 @@ public:
|
||||
// operators
|
||||
bool operator==(DevAmsTray const& o) const
|
||||
{
|
||||
return id == o.id && m_fila_type == o.m_fila_type && filament_setting_id == o.filament_setting_id && color == o.color;
|
||||
return id == o.id && m_fila_type == o.m_fila_type && filament_setting_id == o.filament_setting_id &&
|
||||
color == o.color && is_empty == o.is_empty;
|
||||
}
|
||||
bool operator!=(DevAmsTray const& o) const { return !operator==(o); }
|
||||
|
||||
// setters
|
||||
void reset();
|
||||
void UpdateColorFromStr(const std::string& color);
|
||||
void UpdateEmptyState(bool material_fields_present, bool classify_empty);
|
||||
void set_hold_count() { hold_count = HOLD_COUNT_MAX; }
|
||||
|
||||
// getter
|
||||
|
||||
@@ -64,6 +64,7 @@
|
||||
#include <unordered_set>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
#include <wx/app.h>
|
||||
#include <wx/colour.h>
|
||||
#include <tuple>
|
||||
#include <wx/dir.h>
|
||||
@@ -431,6 +432,32 @@ NozzleVolumeType convert_to_nozzle_type(const std::string &str)
|
||||
return res;
|
||||
}
|
||||
|
||||
std::string resolve_filament_printer_model(const std::string& printer_type, PresetBundle* preset_bundle)
|
||||
{
|
||||
if (!preset_bundle)
|
||||
return {};
|
||||
|
||||
// Devices with a shipped printer config (Bambu) resolve directly.
|
||||
if (const std::string display_name = DevPrinterConfigUtil::get_printer_display_name(printer_type); !display_name.empty())
|
||||
return display_name;
|
||||
|
||||
// A vendor model id the device reported (SSDP modelNumber, manual binding).
|
||||
if (!printer_type.empty()) {
|
||||
if (const std::string model_name = preset_bundle->get_printer_model_display_name(printer_type); !model_name.empty())
|
||||
return model_name;
|
||||
}
|
||||
|
||||
// OrcaSonar's model id is optional; a generic device resolves against the selected profile.
|
||||
if (const ConfigOptionString* model = preset_bundle->printers.get_selected_preset().config.opt<ConfigOptionString>("printer_model");
|
||||
model && !model->value.empty()) {
|
||||
BOOST_LOG_TRIVIAL(info) << "resolve_filament_printer_model: device type \"" << printer_type
|
||||
<< "\" has no installed model; using the selected profile \"" << model->value << "\"";
|
||||
return model->value;
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
wxString MachineObject::get_printer_type_display_str() const
|
||||
{
|
||||
std::string display_name = DevPrinterConfigUtil::get_printer_display_name(printer_type);
|
||||
@@ -1733,6 +1760,10 @@ int MachineObject::check_resume_condition()
|
||||
}
|
||||
int MachineObject::command_ams_change_filament(bool load, std::string ams_id, std::string slot_id, int old_temp, int new_temp, std::optional<int> extruder_id)
|
||||
{
|
||||
if (!printer_supports_command("print.ams_change_filament")) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "command_ams_change_filament: printer agent does not support the command";
|
||||
return command_with_dialog(ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED);
|
||||
}
|
||||
json j;
|
||||
try {
|
||||
auto tray_id = 0;
|
||||
@@ -1776,6 +1807,10 @@ int MachineObject::command_ams_change_filament(bool load, std::string ams_id, st
|
||||
|
||||
int MachineObject::command_ams_user_settings(bool start_read_opt, bool tray_read_opt, bool remain_flag)
|
||||
{
|
||||
if (!printer_supports_command("print.ams_user_setting")) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "command_ams_user_settings: printer agent does not support the command";
|
||||
return command_with_dialog(ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED);
|
||||
}
|
||||
json j;
|
||||
j["print"]["command"] = "ams_user_setting";
|
||||
j["print"]["sequence_id"] = std::to_string(MachineObject::m_sequence_id++);
|
||||
@@ -1784,22 +1819,59 @@ int MachineObject::command_ams_user_settings(bool start_read_opt, bool tray_read
|
||||
j["print"]["tray_read_option"] = tray_read_opt;
|
||||
j["print"]["calibrate_remain_flag"] = remain_flag;
|
||||
|
||||
m_fila_system->GetAmsSystemSetting().SetDetectOnInsertEnabled(tray_read_opt);
|
||||
m_fila_system->GetAmsSystemSetting().SetDetectOnPowerupEnabled(start_read_opt);
|
||||
m_fila_system->GetAmsSystemSetting().SetDetectRemainEnabled(remain_flag);
|
||||
ams_user_setting_start = time(nullptr);
|
||||
|
||||
return this->publish_json(j);
|
||||
const int rc = this->publish_json(j);
|
||||
if (rc == 0) {
|
||||
m_fila_system->GetAmsSystemSetting().SetDetectOnInsertEnabled(tray_read_opt);
|
||||
m_fila_system->GetAmsSystemSetting().SetDetectOnPowerupEnabled(start_read_opt);
|
||||
m_fila_system->GetAmsSystemSetting().SetDetectRemainEnabled(remain_flag);
|
||||
ams_user_setting_start = time(nullptr);
|
||||
}
|
||||
return rc;
|
||||
}
|
||||
|
||||
int MachineObject::command_ams_calibrate(int ams_id)
|
||||
{
|
||||
if (!m_agent) return -1;
|
||||
if (!m_agent->owns_agent(printer_agent_id))
|
||||
return command_with_dialog(ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED);
|
||||
return command_with_dialog(m_agent->command_ams_calibrate(get_dev_id(), ams_id, MachineObject::m_sequence_id++, is_lan_mode_printer()));
|
||||
}
|
||||
|
||||
bool MachineObject::printer_supports_command(const char* command) const
|
||||
{
|
||||
if (!command || !m_agent)
|
||||
return false;
|
||||
return m_agent->supports_command(printer_agent_id, get_dev_id(), command);
|
||||
}
|
||||
|
||||
bool MachineObject::printer_uses_filament_mapping() const
|
||||
{
|
||||
if (!m_agent)
|
||||
return false;
|
||||
// An unrecorded owner goes through owns_agent() like any other, because
|
||||
// whichever agent is active is also the one formatting the payload. Only a
|
||||
// recorded owner that differs from the active agent is a mismatch.
|
||||
return m_agent->uses_filament_mapping(printer_agent_id);
|
||||
}
|
||||
|
||||
bool MachineObject::printer_supports_feature(const char* feature) const
|
||||
{
|
||||
return feature && m_agent && m_agent->supports_feature(printer_agent_id, get_dev_id(), feature);
|
||||
}
|
||||
|
||||
bool MachineObject::supports_extrusion_cali() const
|
||||
{
|
||||
// Devices with no agent id predate the agent split and keep the Bambu path.
|
||||
return printer_agent_id.empty() || printer_agent_id == BBL_PRINTER_AGENT_ID;
|
||||
}
|
||||
|
||||
int MachineObject::command_ams_filament_settings(int ams_id, int slot_id, std::string filament_id, std::string setting_id, std::string tray_color, std::string tray_type, int nozzle_temp_min, int nozzle_temp_max)
|
||||
{
|
||||
if (!printer_supports_command("print.ams_filament_setting")) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "command_ams_filament_settings: printer agent does not support slot metadata updates";
|
||||
return command_with_dialog(ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED);
|
||||
}
|
||||
|
||||
int tag_tray_id = 0;
|
||||
int tag_ams_id = ams_id;
|
||||
int tag_slot_id = slot_id;
|
||||
@@ -1834,6 +1906,10 @@ int MachineObject::command_ams_filament_settings(int ams_id, int slot_id, std::s
|
||||
int MachineObject::command_ams_refresh_rfid(int ams_id, int slot_id)
|
||||
{
|
||||
if (!m_agent) return -1;
|
||||
if (!printer_supports_command("print.ams_get_rfid")) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "command_ams_refresh_rfid: printer agent does not support the command";
|
||||
return command_with_dialog(ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED);
|
||||
}
|
||||
return command_with_dialog(m_agent->command_ams_refresh_rfid(get_dev_id(), ams_id, slot_id, MachineObject::m_sequence_id++, is_lan_mode_printer()));
|
||||
}
|
||||
|
||||
@@ -1847,11 +1923,19 @@ int MachineObject::command_start_camera()
|
||||
int MachineObject::command_ams_select_tray(std::string tray_id)
|
||||
{
|
||||
if (!m_agent) return -1;
|
||||
if (!printer_supports_command("print.ams_change_filament")) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "command_ams_select_tray: printer agent does not support the command";
|
||||
return command_with_dialog(ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED);
|
||||
}
|
||||
return command_with_dialog(m_agent->command_ams_select_tray(get_dev_id(), tray_id, MachineObject::m_sequence_id++, is_lan_mode_printer()));
|
||||
}
|
||||
|
||||
int MachineObject::command_ams_control(std::string action)
|
||||
{
|
||||
if (!printer_supports_command("print.ams_control")) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "command_ams_control: printer agent does not support the command";
|
||||
return command_with_dialog(ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED);
|
||||
}
|
||||
if (action == "resume" && check_resume_condition()) return 0;
|
||||
|
||||
//valid actions
|
||||
@@ -1867,6 +1951,10 @@ int MachineObject::command_ams_control(std::string action)
|
||||
|
||||
int MachineObject::command_ams_drying_stop()
|
||||
{
|
||||
if (!printer_supports_command("print.auto_stop_ams_dry")) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "command_ams_drying_stop: printer agent does not support the command";
|
||||
return command_with_dialog(ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED);
|
||||
}
|
||||
json j;
|
||||
j["print"]["command"] = "auto_stop_ams_dry";
|
||||
j["print"]["sequence_id"] = std::to_string(MachineObject::m_sequence_id++);
|
||||
@@ -2817,7 +2905,9 @@ bool MachineObject::is_camera_busy_off()
|
||||
int MachineObject::publish_json(const json& json_item, int qos, int flag)
|
||||
{
|
||||
int rtn = 0;
|
||||
if (is_lan_mode_printer()) {
|
||||
if (m_agent && !m_agent->owns_agent(printer_agent_id)) {
|
||||
rtn = ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED;
|
||||
} else if (is_lan_mode_printer()) {
|
||||
rtn = local_publish_json(json_item.dump(), qos, flag);
|
||||
} else {
|
||||
rtn = cloud_publish_json(json_item.dump(), qos, flag);
|
||||
@@ -2861,6 +2951,7 @@ int MachineObject::local_publish_json(std::string json_str, int qos, int flag)
|
||||
std::string MachineObject::setting_id_to_type(std::string setting_id, std::string tray_type)
|
||||
{
|
||||
std::string type;
|
||||
if (wxTheApp == nullptr) return tray_type;
|
||||
PresetBundle* preset_bundle = GUI::wxGetApp().preset_bundle;
|
||||
if (preset_bundle) {
|
||||
for (auto it = preset_bundle->filaments.begin(); it != preset_bundle->filaments.end(); it++) {
|
||||
@@ -4197,6 +4288,9 @@ int MachineObject::parse_json(std::string tunnel, std::string payload, bool key_
|
||||
vt_slot[0].setting_id = jj["tray_info_idx"].get<std::string>();
|
||||
//vt_tray.type = jj["tray_type"].get<std::string>();
|
||||
vt_slot[0].m_fila_type = setting_id_to_type(vt_slot[0].setting_id, jj["tray_type"].get<std::string>());
|
||||
// The ack carries the whole slot; re-derive empty so the panel flips off
|
||||
// "Empty" without waiting out the hold.
|
||||
vt_slot[0].UpdateEmptyState(true, printer_agent_id == ORCA_PRINTER_AGENT_ID);
|
||||
// delay update
|
||||
vt_slot[0].set_hold_count();
|
||||
} else {
|
||||
@@ -4222,6 +4316,9 @@ int MachineObject::parse_json(std::string tunnel, std::string payload, bool key_
|
||||
|
||||
tray_it->second->setting_id = jj["tray_info_idx"].get<std::string>();
|
||||
tray_it->second->m_fila_type = setting_id_to_type(tray_it->second->setting_id, jj["tray_type"].get<std::string>());
|
||||
// The ack carries the whole slot; re-derive empty so the panel flips off
|
||||
// "Empty" without waiting out the hold.
|
||||
tray_it->second->UpdateEmptyState(true, printer_agent_id == ORCA_PRINTER_AGENT_ID);
|
||||
// delay update
|
||||
tray_it->second->set_hold_count();
|
||||
} else {
|
||||
@@ -5160,7 +5257,7 @@ DevAmsTray MachineObject::parse_vt_tray(json vtray)
|
||||
//std::string type = vtray["tray_type"].get<std::string>();
|
||||
std::string type = setting_id_to_type(vt_tray.setting_id, vtray["tray_type"].get<std::string>());
|
||||
// vt_tray.setting_id is our OF id (translated on the way in); the two support ids below are the printer's own.
|
||||
auto* agent = GUI::wxGetApp().getAgent();
|
||||
auto* agent = wxTheApp != nullptr ? GUI::wxGetApp().getAgent() : nullptr;
|
||||
const std::string printer_filament_id = agent ? agent->from_orca_filament_id(vt_tray.setting_id) : vt_tray.setting_id;
|
||||
if (printer_filament_id == "GFS00") {
|
||||
vt_tray.m_fila_type = "PLA-S";
|
||||
@@ -5255,6 +5352,8 @@ DevAmsTray MachineObject::parse_vt_tray(json vtray)
|
||||
else {
|
||||
vt_tray.remain = -1;
|
||||
}
|
||||
vt_tray.UpdateEmptyState(vtray.contains("tray_info_idx") && vtray.contains("tray_type"),
|
||||
printer_agent_id == ORCA_PRINTER_AGENT_ID);
|
||||
}
|
||||
|
||||
return vt_tray;
|
||||
@@ -5548,11 +5647,11 @@ void MachineObject::parse_new_info2(const json& info)
|
||||
if (capabilities_it == info.end() || !capabilities_it->is_object())
|
||||
return;
|
||||
const auto flags_it = capabilities_it->find("flags");
|
||||
if (flags_it == capabilities_it->end() || !flags_it->is_object())
|
||||
const bool has_flags = flags_it != capabilities_it->end() && flags_it->is_object();
|
||||
if (!has_flags)
|
||||
return;
|
||||
|
||||
const json& flags = *flags_it;
|
||||
BOOST_LOG_TRIVIAL(info) << "parse_new_info2: OrcaSonar capability flags=" << flags.dump();
|
||||
BOOST_LOG_TRIVIAL(info) << "parse_new_info2: capability flags=" << flags.dump();
|
||||
|
||||
auto parse_bool = [&flags](const char* name, bool& target) {
|
||||
const auto it = flags.find(name);
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
#include <string>
|
||||
#include <memory>
|
||||
#include <chrono>
|
||||
#include <set>
|
||||
#include <unordered_set>
|
||||
#include <optional>
|
||||
#include <boost/thread.hpp>
|
||||
@@ -72,6 +73,7 @@ class DeviceErrorDialog; // Previous definitions
|
||||
|
||||
class NetworkAgent;
|
||||
// Orca: ManualPaCaliMethod now provided by DeviceCore/DevCalib.h (enum class)
|
||||
class PresetBundle;
|
||||
|
||||
#define UpgradeNoError 0
|
||||
#define UpgradeDownloadFailed -1
|
||||
@@ -107,6 +109,11 @@ struct DevPrintTaskRatingInfo;
|
||||
// given nozzle diameter (mm), bucketed per nozzle size to mirror the printer firmware.
|
||||
bool is_stringing_prone_filament(const std::string& filament_id, float nozzle_diameter);
|
||||
|
||||
// The printer model filament presets are matched against for a connected machine. OrcaSonar's
|
||||
// model id is optional, so a device with no installed vendor model falls back to the selected
|
||||
// printer profile rather than resolving to no filament list at all.
|
||||
std::string resolve_filament_printer_model(const std::string& printer_type, PresetBundle* preset_bundle);
|
||||
|
||||
class MachineObject
|
||||
{
|
||||
private:
|
||||
@@ -795,6 +802,13 @@ public:
|
||||
int command_refresh_nozzle();
|
||||
int command_set_chamber(int temp);
|
||||
int check_resume_condition();
|
||||
bool printer_supports_command(const char* command) const;
|
||||
bool printer_supports_feature(const char* feature) const;
|
||||
bool printer_uses_filament_mapping() const;
|
||||
// The per-tray flow-dynamics K/N records are a Bambu firmware feature. Other
|
||||
// agents have no printer-side calibration to read or write, so the AMS UI must
|
||||
// not offer K/N for them.
|
||||
bool supports_extrusion_cali() const;
|
||||
// ams controls
|
||||
//int command_ams_switch(int tray_index, int old_temp = 210, int new_temp = 210);
|
||||
int command_ams_change_filament(bool load, std::string ams_id, std::string slot_id, int old_temp = 210, int new_temp = 210, std::optional<int> extruder_id = std::nullopt);
|
||||
|
||||
@@ -970,7 +970,11 @@ int ReselectMachineDialog::CaculateSwitcherDistribution(MachineObject* obj, cons
|
||||
const auto& can = ams.cans[j];
|
||||
auto id = getTrayID(obj, ams.ams_id, can.can_id);
|
||||
auto material = can.material_name;
|
||||
if (can.material_state == AMSCanType::AMS_CAN_TYPE_THIRDBRAND ||
|
||||
if (can.is_empty || can.material_state == AMSCanType::AMS_CAN_TYPE_EMPTY)
|
||||
{
|
||||
material = L("Empty");
|
||||
}
|
||||
else if (can.material_state == AMSCanType::AMS_CAN_TYPE_THIRDBRAND ||
|
||||
can.material_state == AMSCanType::AMS_CAN_TYPE_BRAND ||
|
||||
can.material_state == AMSCanType::AMS_CAN_TYPE_VIRTUAL)
|
||||
{
|
||||
@@ -979,10 +983,6 @@ int ReselectMachineDialog::CaculateSwitcherDistribution(MachineObject* obj, cons
|
||||
material = L("?");
|
||||
}
|
||||
}
|
||||
if (can.material_state == AMSCanType::AMS_CAN_TYPE_EMPTY)
|
||||
{
|
||||
material = L("Empty");
|
||||
}
|
||||
|
||||
auto itOK = std::find_if(posOK.begin(), posOK.end(), [&](const trayHelper& tray){
|
||||
auto amsID = std::get<0>(tray);
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include "libslic3r/ProjectTask.hpp"
|
||||
#include "DeviceManager.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
namespace GUI {
|
||||
|
||||
// True when the normalized ams_mapping2 carries any entry the agent's serializer
|
||||
// would put on the wire: every integer pair except the {255,255} unmatched
|
||||
// sentinel, external slots ({255,0}/{254,0}) included. This mirrors
|
||||
// OrcaPrinterAgent::build_filament_mapping exactly, so the GUI gate and the
|
||||
// agent gate agree; a mismatch lets an entry reach the agent and be refused late
|
||||
// with a generic publish error instead of the designed message.
|
||||
inline bool has_engaged_filament_mapping(const std::string& ams_mapping2)
|
||||
{
|
||||
const nlohmann::json mapping = nlohmann::json::parse(ams_mapping2, nullptr, false);
|
||||
if (mapping.is_discarded() || !mapping.is_array())
|
||||
return false;
|
||||
for (const auto& entry : mapping) {
|
||||
if (!entry.is_object())
|
||||
continue;
|
||||
const auto ams_id_it = entry.find("ams_id");
|
||||
const auto slot_id_it = entry.find("slot_id");
|
||||
if (ams_id_it == entry.end() || slot_id_it == entry.end())
|
||||
continue;
|
||||
if (!ams_id_it->is_number_integer() || !slot_id_it->is_number_integer())
|
||||
continue;
|
||||
if (ams_id_it->get<int>() == 255 && slot_id_it->get<int>() == 255)
|
||||
continue; // unmatched/unused sentinel: the serializer drops it
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// A device with no AMS units has a single source: the external spool. OrcaSlicer's
|
||||
// auto-mapping force-selects it for every filament (DevMapping.cpp), but that is not
|
||||
// a lane choice: keep it out of the send gate and off print.gcode_file.
|
||||
inline void drop_forced_external_selection(bool device_has_ams, std::string& ams_mapping2)
|
||||
{
|
||||
if (!device_has_ams)
|
||||
ams_mapping2.clear();
|
||||
}
|
||||
|
||||
// A used filament with no target would be silently dropped from the wire
|
||||
// mapping, so the print must be refused rather than run the wrong material.
|
||||
// m_ams_mapping_result carries exactly the filaments the slice uses.
|
||||
inline bool has_used_filament_without_target(const std::vector<FilamentInfo>& result)
|
||||
{
|
||||
for (const auto& f : result) {
|
||||
if (f.get_ams_id() < 0 || f.get_slot_id() < 0)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// True when at least one used filament already has a target. The used-unmapped
|
||||
// refusal applies to a partially mapped print; a print with no mapping at all
|
||||
// is handled by the existing all-invalid send-path flow.
|
||||
inline bool has_any_mapped_target(const std::vector<FilamentInfo>& result)
|
||||
{
|
||||
for (const auto& f : result) {
|
||||
if (f.get_ams_id() >= 0 && f.get_slot_id() >= 0)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
// Refusal reason for a printer agent that serializes lane selection into
|
||||
// print.gcode_file's per-print filament_mapping field.
|
||||
enum class MappingSendError {
|
||||
none, // no refusal; ams_mapping2 is normalized for send
|
||||
unsupported, // a mapping is engaged but the connector did not advertise it
|
||||
incomplete, // a used filament has no target while others do
|
||||
};
|
||||
|
||||
// Applies the send-time mapping policy and normalizes ams_mapping2 in place.
|
||||
// Agents that do not speak filament_mapping keep their legacy payload untouched.
|
||||
inline MappingSendError prepare_filament_mapping_for_send(MachineObject* obj,
|
||||
std::string& ams_mapping2,
|
||||
const std::vector<FilamentInfo>& mapping_result)
|
||||
{
|
||||
if (!obj || !obj->printer_uses_filament_mapping())
|
||||
return MappingSendError::none;
|
||||
|
||||
// A device with no AMS units has one source, the external spool:
|
||||
// auto-mapping force-selects it, which is not a lane choice. Drop it before
|
||||
// the capability gate so it cannot refuse a print nobody mapped.
|
||||
drop_forced_external_selection(obj->HasAms(), ams_mapping2);
|
||||
|
||||
if (!obj->printer_supports_feature("filament_mapping") && has_engaged_filament_mapping(ams_mapping2))
|
||||
return MappingSendError::unsupported;
|
||||
if (has_any_mapped_target(mapping_result) && has_used_filament_without_target(mapping_result))
|
||||
return MappingSendError::incomplete;
|
||||
return MappingSendError::none;
|
||||
}
|
||||
|
||||
} // namespace GUI
|
||||
} // namespace Slic3r
|
||||
@@ -1160,7 +1160,8 @@ void GUI_App::post_init()
|
||||
m_agent->set_on_http_error_fn([this](CloudEvent event, unsigned int status, std::string body) {
|
||||
this->handle_http_error(status, body, event.provider);
|
||||
});
|
||||
m_agent->start_discovery(true, false);
|
||||
if (should_start_ssdp_discovery())
|
||||
m_agent->start_discovery(true, false);
|
||||
}
|
||||
|
||||
//update the plugin tips
|
||||
@@ -3232,9 +3233,16 @@ bool GUI_App::on_init_inner()
|
||||
#ifdef _MSW_DARK_MODE
|
||||
|
||||
#ifndef __WINDOWS__
|
||||
// The label colours and the dark mode flag were set above from the saved mode. When the system theme
|
||||
// differs from it, refresh them, or dialogs that map their colours stay in the old mode for this session.
|
||||
const bool saved_dark_mode = dark_mode();
|
||||
wxSystemAppearance app = wxSystemSettings::GetAppearance();
|
||||
GUI::wxGetApp().app_config->set("dark_color_mode", app.IsDark() ? "1" : "0");
|
||||
GUI::wxGetApp().app_config->save();
|
||||
if (dark_mode() != saved_dark_mode) {
|
||||
init_label_colours();
|
||||
Update_dark_mode_flag();
|
||||
}
|
||||
#endif // __APPLE__
|
||||
|
||||
|
||||
@@ -4165,6 +4173,18 @@ std::string GUI_App::resolve_printer_agent_id(const std::string& stored_id) cons
|
||||
return (preset_bundle && preset_bundle->is_bbl_vendor()) ? BBL_PRINTER_AGENT_ID : ORCA_PRINTER_AGENT_ID;
|
||||
}
|
||||
|
||||
bool GUI_App::should_start_ssdp_discovery()
|
||||
{
|
||||
// Non-Orca agents (BBL, plugins) keep their own discovery; OrcaSonar only scans in the
|
||||
// background when one of its machines is already configured, otherwise the popup drives it.
|
||||
const std::string agent_id = resolve_printer_agent_id(
|
||||
preset_bundle ? preset_bundle->printers.get_edited_preset().config.opt_string("printer_agent")
|
||||
: std::string());
|
||||
if (agent_id != ORCA_PRINTER_AGENT_ID)
|
||||
return true;
|
||||
return m_device_manager && !m_device_manager->get_my_machine_list(ORCA_PRINTER_AGENT_ID).empty();
|
||||
}
|
||||
|
||||
std::string GUI_App::canonical_printer_agent_id(const std::string& picked_id)
|
||||
{
|
||||
return picked_id == resolve_printer_agent_id("") ? std::string() : picked_id;
|
||||
@@ -4236,7 +4256,8 @@ void GUI_App::switch_printer_agent()
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": printer agent switched to " << effective_agent_id;
|
||||
|
||||
// Start discovery so Python agents can populate the device list via SSDP callback
|
||||
m_agent->start_discovery(true, false);
|
||||
if (should_start_ssdp_discovery())
|
||||
m_agent->start_discovery(true, false);
|
||||
|
||||
// Auto-switch MachineObject (new agent has empty device_info, so always re-select)
|
||||
select_machine(effective_agent_id);
|
||||
|
||||
@@ -403,6 +403,9 @@ public:
|
||||
// Reconcile the live printer agent with the stored preset selection.
|
||||
void switch_printer_agent();
|
||||
|
||||
// Whether the background SSDP scan should run for the effective printer agent.
|
||||
bool should_start_ssdp_discovery();
|
||||
|
||||
std::string resolve_printer_agent_id(const std::string& stored_id) const;
|
||||
// ORCA TODO: in the future, bbl presets should specify "bbl" printer agent id
|
||||
// then, all resolve and canonical would just be ORCA<->""
|
||||
|
||||
@@ -1617,7 +1617,7 @@ void ObjectList::list_manipulation(const wxPoint& mouse_pos, bool evt_context_me
|
||||
int obj_idx, vol_idx;
|
||||
get_selected_item_indexes(obj_idx, vol_idx, item);
|
||||
if (obj_idx != -1) {
|
||||
Plater::TakeSnapshot(plater, _u8L("Shift objects to bed"));
|
||||
Plater::TakeSnapshot snapshot(plater, _u8L("Shift objects to bed"));
|
||||
(*m_objects)[obj_idx]->ensure_on_bed();
|
||||
cnv->reload_scene(true, true);
|
||||
update_info_items(obj_idx);
|
||||
@@ -2779,12 +2779,13 @@ void ObjectList::del_info_item(const int obj_idx, InfoItemType type)
|
||||
GLCanvas3D* cnv = plater->canvas3D();
|
||||
|
||||
switch (type) {
|
||||
case InfoItemType::CustomSupports:
|
||||
case InfoItemType::CustomSupports: {
|
||||
cnv->get_gizmos_manager().reset_all_states();
|
||||
Plater::TakeSnapshot(plater, "Remove support painting");
|
||||
Plater::TakeSnapshot snapshot(plater, "Remove support painting");
|
||||
for (ModelVolume* mv : (*m_objects)[obj_idx]->volumes)
|
||||
mv->supported_facets.reset();
|
||||
break;
|
||||
}
|
||||
|
||||
// BBS: remove CustomSeam
|
||||
|
||||
@@ -2795,19 +2796,21 @@ void ObjectList::del_info_item(const int obj_idx, InfoItemType type)
|
||||
}
|
||||
break;
|
||||
|
||||
case InfoItemType::MmSegmentation:
|
||||
case InfoItemType::MmSegmentation: {
|
||||
cnv->get_gizmos_manager().reset_all_states();
|
||||
Plater::TakeSnapshot(plater, "Remove color painting");
|
||||
Plater::TakeSnapshot snapshot(plater, "Remove color painting");
|
||||
for (ModelVolume* mv : (*m_objects)[obj_idx]->volumes)
|
||||
mv->mmu_segmentation_facets.reset();
|
||||
break;
|
||||
}
|
||||
|
||||
case InfoItemType::FuzzySkin:
|
||||
case InfoItemType::FuzzySkin: {
|
||||
cnv->get_gizmos_manager().reset_all_states();
|
||||
Plater::TakeSnapshot(plater, _u8L("Remove paint-on fuzzy skin"));
|
||||
Plater::TakeSnapshot snapshot(plater, _u8L("Remove paint-on fuzzy skin"));
|
||||
for (ModelVolume* mv : (*m_objects)[obj_idx]->volumes)
|
||||
mv->fuzzy_skin_facets.reset();
|
||||
break;
|
||||
}
|
||||
|
||||
// BBS: remove Sinking
|
||||
case InfoItemType::Undef : assert(false); break;
|
||||
@@ -6718,7 +6721,7 @@ void ObjectList::OnEditingStarted(wxDataViewEvent &event)
|
||||
else if (col == colSinking) {
|
||||
Plater * plater = wxGetApp().plater();
|
||||
GLCanvas3D *cnv = plater->canvas3D();
|
||||
Plater::TakeSnapshot(plater, _u8L("Shift objects to bed"));
|
||||
Plater::TakeSnapshot snapshot(plater, _u8L("Shift objects to bed"));
|
||||
int obj_idx, vol_idx;
|
||||
get_selected_item_indexes(obj_idx, vol_idx, item);
|
||||
(*m_objects)[obj_idx]->ensure_on_bed();
|
||||
|
||||
+119
-90
@@ -1,13 +1,16 @@
|
||||
#include "ImageGrid.h"
|
||||
#include "Printer/PrinterFileSystem.h"
|
||||
#include "Printer/BambuFileGridModel.h"
|
||||
#include "wxExtensions.hpp"
|
||||
#include "Widgets/Label.hpp"
|
||||
#include "I18N.hpp"
|
||||
#include "GUI_App.hpp"
|
||||
#include "GUI.hpp"
|
||||
|
||||
#include <slic3r/GUI/Printer/IFileGridModel.h>
|
||||
#include <utility>
|
||||
#include <boost/smart_ptr/shared_ptr.hpp>
|
||||
#include <boost/smart_ptr/make_shared_object.hpp>
|
||||
#include <wx/colour.h>
|
||||
#include <cstddef>
|
||||
#include <wx/dcclient.h>
|
||||
@@ -76,18 +79,11 @@ ImageGrid::ImageGrid(wxWindow * parent)
|
||||
|
||||
void ImageGrid::SetFileSystem(boost::shared_ptr<PrinterFileSystem> file_sys)
|
||||
{
|
||||
if (m_file_sys) {
|
||||
m_file_sys->Unbind(EVT_MODE_CHANGED, &ImageGrid::changedEvent, this);
|
||||
m_file_sys->Unbind(EVT_FILE_CHANGED, &ImageGrid::changedEvent, this);
|
||||
m_file_sys->Unbind(EVT_THUMBNAIL, &ImageGrid::changedEvent, this);
|
||||
m_file_sys->Unbind(EVT_DOWNLOAD, &ImageGrid::changedEvent, this);
|
||||
}
|
||||
m_file_sys = file_sys;
|
||||
if (m_file_sys) {
|
||||
m_file_sys->Bind(EVT_MODE_CHANGED, &ImageGrid::changedEvent, this);
|
||||
m_file_sys->Bind(EVT_FILE_CHANGED, &ImageGrid::changedEvent, this);
|
||||
m_file_sys->Bind(EVT_THUMBNAIL, &ImageGrid::changedEvent, this);
|
||||
m_file_sys->Bind(EVT_DOWNLOAD, &ImageGrid::changedEvent, this);
|
||||
m_model.reset();
|
||||
if (file_sys) {
|
||||
m_model = boost::make_shared<BambuFileGridModel>(file_sys);
|
||||
m_model->SetChangeHandler([this](FileGridChange c) { onModelChange(c); });
|
||||
}
|
||||
m_row_count = 0;
|
||||
m_col_count = 1;
|
||||
@@ -96,9 +92,22 @@ void ImageGrid::SetFileSystem(boost::shared_ptr<PrinterFileSystem> file_sys)
|
||||
UpdateFileSystem();
|
||||
}
|
||||
|
||||
void ImageGrid::SetModel(boost::shared_ptr<IFileGridModel> model)
|
||||
{
|
||||
m_file_sys.reset();
|
||||
m_model = std::move(model);
|
||||
if (m_model)
|
||||
m_model->SetChangeHandler([this](FileGridChange c) { onModelChange(c); });
|
||||
m_row_count = 0;
|
||||
m_col_count = 1;
|
||||
m_row_offset = 0;
|
||||
m_scroll_offset = 0;
|
||||
UpdateFileSystem();
|
||||
}
|
||||
|
||||
void ImageGrid::SetStatus(ScalableBitmap const & icon, wxString const &msg)
|
||||
{
|
||||
int code = m_file_sys ? m_file_sys->GetLastError() : 1;
|
||||
int code = m_model ? m_model->GetLastError() : 1;
|
||||
m_status_icon = icon;
|
||||
m_status_msg = wxString::Format(msg, code);
|
||||
BOOST_LOG_TRIVIAL(info) << "ImageGrid::SetStatus: " << m_status_msg.ToUTF8().data();
|
||||
@@ -107,26 +116,26 @@ void ImageGrid::SetStatus(ScalableBitmap const & icon, wxString const &msg)
|
||||
|
||||
void Slic3r::GUI::ImageGrid::SetFileType(int type, std::string const &storage)
|
||||
{
|
||||
if (!m_file_sys)
|
||||
if (!m_model)
|
||||
return;
|
||||
m_file_sys->SetFileType((PrinterFileSystem::FileType) type, storage);
|
||||
m_model->SetFileType((FileGridType) type, storage);
|
||||
}
|
||||
|
||||
void Slic3r::GUI::ImageGrid::SetGroupMode(int mode)
|
||||
{
|
||||
if (!m_file_sys)
|
||||
if (!m_model)
|
||||
return;
|
||||
if (m_file_sys->GetCount() == 0) {
|
||||
m_file_sys->SetGroupMode((PrinterFileSystem::GroupMode) mode);
|
||||
if (m_model->GetCount() == 0) {
|
||||
m_model->SetGroupMode((FileGridGroup) mode);
|
||||
return;
|
||||
}
|
||||
wxSize size = GetClientSize();
|
||||
int index = (m_row_offset + 1 < m_row_count || m_row_count == 0)
|
||||
? m_row_offset / 4 * m_col_count
|
||||
: ((m_file_sys->GetCount() + m_col_count - 1) / m_col_count - (size.y + m_border_size.GetHeight() - 1) / m_cell_size.GetHeight()) * m_col_count;
|
||||
auto & file = m_file_sys->GetFile(index);
|
||||
m_file_sys->SetGroupMode((PrinterFileSystem::GroupMode) mode);
|
||||
index = m_file_sys->GetIndexAtTime(file.time);
|
||||
: ((m_model->GetCount() + m_col_count - 1) / m_col_count - (size.y + m_border_size.GetHeight() - 1) / m_cell_size.GetHeight()) * m_col_count;
|
||||
time_t time = m_model->GetFile(index).time;
|
||||
m_model->SetGroupMode((FileGridGroup) mode);
|
||||
index = m_model->GetIndexAtTime(time);
|
||||
// UpdateFileSystem(); call by changed event
|
||||
m_row_offset = index / m_col_count * 4;
|
||||
if (m_row_offset >= m_row_count)
|
||||
@@ -137,8 +146,8 @@ void Slic3r::GUI::ImageGrid::SetGroupMode(int mode)
|
||||
void Slic3r::GUI::ImageGrid::SetSelecting(bool selecting)
|
||||
{
|
||||
m_selecting = selecting;
|
||||
if (m_file_sys)
|
||||
m_file_sys->SelectAll(false);
|
||||
if (m_model)
|
||||
m_model->SelectAll(false);
|
||||
Refresh();
|
||||
}
|
||||
|
||||
@@ -166,14 +175,14 @@ void Slic3r::GUI::ImageGrid::Rescale()
|
||||
void Slic3r::GUI::ImageGrid::Select(size_t index)
|
||||
{
|
||||
if (m_selecting) {
|
||||
m_file_sys->ToggleSelect(index);
|
||||
m_model->ToggleSelect(index);
|
||||
Refresh();
|
||||
return;
|
||||
}
|
||||
if (m_file_sys->GetGroupMode() == PrinterFileSystem::G_NONE) {
|
||||
if (m_model->GetGroupMode() == FileGridGroup::All) {
|
||||
return;
|
||||
}
|
||||
index = m_file_sys->EnterSubGroup(index);
|
||||
index = m_model->EnterSubGroup(index);
|
||||
// UpdateFileSystem(); call by changed event
|
||||
m_row_offset = index / m_col_count * 4;
|
||||
if (m_row_offset >= m_row_count)
|
||||
@@ -193,9 +202,9 @@ void Slic3r::GUI::ImageGrid::DoAction(size_t index, int action)
|
||||
|
||||
void Slic3r::GUI::ImageGrid::UpdateFileSystem()
|
||||
{
|
||||
if (!m_file_sys) return;
|
||||
if (m_file_sys->GetFileType() < PrinterFileSystem::F_MODEL) {
|
||||
if (m_file_sys->GetGroupMode() == PrinterFileSystem::G_NONE) {
|
||||
if (!m_model) return;
|
||||
if (m_model->GetFileType() < FileGridType::Model) {
|
||||
if (m_model->GetGroupMode() == FileGridGroup::All) {
|
||||
m_cell_size.Set(396, 228);
|
||||
m_border_size.Set(384, 216);
|
||||
}
|
||||
@@ -216,10 +225,10 @@ void Slic3r::GUI::ImageGrid::UpdateFileSystem()
|
||||
|
||||
void ImageGrid::UpdateLayout()
|
||||
{
|
||||
if (!m_file_sys) return;
|
||||
if (!m_model) return;
|
||||
wxSize size = GetClientSize();
|
||||
wxSize title_mask_size{0, 60 * em_unit(this) / 10};
|
||||
if (m_file_sys->GetGroupMode() == PrinterFileSystem::G_NONE) {
|
||||
if (m_model->GetGroupMode() == FileGridGroup::All) {
|
||||
title_mask_size.y = 20 * em_unit(this) / 10;
|
||||
size.y -= title_mask_size.y;
|
||||
}
|
||||
@@ -227,7 +236,7 @@ void ImageGrid::UpdateLayout()
|
||||
int cell_height = m_cell_size.GetHeight();
|
||||
int ncol = (size.GetWidth() - cell_width + m_border_size.GetWidth()) / cell_width;
|
||||
if (ncol <= 0) ncol = 1;
|
||||
int total_height = (m_file_sys->GetCount() + ncol - 1) / ncol * cell_height + cell_height - m_border_size.GetHeight();
|
||||
int total_height = (m_model->GetCount() + ncol - 1) / ncol * cell_height + cell_height - m_border_size.GetHeight();
|
||||
int nrow = (total_height - size.GetHeight() + cell_height / 4 - 1) / (cell_height / 4);
|
||||
m_row_offset = m_row_offset * m_col_count / ncol;
|
||||
m_col_count = ncol;
|
||||
@@ -236,7 +245,7 @@ void ImageGrid::UpdateLayout()
|
||||
m_row_offset = m_row_count == 0 ? 0 : m_row_count - 1;
|
||||
m_scroll_offset = 0;
|
||||
// create mask
|
||||
if (m_file_sys->GetGroupMode() == PrinterFileSystem::G_NONE) {
|
||||
if (m_model->GetGroupMode() == FileGridGroup::All) {
|
||||
title_mask_size.x = (m_col_count - 1) * m_cell_size.GetWidth() + m_border_size.GetWidth();
|
||||
}
|
||||
else {
|
||||
@@ -252,7 +261,7 @@ void ImageGrid::UpdateLayout()
|
||||
|
||||
void Slic3r::GUI::ImageGrid::UpdateFocusRange()
|
||||
{
|
||||
if (!m_file_sys) return;
|
||||
if (!m_model) return;
|
||||
wxSize size = GetClientSize();
|
||||
wxPoint off;
|
||||
int index = firstItem(size, off);
|
||||
@@ -261,15 +270,21 @@ void Slic3r::GUI::ImageGrid::UpdateFocusRange()
|
||||
count += m_col_count;
|
||||
off.y += m_cell_size.y;
|
||||
}
|
||||
m_file_sys->SetFocusRange(index, count);
|
||||
m_model->SetFocusRange(index, count);
|
||||
}
|
||||
|
||||
bool Slic3r::GUI::ImageGrid::hasSecondAction(const FileGridCard &card) const
|
||||
{
|
||||
return m_show_download || card.downloading ||
|
||||
(m_model->GetFileType() == FileGridType::Model && card.printable);
|
||||
}
|
||||
|
||||
std::pair<int, size_t> Slic3r::GUI::ImageGrid::HitTest(wxPoint const &pt)
|
||||
{
|
||||
if (!m_file_sys)
|
||||
if (!m_model)
|
||||
return {HIT_NONE, -1};
|
||||
wxSize size = GetClientSize();
|
||||
if (m_file_sys->GetCount() == 0) {
|
||||
if (m_model->GetCount() == 0) {
|
||||
if (wxRect({0, 0}, m_border_size).CenterIn(wxRect({0, 0}, size)).Contains(pt))
|
||||
return {HIT_STATUS, 0};
|
||||
return {HIT_NONE, -1};
|
||||
@@ -287,17 +302,19 @@ std::pair<int, size_t> Slic3r::GUI::ImageGrid::HitTest(wxPoint const &pt)
|
||||
index += m_col_count;
|
||||
off.y -= m_cell_size.GetHeight();
|
||||
}
|
||||
if (index >= m_file_sys->GetCount()) { return {HIT_NONE, -1}; }
|
||||
if (index >= m_model->GetCount()) { return {HIT_NONE, -1}; }
|
||||
if (!m_content_rect.Contains(off)) { return {HIT_NONE, -1}; }
|
||||
if (!m_selecting) {
|
||||
wxRect hover_rect{0, m_content_rect.GetHeight() - m_buttons_background.GetHeight(), m_content_rect.GetWidth(), m_buttons_background.GetHeight()};
|
||||
auto & file = m_file_sys->GetFile(index);
|
||||
int btn = file.IsDownload() && file.DownloadProgress() >= 0 ? 3 : 2;
|
||||
if (m_file_sys->GetFileType() == PrinterFileSystem::F_MODEL) {
|
||||
if (m_show_download)
|
||||
const FileGridCard &card = m_model->GetFile(index);
|
||||
int btn = card.downloading && card.download_progress >= 0 ? 3 : 2;
|
||||
if (m_model->GetFileType() == FileGridType::Model) {
|
||||
if (m_show_download && card.printable)
|
||||
btn = 3;
|
||||
hover_rect.y -= m_content_rect.GetHeight() * 64 / 264;
|
||||
}
|
||||
if (!hasSecondAction(card))
|
||||
btn = 1;
|
||||
if (hover_rect.Contains(off.x, off.y)) {
|
||||
return {HIT_ACTION, index * 4 + off.x * btn / hover_rect.GetWidth()};
|
||||
} // Two buttons
|
||||
@@ -315,12 +332,12 @@ void ImageGrid::mouseMoved(wxMouseEvent& event)
|
||||
m_hit_item = hit.second;
|
||||
if (hit.first == HIT_ITEM) {
|
||||
SetToolTip({});
|
||||
auto & file = m_file_sys->GetFile(hit.second);
|
||||
if (auto title = file.Title(); !title.empty()) {
|
||||
auto tip = wxString::Format(_L("File: %s\nTitle: %s\n"), from_u8(file.name), from_u8(title));
|
||||
const FileGridCard &card = m_model->GetFile(hit.second);
|
||||
if (auto title = card.title; !title.empty()) {
|
||||
auto tip = wxString::Format(_L("File: %s\nTitle: %s\n"), from_u8(card.name), from_u8(title));
|
||||
SetToolTip(tip);
|
||||
} else {
|
||||
SetToolTip(from_u8(file.name));
|
||||
SetToolTip(from_u8(card.name));
|
||||
}
|
||||
} else
|
||||
SetToolTip({});
|
||||
@@ -372,9 +389,9 @@ void ImageGrid::mouseReleased(wxMouseEvent& event)
|
||||
else if (m_hit_type == HIT_ACTION)
|
||||
DoAction(m_hit_item / 4, m_hit_item & 3);
|
||||
else if (m_hit_type == HIT_MODE)
|
||||
SetGroupMode(static_cast<PrinterFileSystem::GroupMode>(2 - m_hit_item));
|
||||
SetGroupMode(static_cast<int>(2 - m_hit_item));
|
||||
else if (m_hit_type == HIT_STATUS)
|
||||
m_file_sys->Retry();
|
||||
m_model->Retry();
|
||||
else
|
||||
Refresh();
|
||||
} else {
|
||||
@@ -403,20 +420,11 @@ void ImageGrid::mouseWheelMoved(wxMouseEvent &event)
|
||||
Refresh();
|
||||
}
|
||||
|
||||
void Slic3r::GUI::ImageGrid::changedEvent(wxCommandEvent& evt)
|
||||
void Slic3r::GUI::ImageGrid::onModelChange(FileGridChange change)
|
||||
{
|
||||
evt.Skip();
|
||||
BOOST_LOG_TRIVIAL(debug) << "ImageGrid::changedEvent: " << evt.GetEventType() << " index: " << evt.GetInt()
|
||||
<< " name: " << evt.GetString().ToUTF8().data() << " extra: " << evt.GetExtraLong();
|
||||
if (evt.GetEventType() == EVT_FILE_CHANGED) {
|
||||
if (evt.GetInt() == -1)
|
||||
m_file_sys->DownloadCheckFiles(wxGetApp().app_config->get("download_path"));
|
||||
BOOST_LOG_TRIVIAL(debug) << "ImageGrid::onModelChange: " << (int) change;
|
||||
if (change == FileGridChange::Files || change == FileGridChange::Mode)
|
||||
UpdateFileSystem();
|
||||
}
|
||||
else if (evt.GetEventType() == EVT_MODE_CHANGED)
|
||||
UpdateFileSystem();
|
||||
//else if (evt.GetEventType() == EVT_THUMBNAIL)
|
||||
// RefreshRect(itemRect(evt.GetInt()), false);
|
||||
else
|
||||
Refresh();
|
||||
}
|
||||
@@ -431,7 +439,7 @@ void ImageGrid::paintEvent(wxPaintEvent& evt)
|
||||
size_t Slic3r::GUI::ImageGrid::firstItem(wxSize const &size, wxPoint &off)
|
||||
{
|
||||
int size_y = size.y;
|
||||
if (m_file_sys->GetGroupMode() == PrinterFileSystem::G_NONE)
|
||||
if (m_model->GetGroupMode() == FileGridGroup::All)
|
||||
size_y -= m_title_mask.GetHeight();
|
||||
int offx = (size.x - (m_col_count - 1) * m_cell_size.GetWidth() - m_border_size.GetWidth()) / 2;
|
||||
int offy = (m_row_offset + 1 < m_row_count || m_row_count == 0) ?
|
||||
@@ -439,8 +447,8 @@ size_t Slic3r::GUI::ImageGrid::firstItem(wxSize const &size, wxPoint &off)
|
||||
size_y - (size_y + m_border_size.GetHeight() - 1) / m_cell_size.GetHeight() * m_cell_size.GetHeight();
|
||||
int index = (m_row_offset + 1 < m_row_count || m_row_count == 0) ?
|
||||
m_row_offset / 4 * m_col_count :
|
||||
((m_file_sys->GetCount() + m_col_count - 1) / m_col_count - (size_y + m_border_size.GetHeight() - 1) / m_cell_size.GetHeight()) * m_col_count;
|
||||
if (m_file_sys->GetGroupMode() == PrinterFileSystem::G_NONE)
|
||||
((m_model->GetCount() + m_col_count - 1) / m_col_count - (size_y + m_border_size.GetHeight() - 1) / m_cell_size.GetHeight()) * m_col_count;
|
||||
if (m_model->GetGroupMode() == FileGridGroup::All)
|
||||
offy += m_title_mask.GetHeight();
|
||||
off = wxPoint{offx, offy};
|
||||
return index;
|
||||
@@ -534,7 +542,7 @@ void ImageGrid::render(wxDC& dc)
|
||||
wxSize size = GetClientSize();
|
||||
dc.SetPen(wxPen(GetBackgroundColour()));
|
||||
dc.SetBrush(wxBrush(GetBackgroundColour()));
|
||||
if (!m_file_sys || m_file_sys->GetCount() == 0) {
|
||||
if (!m_model || m_model->GetCount() == 0) {
|
||||
dc.DrawRectangle({ 0, 0, size.x, size.y });
|
||||
if (!m_status_msg.IsEmpty()) {
|
||||
auto si = m_status_icon.GetBmpSize();
|
||||
@@ -562,7 +570,7 @@ void ImageGrid::render(wxDC& dc)
|
||||
{
|
||||
// Draw one line
|
||||
wxPoint pt{off.x, off.y};
|
||||
end = (index + m_col_count) < m_file_sys->GetCount() ? index + m_col_count : m_file_sys->GetCount();
|
||||
end = (index + m_col_count) < m_model->GetCount() ? index + m_col_count : m_model->GetCount();
|
||||
while (index < end) {
|
||||
pt += m_content_rect.GetTopLeft();
|
||||
// Draw content
|
||||
@@ -571,7 +579,7 @@ void ImageGrid::render(wxDC& dc)
|
||||
&ImageGrid::renderContent1,
|
||||
&ImageGrid::renderContent2
|
||||
};
|
||||
(this->*contentRender[m_file_sys->GetFileType()])(dc, pt, index, hit_image == index);
|
||||
(this->*contentRender[static_cast<int>(m_model->GetFileType())])(dc, pt, index, hit_image == index);
|
||||
pt -= m_content_rect.GetTopLeft();
|
||||
// Draw colume spacing at right
|
||||
dc.DrawRectangle({pt.x + m_border_size.GetWidth(), pt.y, m_cell_size.GetWidth() - m_border_size.GetWidth(), m_border_size.GetHeight()});
|
||||
@@ -588,13 +596,13 @@ void ImageGrid::render(wxDC& dc)
|
||||
off.y += m_cell_size.GetHeight();
|
||||
}
|
||||
// Draw floating date range for non-group list
|
||||
if (m_file_sys->GetGroupMode() == PrinterFileSystem::G_NONE && m_file_sys->GetCount() > 0) {
|
||||
if (m_model->GetGroupMode() == FileGridGroup::All && m_model->GetCount() > 0) {
|
||||
//dc.DrawBitmap(m_title_mask, {off.x, 0});
|
||||
dc.DrawRectangle({off.x, 0}, m_title_mask.GetSize());
|
||||
auto & file1 = m_file_sys->GetFile(start);
|
||||
auto & file2 = m_file_sys->GetFile(end - 1);
|
||||
auto date1 = wxDateTime((time_t) file1.time).Format(_L(TIME_FORMATS[m_file_sys->GetGroupMode()]));
|
||||
auto date2 = wxDateTime((time_t) file2.time).Format(_L(TIME_FORMATS[m_file_sys->GetGroupMode()]));
|
||||
const FileGridCard & file1 = m_model->GetFile(start);
|
||||
const FileGridCard & file2 = m_model->GetFile(end - 1);
|
||||
auto date1 = wxDateTime((time_t) file1.time).Format(_L(TIME_FORMATS[static_cast<int>(m_model->GetGroupMode())]));
|
||||
auto date2 = wxDateTime((time_t) file2.time).Format(_L(TIME_FORMATS[static_cast<int>(m_model->GetGroupMode())]));
|
||||
dc.SetFont(Label::Head_16);
|
||||
dc.SetTextForeground(StateColor::darkModeColorFor("#262E30"));
|
||||
dc.DrawText(date1 + " - " + date2, wxPoint{off.x, 2});
|
||||
@@ -604,7 +612,7 @@ void ImageGrid::render(wxDC& dc)
|
||||
dc.DrawRectangle({off.x, off.y, size.x - off.x * 2, size.y - off.y});
|
||||
// Draw position bar
|
||||
if (m_timer.IsRunning()) {
|
||||
int total_height = (m_file_sys->GetCount() + m_col_count - 1) / m_col_count * m_cell_size.GetHeight() + m_cell_size.GetHeight() - m_border_size.GetHeight();
|
||||
int total_height = (m_model->GetCount() + m_col_count - 1) / m_col_count * m_cell_size.GetHeight() + m_cell_size.GetHeight() - m_border_size.GetHeight();
|
||||
if (total_height > size.y) {
|
||||
int offset = (m_row_offset + 1 < m_row_count || m_row_count == 0) ? m_row_offset * (m_cell_size.GetHeight() / 4) : total_height - size.y;
|
||||
wxRect rect = {size.x - 16, offset * size.y / total_height, 8,
|
||||
@@ -618,15 +626,35 @@ void ImageGrid::render(wxDC& dc)
|
||||
void Slic3r::GUI::ImageGrid::renderContent1(wxDC &dc, wxPoint const &pt, int index, bool hit)
|
||||
{
|
||||
bool selected = false;
|
||||
auto &file = m_file_sys->GetFile(index, selected);
|
||||
const FileGridCard &card = m_model->GetFile(index, selected);
|
||||
// Draw thumbnail
|
||||
if (file.thumbnail.IsOk()) {
|
||||
float hs = (float) m_content_rect.GetWidth() / file.thumbnail.GetWidth();
|
||||
float vs = (float) m_content_rect.GetHeight() / file.thumbnail.GetHeight();
|
||||
dc.SetUserScale(hs, vs);
|
||||
dc.DrawBitmap(file.thumbnail, {(int) (pt.x / hs), (int) (pt.y / vs)});
|
||||
dc.SetUserScale(1, 1);
|
||||
if (m_file_sys->GetGroupMode() != PrinterFileSystem::G_NONE) { dc.DrawBitmap(m_title_mask, pt); }
|
||||
if (card.thumbnail.IsOk()) {
|
||||
if (m_model->preserve_thumbnail_aspect()) {
|
||||
// Contain: fit the whole thumbnail and letterbox against the tile background.
|
||||
float scale = std::min((float) m_content_rect.GetWidth() / card.thumbnail.GetWidth(),
|
||||
(float) m_content_rect.GetHeight() / card.thumbnail.GetHeight());
|
||||
int thumb_w = (int) (card.thumbnail.GetWidth() * scale);
|
||||
int thumb_h = (int) (card.thumbnail.GetHeight() * scale);
|
||||
int offx = (m_content_rect.GetWidth() - thumb_w) / 2;
|
||||
int offy = (m_content_rect.GetHeight() - thumb_h) / 2;
|
||||
auto brush = dc.GetBrush();
|
||||
auto pen = dc.GetPen();
|
||||
dc.SetBrush(StateColor::darkModeColorFor(0xEEEEEE));
|
||||
dc.SetPen(StateColor::darkModeColorFor(0xEEEEEE));
|
||||
dc.DrawRectangle(pt, m_content_rect.GetSize());
|
||||
dc.SetUserScale(scale, scale);
|
||||
dc.DrawBitmap(card.thumbnail, {(int) ((pt.x + offx) / scale), (int) ((pt.y + offy) / scale)});
|
||||
dc.SetUserScale(1, 1);
|
||||
dc.SetBrush(brush);
|
||||
dc.SetPen(pen);
|
||||
} else {
|
||||
float hs = (float) m_content_rect.GetWidth() / card.thumbnail.GetWidth();
|
||||
float vs = (float) m_content_rect.GetHeight() / card.thumbnail.GetHeight();
|
||||
dc.SetUserScale(hs, vs);
|
||||
dc.DrawBitmap(card.thumbnail, {(int) (pt.x / hs), (int) (pt.y / vs)});
|
||||
dc.SetUserScale(1, 1);
|
||||
}
|
||||
if (m_model->GetGroupMode() != FileGridGroup::All) { dc.DrawBitmap(m_title_mask, pt); }
|
||||
}
|
||||
bool show_download_state_always = true;
|
||||
// Draw checked icon
|
||||
@@ -634,14 +662,14 @@ void Slic3r::GUI::ImageGrid::renderContent1(wxDC &dc, wxPoint const &pt, int ind
|
||||
dc.DrawBitmap(selected ? m_checked_icon.bmp() : m_unchecked_icon.bmp(), pt + wxPoint{10, 10});
|
||||
// can't handle alpha
|
||||
// dc.GradientFillLinear({pt.x, pt.y, m_border_size.GetWidth(), 60}, wxColour(0x6F, 0x6F, 0x6F, 0x99), wxColour(0x6F, 0x6F, 0x6F, 0), wxBOTTOM);
|
||||
else if (m_file_sys->GetGroupMode() == PrinterFileSystem::G_NONE) {
|
||||
else if (m_model->GetGroupMode() == FileGridGroup::All) {
|
||||
wxString nonHoverText;
|
||||
wxString secondAction = m_show_download ? _L("Download") : "";
|
||||
wxString thirdAction;
|
||||
int states = 0;
|
||||
// Draw download progress
|
||||
if (file.IsDownload()) {
|
||||
int progress = file.DownloadProgress();
|
||||
if (card.downloading) {
|
||||
int progress = card.download_progress;
|
||||
if (progress == -1) {
|
||||
secondAction = _L("Cancel");
|
||||
nonHoverText = _L("Download waiting...");
|
||||
@@ -659,7 +687,7 @@ void Slic3r::GUI::ImageGrid::renderContent1(wxDC &dc, wxPoint const &pt, int ind
|
||||
thirdAction = wxString::Format(L"%d%%...", progress);
|
||||
}
|
||||
}
|
||||
if (m_file_sys->GetFileType() == PrinterFileSystem::F_MODEL) {
|
||||
if (m_model->GetFileType() == FileGridType::Model && card.printable) {
|
||||
if (secondAction != _L("Play"))
|
||||
thirdAction = secondAction;
|
||||
secondAction = _L_CONTEXT("Print", "Verb");
|
||||
@@ -669,7 +697,8 @@ void Slic3r::GUI::ImageGrid::renderContent1(wxDC &dc, wxPoint const &pt, int ind
|
||||
wxArrayString texts;
|
||||
if (hit) {
|
||||
texts.Add(_L("Delete"));
|
||||
texts.Add(secondAction);
|
||||
if (hasSecondAction(card))
|
||||
texts.Add(secondAction);
|
||||
if (!thirdAction.IsEmpty())
|
||||
texts.Add(thirdAction);
|
||||
renderButtons(dc, texts, rect, m_hit_type == HIT_ACTION ? m_hit_item & 3 : -1, states);
|
||||
@@ -679,7 +708,7 @@ void Slic3r::GUI::ImageGrid::renderContent1(wxDC &dc, wxPoint const &pt, int ind
|
||||
}
|
||||
} else {
|
||||
dc.SetTextForeground(*wxWHITE); // time text color
|
||||
auto date = wxDateTime((time_t) file.time).Format(_L(TIME_FORMATS[m_file_sys->GetGroupMode()]));
|
||||
auto date = wxDateTime((time_t) card.time).Format(_L(TIME_FORMATS[static_cast<int>(m_model->GetGroupMode())]));
|
||||
dc.DrawText(date, pt + wxPoint{24, 16});
|
||||
}
|
||||
if (m_selecting && show_download_state_always)
|
||||
@@ -688,7 +717,7 @@ void Slic3r::GUI::ImageGrid::renderContent1(wxDC &dc, wxPoint const &pt, int ind
|
||||
|
||||
void Slic3r::GUI::ImageGrid::renderContent2(wxDC &dc, wxPoint const &pt, int index, bool hit)
|
||||
{
|
||||
auto &file = m_file_sys->GetFile(index);
|
||||
const FileGridCard &card = m_model->GetFile(index);
|
||||
// Draw thumbnail & buttons
|
||||
int h = m_content_rect.GetHeight() * 64 / 264;
|
||||
m_content_rect.SetHeight(m_content_rect.GetHeight() - h);
|
||||
@@ -711,14 +740,14 @@ void Slic3r::GUI::ImageGrid::renderContent2(wxDC &dc, wxPoint const &pt, int ind
|
||||
auto em = em_unit(this);
|
||||
wxRect rect{pt.x, pt.y + m_content_rect.GetHeight() - h, m_content_rect.GetWidth(), h / 2};
|
||||
rect.Deflate(em, 0);
|
||||
renderText2(dc, from_u8(file.name), rect);
|
||||
renderText2(dc, from_u8(card.name), rect);
|
||||
rect.Offset(0, h / 2);
|
||||
rect.SetWidth(rect.GetWidth() / 2 - em);
|
||||
dc.SetFont(Label::Body_13);
|
||||
dc.SetTextForeground(StateColor::darkModeColorFor("#6B6B6B"));
|
||||
renderIconText(dc, m_model_time_icon, file.Metadata("Time", "0m"), rect);
|
||||
renderIconText(dc, m_model_time_icon, card.time_text.empty() ? wxString("0m") : from_u8(card.time_text), rect);
|
||||
rect.Offset(m_content_rect.GetWidth() / 2, 0);
|
||||
renderIconText(dc, m_model_weight_icon, file.Metadata("Weight", "0g"), rect);
|
||||
renderIconText(dc, m_model_weight_icon, card.weight_text.empty() ? wxString("0g") : from_u8(card.weight_text), rect);
|
||||
}
|
||||
|
||||
void Slic3r::GUI::ImageGrid::renderButtons(wxDC &dc, wxArrayString const &texts, wxRect const &rect2, size_t hit, int states)
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
|
||||
#include "Widgets/StateColor.hpp"
|
||||
#include "wxExtensions.hpp"
|
||||
#include "Printer/IFileGridModel.h"
|
||||
|
||||
class Button;
|
||||
class Label;
|
||||
@@ -44,6 +45,8 @@ public:
|
||||
|
||||
void SetFileSystem(boost::shared_ptr<PrinterFileSystem> file_sys);
|
||||
|
||||
void SetModel(boost::shared_ptr<IFileGridModel> model);
|
||||
|
||||
void SetStatus(ScalableBitmap const & icon, wxString const &msg);
|
||||
|
||||
boost::shared_ptr<PrinterFileSystem> GetFileSystem() { return m_file_sys; }
|
||||
@@ -76,9 +79,12 @@ protected:
|
||||
|
||||
std::pair<int, size_t> HitTest(wxPoint const &pt);
|
||||
|
||||
// True when the hover bar shows a second action next to Delete.
|
||||
bool hasSecondAction(const FileGridCard &card) const;
|
||||
|
||||
protected:
|
||||
|
||||
void changedEvent(wxCommandEvent& evt);
|
||||
void onModelChange(FileGridChange change);
|
||||
|
||||
void paintEvent(wxPaintEvent& evt);
|
||||
|
||||
@@ -117,6 +123,7 @@ protected:
|
||||
|
||||
private:
|
||||
boost::shared_ptr<PrinterFileSystem> m_file_sys;
|
||||
boost::shared_ptr<IFileGridModel> m_model;
|
||||
ScalableBitmap m_status_icon;
|
||||
wxString m_status_msg;
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
#include "libslic3r/TextureDisplacement.hpp"
|
||||
#include <exception>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include "slic3r/GUI/Jobs/Worker.hpp"
|
||||
#include <memory>
|
||||
|
||||
@@ -27,7 +26,6 @@
|
||||
#include "slic3r/Utils/UndoRedo.hpp"
|
||||
#include "libslic3r/ObjectID.hpp"
|
||||
#include "libslic3r/TriangleMesh.hpp"
|
||||
#include <array>
|
||||
|
||||
namespace Slic3r::GUI {
|
||||
|
||||
@@ -128,71 +126,16 @@ void TextureDisplacementBakeJob::finalize(bool canceled, std::exception_ptr &ept
|
||||
{ mix.a_percent(), 100 - mix.a_percent() });
|
||||
});
|
||||
|
||||
// Whether the bake replaced the triangles or only moved the vertices it was given. The classic
|
||||
// path is topology-preserving by contract, so every per-triangle annotation on the volume still
|
||||
// means what it did; the default pipeline refines, cuts and simplifies, so none of them do.
|
||||
// Both are checked: the flag is what the bake promised, the count is what it delivered. Only
|
||||
// the triangles matter - the annotations are per facet and reference no vertex index - so a
|
||||
// vertex count that changed on its own (the bake compacts unreferenced vertices away) is not a
|
||||
// reason to remap anything.
|
||||
const bool topology_changed = m_input.options.pipeline_v2 ||
|
||||
m_result.its.indices.size() != m_input.base_mesh.indices.size();
|
||||
|
||||
// Carry the volume's existing paint - the model's own colours above all - across that rebuild.
|
||||
// Without it a bake left the mmu_segmentation_facets bitstream pointing at a triangle set that
|
||||
// no longer existed, which is what scrambled the colours of an imported coloured model even
|
||||
// with colour switched off on every layer and nothing in the bake that was meant to touch them.
|
||||
//
|
||||
// transfer_facet_paint() rather than ModelVolume::restore_painting(): the latter's remap wants
|
||||
// a target triangle overlapping the source paint to within 0.01 mm, which the baked surface is
|
||||
// nowhere near - it has moved by the relief depth. Nothing to do when the topology is intact:
|
||||
// triangle i is still triangle i, so the paint is already exactly right and carrying it at all
|
||||
// could only lose detail.
|
||||
std::array<TriangleSelector::TriangleSplittingData, 4> before;
|
||||
if (topology_changed) {
|
||||
before[0] = volume->supported_facets.get_data();
|
||||
before[1] = volume->seam_facets.get_data();
|
||||
before[2] = volume->mmu_segmentation_facets.get_data();
|
||||
before[3] = volume->fuzzy_skin_facets.get_data();
|
||||
}
|
||||
|
||||
volume->set_mesh(std::move(m_result));
|
||||
volume->set_new_unique_id();
|
||||
volume->calculate_convex_hull();
|
||||
|
||||
if (topology_changed) {
|
||||
const std::array<FacetsAnnotation *, 4> target{ &volume->supported_facets, &volume->seam_facets,
|
||||
&volume->mmu_segmentation_facets,
|
||||
&volume->fuzzy_skin_facets };
|
||||
for (size_t ch = 0; ch < target.size(); ++ch) {
|
||||
const std::vector<uint8_t> state =
|
||||
transfer_facet_paint(m_input.base_mesh, before[ch], volume->mesh().its);
|
||||
// Nothing was painted in this channel, or there is nothing to paint it onto: either
|
||||
// way the annotation must not keep describing the mesh that is gone.
|
||||
if (state.empty()) {
|
||||
target[ch]->reset();
|
||||
continue;
|
||||
}
|
||||
TriangleSelector selector(volume->mesh());
|
||||
bool any = false;
|
||||
for (size_t i = 0; i < state.size(); ++i)
|
||||
if (state[i] != 0) {
|
||||
selector.set_facet(int(i), EnforcerBlockerType(state[i]));
|
||||
any = true;
|
||||
}
|
||||
if (any)
|
||||
target[ch]->set(selector);
|
||||
else
|
||||
target[ch]->reset();
|
||||
}
|
||||
}
|
||||
|
||||
// Colour lands in mmu_segmentation_facets, merged *over* whatever is already painted there
|
||||
// rather than replacing it: a triangle the texture does not colour keeps its existing filament,
|
||||
// and one the user never painted at all stays at NONE, which already means "the volume's own
|
||||
// filament". That is what confines the effect to the painted area without having to invent a
|
||||
// colour for everything outside it. What was already painted is whatever the transfer above
|
||||
// left in the annotation, so this bake's colour lands on top of it rather than instead of it.
|
||||
// colour for everything outside it. Safe to index straight onto the new mesh - the bake is
|
||||
// topology-preserving, so triangle i is still triangle i.
|
||||
if (!m_triangle_color.empty() && m_triangle_color.size() == volume->mesh().its.indices.size()) {
|
||||
TriangleSelector selector(volume->mesh());
|
||||
const TriangleSelector::TriangleSplittingData &existing = volume->mmu_segmentation_facets.get_data();
|
||||
@@ -216,6 +159,7 @@ void TextureDisplacementBakeJob::finalize(bool canceled, std::exception_ptr &ept
|
||||
// pipeline, which rebuilds and then simplifies the mesh. A mask left behind against the old
|
||||
// topology is exactly what makes the gizmo reload an empty selector over a non-empty mask and
|
||||
// then erase it on the next flush - see GLGizmoTextureDisplacement::update_from_model_object().
|
||||
const bool topology_changed = m_input.base_mesh.indices.size() != volume->mesh().its.indices.size();
|
||||
for (int slot = 0; slot < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++slot) {
|
||||
const auto it = std::find_if(m_input.layers.begin(), m_input.layers.end(),
|
||||
[slot](const TextureDisplacementLayer &l) { return l.slot == slot; });
|
||||
|
||||
@@ -221,26 +221,40 @@ MediaFilePanel::MediaFilePanel(wxWindow * parent)
|
||||
SetSelecting(false);
|
||||
});
|
||||
|
||||
auto onShowHide = [this](auto &e) {
|
||||
e.Skip();
|
||||
if (auto w = dynamic_cast<wxWindow *>(e.GetEventObject()); !w || w->IsBeingDeleted())
|
||||
return;
|
||||
CallAfter([this] {
|
||||
auto fs = m_image_grid ? m_image_grid->GetFileSystem() : nullptr;
|
||||
if (fs) IsShownOnScreen() ? fs->Start() : fs->Stop();
|
||||
});
|
||||
};
|
||||
Bind(wxEVT_SHOW, onShowHide);
|
||||
parent->GetParent()->Bind(wxEVT_SHOW, onShowHide);
|
||||
Bind(wxEVT_SHOW, &MediaFilePanel::on_show_hide, this);
|
||||
// A Device sub-tab switch hides the page container (StoragePanel); a main tab
|
||||
// switch hides MonitorPanel inside LazyPage. Neither event reaches the other,
|
||||
// so watch every ancestor below the top-level frame.
|
||||
for (wxWindow* w = GetParent(); w && !w->IsTopLevel(); w = w->GetParent()) {
|
||||
w->Bind(wxEVT_SHOW, &MediaFilePanel::on_show_hide, this);
|
||||
m_show_hosts.push_back(w);
|
||||
}
|
||||
|
||||
m_lan_user = "bblp";
|
||||
}
|
||||
|
||||
MediaFilePanel::~MediaFilePanel()
|
||||
{
|
||||
Unbind(wxEVT_SHOW, &MediaFilePanel::on_show_hide, this);
|
||||
for (wxWindow* w : m_show_hosts)
|
||||
if (w && !w->IsBeingDeleted())
|
||||
w->Unbind(wxEVT_SHOW, &MediaFilePanel::on_show_hide, this);
|
||||
m_show_hosts.clear();
|
||||
|
||||
UpdateByObj(nullptr);
|
||||
}
|
||||
|
||||
void MediaFilePanel::on_show_hide(wxShowEvent& e)
|
||||
{
|
||||
e.Skip();
|
||||
if (auto w = dynamic_cast<wxWindow*>(e.GetEventObject()); !w || w->IsBeingDeleted())
|
||||
return;
|
||||
CallAfter([this] {
|
||||
auto fs = m_image_grid ? m_image_grid->GetFileSystem() : nullptr;
|
||||
if (fs) IsShownOnScreen() ? fs->Start() : fs->Stop();
|
||||
});
|
||||
}
|
||||
|
||||
void MediaFilePanel::UpdateByObj(MachineObject* obj)
|
||||
{
|
||||
bool sdcard_state_changed = false;
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include <boost/smart_ptr/shared_ptr.hpp>
|
||||
#include <cstddef>
|
||||
#include <set>
|
||||
#include <vector>
|
||||
|
||||
#include "GUI_Utils.hpp"
|
||||
#include "wxExtensions.hpp"
|
||||
@@ -53,6 +54,8 @@ public:
|
||||
private:
|
||||
void modeChanged(wxCommandEvent & e);
|
||||
|
||||
void on_show_hide(wxShowEvent & e);
|
||||
|
||||
void fetchUrl(boost::weak_ptr<PrinterFileSystem> fs);
|
||||
|
||||
void doAction(size_t index, int action);
|
||||
@@ -99,6 +102,8 @@ private:
|
||||
int m_last_mode = 0;
|
||||
int m_last_type = 0;
|
||||
std::set<int> m_last_errors;
|
||||
|
||||
std::vector<wxWindow*> m_show_hosts;
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
#include "Plater.hpp"
|
||||
#include "MainFrame.hpp"
|
||||
#include "MediaPlayCtrl.h"
|
||||
#include "MediaFilePanel.h"
|
||||
#include "StoragePanel.h"
|
||||
#include "BindDialog.hpp"
|
||||
|
||||
#include "DeviceCore/DevManager.h"
|
||||
@@ -201,9 +201,9 @@ void MonitorPanel::init_tabpanel()
|
||||
m_tabpanel->SetBackgroundColour(wxColour("#FEFFFF"));
|
||||
m_tabpanel->Bind(wxEVT_BOOKCTRL_PAGE_CHANGED, [this](wxBookCtrlEvent& e) {
|
||||
auto page = m_tabpanel->GetCurrentPage();
|
||||
if (page == m_media_file_panel) {
|
||||
if (page == m_storage_panel) {
|
||||
auto title = m_tabpanel->GetPageText(m_tabpanel->GetSelection());
|
||||
m_media_file_panel->SwitchStorage(title == _L("Storage"));
|
||||
m_storage_panel->SwitchStorage(title == _L("Storage"));
|
||||
}
|
||||
// The first page is selected while the panel is built off screen.
|
||||
if (page->IsShownOnScreen())
|
||||
@@ -217,8 +217,8 @@ void MonitorPanel::init_tabpanel()
|
||||
add_build_steps_of(*m_status_info_panel);
|
||||
m_tabpanel->AddPage(m_status_info_panel, _L("Status"), true);
|
||||
add_build_step([this] {
|
||||
m_media_file_panel = new MediaFilePanel(m_tabpanel);
|
||||
m_tabpanel->AddPage(m_media_file_panel, _L("Storage"), false);
|
||||
m_storage_panel = new StoragePanel(m_tabpanel);
|
||||
m_tabpanel->AddPage(m_storage_panel, _L("Storage"), false);
|
||||
});
|
||||
add_build_step([this] {
|
||||
m_upgrade_panel = new UpgradePanel(m_tabpanel);
|
||||
@@ -267,7 +267,8 @@ void MonitorPanel::on_sys_color_changed()
|
||||
{
|
||||
m_status_info_panel->on_sys_color_changed();
|
||||
m_upgrade_panel->on_sys_color_changed();
|
||||
m_media_file_panel->Rescale();
|
||||
m_storage_panel->on_sys_color_changed();
|
||||
m_storage_panel->Rescale();
|
||||
}
|
||||
|
||||
void MonitorPanel::msw_rescale()
|
||||
@@ -279,7 +280,7 @@ void MonitorPanel::msw_rescale()
|
||||
m_tabpanel->Rescale();
|
||||
//m_status_add_machine_panel->msw_rescale();
|
||||
m_status_info_panel->msw_rescale();
|
||||
m_media_file_panel->Rescale();
|
||||
m_storage_panel->Rescale();
|
||||
m_upgrade_panel->msw_rescale();
|
||||
m_hms_panel->msw_rescale();
|
||||
|
||||
@@ -426,8 +427,8 @@ void MonitorPanel::update_all()
|
||||
}
|
||||
} else if (current_page == m_upgrade_panel) {
|
||||
m_upgrade_panel->update(obj);
|
||||
} else if (current_page == m_media_file_panel) {
|
||||
m_media_file_panel->UpdateByObj(obj);
|
||||
} else if (current_page == m_storage_panel) {
|
||||
m_storage_panel->UpdateByObj(obj);
|
||||
}
|
||||
|
||||
if (current_page == m_hms_panel || (obj->GetHMS()->GetHMSItems().size() != m_hms_panel->temp_hms_list.size())) {
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
namespace Slic3r {
|
||||
namespace GUI {
|
||||
|
||||
class MediaFilePanel;
|
||||
class StoragePanel;
|
||||
|
||||
class AddMachinePanel : public wxPanel
|
||||
{
|
||||
@@ -83,7 +83,7 @@ private:
|
||||
wxSizer* m_main_sizer{ nullptr };
|
||||
|
||||
StatusPanel* m_status_info_panel{ nullptr };
|
||||
MediaFilePanel* m_media_file_panel{ nullptr };
|
||||
StoragePanel* m_storage_panel{ nullptr };
|
||||
UpgradePanel* m_upgrade_panel{ nullptr };
|
||||
HMSPanel* m_hms_panel{ nullptr };
|
||||
|
||||
|
||||
@@ -0,0 +1,275 @@
|
||||
#include "OrcaFilesPanel.h"
|
||||
|
||||
#include "DeviceManager.hpp"
|
||||
#include "GUI_App.hpp"
|
||||
#include "GUI.hpp"
|
||||
#include "I18N.hpp"
|
||||
#include "ImageGrid.h"
|
||||
#include "MsgDialog.hpp"
|
||||
#include "Widgets/Button.hpp"
|
||||
#include "Widgets/Label.hpp"
|
||||
#include "Widgets/StaticBox.hpp"
|
||||
#include "Widgets/StateColor.hpp"
|
||||
#include "slic3r/Utils/NetworkAgent.hpp"
|
||||
#include "slic3r/Utils/bambu_networking.hpp"
|
||||
|
||||
#include <boost/make_shared.hpp>
|
||||
#include <slic3r/GUI/wxExtensions.hpp>
|
||||
#include <slic3r/GUI/Printer/IFileGridModel.h>
|
||||
#include <cstddef>
|
||||
#include <boost/smart_ptr/make_shared_object.hpp>
|
||||
#include <wx/colour.h>
|
||||
#include <wx/panel.h>
|
||||
#include <wx/event.h>
|
||||
#include <wx/gdicmn.h>
|
||||
#include <wx/sizer.h>
|
||||
#include <utility>
|
||||
|
||||
namespace Slic3r {
|
||||
namespace GUI {
|
||||
|
||||
OrcaFilesPanel::OrcaFilesPanel(wxWindow* parent)
|
||||
: wxPanel(parent, wxID_ANY)
|
||||
, m_bmp_loading(this, "media_loading", 0)
|
||||
, m_bmp_failed(this, "media_failed", 0)
|
||||
, m_bmp_empty(this, "media_empty", 0)
|
||||
{
|
||||
const wxColour light_background("#EEEEEE");
|
||||
SetBackgroundColour(wxGetApp().dark_mode() ? StateColor::darkModeColorFor(light_background) : light_background);
|
||||
|
||||
auto* sizer = new wxBoxSizer(wxVERTICAL);
|
||||
auto* toolbar = new wxBoxSizer(wxHORIZONTAL);
|
||||
toolbar->SetMinSize({-1, 75 * em_unit(this) / 10});
|
||||
|
||||
m_root_label = new Label(this, _L("gcodes"));
|
||||
m_root_label->SetFont(Label::Head_16);
|
||||
toolbar->Add(m_root_label, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, 24);
|
||||
|
||||
m_time_panel = new ::StaticBox(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxBORDER_NONE);
|
||||
m_time_panel->SetCornerRadius(0);
|
||||
m_button_year = new ::Button(m_time_panel, _L("Year"), "", wxBORDER_NONE);
|
||||
m_button_month = new ::Button(m_time_panel, _L("Month"), "", wxBORDER_NONE);
|
||||
m_button_all = new ::Button(m_time_panel, _L("All Files"), "", wxBORDER_NONE);
|
||||
m_button_year->SetToolTip(_L("Group files by year, recent first."));
|
||||
m_button_month->SetToolTip(_L("Group files by month, recent first."));
|
||||
m_button_all->SetToolTip(_L("Show all files, recent first."));
|
||||
for (auto* button : {m_button_year, m_button_month, m_button_all}) {
|
||||
button->SetBackgroundColor(StateColor(
|
||||
std::make_pair(0xEEEEEE, static_cast<int>(StateColor::Checked)),
|
||||
std::make_pair(*wxLIGHT_GREY, static_cast<int>(StateColor::Hovered)),
|
||||
std::make_pair(*wxWHITE, static_cast<int>(StateColor::Normal))));
|
||||
button->SetTextColor(StateColor(
|
||||
std::make_pair(0x3B4446, static_cast<int>(StateColor::Checked)),
|
||||
std::make_pair(*wxLIGHT_GREY, static_cast<int>(StateColor::Hovered)),
|
||||
std::make_pair(0xABACAC, static_cast<int>(StateColor::Normal))));
|
||||
button->SetCanFocus(false);
|
||||
}
|
||||
auto* time_sizer = new wxBoxSizer(wxHORIZONTAL);
|
||||
time_sizer->Add(m_button_year, 0, wxALIGN_CENTER_VERTICAL | wxLEFT | wxRIGHT, 24);
|
||||
time_sizer->Add(m_button_month, 0, wxALIGN_CENTER_VERTICAL);
|
||||
time_sizer->Add(m_button_all, 0, wxALIGN_CENTER_VERTICAL | wxLEFT | wxRIGHT, 24);
|
||||
m_time_panel->SetSizer(time_sizer);
|
||||
toolbar->AddStretchSpacer(1);
|
||||
toolbar->Add(m_time_panel, 0, wxALIGN_CENTER_VERTICAL);
|
||||
toolbar->AddStretchSpacer(1);
|
||||
|
||||
m_button_refresh = new ::Button(this, _L("Refresh"));
|
||||
m_button_refresh->SetToolTip(_L("Reload file list from printer."));
|
||||
m_button_refresh->SetFont(Label::Body_12);
|
||||
m_button_refresh->SetCornerRadius(12);
|
||||
m_button_refresh->SetPaddingSize({10, 6});
|
||||
m_button_refresh->SetCanFocus(false);
|
||||
m_button_refresh->SetBorderWidth(0);
|
||||
toolbar->Add(m_button_refresh, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, 24);
|
||||
sizer->Add(toolbar, 0, wxEXPAND);
|
||||
|
||||
m_image_grid = new ImageGrid(this);
|
||||
// Theme this grid instance without changing the shared Bambu defaults.
|
||||
m_image_grid->SetBackgroundColour(GetBackgroundColour());
|
||||
m_image_grid->SetSelecting(false);
|
||||
m_image_grid->ShowDownload(false);
|
||||
m_image_grid->SetStatus(m_bmp_failed, _L("Please confirm if the printer is connected."));
|
||||
sizer->Add(m_image_grid, 1, wxEXPAND);
|
||||
SetSizer(sizer);
|
||||
applyThemeColors();
|
||||
updateGroupButtons(m_group_mode);
|
||||
|
||||
auto group_clicked = [this](wxCommandEvent& event) {
|
||||
const FileGridGroup mode = event.GetEventObject() == m_button_year ? FileGridGroup::Year :
|
||||
event.GetEventObject() == m_button_month ? FileGridGroup::Month : FileGridGroup::All;
|
||||
m_image_grid->SetGroupMode(static_cast<int>(mode));
|
||||
updateGroupButtons(mode);
|
||||
};
|
||||
m_button_year->Bind(wxEVT_COMMAND_BUTTON_CLICKED, group_clicked);
|
||||
m_button_month->Bind(wxEVT_COMMAND_BUTTON_CLICKED, group_clicked);
|
||||
m_button_all->Bind(wxEVT_COMMAND_BUTTON_CLICKED, group_clicked);
|
||||
|
||||
m_button_refresh->Bind(wxEVT_COMMAND_BUTTON_CLICKED, [this](wxCommandEvent& event) {
|
||||
event.Skip();
|
||||
if (m_model)
|
||||
m_model->Refresh();
|
||||
});
|
||||
|
||||
m_image_grid->Bind(EVT_ITEM_ACTION, [this](wxCommandEvent& event) {
|
||||
const int action = event.GetInt();
|
||||
if (!m_model || m_device_id.empty())
|
||||
return;
|
||||
|
||||
const size_t index = static_cast<size_t>(event.GetExtraLong());
|
||||
if (index >= m_model->GetCount())
|
||||
return;
|
||||
const FileGridCard& card = m_model->GetFile(index);
|
||||
if (card.id.empty())
|
||||
return;
|
||||
const std::string path = card.id;
|
||||
const std::string name = card.name;
|
||||
|
||||
if (action == 0) {
|
||||
MessageDialog dlg(this,
|
||||
wxString::Format(_L("Do you want to delete the file '%s' from printer?"), from_u8(name)),
|
||||
_L("Delete file"), wxYES_NO | wxICON_WARNING);
|
||||
if (dlg.ShowModal() != wxID_YES)
|
||||
return;
|
||||
|
||||
m_model->DeleteFile(path, [this](bool ok) {
|
||||
if (ok)
|
||||
return;
|
||||
MessageDialog(this, _L("Failed to delete the file from printer."), _L("Delete file"),
|
||||
wxOK | wxICON_ERROR).ShowModal();
|
||||
});
|
||||
} else if (action == 1) {
|
||||
// OrcaSonar unpacks a .gcode.3mf to its plate G-code before starting the print.
|
||||
MessageDialog dlg(this,
|
||||
wxString::Format(_L("Do you want to print the file '%s' from printer?"), from_u8(name)),
|
||||
_L("Print file"), wxYES_NO | wxICON_WARNING);
|
||||
if (dlg.ShowModal() != wxID_YES)
|
||||
return;
|
||||
|
||||
NetworkAgent* agent = wxGetApp().getAgent();
|
||||
if (!agent)
|
||||
return;
|
||||
|
||||
PrintParams params;
|
||||
params.dev_id = m_device_id;
|
||||
params.dst_file = path;
|
||||
params.connection_type = "lan";
|
||||
if (agent->start_sdcard_print(params, {}, {}) != BAMBU_NETWORK_SUCCESS)
|
||||
MessageDialog(this, _L("Failed to start the print job."), _L("Print file"),
|
||||
wxOK | wxICON_ERROR).ShowModal();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
OrcaFilesPanel::~OrcaFilesPanel()
|
||||
{
|
||||
if (m_model)
|
||||
m_model->SetStatusHandler({});
|
||||
if (m_model)
|
||||
m_model->SetGroupModeHandler({});
|
||||
m_image_grid->SetModel(nullptr);
|
||||
m_model.reset();
|
||||
}
|
||||
|
||||
void OrcaFilesPanel::UpdateByObj(MachineObject* obj)
|
||||
{
|
||||
const std::string device_id = obj ? obj->get_dev_id() : std::string();
|
||||
if (device_id == m_device_id)
|
||||
return;
|
||||
|
||||
m_device_id = device_id;
|
||||
if (m_model)
|
||||
m_model->SetStatusHandler({});
|
||||
if (m_model)
|
||||
m_model->SetGroupModeHandler({});
|
||||
m_image_grid->SetModel(nullptr);
|
||||
m_model.reset();
|
||||
|
||||
if (m_device_id.empty()) {
|
||||
m_button_refresh->Enable(false);
|
||||
m_image_grid->SetStatus(m_bmp_failed, _L("Please confirm if the printer is connected."));
|
||||
return;
|
||||
}
|
||||
|
||||
m_model = boost::make_shared<RemoteFileGridModel>(m_device_id);
|
||||
m_model->SetStatusHandler([this](RemoteFileGridModel::Status status) { updateStatus(status); });
|
||||
m_model->SetGroupModeHandler([this](FileGridGroup mode) { updateGroupButtons(mode); });
|
||||
m_model->SetGroupMode(m_group_mode);
|
||||
m_image_grid->SetModel(m_model);
|
||||
m_image_grid->SetFileType(static_cast<int>(FileGridType::Model), {});
|
||||
m_button_refresh->Enable(true);
|
||||
m_model->Refresh();
|
||||
}
|
||||
|
||||
void OrcaFilesPanel::updateStatus(RemoteFileGridModel::Status status)
|
||||
{
|
||||
switch (status) {
|
||||
case RemoteFileGridModel::Status::Loading:
|
||||
m_image_grid->SetStatus(m_bmp_loading, _L("Loading file list..."));
|
||||
break;
|
||||
case RemoteFileGridModel::Status::Ready:
|
||||
break;
|
||||
case RemoteFileGridModel::Status::Empty:
|
||||
m_image_grid->SetStatus(m_bmp_empty, _L("No files"));
|
||||
break;
|
||||
case RemoteFileGridModel::Status::Failed:
|
||||
m_image_grid->SetStatus(m_bmp_failed, _L("Load failed"));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void OrcaFilesPanel::Rescale()
|
||||
{
|
||||
m_bmp_loading.msw_rescale();
|
||||
m_bmp_failed.msw_rescale();
|
||||
m_bmp_empty.msw_rescale();
|
||||
|
||||
auto* toolbar = GetSizer()->GetItem(static_cast<size_t>(0))->GetSizer();
|
||||
toolbar->SetMinSize({-1, 75 * em_unit(this) / 10});
|
||||
m_root_label->SetFont(Label::Head_16);
|
||||
m_button_refresh->Rescale();
|
||||
m_button_year->Rescale();
|
||||
m_button_month->Rescale();
|
||||
m_button_all->Rescale();
|
||||
m_image_grid->Rescale();
|
||||
}
|
||||
|
||||
void OrcaFilesPanel::on_sys_color_changed()
|
||||
{
|
||||
applyThemeColors();
|
||||
Layout();
|
||||
Refresh();
|
||||
}
|
||||
|
||||
void OrcaFilesPanel::applyThemeColors()
|
||||
{
|
||||
const bool dark = wxGetApp().dark_mode();
|
||||
const wxColour light_background("#EEEEEE");
|
||||
const wxColour light_text("#262E30");
|
||||
const wxColour light_teal("#009688");
|
||||
const wxColour background = dark ? StateColor::darkModeColorFor(light_background) : light_background;
|
||||
|
||||
SetBackgroundColour(background);
|
||||
m_root_label->SetBackgroundColour(background);
|
||||
m_root_label->SetForegroundColour(dark ? StateColor::darkModeColorFor(light_text) : light_text);
|
||||
m_time_panel->SetBackgroundColour(background);
|
||||
m_button_refresh->SetBackgroundColorNormal(dark ? StateColor::darkModeColorFor(light_teal) : light_teal);
|
||||
m_button_refresh->SetTextColorNormal(*wxWHITE);
|
||||
m_image_grid->SetBackgroundColour(background);
|
||||
m_image_grid->Refresh();
|
||||
m_button_year->Refresh();
|
||||
m_button_month->Refresh();
|
||||
m_button_all->Refresh();
|
||||
}
|
||||
|
||||
void OrcaFilesPanel::updateGroupButtons(FileGridGroup mode)
|
||||
{
|
||||
m_group_mode = mode;
|
||||
::Button* buttons[] = {m_button_all, m_button_month, m_button_year};
|
||||
const int selected = static_cast<int>(mode);
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
buttons[i]->SetValue(i == selected);
|
||||
buttons[i]->SetFont(i == selected ? Label::Head_14 : Label::Body_14);
|
||||
}
|
||||
}
|
||||
|
||||
}}
|
||||
@@ -0,0 +1,53 @@
|
||||
#pragma once
|
||||
|
||||
#include "GUI_Utils.hpp"
|
||||
#include "wxExtensions.hpp"
|
||||
#include "Printer/RemoteFileGridModel.h"
|
||||
|
||||
#include <boost/smart_ptr/shared_ptr.hpp>
|
||||
#include <slic3r/GUI/Printer/IFileGridModel.h>
|
||||
#include <string>
|
||||
#include <wx/panel.h>
|
||||
|
||||
class Button;
|
||||
class Label;
|
||||
class StaticBox;
|
||||
|
||||
namespace Slic3r {
|
||||
class MachineObject;
|
||||
|
||||
namespace GUI {
|
||||
|
||||
class ImageGrid;
|
||||
|
||||
class OrcaFilesPanel : public wxPanel
|
||||
{
|
||||
public:
|
||||
explicit OrcaFilesPanel(wxWindow* parent);
|
||||
~OrcaFilesPanel() override;
|
||||
|
||||
void UpdateByObj(MachineObject* obj);
|
||||
void Rescale();
|
||||
void on_sys_color_changed();
|
||||
|
||||
private:
|
||||
void applyThemeColors();
|
||||
void updateGroupButtons(FileGridGroup mode);
|
||||
void updateStatus(RemoteFileGridModel::Status status);
|
||||
|
||||
ScalableBitmap m_bmp_loading;
|
||||
ScalableBitmap m_bmp_failed;
|
||||
ScalableBitmap m_bmp_empty;
|
||||
Label* m_root_label = nullptr;
|
||||
::StaticBox* m_time_panel = nullptr;
|
||||
::Button* m_button_year = nullptr;
|
||||
::Button* m_button_month = nullptr;
|
||||
::Button* m_button_all = nullptr;
|
||||
::Button* m_button_refresh = nullptr;
|
||||
ImageGrid* m_image_grid = nullptr;
|
||||
boost::shared_ptr<RemoteFileGridModel> m_model;
|
||||
std::string m_device_id;
|
||||
FileGridGroup m_group_mode = FileGridGroup::All;
|
||||
};
|
||||
|
||||
}}
|
||||
@@ -6467,9 +6467,10 @@ void Sidebar::sync_ams_list(bool is_from_big_sync_btn)
|
||||
for (const auto &entry : wxGetApp().preset_bundle->filament_ams_list) {
|
||||
const auto &tray = entry.second;
|
||||
const bool has_filament = !tray.opt_string("filament_id", 0u).empty();
|
||||
const bool has_type = !tray.opt_string("filament_type", 0u).empty();
|
||||
const bool is_placeholder = tray.has("filament_slot_placeholder") &&
|
||||
tray.opt_bool("filament_slot_placeholder", 0u);
|
||||
if (!has_filament && !is_placeholder) {
|
||||
if (!has_filament && !is_placeholder && !has_type) {
|
||||
continue;
|
||||
}
|
||||
if (combo_index >= p->combos_filament.size()) {
|
||||
|
||||
@@ -0,0 +1,204 @@
|
||||
#include "BambuFileGridModel.h"
|
||||
|
||||
#include "slic3r/GUI/GUI_App.hpp"
|
||||
|
||||
#include <slic3r/GUI/Printer/IFileGridModel.h>
|
||||
#include <boost/smart_ptr/shared_ptr.hpp>
|
||||
#include <slic3r/GUI/Printer/PrinterFileSystem.h>
|
||||
#include <cstddef>
|
||||
#include <ctime>
|
||||
#include <functional>
|
||||
#include <utility>
|
||||
#include <wx/event.h>
|
||||
|
||||
namespace Slic3r {
|
||||
namespace GUI {
|
||||
|
||||
static const FileGridCard& emptyCard()
|
||||
{
|
||||
static const FileGridCard card;
|
||||
return card;
|
||||
}
|
||||
|
||||
BambuFileGridModel::BambuFileGridModel(boost::shared_ptr<PrinterFileSystem> fs)
|
||||
: m_fs(std::move(fs))
|
||||
{
|
||||
if (m_fs) {
|
||||
m_fs->Bind(EVT_FILE_CHANGED, &BambuFileGridModel::onFileChanged, this);
|
||||
m_fs->Bind(EVT_MODE_CHANGED, &BambuFileGridModel::onModeChanged, this);
|
||||
m_fs->Bind(EVT_THUMBNAIL, &BambuFileGridModel::onThumbnail, this);
|
||||
m_fs->Bind(EVT_DOWNLOAD, &BambuFileGridModel::onDownload, this);
|
||||
}
|
||||
}
|
||||
|
||||
BambuFileGridModel::~BambuFileGridModel()
|
||||
{
|
||||
if (m_fs) {
|
||||
m_fs->Unbind(EVT_FILE_CHANGED, &BambuFileGridModel::onFileChanged, this);
|
||||
m_fs->Unbind(EVT_MODE_CHANGED, &BambuFileGridModel::onModeChanged, this);
|
||||
m_fs->Unbind(EVT_THUMBNAIL, &BambuFileGridModel::onThumbnail, this);
|
||||
m_fs->Unbind(EVT_DOWNLOAD, &BambuFileGridModel::onDownload, this);
|
||||
}
|
||||
}
|
||||
|
||||
void BambuFileGridModel::EnsureCards() const
|
||||
{
|
||||
if (!m_dirty && (!m_fs || m_cards.size() == m_fs->GetCount()))
|
||||
return;
|
||||
m_dirty = false;
|
||||
m_cards.clear();
|
||||
if (!m_fs)
|
||||
return;
|
||||
size_t count = m_fs->GetCount();
|
||||
m_cards.reserve(count);
|
||||
for (size_t i = 0; i < count; ++i) {
|
||||
bool selected = false;
|
||||
PrinterFileSystem::File const &file = m_fs->GetFile(i, selected);
|
||||
FileGridCard card;
|
||||
card.id = file.path;
|
||||
card.name = file.name;
|
||||
card.title = file.Title();
|
||||
card.time_text = file.Metadata("Time", "");
|
||||
card.weight_text = file.Metadata("Weight", "");
|
||||
card.time = file.time;
|
||||
card.downloading = file.IsDownload();
|
||||
card.download_progress = file.DownloadProgress();
|
||||
card.selected = selected;
|
||||
card.thumbnail = file.thumbnail;
|
||||
m_cards.push_back(std::move(card));
|
||||
}
|
||||
}
|
||||
|
||||
size_t BambuFileGridModel::GetCount() const
|
||||
{
|
||||
return m_fs ? m_fs->GetCount() : 0;
|
||||
}
|
||||
|
||||
const FileGridCard& BambuFileGridModel::GetFile(size_t index, bool& selected) const
|
||||
{
|
||||
EnsureCards();
|
||||
if (!m_fs || index >= m_cards.size()) {
|
||||
selected = false;
|
||||
return emptyCard();
|
||||
}
|
||||
m_fs->GetFile(index, selected);
|
||||
return m_cards[index];
|
||||
}
|
||||
|
||||
const FileGridCard& BambuFileGridModel::GetFile(size_t index) const
|
||||
{
|
||||
EnsureCards();
|
||||
if (index >= m_cards.size())
|
||||
return emptyCard();
|
||||
return m_cards[index];
|
||||
}
|
||||
|
||||
FileGridType BambuFileGridModel::GetFileType() const
|
||||
{
|
||||
return m_fs ? static_cast<FileGridType>(m_fs->GetFileType()) : FileGridType::Timelapse;
|
||||
}
|
||||
|
||||
void BambuFileGridModel::SetFileType(FileGridType type, const std::string& storage)
|
||||
{
|
||||
if (m_fs)
|
||||
m_fs->SetFileType(static_cast<PrinterFileSystem::FileType>(type), storage);
|
||||
}
|
||||
|
||||
FileGridGroup BambuFileGridModel::GetGroupMode() const
|
||||
{
|
||||
return m_fs ? static_cast<FileGridGroup>(m_fs->GetGroupMode()) : FileGridGroup::All;
|
||||
}
|
||||
|
||||
void BambuFileGridModel::SetGroupMode(FileGridGroup mode)
|
||||
{
|
||||
if (m_fs)
|
||||
m_fs->SetGroupMode(static_cast<PrinterFileSystem::GroupMode>(mode));
|
||||
}
|
||||
|
||||
size_t BambuFileGridModel::GetIndexAtTime(time_t time) const
|
||||
{
|
||||
return m_fs ? m_fs->GetIndexAtTime(static_cast<boost::uint32_t>(time)) : 0;
|
||||
}
|
||||
|
||||
size_t BambuFileGridModel::EnterSubGroup(size_t index)
|
||||
{
|
||||
return m_fs ? m_fs->EnterSubGroup(index) : index;
|
||||
}
|
||||
|
||||
void BambuFileGridModel::ToggleSelect(size_t index)
|
||||
{
|
||||
if (m_fs)
|
||||
m_fs->ToggleSelect(index);
|
||||
}
|
||||
|
||||
void BambuFileGridModel::SelectAll(bool select)
|
||||
{
|
||||
if (m_fs)
|
||||
m_fs->SelectAll(select);
|
||||
}
|
||||
|
||||
void BambuFileGridModel::SetFocusRange(size_t start, size_t count)
|
||||
{
|
||||
if (m_fs)
|
||||
m_fs->SetFocusRange(start, count);
|
||||
}
|
||||
|
||||
void BambuFileGridModel::Retry()
|
||||
{
|
||||
if (m_fs)
|
||||
m_fs->Retry();
|
||||
}
|
||||
|
||||
int BambuFileGridModel::GetLastError() const
|
||||
{
|
||||
return m_fs ? m_fs->GetLastError() : 1;
|
||||
}
|
||||
|
||||
void BambuFileGridModel::DownloadCheckFiles(const std::string& path)
|
||||
{
|
||||
if (m_fs)
|
||||
m_fs->DownloadCheckFiles(path);
|
||||
}
|
||||
|
||||
void BambuFileGridModel::SetChangeHandler(std::function<void(FileGridChange)> handler)
|
||||
{
|
||||
m_handler = std::move(handler);
|
||||
}
|
||||
|
||||
void BambuFileGridModel::notify(FileGridChange change)
|
||||
{
|
||||
if (m_handler)
|
||||
m_handler(change);
|
||||
}
|
||||
|
||||
void BambuFileGridModel::onFileChanged(wxCommandEvent& evt)
|
||||
{
|
||||
evt.Skip();
|
||||
if (evt.GetInt() == -1 && m_fs)
|
||||
m_fs->DownloadCheckFiles(wxGetApp().app_config->get("download_path"));
|
||||
m_dirty = true;
|
||||
notify(FileGridChange::Files);
|
||||
}
|
||||
|
||||
void BambuFileGridModel::onModeChanged(wxCommandEvent& evt)
|
||||
{
|
||||
evt.Skip();
|
||||
m_dirty = true;
|
||||
notify(FileGridChange::Mode);
|
||||
}
|
||||
|
||||
void BambuFileGridModel::onThumbnail(wxCommandEvent& evt)
|
||||
{
|
||||
evt.Skip();
|
||||
m_dirty = true;
|
||||
notify(FileGridChange::Thumbnail);
|
||||
}
|
||||
|
||||
void BambuFileGridModel::onDownload(wxCommandEvent& evt)
|
||||
{
|
||||
evt.Skip();
|
||||
m_dirty = true;
|
||||
notify(FileGridChange::Download);
|
||||
}
|
||||
|
||||
}}
|
||||
@@ -0,0 +1,61 @@
|
||||
#pragma once
|
||||
|
||||
#include "IFileGridModel.h"
|
||||
#include "PrinterFileSystem.h"
|
||||
|
||||
#include <boost/smart_ptr/shared_ptr.hpp>
|
||||
#include <wx/event.h>
|
||||
#include <cstddef>
|
||||
#include <ctime>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace Slic3r {
|
||||
namespace GUI {
|
||||
|
||||
// IFileGridModel over PrinterFileSystem, behavior-identical to ImageGrid's direct use.
|
||||
class BambuFileGridModel : public wxEvtHandler, public IFileGridModel
|
||||
{
|
||||
public:
|
||||
explicit BambuFileGridModel(boost::shared_ptr<PrinterFileSystem> fs);
|
||||
~BambuFileGridModel() override;
|
||||
|
||||
size_t GetCount() const override;
|
||||
const FileGridCard& GetFile(size_t index, bool& selected) const override;
|
||||
const FileGridCard& GetFile(size_t index) const override;
|
||||
|
||||
FileGridType GetFileType() const override;
|
||||
void SetFileType(FileGridType type, const std::string& storage) override;
|
||||
FileGridGroup GetGroupMode() const override;
|
||||
void SetGroupMode(FileGridGroup mode) override;
|
||||
|
||||
size_t GetIndexAtTime(time_t time) const override;
|
||||
size_t EnterSubGroup(size_t index) override;
|
||||
|
||||
void ToggleSelect(size_t index) override;
|
||||
void SelectAll(bool select) override;
|
||||
void SetFocusRange(size_t start, size_t count) override;
|
||||
|
||||
void Retry() override;
|
||||
int GetLastError() const override;
|
||||
void DownloadCheckFiles(const std::string& path) override;
|
||||
|
||||
void SetChangeHandler(std::function<void(FileGridChange)> handler) override;
|
||||
|
||||
private:
|
||||
void EnsureCards() const;
|
||||
void notify(FileGridChange change);
|
||||
|
||||
void onFileChanged(wxCommandEvent& evt);
|
||||
void onModeChanged(wxCommandEvent& evt);
|
||||
void onThumbnail(wxCommandEvent& evt);
|
||||
void onDownload(wxCommandEvent& evt);
|
||||
|
||||
boost::shared_ptr<PrinterFileSystem> m_fs;
|
||||
std::function<void(FileGridChange)> m_handler;
|
||||
mutable std::vector<FileGridCard> m_cards;
|
||||
mutable bool m_dirty = true;
|
||||
};
|
||||
|
||||
}}
|
||||
@@ -0,0 +1,67 @@
|
||||
#pragma once
|
||||
|
||||
#include <wx/bitmap.h>
|
||||
#include <cstddef>
|
||||
#include <ctime>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
|
||||
namespace Slic3r {
|
||||
namespace GUI {
|
||||
|
||||
// Mirrors PrinterFileSystem::FileType ordering (F_TIMELAPSE=0, F_VIDEO=1, F_MODEL=2).
|
||||
enum class FileGridType { Timelapse = 0, Video = 1, Model = 2 };
|
||||
// Mirrors PrinterFileSystem::GroupMode ordering (G_NONE=0, G_MONTH=1, G_YEAR=2).
|
||||
enum class FileGridGroup { All = 0, Month = 1, Year = 2 };
|
||||
// Mirrors the PrinterFileSystem wx events ImageGrid listens to.
|
||||
enum class FileGridChange { Files, Mode, Thumbnail, Download };
|
||||
|
||||
// One rendered card. Neutral: no Bambu transfer internals, no filesystem paths beyond id.
|
||||
struct FileGridCard {
|
||||
std::string id;
|
||||
std::string name;
|
||||
std::string title;
|
||||
std::string time_text;
|
||||
std::string weight_text;
|
||||
time_t time = 0;
|
||||
bool downloading = false;
|
||||
int download_progress = 0; // -1 waiting, <0 failed, 0..100 progress
|
||||
bool selected = false;
|
||||
bool printable = true;
|
||||
wxBitmap thumbnail;
|
||||
};
|
||||
|
||||
// Data source behind ImageGrid. Implemented today by BambuFileGridModel (wrapping
|
||||
// PrinterFileSystem); a non-Bambu model can implement the same surface later.
|
||||
class IFileGridModel {
|
||||
public:
|
||||
virtual ~IFileGridModel() = default;
|
||||
|
||||
virtual size_t GetCount() const = 0;
|
||||
virtual const FileGridCard& GetFile(size_t index, bool& selected) const = 0;
|
||||
virtual const FileGridCard& GetFile(size_t index) const = 0;
|
||||
|
||||
virtual FileGridType GetFileType() const = 0;
|
||||
virtual void SetFileType(FileGridType type, const std::string& storage) = 0;
|
||||
virtual FileGridGroup GetGroupMode() const = 0;
|
||||
virtual void SetGroupMode(FileGridGroup mode) = 0;
|
||||
|
||||
// When true, the thumbnail is fitted inside the tile instead of stretched to fill it.
|
||||
virtual bool preserve_thumbnail_aspect() const { return false; }
|
||||
|
||||
virtual size_t GetIndexAtTime(time_t time) const = 0;
|
||||
virtual size_t EnterSubGroup(size_t index) = 0;
|
||||
|
||||
virtual void ToggleSelect(size_t index) = 0;
|
||||
virtual void SelectAll(bool select) = 0;
|
||||
virtual void SetFocusRange(size_t start, size_t count) = 0;
|
||||
|
||||
virtual void Retry() = 0;
|
||||
virtual int GetLastError() const = 0;
|
||||
virtual void DownloadCheckFiles(const std::string& path) = 0;
|
||||
|
||||
// ImageGrid sets one handler; the model invokes it on the UI thread on change.
|
||||
virtual void SetChangeHandler(std::function<void(FileGridChange)>) = 0;
|
||||
};
|
||||
|
||||
}}
|
||||
@@ -0,0 +1,545 @@
|
||||
#include "RemoteFileGridModel.h"
|
||||
|
||||
#include "IPrinterAgent.hpp"
|
||||
#include "slic3r/GUI/GUI_App.hpp"
|
||||
#include "slic3r/Utils/NetworkAgent.hpp"
|
||||
|
||||
#include <slic3r/GUI/Printer/IFileGridModel.h>
|
||||
#include <string>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
#include <ctime>
|
||||
#include <functional>
|
||||
#include <cstddef>
|
||||
#include <wx/datetime.h>
|
||||
#include <wx/gdicmn.h>
|
||||
#include <wx/image.h>
|
||||
#include <wx/mstream.h>
|
||||
|
||||
#include <boost/format.hpp>
|
||||
#include <boost/regex.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cmath>
|
||||
#include <utility>
|
||||
|
||||
namespace Slic3r {
|
||||
namespace GUI {
|
||||
|
||||
namespace {
|
||||
|
||||
const FileGridCard& emptyCard()
|
||||
{
|
||||
static const FileGridCard card;
|
||||
return card;
|
||||
}
|
||||
|
||||
// Same display format as PrinterFileSystem: "1d2h3m", dropping a leading "0d"/"0h".
|
||||
std::string durationString(long duration)
|
||||
{
|
||||
static boost::regex rx("^0d(0h)?");
|
||||
auto time = boost::format("%1%d%2%h%3%m") % (duration / 86400) % ((duration % 86400) / 3600) % ((duration % 3600) / 60);
|
||||
return boost::regex_replace(time.str(), rx, "");
|
||||
}
|
||||
|
||||
// Case-insensitive suffix test shared by the file filters below.
|
||||
bool ends_with_ci(const std::string& name, const std::string& suffix)
|
||||
{
|
||||
return name.size() >= suffix.size() &&
|
||||
std::equal(suffix.rbegin(), suffix.rend(), name.rbegin(),
|
||||
[](char a, char b) {
|
||||
return std::tolower(static_cast<unsigned char>(a)) ==
|
||||
std::tolower(static_cast<unsigned char>(b));
|
||||
});
|
||||
}
|
||||
|
||||
// .gcode / .3mf only, case-insensitive; a .gcode.3mf name matches via the .3mf suffix.
|
||||
bool is_displayable_file(const std::string& name)
|
||||
{
|
||||
return ends_with_ci(name, ".gcode") || ends_with_ci(name, ".3mf");
|
||||
}
|
||||
|
||||
// .gcode and .gcode.3mf are printable; OrcaSonar unpacks a 3MF to its plate
|
||||
// G-code. A bare .3mf is an unsliced project file, so it stays unprintable.
|
||||
bool is_printable_gcode(const std::string& name)
|
||||
{
|
||||
return ends_with_ci(name, ".gcode") || ends_with_ci(name, ".gcode.3mf");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
RemoteFileGridModel::RemoteFileGridModel(std::string device_id)
|
||||
: m_device_id(std::move(device_id))
|
||||
{
|
||||
}
|
||||
|
||||
RemoteFileGridModel::~RemoteFileGridModel()
|
||||
{
|
||||
m_lifetime.reset();
|
||||
}
|
||||
|
||||
void RemoteFileGridModel::Refresh()
|
||||
{
|
||||
const std::uint64_t request = ++m_request;
|
||||
const std::string device_id = m_device_id;
|
||||
m_thumbnail_cache.clear();
|
||||
m_thumbnail_requested.clear();
|
||||
m_thumbnail_queue.clear();
|
||||
m_metadata_cache.clear();
|
||||
m_metadata_requested.clear();
|
||||
m_metadata_queue.clear();
|
||||
for (FileGridCard& card : m_cards)
|
||||
card.thumbnail = wxBitmap();
|
||||
m_last_error = 0;
|
||||
setStatus(Status::Loading);
|
||||
|
||||
NetworkAgent* agent = wxGetApp().getAgent();
|
||||
if (!agent) {
|
||||
m_last_error = -1;
|
||||
m_cards.clear();
|
||||
m_month.clear();
|
||||
m_year.clear();
|
||||
setStatus(Status::Failed);
|
||||
if (m_change_handler)
|
||||
m_change_handler(FileGridChange::Files);
|
||||
return;
|
||||
}
|
||||
|
||||
std::weak_ptr<int> lifetime = m_lifetime;
|
||||
int result = agent->list_printer_files(device_id,
|
||||
[this, lifetime, request, device_id](int result, std::vector<PrinterFileEntry> files) {
|
||||
if (lifetime.expired())
|
||||
return;
|
||||
onFilesLoaded(request, device_id, result, std::move(files));
|
||||
});
|
||||
|
||||
if (result != 0 && request == m_request && m_status == Status::Loading) {
|
||||
m_last_error = result;
|
||||
m_cards.clear();
|
||||
m_month.clear();
|
||||
m_year.clear();
|
||||
setStatus(Status::Failed);
|
||||
if (m_change_handler)
|
||||
m_change_handler(FileGridChange::Files);
|
||||
}
|
||||
}
|
||||
|
||||
void RemoteFileGridModel::onFilesLoaded(std::uint64_t request, const std::string& device_id, int result,
|
||||
std::vector<PrinterFileEntry> files)
|
||||
{
|
||||
if (request != m_request || device_id != m_device_id)
|
||||
return;
|
||||
|
||||
m_last_error = result;
|
||||
m_cards.clear();
|
||||
if (result == 0) {
|
||||
files.erase(std::remove_if(files.begin(), files.end(),
|
||||
[](const PrinterFileEntry& file) { return !is_displayable_file(file.name); }),
|
||||
files.end());
|
||||
std::sort(files.begin(), files.end(), [](const PrinterFileEntry& lhs, const PrinterFileEntry& rhs) {
|
||||
return lhs.modified > rhs.modified;
|
||||
});
|
||||
m_cards.reserve(files.size());
|
||||
for (const PrinterFileEntry& file : files) {
|
||||
FileGridCard card;
|
||||
card.id = file.path;
|
||||
card.name = file.name;
|
||||
card.time = static_cast<time_t>(file.modified);
|
||||
card.printable = is_printable_gcode(file.name);
|
||||
m_cards.emplace_back(std::move(card));
|
||||
}
|
||||
buildGroups();
|
||||
setStatus(m_cards.empty() ? Status::Empty : Status::Ready);
|
||||
} else {
|
||||
m_month.clear();
|
||||
m_year.clear();
|
||||
setStatus(Status::Failed);
|
||||
}
|
||||
|
||||
if (m_change_handler)
|
||||
m_change_handler(FileGridChange::Files);
|
||||
}
|
||||
|
||||
void RemoteFileGridModel::setStatus(Status status)
|
||||
{
|
||||
m_status = status;
|
||||
if (m_status_handler)
|
||||
m_status_handler(status);
|
||||
}
|
||||
|
||||
void RemoteFileGridModel::SetStatusHandler(std::function<void(Status)> handler)
|
||||
{
|
||||
m_status_handler = std::move(handler);
|
||||
}
|
||||
|
||||
size_t RemoteFileGridModel::GetCount() const
|
||||
{
|
||||
if (m_group_mode == FileGridGroup::Year)
|
||||
return m_year.size();
|
||||
if (m_group_mode == FileGridGroup::Month)
|
||||
return m_month.size();
|
||||
return m_cards.size();
|
||||
}
|
||||
|
||||
const FileGridCard& RemoteFileGridModel::GetFile(size_t index, bool& selected) const
|
||||
{
|
||||
selected = false;
|
||||
const size_t card_index = cardIndexForGroup(index);
|
||||
if (card_index >= m_cards.size())
|
||||
return emptyCard();
|
||||
FileGridCard& card = m_cards[card_index];
|
||||
const auto thumbnail = m_thumbnail_cache.find(card.id);
|
||||
card.thumbnail = thumbnail == m_thumbnail_cache.end() ? wxBitmap() : thumbnail->second;
|
||||
return card;
|
||||
}
|
||||
|
||||
const FileGridCard& RemoteFileGridModel::GetFile(size_t index) const
|
||||
{
|
||||
bool selected = false;
|
||||
return GetFile(index, selected);
|
||||
}
|
||||
|
||||
size_t RemoteFileGridModel::cardIndexForGroup(size_t index) const
|
||||
{
|
||||
if (m_group_mode == FileGridGroup::All)
|
||||
return index;
|
||||
if (m_group_mode == FileGridGroup::Month)
|
||||
return index < m_month.size() ? m_month[index] : m_cards.size();
|
||||
return index < m_year.size() && m_year[index] < m_month.size() ? m_month[m_year[index]] : m_cards.size();
|
||||
}
|
||||
|
||||
FileGridType RemoteFileGridModel::GetFileType() const
|
||||
{
|
||||
return m_file_type;
|
||||
}
|
||||
|
||||
void RemoteFileGridModel::SetFileType(FileGridType type, const std::string&)
|
||||
{
|
||||
m_file_type = type;
|
||||
}
|
||||
|
||||
FileGridGroup RemoteFileGridModel::GetGroupMode() const
|
||||
{
|
||||
return m_group_mode;
|
||||
}
|
||||
|
||||
void RemoteFileGridModel::SetGroupMode(FileGridGroup mode)
|
||||
{
|
||||
if (mode != FileGridGroup::All && mode != FileGridGroup::Month && mode != FileGridGroup::Year)
|
||||
return;
|
||||
if (m_group_mode == mode)
|
||||
return;
|
||||
m_group_mode = mode;
|
||||
if (m_group_mode_handler)
|
||||
m_group_mode_handler(mode);
|
||||
if (m_change_handler)
|
||||
m_change_handler(FileGridChange::Mode);
|
||||
}
|
||||
|
||||
size_t RemoteFileGridModel::GetIndexAtTime(time_t time) const
|
||||
{
|
||||
if (m_cards.empty())
|
||||
return 0;
|
||||
|
||||
size_t low = 0;
|
||||
size_t high = m_cards.size();
|
||||
while (low < high) {
|
||||
const size_t middle = low + (high - low) / 2;
|
||||
if (m_cards[middle].time >= time)
|
||||
low = middle + 1;
|
||||
else
|
||||
high = middle;
|
||||
}
|
||||
const size_t card_index = low == 0 ? 0 : low - 1;
|
||||
if (m_group_mode == FileGridGroup::All)
|
||||
return card_index;
|
||||
|
||||
const auto month = std::upper_bound(m_month.begin(), m_month.end(), card_index);
|
||||
const size_t month_index = month == m_month.begin() ? 0 : static_cast<size_t>(month - m_month.begin() - 1);
|
||||
if (m_group_mode == FileGridGroup::Month)
|
||||
return month_index;
|
||||
|
||||
const auto year = std::upper_bound(m_year.begin(), m_year.end(), card_index,
|
||||
[this](size_t file_index, size_t month_index) {
|
||||
return file_index < m_month[month_index];
|
||||
});
|
||||
return year == m_year.begin() ? 0 : static_cast<size_t>(year - m_year.begin() - 1);
|
||||
}
|
||||
|
||||
size_t RemoteFileGridModel::EnterSubGroup(size_t index)
|
||||
{
|
||||
if (m_group_mode == FileGridGroup::All)
|
||||
return index;
|
||||
if (m_group_mode == FileGridGroup::Year) {
|
||||
if (index >= m_year.size())
|
||||
return 0;
|
||||
index = m_year[index];
|
||||
} else {
|
||||
if (index >= m_month.size())
|
||||
return 0;
|
||||
index = m_month[index];
|
||||
}
|
||||
SetGroupMode(static_cast<FileGridGroup>(static_cast<int>(m_group_mode) - 1));
|
||||
return index;
|
||||
}
|
||||
|
||||
void RemoteFileGridModel::ToggleSelect(size_t)
|
||||
{
|
||||
}
|
||||
|
||||
void RemoteFileGridModel::SelectAll(bool)
|
||||
{
|
||||
}
|
||||
|
||||
void RemoteFileGridModel::SetFocusRange(size_t start, size_t count)
|
||||
{
|
||||
if (m_status == Status::Loading || start >= GetCount())
|
||||
return;
|
||||
const size_t end = start + std::min(count, GetCount() - start);
|
||||
for (size_t i = start; i < end; ++i) {
|
||||
const FileGridCard& card = GetFile(i);
|
||||
if (!card.id.empty()) {
|
||||
requestThumbnail(card.id);
|
||||
requestMetadata(card.id);
|
||||
}
|
||||
}
|
||||
pumpThumbnailQueue();
|
||||
pumpMetadataQueue();
|
||||
}
|
||||
|
||||
void RemoteFileGridModel::Retry()
|
||||
{
|
||||
Refresh();
|
||||
}
|
||||
|
||||
int RemoteFileGridModel::GetLastError() const
|
||||
{
|
||||
return m_last_error;
|
||||
}
|
||||
|
||||
void RemoteFileGridModel::DownloadCheckFiles(const std::string&)
|
||||
{
|
||||
}
|
||||
|
||||
void RemoteFileGridModel::DeleteFile(const std::string& path, std::function<void(bool ok)> done)
|
||||
{
|
||||
NetworkAgent* agent = wxGetApp().getAgent();
|
||||
if (!agent || m_device_id.empty() || path.empty()) {
|
||||
if (done)
|
||||
done(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const std::uint64_t request = m_request;
|
||||
const std::string device_id = m_device_id;
|
||||
const std::weak_ptr<int> lifetime = m_lifetime;
|
||||
const auto completed = std::make_shared<bool>(false);
|
||||
auto finish = [this, lifetime, completed, request, device_id, path, done = std::move(done)](int result) {
|
||||
if (*completed)
|
||||
return;
|
||||
*completed = true;
|
||||
if (lifetime.expired())
|
||||
return;
|
||||
|
||||
const bool ok = result == 0;
|
||||
if (ok && request == m_request && device_id == m_device_id) {
|
||||
m_thumbnail_cache.erase(path);
|
||||
m_thumbnail_requested.erase(path);
|
||||
m_cards.erase(std::remove_if(m_cards.begin(), m_cards.end(),
|
||||
[&path](const FileGridCard& card) { return card.id == path; }),
|
||||
m_cards.end());
|
||||
buildGroups();
|
||||
setStatus(m_cards.empty() ? Status::Empty : Status::Ready);
|
||||
if (m_change_handler)
|
||||
m_change_handler(FileGridChange::Files);
|
||||
}
|
||||
if (done)
|
||||
done(ok);
|
||||
};
|
||||
|
||||
const int result = agent->delete_printer_file(device_id, path, finish);
|
||||
if (result != 0)
|
||||
finish(result);
|
||||
}
|
||||
|
||||
void RemoteFileGridModel::SetChangeHandler(std::function<void(FileGridChange)> handler)
|
||||
{
|
||||
m_change_handler = std::move(handler);
|
||||
}
|
||||
|
||||
void RemoteFileGridModel::SetGroupModeHandler(std::function<void(FileGridGroup)> handler)
|
||||
{
|
||||
m_group_mode_handler = std::move(handler);
|
||||
}
|
||||
|
||||
void RemoteFileGridModel::buildGroups()
|
||||
{
|
||||
m_month.clear();
|
||||
m_year.clear();
|
||||
if (m_cards.empty())
|
||||
return;
|
||||
|
||||
m_month.push_back(0);
|
||||
m_year.push_back(0);
|
||||
wxDateTime previous(static_cast<time_t>(m_cards.front().time));
|
||||
for (size_t i = 1; i < m_cards.size(); ++i) {
|
||||
const wxDateTime current(static_cast<time_t>(m_cards[i].time));
|
||||
if (current.GetYear() != previous.GetYear()) {
|
||||
m_year.push_back(m_month.size());
|
||||
m_month.push_back(i);
|
||||
} else if (current.GetMonth() != previous.GetMonth()) {
|
||||
m_month.push_back(i);
|
||||
}
|
||||
previous = current;
|
||||
}
|
||||
}
|
||||
|
||||
void RemoteFileGridModel::requestThumbnail(const std::string& path)
|
||||
{
|
||||
if (m_thumbnail_cache.find(path) != m_thumbnail_cache.end() || !m_thumbnail_requested.insert(path).second)
|
||||
return;
|
||||
m_thumbnail_queue.push_back(path);
|
||||
}
|
||||
|
||||
void RemoteFileGridModel::pumpThumbnailQueue()
|
||||
{
|
||||
constexpr size_t MAX_THUMBNAIL_REQUESTS = 4;
|
||||
if (m_pumping_thumbnail_queue)
|
||||
return;
|
||||
m_pumping_thumbnail_queue = true;
|
||||
|
||||
NetworkAgent* agent = wxGetApp().getAgent();
|
||||
if (!agent) {
|
||||
while (!m_thumbnail_queue.empty()) {
|
||||
m_thumbnail_cache.emplace(m_thumbnail_queue.front(), wxBitmap());
|
||||
m_thumbnail_queue.pop_front();
|
||||
}
|
||||
m_pumping_thumbnail_queue = false;
|
||||
return;
|
||||
}
|
||||
|
||||
while (!m_thumbnail_queue.empty() && m_thumbnail_requests_in_flight < MAX_THUMBNAIL_REQUESTS) {
|
||||
std::string path = std::move(m_thumbnail_queue.front());
|
||||
m_thumbnail_queue.pop_front();
|
||||
++m_thumbnail_requests_in_flight;
|
||||
|
||||
const std::uint64_t request = m_request;
|
||||
const std::string device_id = m_device_id;
|
||||
const std::weak_ptr<int> lifetime = m_lifetime;
|
||||
const auto completed = std::make_shared<bool>(false);
|
||||
auto finish = [this, lifetime, completed, request, device_id, path](int result, std::string image) {
|
||||
if (*completed)
|
||||
return;
|
||||
*completed = true;
|
||||
if (lifetime.expired())
|
||||
return;
|
||||
onThumbnailLoaded(request, device_id, path, result, std::move(image));
|
||||
};
|
||||
|
||||
const int result = agent->get_printer_file_thumbnail(device_id, path, finish);
|
||||
if (result != 0)
|
||||
finish(result, {});
|
||||
}
|
||||
m_pumping_thumbnail_queue = false;
|
||||
}
|
||||
|
||||
void RemoteFileGridModel::onThumbnailLoaded(std::uint64_t request, const std::string& device_id,
|
||||
const std::string& path, int result, std::string image)
|
||||
{
|
||||
if (m_thumbnail_requests_in_flight > 0)
|
||||
--m_thumbnail_requests_in_flight;
|
||||
|
||||
if (request == m_request && device_id == m_device_id) {
|
||||
wxBitmap bitmap;
|
||||
if (result == 0 && !image.empty()) {
|
||||
wxMemoryInputStream stream(image.data(), image.size());
|
||||
wxImage decoded(stream, wxBITMAP_TYPE_ANY);
|
||||
if (decoded.IsOk())
|
||||
bitmap = wxBitmap(decoded);
|
||||
}
|
||||
m_thumbnail_cache[path] = std::move(bitmap);
|
||||
if (m_change_handler)
|
||||
m_change_handler(FileGridChange::Thumbnail);
|
||||
}
|
||||
|
||||
pumpThumbnailQueue();
|
||||
}
|
||||
|
||||
void RemoteFileGridModel::requestMetadata(const std::string& path)
|
||||
{
|
||||
if (m_metadata_cache.find(path) != m_metadata_cache.end() || !m_metadata_requested.insert(path).second)
|
||||
return;
|
||||
m_metadata_queue.push_back(path);
|
||||
}
|
||||
|
||||
void RemoteFileGridModel::pumpMetadataQueue()
|
||||
{
|
||||
constexpr size_t MAX_METADATA_REQUESTS = 4;
|
||||
if (m_pumping_metadata_queue)
|
||||
return;
|
||||
m_pumping_metadata_queue = true;
|
||||
|
||||
NetworkAgent* agent = wxGetApp().getAgent();
|
||||
if (!agent) {
|
||||
while (!m_metadata_queue.empty()) {
|
||||
m_metadata_cache.emplace(m_metadata_queue.front(), PrinterFileMetadata{});
|
||||
m_metadata_queue.pop_front();
|
||||
}
|
||||
m_pumping_metadata_queue = false;
|
||||
return;
|
||||
}
|
||||
|
||||
while (!m_metadata_queue.empty() && m_metadata_requests_in_flight < MAX_METADATA_REQUESTS) {
|
||||
std::string path = std::move(m_metadata_queue.front());
|
||||
m_metadata_queue.pop_front();
|
||||
++m_metadata_requests_in_flight;
|
||||
|
||||
const std::uint64_t request = m_request;
|
||||
const std::string device_id = m_device_id;
|
||||
const std::weak_ptr<int> lifetime = m_lifetime;
|
||||
const auto completed = std::make_shared<bool>(false);
|
||||
auto finish = [this, lifetime, completed, request, device_id, path](int result, PrinterFileMetadata meta) {
|
||||
if (*completed)
|
||||
return;
|
||||
*completed = true;
|
||||
if (lifetime.expired())
|
||||
return;
|
||||
onMetadataLoaded(request, device_id, path, result, meta);
|
||||
};
|
||||
|
||||
const int result = agent->get_printer_file_metadata(device_id, path, finish);
|
||||
if (result != 0)
|
||||
finish(result, {});
|
||||
}
|
||||
m_pumping_metadata_queue = false;
|
||||
}
|
||||
|
||||
void RemoteFileGridModel::onMetadataLoaded(std::uint64_t request, const std::string& device_id,
|
||||
const std::string& path, int result, PrinterFileMetadata meta)
|
||||
{
|
||||
if (m_metadata_requests_in_flight > 0)
|
||||
--m_metadata_requests_in_flight;
|
||||
|
||||
if (request == m_request && device_id == m_device_id) {
|
||||
m_metadata_cache[path] = meta;
|
||||
if (result == 0) {
|
||||
for (FileGridCard& card : m_cards) {
|
||||
if (card.id != path)
|
||||
continue;
|
||||
card.time_text = meta.estimated_time > 0 ? durationString(meta.estimated_time) : std::string();
|
||||
card.weight_text = meta.filament_weight > 0
|
||||
? std::to_string(int(std::round(meta.filament_weight))) + 'g'
|
||||
: std::string();
|
||||
break;
|
||||
}
|
||||
if (m_change_handler)
|
||||
m_change_handler(FileGridChange::Thumbnail);
|
||||
}
|
||||
}
|
||||
|
||||
pumpMetadataQueue();
|
||||
}
|
||||
|
||||
}}
|
||||
@@ -0,0 +1,100 @@
|
||||
#pragma once
|
||||
|
||||
#include "IFileGridModel.h"
|
||||
|
||||
#include "slic3r/Utils/IPrinterAgent.hpp"
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <ctime>
|
||||
#include <deque>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
namespace Slic3r {
|
||||
namespace GUI {
|
||||
|
||||
class RemoteFileGridModel : public IFileGridModel
|
||||
{
|
||||
public:
|
||||
enum class Status { Loading, Ready, Empty, Failed };
|
||||
|
||||
explicit RemoteFileGridModel(std::string device_id);
|
||||
~RemoteFileGridModel() override;
|
||||
|
||||
void Refresh();
|
||||
Status GetStatus() const { return m_status; }
|
||||
void SetStatusHandler(std::function<void(Status)> handler);
|
||||
void SetGroupModeHandler(std::function<void(FileGridGroup)> handler);
|
||||
|
||||
size_t GetCount() const override;
|
||||
const FileGridCard& GetFile(size_t index, bool& selected) const override;
|
||||
const FileGridCard& GetFile(size_t index) const override;
|
||||
|
||||
FileGridType GetFileType() const override;
|
||||
void SetFileType(FileGridType type, const std::string& storage) override;
|
||||
FileGridGroup GetGroupMode() const override;
|
||||
void SetGroupMode(FileGridGroup mode) override;
|
||||
|
||||
size_t GetIndexAtTime(time_t time) const override;
|
||||
size_t EnterSubGroup(size_t index) override;
|
||||
|
||||
void ToggleSelect(size_t index) override;
|
||||
void SelectAll(bool select) override;
|
||||
void SetFocusRange(size_t start, size_t count) override;
|
||||
|
||||
void Retry() override;
|
||||
int GetLastError() const override;
|
||||
void DownloadCheckFiles(const std::string& path) override;
|
||||
bool preserve_thumbnail_aspect() const override { return true; }
|
||||
|
||||
// Delete `path` from the printer; `done` reports success on the UI thread.
|
||||
void DeleteFile(const std::string& path, std::function<void(bool ok)> done);
|
||||
|
||||
void SetChangeHandler(std::function<void(FileGridChange)> handler) override;
|
||||
|
||||
private:
|
||||
void setStatus(Status status);
|
||||
void onFilesLoaded(std::uint64_t request, const std::string& device_id, int result,
|
||||
std::vector<PrinterFileEntry> files);
|
||||
void buildGroups();
|
||||
size_t cardIndexForGroup(size_t index) const;
|
||||
void requestThumbnail(const std::string& path);
|
||||
void pumpThumbnailQueue();
|
||||
void onThumbnailLoaded(std::uint64_t request, const std::string& device_id,
|
||||
const std::string& path, int result, std::string image);
|
||||
void requestMetadata(const std::string& path);
|
||||
void pumpMetadataQueue();
|
||||
void onMetadataLoaded(std::uint64_t request, const std::string& device_id,
|
||||
const std::string& path, int result, PrinterFileMetadata meta);
|
||||
|
||||
std::string m_device_id;
|
||||
mutable std::vector<FileGridCard> m_cards;
|
||||
std::vector<size_t> m_month;
|
||||
std::vector<size_t> m_year;
|
||||
std::function<void(FileGridChange)> m_change_handler;
|
||||
std::function<void(Status)> m_status_handler;
|
||||
std::function<void(FileGridGroup)> m_group_mode_handler;
|
||||
std::shared_ptr<int> m_lifetime = std::make_shared<int>(0);
|
||||
std::unordered_map<std::string, wxBitmap> m_thumbnail_cache;
|
||||
std::unordered_set<std::string> m_thumbnail_requested;
|
||||
std::deque<std::string> m_thumbnail_queue;
|
||||
std::unordered_map<std::string, PrinterFileMetadata> m_metadata_cache;
|
||||
std::unordered_set<std::string> m_metadata_requested;
|
||||
std::deque<std::string> m_metadata_queue;
|
||||
std::uint64_t m_request = 0;
|
||||
size_t m_thumbnail_requests_in_flight = 0;
|
||||
bool m_pumping_thumbnail_queue = false;
|
||||
size_t m_metadata_requests_in_flight = 0;
|
||||
bool m_pumping_metadata_queue = false;
|
||||
int m_last_error = 0;
|
||||
FileGridType m_file_type = FileGridType::Timelapse;
|
||||
FileGridGroup m_group_mode = FileGridGroup::All;
|
||||
Status m_status = Status::Loading;
|
||||
};
|
||||
|
||||
}}
|
||||
@@ -36,6 +36,8 @@
|
||||
#include "libslic3r/MultiNozzleUtils.hpp" // filament-change-gap model for the best-position popup
|
||||
#include "BackgroundSlicingProcess.hpp" // complete type for background_process().get_current_gcode_result()
|
||||
#include "DeviceCore/DevStorage.h"
|
||||
#include "slic3r/Utils/NetworkAgentFactory.hpp"
|
||||
#include "FilamentMappingUtils.hpp"
|
||||
|
||||
#include <wx/event.h>
|
||||
#include <string>
|
||||
@@ -3568,6 +3570,9 @@ void SelectMachineDialog::navigate_to_timelapse_page()
|
||||
this->EndModal(wxID_CANCEL);
|
||||
}
|
||||
|
||||
// Mapping helpers live in FilamentMappingUtils.hpp (shared with
|
||||
// SendMultiMachinePage); they mirror the agent serializer exactly.
|
||||
|
||||
void SelectMachineDialog::on_send_print()
|
||||
{
|
||||
BOOST_LOG_TRIVIAL(info) << "print_job: on_ok to send";
|
||||
@@ -3622,6 +3627,28 @@ void SelectMachineDialog::on_send_print()
|
||||
|
||||
get_ams_mapping_result(ams_mapping_array,ams_mapping_array2, ams_mapping_info);
|
||||
|
||||
// The policy lives in FilamentMappingUtils.hpp; this dialog only differs
|
||||
// from the multi-device page in how it reports a refusal.
|
||||
switch (prepare_filament_mapping_for_send(obj_, ams_mapping_array2, m_ams_mapping_result)) {
|
||||
case MappingSendError::unsupported:
|
||||
BOOST_LOG_TRIVIAL(warning) << "print_job: connector does not advertise filament_mapping; refusing mapped print";
|
||||
m_status_bar->set_status_text(_L("AMS filament mapping is not available for this printer. Clear the AMS mapping before printing."));
|
||||
Enable_Send_Button(true);
|
||||
return;
|
||||
case MappingSendError::incomplete:
|
||||
BOOST_LOG_TRIVIAL(warning) << "print_job: a used filament has no AMS target; refusing print";
|
||||
m_status_bar->set_status_text(_L("A filament used by this print has no AMS mapping. Assign it before printing."));
|
||||
Enable_Send_Button(true);
|
||||
return;
|
||||
case MappingSendError::none:
|
||||
break;
|
||||
default:
|
||||
// A refusal added without a handler here must not silently send.
|
||||
BOOST_LOG_TRIVIAL(warning) << "print_job: unrecognized mapping refusal; refusing print";
|
||||
Enable_Send_Button(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_print_type == PrintFromType::FROM_NORMAL) {
|
||||
result = m_plater->send_gcode(m_print_plate_idx, [this](int export_stage, int current, int total, bool& cancel) {
|
||||
if (this->m_is_canceled) return;
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
#include "GUI.hpp"
|
||||
#include "GUI_App.hpp"
|
||||
#include "slic3r/Utils/NetworkAgent.hpp"
|
||||
#include "GUI_Preview.hpp"
|
||||
#include "MainFrame.hpp"
|
||||
#include "Widgets/ProgressDialog.hpp"
|
||||
@@ -847,8 +848,8 @@ void SelectMachinePopup::update_machine_list(wxCommandEvent &event)
|
||||
|
||||
void SelectMachinePopup::start_ssdp(bool start)
|
||||
{
|
||||
return;
|
||||
//if (wxGetApp().getAgent()) { wxGetApp().getAgent()->start_discovery(true, start); }
|
||||
if (auto* agent = wxGetApp().getAgent())
|
||||
agent->start_discovery(true, start);
|
||||
}
|
||||
|
||||
void SelectMachinePopup::OnLeftUp(wxMouseEvent &event)
|
||||
|
||||
@@ -61,6 +61,10 @@
|
||||
|
||||
#include "DeviceCore/DevManager.h"
|
||||
#include "DeviceCore/DevStorage.h"
|
||||
|
||||
#include "slic3r/Utils/NetworkAgentFactory.hpp"
|
||||
#include "FilamentMappingUtils.hpp"
|
||||
|
||||
#include "libslic3r/AppConfig.hpp"
|
||||
#include "libslic3r/GCode/GCodeProcessor.hpp"
|
||||
#include "libslic3r/Model.hpp"
|
||||
@@ -760,6 +764,8 @@ bool SendMultiMachinePage::get_ams_mapping_result(std::string &mapping_array_str
|
||||
return true;
|
||||
}
|
||||
|
||||
// Mapping helpers live in FilamentMappingUtils.hpp, shared with SelectMachine.
|
||||
|
||||
void SendMultiMachinePage::on_send(wxCommandEvent& event)
|
||||
{
|
||||
event.Skip();
|
||||
@@ -808,6 +814,30 @@ void SendMultiMachinePage::on_send(wxCommandEvent& event)
|
||||
|
||||
if (!wxGetApp().is_blocking_printing(obj)) {
|
||||
PrintParams params = request_params(obj);
|
||||
switch (prepare_filament_mapping_for_send(obj, params.ams_mapping2, m_ams_mapping_result)) {
|
||||
case MappingSendError::unsupported:
|
||||
{
|
||||
BOOST_LOG_TRIVIAL(warning) << "SendMultiMachinePage: connector does not advertise filament_mapping; refusing mapped print for "
|
||||
<< obj->get_dev_id();
|
||||
MessageDialog unavailable_msg(nullptr, _L("AMS filament mapping is not available for this printer. Clear the AMS mapping before printing."), "", wxICON_WARNING | wxOK);
|
||||
unavailable_msg.ShowModal();
|
||||
return;
|
||||
}
|
||||
case MappingSendError::incomplete:
|
||||
{
|
||||
BOOST_LOG_TRIVIAL(warning) << "SendMultiMachinePage: a used filament has no target; refusing print for " << obj->get_dev_id();
|
||||
MessageDialog incomplete_msg(nullptr, _L("A filament used by this print has no AMS mapping. Assign it before printing."), "", wxICON_WARNING | wxOK);
|
||||
incomplete_msg.ShowModal();
|
||||
return;
|
||||
}
|
||||
case MappingSendError::none:
|
||||
break;
|
||||
default:
|
||||
// A refusal added without a handler here must not silently send.
|
||||
BOOST_LOG_TRIVIAL(warning) << "SendMultiMachinePage: unrecognized mapping refusal for "
|
||||
<< obj->get_dev_id();
|
||||
return;
|
||||
}
|
||||
print_params.push_back(params);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4695,12 +4695,14 @@ void StatusPanel::on_filament_edit(wxCommandEvent &event)
|
||||
}
|
||||
|
||||
m_filament_setting_dlg->m_is_third = !DevFilaSystem::IsBBL_Filament(tray->tag_uid);
|
||||
// The tray's own temps pre-fill the dialog for every tray type; a BBL tray
|
||||
// additionally carries the SN and brand.
|
||||
temp_max = tray->nozzle_temp_max;
|
||||
temp_min = tray->nozzle_temp_min;
|
||||
if (!m_filament_setting_dlg->m_is_third)
|
||||
{
|
||||
sn_number = tray->uuid;
|
||||
filament = tray->sub_brands;
|
||||
temp_max = tray->nozzle_temp_max;
|
||||
temp_min = tray->nozzle_temp_min;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4761,11 +4763,11 @@ void StatusPanel::on_ext_spool_edit(wxCommandEvent &event)
|
||||
}
|
||||
|
||||
m_filament_setting_dlg->m_is_third = !DevFilaSystem::IsBBL_Filament(obj->vt_slot[nozzle_index].tag_uid);
|
||||
temp_max = obj->vt_slot[nozzle_index].nozzle_temp_max;
|
||||
temp_min = obj->vt_slot[nozzle_index].nozzle_temp_min;
|
||||
if (!m_filament_setting_dlg->m_is_third) {
|
||||
sn_number = obj->vt_slot[nozzle_index].uuid;
|
||||
filament = obj->vt_slot[nozzle_index].sub_brands;
|
||||
temp_max = obj->vt_slot[nozzle_index].nozzle_temp_max;
|
||||
temp_min = obj->vt_slot[nozzle_index].nozzle_temp_min;
|
||||
}
|
||||
|
||||
m_filament_setting_dlg->Move(wxPoint(current_position_x,current_position_y));
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
#include "StoragePanel.h"
|
||||
|
||||
#include "DeviceManager.hpp"
|
||||
#include "GUI_App.hpp"
|
||||
#include "MediaFilePanel.h"
|
||||
#include "OrcaFilesPanel.h"
|
||||
#include "Widgets/StateColor.hpp"
|
||||
|
||||
#include <wx/colour.h>
|
||||
#include <wx/panel.h>
|
||||
#include <wx/event.h>
|
||||
#include <wx/sizer.h>
|
||||
|
||||
namespace Slic3r {
|
||||
namespace GUI {
|
||||
|
||||
StoragePanel::StoragePanel(wxWindow* parent)
|
||||
: wxPanel(parent, wxID_ANY)
|
||||
{
|
||||
const wxColour light_background("#EEEEEE");
|
||||
SetBackgroundColour(wxGetApp().dark_mode() ? StateColor::darkModeColorFor(light_background) : light_background);
|
||||
SetSizer(new wxBoxSizer(wxVERTICAL));
|
||||
}
|
||||
|
||||
MediaFilePanel* StoragePanel::ensureBambuPanel()
|
||||
{
|
||||
if (!m_bambu_panel) {
|
||||
m_bambu_panel = new MediaFilePanel(this);
|
||||
m_bambu_panel->Hide();
|
||||
GetSizer()->Add(m_bambu_panel, 1, wxEXPAND);
|
||||
wxGetApp().UpdateDarkUIWin(m_bambu_panel);
|
||||
}
|
||||
return m_bambu_panel;
|
||||
}
|
||||
|
||||
OrcaFilesPanel* StoragePanel::ensureOrcaPanel()
|
||||
{
|
||||
if (!m_orca_panel) {
|
||||
m_orca_panel = new OrcaFilesPanel(this);
|
||||
m_orca_panel->Hide();
|
||||
GetSizer()->Add(m_orca_panel, 1, wxEXPAND);
|
||||
wxGetApp().UpdateDarkUIWin(m_orca_panel);
|
||||
}
|
||||
return m_orca_panel;
|
||||
}
|
||||
|
||||
void StoragePanel::showChild(Child child)
|
||||
{
|
||||
if (m_bambu_panel)
|
||||
m_bambu_panel->Show(child == Child::Bambu);
|
||||
if (m_orca_panel)
|
||||
m_orca_panel->Show(child == Child::Orca);
|
||||
m_active_child = child;
|
||||
Layout();
|
||||
}
|
||||
|
||||
void StoragePanel::UpdateByObj(MachineObject* obj)
|
||||
{
|
||||
// Agent capability controls the page; Bambu remains the fallback for other printers.
|
||||
Child target = m_active_child;
|
||||
if (obj)
|
||||
target = obj->printer_supports_feature("printer_files") ? Child::Orca : Child::Bambu;
|
||||
else if (target == Child::None)
|
||||
target = Child::Bambu;
|
||||
|
||||
if (target != m_active_child) {
|
||||
if (m_active_child == Child::Bambu && m_bambu_panel)
|
||||
m_bambu_panel->UpdateByObj(nullptr);
|
||||
else if (m_active_child == Child::Orca && m_orca_panel)
|
||||
m_orca_panel->UpdateByObj(nullptr);
|
||||
|
||||
if (target == Child::Bambu)
|
||||
ensureBambuPanel();
|
||||
else
|
||||
ensureOrcaPanel();
|
||||
showChild(target);
|
||||
}
|
||||
|
||||
if (target == Child::Bambu)
|
||||
ensureBambuPanel()->UpdateByObj(obj);
|
||||
else if (target == Child::Orca)
|
||||
ensureOrcaPanel()->UpdateByObj(obj);
|
||||
}
|
||||
|
||||
void StoragePanel::SwitchStorage(bool external)
|
||||
{
|
||||
if (m_bambu_panel)
|
||||
m_bambu_panel->SwitchStorage(external);
|
||||
}
|
||||
|
||||
void StoragePanel::Rescale()
|
||||
{
|
||||
if (m_bambu_panel)
|
||||
m_bambu_panel->Rescale();
|
||||
if (m_orca_panel)
|
||||
m_orca_panel->Rescale();
|
||||
}
|
||||
|
||||
void StoragePanel::on_sys_color_changed()
|
||||
{
|
||||
const wxColour light_background("#EEEEEE");
|
||||
SetBackgroundColour(wxGetApp().dark_mode() ? StateColor::darkModeColorFor(light_background) : light_background);
|
||||
if (m_bambu_panel)
|
||||
wxGetApp().UpdateDarkUIWin(m_bambu_panel);
|
||||
if (m_orca_panel)
|
||||
wxGetApp().UpdateDarkUIWin(m_orca_panel);
|
||||
if (m_orca_panel)
|
||||
m_orca_panel->on_sys_color_changed();
|
||||
Refresh();
|
||||
}
|
||||
|
||||
}}
|
||||
@@ -0,0 +1,35 @@
|
||||
#pragma once
|
||||
|
||||
#include <wx/panel.h>
|
||||
|
||||
namespace Slic3r {
|
||||
class MachineObject;
|
||||
|
||||
namespace GUI {
|
||||
|
||||
class MediaFilePanel;
|
||||
class OrcaFilesPanel;
|
||||
|
||||
class StoragePanel : public wxPanel
|
||||
{
|
||||
public:
|
||||
explicit StoragePanel(wxWindow* parent);
|
||||
|
||||
void UpdateByObj(MachineObject* obj);
|
||||
void SwitchStorage(bool external);
|
||||
void Rescale();
|
||||
void on_sys_color_changed();
|
||||
|
||||
private:
|
||||
enum class Child { None, Bambu, Orca };
|
||||
|
||||
MediaFilePanel* ensureBambuPanel();
|
||||
OrcaFilesPanel* ensureOrcaPanel();
|
||||
void showChild(Child child);
|
||||
|
||||
MediaFilePanel* m_bambu_panel = nullptr;
|
||||
OrcaFilesPanel* m_orca_panel = nullptr;
|
||||
Child m_active_child = Child::None;
|
||||
};
|
||||
|
||||
}}
|
||||
@@ -1425,6 +1425,12 @@ bool SyncAmsInfoDialog::get_ams_mapping_result(std::string &mapping_array_str, s
|
||||
BOOST_LOG_TRIVIAL(error) << "get_ams_mapping_result, plater is nullptr";
|
||||
}
|
||||
|
||||
// mapping_v1_json is built one entry per filament preset, in preset order, so the
|
||||
// array position is the logical filament index the generated G-code toolchange
|
||||
// references (the identifier handed to the Klipper toolchange macro; see
|
||||
// OrcaPrinterAgent::build_filament_mapping and the index-correlation test in
|
||||
// tests/slic3rutils/test_orca_printer_agent.cpp). Never re-densify after dropping
|
||||
// sentinel entries, or a used filament would be aimed at the wrong lane.
|
||||
for (int i = 0; i < wxGetApp().preset_bundle->filament_presets.size(); i++) {
|
||||
int tray_id = -1;
|
||||
json mapping_item_v1;
|
||||
|
||||
@@ -101,6 +101,7 @@ bool AMSinfo::parse_ams_info(MachineObject *obj, DevAms *ams, bool remain_flag,
|
||||
Caninfo info;
|
||||
// tray is exists
|
||||
if (it != ams->GetTrays().end() && it->second->is_exists) {
|
||||
info.is_empty = it->second->is_empty;
|
||||
if (it->second->is_tray_info_ready()) {
|
||||
info.can_id = it->second->id;
|
||||
info.ctype = it->second->ctype;
|
||||
@@ -168,6 +169,7 @@ void AMSinfo::parse_ext_info(MachineObject* obj, DevAmsTray tray) {
|
||||
this->ams_type = AMSModel::EXT_AMS;
|
||||
Caninfo info;
|
||||
info.can_id = std::to_string(0);
|
||||
info.is_empty = tray.is_empty;
|
||||
this->cans.clear();
|
||||
|
||||
if (tray.id == std::to_string(VIRTUAL_TRAY_MAIN_ID))
|
||||
@@ -1130,9 +1132,9 @@ void AMSLib::render_lite_text(wxDC& dc)
|
||||
dc.SetTextForeground(temp_text_colour);
|
||||
|
||||
auto libsize = GetSize();
|
||||
if (m_info.material_state == AMSCanType::AMS_CAN_TYPE_THIRDBRAND
|
||||
if (!m_info.is_empty && (m_info.material_state == AMSCanType::AMS_CAN_TYPE_THIRDBRAND
|
||||
|| m_info.material_state == AMSCanType::AMS_CAN_TYPE_BRAND
|
||||
|| m_info.material_state == AMSCanType::AMS_CAN_TYPE_VIRTUAL) {
|
||||
|| m_info.material_state == AMSCanType::AMS_CAN_TYPE_VIRTUAL)) {
|
||||
|
||||
if (m_info.material_name.empty()) {
|
||||
auto tsize = dc.GetMultiLineTextExtent("?");
|
||||
@@ -1180,8 +1182,8 @@ void AMSLib::render_lite_text(wxDC& dc)
|
||||
}
|
||||
}
|
||||
|
||||
if (m_info.material_state == AMSCanType::AMS_CAN_TYPE_EMPTY) {
|
||||
auto tsize = dc.GetMultiLineTextExtent("/");
|
||||
if (m_info.material_state == AMSCanType::AMS_CAN_TYPE_EMPTY || m_info.is_empty) {
|
||||
auto tsize = dc.GetMultiLineTextExtent(_L("/"));
|
||||
auto pot = wxPoint((libsize.x - tsize.x) / 2 + FromDIP(2), (libsize.y - tsize.y) / 2 + FromDIP(3));
|
||||
dc.DrawText("/", pot);
|
||||
}
|
||||
@@ -1189,8 +1191,12 @@ void AMSLib::render_lite_text(wxDC& dc)
|
||||
|
||||
void AMSLib::render_generic_text(wxDC &dc)
|
||||
{
|
||||
bool show_k_value = true;
|
||||
if (m_info.material_name.empty()) {
|
||||
// K/N is Bambu firmware's flow-dynamics calibration; agents with no printer-side
|
||||
// records (OrcaSonar, Moonraker) must not show a synthesized value.
|
||||
const bool k_supported = !m_obj || m_obj->supports_extrusion_cali();
|
||||
const bool show_kn = m_show_kn && k_supported;
|
||||
bool show_k_value = k_supported;
|
||||
if (!k_supported || m_info.material_name.empty()) {
|
||||
show_k_value = false;
|
||||
}
|
||||
else if (m_info.cali_idx == -1 || (m_obj && (CalibUtils::get_selected_calib_idx(m_obj->pa_calib_tab, m_info.cali_idx) == -1))) {
|
||||
@@ -1231,9 +1237,9 @@ void AMSLib::render_generic_text(wxDC &dc)
|
||||
}
|
||||
|
||||
auto libsize = GetSize();
|
||||
if (m_info.material_state == AMSCanType::AMS_CAN_TYPE_THIRDBRAND
|
||||
if (!m_info.is_empty && (m_info.material_state == AMSCanType::AMS_CAN_TYPE_THIRDBRAND
|
||||
|| m_info.material_state == AMSCanType::AMS_CAN_TYPE_BRAND
|
||||
|| m_info.material_state == AMSCanType::AMS_CAN_TYPE_VIRTUAL) {
|
||||
|| m_info.material_state == AMSCanType::AMS_CAN_TYPE_VIRTUAL)) {
|
||||
|
||||
if (m_info.material_name.empty() /*&& m_info.material_state != AMSCanType::AMS_CAN_TYPE_VIRTUAL*/) {
|
||||
auto tsize = dc.GetMultiLineTextExtent("?");
|
||||
@@ -1271,7 +1277,7 @@ void AMSLib::render_generic_text(wxDC &dc)
|
||||
auto line_top_tsize = dc.GetMultiLineTextExtent(line_top);
|
||||
auto line_bottom_tsize = dc.GetMultiLineTextExtent(line_bottom);
|
||||
|
||||
if (!m_show_kn) {
|
||||
if (!show_kn) {
|
||||
auto pot_top = wxPoint((libsize.x - line_top_tsize.x) / 2, (libsize.y - line_top_tsize.y) / 2 - line_top_tsize.y + FromDIP(6));
|
||||
dc.DrawText(line_top, pot_top);
|
||||
|
||||
@@ -1302,7 +1308,7 @@ void AMSLib::render_generic_text(wxDC &dc)
|
||||
|
||||
//draw k&n
|
||||
if (m_obj && show_k_value) {
|
||||
if (m_show_kn) {
|
||||
if (show_kn) {
|
||||
wxString str_k = wxString::Format("K %1.3f", m_info.k);
|
||||
wxString str_n = wxString::Format("N %1.3f", m_info.n);
|
||||
dc.SetFont(::Label::Body_11);
|
||||
@@ -1313,7 +1319,7 @@ void AMSLib::render_generic_text(wxDC &dc)
|
||||
}
|
||||
}
|
||||
|
||||
if (m_info.material_state == AMSCanType::AMS_CAN_TYPE_EMPTY) {
|
||||
if (m_info.material_state == AMSCanType::AMS_CAN_TYPE_EMPTY || m_info.is_empty) {
|
||||
auto tsize = dc.GetMultiLineTextExtent(_L("Empty"));
|
||||
auto pot = wxPoint((libsize.x - tsize.x) / 2, (libsize.y - tsize.y) / 2 + FromDIP(3));
|
||||
dc.DrawText(_L("Empty"), pot);
|
||||
@@ -2810,7 +2816,7 @@ void AMSPreview::doRender(wxDC &dc)
|
||||
}
|
||||
else {
|
||||
wxRect rect(left, (size.y - AMS_ITEM_CUBE_SIZE.y) / 2, AMS_ITEM_CUBE_SIZE.x, AMS_ITEM_CUBE_SIZE.y);
|
||||
if (iter->material_state == AMSCanType::AMS_CAN_TYPE_EMPTY) {
|
||||
if (iter->material_state == AMSCanType::AMS_CAN_TYPE_EMPTY || iter->is_empty) {
|
||||
dc.SetPen(wxPen(wxColor(0, 0, 0)));
|
||||
dc.DrawLine(rect.GetRight() - FromDIP(1), rect.GetTop() + FromDIP(1), rect.GetLeft() + FromDIP(1), rect.GetBottom() - FromDIP(1));
|
||||
}
|
||||
|
||||
@@ -200,6 +200,7 @@ struct Caninfo
|
||||
wxString material_name;
|
||||
wxColour material_colour = {*wxWHITE};
|
||||
AMSCanType material_state;
|
||||
bool is_empty = false;
|
||||
int ctype=0;
|
||||
int material_remain = 100;
|
||||
int cali_idx = -1;
|
||||
@@ -215,6 +216,7 @@ public:
|
||||
material_name == other.material_name &&
|
||||
material_colour == other.material_colour &&
|
||||
material_state == other.material_state &&
|
||||
is_empty == other.is_empty &&
|
||||
ctype == other.ctype &&
|
||||
material_remain == other.material_remain &&
|
||||
cali_idx == other.cali_idx &&
|
||||
|
||||
@@ -82,6 +82,11 @@ ComboBox::ComboBox(wxWindow *parent,
|
||||
e.SetId(GetId());
|
||||
GetEventHandler()->ProcessEvent(e);
|
||||
});
|
||||
drop.Bind(EVT_DROPDOWN_ITEM_ACTION, [this](wxCommandEvent &e) {
|
||||
e.SetEventObject(this);
|
||||
e.SetId(GetId());
|
||||
GetEventHandler()->ProcessEvent(e);
|
||||
});
|
||||
drop.Bind(EVT_DISMISS, [this](auto &) {
|
||||
drop_down = false;
|
||||
wxCommandEvent e(wxEVT_COMBOBOX_CLOSEUP);
|
||||
@@ -251,7 +256,7 @@ int ComboBox::Append(const wxString &text,
|
||||
int style)
|
||||
{
|
||||
auto valid_bit_map = bitmap.IsOk() ? bitmap : wxNullBitmap;
|
||||
Item item{text, wxEmptyString, valid_bit_map, valid_bit_map, clientData, group_key, group_label};
|
||||
Item item{text, wxEmptyString, valid_bit_map, valid_bit_map, wxNullBitmap, clientData, group_key, group_label};
|
||||
item.style = style;
|
||||
items.push_back(item);
|
||||
SetClientDataType(wxClientData_Void);
|
||||
@@ -297,6 +302,12 @@ void ComboBox::ForceDropdownOpen()
|
||||
}
|
||||
}
|
||||
|
||||
void ComboBox::CloseDropdown()
|
||||
{
|
||||
if (drop.IsShown())
|
||||
drop.DismissAndNotify();
|
||||
}
|
||||
|
||||
unsigned int ComboBox::GetCount() const { return items.size(); }
|
||||
|
||||
void ComboBox::set_replace_text(wxString text, wxString image_name)
|
||||
@@ -350,6 +361,13 @@ void ComboBox::SetItemBitmap(unsigned int n, wxBitmap const &bitmap)
|
||||
drop.Invalidate();
|
||||
}
|
||||
|
||||
void ComboBox::SetItemActionBitmap(unsigned int n, wxBitmap const &bitmap)
|
||||
{
|
||||
if (n >= items.size()) return;
|
||||
items[n].action_icon = bitmap.IsOk() ? bitmap : wxNullBitmap;
|
||||
drop.Invalidate();
|
||||
}
|
||||
|
||||
int ComboBox::DoInsertItems(const wxArrayStringsAdapter &items,
|
||||
unsigned int pos,
|
||||
void ** clientData,
|
||||
@@ -357,7 +375,7 @@ int ComboBox::DoInsertItems(const wxArrayStringsAdapter &items,
|
||||
{
|
||||
if (pos > this->items.size()) return -1;
|
||||
for (int i = 0; i < items.GetCount(); ++i) {
|
||||
Item item { items[i], wxEmptyString, wxNullBitmap, wxNullBitmap, clientData ? clientData[i] : NULL };
|
||||
Item item { items[i], wxEmptyString, wxNullBitmap, wxNullBitmap, wxNullBitmap, clientData ? clientData[i] : NULL };
|
||||
this->items.insert(this->items.begin() + pos, item);
|
||||
++pos;
|
||||
}
|
||||
@@ -419,16 +437,18 @@ void ComboBox::keyDown(wxKeyEvent& event)
|
||||
case WXK_UP:
|
||||
case WXK_DOWN:
|
||||
case WXK_LEFT:
|
||||
case WXK_RIGHT:
|
||||
if ((event.GetKeyCode() == WXK_UP || event.GetKeyCode() == WXK_LEFT) && GetSelection() > 0) {
|
||||
SetSelection(GetSelection() - 1);
|
||||
} else if ((event.GetKeyCode() == WXK_DOWN || event.GetKeyCode() == WXK_RIGHT) && GetSelection() + 1 < items.size()) {
|
||||
SetSelection(GetSelection() + 1);
|
||||
} else {
|
||||
case WXK_RIGHT: {
|
||||
// Step over disabled rows: the mouse cannot select them either.
|
||||
const int step = (event.GetKeyCode() == WXK_UP || event.GetKeyCode() == WXK_LEFT) ? -1 : 1;
|
||||
int n = GetSelection() + step;
|
||||
while (n >= 0 && n < int(items.size()) && (items[n].style & DD_ITEM_STYLE_DISABLED))
|
||||
n += step;
|
||||
if (n < 0 || n >= int(items.size()))
|
||||
break;
|
||||
}
|
||||
SetSelection(n);
|
||||
sendComboBoxEvent();
|
||||
break;
|
||||
}
|
||||
case WXK_TAB:
|
||||
HandleAsNavigationKey(event);
|
||||
break;
|
||||
|
||||
@@ -96,10 +96,12 @@ public:
|
||||
|
||||
wxBitmap GetItemBitmap(unsigned int n);
|
||||
void SetItemBitmap(unsigned int n, wxBitmap const &bitmap);
|
||||
void SetItemActionBitmap(unsigned int n, wxBitmap const &bitmap);
|
||||
bool is_drop_down(){return drop_down;}
|
||||
void DeleteOneItem(unsigned int pos) { DoDeleteOneItem(pos); }
|
||||
|
||||
void ForceDropdownOpen();
|
||||
void CloseDropdown();
|
||||
|
||||
protected:
|
||||
virtual int DoInsertItems(const wxArrayStringsAdapter &items,
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
#include "slic3r/GUI/wxExtensions.hpp"
|
||||
|
||||
wxDEFINE_EVENT(EVT_DISMISS, wxCommandEvent);
|
||||
wxDEFINE_EVENT(EVT_DROPDOWN_ITEM_ACTION, wxCommandEvent);
|
||||
|
||||
BEGIN_EVENT_TABLE(DropDown, PopupWindow)
|
||||
|
||||
@@ -242,6 +243,19 @@ static wxSize GetBmpSize(wxBitmap & bmp)
|
||||
#endif
|
||||
}
|
||||
|
||||
// The x-range [first, second) of the column at the right of the rows that holds the items' action
|
||||
// icons, empty when no item has one. render() draws the icons in it and mouseReleased() hit-tests
|
||||
// clicks against it, so the drawn icon and its clickable area always match.
|
||||
std::pair<int, int> DropDown::actionColumn() const
|
||||
{
|
||||
if (actionIconWidth <= 0)
|
||||
return {0, 0};
|
||||
int right = rowSize.x;
|
||||
if (rowSize.y * count > GetSize().y) // position bar
|
||||
right -= 6;
|
||||
return {right - actionIconWidth - FromDIP(16), right};
|
||||
}
|
||||
|
||||
static void _DrawSplitItem(const wxWindow* w, wxDC& dc, wxString split_text, wxPoint start_pt, int item_width, int item_height)
|
||||
{
|
||||
// save dc
|
||||
@@ -371,6 +385,10 @@ void DropDown::render(wxDC &dc)
|
||||
rcContent.width -= szBmp.x + 5;
|
||||
}
|
||||
|
||||
const auto action_column = actionColumn();
|
||||
if (action_column.second > action_column.first)
|
||||
rcContent.width = action_column.first - rcContent.x;
|
||||
|
||||
std::set<wxString> groups;
|
||||
// draw texts & icons
|
||||
int index = 0;
|
||||
@@ -434,13 +452,24 @@ void DropDown::render(wxDC &dc)
|
||||
auto text = group.IsEmpty()
|
||||
? (item.group_key.IsEmpty() ? item.text : item.group_label)
|
||||
: item.text;
|
||||
if (item.action_icon.IsOk()) {
|
||||
auto action_size = GetBmpSize(item.action_icon);
|
||||
wxPoint action_pt(action_column.second - action_size.x - FromDIP(8),
|
||||
rcContent.y + (rcContent.height - action_size.y) / 2);
|
||||
dc.DrawBitmap(item.action_icon, action_pt);
|
||||
}
|
||||
|
||||
if (!text_off && !text.IsEmpty()) {
|
||||
wxSize tSize = dc.GetMultiLineTextExtent(text);
|
||||
if (pt.x + tSize.x > rcContent.GetRight()) {
|
||||
if (is_hover && item.tip.IsEmpty())
|
||||
SetToolTip(text);
|
||||
text = wxControl::Ellipsize(text, dc, wxELLIPSIZE_END,
|
||||
rcContent.GetRight() - pt.x);
|
||||
int max_text_width = rcContent.GetRight() - pt.x;
|
||||
if (max_text_width <= 0) {
|
||||
text.clear();
|
||||
} else {
|
||||
if (is_hover && item.tip.IsEmpty())
|
||||
SetToolTip(text);
|
||||
text = wxControl::Ellipsize(text, dc, wxELLIPSIZE_END, max_text_width);
|
||||
}
|
||||
}
|
||||
pt.y += (rcContent.height - textSize.y) / 2;
|
||||
dc.SetFont(GetFont());
|
||||
@@ -529,6 +558,7 @@ void DropDown::messureSize()
|
||||
if (!need_sync) return;
|
||||
textSize = wxSize();
|
||||
iconSize = wxSize();
|
||||
actionIconWidth = 0;
|
||||
count = 0;
|
||||
wxClientDC dc(GetParent() ? GetParent() : this);
|
||||
dc.SetFont(GetFont());
|
||||
@@ -565,8 +595,13 @@ void DropDown::messureSize()
|
||||
size1.x += size2.x + (text_off ? 0 : 5);
|
||||
}
|
||||
}
|
||||
if (item.action_icon.IsOk())
|
||||
actionIconWidth = std::max(actionIconWidth, GetBmpSize(item.action_icon).x);
|
||||
if (size1.x > textSize.x) textSize = size1;
|
||||
}
|
||||
// Every row leaves room for the action column, as render() and actionColumn() lay it out.
|
||||
if (actionIconWidth > 0)
|
||||
textSize.x += actionIconWidth + FromDIP(16);
|
||||
if (!align_icon) iconSize.x = 0;
|
||||
wxSize szContent = textSize;
|
||||
if (szContent.x < FromDIP(120) && !use_content_width)
|
||||
@@ -636,6 +671,11 @@ void DropDown::messureSize()
|
||||
e.SetId(GetId());
|
||||
GetEventHandler()->ProcessEvent(e);
|
||||
});
|
||||
subDropDown->Bind(EVT_DROPDOWN_ITEM_ACTION, [this](wxCommandEvent &e) {
|
||||
e.SetEventObject(this);
|
||||
e.SetId(GetId());
|
||||
GetEventHandler()->ProcessEvent(e);
|
||||
});
|
||||
}
|
||||
need_sync = false;
|
||||
}
|
||||
@@ -714,6 +754,15 @@ void DropDown::mouseReleased(wxMouseEvent& event)
|
||||
if (hover_item < 0)
|
||||
return;
|
||||
if (hover_item >= 0 && (subDropDown == nullptr || subDropDown->group.empty())) { // not moved
|
||||
int index = hoverIndex();
|
||||
if (index >= 0 && items[index].action_icon.IsOk()) {
|
||||
const auto action_column = actionColumn();
|
||||
if (event.GetX() >= action_column.first && event.GetX() < action_column.second) {
|
||||
sendDropDownActionEvent(index);
|
||||
paintNow();
|
||||
return;
|
||||
}
|
||||
}
|
||||
sendDropDownEvent();
|
||||
if (mainDropDown)
|
||||
mainDropDown->hover_item = -1; // To Dismiss mainDropDown
|
||||
@@ -830,6 +879,17 @@ void DropDown::sendDropDownEvent()
|
||||
GetEventHandler()->ProcessEvent(event);
|
||||
}
|
||||
|
||||
void DropDown::sendDropDownActionEvent(int index)
|
||||
{
|
||||
if (index < 0 || index >= int(items.size()) || (items[index].style & DD_ITEM_STYLE_DISABLED))
|
||||
return;
|
||||
wxCommandEvent event(EVT_DROPDOWN_ITEM_ACTION, GetId());
|
||||
event.SetEventObject(this);
|
||||
event.SetInt(index);
|
||||
event.SetString(items[index].text);
|
||||
GetEventHandler()->ProcessEvent(event);
|
||||
}
|
||||
|
||||
void DropDown::Dismiss()
|
||||
{
|
||||
if (subDropDown && subDropDown->IsShown())
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <wx/event.h>
|
||||
#include <vector>
|
||||
#include <cstddef>
|
||||
#include <utility>
|
||||
#include <wx/gdicmn.h>
|
||||
#include "slic3r/GUI/Widgets/StateColor.hpp"
|
||||
#include <boost/date_time/posix_time/ptime.hpp>
|
||||
@@ -27,6 +28,7 @@ class wxWindow;
|
||||
#define DD_ITEM_STYLE_DIMMED 0x0004 // gray text, but still selectable
|
||||
|
||||
wxDECLARE_EVENT(EVT_DISMISS, wxCommandEvent);
|
||||
wxDECLARE_EVENT(EVT_DROPDOWN_ITEM_ACTION, wxCommandEvent);
|
||||
|
||||
class DropDown : public PopupWindow
|
||||
{
|
||||
@@ -37,6 +39,7 @@ public:
|
||||
wxString text_static_tips;// display static tips for TextInput.eg.PrinterInfoBox
|
||||
wxBitmap icon;
|
||||
wxBitmap icon_textctrl;// display icon for TextInput.eg.PrinterInfoBox
|
||||
wxBitmap action_icon;
|
||||
void * data{nullptr};
|
||||
wxString group_key{};
|
||||
wxString group_label{};
|
||||
@@ -65,6 +68,7 @@ private:
|
||||
|
||||
wxSize textSize;
|
||||
wxSize iconSize;
|
||||
int actionIconWidth = 0;
|
||||
wxSize rowSize;
|
||||
|
||||
StateHandler state_handler;
|
||||
@@ -135,6 +139,7 @@ private:
|
||||
int hoverIndex();
|
||||
|
||||
int selectedItem();
|
||||
std::pair<int, int> actionColumn() const;
|
||||
|
||||
friend class ComboBox;
|
||||
void messureSize();
|
||||
@@ -148,6 +153,7 @@ private:
|
||||
void mouseWheelMoved(wxMouseEvent &event);
|
||||
|
||||
void sendDropDownEvent();
|
||||
void sendDropDownActionEvent(int index);
|
||||
|
||||
|
||||
DECLARE_EVENT_TABLE()
|
||||
|
||||
@@ -31,8 +31,23 @@
|
||||
#include <curl/curl.h>
|
||||
#include <utility>
|
||||
|
||||
#ifdef OPENSSL_CERT_OVERRIDE
|
||||
#include <openssl/err.h>
|
||||
#include <openssl/ssl.h>
|
||||
#include <openssl/x509.h>
|
||||
#include <openssl/x509err.h>
|
||||
|
||||
#ifdef __APPLE__
|
||||
# include <unistd.h>
|
||||
#endif
|
||||
|
||||
#ifdef _WIN32
|
||||
# ifndef NOMINMAX
|
||||
# define NOMINMAX
|
||||
# endif
|
||||
# include <windows.h>
|
||||
# include <wincrypt.h>
|
||||
// wincrypt.h uses this token for a certificate-name property identifier.
|
||||
# undef X509_NAME
|
||||
#endif
|
||||
|
||||
namespace fs = boost::filesystem;
|
||||
@@ -955,6 +970,50 @@ std::string Http::tls_system_cert_store()
|
||||
return {};
|
||||
}
|
||||
|
||||
void Http::add_platform_root_certificates(SSL_CTX* ssl_context)
|
||||
{
|
||||
#ifdef _WIN32
|
||||
X509_STORE* openssl_store = SSL_CTX_get_cert_store(ssl_context);
|
||||
if (!openssl_store)
|
||||
throw std::runtime_error("unable to get OpenSSL certificate store");
|
||||
|
||||
const auto load_store = [&](DWORD location) {
|
||||
HCERTSTORE windows_store = CertOpenStore(CERT_STORE_PROV_SYSTEM_W, 0, 0,
|
||||
location | CERT_STORE_OPEN_EXISTING_FLAG | CERT_STORE_READONLY_FLAG,
|
||||
L"ROOT");
|
||||
if (!windows_store)
|
||||
return;
|
||||
|
||||
PCCERT_CONTEXT windows_certificate = nullptr;
|
||||
while ((windows_certificate = CertEnumCertificatesInStore(windows_store, windows_certificate)) != nullptr) {
|
||||
const unsigned char* encoded = windows_certificate->pbCertEncoded;
|
||||
X509* certificate = d2i_X509(nullptr, &encoded, static_cast<long>(windows_certificate->cbCertEncoded));
|
||||
if (!certificate) {
|
||||
ERR_clear_error();
|
||||
continue;
|
||||
}
|
||||
|
||||
ERR_clear_error();
|
||||
if (X509_STORE_add_cert(openssl_store, certificate) != 1)
|
||||
ERR_clear_error();
|
||||
X509_free(certificate);
|
||||
}
|
||||
|
||||
CertCloseStore(windows_store, 0);
|
||||
};
|
||||
|
||||
load_store(CERT_SYSTEM_STORE_CURRENT_USER);
|
||||
load_store(CERT_SYSTEM_STORE_LOCAL_MACHINE);
|
||||
#elif defined(__APPLE__)
|
||||
// macOS ships its system trust roots as a PEM bundle; the bundled OpenSSL's
|
||||
// compiled-in default path is the build directory, so load this explicitly.
|
||||
if (ssl_context && ::access("/etc/ssl/cert.pem", R_OK) == 0)
|
||||
SSL_CTX_load_verify_locations(ssl_context, "/etc/ssl/cert.pem", nullptr);
|
||||
#else
|
||||
(void)ssl_context;
|
||||
#endif
|
||||
}
|
||||
|
||||
std::string Http::url_encode(const std::string &str)
|
||||
{
|
||||
::CURL *curl = ::curl_easy_init();
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
#include "libslic3r/Exception.hpp"
|
||||
#include "libslic3r_version.h"
|
||||
|
||||
typedef struct ssl_ctx_st SSL_CTX;
|
||||
|
||||
#define MAX_SIZE_TO_FILE 3*1024
|
||||
|
||||
namespace Slic3r {
|
||||
@@ -200,6 +202,10 @@ public:
|
||||
// Return empty string on success or error message on fail.
|
||||
static std::string tls_global_init();
|
||||
static std::string tls_system_cert_store();
|
||||
// Add platform root certificates to a standalone OpenSSL context. This
|
||||
// supplements set_default_verify_paths() on platforms where OpenSSL does
|
||||
// not use the native certificate store.
|
||||
static void add_platform_root_certificates(SSL_CTX* ssl_context);
|
||||
|
||||
// converts the given string to an url_encoded_string
|
||||
static std::string url_encode(const std::string &str);
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
#include <functional>
|
||||
#include <cstdint>
|
||||
#include <cmath>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <boost/format.hpp>
|
||||
@@ -68,6 +69,34 @@ enum class CameraStreamMode {
|
||||
http_snapshot // HTTP endpoint returning one image per request
|
||||
};
|
||||
|
||||
struct PrinterFileEntry {
|
||||
std::string path; // gcodes-root-relative, e.g. "sub/foo.gcode"
|
||||
std::string name; // basename for display
|
||||
std::uint64_t size = 0; // bytes
|
||||
std::int64_t modified = 0; // unix seconds
|
||||
};
|
||||
|
||||
// Moonraker per-file metadata: print time and filament usage. Missing fields are 0.
|
||||
struct PrinterFileMetadata {
|
||||
int estimated_time = 0; // seconds
|
||||
double filament_total = 0; // millimetres
|
||||
double filament_weight = 0; // grams
|
||||
};
|
||||
|
||||
// Async listing of the printer's G-code files. The return value reports whether
|
||||
// the request was dispatched; `callback` is invoked once with the result.
|
||||
using PrinterFileListFn = std::function<void(int result, std::vector<PrinterFileEntry> files)>;
|
||||
|
||||
// Async fetch of one G-code file's embedded thumbnail. `image` is the raw image
|
||||
// bytes (empty when the file has no thumbnail); result reports dispatch/transfer.
|
||||
using PrinterFileThumbnailFn = std::function<void(int result, std::string image)>;
|
||||
|
||||
// Async deletion of one printer G-code file. `result` reports dispatch and outcome.
|
||||
using PrinterFileDeleteFn = std::function<void(int result)>;
|
||||
|
||||
// Async fetch of one G-code file's Moonraker metadata. `meta` fields default to 0.
|
||||
using PrinterFileMetadataFn = std::function<void(int result, PrinterFileMetadata meta)>;
|
||||
|
||||
/**
|
||||
* IPrinterAgent - Interface for printer operations.
|
||||
*
|
||||
@@ -109,6 +138,16 @@ public:
|
||||
*/
|
||||
virtual int send_message(std::string dev_id, std::string json_str, int qos, int flag) = 0;
|
||||
|
||||
// Capability queries are per-device because one agent may own many printers.
|
||||
// Legacy agents keep their existing behavior unless they override these.
|
||||
virtual bool supports_command(const std::string& /*dev_id*/, const std::string& /*command*/) const { return true; }
|
||||
virtual bool supports_feature(const std::string& /*dev_id*/, const std::string& /*feature*/) const { return false; }
|
||||
|
||||
// Whether this agent serializes AMS lane selection into print.gcode_file's
|
||||
// per-print filament_mapping field. A dialect question, not a capability one:
|
||||
// the no-AMS external-spool normalization runs before capabilities are known.
|
||||
virtual bool uses_filament_mapping() const { return false; }
|
||||
|
||||
// why: gcode is firmware dialect, not a waist concept - commands whose body is Bambu-dialect
|
||||
// gcode live on the agent that speaks it; the default is an honest refusal that MachineObject's
|
||||
// publish funnel turns into a dialog.
|
||||
@@ -316,6 +355,48 @@ public:
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* List the printer's G-code files, delivered asynchronously via callback.
|
||||
* Agents without a file listing report ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED.
|
||||
*/
|
||||
virtual int list_printer_files(const std::string& dev_id, PrinterFileListFn callback)
|
||||
{
|
||||
(void) dev_id;
|
||||
(void) callback;
|
||||
return ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch one G-code file's embedded thumbnail, delivered asynchronously via
|
||||
* callback. Agents without thumbnail support report ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED.
|
||||
*/
|
||||
virtual int get_printer_file_thumbnail(const std::string& dev_id, const std::string& path, PrinterFileThumbnailFn callback)
|
||||
{
|
||||
(void) dev_id; (void) path; (void) callback;
|
||||
return ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete one G-code file from the printer, delivered asynchronously via
|
||||
* callback. Agents without file deletion report ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED.
|
||||
*/
|
||||
virtual int delete_printer_file(const std::string& dev_id, const std::string& path, PrinterFileDeleteFn callback)
|
||||
{
|
||||
(void) dev_id; (void) path; (void) callback;
|
||||
return ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch one G-code file's Moonraker metadata (print time and filament usage),
|
||||
* delivered asynchronously via callback. Agents without metadata support
|
||||
* report ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED.
|
||||
*/
|
||||
virtual int get_printer_file_metadata(const std::string& dev_id, const std::string& path, PrinterFileMetadataFn callback)
|
||||
{
|
||||
(void) dev_id; (void) path; (void) callback;
|
||||
return ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register callback for fatal HTTP errors.
|
||||
*/
|
||||
|
||||
@@ -806,6 +806,36 @@ int NetworkAgent::send_message(std::string dev_id, std::string json_str, int qos
|
||||
return -1;
|
||||
}
|
||||
|
||||
bool NetworkAgent::owns_agent(const std::string& expected_agent_id) const
|
||||
{
|
||||
if (!m_printer_agent)
|
||||
return false;
|
||||
return expected_agent_id.empty() || m_printer_agent->get_agent_info().id == expected_agent_id;
|
||||
}
|
||||
|
||||
bool NetworkAgent::supports_command(const std::string& expected_agent_id, const std::string& dev_id,
|
||||
const std::string& command) const
|
||||
{
|
||||
if (!owns_agent(expected_agent_id))
|
||||
return false;
|
||||
return m_printer_agent->supports_command(dev_id, command);
|
||||
}
|
||||
|
||||
bool NetworkAgent::supports_feature(const std::string& expected_agent_id, const std::string& dev_id,
|
||||
const std::string& feature) const
|
||||
{
|
||||
if (!owns_agent(expected_agent_id))
|
||||
return false;
|
||||
return m_printer_agent->supports_feature(dev_id, feature);
|
||||
}
|
||||
|
||||
bool NetworkAgent::uses_filament_mapping(const std::string& expected_agent_id) const
|
||||
{
|
||||
if (!owns_agent(expected_agent_id))
|
||||
return false;
|
||||
return m_printer_agent->uses_filament_mapping();
|
||||
}
|
||||
|
||||
int NetworkAgent::command_ams_refresh_rfid(std::string dev_id, int ams_id, int slot_id, int sequence_id, bool lan_mode)
|
||||
{
|
||||
if (m_printer_agent)
|
||||
@@ -1106,4 +1136,32 @@ int NetworkAgent::get_hms_snapshot(std::string dev_id, std::string file_name, st
|
||||
return -1;
|
||||
}
|
||||
|
||||
int NetworkAgent::list_printer_files(const std::string& dev_id, PrinterFileListFn callback)
|
||||
{
|
||||
if (m_printer_agent)
|
||||
return m_printer_agent->list_printer_files(dev_id, callback);
|
||||
return ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
int NetworkAgent::get_printer_file_thumbnail(const std::string& dev_id, const std::string& path, PrinterFileThumbnailFn callback)
|
||||
{
|
||||
if (m_printer_agent)
|
||||
return m_printer_agent->get_printer_file_thumbnail(dev_id, path, callback);
|
||||
return ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
int NetworkAgent::delete_printer_file(const std::string& dev_id, const std::string& path, PrinterFileDeleteFn callback)
|
||||
{
|
||||
if (m_printer_agent)
|
||||
return m_printer_agent->delete_printer_file(dev_id, path, callback);
|
||||
return ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
int NetworkAgent::get_printer_file_metadata(const std::string& dev_id, const std::string& path, PrinterFileMetadataFn callback)
|
||||
{
|
||||
if (m_printer_agent)
|
||||
return m_printer_agent->get_printer_file_metadata(dev_id, path, callback);
|
||||
return ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
@@ -153,6 +153,10 @@ public:
|
||||
int set_on_local_message_fn(OnMessageFn fn);
|
||||
int set_server_callback(OnServerErrFn fn);
|
||||
int send_message(std::string dev_id, std::string json_str, int qos, int flag);
|
||||
bool owns_agent(const std::string& expected_agent_id) const;
|
||||
bool supports_command(const std::string& expected_agent_id, const std::string& dev_id, const std::string& command) const;
|
||||
bool supports_feature(const std::string& expected_agent_id, const std::string& dev_id, const std::string& feature) const;
|
||||
bool uses_filament_mapping(const std::string& expected_agent_id) const;
|
||||
int command_ams_refresh_rfid(std::string dev_id, int ams_id, int slot_id, int sequence_id, bool lan_mode);
|
||||
int command_ams_calibrate(std::string dev_id, int ams_id, int sequence_id, bool lan_mode);
|
||||
int command_ams_select_tray(std::string dev_id, std::string tray_id, int sequence_id, bool lan_mode);
|
||||
@@ -194,6 +198,10 @@ public:
|
||||
std::string from_orca_filament_id(const std::string& orca_filament_id) const;
|
||||
int request_bind_ticket(std::string* ticket);
|
||||
int get_hms_snapshot(std::string dev_id, std::string file_name, std::function<void(std::string, int)> callback);
|
||||
int list_printer_files(const std::string& dev_id, PrinterFileListFn callback);
|
||||
int get_printer_file_thumbnail(const std::string& dev_id, const std::string& path, PrinterFileThumbnailFn callback);
|
||||
int delete_printer_file(const std::string& dev_id, const std::string& path, PrinterFileDeleteFn callback);
|
||||
int get_printer_file_metadata(const std::string& dev_id, const std::string& path, PrinterFileMetadataFn callback);
|
||||
|
||||
private:
|
||||
struct PrinterCallbacks {
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
|
||||
namespace Slic3r {
|
||||
static constexpr char ORCA_PRINTER_AGENT_ID[] = "orca";
|
||||
static constexpr char BBL_PRINTER_AGENT_ID[] = "bbl";
|
||||
static constexpr char BBL_PRINTER_AGENT_ID[] = "bbl";
|
||||
|
||||
// Factory function type for creating printer agents
|
||||
using PrinterAgentFactory =
|
||||
@@ -26,17 +26,20 @@ using PrinterAgentFactory =
|
||||
// Information about a registered printer agent
|
||||
struct PrinterAgentInfo
|
||||
{
|
||||
std::string id; // Registry/config key, e.g. "orca" or a plugin AgentInfo::id
|
||||
std::string display_name; // e.g., "Orca Native", "Bambu Lab"
|
||||
std::string plugin_identifier; // Empty for built-ins, otherwise <plugin_key>;<uuid>;<capability_name>
|
||||
PrinterAgentFactory factory; // Function to create the agent
|
||||
std::string id; // Registry/config key, e.g. "orca" or a plugin AgentInfo::id
|
||||
std::string display_name; // e.g., "Orca Native", "Bambu Lab"
|
||||
std::string plugin_identifier; // Empty for built-ins, otherwise <plugin_key>;<uuid>;<capability_name>
|
||||
PrinterAgentFactory factory; // Function to create the agent
|
||||
|
||||
bool is_plugin() const { return !plugin_identifier.empty(); }
|
||||
PrinterAgentInfo(const std::string& id_, const std::string& display_name_, PrinterAgentFactory factory_)
|
||||
: id(id_), display_name(display_name_), factory(std::move(factory_))
|
||||
{}
|
||||
|
||||
PrinterAgentInfo(const std::string& id_, const std::string& display_name_, const std::string& plugin_identifier, PrinterAgentFactory factory_)
|
||||
PrinterAgentInfo(const std::string& id_,
|
||||
const std::string& display_name_,
|
||||
const std::string& plugin_identifier,
|
||||
PrinterAgentFactory factory_)
|
||||
: id(id_), display_name(display_name_), plugin_identifier(plugin_identifier), factory(std::move(factory_))
|
||||
{}
|
||||
};
|
||||
@@ -118,9 +121,9 @@ public:
|
||||
* @param log_dir Directory for log files
|
||||
* @return Shared pointer to IPrinterAgent, or nullptr if ID not found
|
||||
*/
|
||||
static std::shared_ptr<IPrinterAgent> create_printer_agent_by_id(const std::string& id,
|
||||
static std::shared_ptr<IPrinterAgent> create_printer_agent_by_id(const std::string& id,
|
||||
std::shared_ptr<ICloudServiceAgent> cloud_agent,
|
||||
const std::string& log_dir);
|
||||
const std::string& log_dir);
|
||||
|
||||
/**
|
||||
* Clear the printer agent cache.
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
#include "OrcaCloudServiceAgent.hpp"
|
||||
#include "CloudProvider.hpp"
|
||||
#include "Http.hpp"
|
||||
#include "ICameraSignalingChannel.hpp"
|
||||
#include "OrcaCloudSignalingChannel.hpp"
|
||||
#include "OrcaMqttConnection.hpp"
|
||||
#include "bambu_networking.hpp"
|
||||
#include "ICloudServiceAgent.hpp"
|
||||
#include "libslic3r/Utils.hpp"
|
||||
@@ -12,8 +15,11 @@
|
||||
#include <boost/asio/ip/tcp.hpp>
|
||||
#include <boost/asio/io_context.hpp>
|
||||
#include <boost/beast/core/detail/base64.hpp>
|
||||
#include <boost/beast/core.hpp>
|
||||
#include <boost/beast/websocket.hpp>
|
||||
#include <boost/filesystem.hpp>
|
||||
#include <boost/filesystem/operations.hpp>
|
||||
#include <boost/filesystem/path.hpp>
|
||||
#include <boost/log/trivial.hpp>
|
||||
#include <boost/uuid/name_generator_sha1.hpp>
|
||||
#include <boost/uuid/uuid.hpp>
|
||||
@@ -37,14 +43,18 @@
|
||||
#include <openssl/hmac.h>
|
||||
#include <openssl/rand.h>
|
||||
#include <openssl/sha.h>
|
||||
#include <openssl/ssl.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <condition_variable>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <fstream>
|
||||
#include <iomanip>
|
||||
#include <optional>
|
||||
#include <random>
|
||||
#include <set>
|
||||
#include <sstream>
|
||||
|
||||
#include <string>
|
||||
@@ -513,6 +523,7 @@ OrcaCloudServiceAgent::OrcaCloudServiceAgent(std::string log_dir)
|
||||
, api_base_url(ORCA_DEFAULT_API_URL)
|
||||
, auth_base_url(ORCA_DEFAULT_AUTH_URL)
|
||||
, cloud_base_url(ORCA_DEFAULT_CLOUD_URL)
|
||||
, mqtt_connection(std::make_unique<OrcaMqttConnection>())
|
||||
{
|
||||
auth_headers["apikey"] = ORCA_DEFAULT_PUB_KEY;
|
||||
pkce_bundle.loopback_port = choose_loopback_port();
|
||||
@@ -523,6 +534,8 @@ OrcaCloudServiceAgent::OrcaCloudServiceAgent(std::string log_dir)
|
||||
|
||||
OrcaCloudServiceAgent::~OrcaCloudServiceAgent()
|
||||
{
|
||||
if (mqtt_connection)
|
||||
mqtt_connection->stop();
|
||||
stop_health_check();
|
||||
if (refresh_thread.joinable()) {
|
||||
refresh_thread.join();
|
||||
@@ -1000,6 +1013,8 @@ bool OrcaCloudServiceAgent::run_health_check(const std::atomic_bool* cancel)
|
||||
return false;
|
||||
|
||||
const bool connected = res.success;
|
||||
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: cloud health connected=" << connected << " status=" << res.status
|
||||
<< " response_bytes=" << res.body.size();
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> lock(state_mutex);
|
||||
is_connected = connected;
|
||||
@@ -1012,11 +1027,32 @@ bool OrcaCloudServiceAgent::run_health_check(const std::atomic_bool* cancel)
|
||||
|
||||
int OrcaCloudServiceAgent::connect_server()
|
||||
{
|
||||
const bool logged_in = is_user_login();
|
||||
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: OrcaCloudServiceAgent::connect_server logged_in=" << logged_in
|
||||
<< " api_base_url=" << api_base_url;
|
||||
if (!logged_in) {
|
||||
if (mqtt_connection)
|
||||
mqtt_connection->stop();
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> lock(state_mutex);
|
||||
is_connected = false;
|
||||
}
|
||||
BOOST_LOG_TRIVIAL(warning) << "OrcaCloudServiceAgent: connect_server requires a logged-in user";
|
||||
invoke_server_connected_callback(-1, 401);
|
||||
return BAMBU_NETWORK_ERR_INVALID_HANDLE;
|
||||
}
|
||||
|
||||
// connect_server() remains a REST health probe. The long-lived fleet MQTT socket
|
||||
// is started lazily by set_user_selected_machine -> configure_selected_printer_mqtt;
|
||||
// subscriptions queued before that point are replayed when it starts.
|
||||
return run_health_check(nullptr) ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_CONNECTION_TO_SERVER_FAILED;
|
||||
}
|
||||
|
||||
bool OrcaCloudServiceAgent::is_server_connected()
|
||||
{
|
||||
// The REST health probe is the signal; the per-printer MQTT socket does not gate
|
||||
// whole-cloud connectivity (one printer reconnecting must not report the whole
|
||||
// cloud as lost).
|
||||
std::lock_guard<std::recursive_mutex> lock(state_mutex);
|
||||
return is_connected;
|
||||
}
|
||||
@@ -1074,13 +1110,238 @@ int OrcaCloudServiceAgent::stop_subscribe(std::string module)
|
||||
|
||||
int OrcaCloudServiceAgent::add_subscribe(std::vector<std::string> dev_list)
|
||||
{
|
||||
(void) dev_list;
|
||||
return BAMBU_NETWORK_SUCCESS;
|
||||
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: OrcaCloudServiceAgent::add_subscribe count=" << dev_list.size()
|
||||
<< " logged_in=" << is_user_login() << " mqtt_connection=" << (mqtt_connection ? "set" : "null");
|
||||
if (!is_user_login() || !mqtt_connection) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: add_subscribe rejected because cloud is not ready";
|
||||
return BAMBU_NETWORK_ERR_INVALID_HANDLE;
|
||||
}
|
||||
bool queued = true;
|
||||
for (const std::string& dev_id : dev_list)
|
||||
queued = mqtt_connection->subscribe(dev_id) && queued;
|
||||
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: add_subscribe queued=" << queued;
|
||||
return queued ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_CONNECT_FAILED;
|
||||
}
|
||||
|
||||
int OrcaCloudServiceAgent::del_subscribe(std::vector<std::string> dev_list)
|
||||
{
|
||||
(void) dev_list;
|
||||
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: OrcaCloudServiceAgent::del_subscribe count=" << dev_list.size()
|
||||
<< " logged_in=" << is_user_login() << " mqtt_connection=" << (mqtt_connection ? "set" : "null");
|
||||
if (!is_user_login() || !mqtt_connection) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: del_subscribe rejected because cloud is not ready";
|
||||
return BAMBU_NETWORK_ERR_INVALID_HANDLE;
|
||||
}
|
||||
bool queued = true;
|
||||
for (const std::string& dev_id : dev_list)
|
||||
queued = mqtt_connection->unsubscribe(dev_id) && queued;
|
||||
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: del_subscribe queued=" << queued;
|
||||
return queued ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_CONNECT_FAILED;
|
||||
}
|
||||
|
||||
int OrcaCloudServiceAgent::configure_selected_printer_mqtt(const std::string& dev_id,
|
||||
OrcaMqttConnection::StateHandler state_handler)
|
||||
{
|
||||
(void) dev_id;
|
||||
if (!ensure_token_fresh("configure_selected_printer_mqtt"))
|
||||
{
|
||||
BOOST_LOG_TRIVIAL(warning) << "ensure_token_fresh returned false";
|
||||
return BAMBU_NETWORK_ERR_CONNECTION_TO_SERVER_FAILED;
|
||||
}
|
||||
|
||||
OrcaMqttConnection::Config cfg;
|
||||
cfg.url = "wss://" + api_base_url + "/api/v1/printers/mqtt";
|
||||
cfg.use_tls = true;
|
||||
cfg.bearer_provider = [this] { return get_access_token(); };
|
||||
cfg.client_id = "OrcaSlicer";
|
||||
cfg.keepalive_seconds = 300;
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_selected_url_mutex);
|
||||
m_selected_printer_mqtt_url = cfg.url;
|
||||
}
|
||||
|
||||
if (mqtt_connection->is_running()) {
|
||||
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: fleet MQTT connection already running";
|
||||
return BAMBU_NETWORK_SUCCESS;
|
||||
}
|
||||
|
||||
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: configuring fleet MQTT endpoint=" << cfg.url;
|
||||
|
||||
// NOTE: no lock is held across start() — it blocks for the whole initial connect
|
||||
// attempt (up to ~10s), and the message handler below re-enters callback_mutex on
|
||||
// the MQTT worker thread.
|
||||
const bool ok = mqtt_connection->start(
|
||||
cfg,
|
||||
[this](const std::string& id, const std::string& payload) { deliver_cloud_message(id, payload); },
|
||||
std::move(state_handler));
|
||||
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: fleet MQTT start returned=" << ok;
|
||||
return ok ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_CONNECTION_TO_SERVER_FAILED;
|
||||
}
|
||||
|
||||
void OrcaCloudServiceAgent::teardown_selected_printer_mqtt()
|
||||
{
|
||||
if (mqtt_connection) {
|
||||
mqtt_connection->stop();
|
||||
// The connection object is reused for the next printer; drop this printer's
|
||||
// report topic so its 1:1 socket does not re-subscribe the previous device.
|
||||
mqtt_connection->clear_subscriptions();
|
||||
}
|
||||
std::lock_guard<std::mutex> lock(m_selected_url_mutex);
|
||||
m_selected_printer_mqtt_url.clear();
|
||||
}
|
||||
|
||||
std::string OrcaCloudServiceAgent::selected_printer_mqtt_url() const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_selected_url_mutex);
|
||||
return m_selected_printer_mqtt_url;
|
||||
}
|
||||
|
||||
void OrcaCloudServiceAgent::deliver_cloud_message(const std::string& dev_id, const std::string& payload)
|
||||
{
|
||||
OnMessageFn callback;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(callback_mutex);
|
||||
callback = printer_status_callback;
|
||||
}
|
||||
if (callback)
|
||||
callback(dev_id, payload);
|
||||
}
|
||||
|
||||
int OrcaCloudServiceAgent::set_printer_status_callback(OnMessageFn fn)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(callback_mutex);
|
||||
printer_status_callback = std::move(fn);
|
||||
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: printer status callback=" << (printer_status_callback ? "set" : "clear");
|
||||
return BAMBU_NETWORK_SUCCESS;
|
||||
}
|
||||
|
||||
int OrcaCloudServiceAgent::send_printer_command(const std::string& dev_id, const std::string& body)
|
||||
{
|
||||
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: send_printer_command dev_id=" << dev_id
|
||||
<< " body_bytes=" << body.size() << " logged_in=" << is_user_login();
|
||||
if (dev_id.empty() || !is_user_login()) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: send_printer_command rejected";
|
||||
return BAMBU_NETWORK_ERR_INVALID_HANDLE;
|
||||
}
|
||||
|
||||
const std::string path = std::string(ORCA_CLOUD_PRINTER) + "/" + dev_id + "/commands";
|
||||
std::string response;
|
||||
unsigned int http_code = 0;
|
||||
int result = http_post(path, body, &response, &http_code);
|
||||
BOOST_LOG_TRIVIAL(info) << "OrcaCloudServiceAgent: command dev=" << dev_id
|
||||
<< " http=" << http_code << " result=" << result
|
||||
<< " response_bytes=" << response.size();
|
||||
return (result == BAMBU_NETWORK_SUCCESS && http_code >= 200 && http_code < 300)
|
||||
? BAMBU_NETWORK_SUCCESS
|
||||
: BAMBU_NETWORK_ERR_CONNECT_FAILED;
|
||||
}
|
||||
|
||||
int OrcaCloudServiceAgent::upload_gcode_via_cloud(const std::string& dev_id,
|
||||
const std::string& local_gcode_path,
|
||||
std::string* job_id,
|
||||
OnUpdateStatusFn update_fn,
|
||||
WasCancelledFn cancel_fn)
|
||||
{
|
||||
if (dev_id.empty() || local_gcode_path.empty() || !is_user_login())
|
||||
return BAMBU_NETWORK_ERR_INVALID_HANDLE;
|
||||
if (cancel_fn && cancel_fn())
|
||||
return BAMBU_NETWORK_ERR_CANCELED;
|
||||
|
||||
// Step 1: POST print-jobs/uploads -> a short-lived presigned R2 PUT URL. No
|
||||
// metadata rides this request; filename/start are only relevant to the HTTP
|
||||
// .../start finalize route, which this MQTT-driven flow does not call.
|
||||
const std::string uploads_path = std::string(ORCA_CLOUD_PRINTER) + "/" + Http::url_encode(dev_id) + "/print-jobs/uploads";
|
||||
std::string response;
|
||||
unsigned int http_code = 0;
|
||||
int result = http_post(uploads_path, "{}", &response, &http_code);
|
||||
if (result != BAMBU_NETWORK_SUCCESS || http_code < 200 || http_code >= 300) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "OrcaCloudServiceAgent: print-jobs/uploads failed http_code=" << http_code;
|
||||
return BAMBU_NETWORK_ERR_CONNECT_FAILED;
|
||||
}
|
||||
|
||||
std::string upload_job_id;
|
||||
std::string upload_url;
|
||||
try {
|
||||
const nlohmann::json j = nlohmann::json::parse(response);
|
||||
upload_job_id = j.value("job_id", "");
|
||||
upload_url = j.value("upload_url", "");
|
||||
} catch (const std::exception& e) {
|
||||
BOOST_LOG_TRIVIAL(error) << "OrcaCloudServiceAgent: failed to parse print-jobs/uploads response: " << e.what();
|
||||
return BAMBU_NETWORK_ERR_CONNECT_FAILED;
|
||||
}
|
||||
if (upload_job_id.empty() || upload_url.empty()) {
|
||||
BOOST_LOG_TRIVIAL(error) << "OrcaCloudServiceAgent: print-jobs/uploads response missing job_id/upload_url";
|
||||
return BAMBU_NETWORK_ERR_CONNECT_FAILED;
|
||||
}
|
||||
|
||||
if (cancel_fn && cancel_fn())
|
||||
return BAMBU_NETWORK_ERR_CANCELED;
|
||||
|
||||
// Step 2: PUT the G-code straight to R2 with the one-time URL from step 1. This
|
||||
// is a scoped, PUT-only, short-TTL capability with no bearer token of its own,
|
||||
// so it bypasses http_put (which always prefixes api_base_url and attaches the
|
||||
// cloud session's Authorization header - neither belongs on an R2 PUT).
|
||||
bool canceled = false;
|
||||
unsigned put_status = 0;
|
||||
std::string put_error;
|
||||
Http::put(upload_url)
|
||||
.tls_verify(true)
|
||||
.header("Content-Type", "text/x.gcode")
|
||||
.set_put_body(boost::filesystem::path(local_gcode_path))
|
||||
.timeout_connect(5)
|
||||
.timeout_max(300) // large G-code over a slow link
|
||||
.on_progress([&](Http::Progress progress, bool& cancel) {
|
||||
if (cancel_fn && cancel_fn()) {
|
||||
cancel = true;
|
||||
canceled = true;
|
||||
return;
|
||||
}
|
||||
if (update_fn && progress.ultotal > 0) {
|
||||
const int percent = static_cast<int>((progress.ulnow * 100) / progress.ultotal);
|
||||
update_fn(PrintingStageUpload, percent, "Uploading...");
|
||||
}
|
||||
})
|
||||
.on_complete([&](std::string, unsigned status) { put_status = status; })
|
||||
.on_error([&](std::string, std::string err, unsigned status) {
|
||||
put_status = status;
|
||||
put_error = std::move(err);
|
||||
})
|
||||
.perform_sync();
|
||||
|
||||
if (canceled)
|
||||
return BAMBU_NETWORK_ERR_CANCELED;
|
||||
if (put_status < 200 || put_status >= 300) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "OrcaCloudServiceAgent: R2 upload failed status=" << put_status << " error=" << put_error;
|
||||
return BAMBU_NETWORK_ERR_PRINT_SG_UPLOAD_FTP_FAILED;
|
||||
}
|
||||
|
||||
if (job_id)
|
||||
*job_id = std::move(upload_job_id);
|
||||
return BAMBU_NETWORK_SUCCESS;
|
||||
}
|
||||
|
||||
int OrcaCloudServiceAgent::start_cloud_print_job(const std::string& dev_id,
|
||||
const std::string& job_id,
|
||||
const std::string& filename,
|
||||
bool start)
|
||||
{
|
||||
if (dev_id.empty() || job_id.empty() || !is_user_login())
|
||||
return BAMBU_NETWORK_ERR_INVALID_HANDLE;
|
||||
|
||||
nlohmann::json body;
|
||||
if (!filename.empty())
|
||||
body["filename"] = filename;
|
||||
body["start"] = start;
|
||||
|
||||
const std::string path = std::string(ORCA_CLOUD_PRINTER) + "/" + Http::url_encode(dev_id) + "/print-jobs/" +
|
||||
Http::url_encode(job_id) + "/start";
|
||||
std::string response;
|
||||
unsigned int http_code = 0;
|
||||
const int result = http_post(path, body.dump(), &response, &http_code);
|
||||
if (result != BAMBU_NETWORK_SUCCESS || http_code < 200 || http_code >= 300) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "OrcaCloudServiceAgent: print-jobs/" << job_id << "/start failed http_code=" << http_code;
|
||||
return BAMBU_NETWORK_ERR_CONNECT_FAILED;
|
||||
}
|
||||
return BAMBU_NETWORK_SUCCESS;
|
||||
}
|
||||
|
||||
@@ -2139,6 +2400,10 @@ bool OrcaCloudServiceAgent::set_user_session(const json& session_json, bool noti
|
||||
|
||||
void OrcaCloudServiceAgent::clear_session(bool all_backends)
|
||||
{
|
||||
if (mqtt_connection) {
|
||||
mqtt_connection->stop();
|
||||
mqtt_connection->clear_subscriptions();
|
||||
}
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(session_mutex);
|
||||
session = SessionInfo{};
|
||||
@@ -2254,7 +2519,11 @@ int OrcaCloudServiceAgent::http_get(const std::string& path, std::string* respon
|
||||
return (res.success && !suppress) ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_CONNECT_FAILED;
|
||||
}
|
||||
|
||||
int OrcaCloudServiceAgent::http_post(const std::string& path, const std::string& body, std::string* response_body, unsigned int* http_code)
|
||||
int OrcaCloudServiceAgent::http_post(const std::string& path,
|
||||
const std::string& body,
|
||||
std::string* response_body,
|
||||
unsigned int* http_code,
|
||||
const std::string& content_type)
|
||||
{
|
||||
std::string url = api_base_url + path;
|
||||
BOOST_LOG_TRIVIAL(trace) << "OrcaCloudServiceAgent: POST " << url;
|
||||
@@ -2278,7 +2547,7 @@ int OrcaCloudServiceAgent::http_post(const std::string& path, const std::string&
|
||||
http.header("Authorization", "Bearer " + token);
|
||||
}
|
||||
|
||||
http.header("Content-Type", "application/json");
|
||||
http.header("Content-Type", content_type);
|
||||
http.set_post_body(body);
|
||||
|
||||
http.on_complete([&](std::string resp_body, unsigned resp_status) {
|
||||
@@ -2745,19 +3014,28 @@ int OrcaCloudServiceAgent::get_user_print_info(unsigned int* http_code, std::str
|
||||
if (http_code)
|
||||
*http_code = code;
|
||||
|
||||
if (result != 0 || code != 200)
|
||||
if (result != 0 || code != 200) {
|
||||
BOOST_LOG_TRIVIAL(error) << "OrcaCloudServiceAgent: get_user_print_info failed - http_code=" << code << ", response=" << response;
|
||||
return result != 0 ? result : BAMBU_NETWORK_ERR_GET_SETTING_LIST_FAILED;
|
||||
}
|
||||
|
||||
BOOST_LOG_TRIVIAL(trace) << "OrcaCloudServiceAgent: get_user_print_info fetched - http_code=" << code << ", response=" << response;
|
||||
|
||||
try {
|
||||
auto resp_json = nlohmann::json::parse(response);
|
||||
nlohmann::json devices = nlohmann::json::array();
|
||||
|
||||
for (const auto& printer : resp_json.value("data", nlohmann::json::array())) {
|
||||
const std::string role = printer.value("access_role", "");
|
||||
nlohmann::json device;
|
||||
std::string role = printer.value("access_role", "");
|
||||
|
||||
// A printer with the role "view" only has monitoring access for orca cloud.
|
||||
// The printer is owned by a different person and was shared to the current user without
|
||||
// any permission to control the printer so we discard this printer. Comment this out if
|
||||
// OrcaSlicer wants to support view only printers.
|
||||
if (role.empty() || role == "viewer")
|
||||
continue;
|
||||
|
||||
nlohmann::json device;
|
||||
device["dev_id"] = printer.value("id", "");
|
||||
device["dev_name"] = printer.value("name", "");
|
||||
if (printer.contains("model") && printer["model"].is_string())
|
||||
@@ -2771,15 +3049,20 @@ int OrcaCloudServiceAgent::get_user_print_info(unsigned int* http_code, std::str
|
||||
device["task_status"] = status["job"].value("state", "");
|
||||
}
|
||||
device["dev_online"] = online;
|
||||
devices.push_back(std::move(device));
|
||||
|
||||
devices.push_back(device);
|
||||
}
|
||||
|
||||
if (http_body) {
|
||||
nlohmann::json out;
|
||||
out["devices"] = std::move(devices);
|
||||
out["devices"] = devices;
|
||||
*http_body = out.dump();
|
||||
}
|
||||
} catch (const std::exception&) {
|
||||
|
||||
BOOST_LOG_TRIVIAL(debug) << "OrcaCloudServiceAgent: get_user_print_info parsed - device_count=" << devices.size()
|
||||
<< ", devices=" << devices.dump();
|
||||
} catch (const std::exception& e) {
|
||||
BOOST_LOG_TRIVIAL(error) << "OrcaCloudServiceAgent: get_user_print_info parse exception - " << e.what();
|
||||
return BAMBU_NETWORK_ERR_GET_SETTING_LIST_FAILED;
|
||||
}
|
||||
|
||||
@@ -2864,6 +3147,9 @@ int OrcaCloudServiceAgent::get_camera_url(std::string dev_id, std::function<void
|
||||
return BAMBU_NETWORK_SUCCESS;
|
||||
}
|
||||
|
||||
std::unique_ptr<ICameraSignalingChannel> OrcaCloudServiceAgent::create_camera_signaling_channel(const std::string& dev_id)
|
||||
{ return std::make_unique<OrcaCloudSignalingChannel>(this, dev_id); }
|
||||
|
||||
int OrcaCloudServiceAgent::get_design_staffpick(int offset, int limit, std::function<void(std::string)> callback)
|
||||
{
|
||||
BOOST_LOG_TRIVIAL(debug) << "OrcaCloudServiceAgent: get_design_staffpick (stub)";
|
||||
|
||||
@@ -4,6 +4,11 @@
|
||||
#include "CloudProvider.hpp"
|
||||
#include "ICameraSignalingChannel.hpp"
|
||||
#include "ICloudServiceAgent.hpp"
|
||||
#include <boost/asio.hpp>
|
||||
#include <boost/asio/ssl.hpp>
|
||||
#include <boost/beast/core.hpp>
|
||||
#include <boost/beast/ssl.hpp>
|
||||
#include <boost/beast/websocket.hpp>
|
||||
#include "bambu_networking.hpp"
|
||||
#include <cstdlib>
|
||||
#include "libslic3r/ProjectTask.hpp"
|
||||
@@ -13,6 +18,9 @@
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <functional>
|
||||
#include <condition_variable>
|
||||
#include <cstdint>
|
||||
#include <set>
|
||||
#include <memory>
|
||||
#include <thread>
|
||||
#include <unordered_map>
|
||||
@@ -20,6 +28,8 @@
|
||||
#include <vector>
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include "OrcaMqttConnection.hpp"
|
||||
|
||||
class wxSecretStore;
|
||||
|
||||
namespace Slic3r {
|
||||
@@ -157,6 +167,8 @@ public:
|
||||
|
||||
// Configuration
|
||||
void configure_urls(AppConfig* app_config);
|
||||
// Hostname only; cloud REST, MQTT, and WebRTC signaling use fixed TLS
|
||||
// endpoints on port 443.
|
||||
void set_api_base_url(const std::string& url);
|
||||
void set_auth_base_url(const std::string& url);
|
||||
void set_cloud_base_url(const std::string& url);
|
||||
@@ -216,6 +228,19 @@ public:
|
||||
int add_subscribe(std::vector<std::string> dev_list) override;
|
||||
int del_subscribe(std::vector<std::string> dev_list) override;
|
||||
void enable_multi_machine(bool enable) override;
|
||||
int set_printer_status_callback(OnMessageFn fn);
|
||||
int send_printer_command(const std::string& dev_id, const std::string& body);
|
||||
|
||||
int upload_gcode_via_cloud(const std::string& dev_id,
|
||||
const std::string& local_gcode_path,
|
||||
std::string* job_id,
|
||||
OnUpdateStatusFn update_fn,
|
||||
WasCancelledFn cancel_fn);
|
||||
|
||||
int start_cloud_print_job(const std::string& dev_id,
|
||||
const std::string& job_id,
|
||||
const std::string& filename,
|
||||
bool start = true);
|
||||
|
||||
// ========================================================================
|
||||
// ICloudServiceAgent Interface Implementation - Settings Synchronization
|
||||
@@ -251,7 +276,7 @@ public:
|
||||
// ICloudServiceAgent Interface Implementation - Model Mall & Publishing
|
||||
// ========================================================================
|
||||
int get_camera_url(std::string dev_id, std::function<void(std::string)> callback) override;
|
||||
// std::unique_ptr<ICameraSignalingChannel> create_camera_signaling_channel(const std::string& dev_id) override;
|
||||
std::unique_ptr<ICameraSignalingChannel> create_camera_signaling_channel(const std::string& dev_id) override;
|
||||
int get_design_staffpick(int offset, int limit, std::function<void(std::string)> callback) override;
|
||||
int start_publish(PublishParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, std::string* out) override;
|
||||
int get_model_publish_url(std::string* url) override;
|
||||
@@ -357,7 +382,29 @@ public:
|
||||
|
||||
static std::string generate_uuid_for_setting_id(const std::string& name, const std::string& user_id = "");
|
||||
|
||||
OrcaMqttConnection* get_mqtt_connection() noexcept {
|
||||
return mqtt_connection.get();
|
||||
}
|
||||
|
||||
const OrcaMqttConnection* get_mqtt_connection() const noexcept {
|
||||
return mqtt_connection.get();
|
||||
}
|
||||
|
||||
// Account-scoped cloud socket: wss://<api_base_url>/api/v1/printers/mqtt.
|
||||
// configure_ blocks for the duration of the initial connect attempt, so callers
|
||||
// drive it off the UI thread; teardown_ is synchronous. The dev_id argument is
|
||||
// retained for source compatibility with the printer-agent lifecycle; it does
|
||||
// not participate in endpoint construction.
|
||||
int configure_selected_printer_mqtt(const std::string& dev_id,
|
||||
OrcaMqttConnection::StateHandler state_handler = {});
|
||||
void teardown_selected_printer_mqtt();
|
||||
// Test hook: the wss:// URL of the current fleet socket ("" when none).
|
||||
std::string selected_printer_mqtt_url() const;
|
||||
|
||||
private:
|
||||
// Fans one inbound fleet MQTT message out to printer_status_callback.
|
||||
void deliver_cloud_message(const std::string& dev_id, const std::string& payload);
|
||||
|
||||
// Sync protocol helpers
|
||||
int sync_pull(
|
||||
std::function<void(const SyncPullResponse&)> on_success,
|
||||
@@ -381,7 +428,11 @@ private:
|
||||
|
||||
// HTTP request helpers
|
||||
int http_get(const std::string& path, std::string* response_body, unsigned int* http_code);
|
||||
int http_post(const std::string& path, const std::string& body, std::string* response_body, unsigned int* http_code);
|
||||
int http_post(const std::string& path,
|
||||
const std::string& body,
|
||||
std::string* response_body,
|
||||
unsigned int* http_code,
|
||||
const std::string& content_type = "application/json");
|
||||
int http_put(const std::string& path, const std::string& body, std::string* response_body, unsigned int* http_code);
|
||||
int http_delete(const std::string& path, std::string* response_body, unsigned int* http_code);
|
||||
std::map<std::string, std::string> data_headers();
|
||||
@@ -443,6 +494,9 @@ private:
|
||||
std::chrono::system_clock::now().time_since_epoch()).count()};
|
||||
|
||||
// Member variables - connection state
|
||||
std::unique_ptr<OrcaMqttConnection> mqtt_connection;
|
||||
std::string m_selected_printer_mqtt_url; // guarded by m_selected_url_mutex
|
||||
mutable std::mutex m_selected_url_mutex;
|
||||
bool is_connected{false};
|
||||
bool enable_track{false};
|
||||
bool multi_machine_enabled{false};
|
||||
@@ -456,6 +510,7 @@ private:
|
||||
AppOnHttpErrorFn on_http_error_fn;
|
||||
GetCountryCodeFn get_country_code_fn;
|
||||
QueueOnMainFn queue_on_main_fn;
|
||||
OnMessageFn printer_status_callback;
|
||||
mutable std::mutex callback_mutex;
|
||||
|
||||
// Thread safety
|
||||
|
||||
@@ -0,0 +1,355 @@
|
||||
#include "OrcaCloudSignalingChannel.hpp"
|
||||
|
||||
#include "Http.hpp"
|
||||
#include "OrcaCloudServiceAgent.hpp"
|
||||
#include "ICameraSignalingChannel.hpp"
|
||||
|
||||
#include <boost/asio/buffer.hpp>
|
||||
#include <boost/asio/connect.hpp>
|
||||
#include <boost/asio/ip/tcp.hpp>
|
||||
#include <boost/asio/post.hpp>
|
||||
#include <boost/asio/ssl/verify_mode.hpp>
|
||||
#include <boost/asio/ssl/host_name_verification.hpp>
|
||||
#include <boost/asio/ssl/stream_base.hpp>
|
||||
#include <boost/beast/core.hpp>
|
||||
#include <boost/beast/core/stream_traits.hpp>
|
||||
#include <boost/beast/core/flat_buffer.hpp>
|
||||
#include <boost/beast/core/buffers_to_string.hpp>
|
||||
#include <boost/log/trivial.hpp>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <ios>
|
||||
#include <exception>
|
||||
#include <cstddef>
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include <openssl/ssl.h>
|
||||
|
||||
#include <cctype>
|
||||
#include <iomanip>
|
||||
#include <openssl/tls1.h>
|
||||
#include <sstream>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
OrcaCloudSignalingChannel::OrcaCloudSignalingChannel(OrcaCloudServiceAgent* cloud, std::string dev_id)
|
||||
: m_cloud(cloud)
|
||||
, m_dev_id(std::move(dev_id))
|
||||
{
|
||||
}
|
||||
|
||||
OrcaCloudSignalingChannel::~OrcaCloudSignalingChannel()
|
||||
{
|
||||
close();
|
||||
}
|
||||
|
||||
void OrcaCloudSignalingChannel::open()
|
||||
{
|
||||
bool expected = false;
|
||||
if (!m_open.compare_exchange_strong(expected, true))
|
||||
return;
|
||||
m_stop.store(false);
|
||||
m_thread = std::thread([this] { run(); });
|
||||
}
|
||||
|
||||
void OrcaCloudSignalingChannel::close()
|
||||
{
|
||||
m_stop.store(true);
|
||||
std::shared_ptr<Connection> conn;
|
||||
Http::Ptr request;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
conn = m_conn;
|
||||
request = m_inflight_requests;
|
||||
}
|
||||
// m_conn is installed only after the live-token request completes, so
|
||||
// cancel the request independently before waiting for the worker.
|
||||
if (request)
|
||||
request->cancel();
|
||||
if (conn) {
|
||||
// Established session: close the socket on the io_context's own thread so
|
||||
// the pending async_read completes and io_context.run() unwinds.
|
||||
boost::asio::post(conn->io_context, [conn] {
|
||||
boost::system::error_code ec;
|
||||
boost::beast::get_lowest_layer(conn->websocket).cancel(ec);
|
||||
boost::beast::get_lowest_layer(conn->websocket).close(ec);
|
||||
});
|
||||
// Pre-run() phase (still in the synchronous connect/handshake): best-effort
|
||||
// direct interruption.
|
||||
boost::system::error_code ec;
|
||||
boost::beast::get_lowest_layer(conn->websocket).cancel(ec);
|
||||
boost::beast::get_lowest_layer(conn->websocket).close(ec);
|
||||
}
|
||||
if (m_thread.joinable())
|
||||
m_thread.join();
|
||||
m_open.store(false);
|
||||
}
|
||||
|
||||
void OrcaCloudSignalingChannel::send_offer(std::string sdp)
|
||||
{
|
||||
send_json(nlohmann::json{{"type", "webrtc.offer"}, {"sdp", std::move(sdp)}}.dump());
|
||||
}
|
||||
|
||||
void OrcaCloudSignalingChannel::send_ice(std::string candidate, std::string mid)
|
||||
{
|
||||
send_json(nlohmann::json{{"type", "webrtc.ice"},
|
||||
{"candidate", std::move(candidate)},
|
||||
{"sdpMid", std::move(mid)}}
|
||||
.dump());
|
||||
}
|
||||
|
||||
std::string OrcaCloudSignalingChannel::encode_path_component(const std::string& value)
|
||||
{
|
||||
std::ostringstream encoded;
|
||||
encoded << std::uppercase << std::hex;
|
||||
for (unsigned char c : value) {
|
||||
if (std::isalnum(c) || c == '-' || c == '_' || c == '.' || c == '~')
|
||||
encoded << c;
|
||||
else
|
||||
encoded << '%' << std::setw(2) << std::setfill('0') << static_cast<unsigned int>(c);
|
||||
}
|
||||
return encoded.str();
|
||||
}
|
||||
|
||||
void OrcaCloudSignalingChannel::unavailable(CameraUnavailableReason reason, std::string detail)
|
||||
{
|
||||
if (on_unavailable)
|
||||
on_unavailable(reason, std::move(detail));
|
||||
}
|
||||
|
||||
void OrcaCloudSignalingChannel::run()
|
||||
{
|
||||
try {
|
||||
if (!m_cloud || !m_cloud->ensure_token_fresh("camera")) {
|
||||
unavailable(CameraUnavailableReason::Error, "Unable to refresh OrcaCloud credentials");
|
||||
m_open.store(false);
|
||||
return;
|
||||
}
|
||||
const std::string token = m_cloud->get_access_token();
|
||||
// OrcaCloud exposes a bare API hostname. WebRTC signaling uses the
|
||||
// fixed HTTPS/WSS endpoints on port 443; custom schemes, ports, and
|
||||
// base paths are not supported by this agent.
|
||||
const std::string host = m_cloud->get_cloud_service_host();
|
||||
if (token.empty() || host.empty()) {
|
||||
unavailable(CameraUnavailableReason::Error, "OrcaCloud session is unavailable");
|
||||
m_open.store(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const std::string live_token_url =
|
||||
"https://" + host + "/api/v1/printers/" + encode_path_component(m_dev_id) + "/live-token";
|
||||
BOOST_LOG_TRIVIAL(info) << "signaling: POST " << live_token_url << " (dev_id=" << m_dev_id << ")";
|
||||
|
||||
nlohmann::json token_response;
|
||||
std::string token_body;
|
||||
unsigned int http_code = 0;
|
||||
|
||||
auto request = std::make_shared<Slic3r::Http>(Http::post(live_token_url));
|
||||
request->set_post_body(std::string("{}"))
|
||||
.header("Authorization", "Bearer " + token)
|
||||
.header("Content-Type", "application/json")
|
||||
.tls_verify(true)
|
||||
.timeout_max(30)
|
||||
.on_complete([&token_body, &http_code](std::string body, unsigned status) {
|
||||
http_code = status;
|
||||
token_body = std::move(body);
|
||||
})
|
||||
.on_error([&http_code](std::string, std::string, unsigned status) {
|
||||
http_code = status;
|
||||
});
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
m_inflight_requests = request;
|
||||
}
|
||||
|
||||
// If close() raced with request setup, make sure this request is
|
||||
// cancelled before entering the blocking call.
|
||||
if (m_stop.load())
|
||||
request->cancel();
|
||||
try {
|
||||
request->perform_sync();
|
||||
} catch (...) {
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
if (m_inflight_requests == request)
|
||||
m_inflight_requests.reset();
|
||||
throw;
|
||||
}
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
if (m_inflight_requests == request)
|
||||
m_inflight_requests.reset();
|
||||
}
|
||||
|
||||
try {
|
||||
token_response = nlohmann::json::parse(token_body);
|
||||
} catch (const std::exception&) {
|
||||
}
|
||||
if (http_code < 200 || http_code >= 300 || !token_response.contains("token")) {
|
||||
unavailable(CameraUnavailableReason::Error, "Unable to mint camera live token");
|
||||
m_open.store(false);
|
||||
return;
|
||||
}
|
||||
|
||||
std::vector<CameraIceServer> ice_servers;
|
||||
if (token_response.contains("ice_servers") && token_response["ice_servers"].is_array()) {
|
||||
for (const auto& entry : token_response["ice_servers"]) {
|
||||
if (entry.is_string()) {
|
||||
ice_servers.push_back({entry.get<std::string>(), {}, {}});
|
||||
} else if (entry.is_object()) {
|
||||
// RTCIceServer.urls is "string | string[]" (Cloudflare
|
||||
// Realtime returns an array). Emit one CameraIceServer per
|
||||
// URL, sharing the credentials.
|
||||
const std::string username = entry.value("username", std::string{});
|
||||
const std::string credential = entry.value("credential", std::string{});
|
||||
const auto add_url = [&](const nlohmann::json& url) {
|
||||
if (url.is_string() && !url.get<std::string>().empty())
|
||||
ice_servers.push_back({url.get<std::string>(), username, credential});
|
||||
};
|
||||
const auto urls = entry.find("urls");
|
||||
if (urls != entry.end()) {
|
||||
if (urls->is_array()) {
|
||||
for (const auto& url : *urls)
|
||||
add_url(url);
|
||||
} else {
|
||||
add_url(*urls);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
auto conn = std::make_shared<Connection>();
|
||||
conn->ssl_context.set_default_verify_paths();
|
||||
Http::add_platform_root_certificates(conn->ssl_context.native_handle());
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
m_conn = conn;
|
||||
}
|
||||
auto& websocket = conn->websocket;
|
||||
boost::asio::ip::tcp::resolver resolver(conn->io_context);
|
||||
const auto endpoints = resolver.resolve(host, "443");
|
||||
boost::asio::connect(boost::beast::get_lowest_layer(websocket), endpoints);
|
||||
if (!SSL_set_tlsext_host_name(websocket.next_layer().native_handle(), host.c_str()))
|
||||
throw std::runtime_error("Unable to configure TLS server name");
|
||||
websocket.next_layer().set_verify_mode(boost::asio::ssl::verify_peer);
|
||||
websocket.next_layer().set_verify_callback(boost::asio::ssl::host_name_verification(host));
|
||||
websocket.next_layer().handshake(boost::asio::ssl::stream_base::client);
|
||||
const std::string ws_target = "/api/v1/printers/" + encode_path_component(m_dev_id) +
|
||||
"/camera/live?token=" +
|
||||
encode_path_component(token_response["token"].get<std::string>());
|
||||
websocket.handshake(host, ws_target);
|
||||
BOOST_LOG_TRIVIAL(info) << "signaling: websocket handshake ok (" << ice_servers.size()
|
||||
<< " ice servers)";
|
||||
|
||||
if (on_ready)
|
||||
on_ready(std::move(ice_servers));
|
||||
send_json(nlohmann::json{{"type", "camera.mode"}, {"mode", "webrtc"}}.dump());
|
||||
// Async read loop, driven by the connection's own io_context. run()
|
||||
// returns once close() has shut the socket down, giving a bounded,
|
||||
// deadlock-free teardown from any thread.
|
||||
do_read(conn);
|
||||
conn->io_context.run();
|
||||
} catch (const std::exception& e) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "signaling: run() exception: " << e.what();
|
||||
if (!m_stop.load())
|
||||
unavailable(CameraUnavailableReason::Closed, e.what());
|
||||
}
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
m_conn.reset();
|
||||
}
|
||||
m_open.store(false);
|
||||
}
|
||||
|
||||
void OrcaCloudSignalingChannel::do_read(std::shared_ptr<Connection> conn)
|
||||
{
|
||||
auto buffer = std::make_shared<boost::beast::flat_buffer>();
|
||||
conn->websocket.async_read(
|
||||
*buffer, [this, conn, buffer](boost::system::error_code ec, std::size_t) {
|
||||
if (ec) {
|
||||
if (!m_stop.load())
|
||||
unavailable(CameraUnavailableReason::Closed, ec.message());
|
||||
return; // do not re-arm; io_context.run() unwinds
|
||||
}
|
||||
const std::string raw = boost::beast::buffers_to_string(buffer->data());
|
||||
// A malformed or unexpectedly-shaped message must not tear down the
|
||||
// session: parse/dispatch is guarded.
|
||||
try {
|
||||
dispatch_message(nlohmann::json::parse(raw), raw);
|
||||
} catch (const std::exception& e) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "signaling: ignoring malformed message: " << e.what()
|
||||
<< " raw=" << raw.substr(0, 256);
|
||||
}
|
||||
if (!m_stop.load())
|
||||
do_read(conn);
|
||||
});
|
||||
}
|
||||
|
||||
// Returns the string at key, or "" if absent or not a string (JSON null included).
|
||||
static std::string json_string(const nlohmann::json& object, const char* key)
|
||||
{
|
||||
const auto it = object.find(key);
|
||||
return (it != object.end() && it->is_string()) ? it->get<std::string>() : std::string{};
|
||||
}
|
||||
|
||||
void OrcaCloudSignalingChannel::dispatch_message(const nlohmann::json& message, const std::string& raw)
|
||||
{
|
||||
const std::string type = json_string(message, "type");
|
||||
BOOST_LOG_TRIVIAL(info) << "signaling: recv type=" << type << " raw=" << raw.substr(0, 256);
|
||||
if (type == "webrtc.answer") {
|
||||
const std::string sdp = json_string(message, "sdp");
|
||||
if (on_answer && !sdp.empty())
|
||||
on_answer(sdp);
|
||||
} else if (type == "webrtc.ice" && message.contains("candidate")) {
|
||||
// The peer may send "candidate" as a flat string or as a nested
|
||||
// RTCIceCandidateInit object { candidate, sdpMid, sdpMLineIndex }.
|
||||
const nlohmann::json& candidate = message["candidate"];
|
||||
std::string sdp_candidate;
|
||||
std::string mid = json_string(message, "sdpMid");
|
||||
if (candidate.is_string()) {
|
||||
sdp_candidate = candidate.get<std::string>();
|
||||
} else if (candidate.is_object()) {
|
||||
sdp_candidate = json_string(candidate, "candidate");
|
||||
std::string nested_mid = json_string(candidate, "sdpMid");
|
||||
if (!nested_mid.empty())
|
||||
mid = std::move(nested_mid);
|
||||
}
|
||||
if (on_ice && !sdp_candidate.empty())
|
||||
on_ice(sdp_candidate, mid);
|
||||
} else if (type == "webrtc.unavailable") {
|
||||
const std::string reason = json_string(message, "reason");
|
||||
unavailable(reason == "busy" ? CameraUnavailableReason::Busy
|
||||
: reason == "disabled" ? CameraUnavailableReason::Disabled
|
||||
: CameraUnavailableReason::Error,
|
||||
reason.empty() ? "error" : reason);
|
||||
}
|
||||
}
|
||||
|
||||
void OrcaCloudSignalingChannel::send_json(const std::string& message)
|
||||
{
|
||||
std::shared_ptr<Connection> conn;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
conn = m_conn;
|
||||
}
|
||||
if (!conn || m_stop.load())
|
||||
return;
|
||||
// Serialize the write onto the io_context thread (same thread that runs
|
||||
// async_read), so reads and writes never touch the stream concurrently.
|
||||
auto payload = std::make_shared<std::string>(message);
|
||||
boost::asio::post(conn->io_context, [this, conn, payload] {
|
||||
if (m_stop.load())
|
||||
return;
|
||||
boost::system::error_code ec;
|
||||
conn->websocket.write(boost::asio::buffer(*payload), ec);
|
||||
if (ec && !m_stop.load())
|
||||
unavailable(CameraUnavailableReason::Closed, ec.message());
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -0,0 +1,69 @@
|
||||
#pragma once
|
||||
|
||||
#include "ICameraSignalingChannel.hpp"
|
||||
#include "OrcaCloudServiceAgent.hpp"
|
||||
#include "Http.hpp"
|
||||
|
||||
#include <boost/asio/io_context.hpp>
|
||||
#include <boost/asio/ip/tcp.hpp>
|
||||
#include <boost/asio/ssl.hpp>
|
||||
#include <boost/asio/ssl/context.hpp>
|
||||
#include <boost/beast/ssl.hpp>
|
||||
#include <boost/beast/ssl/ssl_stream.hpp>
|
||||
#include <boost/beast/websocket.hpp>
|
||||
|
||||
#include <boost/beast/websocket/stream.hpp>
|
||||
#include <nlohmann/json_fwd.hpp>
|
||||
|
||||
#include <atomic>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
class OrcaCloudSignalingChannel : public ICameraSignalingChannel {
|
||||
public:
|
||||
OrcaCloudSignalingChannel(OrcaCloudServiceAgent* cloud, std::string dev_id);
|
||||
~OrcaCloudSignalingChannel() override;
|
||||
|
||||
void open() override;
|
||||
void close() override;
|
||||
void send_offer(std::string sdp) override;
|
||||
void send_ice(std::string candidate, std::string mid) override;
|
||||
|
||||
private:
|
||||
using WebSocket = boost::beast::websocket::stream<
|
||||
boost::beast::ssl_stream<boost::asio::ip::tcp::socket>>;
|
||||
|
||||
// The io_context and ssl_context must outlive the websocket stream that
|
||||
// references them. Bundling them here with the stream declared last makes
|
||||
// the destruction order correct (stream first, then contexts), and lets a
|
||||
// single shared_ptr own the whole set.
|
||||
struct Connection {
|
||||
boost::asio::io_context io_context;
|
||||
boost::asio::ssl::context ssl_context{boost::asio::ssl::context::tls_client};
|
||||
WebSocket websocket{io_context, ssl_context};
|
||||
};
|
||||
|
||||
void run();
|
||||
void do_read(std::shared_ptr<Connection> conn);
|
||||
void dispatch_message(const nlohmann::json& message, const std::string& raw);
|
||||
void send_json(const std::string& message);
|
||||
void unavailable(CameraUnavailableReason reason, std::string detail);
|
||||
static std::string encode_path_component(const std::string& value);
|
||||
|
||||
OrcaCloudServiceAgent* m_cloud;
|
||||
|
||||
Http::Ptr m_inflight_requests{nullptr};
|
||||
|
||||
std::string m_dev_id;
|
||||
std::atomic<bool> m_stop{false};
|
||||
std::atomic<bool> m_open{false};
|
||||
std::thread m_thread;
|
||||
mutable std::mutex m_mutex;
|
||||
std::shared_ptr<Connection> m_conn;
|
||||
};
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -0,0 +1,920 @@
|
||||
#include "OrcaMqttConnection.hpp"
|
||||
#include "Http.hpp"
|
||||
|
||||
#include <boost/asio.hpp>
|
||||
#include <boost/asio/io_context.hpp>
|
||||
#include <boost/asio/ip/tcp.hpp>
|
||||
#include <boost/asio/post.hpp>
|
||||
#include <boost/asio/buffer.hpp>
|
||||
#include <boost/asio/error.hpp>
|
||||
#include <boost/asio/ssl.hpp>
|
||||
#include <boost/asio/ssl/context.hpp>
|
||||
#include <boost/asio/steady_timer.hpp>
|
||||
#include <boost/asio/ssl/verify_mode.hpp>
|
||||
#include <boost/asio/ssl/host_name_verification.hpp>
|
||||
#include <boost/asio/ssl/stream_base.hpp>
|
||||
#include <boost/beast/core.hpp>
|
||||
#include <boost/beast/core/flat_buffer.hpp>
|
||||
#include <boost/beast/core/stream_traits.hpp>
|
||||
#include <boost/beast/core/buffers_to_string.hpp>
|
||||
#include <boost/beast/http/field.hpp>
|
||||
#include <boost/beast/http/message.hpp>
|
||||
#include <boost/beast/http/string_body.hpp>
|
||||
#include <boost/beast/ssl.hpp>
|
||||
#include <boost/beast/websocket.hpp>
|
||||
#include <boost/beast/websocket/rfc6455.hpp>
|
||||
#include <boost/beast/websocket/stream_base.hpp>
|
||||
|
||||
#include <boost/system/errc.hpp>
|
||||
#include <boost/system/error_code.hpp>
|
||||
|
||||
#include <openssl/ssl.h>
|
||||
#include <openssl/tls1.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <deque>
|
||||
#include <exception>
|
||||
#include <ios>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <sstream>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
struct OrcaMqttConnection::Connection {
|
||||
boost::asio::io_context io_context;
|
||||
boost::asio::ssl::context ssl_context;
|
||||
boost::asio::ip::tcp::resolver resolver;
|
||||
boost::asio::steady_timer keepalive_timer;
|
||||
boost::beast::flat_buffer read_buffer;
|
||||
// Reassembly buffer for the MQTT byte stream: each WebSocket message carries an
|
||||
// arbitrary byte range of that stream, so partial packets accumulate here until
|
||||
// complete. Only the MQTT worker thread touches it.
|
||||
std::string mqtt_rx;
|
||||
std::deque<std::shared_ptr<std::vector<uint8_t>>> outbound_packets;
|
||||
boost::system::error_code terminal_error;
|
||||
std::atomic_bool async_session_started{false};
|
||||
bool write_in_progress{false};
|
||||
// Exactly one of these is engaged once ws_handshake() has run: wss for
|
||||
// wss:// endpoints, ws for plaintext ws://.
|
||||
std::optional<TlsWebSocket> wss;
|
||||
std::optional<PlainWebSocket> ws;
|
||||
|
||||
Connection()
|
||||
: ssl_context(boost::asio::ssl::context::tls_client)
|
||||
, resolver(io_context)
|
||||
, keepalive_timer(io_context)
|
||||
{}
|
||||
};
|
||||
|
||||
namespace {
|
||||
// Apply / clear a tcp_stream timeout on whichever websocket is engaged.
|
||||
// Templated on the connection type only because Connection is a private nested
|
||||
// type: a deduced parameter needs no (inaccessible) name for it.
|
||||
template<class Conn> void expires_after(Conn& conn, std::chrono::seconds timeout) {
|
||||
if (conn.wss) boost::beast::get_lowest_layer(*conn.wss).expires_after(timeout);
|
||||
else if (conn.ws) boost::beast::get_lowest_layer(*conn.ws).expires_after(timeout);
|
||||
}
|
||||
template<class Conn> void expires_never(Conn& conn) {
|
||||
if (conn.wss) boost::beast::get_lowest_layer(*conn.wss).expires_never();
|
||||
else if (conn.ws) boost::beast::get_lowest_layer(*conn.ws).expires_never();
|
||||
}
|
||||
|
||||
// Total byte length of the MQTT packet starting at `offset` in `stream`. Returns
|
||||
// kMqttPacketIncomplete when the fixed header's remaining-length varint is not yet
|
||||
// complete, or kMqttPacketMalformed when it cannot be valid (more than four length
|
||||
// bytes, or a declared size past the sanity cap). The caller compares a real length
|
||||
// against the bytes available.
|
||||
constexpr std::size_t kMqttPacketIncomplete = 0;
|
||||
constexpr std::size_t kMqttPacketMalformed = static_cast<std::size_t>(-1);
|
||||
// MQTT permits ~256 MiB; nothing this client receives is close, so a larger
|
||||
// declared length is treated as malformed rather than buffered.
|
||||
constexpr std::size_t kMqttMaxPacketBytes = 16 * 1024 * 1024;
|
||||
|
||||
std::size_t mqtt_packet_length(const std::string& stream, std::size_t offset) {
|
||||
if (offset >= stream.size())
|
||||
return kMqttPacketIncomplete;
|
||||
std::size_t multiplier = 1;
|
||||
std::size_t remaining = 0;
|
||||
std::size_t i = offset + 1;
|
||||
for (int len_bytes = 0; len_bytes < 4; ++len_bytes) {
|
||||
if (i >= stream.size())
|
||||
return kMqttPacketIncomplete; // varint not complete yet
|
||||
const std::uint8_t byte = static_cast<std::uint8_t>(stream[i++]);
|
||||
remaining += static_cast<std::size_t>(byte & 0x7f) * multiplier;
|
||||
if ((byte & 0x80) == 0) {
|
||||
const std::size_t total = (i - offset) + remaining;
|
||||
return total > kMqttMaxPacketBytes ? kMqttPacketMalformed : total;
|
||||
}
|
||||
multiplier *= 128;
|
||||
}
|
||||
return kMqttPacketMalformed; // more than four length bytes
|
||||
}
|
||||
} // namespace
|
||||
|
||||
OrcaMqttConnection::~OrcaMqttConnection() { stop(); }
|
||||
|
||||
bool OrcaMqttConnection::start(const Config& config, MessageHandler on_message, StateHandler on_state) {
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> lifecycle_lock(lifecycle_mutex);
|
||||
stop();
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex);
|
||||
current_config = config;
|
||||
this->on_message = std::move(on_message);
|
||||
this->on_state = std::move(on_state);
|
||||
initial_result = false;
|
||||
initial_completed = false;
|
||||
connected = false;
|
||||
m_last_connack_rc.store(-1);
|
||||
}
|
||||
stopping.store(false);
|
||||
worker = std::thread(&OrcaMqttConnection::run, this);
|
||||
}
|
||||
|
||||
// Wait without holding lifecycle_mutex so a concurrent stop() can cancel the
|
||||
// attempt and signal completion instead of blocking until the timeout.
|
||||
std::unique_lock<std::mutex> lock(mutex);
|
||||
if (!initial_cv.wait_for(lock, std::chrono::seconds(10), [this] { return initial_completed; })) {
|
||||
initial_completed = true;
|
||||
initial_result = false;
|
||||
}
|
||||
return initial_result;
|
||||
}
|
||||
|
||||
void OrcaMqttConnection::stop() {
|
||||
std::lock_guard<std::recursive_mutex> lifecycle_lock(lifecycle_mutex);
|
||||
stopping.store(true);
|
||||
state_cv.notify_all();
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(connection_mutex);
|
||||
if (active_connection) {
|
||||
if (active_connection->async_session_started.load()) {
|
||||
// The worker owns the live WebSocket. Stop dispatching its
|
||||
// asynchronous operations; the worker closes the socket after
|
||||
// leaving the event loop.
|
||||
active_connection->io_context.stop();
|
||||
} else {
|
||||
// Setup still uses synchronous operations on the worker. Wake a
|
||||
// pending resolve/connect/CONNACK read without competing with a
|
||||
// live asynchronous session.
|
||||
auto shutdown_socket = [](auto& websocket) {
|
||||
auto& socket = boost::beast::get_lowest_layer(websocket).socket();
|
||||
boost::system::error_code socket_error;
|
||||
if (socket.cancel(socket_error))
|
||||
return;
|
||||
if (socket.shutdown(boost::asio::ip::tcp::socket::shutdown_both, socket_error))
|
||||
return;
|
||||
if (socket.close(socket_error))
|
||||
return;
|
||||
};
|
||||
if (active_connection->wss)
|
||||
shutdown_socket(*active_connection->wss);
|
||||
else if (active_connection->ws)
|
||||
shutdown_socket(*active_connection->ws);
|
||||
active_connection->resolver.cancel();
|
||||
}
|
||||
}
|
||||
}
|
||||
if (worker.joinable())
|
||||
worker.join();
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex);
|
||||
connected = false;
|
||||
acknowledged_subscriptions.clear();
|
||||
pending_subscribe_packets.clear();
|
||||
pending_requests.clear();
|
||||
if (!initial_completed) {
|
||||
initial_completed = true;
|
||||
initial_result = false;
|
||||
}
|
||||
}
|
||||
initial_cv.notify_all();
|
||||
}
|
||||
|
||||
bool OrcaMqttConnection::is_running() const {
|
||||
return worker.joinable() && !stopping.load();
|
||||
}
|
||||
|
||||
void OrcaMqttConnection::flush_subscription_change() {
|
||||
std::shared_ptr<Connection> conn;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(connection_mutex);
|
||||
conn = active_connection;
|
||||
}
|
||||
bool connacked;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex);
|
||||
connacked = connected;
|
||||
}
|
||||
if (!conn || !connacked)
|
||||
return; // no live MQTT session yet — the worker sends the set on CONNACK
|
||||
boost::asio::post(conn->io_context, [this, conn] {
|
||||
if (!stopping.load() && connected.load())
|
||||
send_pending_subscriptions(conn);
|
||||
});
|
||||
}
|
||||
|
||||
bool OrcaMqttConnection::subscribe(const std::string& dev_id) {
|
||||
if (dev_id.empty()) {
|
||||
return false;
|
||||
}
|
||||
const std::string topic = report_topic(dev_id);
|
||||
if (topic.size() > 96) { // MQTT topic filter cap enforced by the service
|
||||
return false;
|
||||
}
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex);
|
||||
if (subscriptions.count(topic) != 0 && pending_unsubscriptions.count(topic) == 0) {
|
||||
return true;
|
||||
}
|
||||
subscriptions.insert(topic);
|
||||
pending_unsubscriptions.erase(topic);
|
||||
pending_subscriptions.insert(topic);
|
||||
}
|
||||
state_cv.notify_all();
|
||||
flush_subscription_change(); // ask the worker to emit SUBSCRIBE now (no reconnect)
|
||||
return true;
|
||||
}
|
||||
|
||||
bool OrcaMqttConnection::unsubscribe(const std::string& dev_id) {
|
||||
const std::string topic = report_topic(dev_id);
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex);
|
||||
subscriptions.erase(topic);
|
||||
acknowledged_subscriptions.erase(topic);
|
||||
pending_subscriptions.erase(topic);
|
||||
pending_unsubscriptions.insert(topic);
|
||||
for (auto it = pending_subscribe_packets.begin(); it != pending_subscribe_packets.end();) {
|
||||
if (it->second == topic)
|
||||
it = pending_subscribe_packets.erase(it);
|
||||
else
|
||||
++it;
|
||||
}
|
||||
for (auto it = pending_requests.begin(); it != pending_requests.end();) {
|
||||
if (it->first == dev_id)
|
||||
it = pending_requests.erase(it);
|
||||
else
|
||||
++it;
|
||||
}
|
||||
}
|
||||
state_cv.notify_all();
|
||||
flush_subscription_change(); // ask the worker to emit UNSUBSCRIBE now (no reconnect)
|
||||
return true;
|
||||
}
|
||||
|
||||
void OrcaMqttConnection::clear_subscriptions() {
|
||||
std::lock_guard<std::mutex> lock(mutex);
|
||||
subscriptions.clear();
|
||||
pending_subscriptions.clear();
|
||||
pending_unsubscriptions.clear();
|
||||
acknowledged_subscriptions.clear();
|
||||
pending_subscribe_packets.clear();
|
||||
pending_requests.clear();
|
||||
}
|
||||
|
||||
bool OrcaMqttConnection::parse_endpoint(const std::string& url, Endpoint& endpoint) {
|
||||
std::string rest;
|
||||
std::string default_port;
|
||||
if (url.rfind("wss://", 0) == 0) { rest = url.substr(6); default_port = "443"; }
|
||||
else if (url.rfind("ws://", 0) == 0) { rest = url.substr(5); default_port = "80"; }
|
||||
else return false;
|
||||
|
||||
const auto slash = rest.find('/');
|
||||
const std::string authority = rest.substr(0, slash);
|
||||
endpoint.target = (slash == std::string::npos) ? "/" : rest.substr(slash);
|
||||
|
||||
// host[:port] — leave an unbracketed IPv6 literal alone
|
||||
const auto colon = authority.rfind(':');
|
||||
if (colon != std::string::npos && authority.find(']') == std::string::npos) {
|
||||
endpoint.host = authority.substr(0, colon);
|
||||
endpoint.port = authority.substr(colon + 1);
|
||||
} else {
|
||||
endpoint.host = authority;
|
||||
endpoint.port = default_port;
|
||||
}
|
||||
return !endpoint.host.empty() && !endpoint.port.empty() && !endpoint.target.empty();
|
||||
}
|
||||
|
||||
void OrcaMqttConnection::append_string(std::vector<uint8_t>& packet, const std::string& value) {
|
||||
if (value.size() > 0xffff)
|
||||
throw std::runtime_error("MQTT string is too long");
|
||||
packet.push_back(static_cast<uint8_t>(value.size() >> 8));
|
||||
packet.push_back(static_cast<uint8_t>(value.size() & 0xff));
|
||||
packet.insert(packet.end(), value.begin(), value.end());
|
||||
}
|
||||
|
||||
void OrcaMqttConnection::prepend_remaining_length(std::vector<uint8_t>& packet, size_t length) {
|
||||
std::vector<uint8_t> encoded;
|
||||
do {
|
||||
uint8_t byte = static_cast<uint8_t>(length % 128);
|
||||
length /= 128;
|
||||
if (length != 0)
|
||||
byte |= 0x80;
|
||||
encoded.push_back(byte);
|
||||
} while (length != 0);
|
||||
packet.insert(packet.begin() + 1, encoded.begin(), encoded.end());
|
||||
}
|
||||
|
||||
std::vector<uint8_t> OrcaMqttConnection::make_connect_packet(
|
||||
const std::string& client_id, const std::string& username,
|
||||
const std::string& password, int keepalive_seconds) {
|
||||
std::vector<uint8_t> packet{0x10};
|
||||
append_string(packet, "MQTT");
|
||||
packet.push_back(4); // protocol level 3.1.1
|
||||
|
||||
uint8_t flags = 0x02; // clean session
|
||||
if (!username.empty()) { flags |= 0x80; if (!password.empty()) flags |= 0x40; }
|
||||
packet.push_back(flags);
|
||||
|
||||
packet.push_back(static_cast<uint8_t>(keepalive_seconds >> 8));
|
||||
packet.push_back(static_cast<uint8_t>(keepalive_seconds & 0xff));
|
||||
|
||||
append_string(packet, client_id.empty() ? "OrcaSlicer" : client_id);
|
||||
if (!username.empty()) {
|
||||
append_string(packet, username);
|
||||
if (!password.empty()) append_string(packet, password);
|
||||
}
|
||||
prepend_remaining_length(packet, packet.size() - 1);
|
||||
return packet;
|
||||
}
|
||||
|
||||
std::string OrcaMqttConnection::report_topic(const std::string& device_id) { return "device/" + device_id + "/report"; }
|
||||
|
||||
std::string OrcaMqttConnection::request_topic(const std::string& id) { return "device/" + id + "/request"; }
|
||||
|
||||
std::vector<uint8_t> OrcaMqttConnection::make_publish_packet(const std::string& topic, const std::string& payload) {
|
||||
std::vector<uint8_t> packet{0x30}; // PUBLISH, QoS 0, no retain
|
||||
append_string(packet, topic); // no packet id at QoS 0
|
||||
packet.insert(packet.end(), payload.begin(), payload.end());
|
||||
prepend_remaining_length(packet, packet.size() - 1);
|
||||
return packet;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> OrcaMqttConnection::make_subscribe_packet(uint16_t id, const std::string& topic, uint8_t qos) {
|
||||
std::vector<uint8_t> packet{0x82};
|
||||
packet.push_back(id >> 8); packet.push_back(id & 0xff);
|
||||
append_string(packet, topic);
|
||||
packet.push_back(qos);
|
||||
prepend_remaining_length(packet, packet.size() - 1);
|
||||
return packet;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> OrcaMqttConnection::make_unsubscribe_packet(uint16_t id, const std::string& topic) {
|
||||
std::vector<uint8_t> packet{0xA2};
|
||||
packet.push_back(id >> 8); packet.push_back(id & 0xff);
|
||||
append_string(packet, topic);
|
||||
prepend_remaining_length(packet, packet.size() - 1);
|
||||
return packet;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> OrcaMqttConnection::make_ping_packet() { return {0xc0, 0}; }
|
||||
|
||||
void OrcaMqttConnection::ws_write(Connection& conn, const std::vector<uint8_t>& packet) {
|
||||
if (packet.empty()) {
|
||||
return;
|
||||
}
|
||||
if (conn.wss) {
|
||||
conn.wss->binary(true);
|
||||
conn.wss->write(boost::asio::buffer(packet));
|
||||
} else if (conn.ws) {
|
||||
conn.ws->binary(true);
|
||||
conn.ws->write(boost::asio::buffer(packet));
|
||||
}
|
||||
}
|
||||
|
||||
void OrcaMqttConnection::close_connection(Connection& conn) {
|
||||
boost::system::error_code error;
|
||||
if (conn.wss) {
|
||||
auto& socket = boost::beast::get_lowest_layer(*conn.wss).socket();
|
||||
if (socket.cancel(error))
|
||||
return;
|
||||
if (socket.shutdown(boost::asio::ip::tcp::socket::shutdown_both, error))
|
||||
return;
|
||||
if (socket.close(error))
|
||||
return;
|
||||
} else if (conn.ws) {
|
||||
auto& socket = boost::beast::get_lowest_layer(*conn.ws).socket();
|
||||
if (socket.cancel(error))
|
||||
return;
|
||||
if (socket.shutdown(boost::asio::ip::tcp::socket::shutdown_both, error))
|
||||
return;
|
||||
if (socket.close(error))
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void OrcaMqttConnection::enqueue_packet(const std::shared_ptr<Connection>& conn,
|
||||
std::vector<uint8_t> packet) {
|
||||
if (!conn || packet.empty())
|
||||
return;
|
||||
conn->outbound_packets.emplace_back(std::make_shared<std::vector<uint8_t>>(std::move(packet)));
|
||||
start_async_write(conn);
|
||||
}
|
||||
|
||||
void OrcaMqttConnection::start_async_write(const std::shared_ptr<Connection>& conn) {
|
||||
if (!conn || conn->write_in_progress || conn->outbound_packets.empty() || stopping.load())
|
||||
return;
|
||||
|
||||
conn->write_in_progress = true;
|
||||
const auto packet = conn->outbound_packets.front();
|
||||
auto on_write = [this, conn](const boost::system::error_code& error, std::size_t) {
|
||||
conn->write_in_progress = false;
|
||||
if (error) {
|
||||
if (!stopping.load())
|
||||
conn->terminal_error = error;
|
||||
conn->io_context.stop();
|
||||
return;
|
||||
}
|
||||
conn->outbound_packets.pop_front();
|
||||
start_async_write(conn);
|
||||
};
|
||||
if (conn->wss) {
|
||||
conn->wss->binary(true);
|
||||
conn->wss->async_write(boost::asio::buffer(*packet), std::move(on_write));
|
||||
} else if (conn->ws) {
|
||||
conn->ws->binary(true);
|
||||
conn->ws->async_write(boost::asio::buffer(*packet), std::move(on_write));
|
||||
} else {
|
||||
conn->write_in_progress = false;
|
||||
conn->outbound_packets.pop_front();
|
||||
}
|
||||
}
|
||||
|
||||
void OrcaMqttConnection::start_async_read(const std::shared_ptr<Connection>& conn) {
|
||||
if (!conn || stopping.load())
|
||||
return;
|
||||
|
||||
auto on_read = [this, conn](const boost::system::error_code& error, std::size_t) {
|
||||
if (error) {
|
||||
if (!stopping.load())
|
||||
conn->terminal_error = error;
|
||||
conn->io_context.stop();
|
||||
return;
|
||||
}
|
||||
const std::string message = boost::beast::buffers_to_string(conn->read_buffer.data());
|
||||
conn->read_buffer.consume(conn->read_buffer.size());
|
||||
feed_mqtt(*conn, message);
|
||||
start_async_read(conn);
|
||||
};
|
||||
if (conn->wss)
|
||||
conn->wss->async_read(conn->read_buffer, std::move(on_read));
|
||||
else if (conn->ws)
|
||||
conn->ws->async_read(conn->read_buffer, std::move(on_read));
|
||||
}
|
||||
|
||||
bool OrcaMqttConnection::drain_mqtt_packets(std::string& stream, std::vector<std::string>& packets) {
|
||||
std::size_t consumed = 0;
|
||||
bool ok = true;
|
||||
while (consumed < stream.size()) {
|
||||
const std::size_t total = mqtt_packet_length(stream, consumed);
|
||||
if (total == kMqttPacketIncomplete)
|
||||
break; // wait for the rest of the stream
|
||||
if (total == kMqttPacketMalformed) {
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
if (consumed + total > stream.size())
|
||||
break; // header complete, payload still arriving
|
||||
packets.emplace_back(stream, consumed, total);
|
||||
consumed += total;
|
||||
}
|
||||
if (consumed > 0)
|
||||
stream.erase(0, consumed);
|
||||
if (!ok)
|
||||
stream.clear(); // drop the poisoned tail; the caller closes the connection
|
||||
return ok;
|
||||
}
|
||||
|
||||
void OrcaMqttConnection::feed_mqtt(Connection& conn, const std::string& bytes) {
|
||||
conn.mqtt_rx.append(bytes);
|
||||
std::vector<std::string> packets;
|
||||
if (!drain_mqtt_packets(conn.mqtt_rx, packets)) {
|
||||
// A malformed header can never resync; drop the session so the worker
|
||||
// reconnects with a clean MQTT stream instead of buffering forever.
|
||||
if (!stopping.load())
|
||||
conn.terminal_error = boost::system::errc::make_error_code(boost::system::errc::protocol_error);
|
||||
conn.io_context.stop();
|
||||
return;
|
||||
}
|
||||
for (const std::string& packet : packets)
|
||||
handle_packet(packet);
|
||||
}
|
||||
|
||||
void OrcaMqttConnection::schedule_keepalive(const std::shared_ptr<Connection>& conn) {
|
||||
const int keepalive = current_config.keepalive_seconds;
|
||||
if (!conn || keepalive <= 0 || stopping.load())
|
||||
return;
|
||||
|
||||
conn->keepalive_timer.expires_after(std::chrono::seconds(std::max(1, keepalive / 2)));
|
||||
conn->keepalive_timer.async_wait([this, conn](const boost::system::error_code& error) {
|
||||
if (error || stopping.load())
|
||||
return;
|
||||
enqueue_packet(conn, make_ping_packet());
|
||||
schedule_keepalive(conn);
|
||||
});
|
||||
}
|
||||
|
||||
void OrcaMqttConnection::post_packet(const std::shared_ptr<Connection>& conn,
|
||||
std::vector<uint8_t> packet) {
|
||||
if (!conn || packet.empty())
|
||||
return;
|
||||
boost::asio::post(conn->io_context, [this, conn, packet = std::move(packet)]() mutable {
|
||||
if (!stopping.load())
|
||||
enqueue_packet(conn, std::move(packet));
|
||||
});
|
||||
}
|
||||
|
||||
std::size_t OrcaMqttConnection::ws_read(Connection& conn, boost::beast::flat_buffer& buffer,
|
||||
boost::system::error_code& ec) {
|
||||
if (conn.wss)
|
||||
return conn.wss->read(buffer, ec);
|
||||
if (conn.ws)
|
||||
return conn.ws->read(buffer, ec);
|
||||
ec = boost::asio::error::not_connected;
|
||||
return 0;
|
||||
}
|
||||
|
||||
void OrcaMqttConnection::ws_handshake(Connection& conn, const Config& config, const Endpoint& endpoint) {
|
||||
const auto results = conn.resolver.resolve(endpoint.host, endpoint.port);
|
||||
|
||||
std::string token;
|
||||
if (config.bearer_provider) {
|
||||
token = config.bearer_provider();
|
||||
}
|
||||
auto decorator = [token](boost::beast::websocket::request_type& request) {
|
||||
request.set(boost::beast::http::field::user_agent, "OrcaSlicer");
|
||||
if (!token.empty())
|
||||
request.set(boost::beast::http::field::authorization, "Bearer " + token);
|
||||
request.set("Sec-WebSocket-Protocol", "mqtt");
|
||||
};
|
||||
boost::beast::http::response<boost::beast::http::string_body> response;
|
||||
boost::system::error_code handshake_error;
|
||||
|
||||
if (config.use_tls) {
|
||||
// stop() inspects the engaged optional under connection_mutex; publish it
|
||||
// under the same lock, then release before the blocking connect.
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(connection_mutex);
|
||||
conn.wss.emplace(conn.io_context, conn.ssl_context);
|
||||
}
|
||||
auto& websocket = *conn.wss;
|
||||
auto& stream = boost::beast::get_lowest_layer(websocket);
|
||||
stream.expires_after(std::chrono::seconds(10));
|
||||
stream.connect(results);
|
||||
// Set SNI before the TLS handshake so the cloud edge selects the correct
|
||||
// certificate.
|
||||
auto& tls_stream = websocket.next_layer();
|
||||
if (!SSL_set_tlsext_host_name(tls_stream.native_handle(), endpoint.host.c_str()))
|
||||
throw std::runtime_error("failed to set Orca Cloud TLS server name");
|
||||
if (!config.ca_file.empty())
|
||||
conn.ssl_context.load_verify_file(config.ca_file);
|
||||
else
|
||||
conn.ssl_context.set_default_verify_paths();
|
||||
Http::add_platform_root_certificates(conn.ssl_context.native_handle());
|
||||
tls_stream.set_verify_mode(boost::asio::ssl::verify_peer);
|
||||
tls_stream.set_verify_callback(boost::asio::ssl::host_name_verification(endpoint.host));
|
||||
tls_stream.handshake(boost::asio::ssl::stream_base::client);
|
||||
websocket.set_option(boost::beast::websocket::stream_base::decorator(decorator));
|
||||
websocket.handshake(response, endpoint.host, endpoint.target, handshake_error);
|
||||
} else {
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(connection_mutex);
|
||||
conn.ws.emplace(conn.io_context);
|
||||
}
|
||||
auto& websocket = *conn.ws;
|
||||
auto& stream = boost::beast::get_lowest_layer(websocket);
|
||||
stream.expires_after(std::chrono::seconds(10));
|
||||
stream.connect(results);
|
||||
websocket.set_option(boost::beast::websocket::stream_base::decorator(decorator));
|
||||
websocket.handshake(response, endpoint.host, endpoint.target, handshake_error);
|
||||
}
|
||||
|
||||
if (handshake_error) {
|
||||
throw boost::system::system_error(handshake_error, "Orca WebSocket handshake");
|
||||
}
|
||||
if (response["Sec-WebSocket-Protocol"] != "mqtt") {
|
||||
throw std::runtime_error("Orca WebSocket did not negotiate MQTT");
|
||||
}
|
||||
}
|
||||
|
||||
bool OrcaMqttConnection::send_request(const std::string& dev_id, const std::string& payload) {
|
||||
if (dev_id.empty()) {
|
||||
return false;
|
||||
}
|
||||
if (!connected.load()) {
|
||||
return false;
|
||||
}
|
||||
std::shared_ptr<Connection> conn;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(connection_mutex);
|
||||
conn = active_connection;
|
||||
}
|
||||
if (!conn) {
|
||||
return false;
|
||||
}
|
||||
const std::string report = report_topic(dev_id);
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex);
|
||||
if (subscriptions.count(report) != 0 && acknowledged_subscriptions.count(report) == 0) {
|
||||
pending_requests.emplace_back(dev_id, payload);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
post_packet(conn, make_publish_packet(request_topic(dev_id), payload));
|
||||
return true;
|
||||
}
|
||||
|
||||
void OrcaMqttConnection::connect_and_read() {
|
||||
auto connection = std::make_shared<Connection>();
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(connection_mutex);
|
||||
active_connection = connection;
|
||||
if (stopping.load()) {
|
||||
active_connection.reset();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
auto clear_connection = [this, connection] {
|
||||
close_connection(*connection);
|
||||
std::lock_guard<std::mutex> lock(connection_mutex);
|
||||
if (active_connection == connection)
|
||||
active_connection.reset();
|
||||
};
|
||||
|
||||
try {
|
||||
Endpoint endpoint;
|
||||
if (!parse_endpoint(current_config.url, endpoint)) {
|
||||
throw std::runtime_error("invalid Orca Cloud WebSocket endpoint");
|
||||
}
|
||||
|
||||
|
||||
ws_handshake(*connection, current_config, endpoint);
|
||||
|
||||
// Auth precedence: a bearer_provider authenticates the WebSocket upgrade, so the
|
||||
// CONNECT username/password fields are omitted entirely (the cloud form).
|
||||
const bool use_bearer = static_cast<bool>(current_config.bearer_provider);
|
||||
ws_write(*connection, make_connect_packet(current_config.client_id,
|
||||
use_bearer ? std::string() : current_config.username,
|
||||
use_bearer ? std::string() : current_config.password,
|
||||
current_config.keepalive_seconds));
|
||||
|
||||
boost::beast::flat_buffer buffer;
|
||||
expires_after(*connection, std::chrono::seconds(10));
|
||||
boost::system::error_code connack_error;
|
||||
ws_read(*connection, buffer, connack_error);
|
||||
if (connack_error)
|
||||
throw boost::system::system_error(connack_error, "read Orca MQTT CONNACK");
|
||||
// Beast leaves this expiry armed after the synchronous CONNACK read above;
|
||||
// disable it before starting the long-lived async WebSocket session.
|
||||
expires_never(*connection);
|
||||
const std::string connack = boost::beast::buffers_to_string(buffer.data());
|
||||
// rc: 0 accepted, 1..5 refusal, -1 not a CONNACK. A WebSocket message may
|
||||
// carry more than the CONNACK; only its first four bytes are the CONNACK.
|
||||
const int rc = (connack.size() >= 4 && static_cast<uint8_t>(connack[0]) == 0x20)
|
||||
? static_cast<int>(static_cast<uint8_t>(connack[3]))
|
||||
: -1;
|
||||
m_last_connack_rc.store(rc);
|
||||
if (rc != 0) {
|
||||
if (rc == 4 || rc == 5) {
|
||||
// Bad credentials / not authorized — retrying cannot help. Make run()'s
|
||||
// loop exit and unblock any waiting start().
|
||||
stopping.store(true);
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex);
|
||||
initial_completed = true;
|
||||
initial_result = false;
|
||||
}
|
||||
initial_cv.notify_all();
|
||||
}
|
||||
throw std::runtime_error("Orca MQTT CONNECT refused rc=" + std::to_string(rc));
|
||||
}
|
||||
|
||||
// Any bytes the broker sent after the 4-byte CONNACK in the same message
|
||||
// start the MQTT stream; keep them for the reassembler.
|
||||
if (connack.size() > 4)
|
||||
connection->mqtt_rx.assign(connack, 4, std::string::npos);
|
||||
|
||||
// The subscription acknowledgement belongs to this MQTT session. Clear
|
||||
// the previous session's state before notifying the owner, because the
|
||||
// reconnect callback immediately queues the printer's initial requests.
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex);
|
||||
acknowledged_subscriptions.clear();
|
||||
pending_subscribe_packets.clear();
|
||||
}
|
||||
connection->async_session_started.store(true);
|
||||
notify_state(true);
|
||||
reconnect_delay_seconds.store(1); // a fresh CONNACK resets the backoff
|
||||
send_current_subscriptions(connection);
|
||||
feed_mqtt(*connection, {}); // drain anything that rode in with the CONNACK
|
||||
start_async_read(connection);
|
||||
schedule_keepalive(connection);
|
||||
const std::size_t handlers_run = connection->io_context.run();
|
||||
if (handlers_run == 0 && !stopping.load())
|
||||
throw std::runtime_error("Orca MQTT event loop stopped unexpectedly");
|
||||
if (connection->terminal_error && !stopping.load())
|
||||
throw boost::system::system_error(connection->terminal_error, "read Orca MQTT message");
|
||||
clear_connection();
|
||||
if (!stopping.load())
|
||||
notify_state(false);
|
||||
} catch (...) {
|
||||
clear_connection();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
void OrcaMqttConnection::send_current_subscriptions(const std::shared_ptr<Connection>& conn) {
|
||||
std::vector<std::string> topics;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex);
|
||||
topics.assign(subscriptions.begin(), subscriptions.end());
|
||||
acknowledged_subscriptions.clear();
|
||||
pending_subscribe_packets.clear();
|
||||
for (const std::string& topic : topics)
|
||||
pending_subscriptions.erase(topic);
|
||||
}
|
||||
for (const std::string& topic : topics) {
|
||||
const uint16_t packet_id = next_packet_id++;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex);
|
||||
pending_subscribe_packets[packet_id] = topic;
|
||||
}
|
||||
enqueue_packet(conn, make_subscribe_packet(packet_id, topic, 1));
|
||||
}
|
||||
}
|
||||
|
||||
void OrcaMqttConnection::send_pending_subscriptions(const std::shared_ptr<Connection>& conn) {
|
||||
std::vector<std::string> subscribe_topics;
|
||||
std::vector<std::string> unsubscribe_topics;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex);
|
||||
subscribe_topics.assign(pending_subscriptions.begin(), pending_subscriptions.end());
|
||||
unsubscribe_topics.assign(pending_unsubscriptions.begin(), pending_unsubscriptions.end());
|
||||
pending_subscriptions.clear();
|
||||
pending_unsubscriptions.clear();
|
||||
}
|
||||
for (const std::string& topic : subscribe_topics) {
|
||||
const uint16_t packet_id = next_packet_id++;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex);
|
||||
pending_subscribe_packets[packet_id] = topic;
|
||||
}
|
||||
enqueue_packet(conn, make_subscribe_packet(packet_id, topic, 1));
|
||||
}
|
||||
for (const std::string& topic : unsubscribe_topics) {
|
||||
const uint16_t packet_id = next_packet_id++;
|
||||
enqueue_packet(conn, make_unsubscribe_packet(packet_id, topic));
|
||||
}
|
||||
}
|
||||
|
||||
void OrcaMqttConnection::handle_packet(const std::string& packet) {
|
||||
if (packet.size() < 2) {
|
||||
return;
|
||||
}
|
||||
const uint8_t header = static_cast<uint8_t>(packet[0]);
|
||||
const uint8_t packet_type = header >> 4;
|
||||
if (packet_type != 3) { // Only QoS 0 PUBLISH carries printer status.
|
||||
if (packet_type == 9 && packet.size() >= 5) {
|
||||
const uint16_t packet_id = (static_cast<unsigned int>(static_cast<uint8_t>(packet[2])) << 8) |
|
||||
static_cast<unsigned int>(static_cast<uint8_t>(packet[3]));
|
||||
std::ostringstream result_codes;
|
||||
for (size_t index = 4; index < packet.size(); ++index) {
|
||||
if (index != 4)
|
||||
result_codes << ',';
|
||||
result_codes << "0x" << std::hex << static_cast<unsigned int>(static_cast<uint8_t>(packet[index]));
|
||||
}
|
||||
|
||||
// Each production SUBSCRIBE packet currently contains one topic.
|
||||
// MQTT grants QoS 0 or 1 for a requested QoS 1 subscription; 0x80
|
||||
// means the subscription was rejected.
|
||||
const uint8_t result = static_cast<uint8_t>(packet[4]);
|
||||
std::string topic;
|
||||
std::deque<std::pair<std::string, std::string>> requests;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex);
|
||||
auto pending = pending_subscribe_packets.find(packet_id);
|
||||
if (pending != pending_subscribe_packets.end()) {
|
||||
topic = pending->second;
|
||||
pending_subscribe_packets.erase(pending);
|
||||
for (auto it = pending_requests.begin(); it != pending_requests.end();) {
|
||||
if (report_topic(it->first) == topic) {
|
||||
requests.push_back(std::move(*it));
|
||||
it = pending_requests.erase(it);
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
if (result == 0 || result == 1) {
|
||||
acknowledged_subscriptions.insert(topic);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (topic.empty()) {
|
||||
} else if (result == 0 || result == 1) {
|
||||
for (const auto& request : requests) {
|
||||
if (!send_request(request.first, request.second)) {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
size_t index = 1;
|
||||
size_t multiplier = 1;
|
||||
size_t remaining = 0;
|
||||
uint8_t encoded = 0;
|
||||
do {
|
||||
if (index >= packet.size() || multiplier > 128 * 128 * 128) {
|
||||
return;
|
||||
}
|
||||
encoded = static_cast<uint8_t>(packet[index++]);
|
||||
remaining += (encoded & 0x7f) * multiplier;
|
||||
multiplier *= 128;
|
||||
} while ((encoded & 0x80) != 0);
|
||||
const size_t remaining_end = index + remaining;
|
||||
if (remaining_end > packet.size() || remaining < 2 || index + 2 > remaining_end) {
|
||||
return;
|
||||
}
|
||||
const uint16_t topic_length = (static_cast<uint8_t>(packet[index]) << 8) |
|
||||
static_cast<uint8_t>(packet[index + 1]);
|
||||
index += 2;
|
||||
if (topic_length > packet.size() - index) {
|
||||
return;
|
||||
}
|
||||
const std::string topic(packet.data() + index, topic_length);
|
||||
index += topic_length;
|
||||
if (((header >> 1) & 0x03) != 0) {
|
||||
if (index + 2 > remaining_end) {
|
||||
return;
|
||||
}
|
||||
index += 2; // QoS 1/2 packet identifier; the service currently sends QoS 0.
|
||||
}
|
||||
const size_t payload_size = remaining_end - index;
|
||||
// topic is "device/<id>/report" (or "/request"); hand the id up, drop anything else.
|
||||
std::string dev_id;
|
||||
if (topic.rfind("device/", 0) == 0) {
|
||||
const size_t id_start = 7;
|
||||
const size_t id_end = topic.rfind('/');
|
||||
if (id_end != std::string::npos && id_end > id_start)
|
||||
dev_id = topic.substr(id_start, id_end - id_start);
|
||||
}
|
||||
if (dev_id.empty()) {
|
||||
} else if (on_message) {
|
||||
on_message(dev_id, packet.substr(index, remaining_end - index));
|
||||
} else {
|
||||
}
|
||||
}
|
||||
|
||||
void OrcaMqttConnection::notify_state(bool is_now_connected) {
|
||||
StateHandler callback;
|
||||
bool initial = false;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex);
|
||||
connected = is_now_connected;
|
||||
initial = !initial_completed;
|
||||
if (initial) {
|
||||
initial_result = is_now_connected;
|
||||
initial_completed = true;
|
||||
}
|
||||
callback = on_state;
|
||||
}
|
||||
if (initial)
|
||||
initial_cv.notify_all();
|
||||
else if (callback)
|
||||
callback(is_now_connected, false);
|
||||
}
|
||||
|
||||
void OrcaMqttConnection::run() {
|
||||
while (!stopping.load()) {
|
||||
const int retry_seconds = reconnect_delay_seconds.load();
|
||||
const uint64_t attempt = ++m_attempt_number;
|
||||
try {
|
||||
connect_and_read();
|
||||
} catch (const std::exception& error) {
|
||||
if (!stopping.load())
|
||||
notify_state(false);
|
||||
}
|
||||
if (stopping.load())
|
||||
break;
|
||||
// Grow the backoff only across attempts that never reached CONNACK; a
|
||||
// successful connection resets reconnect_delay_seconds to 1 (connect_and_read).
|
||||
reconnect_delay_seconds.store(std::min(retry_seconds * 2, 30));
|
||||
std::unique_lock<std::mutex> lock(mutex);
|
||||
state_cv.wait_for(lock, std::chrono::seconds(retry_seconds), [this] { return stopping.load(); });
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -0,0 +1,172 @@
|
||||
#ifndef slic3r_OrcaMqttConnection_hpp_
|
||||
#define slic3r_OrcaMqttConnection_hpp_
|
||||
|
||||
#include <boost/asio/ssl.hpp>
|
||||
#include <boost/asio/ssl/stream.hpp>
|
||||
#include <boost/beast/core.hpp>
|
||||
#include <boost/beast/core/tcp_stream.hpp>
|
||||
#include <boost/beast/core/flat_buffer.hpp>
|
||||
#include <boost/beast/ssl.hpp>
|
||||
#include <boost/beast/websocket.hpp>
|
||||
#include <atomic>
|
||||
#include <boost/beast/websocket/stream.hpp>
|
||||
#include <condition_variable>
|
||||
#include <deque>
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
// Minimal MQTT 3.1.1 codec + WebSocket transport (ws:// and wss://), shared by the
|
||||
// LAN (OrcaSonar) and cloud (fleet) printer connections. Both PUBLISH
|
||||
// commands to device/<id>/request and SUBSCRIBE device/<id>/report; Config is the
|
||||
// only per-transport difference.
|
||||
class OrcaMqttConnection
|
||||
{
|
||||
public:
|
||||
using TokenProvider = std::function<std::string()>;
|
||||
using MessageHandler = std::function<void(const std::string&, const std::string&)>;
|
||||
using StateHandler = std::function<void(bool connected, bool initial)>;
|
||||
|
||||
struct Endpoint { std::string host; std::string port; std::string target; };
|
||||
|
||||
struct Config {
|
||||
std::string url;
|
||||
bool use_tls = false;
|
||||
std::string ca_file;
|
||||
TokenProvider bearer_provider; // set => bearer on WS upgrade, CONNECT creds omitted
|
||||
std::string username;
|
||||
std::string password;
|
||||
std::string client_id = "OrcaSlicer";
|
||||
int keepalive_seconds = 60;
|
||||
};
|
||||
|
||||
static bool parse_endpoint(const std::string& url, Endpoint& endpoint);
|
||||
|
||||
// Build an MQTT 3.1.1 CONNECT packet. Clean-session is always set; the
|
||||
// username/password connect flags and payload fields are added only when
|
||||
// username is non-empty (the cloud form authenticates via a bearer on the
|
||||
// WebSocket upgrade and omits CONNECT credentials). Public for unit tests.
|
||||
static std::vector<uint8_t> make_connect_packet(const std::string& client_id,
|
||||
const std::string& username,
|
||||
const std::string& password,
|
||||
int keepalive_seconds);
|
||||
|
||||
// Topic-string helpers for the per-device request/report channels and the
|
||||
// MQTT 3.1.1 PUBLISH / SUBSCRIBE / UNSUBSCRIBE packet builders. All public
|
||||
// for unit tests. make_publish_packet emits QoS 0 (no packet identifier).
|
||||
static std::string request_topic(const std::string& dev_id); // "device/<id>/request"
|
||||
static std::string report_topic(const std::string& dev_id); // "device/<id>/report"
|
||||
static std::vector<uint8_t> make_publish_packet(const std::string& topic, const std::string& payload);
|
||||
static std::vector<uint8_t> make_subscribe_packet(uint16_t packet_id, const std::string& topic, uint8_t qos);
|
||||
static std::vector<uint8_t> make_unsubscribe_packet(uint16_t packet_id, const std::string& topic);
|
||||
|
||||
// Split a raw MQTT byte stream into complete packets: each complete packet is
|
||||
// appended to `packets` and erased from `stream`; a partial trailing packet is
|
||||
// left in `stream` for the next call. Returns false when the stream begins with
|
||||
// a malformed header, which cannot resync — the caller must drop the connection.
|
||||
// Public for unit tests. MQTT-over-WebSocket allows a packet to span frames and
|
||||
// several packets per frame, so the receiver must reassemble the stream rather
|
||||
// than assume one packet per WebSocket message.
|
||||
static bool drain_mqtt_packets(std::string& stream, std::vector<std::string>& packets);
|
||||
|
||||
~OrcaMqttConnection();
|
||||
|
||||
bool start(const Config& config, MessageHandler on_message, StateHandler on_state);
|
||||
void stop();
|
||||
// True while the worker thread is alive (connected OR retrying). Lets callers
|
||||
// avoid restarting a healthy connection.
|
||||
bool is_running() const;
|
||||
// True once CONNACK has been received and the socket has not since dropped.
|
||||
bool is_connected() const { return connected.load(); }
|
||||
bool subscribe(const std::string& dev_id);
|
||||
bool unsubscribe(const std::string& dev_id);
|
||||
// Last MQTT CONNACK return code: 0 ok, 1..5 refusal, -1 none seen this attempt.
|
||||
int last_connack_rc() const { return m_last_connack_rc.load(); }
|
||||
void clear_subscriptions();
|
||||
bool send_request(const std::string& dev_id, const std::string& payload);
|
||||
|
||||
private:
|
||||
// The endpoint may be either a TLS (wss://) or a plaintext (ws://) WebSocket;
|
||||
// Connection holds whichever one is engaged and the ws_* helpers below
|
||||
// dispatch on it.
|
||||
using TlsWebSocket = boost::beast::websocket::stream<
|
||||
boost::asio::ssl::stream<boost::beast::tcp_stream>>;
|
||||
using PlainWebSocket = boost::beast::websocket::stream<boost::beast::tcp_stream>;
|
||||
struct Connection;
|
||||
|
||||
static void append_string(std::vector<uint8_t>& packet, const std::string& value);
|
||||
static void prepend_remaining_length(std::vector<uint8_t>& packet, size_t length);
|
||||
static std::vector<uint8_t> make_ping_packet();
|
||||
|
||||
// Transport dispatch: each forwards to conn.wss (TLS) or conn.ws (plaintext).
|
||||
// These synchronous operations are called only by the MQTT worker during
|
||||
// connection setup. Once the MQTT session is established, all socket I/O is
|
||||
// asynchronous and owned by that worker's io_context.
|
||||
void ws_write(Connection& conn, const std::vector<uint8_t>& packet);
|
||||
std::size_t ws_read(Connection& conn, boost::beast::flat_buffer& buffer, boost::system::error_code& ec);
|
||||
void ws_handshake(Connection& conn, const Config& config, const Endpoint& endpoint);
|
||||
void close_connection(Connection& conn);
|
||||
void enqueue_packet(const std::shared_ptr<Connection>& conn, std::vector<uint8_t> packet);
|
||||
void start_async_write(const std::shared_ptr<Connection>& conn);
|
||||
void start_async_read(const std::shared_ptr<Connection>& conn);
|
||||
void schedule_keepalive(const std::shared_ptr<Connection>& conn);
|
||||
void post_packet(const std::shared_ptr<Connection>& conn, std::vector<uint8_t> packet);
|
||||
// Ask the MQTT worker to emit subscription changes on its own io_context.
|
||||
void flush_subscription_change();
|
||||
void connect_and_read();
|
||||
void send_current_subscriptions(const std::shared_ptr<Connection>& conn);
|
||||
void send_pending_subscriptions(const std::shared_ptr<Connection>& conn);
|
||||
void handle_packet(const std::string& packet);
|
||||
// Append freshly received WebSocket bytes to `conn`'s MQTT stream and dispatch
|
||||
// every complete packet. An MQTT packet may span several WebSocket messages and
|
||||
// several packets may arrive in one, so the stream is reassembled here.
|
||||
void feed_mqtt(Connection& conn, const std::string& bytes);
|
||||
void notify_state(bool is_now_connected);
|
||||
void run();
|
||||
|
||||
std::atomic_bool stopping{true};
|
||||
std::atomic_int reconnect_delay_seconds{1};
|
||||
// Serialises the whole of start() and stop() against each other, so the UI
|
||||
// thread's stop() (disconnect / dtor) cannot race the connect thread's start()
|
||||
// into a concurrent worker.join(). Recursive because start() calls stop().
|
||||
std::recursive_mutex lifecycle_mutex;
|
||||
std::thread worker;
|
||||
std::mutex mutex;
|
||||
std::mutex connection_mutex;
|
||||
std::shared_ptr<Connection> active_connection;
|
||||
std::condition_variable initial_cv;
|
||||
std::condition_variable state_cv;
|
||||
Config current_config;
|
||||
MessageHandler on_message;
|
||||
StateHandler on_state;
|
||||
// Full report-topic strings ("device/<id>/report"), not bare device ids.
|
||||
std::set<std::string> subscriptions;
|
||||
std::set<std::string> pending_subscriptions;
|
||||
std::set<std::string> pending_unsubscriptions;
|
||||
// Requests for a subscribed device wait until the corresponding SUBACK is
|
||||
// received. Otherwise an immediate pushall response can be published by
|
||||
// the broker before this client is actually subscribed to the report topic.
|
||||
std::set<std::string> acknowledged_subscriptions;
|
||||
std::map<uint16_t, std::string> pending_subscribe_packets;
|
||||
std::deque<std::pair<std::string, std::string>> pending_requests;
|
||||
std::atomic<uint16_t> next_packet_id{1};
|
||||
std::atomic<int> m_last_connack_rc{-1};
|
||||
uint64_t m_attempt_number{0}; // worker-thread diagnostic sequence
|
||||
bool initial_result{false};
|
||||
bool initial_completed{false};
|
||||
std::atomic_bool connected{false};
|
||||
};
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
#endif // slic3r_OrcaMqttConnection_hpp_
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,22 +2,34 @@
|
||||
#define __ORCA_PRINTER_AGENT_HPP__
|
||||
|
||||
#include "IPrinterAgent.hpp"
|
||||
#include "ICloudServiceAgent.hpp"
|
||||
#include "OrcaCloudServiceAgent.hpp"
|
||||
#include "OrcaMqttConnection.hpp"
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include "bambu_networking.hpp"
|
||||
#include <string>
|
||||
#include <mutex>
|
||||
#include <memory>
|
||||
#include <thread>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace Slic3r { class ICloudServiceAgent; }
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
class OrcaCloudServiceAgent;
|
||||
|
||||
/**
|
||||
* OrcaPrinterAgent - Stub implementation for printer operations.
|
||||
* OrcaPrinterAgent - OrcaSonar MQTT printer agent.
|
||||
*
|
||||
* All printer-related operations are currently stubs that return success.
|
||||
* Actual printer connectivity requires the BBL SDK or future Orca implementation.
|
||||
* LAN and cloud commands use the same OrcaSonar protocol payloads; only the
|
||||
* MQTT connection selected by route_send() differs.
|
||||
*/
|
||||
class OrcaPrinterAgent : public IPrinterAgent {
|
||||
class OrcaPrinterAgent : public IPrinterAgent
|
||||
{
|
||||
public:
|
||||
explicit OrcaPrinterAgent(std::string log_dir);
|
||||
~OrcaPrinterAgent() override;
|
||||
@@ -27,9 +39,18 @@ public:
|
||||
// ========================================================================
|
||||
|
||||
void set_cloud_agent(std::shared_ptr<ICloudServiceAgent> cloud) override;
|
||||
CameraStreamMode get_camera_stream_mode() const override;
|
||||
std::string get_camera_url() const override;
|
||||
int list_printer_files(const std::string& dev_id, PrinterFileListFn callback) override;
|
||||
int get_printer_file_thumbnail(const std::string& dev_id, const std::string& path, PrinterFileThumbnailFn callback) override;
|
||||
int delete_printer_file(const std::string& dev_id, const std::string& path, PrinterFileDeleteFn callback) override;
|
||||
int get_printer_file_metadata(const std::string& dev_id, const std::string& path, PrinterFileMetadataFn callback) override;
|
||||
|
||||
// Communication
|
||||
int send_message(std::string dev_id, std::string json_str, int qos, int flag) override;
|
||||
bool supports_command(const std::string& dev_id, const std::string& command) const override;
|
||||
bool supports_feature(const std::string& dev_id, const std::string& feature) const override;
|
||||
bool uses_filament_mapping() const override { return true; }
|
||||
int connect_printer(const PrinterConnectionParams& params) override;
|
||||
int disconnect_printer() override;
|
||||
int send_message_to_printer(std::string dev_id, std::string json_str, int qos, int flag) override;
|
||||
@@ -44,6 +65,7 @@ public:
|
||||
// Machine Selection
|
||||
std::string get_user_selected_machine() override;
|
||||
int set_user_selected_machine(std::string dev_id) override;
|
||||
int unbind(std::string dev_id) override;
|
||||
|
||||
/**
|
||||
* Get agent information.
|
||||
@@ -55,6 +77,8 @@ public:
|
||||
|
||||
// Print Job Operations
|
||||
int start_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn) override;
|
||||
// Unimplemented on OrcaSonar: reports a non-success result so callers fall back to
|
||||
// start_print() instead of treating the missing send as success.
|
||||
int start_local_print_with_record(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn) override;
|
||||
int start_send_gcode_to_sdcard(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn) override;
|
||||
int start_local_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn) override;
|
||||
@@ -70,10 +94,189 @@ public:
|
||||
int set_on_local_message_fn(OnMessageFn fn) override;
|
||||
int set_queue_on_main_fn(QueueOnMainFn fn) override;
|
||||
|
||||
int command_ams_refresh_rfid(std::string dev_id, int ams_id, int tray_id, int sequence_id, bool lan_mode) override;
|
||||
int command_ams_calibrate(std::string dev_id, int ams_id, int sequence_id, bool lan_mode) override;
|
||||
int command_ams_select_tray(std::string dev_id, std::string tray_id, int sequence_id, bool lan_mode) override;
|
||||
int command_start_camera(std::string dev_id) override;
|
||||
int command_xyz_abs(std::string dev_id, int sequence_id, bool lan_mode) override;
|
||||
int command_auto_leveling(std::string dev_id, int sequence_id, bool lan_mode) override;
|
||||
int command_go_home(std::string dev_id, bool is_printing, bool supports_mqtt_homing, int sequence_id, bool lan_mode) override;
|
||||
int command_set_bed(std::string dev_id, int temp, bool supports_mqtt_bed_ctrl, int sequence_id, bool lan_mode) override;
|
||||
int command_set_nozzle(std::string dev_id, int temp, int sequence_id, bool lan_mode) override;
|
||||
int command_axis_control(std::string dev_id,
|
||||
std::string axis,
|
||||
double unit,
|
||||
double input_val,
|
||||
int speed,
|
||||
bool is_core_xy,
|
||||
bool supports_mqtt_axis_control,
|
||||
int sequence_id,
|
||||
bool lan_mode) override;
|
||||
|
||||
// Test-only: drive emit_connect_sequence directly (no socket).
|
||||
void run_connect_sequence_for_test(const std::string& dev_id)
|
||||
{
|
||||
emit_connect_sequence(dev_id, [](const std::string&) {}, [](const std::string&) {});
|
||||
}
|
||||
|
||||
// Test-only: advance the LAN connection epoch without a connect/disconnect cycle.
|
||||
void bump_lan_generation_for_test() { ++m_lan_generation; }
|
||||
// Test-only: the same for the (independent) cloud selection epoch.
|
||||
void bump_cloud_generation_for_test() { ++m_cloud_generation; }
|
||||
|
||||
FilamentSyncMode get_filament_sync_mode() const override { return FilamentSyncMode::subscription; }
|
||||
|
||||
protected:
|
||||
// Forward one inbound printer message to on_message_fn or on_local_message_fn (marshalled onto the UI
|
||||
// thread via queue_on_main_fn when set). Body of every connection's MessageHandler.
|
||||
void deliver_to_sink(const std::string& dev_id, const std::string& payload, bool local);
|
||||
|
||||
// Extract OrcaSonar's print.ipcam.stream_mode from LAN reports before they
|
||||
// are forwarded to the GUI. The getters below then read this agent-owned state.
|
||||
void parse_ipcam_info(const std::string& dev_id, const std::string& payload);
|
||||
|
||||
// Orca-dialect -> Bambu-dialect compatibility shim for inbound reports: the single
|
||||
// place Orca Protocol JSON is rewritten into the shapes MachineObject::parse_json
|
||||
// already handles, so parse_json needs no Orca-specific changes. Self-contained
|
||||
// (its cache is a function-local static) and deletable together with its call site
|
||||
// once parse_json reads the Orca dialect natively. See the definition for the
|
||||
// per-rule detail. Returns the payload unchanged when no rule applies.
|
||||
std::string merge_capabilities(const std::string& dev_id, const std::string& payload);
|
||||
void forget_device_capabilities(const std::string& dev_id);
|
||||
int prepare_outgoing_request(const std::string& dev_id, const std::string& payload,
|
||||
std::string& command, std::string& prepared) const;
|
||||
|
||||
// Report the asynchronous LAN connection state using the same callback contract as
|
||||
// the other printer agents. The transport result cannot be returned by
|
||||
// connect_printer(), which only starts the worker.
|
||||
void dispatch_local_connect(int state, const std::string& dev_id, const std::string& message);
|
||||
|
||||
// The LAN inbound-message handler for one connection generation: forwards to
|
||||
// deliver_to_sink only while `generation` is still the live epoch.
|
||||
std::function<void(const std::string&, const std::string&)> make_lan_message_handler(uint64_t generation);
|
||||
|
||||
// Pure LAN-address parsing + client-id. protected static so the test Probe reaches them.
|
||||
static bool parse_lan_endpoint(const std::string& dev_ip, std::string& host, std::string& port);
|
||||
static std::string make_lan_client_id(const std::string& dev_id);
|
||||
|
||||
// Pure serializer for PrintParams::ams_mapping2 -> print.gcode_file.filament_mapping.
|
||||
// Entries are re-keyed by their array position; {255,255} is dropped. Empty when the
|
||||
// input is empty, malformed, or has no usable entries. protected static for the test Probe.
|
||||
static nlohmann::json build_filament_mapping(const std::string& ams_mapping2);
|
||||
|
||||
// Pure builder for the print.gcode_file command payload. A non-empty mapping is
|
||||
// included as filament_mapping; an empty one is omitted so the payload is
|
||||
// byte-identical to an unmapped print. protected static for the test Probe.
|
||||
static nlohmann::json build_gcode_file_payload(const std::string& sequence_id,
|
||||
const std::string& target,
|
||||
const nlohmann::json& filament_mapping);
|
||||
|
||||
// Pure JSON -> entries normalization for the files.list MQTT reply
|
||||
// ({"files":{...,"entries":[{name,path,is_dir,size,modified}]}}). Directory
|
||||
// entries and entries without a usable name are skipped; a missing path falls
|
||||
// back to the name and size/modified default to 0. protected static for the Probe.
|
||||
static std::vector<PrinterFileEntry> parse_files_list_reply(const std::string& payload);
|
||||
|
||||
// Pure JSON -> metadata normalization for the files.metadata MQTT reply. Missing
|
||||
// or malformed fields default to 0. protected static for the Probe.
|
||||
static PrinterFileMetadata parse_files_metadata_reply(const std::string& payload);
|
||||
|
||||
// Pure JSON -> image-bytes extraction for the files.thumbnails MQTT reply
|
||||
// ({"files":{...,"items":[{path,data|state,...}]}}). Returns decoded image bytes
|
||||
// for `path`, or empty when the path has no thumbnail, an error state, or is
|
||||
// absent. protected static for the test Probe.
|
||||
static std::string parse_files_thumbnail_reply(const std::string& payload, const std::string& path);
|
||||
|
||||
// Dispatch one files.* command (list/metadata/delete) over the LAN MQTT
|
||||
// connection and route its reply, matched by sequence_id, into `complete`.
|
||||
// `complete` runs once on the UI thread (or the calling thread when no queue is
|
||||
// set) with the raw reply payload on success, or an empty string on
|
||||
// failure/timeout. Returns whether the request was dispatched.
|
||||
int send_files_request(const std::string& dev_id, const std::string& command, nlohmann::json fields,
|
||||
std::function<void(int result, const std::string& payload)> complete);
|
||||
|
||||
// Consume an inbound report that answers a pending files.* request. Returns true
|
||||
// when it matched, so the caller does not forward it to the machine-state sink.
|
||||
bool try_consume_files_reply(const std::string& dev_id, const std::string& payload);
|
||||
|
||||
// Test hook: the ws:// URL connect_printer built for the current LAN session ("" if none).
|
||||
std::string lan_connection_target() const;
|
||||
// Shared post-connect sequence: SUBSCRIBE, then pushing.start, pushall,
|
||||
// info.get_version, info.get_capabilities. Runs identically on LAN and cloud.
|
||||
void on_connected(const std::string& dev_id, OrcaMqttConnection* conn, uint64_t generation);
|
||||
|
||||
// The post-connect command sequence, factored behind a seam so a test can
|
||||
// observe the SUBSCRIBE + 4 request payloads without a live OrcaMqttConnection.
|
||||
virtual void emit_connect_sequence(const std::string& dev_id,
|
||||
std::function<void(const std::string&)> subscribe,
|
||||
std::function<void(const std::string&)> request);
|
||||
static std::string seq(int n); // decimal string in the OrcaSlicer 20000..29999 band
|
||||
static std::string build_pushing_start(const std::string& sequence_id);
|
||||
static std::string build_pushing_stop(const std::string& sequence_id);
|
||||
static std::string build_pushall(const std::string& sequence_id);
|
||||
static std::string build_get_version(const std::string& sequence_id);
|
||||
static std::string build_get_capabilities(const std::string& sequence_id);
|
||||
|
||||
private:
|
||||
class OrcaSonarDiscovery;
|
||||
|
||||
std::string log_dir;
|
||||
std::string selected_machine;
|
||||
|
||||
enum CurrentConn { NONE, CLOUD, LAN };
|
||||
static const char* connection_type_name(CurrentConn connection);
|
||||
|
||||
// The transport for the printer currently selected by the UI. LAN and
|
||||
// cloud sessions have separate connection objects, so this is selection
|
||||
// state rather than an inference from whichever socket happens to exist.
|
||||
CurrentConn m_current_connection = NONE;
|
||||
|
||||
std::shared_ptr<ICloudServiceAgent> m_cloud_agent;
|
||||
std::shared_ptr<OrcaMqttConnection> lan_mqtt_connection;
|
||||
|
||||
// Two independent epochs: a cloud (de)selection must not fence the live LAN
|
||||
// feed, and vice versa. Each transport's connect thread and inbound handler
|
||||
// compare against their own counter only.
|
||||
std::atomic<uint64_t> m_lan_generation{0};
|
||||
std::atomic<uint64_t> m_cloud_generation{0};
|
||||
|
||||
// The short-lived threads that run the blocking initial connect for the current
|
||||
// LAN / cloud session. Joined members (never detached) so they cannot outlive
|
||||
// *this or the connection they hold a raw pointer to.
|
||||
std::thread m_lan_connect_thread;
|
||||
std::thread m_cloud_connect_thread;
|
||||
|
||||
std::unique_ptr<OrcaSonarDiscovery> m_discovery;
|
||||
|
||||
std::string m_lan_dev_id; // guarded by state_mutex
|
||||
std::string m_lan_url; // guarded by state_mutex — the Config.url of the live LAN session
|
||||
bool m_lan_use_ssl = false; // guarded by state_mutex
|
||||
std::string m_lan_ca_file; // guarded by state_mutex
|
||||
CameraStreamMode m_camera_stream_mode = CameraStreamMode::none; // guarded by state_mutex
|
||||
std::string m_camera_url; // guarded by state_mutex
|
||||
|
||||
// Pending files.* MQTT replies, keyed by sequence_id. The registry is
|
||||
// heap-owned so a timeout watchdog can outlive the agent without touching
|
||||
// `this`; each entry carries its own one-shot guard.
|
||||
struct FilesReply {
|
||||
std::string device_id;
|
||||
std::function<void(int, const std::string&)> complete;
|
||||
std::shared_ptr<std::atomic<bool>> fired = std::make_shared<std::atomic<bool>>(false);
|
||||
};
|
||||
struct FilesReplyRegistry {
|
||||
std::mutex mutex;
|
||||
std::unordered_map<std::string, FilesReply> entries;
|
||||
};
|
||||
std::shared_ptr<FilesReplyRegistry> m_files_replies = std::make_shared<FilesReplyRegistry>();
|
||||
|
||||
OrcaCloudServiceAgent* get_orca_cloud_agent();
|
||||
|
||||
OrcaMqttConnection* get_appropriate_mqtt_connection(bool is_lan = true);
|
||||
static bool parse_nonnegative_command_id(const std::string& value, int& result);
|
||||
|
||||
// Route one command payload to device/<dev_id>/request on the LAN or the shared
|
||||
// cloud connection. The uniform send path for both send_message* overrides.
|
||||
int route_send(bool is_lan, const std::string& dev_id, const std::string& json_str);
|
||||
|
||||
// Callbacks
|
||||
OnMsgArrivedFn on_ssdp_msg_fn;
|
||||
|
||||
@@ -52,6 +52,20 @@ static std::set<int> tools_for_role(const std::string& gcode, const std::string&
|
||||
return tools;
|
||||
}
|
||||
|
||||
// Every id named by a Tn command anywhere in `gcode`. A non-BBL single-extruder multi-filament
|
||||
// setup emits T<filament_id>, and that id is what the Klipper toolchange macro consumes.
|
||||
static std::set<int> tool_ids(const std::string& gcode)
|
||||
{
|
||||
std::set<int> tools;
|
||||
GCodeReader reader;
|
||||
reader.parse_buffer(gcode, [&tools](GCodeReader&, const GCodeReader::GCodeLine& line) {
|
||||
const std::string cmd(line.cmd());
|
||||
if (cmd.size() >= 2 && cmd[0] == 'T' && std::isdigit((unsigned char)cmd[1]))
|
||||
tools.insert(std::stoi(cmd.substr(1)));
|
||||
});
|
||||
return tools;
|
||||
}
|
||||
|
||||
// X where the nozzle sits while each tagged _WAIT_FOR_TEMP_ON_WIPE_TOWER M109 blocks:
|
||||
// the nearest preceding G1 carrying an X (the park travel emitted just before the wait).
|
||||
static std::vector<double> wait_park_xs(const std::string& gcode)
|
||||
@@ -299,6 +313,29 @@ TEST_CASE("Each feature prints with its assigned filament (three filaments)", "[
|
||||
CHECK(tools_for_role(gcode, "infill") == std::set<int>{ 1 }); // filament 2
|
||||
}
|
||||
|
||||
// The per-print wire mapping keys each entry by the filament's index in the config filament
|
||||
// arrays (the ams_mapping2 position). Pin that the id the toolchange emits is that index even
|
||||
// when the used filaments skip a middle one: filament 2 (index 1) is configured but unused, so
|
||||
// a renumbering to the used set would emit {0, 1} instead of {0, 2} and aim the map at the
|
||||
// wrong lane.
|
||||
TEST_CASE("Toolchange ids keep the config index of the used filament when a middle filament is unused", "[MultiFilament]")
|
||||
{
|
||||
const std::string gcode = slice({ cube(20) },
|
||||
multifilament_config(3, {
|
||||
{ "sparse_infill_filament_id", 1 },
|
||||
{ "internal_solid_filament_id", 1 },
|
||||
{ "top_surface_filament_id", 1 },
|
||||
{ "bottom_surface_filament_id", 1 },
|
||||
{ "outer_wall_filament_id", 3 },
|
||||
{ "inner_wall_filament_id", 3 },
|
||||
{ "skirt_loops", 0 },
|
||||
{ "brim_type", "no_brim" },
|
||||
}));
|
||||
CHECK(tools_for_role(gcode, "perimeter") == std::set<int>{ 2 }); // filament 3
|
||||
CHECK(tools_for_role(gcode, "infill") == std::set<int>{ 0 }); // filament 1
|
||||
CHECK(tool_ids(gcode) == std::set<int>{ 0, 2 });
|
||||
}
|
||||
|
||||
// The override must survive tool ordering: object 1's walls print on their filament's
|
||||
// tool, object 0 stays on the first. If dropped, every wall prints on tool 0.
|
||||
TEST_CASE("Per-object wall filament override is honored", "[MultiFilament]")
|
||||
|
||||
@@ -27,6 +27,7 @@ add_executable(${_TEST_NAME}_tests
|
||||
test_toolordering_nozzle_group.cpp
|
||||
test_parallel_resolve.cpp
|
||||
test_preset_bundle_loading.cpp
|
||||
test_preset_bundle_ams_sync.cpp
|
||||
test_preset_setting_id.cpp
|
||||
test_preset_diff.cpp
|
||||
test_vendor_cache.cpp
|
||||
|
||||
@@ -2043,3 +2043,40 @@ TEST_CASE("3MF XML entries declaring more than an int can hold fail to load", "[
|
||||
CHECK_FALSE(parser.check_3mf_from_prusa(path));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Loading a gcode.3mf from a stream keeps the plate's pick and pattern file paths", "[3mf][Regression]")
|
||||
{
|
||||
Model model;
|
||||
REQUIRE(load_stl((std::string(TEST_DATA_DIR) + "/test_3mf/Prusa.stl").c_str(), &model));
|
||||
model.add_default_instances();
|
||||
ScopedTemporaryDir backup_dir("orca_pick");
|
||||
model.set_backup_path(backup_dir.string());
|
||||
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
|
||||
|
||||
auto plate = std::make_unique<PlateData>();
|
||||
plate->plate_index = 0;
|
||||
plate->pick_file = (backup_dir.path() / "pick.png").string();
|
||||
plate->pattern_bbox_file = (backup_dir.path() / "plate.json").string();
|
||||
|
||||
ScopedTemporaryFile temp(".3mf");
|
||||
const std::string test_file = temp.string();
|
||||
StoreParams store_params;
|
||||
store_params.path = test_file.c_str();
|
||||
store_params.model = &model;
|
||||
store_params.config = &config;
|
||||
store_params.plate_data_list.push_back(plate.get());
|
||||
store_params.strategy = SaveStrategy::Zip64 | SaveStrategy::Silence;
|
||||
REQUIRE(store_bbs_3mf(store_params));
|
||||
|
||||
boost::nowide::ifstream in(test_file, std::ios::binary);
|
||||
Model dst_model;
|
||||
DynamicPrintConfig dst_config;
|
||||
PlateDataPtrs dst_plates;
|
||||
Semver file_version;
|
||||
dst_model.set_backup_path(backup_dir.string());
|
||||
REQUIRE(load_gcode_3mf_from_stream(in, &dst_config, &dst_model, &dst_plates, &file_version));
|
||||
REQUIRE(dst_plates.size() == 1);
|
||||
CHECK(dst_plates.front()->pick_file == "Metadata/pick_1.png");
|
||||
CHECK(dst_plates.front()->pattern_bbox_file == "Metadata/plate_1.json");
|
||||
release_PlateData_list(dst_plates);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,197 @@
|
||||
#include <catch2/catch_all.hpp>
|
||||
|
||||
#include <libslic3r/PrintConfig.hpp>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "catch2/catch_test_macros.hpp"
|
||||
#include "libslic3r/Preset.hpp"
|
||||
#include "libslic3r/PresetBundle.hpp"
|
||||
|
||||
using namespace Slic3r;
|
||||
|
||||
namespace {
|
||||
|
||||
// In-memory system filament preset. Mirrors the helper in test_preset_bundle_loading.cpp.
|
||||
Preset &add_system_filament(PresetBundle &bundle, const std::string &name, const std::string &filament_id, const std::string &filament_type)
|
||||
{
|
||||
DynamicPrintConfig config(bundle.filaments.default_preset().config);
|
||||
config.option<ConfigOptionString>(BBL_JSON_KEY_INHERITS, true)->value = "";
|
||||
config.option<ConfigOptionStrings>("filament_type", true)->values = {filament_type};
|
||||
Preset &preset = bundle.filaments.load_preset(std::string(), name, config, /*select=*/false);
|
||||
preset.is_system = true;
|
||||
preset.filament_id = filament_id;
|
||||
return preset;
|
||||
}
|
||||
|
||||
// One AMS tray entry as build_filament_ams_list produces it.
|
||||
DynamicPrintConfig make_tray(const std::string &filament_id, const std::string &filament_type, const std::string &color, const std::string &ams_id,
|
||||
const std::string &slot_id)
|
||||
{
|
||||
DynamicPrintConfig tray;
|
||||
tray.set_key_value("filament_id", new ConfigOptionStrings{filament_id});
|
||||
tray.set_key_value("filament_type", new ConfigOptionStrings{filament_type});
|
||||
tray.set_key_value("filament_colour", new ConfigOptionStrings{color});
|
||||
tray.set_key_value("filament_colour_type", new ConfigOptionStrings{"1"});
|
||||
tray.set_key_value("filament_multi_colour", new ConfigOptionStrings{});
|
||||
tray.set_key_value("ams_id", new ConfigOptionStrings{ams_id});
|
||||
tray.set_key_value("slot_id", new ConfigOptionStrings{slot_id});
|
||||
return tray;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// A tray the printer UI wrote carries a material type but no OrcaSlicer preset id. It must still
|
||||
// sync, resolved to Generic <type> with the tray's own color.
|
||||
TEST_CASE("AMS sync resolves a printer-set tray to Generic by material type", "[Preset][AMS]")
|
||||
{
|
||||
PresetBundle bundle;
|
||||
add_system_filament(bundle, "Generic PETG @Q2", "OFYPdQJh", "PETG");
|
||||
add_system_filament(bundle, "Generic PLA @Q2", "GFL99", "PLA");
|
||||
|
||||
bundle.filament_ams_list[0] = make_tray("OFYPdQJh", "PETG", "#FE717A", "0", "0"); // preset-backed
|
||||
bundle.filament_ams_list[1] = make_tray("", "PETG", "#898F9B", "0", "1"); // printer-set, no id
|
||||
bundle.filament_ams_list[2] = make_tray("", "PLA", "#FAFAFA", "0", "2"); // printer-set, no id
|
||||
bundle.filament_ams_list[3] = make_tray("", "", "#000000", "0", "3"); // empty slot
|
||||
|
||||
std::vector<std::pair<DynamicPrintConfig *, std::string>> unknowns;
|
||||
std::map<int, AMSMapInfo> maps;
|
||||
MergeFilamentInfo merge;
|
||||
|
||||
const unsigned int count = bundle.sync_ams_list(unknowns, /*use_map=*/false, maps, /*enable_append=*/false, merge);
|
||||
|
||||
CHECK(count == 3);
|
||||
CHECK(unknowns.empty());
|
||||
REQUIRE(bundle.filament_presets.size() == 3);
|
||||
CHECK(bundle.filament_presets[0] == "Generic PETG @Q2");
|
||||
CHECK(bundle.filament_presets[1] == "Generic PETG @Q2");
|
||||
CHECK(bundle.filament_presets[2] == "Generic PLA @Q2");
|
||||
|
||||
const ConfigOptionStrings *colors = bundle.project_config.option<ConfigOptionStrings>("filament_colour");
|
||||
REQUIRE(colors != nullptr);
|
||||
REQUIRE(colors->values.size() == 3);
|
||||
CHECK(colors->values[0] == "#FE717A");
|
||||
CHECK(colors->values[1] == "#898F9B");
|
||||
CHECK(colors->values[2] == "#FAFAFA");
|
||||
}
|
||||
|
||||
// Without a preset id and without a material type there is nothing to resolve; the tray stays
|
||||
// skipped in a direct sync.
|
||||
TEST_CASE("AMS sync skips a tray without a preset id or a material type", "[Preset][AMS]")
|
||||
{
|
||||
PresetBundle bundle;
|
||||
add_system_filament(bundle, "Generic PETG @Q2", "OFYPdQJh", "PETG");
|
||||
bundle.filament_ams_list[0] = make_tray("", "", "#000000", "0", "0");
|
||||
|
||||
std::vector<std::pair<DynamicPrintConfig *, std::string>> unknowns;
|
||||
std::map<int, AMSMapInfo> maps;
|
||||
MergeFilamentInfo merge;
|
||||
|
||||
CHECK(bundle.sync_ams_list(unknowns, /*use_map=*/false, maps, /*enable_append=*/false, merge) == 0);
|
||||
CHECK(bundle.filament_presets.empty());
|
||||
}
|
||||
|
||||
// Guard the pre-existing behavior: a non-empty id that matches no preset still resolves by type.
|
||||
TEST_CASE("AMS sync still resolves an unmatched preset id by material type", "[Preset][AMS]")
|
||||
{
|
||||
PresetBundle bundle;
|
||||
add_system_filament(bundle, "Generic PETG @Q2", "OFYPdQJh", "PETG");
|
||||
bundle.filament_ams_list[0] = make_tray("REMOVED_ID", "PETG", "#123456", "0", "0");
|
||||
|
||||
std::vector<std::pair<DynamicPrintConfig *, std::string>> unknowns;
|
||||
std::map<int, AMSMapInfo> maps;
|
||||
MergeFilamentInfo merge;
|
||||
|
||||
CHECK(bundle.sync_ams_list(unknowns, /*use_map=*/false, maps, /*enable_append=*/false, merge) == 1);
|
||||
CHECK(unknowns.size() == 1);
|
||||
REQUIRE(bundle.filament_presets.size() == 1);
|
||||
CHECK(bundle.filament_presets[0] == "Generic PETG @Q2");
|
||||
}
|
||||
|
||||
// Mapping-mode sync used to force "Generic PLA" for an id-less tray; it must use the type instead.
|
||||
TEST_CASE("AMS sync in mapping mode resolves a printer-set tray to Generic, not Generic PLA", "[Preset][AMS]")
|
||||
{
|
||||
PresetBundle bundle;
|
||||
add_system_filament(bundle, "Generic PETG @Q2", "OFYPdQJh", "PETG");
|
||||
add_system_filament(bundle, "Generic PLA @Q2", "GFL99", "PLA");
|
||||
|
||||
bundle.filament_presets = {"Generic PLA @Q2"};
|
||||
bundle.project_config.option<ConfigOptionStrings>("filament_colour")->values = {"#000000"};
|
||||
bundle.project_config.option<ConfigOptionStrings>("filament_colour_type")->values = {"1"};
|
||||
|
||||
bundle.filament_ams_list[0] = make_tray("", "PETG", "#898F9B", "0", "1");
|
||||
|
||||
std::map<int, AMSMapInfo> maps;
|
||||
maps[0] = AMSMapInfo{"0", "1"};
|
||||
|
||||
std::vector<std::pair<DynamicPrintConfig *, std::string>> unknowns;
|
||||
MergeFilamentInfo merge;
|
||||
|
||||
const unsigned int count = bundle.sync_ams_list(unknowns, /*use_map=*/true, maps, /*enable_append=*/false, merge);
|
||||
|
||||
CHECK(count == 1);
|
||||
CHECK(unknowns.empty());
|
||||
REQUIRE(bundle.filament_presets.size() == 1);
|
||||
CHECK(bundle.filament_presets[0] == "Generic PETG @Q2");
|
||||
CHECK(bundle.project_config.option<ConfigOptionStrings>("filament_colour")->values[0] == "#898F9B");
|
||||
}
|
||||
|
||||
// The dialog builds ams_mapping2 one entry per filament_presets index, while the generated
|
||||
// G-code's toolchange names the filament's index in the slice's filament config arrays. Those
|
||||
// are the same index only if full_config() collects the filament arrays in filament_presets
|
||||
// order; if a refactor reorders one side, every mapped print aims at the wrong lane.
|
||||
TEST_CASE("Full config filament arrays follow the selected filament preset order", "[Preset][AMS]")
|
||||
{
|
||||
PresetBundle bundle;
|
||||
add_system_filament(bundle, "Generic PETG @Q2", "OFYPdQJh", "PETG");
|
||||
add_system_filament(bundle, "Generic PLA @Q2", "GFL99", "PLA");
|
||||
|
||||
// Deliberately not the load order: the arrays must follow this selection.
|
||||
bundle.filament_presets = {"Generic PLA @Q2", "Generic PETG @Q2"};
|
||||
|
||||
const DynamicPrintConfig full = bundle.full_config(/*apply_extruder=*/false);
|
||||
REQUIRE(full.option<ConfigOptionStrings>("filament_type") != nullptr);
|
||||
CHECK(full.option<ConfigOptionStrings>("filament_settings_id")->values == std::vector<std::string>{"Generic PLA @Q2", "Generic PETG @Q2"});
|
||||
CHECK(full.option<ConfigOptionStrings>("filament_type")->values == std::vector<std::string>{"PLA", "PETG"});
|
||||
// The AMS sync matches trays by filament_id, so this array must follow the same order.
|
||||
CHECK(full.option<ConfigOptionStrings>("filament_ids")->values == std::vector<std::string>{"GFL99", "OFYPdQJh"});
|
||||
}
|
||||
|
||||
// get_ams_cobox_infos builds the combo arrays that SyncAmsInfoDialog pairs with AMS colors
|
||||
// positionally. A printer-set tray must appear so the array stays aligned with the sync output.
|
||||
TEST_CASE("AMS combo info keeps a printer-set tray and stays aligned", "[Preset][AMS]")
|
||||
{
|
||||
PresetBundle bundle;
|
||||
add_system_filament(bundle, "Generic PETG @Q2", "OFYPdQJh", "PETG");
|
||||
add_system_filament(bundle, "Generic PLA @Q2", "GFL99", "PLA");
|
||||
|
||||
auto tray_a = make_tray("OFYPdQJh", "PETG", "#FE717A", "0", "0");
|
||||
tray_a.set_key_value("tray_name", new ConfigOptionStrings{"A"});
|
||||
auto tray_b = make_tray("", "PETG", "#898F9B", "0", "1");
|
||||
tray_b.set_key_value("tray_name", new ConfigOptionStrings{"B"});
|
||||
auto tray_c = make_tray("GFL99", "PLA", "#FAFAFA", "0", "2");
|
||||
tray_c.set_key_value("tray_name", new ConfigOptionStrings{"C"});
|
||||
bundle.filament_ams_list[0] = tray_a;
|
||||
bundle.filament_ams_list[1] = tray_b;
|
||||
bundle.filament_ams_list[2] = tray_c;
|
||||
|
||||
AMSComboInfo combo_info;
|
||||
bundle.get_ams_cobox_infos(combo_info);
|
||||
|
||||
// The id-less PETG tray resolves to a generic preset instead of being dropped, so C keeps
|
||||
// its index and its color.
|
||||
REQUIRE(combo_info.ams_filament_presets.size() == 3);
|
||||
CHECK(combo_info.ams_filament_presets[0] == "Generic PETG @Q2");
|
||||
CHECK(combo_info.ams_filament_presets[1] == "Generic PETG @Q2");
|
||||
CHECK(combo_info.ams_filament_presets[2] == "Generic PLA @Q2");
|
||||
REQUIRE(combo_info.ams_filament_colors.size() == 3);
|
||||
CHECK(combo_info.ams_filament_colors[0] == "#FE717A");
|
||||
CHECK(combo_info.ams_filament_colors[1] == "#898F9B");
|
||||
CHECK(combo_info.ams_filament_colors[2] == "#FAFAFA");
|
||||
REQUIRE(combo_info.ams_names.size() == 3);
|
||||
CHECK(combo_info.ams_names[0] == "A");
|
||||
CHECK(combo_info.ams_names[1] == "B");
|
||||
CHECK(combo_info.ams_names[2] == "C");
|
||||
}
|
||||
@@ -1604,6 +1604,23 @@ TEST_CASE("Filaments offered for a machine follow the app's compatibility rule",
|
||||
}
|
||||
}
|
||||
|
||||
// An agent-reported model id (SSDP modelNumber, manual binding) must resolve to the display
|
||||
// name printer profiles use as printer_model; the AMS dialogs look filaments up by it.
|
||||
TEST_CASE("Printer model ids resolve to vendor display names", "[Preset][Bundle]")
|
||||
{
|
||||
PresetBundle bundle;
|
||||
VendorProfile qidi("Qidi");
|
||||
qidi.name = "Qidi";
|
||||
VendorProfile::PrinterModel model;
|
||||
model.model_id = "Qidi-Q1Pro";
|
||||
model.name = "Qidi Q1 Pro";
|
||||
qidi.models.push_back(model);
|
||||
bundle.vendors.emplace(qidi.id, qidi);
|
||||
|
||||
CHECK(bundle.get_printer_model_display_name("Qidi-Q1Pro") == "Qidi Q1 Pro");
|
||||
CHECK(bundle.get_printer_model_display_name("unknown-model").empty());
|
||||
}
|
||||
|
||||
|
||||
namespace {
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user