Compare commits

..
Author SHA1 Message Date
Lam Wei Lun f608698439 Initial Commit for switching files operations for OrcaPrinterAgent to using MQTT 2026-10-06 18:25:16 +08:00
Lam Wei Lun 432d6be518 Print/Delete support for .gcode.3mf files 2026-10-06 15:42:32 +08:00
Ian Chua 3e7f0c027a fix: fixes from run_clang_tidy.sh --fix 2026-10-06 14:54:51 +08:00
Lam Wei Lun 504a143b98 Missing headers 2026-10-06 14:19:42 +08:00
Lam Wei Lun a87639ac41 Merge main and fix conflicts 2026-10-06 13:43:22 +08:00
Lam Wei Lun ecbd03fed7 merge main 2026-10-06 13:08:46 +08:00
Lam Wei Lun ccbb73baaf ## Summary
Follow-up to the feature/OrcaSonar review. Scopes the new behavior to the
OrcaSonar agent, fixes several AMS-sync and lifecycle bugs, and makes the
MQTT tests run in CI. Non-OrcaSonar agents keep their existing behavior.

## Changes

Discovery
- Gate the background SSDP scan to a configured OrcaSonar machine instead of
  running for every non-BBL profile; drive discovery on demand from the
  machine-select popup and back off idle rounds from 5s to 60s.
- Add NSLocalNetworkUsageDescription and NSBonjourServices to both macOS
  Info.plist templates.

AMS sync
- Resolve id-less printer-set trays by material type in get_ams_cobox_infos
  and the sidebar badge loop, matching sync_ams_list, so combo arrays and
  badges stay aligned.
- Keep a printer-set tray that resolved by type out of the "unknown or
  incompatible filaments" dialog.

AMS device UI
- Gate the "Empty" tray classification, and the editable temperature fields,
  to the OrcaSonar agent; Bambu keeps "?" and read-only presets.
- Validate AMS material temps on confirm: reject out-of-range values and
  min > max before the command is sent.

Storage tab
- Bind the show/hide handler on the panel's ancestors (StoragePanel and
  MonitorPanel) and unbind on destruction, so the Bambu file system stops
  when the tab is left.

Transport
- Load /etc/ssl/cert.pem on macOS in add_platform_root_certificates, after
  set_default_verify_paths.
- Release lifecycle_mutex before start()'s connect wait so stop() can cancel
  the attempt and signal completion instead of stalling the UI thread.

Localization
- Add OrcaFilesPanel.cpp to list.txt and regenerate the catalogs.

## Tests
- Un-hide the loopback MockBroker integration tests (ephemeral port, no
  network) so ctest registers and runs them.
- Mock broker tracks SUBSCRIBE/UNSUBSCRIBE and delivers push_report only to a
  subscribed topic; the reconnect test asserts the re-SUBSCRIBE.
- Add bbl empty-tray cases, an AMS combo-alignment case, and unknowns
  assertions to the AMS sync tests.
2026-10-06 12:09:23 +08:00
Ian Chua 029bc7928f AMS Sync, Filament Mapping, Storage Tab (#16156)
## Summary

Adds the remaining OrcaPrinterAgent work on top of feature/OrcaSonar:
connector capability gating, AMS filament mapping, and the printer
Storage tab. Contains no changes to the other printer agents (Moonraker,
Qidi, Snapmaker, BBL, Creality) — that refactor belongs to its own
branch.

## Changes

Capability gating (IPrinterAgent, OrcaPrinterAgent)

• Parse supported_features, capabilities.protocol.features, and
supported_commands from get_capabilities, including replies without a
flags block.
• Add supports_command, supports_feature, and uses_filament_mapping
queries with defaults that preserve existing agent behavior.
• Gate macro-backed AMS commands on fms plus the advertised command, and
slot metadata writes on filament_slots. Unknown capability means not
supported.

## AMS filament mapping

• Serialize ams_mapping2 into print.gcode_file.filament_mapping, keyed
by array position, dropping {255,255}; omit when empty so unmapped
prints stay byte-identical.
• Refuse a mapped print visibly until the correlation flag and
filament_mapping capability are satisfied, and refuse partially mapped
prints with an unmapped used filament. The GUI predicate shares the
serializer's definition.
• Resolve printer-set trays to the matching Generic preset by material
type and color; skip forced external-spool selection on printers without
an AMS.
• start_local_print_with_record returns non-success so PrintJob falls
back to start_print.

## AMS device UI on non-Bambu agents

• Resolve the printer model from the agent model id or selected profile
so the AMS Materials Setting dropdown and Confirm button work;
third-party trays keep editable temps.
• Render explicitly-empty trays as "Empty" via a wire-derived is_empty,
keeping the slot editable.
• Hide flow-dynamics K/N controls for non-Bambu agents, leaving Bambu
behavior unchanged.

## Storage tab (printer G-code files)

• New IPrinterAgent file APIs — list_printer_files,
get_printer_file_thumbnail, delete_printer_file,
get_printer_file_metadata — with ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED
defaults.
• NetworkAgent forwards to the bound agent; OrcaPrinterAgent implements
them against /server/files.
• New StoragePanel / OrcaFilesPanel and a Printer/ file-grid abstraction
(IFileGridModel, RemoteFileGridModel, BambuFileGridModel).

## Tests

Covers serializer keying/omission, GUI/agent gate agreement, payload
shape, capability parsing and per-command AMS gating, model resolution,
empty-tray classification, and AMS sync.
2026-10-05 18:52:45 +08:00
Lam Wei Lun 0a8fce0913 Add support for printing gcode files in the Storage tab 2026-10-05 18:22:29 +08:00
Lam Wei Lun 3942ba7005 OrcaPrinterAgent Storage Tab 2026-10-05 18:22:29 +08:00
Lam Wei Lun 0110572a77 Removal of specific ORCA_PRINTER_AGENT_ID usage in DeviceManager/SelectMachine/SendMultiMachinePage. Generalized the capabilities under IPrinterAgent 2026-10-05 18:22:29 +08:00
Lam Wei Lun 2c2869ab07 fix: make the AMS device UI work on non-Bambu printer agents
Resolve the filament list's printer model from the agent-reported model id or the selected printer profile, so the AMS Materials Setting dropdown and its Confirm button work on OrcaSonar. Third-party trays pre-fill and allow editing their temps, and the popup's initial selection no longer overwrites them.

Render explicitly-empty trays as "Empty" instead of "?" using a wire-derived DevAmsTray::is_empty, while keeping the slot editable.

Hide the flow-dynamics K/N controls for non-Bambu agents — tile, dialog, validation and sends — with Bambu behavior unchanged.

Tests cover model resolution, the K/N gate and empty-tray classification.
2026-10-05 18:22:29 +08:00
Lam Wei Lun bf63eb1226 feat: enable filament mapping and resolve printer-set AMS trays by material type
Drop the index-correlation gate so mapped prints are gated only on the filament_mapping capability
Resolve AMS trays set on the printer UI to the matching Generic preset using their material type and color
Skip the forced external-spool selection on printers without an AMS and add coverage for the mapping gates
2026-10-05 18:22:26 +08:00
Lam Wei Lun 38e57888a5 feat: add filament mapping and AMS capability gates for the orca printer agent
Parse connector-scope capabilities from get_capabilities: supported_features, capabilities.protocol.features and supported_commands are read for orca devices even when the reply has no flags block, reset before each parse, and left untouched for Bambu.

Gate macro-backed AMS commands on fms plus the advertised command (change filament, select tray, control, user settings, RFID, drying stop); gate slot metadata writes on filament_slots. Unknown capability is not support.

Serialize PrintParams::ams_mapping2 into print.gcode_file.filament_mapping with the array position as filament_index and {255,255} dropped. The field is omitted when empty, so an unmapped print stays byte-identical. A mapped print is refused visibly while ORCA_FILAMENT_MAPPING_CORRELATION_VERIFIED is false or the connector did not advertise filament_mapping; the GUI predicate shares the serializer's definition so both gates agree. Refuse a partially mapped print that leaves a used filament without a target.

Return a non-success result from start_local_print_with_record so PrintJob falls back to start_print instead of treating an unsent print as success.

Tests cover serializer keying and omission, GUI/agent gate agreement, payload shape, the with_record result, capability parsing, and the per-command AMS gate.
2026-10-05 18:22:23 +08:00
Ian Chua fc079b2e5b test: add OrcaSonar printer agent coverage 2026-10-05 16:40:22 +08:00
Ian Chua a88843c6f8 feat: initial implementation of OrcaSonar Printer Agent 2026-10-05 16:15:17 +08:00
114 changed files with 11005 additions and 1380 deletions
+7
View File
@@ -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 -->
+44 -10
View File
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-05 10:49-0300\n"
"POT-Creation-Date: 2026-10-06 13:42+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"
@@ -4046,6 +4046,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 ""
@@ -5782,7 +5785,7 @@ msgstr ""
msgid "Size:"
msgstr ""
#, possible-c-format, possible-boost-format
#, possible-boost-format
msgid "Conflicts of G-code paths have been found at layer %d, Z = %.2lfmm. Please separate the conflicted objects farther (%s <-> %s)."
msgstr ""
@@ -6621,6 +6624,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"
@@ -9464,14 +9483,6 @@ msgstr ""
msgid "Show incompatible/unsupported presets in the printer and filament dropdown lists. These presets cannot be selected."
msgstr ""
msgid "(Experimental) Use printer agents instead of print hosts"
msgstr ""
msgid ""
"Route print jobs for non-Bambu printers through printer plug-in agents instead of the classic print-host upload flow.\n"
"When disabled, OrcaSlicer uses the legacy print-host behavior."
msgstr ""
msgid "Experimental Features"
msgstr ""
@@ -9483,6 +9494,14 @@ msgid ""
"Highly experimental! Slow and may create artifact."
msgstr ""
msgid "Use printer agents instead of print hosts"
msgstr ""
msgid ""
"Route print jobs for non-Bambu printers through printer plug-in agents instead of the classic print-host upload flow.\n"
"When disabled, OrcaSlicer uses the legacy print-host behavior."
msgstr ""
msgid "Allow Abnormal Storage"
msgstr ""
@@ -10188,6 +10207,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 ""
@@ -14779,6 +14804,12 @@ msgstr ""
msgid "If the model has two parts that are symmetric about the Y axis, and you want these parts to have symmetric textures, please click this option on one of the parts."
msgstr ""
msgid "Fill pattern tops"
msgstr ""
msgid "Choose this option if you want to completely fill in the tops of the infill pattern"
msgstr ""
msgid "Infill combination - Max layer height"
msgstr ""
@@ -26300,6 +26331,9 @@ msgstr ""
msgid "An error occurred loading %s"
msgstr ""
msgid "/"
msgstr ""
#, possible-boost-format
msgid "Load %1% to %2%"
msgstr ""
+51 -17
View File
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-05 10:49-0300\n"
"POT-Creation-Date: 2026-10-06 13:42+0800\n"
"PO-Revision-Date: 2025-03-15 10:55+0100\n"
"Last-Translator: \n"
"Language-Team: \n"
@@ -4426,6 +4426,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)"
@@ -6300,7 +6303,7 @@ msgid "Size:"
msgstr "Mida:"
# AI Translated
#, c-format, boost-format
#, boost-format
msgid "Conflicts of G-code paths have been found at layer %d, Z = %.2lfmm. Please separate the conflicted objects farther (%s <-> %s)."
msgstr "S'han trobat conflictes de rutes de G-code a la capa %d, Z = %.2lfmm. Si us plau, separeu els objectes conflictius més lluny ( %s <-> %s )."
@@ -7180,6 +7183,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"
@@ -10420,18 +10439,6 @@ msgstr "Mostrar els perfils no compatibles"
msgid "Show incompatible/unsupported presets in the printer and filament dropdown lists. These presets cannot be selected."
msgstr "Mostra els perfils incompatibles o no compatibles a les llistes desplegables d'impressora i de filament. Aquests perfils no es poden seleccionar."
# AI Translated
msgid "(Experimental) Use printer agents instead of print hosts"
msgstr "(Experimental) Utilitza agents d'impressora en lloc d'amfitrions d'impressió"
# AI Translated
msgid ""
"Route print jobs for non-Bambu printers through printer plug-in agents instead of the classic print-host upload flow.\n"
"When disabled, OrcaSlicer uses the legacy print-host behavior."
msgstr ""
"Envia els treballs d'impressió de les impressores que no són Bambu a través dels agents de connector d'impressora en lloc del flux clàssic de pujada a l'amfitrió d'impressió.\n"
"Quan està desactivat, OrcaSlicer utilitza el comportament antic de l'amfitrió d'impressió."
# AI Translated
msgid "Experimental Features"
msgstr "Funcions experimentals"
@@ -10448,6 +10455,17 @@ msgstr ""
"Intenta conservar les característiques pintades (color/costura/suport/pell difusa, etc.) després de canviar la malla de l'objecte (com ara tallar, recarregar des del disc, simplificar, reparar, etc.).\n"
"Molt experimental! És lent i pot generar defectes."
msgid "Use printer agents instead of print hosts"
msgstr ""
# AI Translated
msgid ""
"Route print jobs for non-Bambu printers through printer plug-in agents instead of the classic print-host upload flow.\n"
"When disabled, OrcaSlicer uses the legacy print-host behavior."
msgstr ""
"Envia els treballs d'impressió de les impressores que no són Bambu a través dels agents de connector d'impressora en lloc del flux clàssic de pujada a l'amfitrió d'impressió.\n"
"Quan està desactivat, OrcaSlicer utilitza el comportament antic de l'amfitrió d'impressió."
msgid "Allow Abnormal Storage"
msgstr "Permet emmagatzematge anormal"
@@ -11263,6 +11281,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ó"
@@ -16564,6 +16588,12 @@ msgstr "Farciment simètric eix Y"
msgid "If the model has two parts that are symmetric about the Y axis, and you want these parts to have symmetric textures, please click this option on one of the parts."
msgstr "Si el model té dues parts simètriques respecte a l'eix Y i voleu que aquestes parts tinguin textures simètriques, feu clic en aquesta opció en una de les parts."
msgid "Fill pattern tops"
msgstr ""
msgid "Choose this option if you want to completely fill in the tops of the infill pattern"
msgstr ""
msgid "Infill combination - Max layer height"
msgstr "Combinació de farciment: alçada màxima de la capa"
@@ -30507,6 +30537,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%"
@@ -32210,9 +32243,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 "
@@ -32418,6 +32448,10 @@ msgstr ""
#~ msgid "Support/Color Painting: adjust section position"
#~ msgstr "Suport/Pintat de color: configuració de la posició de la secció"
# AI Translated
#~ msgid "(Experimental) Use printer agents instead of print hosts"
#~ msgstr "(Experimental) Utilitza agents d'impressora en lloc d'amfitrions d'impressió"
# AI Translated
#~ msgid "Tab"
#~ msgstr "Pestanya"
+52 -18
View File
@@ -3,7 +3,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Orca Slicer\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-05 10:49-0300\n"
"POT-Creation-Date: 2026-10-06 13:42+0800\n"
"PO-Revision-Date: \n"
"Last-Translator: Jakub Hencl\n"
"Language-Team: \n"
@@ -4384,6 +4384,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)"
@@ -6269,7 +6272,7 @@ msgid "Size:"
msgstr "Velikost:"
# AI Translated
#, c-format, boost-format
#, boost-format
msgid "Conflicts of G-code paths have been found at layer %d, Z = %.2lfmm. Please separate the conflicted objects farther (%s <-> %s)."
msgstr "Byly nalezeny konflikty drah G-code ve vrstvě %d, Z = %.2lf mm. Oddělte prosím konfliktní objekty více od sebe (%s <-> %s)."
@@ -7159,6 +7162,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"
@@ -10382,18 +10401,6 @@ msgstr "Zobrazit nepodporované předvolby"
msgid "Show incompatible/unsupported presets in the printer and filament dropdown lists. These presets cannot be selected."
msgstr "Zobrazovat nekompatibilní/nepodporované předvolby v rozevíracích seznamech tiskáren a filamentů. Tyto předvolby nelze vybrat."
# AI Translated
msgid "(Experimental) Use printer agents instead of print hosts"
msgstr "(Experimentální) Používat agenty tiskárny místo tiskových hostů"
# AI Translated
msgid ""
"Route print jobs for non-Bambu printers through printer plug-in agents instead of the classic print-host upload flow.\n"
"When disabled, OrcaSlicer uses the legacy print-host behavior."
msgstr ""
"Směruje tiskové úlohy pro tiskárny jiné než Bambu přes agenty pluginů tiskárny místo klasického nahrávání na tiskový host.\n"
"Pokud je vypnuto, OrcaSlicer používá původní chování tiskového hosta."
# AI Translated
msgid "Experimental Features"
msgstr "Experimentální funkce"
@@ -10410,6 +10417,17 @@ msgstr ""
"Pokusí se zachovat malované prvky (barva/šev/podpory/fuzzy atd.) po změně sítě objektu (například řez / znovunačtení z disku / zjednodušení / oprava atd.)\n"
"Vysoce experimentální! Pomalé a může vytvářet artefakty."
msgid "Use printer agents instead of print hosts"
msgstr ""
# AI Translated
msgid ""
"Route print jobs for non-Bambu printers through printer plug-in agents instead of the classic print-host upload flow.\n"
"When disabled, OrcaSlicer uses the legacy print-host behavior."
msgstr ""
"Směruje tiskové úlohy pro tiskárny jiné než Bambu přes agenty pluginů tiskárny místo klasického nahrávání na tiskový host.\n"
"Pokud je vypnuto, OrcaSlicer používá původní chování tiskového hosta."
msgid "Allow Abnormal Storage"
msgstr "Povolit neobvyklé úložiště"
@@ -11227,6 +11245,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"
@@ -16534,6 +16558,12 @@ msgstr "Symetrická výplň podle osy Y"
msgid "If the model has two parts that are symmetric about the Y axis, and you want these parts to have symmetric textures, please click this option on one of the parts."
msgstr "Pokud má model dvě části symetrické podle osy Y a chcete, aby měly symetrické textury, klikněte na tuto možnost u jedné z těchto částí."
msgid "Fill pattern tops"
msgstr ""
msgid "Choose this option if you want to completely fill in the tops of the infill pattern"
msgstr ""
msgid "Infill combination - Max layer height"
msgstr "Kombinace výplně – maximální výška vrstvy"
@@ -30517,6 +30547,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%"
@@ -32226,10 +32260,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 "
@@ -32436,6 +32466,10 @@ msgstr ""
#~ msgid "Support/Color Painting: adjust section position"
#~ msgstr "Podpora/barva malování: nastavení pozice úseku"
# AI Translated
#~ msgid "(Experimental) Use printer agents instead of print hosts"
#~ msgstr "(Experimentální) Používat agenty tiskárny místo tiskových hostů"
# AI Translated
#~ msgid "Tab"
#~ msgstr "Tab"
+51 -17
View File
@@ -3,7 +3,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Orca Slicer\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-05 10:49-0300\n"
"POT-Creation-Date: 2026-10-06 13:42+0800\n"
"PO-Revision-Date: \n"
"Last-Translator: Heiko Liebscher <hliebschergmail.com>\n"
"Language-Team: \n"
@@ -4297,6 +4297,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)"
@@ -6159,7 +6162,7 @@ msgstr "Volumen:"
msgid "Size:"
msgstr "Größe:"
#, c-format, boost-format
#, boost-format
msgid "Conflicts of G-code paths have been found at layer %d, Z = %.2lfmm. Please separate the conflicted objects farther (%s <-> %s)."
msgstr "Konflikte von G-Code-Pfaden wurden bei Layer %d, Z = %.2lf mm gefunden.Bitte trennen Sie die konfliktbehafteten Objekte weiter voneinander (%s <-> %s)."
@@ -7027,6 +7030,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"
@@ -10211,18 +10230,6 @@ msgstr "Nicht unterstützte Profile anzeigen"
msgid "Show incompatible/unsupported presets in the printer and filament dropdown lists. These presets cannot be selected."
msgstr "Zeigt inkompatible/nicht unterstützte Profile in den Dropdown-Listen für Drucker und Filament an. Diese Profile können nicht ausgewählt werden."
# AI Translated
msgid "(Experimental) Use printer agents instead of print hosts"
msgstr "(Experimentell) Drucker-Agenten anstelle von Druck-Hosts verwenden"
# AI Translated
msgid ""
"Route print jobs for non-Bambu printers through printer plug-in agents instead of the classic print-host upload flow.\n"
"When disabled, OrcaSlicer uses the legacy print-host behavior."
msgstr ""
"Leitet Druckaufträge für Nicht-Bambu-Drucker über Drucker-Plugin-Agenten statt über den klassischen Druck-Host-Upload.\n"
"Wenn deaktiviert, verwendet OrcaSlicer das bisherige Druck-Host-Verhalten."
msgid "Experimental Features"
msgstr "Experimentelle Funktionen"
@@ -10236,6 +10243,17 @@ msgstr ""
"Versuchen Sie, bemalte Funktionen (Farbe/Naht/Stütze/unscharf usw.) nach Änderung des Objekt-Meshs (z. B. schneiden/neu laden von der Festplatte/vereinfachen/reparieren usw.) beizubehalten\n"
"Sehr experimentell! Langsam und kann Artefakte erzeugen."
msgid "Use printer agents instead of print hosts"
msgstr ""
# AI Translated
msgid ""
"Route print jobs for non-Bambu printers through printer plug-in agents instead of the classic print-host upload flow.\n"
"When disabled, OrcaSlicer uses the legacy print-host behavior."
msgstr ""
"Leitet Druckaufträge für Nicht-Bambu-Drucker über Drucker-Plugin-Agenten statt über den klassischen Druck-Host-Upload.\n"
"Wenn deaktiviert, verwendet OrcaSlicer das bisherige Druck-Host-Verhalten."
msgid "Allow Abnormal Storage"
msgstr "Fehlerhaften Speicher zulassen"
@@ -11046,6 +11064,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"
@@ -16211,6 +16235,12 @@ msgstr "Symmetrische Füllung Y-Achse"
msgid "If the model has two parts that are symmetric about the Y axis, and you want these parts to have symmetric textures, please click this option on one of the parts."
msgstr "Wenn das Modell zwei Teile hat, die symmetrisch zur Y-Achse sind, und Sie möchten, dass diese Teile symmetrische Texturen haben, klicken Sie bitte auf diese Option auf einem der Teile."
msgid "Fill pattern tops"
msgstr ""
msgid "Choose this option if you want to completely fill in the tops of the infill pattern"
msgstr ""
msgid "Infill combination - Max layer height"
msgstr "Kombinieren der Füllung - Maximale Schichthöhe"
@@ -29903,6 +29933,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%"
@@ -31604,9 +31637,6 @@ msgstr ""
#~ msgid "An error occurred loading "
#~ msgstr "Fehler beim Laden von "
#~ msgid "/"
#~ msgstr "/"
# AI Translated
#, c-format, boost-format
#~ msgid "Load %s to "
@@ -31812,6 +31842,10 @@ msgstr ""
#~ msgid "Support/Color Painting: adjust section position"
#~ msgstr "Stützen/Farbmalen: Position des Abschnitts anpassen"
# AI Translated
#~ msgid "(Experimental) Use printer agents instead of print hosts"
#~ msgstr "(Experimentell) Drucker-Agenten anstelle von Druck-Hosts verwenden"
# AI Translated
#~ msgid "Tab"
#~ msgstr "Tab"
+44 -10
View File
@@ -3,7 +3,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Orca Slicer\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-05 10:49-0300\n"
"POT-Creation-Date: 2026-10-06 13:42+0800\n"
"PO-Revision-Date: 2026-06-17 15:44-0300\n"
"Last-Translator: Alexandre Folle de Menezes\n"
"Language-Team: \n"
@@ -4042,6 +4042,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 ""
@@ -5778,7 +5781,7 @@ msgstr ""
msgid "Size:"
msgstr ""
#, c-format, boost-format
#, boost-format
msgid "Conflicts of G-code paths have been found at layer %d, Z = %.2lfmm. Please separate the conflicted objects farther (%s <-> %s)."
msgstr ""
@@ -6617,6 +6620,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"
@@ -9460,14 +9479,6 @@ msgstr ""
msgid "Show incompatible/unsupported presets in the printer and filament dropdown lists. These presets cannot be selected."
msgstr ""
msgid "(Experimental) Use printer agents instead of print hosts"
msgstr ""
msgid ""
"Route print jobs for non-Bambu printers through printer plug-in agents instead of the classic print-host upload flow.\n"
"When disabled, OrcaSlicer uses the legacy print-host behavior."
msgstr ""
msgid "Experimental Features"
msgstr ""
@@ -9479,6 +9490,14 @@ msgid ""
"Highly experimental! Slow and may create artifact."
msgstr ""
msgid "Use printer agents instead of print hosts"
msgstr ""
msgid ""
"Route print jobs for non-Bambu printers through printer plug-in agents instead of the classic print-host upload flow.\n"
"When disabled, OrcaSlicer uses the legacy print-host behavior."
msgstr ""
msgid "Allow Abnormal Storage"
msgstr ""
@@ -10184,6 +10203,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 ""
@@ -14775,6 +14800,12 @@ msgstr ""
msgid "If the model has two parts that are symmetric about the Y axis, and you want these parts to have symmetric textures, please click this option on one of the parts."
msgstr ""
msgid "Fill pattern tops"
msgstr ""
msgid "Choose this option if you want to completely fill in the tops of the infill pattern"
msgstr ""
msgid "Infill combination - Max layer height"
msgstr ""
@@ -26296,6 +26327,9 @@ msgstr ""
msgid "An error occurred loading %s"
msgstr ""
msgid "/"
msgstr ""
#, boost-format
msgid "Load %1% to %2%"
msgstr ""
+51 -17
View File
@@ -3,7 +3,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Orca Slicer\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-05 10:49-0300\n"
"POT-Creation-Date: 2026-10-06 13:42+0800\n"
"PO-Revision-Date: \n"
"Last-Translator: Ian A. Bassi <>\n"
"Language-Team: \n"
@@ -4175,6 +4175,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)"
@@ -6012,7 +6015,7 @@ msgstr "Volumen:"
msgid "Size:"
msgstr "Tamaño:"
#, c-format, boost-format
#, boost-format
msgid "Conflicts of G-code paths have been found at layer %d, Z = %.2lfmm. Please separate the conflicted objects farther (%s <-> %s)."
msgstr "Se han encontrado conflictos de rutas G-Code en la capa %d, Z = %.2lfmm. Por favor, separe más los objetos en conflicto (%s <-> %s)."
@@ -6879,6 +6882,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"
@@ -9972,18 +9991,6 @@ msgstr "Mostrar ajustes preestablecidos no compatibles"
msgid "Show incompatible/unsupported presets in the printer and filament dropdown lists. These presets cannot be selected."
msgstr "Mostrar los ajustes preestablecidos incompatibles o no compatibles en los menús desplegables de impresoras y filamentos. Estos ajustes preestablecidos no se pueden seleccionar."
# AI Translated
msgid "(Experimental) Use printer agents instead of print hosts"
msgstr "(Experimental) Usar agentes de impresora en lugar de hosts de impresión"
# AI Translated
msgid ""
"Route print jobs for non-Bambu printers through printer plug-in agents instead of the classic print-host upload flow.\n"
"When disabled, OrcaSlicer uses the legacy print-host behavior."
msgstr ""
"Envía los trabajos de impresión de impresoras que no son Bambu a través de los agentes de plugin de impresora en lugar del flujo clásico de subida al host de impresión.\n"
"Cuando está desactivado, OrcaSlicer utiliza el comportamiento heredado del host de impresión."
msgid "Experimental Features"
msgstr "Funciones experimentales"
@@ -9997,6 +10004,17 @@ msgstr ""
"Intenta conservar las característica pintadas (color, costuras, soportes, piel rugosa, etc.) tras modificar la malla del objeto (por ejemplo, al cortar, volver a cargar desde el disco, simplificar, corregir, etc.).\n"
"¡Muy experimental! Es lento y puede generar artefactos."
msgid "Use printer agents instead of print hosts"
msgstr ""
# AI Translated
msgid ""
"Route print jobs for non-Bambu printers through printer plug-in agents instead of the classic print-host upload flow.\n"
"When disabled, OrcaSlicer uses the legacy print-host behavior."
msgstr ""
"Envía los trabajos de impresión de impresoras que no son Bambu a través de los agentes de plugin de impresora en lugar del flujo clásico de subida al host de impresión.\n"
"Cuando está desactivado, OrcaSlicer utiliza el comportamiento heredado del host de impresión."
msgid "Allow Abnormal Storage"
msgstr "Permitir almacenamiento anómalo"
@@ -10778,6 +10796,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"
@@ -15863,6 +15887,12 @@ msgstr "Relleno simétrico respecto al eje Y"
msgid "If the model has two parts that are symmetric about the Y axis, and you want these parts to have symmetric textures, please click this option on one of the parts."
msgstr "Si el modelo tiene dos piezas simétricas respecto al eje Y y desea que tengan texturas simétricas, active esta opción en una de las piezas."
msgid "Fill pattern tops"
msgstr ""
msgid "Choose this option if you want to completely fill in the tops of the infill pattern"
msgstr ""
msgid "Infill combination - Max layer height"
msgstr "Combinación de relleno - Altura máxima de la capa"
@@ -29459,6 +29489,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%"
@@ -30138,6 +30171,10 @@ msgstr ""
"Evita la deformación\n"
"¿Sabías que al imprimir materiales propensos a la deformación como el ABS, aumentar adecuadamente la temperatura de la cama térmica puede reducir la probabilidad de deformaciones?"
# AI Translated
#~ msgid "(Experimental) Use printer agents instead of print hosts"
#~ msgstr "(Experimental) Usar agentes de impresora en lugar de hosts de impresión"
#~ msgid "X"
#~ msgstr "X"
@@ -31160,9 +31197,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 "
+51 -17
View File
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-05 10:49-0300\n"
"POT-Creation-Date: 2026-10-06 13:42+0800\n"
"PO-Revision-Date: 2026-07-20 13:33+0200\n"
"Last-Translator: Manu Goiogana <mgoiogana@gmail.com>\n"
"Language-Team: \n"
@@ -4218,6 +4218,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)"
@@ -6066,7 +6069,7 @@ msgstr "Bolumena:"
msgid "Size:"
msgstr "Tamaina:"
#, c-format, boost-format
#, boost-format
msgid "Conflicts of G-code paths have been found at layer %d, Z = %.2lfmm. Please separate the conflicted objects farther (%s <-> %s)."
msgstr "G-code ibilbideen gatazkak aurkitu dira %d geruzan, Z = %.2lf mm. Urrundu gehiago gatazkan dauden objektuak (%s <-> %s)."
@@ -6930,6 +6933,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"
@@ -10061,18 +10080,6 @@ msgstr "Erakutsi onartzen ez diren aurrezarpenak"
msgid "Show incompatible/unsupported presets in the printer and filament dropdown lists. These presets cannot be selected."
msgstr "Erakutsi bateraezinak edo onartu gabeak diren aurrezarpenak inprimagailuaren eta filamentuaren goitibeherako zerrendetan. Aurrezarpen hauek ezin dira hautatu."
# AI Translated
msgid "(Experimental) Use printer agents instead of print hosts"
msgstr "(Esperimentala) Erabili inprimagailu-agenteak inprimatze-hostenen ordez"
# AI Translated
msgid ""
"Route print jobs for non-Bambu printers through printer plug-in agents instead of the classic print-host upload flow.\n"
"When disabled, OrcaSlicer uses the legacy print-host behavior."
msgstr ""
"Bideratu Bambu ez diren inprimagailuen inprimatze-lanak inprimagailuaren plugin-agenteen bidez, inprimatze-hostera igotzeko fluxu klasikoaren ordez.\n"
"Desgaituta dagoenean, OrcaSlicer-ek inprimatze-hostaren aurreko portaera erabiltzen du."
msgid "Experimental Features"
msgstr "Ezaugarri esperimentalak"
@@ -10086,6 +10093,17 @@ msgstr ""
"Saiatu pintatutako eginbideak (kolorea/jostura/euskarria/gainazal zimurra eta abar) mantentzen objektuaren sarea aldatu ondoren (adibidez, ebaki/diskotik birkargatu/sinplifikatu/konpondu eta abar).\n"
"Oso esperimentala! Motela da eta artefaktuak sor ditzake."
msgid "Use printer agents instead of print hosts"
msgstr ""
# AI Translated
msgid ""
"Route print jobs for non-Bambu printers through printer plug-in agents instead of the classic print-host upload flow.\n"
"When disabled, OrcaSlicer uses the legacy print-host behavior."
msgstr ""
"Bideratu Bambu ez diren inprimagailuen inprimatze-lanak inprimagailuaren plugin-agenteen bidez, inprimatze-hostera igotzeko fluxu klasikoaren ordez.\n"
"Desgaituta dagoenean, OrcaSlicer-ek inprimatze-hostaren aurreko portaera erabiltzen du."
msgid "Allow Abnormal Storage"
msgstr "Baimendu biltegiratze anormala"
@@ -10884,6 +10902,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"
@@ -16022,6 +16046,12 @@ msgstr "Betegarri simetrikoaren Y ardatza"
msgid "If the model has two parts that are symmetric about the Y axis, and you want these parts to have symmetric textures, please click this option on one of the parts."
msgstr "Modeloak Y ardatzarekiko simetrikoak diren bi zati baditu eta zati horiek testura simetrikoak izatea nahi baduzu, aktibatu aukera hau zatietako batean."
msgid "Fill pattern tops"
msgstr ""
msgid "Choose this option if you want to completely fill in the tops of the infill pattern"
msgstr ""
msgid "Infill combination - Max layer height"
msgstr "Betegarri-konbinazioa - Gehieneko geruza-altuera"
@@ -29650,6 +29680,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%"
@@ -30329,6 +30362,10 @@ msgstr ""
"Saihestu okertzea\n"
"Ba al zenekien ABS bezalako okertzeko joera duten materialak inprimatzean ohe beroaren tenperatura egoki igotzeak okertzeko probabilitatea murriztu dezakeela?"
# AI Translated
#~ msgid "(Experimental) Use printer agents instead of print hosts"
#~ msgstr "(Esperimentala) Erabili inprimagailu-agenteak inprimatze-hostenen ordez"
#~ msgid "X"
#~ msgstr "X"
@@ -31355,9 +31392,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 "
+51 -17
View File
@@ -6,7 +6,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Orca Slicer\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-05 10:49-0300\n"
"POT-Creation-Date: 2026-10-06 13:42+0800\n"
"PO-Revision-Date: \n"
"Last-Translator: \n"
"Language-Team: Guislain Cyril, Thomas Lété\n"
@@ -4247,6 +4247,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)"
@@ -6101,7 +6104,7 @@ msgstr "Volume :"
msgid "Size:"
msgstr "Taille :"
#, c-format, boost-format
#, boost-format
msgid "Conflicts of G-code paths have been found at layer %d, Z = %.2lfmm. Please separate the conflicted objects farther (%s <-> %s)."
msgstr "Des conflits de chemins G-code ont été trouvés au niveau de la couche %d, z = %.2lfmm. Veuillez séparer davantage les objets en conflit (%s <-> %s)."
@@ -6969,6 +6972,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"
@@ -10081,18 +10100,6 @@ msgstr "Afficher les préréglages non pris en charge"
msgid "Show incompatible/unsupported presets in the printer and filament dropdown lists. These presets cannot be selected."
msgstr "Affiche les préréglages incompatibles ou non pris en charge dans les listes déroulantes d’imprimantes et de filaments. Ces préréglages ne peuvent pas être sélectionnés."
# AI Translated
msgid "(Experimental) Use printer agents instead of print hosts"
msgstr "(Expérimental) Utiliser les agents d’imprimante au lieu des hôtes d’impression"
# AI Translated
msgid ""
"Route print jobs for non-Bambu printers through printer plug-in agents instead of the classic print-host upload flow.\n"
"When disabled, OrcaSlicer uses the legacy print-host behavior."
msgstr ""
"Achemine les tâches d’impression des imprimantes non Bambu via les agents de plugin d’imprimante au lieu du flux classique d’envoi vers l’hôte d’impression.\n"
"Lorsque cette option est désactivée, OrcaSlicer utilise l’ancien comportement de l’hôte d’impression."
msgid "Experimental Features"
msgstr "Fonctionnalités expérimentales"
@@ -10106,6 +10113,17 @@ msgstr ""
"Tente de conserver les éléments peints (couleur/couture/support/surface irrégulière, etc.) après une modification du maillage de l’objet (découpe/rechargement depuis le disque/simplification/réparation, etc.)\n"
"Hautement expérimental ! Lent et susceptible de créer des artefacts."
msgid "Use printer agents instead of print hosts"
msgstr ""
# AI Translated
msgid ""
"Route print jobs for non-Bambu printers through printer plug-in agents instead of the classic print-host upload flow.\n"
"When disabled, OrcaSlicer uses the legacy print-host behavior."
msgstr ""
"Achemine les tâches d’impression des imprimantes non Bambu via les agents de plugin d’imprimante au lieu du flux classique d’envoi vers l’hôte d’impression.\n"
"Lorsque cette option est désactivée, OrcaSlicer utilise l’ancien comportement de l’hôte d’impression."
msgid "Allow Abnormal Storage"
msgstr "Autoriser le stockage anormal"
@@ -10900,6 +10918,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"
@@ -16045,6 +16069,12 @@ msgstr "Axe Y de remplissage symétrique"
msgid "If the model has two parts that are symmetric about the Y axis, and you want these parts to have symmetric textures, please click this option on one of the parts."
msgstr "Si le modèle a deux parties symétriques par rapport à l'axe Y et que vous souhaitez que ces parties aient des textures symétriques, veuillez cliquer sur cette option sur l'une des parties."
msgid "Fill pattern tops"
msgstr ""
msgid "Choose this option if you want to completely fill in the tops of the infill pattern"
msgstr ""
msgid "Infill combination - Max layer height"
msgstr "Combinaison de remplissage - Hauteur maximale de la couche"
@@ -29392,6 +29422,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%"
@@ -30071,6 +30104,10 @@ msgstr ""
"Éviter la déformation\n"
"Saviez-vous que lors de l’impression de matériaux susceptibles de se déformer, tels que l’ABS, une augmentation appropriée de la température du plateau chauffant peut réduire la probabilité de déformation?"
# AI Translated
#~ msgid "(Experimental) Use printer agents instead of print hosts"
#~ msgstr "(Expérimental) Utiliser les agents d’imprimante au lieu des hôtes d’impression"
#~ msgid "X"
#~ msgstr "X"
@@ -31090,9 +31127,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 "
+52 -18
View File
@@ -3,7 +3,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Orca Slicer\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-05 10:49-0300\n"
"POT-Creation-Date: 2026-10-06 13:42+0800\n"
"Language: hu\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
@@ -4357,6 +4357,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)"
@@ -6236,7 +6239,7 @@ msgid "Size:"
msgstr "Méret:"
# AI Translated
#, c-format, boost-format
#, boost-format
msgid "Conflicts of G-code paths have been found at layer %d, Z = %.2lfmm. Please separate the conflicted objects farther (%s <-> %s)."
msgstr "G-code útvonalütközés található a(z) %d. rétegen, Z = %.2lfmm. Helyezd távolabb egymástól az ütköző objektumokat (%s <-> %s)."
@@ -7122,6 +7125,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"
@@ -10332,18 +10351,6 @@ msgstr "Nem támogatott beállítások megjelenítése"
msgid "Show incompatible/unsupported presets in the printer and filament dropdown lists. These presets cannot be selected."
msgstr "Megjeleníti a nem kompatibilis vagy nem támogatott beállításokat a nyomtató- és filamentlegördülő listákban. Ezek a beállítások nem választhatók ki."
# AI Translated
msgid "(Experimental) Use printer agents instead of print hosts"
msgstr "(Kísérleti) Nyomtatóügynökök használata nyomtatókiszolgálók helyett"
# AI Translated
msgid ""
"Route print jobs for non-Bambu printers through printer plug-in agents instead of the classic print-host upload flow.\n"
"When disabled, OrcaSlicer uses the legacy print-host behavior."
msgstr ""
"A nem Bambu nyomtatók nyomtatási feladatait a nyomtató bővítményügynökein keresztül továbbítja a klasszikus nyomtatókiszolgálóra való feltöltés helyett.\n"
"Ha ki van kapcsolva, az OrcaSlicer a régi nyomtatókiszolgáló-viselkedést használja."
# AI Translated
msgid "Experimental Features"
msgstr "Kísérleti funkciók"
@@ -10359,6 +10366,17 @@ msgstr ""
"Megpróbálja tartani a festett jellemzőket (szín/varrat/támasz/fuzzy stb.) az objektumháló megváltoztatása után (például kivágás/újratöltés lemezről/egyszerűsítés/javítás stb.)\n"
"Kísérletei szint! Lassú és hibákat hozhat létre."
msgid "Use printer agents instead of print hosts"
msgstr ""
# AI Translated
msgid ""
"Route print jobs for non-Bambu printers through printer plug-in agents instead of the classic print-host upload flow.\n"
"When disabled, OrcaSlicer uses the legacy print-host behavior."
msgstr ""
"A nem Bambu nyomtatók nyomtatási feladatait a nyomtató bővítményügynökein keresztül továbbítja a klasszikus nyomtatókiszolgálóra való feltöltés helyett.\n"
"Ha ki van kapcsolva, az OrcaSlicer a régi nyomtatókiszolgáló-viselkedést használja."
msgid "Allow Abnormal Storage"
msgstr "Rendellenes tároló engedélyezése"
@@ -11177,6 +11195,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"
@@ -16465,6 +16489,12 @@ msgstr "Szimmetrikus kitöltés Y tengely"
msgid "If the model has two parts that are symmetric about the Y axis, and you want these parts to have symmetric textures, please click this option on one of the parts."
msgstr "Ha a modell két, az Y tengelyre szimmetrikus részből áll, és azt szeretnéd, hogy ezek a részek szimmetrikus textúrát kapjanak, kattints erre az opcióra az egyik részen."
msgid "Fill pattern tops"
msgstr ""
msgid "Choose this option if you want to completely fill in the tops of the infill pattern"
msgstr ""
msgid "Infill combination - Max layer height"
msgstr "Kitöltés összevonása - maximális rétegmagasság"
@@ -30380,6 +30410,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%"
@@ -32083,10 +32117,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 "
@@ -32293,6 +32323,10 @@ msgstr ""
#~ msgid "Support/Color Painting: adjust section position"
#~ msgstr "Támasz/Színfestés: metszet pozíciójának beállítása"
# AI Translated
#~ msgid "(Experimental) Use printer agents instead of print hosts"
#~ msgstr "(Kísérleti) Nyomtatóügynökök használata nyomtatókiszolgálók helyett"
# AI Translated
#~ msgid "Tab"
#~ msgstr "Tab"
+51 -17
View File
@@ -3,7 +3,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Orca Slicer\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-05 10:49-0300\n"
"POT-Creation-Date: 2026-10-06 13:42+0800\n"
"PO-Revision-Date: \n"
"Last-Translator: \n"
"Language-Team: \n"
@@ -4350,6 +4350,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)"
@@ -6209,7 +6212,7 @@ msgstr "Volume:"
msgid "Size:"
msgstr "Dimensione:"
#, c-format, boost-format
#, boost-format
msgid "Conflicts of G-code paths have been found at layer %d, Z = %.2lfmm. Please separate the conflicted objects farther (%s <-> %s)."
msgstr "Sono stati trovati conflitti di percorsi nel G-code sullo strato %d, Z = %.2lfmm. Si prega di separare gli oggetti in conflitto (%s <-> %s)."
@@ -7086,6 +7089,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"
@@ -10295,18 +10314,6 @@ msgstr "Mostra i profili non supportati"
msgid "Show incompatible/unsupported presets in the printer and filament dropdown lists. These presets cannot be selected."
msgstr "Mostra i profili incompatibili/non supportati negli elenchi a discesa di stampante e filamento. Questi profili non possono essere selezionati."
# AI Translated
msgid "(Experimental) Use printer agents instead of print hosts"
msgstr "(Sperimentale) Usa gli agenti stampante invece degli host di stampa"
# AI Translated
msgid ""
"Route print jobs for non-Bambu printers through printer plug-in agents instead of the classic print-host upload flow.\n"
"When disabled, OrcaSlicer uses the legacy print-host behavior."
msgstr ""
"Instrada i lavori di stampa delle stampanti non Bambu attraverso gli agenti plugin della stampante invece del classico flusso di caricamento sull'host di stampa.\n"
"Quando è disattivato, OrcaSlicer usa il comportamento legacy dell'host di stampa."
# AI Translated
msgid "Experimental Features"
msgstr "Funzionalità sperimentali"
@@ -10323,6 +10330,17 @@ msgstr ""
"Tenta di mantenere le zone dipinte (colore/cucitura/supporto/ruvida ecc.) dopo la modifica della mesh dell'oggetto (come taglio/ricarica dal disco/semplifica/correggi ecc.)\n"
"Altamente sperimentale! Lento e può creare artefatti."
msgid "Use printer agents instead of print hosts"
msgstr ""
# AI Translated
msgid ""
"Route print jobs for non-Bambu printers through printer plug-in agents instead of the classic print-host upload flow.\n"
"When disabled, OrcaSlicer uses the legacy print-host behavior."
msgstr ""
"Instrada i lavori di stampa delle stampanti non Bambu attraverso gli agenti plugin della stampante invece del classico flusso di caricamento sull'host di stampa.\n"
"Quando è disattivato, OrcaSlicer usa il comportamento legacy dell'host di stampa."
msgid "Allow Abnormal Storage"
msgstr "Consenti memoria anomala"
@@ -11136,6 +11154,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"
@@ -16374,6 +16398,12 @@ msgstr "Riempimento simmetrico asse Y"
msgid "If the model has two parts that are symmetric about the Y axis, and you want these parts to have symmetric textures, please click this option on one of the parts."
msgstr "Se il modello ha due parti simmetriche rispetto all'asse Y e si desidera che queste parti abbiano texture simmetriche, fare clic su questa opzione su una delle parti."
msgid "Fill pattern tops"
msgstr ""
msgid "Choose this option if you want to completely fill in the tops of the infill pattern"
msgstr ""
msgid "Infill combination - Max layer height"
msgstr "Combinazione riempimento - Altezza massima strato"
@@ -30244,6 +30274,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%"
@@ -31947,9 +31980,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 "
@@ -32155,6 +32185,10 @@ msgstr ""
#~ msgid "Support/Color Painting: adjust section position"
#~ msgstr "Supporto/Pittura a colori: regola la posizione della sezione"
# AI Translated
#~ msgid "(Experimental) Use printer agents instead of print hosts"
#~ msgstr "(Sperimentale) Usa gli agenti stampante invece degli host di stampa"
# AI Translated
#~ msgid "Tab"
#~ msgstr "Tab"
+51 -17
View File
@@ -3,7 +3,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Orca Slicer\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-05 10:49-0300\n"
"POT-Creation-Date: 2026-10-06 13:42+0800\n"
"PO-Revision-Date: \n"
"Last-Translator: \n"
"Language-Team: \n"
@@ -4352,6 +4352,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)"
@@ -6233,7 +6236,7 @@ msgstr "ボリューム"
msgid "Size:"
msgstr "サイズ:"
#, c-format, boost-format
#, boost-format
msgid "Conflicts of G-code paths have been found at layer %d, Z = %.2lfmm. Please separate the conflicted objects farther (%s <-> %s)."
msgstr "レイヤー%d、Z = %.2lfmmでG-codeパスの衝突が検出されました。衝突するオブジェクトをもっと離してください(%s <-> %s)。"
@@ -7102,6 +7105,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"
@@ -10329,18 +10348,6 @@ msgstr "非対応のプリセットを表示"
msgid "Show incompatible/unsupported presets in the printer and filament dropdown lists. These presets cannot be selected."
msgstr "プリンターとフィラメントのドロップダウンリストに、互換性のない/非対応のプリセットを表示します。これらのプリセットは選択できません。"
# AI Translated
msgid "(Experimental) Use printer agents instead of print hosts"
msgstr "(実験的) プリントホストの代わりにプリンターエージェントを使用"
# AI Translated
msgid ""
"Route print jobs for non-Bambu printers through printer plug-in agents instead of the classic print-host upload flow.\n"
"When disabled, OrcaSlicer uses the legacy print-host behavior."
msgstr ""
"Bambu 以外のプリンターの印刷ジョブを、従来のプリントホストへのアップロードではなく、プリンターのプラグインエージェント経由で送信します。\n"
"無効の場合、OrcaSlicer は従来のプリントホストの動作を使用します。"
# AI Translated
msgid "Experimental Features"
msgstr "実験的機能"
@@ -10357,6 +10364,17 @@ msgstr ""
"オブジェクトのメッシュを変更した後 (カット/ディスクから再読み込み/簡略化/修正など) も、ペイントした情報 (カラー/継ぎ目/サポート/ファジーなど) の保持を試みます。\n"
"非常に実験的な機能です。処理が遅く、アーティファクトが発生する場合があります。"
msgid "Use printer agents instead of print hosts"
msgstr ""
# AI Translated
msgid ""
"Route print jobs for non-Bambu printers through printer plug-in agents instead of the classic print-host upload flow.\n"
"When disabled, OrcaSlicer uses the legacy print-host behavior."
msgstr ""
"Bambu 以外のプリンターの印刷ジョブを、従来のプリントホストへのアップロードではなく、プリンターのプラグインエージェント経由で送信します。\n"
"無効の場合、OrcaSlicer は従来のプリントホストの動作を使用します。"
msgid "Allow Abnormal Storage"
msgstr "異常なストレージを許可"
@@ -11169,6 +11187,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 "造形タスクを準備"
@@ -16580,6 +16604,12 @@ msgstr "Y軸対称インフィル"
msgid "If the model has two parts that are symmetric about the Y axis, and you want these parts to have symmetric textures, please click this option on one of the parts."
msgstr "モデルにY軸に対して対称な2つのパーツがあり、それらのテクスチャを対称にしたい場合は、一方のパーツでこのオプションをクリックしてください。"
msgid "Fill pattern tops"
msgstr ""
msgid "Choose this option if you want to completely fill in the tops of the infill pattern"
msgstr ""
# AI Translated
msgid "Infill combination - Max layer height"
msgstr "インフィル結合 - 最大積層ピッチ"
@@ -30766,6 +30796,9 @@ msgstr "開発者ツールを有効化"
msgid "An error occurred loading %s"
msgstr "%s の読み込み中にエラーが発生しました"
msgid "/"
msgstr "/"
# AI Translated
#, boost-format
msgid "Load %1% to %2%"
@@ -32506,9 +32539,6 @@ msgstr ""
#~ msgid "An error occurred loading "
#~ msgstr "読み込み中にエラーが発生しました: "
#~ msgid "/"
#~ msgstr "/"
# AI Translated
#, c-format, boost-format
#~ msgid "Load %s to "
@@ -32715,6 +32745,10 @@ msgstr ""
#~ msgid "Support/Color Painting: adjust section position"
#~ msgstr "サポート/色塗り: 断面の位置"
# AI Translated
#~ msgid "(Experimental) Use printer agents instead of print hosts"
#~ msgstr "(実験的) プリントホストの代わりにプリンターエージェントを使用"
# AI Translated
#~ msgid "Tab"
#~ msgstr "Tab"
+51 -17
View File
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Orca Slicer\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-05 10:49-0300\n"
"POT-Creation-Date: 2026-10-06 13:42+0800\n"
"PO-Revision-Date: 2025-06-02 17:12+0900\n"
"Last-Translator: crwusiz <crwusiz@gmail.com>\n"
"Language-Team: \n"
@@ -4377,6 +4377,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)"
@@ -6266,7 +6269,7 @@ msgid "Size:"
msgstr "크기:"
# AI Translated
#, c-format, boost-format
#, boost-format
msgid "Conflicts of G-code paths have been found at layer %d, Z = %.2lfmm. Please separate the conflicted objects farther (%s <-> %s)."
msgstr "레이어 %d, Z = %.2lf mm에서 G-code 경로 충돌이 발견되었습니다. 충돌하는 객체를 더 멀리 분리하세요 (%s <-> %s)."
@@ -7146,6 +7149,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"
@@ -10462,18 +10481,6 @@ msgstr "지원되지 않는 사전 설정 표시"
msgid "Show incompatible/unsupported presets in the printer and filament dropdown lists. These presets cannot be selected."
msgstr "프린터 및 필라멘트 드롭다운 목록에 호환되지 않거나 지원되지 않는 사전 설정을 표시합니다. 이러한 사전 설정은 선택할 수 없습니다."
# AI Translated
msgid "(Experimental) Use printer agents instead of print hosts"
msgstr "(실험적) 출력 호스트 대신 프린터 에이전트 사용"
# AI Translated
msgid ""
"Route print jobs for non-Bambu printers through printer plug-in agents instead of the classic print-host upload flow.\n"
"When disabled, OrcaSlicer uses the legacy print-host behavior."
msgstr ""
"Bambu 이외의 프린터 출력 작업을 기존 출력 호스트 업로드 방식 대신 프린터 플러그인 에이전트를 통해 전달합니다.\n"
"비활성화하면 OrcaSlicer는 기존 출력 호스트 동작을 사용합니다."
# AI Translated
msgid "Experimental Features"
msgstr "실험적 기능"
@@ -10490,6 +10497,17 @@ msgstr ""
"객체 메시를 변경한 후(자르기/디스크에서 다시 불러오기/단순화/수정 등)에도 칠한 기능(색상/재봉선/서포트/퍼지 등)을 유지하려고 시도합니다.\n"
"매우 실험적인 기능입니다! 속도가 느리고 아티팩트가 생길 수 있습니다."
msgid "Use printer agents instead of print hosts"
msgstr ""
# AI Translated
msgid ""
"Route print jobs for non-Bambu printers through printer plug-in agents instead of the classic print-host upload flow.\n"
"When disabled, OrcaSlicer uses the legacy print-host behavior."
msgstr ""
"Bambu 이외의 프린터 출력 작업을 기존 출력 호스트 업로드 방식 대신 프린터 플러그인 에이전트를 통해 전달합니다.\n"
"비활성화하면 OrcaSlicer는 기존 출력 호스트 동작을 사용합니다."
# AI Translated
msgid "Allow Abnormal Storage"
msgstr "비정상 저장 장치 허용"
@@ -11310,6 +11328,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 "출력 작업 준비 중"
@@ -16738,6 +16762,12 @@ msgstr "Y축 대칭 채우기"
msgid "If the model has two parts that are symmetric about the Y axis, and you want these parts to have symmetric textures, please click this option on one of the parts."
msgstr "모델에 Y축을 기준으로 대칭인 두 부품이 있고 이 부품들이 대칭 질감을 갖게 하려면, 두 부품 중 하나에서 이 옵션을 선택하십시오."
msgid "Fill pattern tops"
msgstr ""
msgid "Choose this option if you want to completely fill in the tops of the infill pattern"
msgstr ""
msgid "Infill combination - Max layer height"
msgstr "채우기 조합 - 최대 레이어 높이"
@@ -30783,6 +30813,9 @@ msgstr "개발자 도구 사용"
msgid "An error occurred loading %s"
msgstr "%s 로드 중 오류가 발생했습니다"
msgid "/"
msgstr "/"
# AI Translated
#, boost-format
msgid "Load %1% to %2%"
@@ -32495,9 +32528,6 @@ msgstr ""
#~ msgid "An error occurred loading "
#~ msgstr "로드 중 오류가 발생했습니다: "
#~ msgid "/"
#~ msgstr "/"
# AI Translated
#, c-format, boost-format
#~ msgid "Load %s to "
@@ -32704,6 +32734,10 @@ msgstr ""
#~ msgid "Support/Color Painting: adjust section position"
#~ msgstr "서포트/색상 칠하기: 단면 위치 조정"
# AI Translated
#~ msgid "(Experimental) Use printer agents instead of print hosts"
#~ msgstr "(실험적) 출력 호스트 대신 프린터 에이전트 사용"
# AI Translated
#~ msgid "Tab"
#~ msgstr "탭"
+1
View File
@@ -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
+52 -18
View File
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-05 10:49-0300\n"
"POT-Creation-Date: 2026-10-06 13:42+0800\n"
"PO-Revision-Date: 2026-07-02 14:13+0300\n"
"Last-Translator: Gintaras Kučinskas <sharanchius@gmail.com>\n"
"Language-Team: \n"
@@ -4343,6 +4343,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)"
@@ -6222,7 +6225,7 @@ msgid "Size:"
msgstr "Dydis:"
# AI Translated
#, c-format, boost-format
#, boost-format
msgid "Conflicts of G-code paths have been found at layer %d, Z = %.2lfmm. Please separate the conflicted objects farther (%s <-> %s)."
msgstr "Rasta G-code trajektorijų konfliktų %d sluoksnyje, Z = %.2lfmm. Prašome labiau atskirti konfliktuojančius objektus (%s <-> %s)."
@@ -7107,6 +7110,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"
@@ -10297,18 +10316,6 @@ msgstr "Rodyti nepalaikomus profilius"
msgid "Show incompatible/unsupported presets in the printer and filament dropdown lists. These presets cannot be selected."
msgstr "Rodyti nesuderinamus / nepalaikomus profilius spausdintuvų ir gijų išskleidžiamuosiuose sąrašuose. Šių profilių pasirinkti negalima."
# AI Translated
msgid "(Experimental) Use printer agents instead of print hosts"
msgstr "(Eksperimentinė) Naudoti spausdintuvo agentus vietoj spausdinimo serverių"
# AI Translated
msgid ""
"Route print jobs for non-Bambu printers through printer plug-in agents instead of the classic print-host upload flow.\n"
"When disabled, OrcaSlicer uses the legacy print-host behavior."
msgstr ""
"Nukreipia ne Bambu spausdintuvų spausdinimo užduotis per spausdintuvo papildinių agentus, o ne per klasikinį įkėlimo į spausdinimo serverį srautą.\n"
"Kai išjungta, OrcaSlicer naudoja senąjį spausdinimo serverio veikimą."
msgid "Experimental Features"
msgstr "Eksperimentinis"
@@ -10322,6 +10329,17 @@ msgstr ""
"Bando išsaugoti nudažytas ypatybes (spalvą, siūlę, atramas, šiauštą paviršių ir kt.) pakeitus objekto tinklelio modelį (pvz., po pjovimo, įkėlimo iš naujo, supaprastinimo, taisymo ir kt.)\n"
"Labai eksperimentinė funkcija! Veikia lėtai ir gali sukurti artefaktų."
msgid "Use printer agents instead of print hosts"
msgstr ""
# AI Translated
msgid ""
"Route print jobs for non-Bambu printers through printer plug-in agents instead of the classic print-host upload flow.\n"
"When disabled, OrcaSlicer uses the legacy print-host behavior."
msgstr ""
"Nukreipia ne Bambu spausdintuvų spausdinimo užduotis per spausdintuvo papildinių agentus, o ne per klasikinį įkėlimo į spausdinimo serverį srautą.\n"
"Kai išjungta, OrcaSlicer naudoja senąjį spausdinimo serverio veikimą."
msgid "Allow Abnormal Storage"
msgstr "Leisti sutrikusią (neįprastą) laikmeną"
@@ -11132,6 +11150,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"
@@ -16362,6 +16386,12 @@ msgstr "Simetriškas užpildas pagal Y ašį"
msgid "If the model has two parts that are symmetric about the Y axis, and you want these parts to have symmetric textures, please click this option on one of the parts."
msgstr "Jei modelis turi dvi dalis, kurios yra simetriškos Y ašies atžvilgiu, ir norite, kad šios dalys turėtų simetriškas tekstūras, spustelėkite šią parinktį vienoje iš dalių."
msgid "Fill pattern tops"
msgstr ""
msgid "Choose this option if you want to completely fill in the tops of the infill pattern"
msgstr ""
msgid "Infill combination - Max layer height"
msgstr "Užpildo derinimas – maksimalus sluoksnio aukštis"
@@ -30116,6 +30146,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%"
@@ -30796,6 +30830,10 @@ msgstr ""
"Venkite deformacijų (warping)\n"
"Ar žinojote, kad spausdinant medžiagas, kurios yra linkusios trauktis ir riestis (pvz., ABS), tinkamas kaitinamojo pagrindo temperatūros padidinimas gali sumažinti deformacijų (warping) tikimybę?"
# AI Translated
#~ msgid "(Experimental) Use printer agents instead of print hosts"
#~ msgstr "(Eksperimentinė) Naudoti spausdintuvo agentus vietoj spausdinimo serverių"
#~ msgid "X"
#~ msgstr "X"
@@ -31824,10 +31862,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 "
+51 -17
View File
@@ -3,7 +3,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Orca Slicer\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-05 10:49-0300\n"
"POT-Creation-Date: 2026-10-06 13:42+0800\n"
"PO-Revision-Date: \n"
"Last-Translator: \n"
"Language-Team: \n"
@@ -4719,6 +4719,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)"
@@ -6746,7 +6749,7 @@ msgid "Size:"
msgstr "Maat:"
# AI Translated
#, c-format, boost-format
#, boost-format
msgid "Conflicts of G-code paths have been found at layer %d, Z = %.2lfmm. Please separate the conflicted objects farther (%s <-> %s)."
msgstr "Er zijn conflicten tussen G-code-paden gevonden op laag %d, Z = %.2lfmm. Plaats de conflicterende objecten verder uit elkaar (%s <-> %s)."
@@ -7686,6 +7689,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"
@@ -11154,18 +11173,6 @@ msgstr "Niet-ondersteunde voorinstellingen tonen"
msgid "Show incompatible/unsupported presets in the printer and filament dropdown lists. These presets cannot be selected."
msgstr "Toon incompatibele/niet-ondersteunde voorinstellingen in de keuzelijsten voor printer en filament. Deze voorinstellingen kunnen niet worden geselecteerd."
# AI Translated
msgid "(Experimental) Use printer agents instead of print hosts"
msgstr "(Experimenteel) Printeragents gebruiken in plaats van printhosts"
# AI Translated
msgid ""
"Route print jobs for non-Bambu printers through printer plug-in agents instead of the classic print-host upload flow.\n"
"When disabled, OrcaSlicer uses the legacy print-host behavior."
msgstr ""
"Stuurt printtaken voor niet-Bambu-printers via printer-plug-inagents in plaats van via de klassieke uploadstroom naar de printhost.\n"
"Wanneer dit is uitgeschakeld, gebruikt OrcaSlicer het oude printhostgedrag."
# AI Translated
msgid "Experimental Features"
msgstr "Experimentele functies"
@@ -11182,6 +11189,17 @@ msgstr ""
"Probeer geschilderde kenmerken (kleur/naad/ondersteuning/vaag enz.) te behouden na het wijzigen van de mesh van het object (zoals snijden/opnieuw laden vanaf schijf/vereenvoudigen/repareren enz.)\n"
"Zeer experimenteel! Traag en kan artefacten veroorzaken."
msgid "Use printer agents instead of print hosts"
msgstr ""
# AI Translated
msgid ""
"Route print jobs for non-Bambu printers through printer plug-in agents instead of the classic print-host upload flow.\n"
"When disabled, OrcaSlicer uses the legacy print-host behavior."
msgstr ""
"Stuurt printtaken voor niet-Bambu-printers via printer-plug-inagents in plaats van via de klassieke uploadstroom naar de printhost.\n"
"Wanneer dit is uitgeschakeld, gebruikt OrcaSlicer het oude printhostgedrag."
# AI Translated
msgid "Allow Abnormal Storage"
msgstr "Afwijkend opslagmedium toestaan"
@@ -12026,6 +12044,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"
@@ -17772,6 +17796,12 @@ msgstr "Symmetrische vulling om de Y-as"
msgid "If the model has two parts that are symmetric about the Y axis, and you want these parts to have symmetric textures, please click this option on one of the parts."
msgstr "Als het model twee delen heeft die symmetrisch zijn ten opzichte van de Y-as en u wilt dat deze delen symmetrische texturen hebben, klik dan op deze optie bij een van de delen."
msgid "Fill pattern tops"
msgstr ""
msgid "Choose this option if you want to completely fill in the tops of the infill pattern"
msgstr ""
# AI Translated
msgid "Infill combination - Max layer height"
msgstr "Vullingcombinatie - Maximale laaghoogte"
@@ -32314,6 +32344,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%"
@@ -34080,9 +34113,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 "
@@ -34292,6 +34322,10 @@ msgstr ""
#~ msgid "Support/Color Painting: adjust section position"
#~ msgstr "Support/kleur intekenen: pas de sectie positie aan"
# AI Translated
#~ msgid "(Experimental) Use printer agents instead of print hosts"
#~ msgstr "(Experimenteel) Printeragents gebruiken in plaats van printhosts"
# AI Translated
#~ msgid "Tab"
#~ msgstr "Tab"
+51 -17
View File
@@ -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-05 10:49-0300\n"
"POT-Creation-Date: 2026-10-06 13:42+0800\n"
"PO-Revision-Date: \n"
"Last-Translator: Krzysztof Morga <<tlumaczeniebs@gmail.com>>\n"
"Language-Team: \n"
@@ -4438,6 +4438,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)"
@@ -6341,7 +6344,7 @@ msgstr "Objętość:"
msgid "Size:"
msgstr "Rozmiar:"
#, c-format, boost-format
#, boost-format
msgid "Conflicts of G-code paths have been found at layer %d, Z = %.2lfmm. Please separate the conflicted objects farther (%s <-> %s)."
msgstr "Wykryto konflikty ścieżek G-code na warstwie %d, Z = %.2lfmm. Proszę oddalić od siebie obiekty będące w konflikcie (%s <-> %s)."
@@ -7238,6 +7241,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"
@@ -10560,18 +10579,6 @@ msgstr "Pokaż nieobsługiwane profile"
msgid "Show incompatible/unsupported presets in the printer and filament dropdown lists. These presets cannot be selected."
msgstr "Pokazuj niekompatybilne/nieobsługiwane profile na listach rozwijanych drukarek i filamentów. Tych profili nie można wybrać."
# AI Translated
msgid "(Experimental) Use printer agents instead of print hosts"
msgstr "(Eksperymentalne) Używaj agentów drukarki zamiast serwerów druku"
# AI Translated
msgid ""
"Route print jobs for non-Bambu printers through printer plug-in agents instead of the classic print-host upload flow.\n"
"When disabled, OrcaSlicer uses the legacy print-host behavior."
msgstr ""
"Kieruje zadania druku dla drukarek innych niż Bambu przez agentów wtyczek drukarki zamiast klasycznego przesyłania do serwera druku.\n"
"Gdy opcja jest wyłączona, OrcaSlicer korzysta z dotychczasowego działania serwera druku."
# AI Translated
msgid "Experimental Features"
msgstr "Funkcje eksperymentalne"
@@ -10588,6 +10595,17 @@ msgstr ""
"Próbuje zachować malowane cechy (kolor/szew/podpory/skórę fuzzy itp.) po zmianie siatki obiektu (np. cięcie/wczytanie ponownie z dysku/uproszczenie/naprawa itp.)\n"
"Wysoce eksperymentalne! Powolne i może tworzyć artefakty."
msgid "Use printer agents instead of print hosts"
msgstr ""
# AI Translated
msgid ""
"Route print jobs for non-Bambu printers through printer plug-in agents instead of the classic print-host upload flow.\n"
"When disabled, OrcaSlicer uses the legacy print-host behavior."
msgstr ""
"Kieruje zadania druku dla drukarek innych niż Bambu przez agentów wtyczek drukarki zamiast klasycznego przesyłania do serwera druku.\n"
"Gdy opcja jest wyłączona, OrcaSlicer korzysta z dotychczasowego działania serwera druku."
# AI Translated
msgid "Allow Abnormal Storage"
msgstr "Zezwalaj na nieprawidłową pamięć"
@@ -11411,6 +11429,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"
@@ -16799,6 +16823,12 @@ msgstr "Symetryczne wypełnienie względem osi Y"
msgid "If the model has two parts that are symmetric about the Y axis, and you want these parts to have symmetric textures, please click this option on one of the parts."
msgstr "Jeśli model ma dwie części symetryczne względem osi Y i chcesz, aby miały symetryczne tekstury, zaznacz tę opcję dla jednej z części."
msgid "Fill pattern tops"
msgstr ""
msgid "Choose this option if you want to completely fill in the tops of the infill pattern"
msgstr ""
msgid "Infill combination - Max layer height"
msgstr "Kombinacja wypełnienia - Maks. wysokość warstwy"
@@ -30768,6 +30798,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%"
@@ -31447,6 +31480,10 @@ msgstr ""
"Unikaj odkształceń\n"
"Czy wiesz, że podczas drukowania filamentami podatnymi na odkształcenia, takimi jak ABS, odpowiednie zwiększenie temperatury podgrzewanej płyty może zmniejszyć prawdopodobieństwo odkształceń?"
# AI Translated
#~ msgid "(Experimental) Use printer agents instead of print hosts"
#~ msgstr "(Eksperymentalne) Używaj agentów drukarki zamiast serwerów druku"
#~ msgid "X"
#~ msgstr "X"
@@ -32445,9 +32482,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 "
+50 -16
View File
@@ -3,7 +3,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Orca Slicer\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-05 10:49-0300\n"
"POT-Creation-Date: 2026-10-06 13:42+0800\n"
"PO-Revision-Date: 2026-07-26 11:14-0300\n"
"Last-Translator: Alexandre Folle de Menezes\n"
"Language-Team: Portuguese, Brazilian\n"
@@ -4184,6 +4184,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)"
@@ -6015,7 +6018,7 @@ msgstr "Volume:"
msgid "Size:"
msgstr "Tamanho:"
#, c-format, boost-format
#, boost-format
msgid "Conflicts of G-code paths have been found at layer %d, Z = %.2lfmm. Please separate the conflicted objects farther (%s <-> %s)."
msgstr "Foram encontrados conflitos de caminhos de G-code na camada %d, Z = %.2lfmm. Por favor, separe mais os objetos em conflito (%s <-> %s)."
@@ -6878,6 +6881,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"
@@ -9940,16 +9959,6 @@ msgstr "Mostrar predefinições não suportadas"
msgid "Show incompatible/unsupported presets in the printer and filament dropdown lists. These presets cannot be selected."
msgstr "Exibir predefinições incompatíveis e não suportadas nas listas de impressora e filamento. Essas predefinições não podem ser selecionadas."
msgid "(Experimental) Use printer agents instead of print hosts"
msgstr "(Experimental) Usar agentes de impressora em vez de hosts de impressão"
msgid ""
"Route print jobs for non-Bambu printers through printer plug-in agents instead of the classic print-host upload flow.\n"
"When disabled, OrcaSlicer uses the legacy print-host behavior."
msgstr ""
"Encaminha os trabalhos de impressão de impressoras que não são Bambu pelos agentes de plugin de impressora em vez do fluxo clássico de envio ao host de impressão.\n"
"Quando desativado, o OrcaSlicer usa o comportamento antigo do host de impressão."
msgid "Experimental Features"
msgstr "Recursos Experimentais"
@@ -9963,6 +9972,16 @@ msgstr ""
"Tenta manter recursos pintados (cor/costura/suporte/textura difusa etc.) após alterar a malha do objeto (como cortar/recarregar do disco/simplificar/corrigir etc.)\n"
"Altamente experimental! Lento e pode criar artefatos."
msgid "Use printer agents instead of print hosts"
msgstr ""
msgid ""
"Route print jobs for non-Bambu printers through printer plug-in agents instead of the classic print-host upload flow.\n"
"When disabled, OrcaSlicer uses the legacy print-host behavior."
msgstr ""
"Encaminha os trabalhos de impressão de impressoras que não são Bambu pelos agentes de plugin de impressora em vez do fluxo clássico de envio ao host de impressão.\n"
"Quando desativado, o OrcaSlicer usa o comportamento antigo do host de impressão."
msgid "Allow Abnormal Storage"
msgstr "Permitir Armazenamento Anormal"
@@ -10719,6 +10738,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"
@@ -15794,6 +15819,12 @@ msgstr "Preenchimento simétrico no eixo Y"
msgid "If the model has two parts that are symmetric about the Y axis, and you want these parts to have symmetric textures, please click this option on one of the parts."
msgstr "Se o modelo tiver duas partes simétricas em relação ao eixo Y e você quiser que essas partes tenham texturas simétricas, clique nesta opção em uma das partes."
msgid "Fill pattern tops"
msgstr ""
msgid "Choose this option if you want to completely fill in the tops of the infill pattern"
msgstr ""
msgid "Infill combination - Max layer height"
msgstr "Combinação de preenchimento - Altura máx da camada"
@@ -29339,6 +29370,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%"
@@ -30018,6 +30053,9 @@ msgstr ""
"Evitar empenamento\n"
"Você sabia que ao imprimir materiais propensos ao empenamento como ABS, aumentar adequadamente a temperatura da mesa aquecida pode reduzir a probabilidade de empenamento?"
#~ msgid "(Experimental) Use printer agents instead of print hosts"
#~ msgstr "(Experimental) Usar agentes de impressora em vez de hosts de impressão"
#~ msgid "X"
#~ msgstr "X"
@@ -31039,10 +31077,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 "
+51 -17
View File
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: OrcaSlicer V2.5.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-05 10:49-0300\n"
"POT-Creation-Date: 2026-10-06 13:42+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"
@@ -4319,6 +4319,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)"
@@ -6233,7 +6236,7 @@ msgstr "Объём:"
msgid "Size:"
msgstr "Размер:"
#, c-format, boost-format
#, boost-format
msgid "Conflicts of G-code paths have been found at layer %d, Z = %.2lfmm. Please separate the conflicted objects farther (%s <-> %s)."
msgstr "В G-code на %d слое (z = %.2lf мм) обнаружен конфликт путей. Пожалуйста, разместите конфликтующие модели дальше друг от друга (%s <-> %s)."
@@ -7144,6 +7147,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"
@@ -10321,18 +10340,6 @@ msgstr ""
"\n"
"Примечание: профили остаются недоступными для выбора."
# AI Translated
msgid "(Experimental) Use printer agents instead of print hosts"
msgstr "(Экспериментально) Использовать агентов принтера вместо хостов печати"
# AI Translated
msgid ""
"Route print jobs for non-Bambu printers through printer plug-in agents instead of the classic print-host upload flow.\n"
"When disabled, OrcaSlicer uses the legacy print-host behavior."
msgstr ""
"Отправлять задания печати для принтеров, отличных от Bambu, через агентов плагинов принтера вместо классической загрузки на хост печати.\n"
"Если отключено, OrcaSlicer использует прежнее поведение хоста печати."
msgid "Experimental Features"
msgstr "Экспериментальные настройки"
@@ -10347,6 +10354,17 @@ msgstr ""
"\n"
"Внимание: функция экспериментальна! Возможны падение производительности и артефакты работы."
msgid "Use printer agents instead of print hosts"
msgstr ""
# AI Translated
msgid ""
"Route print jobs for non-Bambu printers through printer plug-in agents instead of the classic print-host upload flow.\n"
"When disabled, OrcaSlicer uses the legacy print-host behavior."
msgstr ""
"Отправлять задания печати для принтеров, отличных от Bambu, через агентов плагинов принтера вместо классической загрузки на хост печати.\n"
"Если отключено, OrcaSlicer использует прежнее поведение хоста печати."
msgid "Allow Abnormal Storage"
msgstr "Игнорировать неисправность хранилища"
@@ -11145,6 +11163,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 "Подготовка задания на печать"
@@ -16569,6 +16593,12 @@ msgstr "Отразить шаблон"
msgid "If the model has two parts that are symmetric about the Y axis, and you want these parts to have symmetric textures, please click this option on one of the parts."
msgstr "Если модель состоит из двух симметричных частей, у одной из них можно отразить шаблон заполнения, чтобы модель имела симметричную текстуру поверхности."
msgid "Fill pattern tops"
msgstr ""
msgid "Choose this option if you want to completely fill in the tops of the infill pattern"
msgstr ""
msgid "Infill combination - Max layer height"
msgstr "Предел высоты объединённого слоя"
@@ -30607,6 +30637,9 @@ msgstr "Включить инструменты разработчика"
msgid "An error occurred loading %s"
msgstr "Ошибка при загрузке %s"
msgid "/"
msgstr "/"
# AI Translated
#, boost-format
msgid "Load %1% to %2%"
@@ -31284,6 +31317,10 @@ msgstr ""
"Предотвращение коробления материала\n"
"Знаете ли вы, что при печати материалами, склонными к короблению, таких как ABS, повышение температуры подогреваемого стола может снизить эту вероятность?"
# AI Translated
#~ msgid "(Experimental) Use printer agents instead of print hosts"
#~ msgstr "(Экспериментально) Использовать агентов принтера вместо хостов печати"
#~ msgid "X"
#~ msgstr "X"
@@ -32318,9 +32355,6 @@ msgstr ""
#~ msgid "An error occurred loading "
#~ msgstr "Ошибка при загрузке "
#~ msgid "/"
#~ msgstr "/"
# AI Translated
#, c-format, boost-format
#~ msgid "Load %s to "
+51 -17
View File
@@ -3,7 +3,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Orca Slicer\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-05 10:49-0300\n"
"POT-Creation-Date: 2026-10-06 13:42+0800\n"
"Language: sv\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
@@ -4784,6 +4784,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)"
@@ -6840,7 +6843,7 @@ msgid "Size:"
msgstr "Storlek:"
# AI Translated
#, c-format, boost-format
#, boost-format
msgid "Conflicts of G-code paths have been found at layer %d, Z = %.2lfmm. Please separate the conflicted objects farther (%s <-> %s)."
msgstr "Konflikter mellan G-code-banor hittades på lager %d, Z = %.2lfmm. Placera de objekt som krockar längre ifrån varandra (%s <-> %s)."
@@ -7781,6 +7784,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 ""
@@ -11297,18 +11316,6 @@ msgstr "Visa förinställningar som inte stöds"
msgid "Show incompatible/unsupported presets in the printer and filament dropdown lists. These presets cannot be selected."
msgstr "Visa inkompatibla förinställningar och förinställningar som inte stöds i rullgardinslistorna för skrivare och filament. Dessa förinställningar kan inte väljas."
# AI Translated
msgid "(Experimental) Use printer agents instead of print hosts"
msgstr "(Experimentellt) Använd skrivaragenter i stället för utskriftsvärdar"
# AI Translated
msgid ""
"Route print jobs for non-Bambu printers through printer plug-in agents instead of the classic print-host upload flow.\n"
"When disabled, OrcaSlicer uses the legacy print-host behavior."
msgstr ""
"Skickar utskriftsjobb för icke-Bambu-skrivare via skrivarens insticksmodulagenter i stället för det klassiska uppladdningsflödet till utskriftsvärden.\n"
"När detta är avaktiverat använder OrcaSlicer det äldre beteendet för utskriftsvärdar."
# AI Translated
msgid "Experimental Features"
msgstr "Experimentella funktioner"
@@ -11325,6 +11332,17 @@ msgstr ""
"Försök att behålla målade egenskaper (färg/söm/support/ojämn yta osv.) efter att objektets mesh ändrats (t.ex. snitt, läs in från disk igen, förenkla, reparera osv.)\n"
"Mycket experimentellt! Långsamt och kan skapa artefakter."
msgid "Use printer agents instead of print hosts"
msgstr ""
# AI Translated
msgid ""
"Route print jobs for non-Bambu printers through printer plug-in agents instead of the classic print-host upload flow.\n"
"When disabled, OrcaSlicer uses the legacy print-host behavior."
msgstr ""
"Skickar utskriftsjobb för icke-Bambu-skrivare via skrivarens insticksmodulagenter i stället för det klassiska uppladdningsflödet till utskriftsvärden.\n"
"När detta är avaktiverat använder OrcaSlicer det äldre beteendet för utskriftsvärdar."
# AI Translated
msgid "Allow Abnormal Storage"
msgstr "Tillåt onormalt lagringsmedium"
@@ -12175,6 +12193,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"
@@ -18034,6 +18058,12 @@ msgstr "Symmetrisk ifyllnad kring Y-axeln"
msgid "If the model has two parts that are symmetric about the Y axis, and you want these parts to have symmetric textures, please click this option on one of the parts."
msgstr "Om modellen har två delar som är symmetriska kring Y-axeln och du vill att dessa delar ska ha symmetriska texturer, klickar du i det här alternativet på en av delarna."
msgid "Fill pattern tops"
msgstr ""
msgid "Choose this option if you want to completely fill in the tops of the infill pattern"
msgstr ""
# AI Translated
msgid "Infill combination - Max layer height"
msgstr "Ifyllnadskombination – max lagerhöjd"
@@ -32648,6 +32678,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%"
@@ -34434,9 +34467,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 "
@@ -34646,6 +34676,10 @@ msgstr ""
#~ msgid "Support/Color Painting: adjust section position"
#~ msgstr "Support/Färgläggning:justera sektions positionen"
# AI Translated
#~ msgid "(Experimental) Use printer agents instead of print hosts"
#~ msgstr "(Experimentellt) Använd skrivaragenter i stället för utskriftsvärdar"
# AI Translated
#~ msgid "Tab"
#~ msgstr "Tabb"
+52 -18
View File
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Orca Slicer\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-05 10:49-0300\n"
"POT-Creation-Date: 2026-10-06 13:42+0800\n"
"PO-Revision-Date: 2026-06-19 13:40+0700\n"
"Last-Translator: Icezaza\n"
"Language-Team: Thai\n"
@@ -4332,6 +4332,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)"
@@ -6192,7 +6195,7 @@ msgid "Size:"
msgstr "ขนาด:"
# AI Translated
#, c-format, boost-format
#, boost-format
msgid "Conflicts of G-code paths have been found at layer %d, Z = %.2lfmm. Please separate the conflicted objects farther (%s <-> %s)."
msgstr "พบความขัดแย้งของเส้นทาง G-code ที่เลเยอร์ %d, Z = %.2lfmm โปรดแยกวัตถุที่ขัดแย้งกันให้ไกลออกไป (%s <-> %s)"
@@ -7061,6 +7064,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"
@@ -10226,18 +10245,6 @@ msgstr "แสดงค่าที่ตั้งไว้ล่วงหน้
msgid "Show incompatible/unsupported presets in the printer and filament dropdown lists. These presets cannot be selected."
msgstr "แสดงค่าที่ตั้งไว้ล่วงหน้าที่ไม่เข้ากันหรือไม่รองรับในรายการเลือกเครื่องพิมพ์และเส้นพลาสติก ไม่สามารถเลือกค่าที่ตั้งไว้ล่วงหน้าเหล่านี้ได้"
# AI Translated
msgid "(Experimental) Use printer agents instead of print hosts"
msgstr "(ทดลอง) ใช้เอเจนต์เครื่องพิมพ์แทนโฮสต์การพิมพ์"
# AI Translated
msgid ""
"Route print jobs for non-Bambu printers through printer plug-in agents instead of the classic print-host upload flow.\n"
"When disabled, OrcaSlicer uses the legacy print-host behavior."
msgstr ""
"ส่งงานพิมพ์ของเครื่องพิมพ์ที่ไม่ใช่ Bambu ผ่านเอเจนต์ปลั๊กอินของเครื่องพิมพ์ แทนการอัพโหลดไปยังโฮสต์การพิมพ์แบบเดิม\n"
"เมื่อปิดใช้ OrcaSlicer จะใช้พฤติกรรมโฮสต์การพิมพ์แบบเดิม"
# AI Translated
msgid "Experimental Features"
msgstr "ฟีเจอร์ทดลอง"
@@ -10253,6 +10260,17 @@ msgstr ""
"พยายามคงคุณสมบัติการทาสีไว้ (สี/รอยตะเข็บ/ส่วนรองรับ/ผิวฟัซซี ฯลฯ) หลังจากเปลี่ยนตาข่ายวัตถุ (เช่น ตัด/โหลดซ้ำจากดิสก์/ลดความซับซ้อน/แก้ไข ฯลฯ)\n"
"น่าทดลองมาก! ช้าและอาจสร้างสิ่งประดิษฐ์"
msgid "Use printer agents instead of print hosts"
msgstr ""
# AI Translated
msgid ""
"Route print jobs for non-Bambu printers through printer plug-in agents instead of the classic print-host upload flow.\n"
"When disabled, OrcaSlicer uses the legacy print-host behavior."
msgstr ""
"ส่งงานพิมพ์ของเครื่องพิมพ์ที่ไม่ใช่ Bambu ผ่านเอเจนต์ปลั๊กอินของเครื่องพิมพ์ แทนการอัพโหลดไปยังโฮสต์การพิมพ์แบบเดิม\n"
"เมื่อปิดใช้ OrcaSlicer จะใช้พฤติกรรมโฮสต์การพิมพ์แบบเดิม"
msgid "Allow Abnormal Storage"
msgstr "อนุญาตให้จัดเก็บผิดปกติ"
@@ -11063,6 +11081,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 "กำลังเตรียมงานพิมพ์"
@@ -16247,6 +16271,12 @@ msgstr "แกน Y ไส้ในแบบสมมาตร"
msgid "If the model has two parts that are symmetric about the Y axis, and you want these parts to have symmetric textures, please click this option on one of the parts."
msgstr "หากแบบจำลองมีสองส่วนที่สมมาตรเกี่ยวกับแกน Y และคุณต้องการให้ส่วนเหล่านี้มีพื้นผิวที่สมมาตร โปรดคลิกตัวเลือกนี้บนส่วนใดส่วนหนึ่ง"
msgid "Fill pattern tops"
msgstr ""
msgid "Choose this option if you want to completely fill in the tops of the infill pattern"
msgstr ""
msgid "Infill combination - Max layer height"
msgstr "ชุดค่าผสม ไส้ใน - ความสูงของชั้นสูงสุด"
@@ -29996,6 +30026,10 @@ msgstr "เปิดใช้งานเครื่องมือนักพ
msgid "An error occurred loading %s"
msgstr "เกิดข้อผิดพลาดในการโหลด %s"
# AI Translated
msgid "/"
msgstr "/"
# AI Translated
#, boost-format
msgid "Load %1% to %2%"
@@ -30675,6 +30709,10 @@ msgstr ""
"หลีกเลี่ยงการบิดเบี้ยว\n"
"คุณรู้หรือไม่ว่าเมื่อพิมพ์วัสดุที่มีแนวโน้มที่จะเกิดการบิดเบี้ยว เช่น ABS การเพิ่มอุณหภูมิฐานพิมพ์อย่างเหมาะสมสามารถลดความน่าจะเป็นของการบิดเบี้ยวได้"
# AI Translated
#~ msgid "(Experimental) Use printer agents instead of print hosts"
#~ msgstr "(ทดลอง) ใช้เอเจนต์เครื่องพิมพ์แทนโฮสต์การพิมพ์"
#~ msgid "X"
#~ msgstr "X"
@@ -31699,10 +31737,6 @@ msgstr ""
#~ msgid "An error occurred loading "
#~ msgstr "เกิดข้อผิดพลาดในการโหลด "
# AI Translated
#~ msgid "/"
#~ msgstr "/"
# AI Translated
#, c-format, boost-format
#~ msgid "Load %s to "
+52 -18
View File
@@ -3,7 +3,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Orca Slicer\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-05 10:49-0300\n"
"POT-Creation-Date: 2026-10-06 13:42+0800\n"
"PO-Revision-Date: 2026-08-21 23:18+0300\n"
"Last-Translator: GlauTech\n"
"Language-Team: \n"
@@ -4384,6 +4384,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)"
@@ -6258,7 +6261,7 @@ msgstr "Hacim:"
msgid "Size:"
msgstr "Boyut:"
#, c-format, boost-format
#, boost-format
msgid "Conflicts of G-code paths have been found at layer %d, Z = %.2lfmm. Please separate the conflicted objects farther (%s <-> %s)."
msgstr "%d katmanında G-code yollarında çakışmalar bulundu, Z = %.2lfmm. Lütfen çakışan nesneleri daha uzağa ayırın (%s <-> %s)."
@@ -7134,6 +7137,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"
@@ -10358,18 +10377,6 @@ msgstr "Desteklenmeyen ön ayarları göster"
msgid "Show incompatible/unsupported presets in the printer and filament dropdown lists. These presets cannot be selected."
msgstr "Yazıcı ve filament açılır listelerinde uyumsuz/desteklenmeyen ön ayarları gösterir. Bu ön ayarlar seçilemez."
# AI Translated
msgid "(Experimental) Use printer agents instead of print hosts"
msgstr "(Deneysel) Baskı sunucuları yerine yazıcı aracılarını kullan"
# AI Translated
msgid ""
"Route print jobs for non-Bambu printers through printer plug-in agents instead of the classic print-host upload flow.\n"
"When disabled, OrcaSlicer uses the legacy print-host behavior."
msgstr ""
"Bambu olmayan yazıcıların baskı işlerini, klasik baskı sunucusuna yükleme akışı yerine yazıcı eklenti aracıları üzerinden yönlendirir.\n"
"Devre dışı bırakıldığında OrcaSlicer eski baskı sunucusu davranışını kullanır."
# AI Translated
msgid "Experimental Features"
msgstr "Deneysel Özellikler"
@@ -10386,6 +10393,17 @@ msgstr ""
"Nesne mesh'i değiştikten sonra (kesme/diskten yeniden yükleme/basitleştirme/onarma vb.) boyanmış özellikleri (renk/dikiş/destek/pütür vb.) korumaya çalışır.\n"
"Oldukça deneyseldir! Yavaştır ve bozulmalara yol açabilir."
msgid "Use printer agents instead of print hosts"
msgstr ""
# AI Translated
msgid ""
"Route print jobs for non-Bambu printers through printer plug-in agents instead of the classic print-host upload flow.\n"
"When disabled, OrcaSlicer uses the legacy print-host behavior."
msgstr ""
"Bambu olmayan yazıcıların baskı işlerini, klasik baskı sunucusuna yükleme akışı yerine yazıcı eklenti aracıları üzerinden yönlendirir.\n"
"Devre dışı bırakıldığında OrcaSlicer eski baskı sunucusu davranışını kullanır."
msgid "Allow Abnormal Storage"
msgstr "Anormal Depolamaya İzin Ver"
@@ -11199,6 +11217,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"
@@ -16503,6 +16527,12 @@ msgstr "Simetrik dolgu Y ekseni"
msgid "If the model has two parts that are symmetric about the Y axis, and you want these parts to have symmetric textures, please click this option on one of the parts."
msgstr "Modelin Y ekseni etrafında simetrik iki parçası varsa ve bu parçaların simetrik dokulara sahip olmasını istiyorsanız, lütfen parçalardan birinde bu seçeneğe tıklayın."
msgid "Fill pattern tops"
msgstr ""
msgid "Choose this option if you want to completely fill in the tops of the infill pattern"
msgstr ""
msgid "Infill combination - Max layer height"
msgstr "Dolgu kombinasyonu - Maksimum katman yüksekliği"
@@ -30445,6 +30475,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%"
@@ -32148,10 +32182,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 "
@@ -32357,6 +32387,10 @@ msgstr ""
#~ msgid "Support/Color Painting: adjust section position"
#~ msgstr "Destek/Renkli Boyama: bölüm konumunu ayarlayın"
# AI Translated
#~ msgid "(Experimental) Use printer agents instead of print hosts"
#~ msgstr "(Deneysel) Baskı sunucuları yerine yazıcı aracılarını kullan"
# AI Translated
#~ msgid "Tab"
#~ msgstr "Sekme"
+51 -17
View File
@@ -3,7 +3,7 @@ msgid ""
msgstr ""
"Project-Id-Version: orcaslicerua\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-05 10:49-0300\n"
"POT-Creation-Date: 2026-10-06 13:42+0800\n"
"PO-Revision-Date: 2026-07-17 16:25+0300\n"
"Last-Translator: Andrij Mizyk <andm1zyk@proton.me>\n"
"Language-Team: Ukrainian\n"
@@ -4298,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)"
@@ -6212,7 +6215,7 @@ msgid "Size:"
msgstr "Розмір:"
# AI Translated
#, c-format, boost-format
#, boost-format
msgid "Conflicts of G-code paths have been found at layer %d, Z = %.2lfmm. Please separate the conflicted objects farther (%s <-> %s)."
msgstr "Виявлено конфлікти шляхів G-code на шарі %d, Z = %.2lf мм. Будь ласка, рознесіть конфліктуючі обʼєкти далі один від одного (%s <-> %s)."
@@ -7103,6 +7106,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"
@@ -10365,18 +10384,6 @@ msgstr "Показати непідтримувані пресети"
msgid "Show incompatible/unsupported presets in the printer and filament dropdown lists. These presets cannot be selected."
msgstr "Показати несумісні/непідтримувані пресети у випадаючому списку принтера і філаменту. Ці пресети не можна вибрати."
# AI Translated
msgid "(Experimental) Use printer agents instead of print hosts"
msgstr "(Експериментально) Використовувати агентів принтера замість хостів друку"
# AI Translated
msgid ""
"Route print jobs for non-Bambu printers through printer plug-in agents instead of the classic print-host upload flow.\n"
"When disabled, OrcaSlicer uses the legacy print-host behavior."
msgstr ""
"Спрямовує завдання друку для принтерів, відмінних від Bambu, через агентів плагінів принтера замість класичного завантаження на хост друку.\n"
"Коли вимкнено, OrcaSlicer використовує попередню поведінку хоста друку."
msgid "Experimental Features"
msgstr "Експериментальні функції"
@@ -10391,6 +10398,17 @@ msgstr ""
"Намагатися зберегти намальовані елементи (колір/шов/підтримки/шорсткість тощо) після зміни сітки обʼєкта (наприклад, розрізання/перезавантаження з диска/спрощення/виправлення тощо)\n"
"Дуже експериментально! Повільно і може створювати артефакти."
msgid "Use printer agents instead of print hosts"
msgstr ""
# AI Translated
msgid ""
"Route print jobs for non-Bambu printers through printer plug-in agents instead of the classic print-host upload flow.\n"
"When disabled, OrcaSlicer uses the legacy print-host behavior."
msgstr ""
"Спрямовує завдання друку для принтерів, відмінних від Bambu, через агентів плагінів принтера замість класичного завантаження на хост друку.\n"
"Коли вимкнено, OrcaSlicer використовує попередню поведінку хоста друку."
msgid "Allow Abnormal Storage"
msgstr "Дозволити незвичне сховище"
@@ -11216,6 +11234,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 "Підготовка завдання для друку"
@@ -16656,6 +16680,12 @@ msgstr "Симетричне заповнення по осі Y"
msgid "If the model has two parts that are symmetric about the Y axis, and you want these parts to have symmetric textures, please click this option on one of the parts."
msgstr "Якщо модель має дві частини, симетричні відносно осі Y, і ви хочете, щоб ці частини мали симетричні текстури, увімкніть цю опцію на одній із частин."
msgid "Fill pattern tops"
msgstr ""
msgid "Choose this option if you want to completely fill in the tops of the infill pattern"
msgstr ""
msgid "Infill combination - Max layer height"
msgstr "Комбінація заповнення - Максимальна висота шару"
@@ -30612,6 +30642,9 @@ msgstr "Увімкнути інструменти розробника"
msgid "An error occurred loading %s"
msgstr "Помилка під час завантаження %s"
msgid "/"
msgstr "/"
# AI Translated
#, boost-format
msgid "Load %1% to %2%"
@@ -31296,6 +31329,10 @@ msgstr ""
"Уникнення деформації\n"
"Чи знаєте ви, що при друку матеріалами, схильними до деформації, такими як ABS, відповідне підвищення температури столу може зменшити ймовірність деформації?"
# AI Translated
#~ msgid "(Experimental) Use printer agents instead of print hosts"
#~ msgstr "(Експериментально) Використовувати агентів принтера замість хостів друку"
#~ msgid "X"
#~ msgstr "X"
@@ -32342,9 +32379,6 @@ msgstr ""
#~ msgid "An error occurred loading "
#~ msgstr "Помилка під час завантаження "
#~ msgid "/"
#~ msgstr "/"
# AI Translated
#, c-format, boost-format
#~ msgid "Load %s to "
+52 -18
View File
@@ -3,7 +3,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Orca Slicer\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-05 10:49-0300\n"
"POT-Creation-Date: 2026-10-06 13:42+0800\n"
"PO-Revision-Date: 2025-10-02 17:43+0700\n"
"Last-Translator: \n"
"Language-Team: hainguyen.ts13@gmail.com\n"
@@ -4558,6 +4558,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)"
@@ -6565,7 +6568,7 @@ msgstr "Thể tích:"
msgid "Size:"
msgstr "Kích thước:"
#, c-format, boost-format
#, boost-format
msgid "Conflicts of G-code paths have been found at layer %d, Z = %.2lfmm. Please separate the conflicted objects farther (%s <-> %s)."
msgstr "Đã tìm thấy xung đột đường đi G-code tại lớp %d, Z = %.2lfmm. Vui lòng tách các vật thể xung đột ra xa hơn (%s <-> %s)."
@@ -7469,6 +7472,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"
@@ -10876,18 +10895,6 @@ msgstr "Hiện cài đặt sẵn không được hỗ trợ"
msgid "Show incompatible/unsupported presets in the printer and filament dropdown lists. These presets cannot be selected."
msgstr "Hiện các cài đặt sẵn không tương thích/không được hỗ trợ trong danh sách thả xuống máy in và filament. Không thể chọn các cài đặt sẵn này."
# AI Translated
msgid "(Experimental) Use printer agents instead of print hosts"
msgstr "(Thử nghiệm) Dùng tác nhân máy in thay cho máy chủ in"
# AI Translated
msgid ""
"Route print jobs for non-Bambu printers through printer plug-in agents instead of the classic print-host upload flow.\n"
"When disabled, OrcaSlicer uses the legacy print-host behavior."
msgstr ""
"Định tuyến các tác vụ in của máy in không phải Bambu qua các tác nhân plugin máy in thay vì luồng tải lên máy chủ in cổ điển.\n"
"Khi tắt, OrcaSlicer sẽ dùng hành vi máy chủ in cũ."
# AI Translated
msgid "Experimental Features"
msgstr "Tính năng thử nghiệm"
@@ -10904,6 +10911,17 @@ msgstr ""
"Cố gắng giữ lại các phần đã tô (màu/đường nối/support/fuzzy v.v.) sau khi thay đổi lưới của vật thể (như cắt/tải lại từ đĩa/đơn giản hóa/sửa v.v.)\n"
"Rất thử nghiệm! Chậm và có thể tạo ra khuyết điểm."
msgid "Use printer agents instead of print hosts"
msgstr ""
# AI Translated
msgid ""
"Route print jobs for non-Bambu printers through printer plug-in agents instead of the classic print-host upload flow.\n"
"When disabled, OrcaSlicer uses the legacy print-host behavior."
msgstr ""
"Định tuyến các tác vụ in của máy in không phải Bambu qua các tác nhân plugin máy in thay vì luồng tải lên máy chủ in cổ điển.\n"
"Khi tắt, OrcaSlicer sẽ dùng hành vi máy chủ in cũ."
# AI Translated
msgid "Allow Abnormal Storage"
msgstr "Cho phép Bộ nhớ bất thường"
@@ -11742,6 +11760,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"
@@ -17202,6 +17226,12 @@ msgstr "Infill đối xứng trục Y"
msgid "If the model has two parts that are symmetric about the Y axis, and you want these parts to have symmetric textures, please click this option on one of the parts."
msgstr "Nếu model có hai phần đối xứng về trục Y, và bạn muốn các phần này có kết cấu đối xứng, vui lòng nhấp tùy chọn này trên một trong các phần."
msgid "Fill pattern tops"
msgstr ""
msgid "Choose this option if you want to completely fill in the tops of the infill pattern"
msgstr ""
msgid "Infill combination - Max layer height"
msgstr "Kết hợp infill - Chiều cao lớp tối đa"
@@ -31285,6 +31315,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%"
@@ -33022,10 +33056,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 "
@@ -33232,6 +33262,10 @@ msgstr ""
#~ msgid "Support/Color Painting: adjust section position"
#~ msgstr "Tô support/màu: điều chỉnh vị trí phần"
# AI Translated
#~ msgid "(Experimental) Use printer agents instead of print hosts"
#~ msgstr "(Thử nghiệm) Dùng tác nhân máy in thay cho máy chủ in"
# AI Translated
#~ msgid "Tab"
#~ msgstr "Tab"
+52 -18
View File
@@ -6,7 +6,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Slic3rPE\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-05 10:49-0300\n"
"POT-Creation-Date: 2026-10-06 13:42+0800\n"
"PO-Revision-Date: 2026-06-11 12:37-0300\n"
"Last-Translator: Handle <mail@bysb.net>\n"
"Language-Team: \n"
@@ -4185,6 +4185,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)"
@@ -6042,7 +6045,7 @@ msgstr "体积:"
msgid "Size:"
msgstr "尺寸:"
#, c-format, boost-format
#, boost-format
msgid "Conflicts of G-code paths have been found at layer %d, Z = %.2lfmm. Please separate the conflicted objects farther (%s <-> %s)."
msgstr "发现G-code路径在层%d,高度为%.2lf mm处有冲突。请将有冲突的对象分离得更远(%s <-> %s)。"
@@ -6906,6 +6909,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"
@@ -10037,18 +10056,6 @@ msgstr "显示不受支持的预设"
msgid "Show incompatible/unsupported presets in the printer and filament dropdown lists. These presets cannot be selected."
msgstr "在打印机和耗材下拉列表中显示不兼容/不受支持的预设。这些预设无法被选择。"
# AI Translated
msgid "(Experimental) Use printer agents instead of print hosts"
msgstr "(实验性)使用打印机代理替代打印主机"
# AI Translated
msgid ""
"Route print jobs for non-Bambu printers through printer plug-in agents instead of the classic print-host upload flow.\n"
"When disabled, OrcaSlicer uses the legacy print-host behavior."
msgstr ""
"非 Bambu 打印机的打印任务将通过打印机插件代理发送,而不是经典的打印主机上传流程。\n"
"禁用时,OrcaSlicer 使用旧的打印主机行为。"
# AI Translated
msgid "Experimental Features"
msgstr "实验性功能"
@@ -10064,6 +10071,17 @@ msgstr ""
"在更改对象网格(如切割/从磁盘重新加载/简化/修复等)后,尝试保留绘制的特征(颜色/接缝/支撑/绒毛等)。\n"
"高度实验性!速度较慢且可能产生瑕疵。"
msgid "Use printer agents instead of print hosts"
msgstr ""
# AI Translated
msgid ""
"Route print jobs for non-Bambu printers through printer plug-in agents instead of the classic print-host upload flow.\n"
"When disabled, OrcaSlicer uses the legacy print-host behavior."
msgstr ""
"非 Bambu 打印机的打印任务将通过打印机插件代理发送,而不是经典的打印主机上传流程。\n"
"禁用时,OrcaSlicer 使用旧的打印主机行为。"
msgid "Allow Abnormal Storage"
msgstr "允许异常存储"
@@ -10851,6 +10869,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 "正在准备打印任务"
@@ -15985,6 +16009,12 @@ msgstr "对称填充Y轴"
msgid "If the model has two parts that are symmetric about the Y axis, and you want these parts to have symmetric textures, please click this option on one of the parts."
msgstr "若模型有两个在Y轴上对称的部分,并且您想对其使用对称的纹理,就请在对应的模型部分上应用此设置。"
msgid "Fill pattern tops"
msgstr ""
msgid "Choose this option if you want to completely fill in the tops of the infill pattern"
msgstr ""
msgid "Infill combination - Max layer height"
msgstr "填充组合 - 最大层高"
@@ -29724,6 +29754,10 @@ msgstr "启用开发者工具"
msgid "An error occurred loading %s"
msgstr "加载 %s 时出错"
# AI Translated
msgid "/"
msgstr "/"
# AI Translated
#, boost-format
msgid "Load %1% to %2%"
@@ -30403,6 +30437,10 @@ msgstr ""
"避免翘曲\n"
"您知道吗?打印ABS这类易翘曲材料时,适当提高热床温度可以降低翘曲的概率。"
# AI Translated
#~ msgid "(Experimental) Use printer agents instead of print hosts"
#~ msgstr "(实验性)使用打印机代理替代打印主机"
#~ msgid "X"
#~ msgstr "X"
@@ -31422,10 +31460,6 @@ msgstr ""
#~ msgid "An error occurred loading "
#~ msgstr "加载时出错: "
# AI Translated
#~ msgid "/"
#~ msgstr "/"
# AI Translated
#, c-format, boost-format
#~ msgid "Load %s to "
+52 -18
View File
@@ -9,7 +9,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Orca Slicer\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-05 10:49-0300\n"
"POT-Creation-Date: 2026-10-06 13:42+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"
@@ -4290,6 +4290,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 之間)"
@@ -6172,7 +6175,7 @@ msgstr "體積:"
msgid "Size:"
msgstr "尺寸:"
#, c-format, boost-format
#, boost-format
msgid "Conflicts of G-code paths have been found at layer %d, Z = %.2lfmm. Please separate the conflicted objects farther (%s <-> %s)."
msgstr "發現 G-code 路徑在 %d 層,Z = %.2lf mm 處的衝突。請將有衝突的物件分離得更遠(%s <-> %s)。"
@@ -7041,6 +7044,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"
@@ -10212,18 +10231,6 @@ msgstr "顯示不支援的預設"
msgid "Show incompatible/unsupported presets in the printer and filament dropdown lists. These presets cannot be selected."
msgstr "在列印裝置和線材下拉選單中顯示不相容/不支援的預設。這些預設無法選取。"
# AI Translated
msgid "(Experimental) Use printer agents instead of print hosts"
msgstr "(實驗性)使用列印裝置代理程式取代列印主機"
# AI Translated
msgid ""
"Route print jobs for non-Bambu printers through printer plug-in agents instead of the classic print-host upload flow.\n"
"When disabled, OrcaSlicer uses the legacy print-host behavior."
msgstr ""
"將非 Bambu 列印裝置的列印工作透過列印裝置外掛代理程式傳送,而非傳統的列印主機上傳流程。\n"
"停用時,OrcaSlicer 會使用舊有的列印主機行為。"
# AI Translated
msgid "Experimental Features"
msgstr "實驗性功能"
@@ -10239,6 +10246,17 @@ msgstr ""
"嘗試在變更物件網格(例如切割/從磁碟重新載入/簡化/修復等)後,保留已繪製的特徵(顏色/Z 縫/支撐/絨毛等)。\n"
"高度實驗性!速度緩慢且可能產生瑕疵。"
msgid "Use printer agents instead of print hosts"
msgstr ""
# AI Translated
msgid ""
"Route print jobs for non-Bambu printers through printer plug-in agents instead of the classic print-host upload flow.\n"
"When disabled, OrcaSlicer uses the legacy print-host behavior."
msgstr ""
"將非 Bambu 列印裝置的列印工作透過列印裝置外掛代理程式傳送,而非傳統的列印主機上傳流程。\n"
"停用時,OrcaSlicer 會使用舊有的列印主機行為。"
msgid "Allow Abnormal Storage"
msgstr "允許異常儲存空間"
@@ -11049,6 +11067,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 "正在準備列印作業"
@@ -16191,6 +16215,12 @@ msgstr "對稱填充Y軸"
msgid "If the model has two parts that are symmetric about the Y axis, and you want these parts to have symmetric textures, please click this option on one of the parts."
msgstr "若模型有兩個在Y軸上對稱的部分,並且您想對其使用對稱的紋理,就請在對應的模型部分上應用此設定。"
msgid "Fill pattern tops"
msgstr ""
msgid "Choose this option if you want to completely fill in the tops of the infill pattern"
msgstr ""
msgid "Infill combination - Max layer height"
msgstr "合併填充 - 最大層高"
@@ -29917,6 +29947,10 @@ msgstr "啟用開發者工具"
msgid "An error occurred loading %s"
msgstr "載入 %s 時發生錯誤"
# AI Translated
msgid "/"
msgstr "/"
# AI Translated
#, boost-format
msgid "Load %1% to %2%"
@@ -30617,6 +30651,10 @@ msgstr ""
"避免翹曲\n"
"您知道嗎?當列印容易翹曲的材料(如 ABS)時,適當提高熱床溫度可以降低翹曲的機率。"
# AI Translated
#~ msgid "(Experimental) Use printer agents instead of print hosts"
#~ msgstr "(實驗性)使用列印裝置代理程式取代列印主機"
#~ msgid "X"
#~ msgstr "X"
@@ -31637,10 +31675,6 @@ msgstr ""
#~ msgid "An error occurred loading "
#~ msgstr "載入時發生錯誤: "
# AI Translated
#~ msgid "/"
#~ msgstr "/"
# AI Translated
#, c-format, boost-format
#~ msgid "Load %s to "
+14 -45
View File
@@ -8,9 +8,6 @@ include) fails wherever the error is. Headers are compiled on their own, and
fail only on errors in their changed lines. Deleting an #include also fails
on every use, changed or not, that now lacks the header it provided.
With -- --fix, clang-tidy adds the missing includes, on those lines only, and
each file it changed is checked again so that only what remains is reported.
The checks come from .clang-tidy at the repository root. The compile database
must come from a configure with SLIC3R_PCH=OFF, or the precompiled header hides
missing includes.
@@ -105,10 +102,9 @@ def is_checked(path):
def changed_files(merge_base):
# Against the working tree, so a local run covers uncommitted edits too.
# core.quotePath=false keeps a non-ASCII path unquoted, and the explicit prefixes
# override diff.noprefix and diff.mnemonicPrefix, so parse_diff sees its b/ prefix.
# core.quotePath=false keeps a non-ASCII path unquoted, so parse_diff sees its b/ prefix.
diff = subprocess.run(["git", "-c", "core.quotePath=false", "diff", "-U0", "--no-color", "--no-ext-diff",
"--src-prefix=a/", "--dst-prefix=b/", "--diff-filter=AMR", merge_base],
"--diff-filter=AMR", merge_base],
check=True, capture_output=True, **UTF8).stdout
return {path: change for path, change in parse_diff(diff).items() if is_checked(path)}
@@ -205,19 +201,13 @@ def errors_alone_at(revision, clang_tidy, build_dir, path):
def check_file(clang_tidy, build_dir, merge_base, path, change, extra_args):
"""Run clang-tidy on one file and return (failed, output, failing diagnostics, fixed)."""
fixing = any(arg.startswith("--fix") for arg in extra_args)
if fixing:
with open(path, "rb") as f:
before = f.read()
"""Run clang-tidy on one file and return (failed, output, failing diagnostics)."""
# A deleted include can orphan uses on unchanged lines, so such a file is
# checked whole and the findings narrowed here. --fix keeps a line filter so
# it never rewrites unrelated code, which for such a file means a second,
# fixing run limited to the lines the first one found wanting.
whole = bool(change.removed_includes)
# checked whole and the findings narrowed here. --fix keeps the line filter
# so it never rewrites unrelated code.
whole = bool(change.removed_includes) and not extra_args
returncode, output, diagnostics = run_clang_tidy(clang_tidy, build_dir, path,
None if whole else change.lines,
[] if whole else extra_args)
None if whole else change.lines, extra_args)
real = os.path.realpath(path)
def introduced(d):
@@ -233,25 +223,14 @@ def check_file(clang_tidy, build_dir, merge_base, path, change, extra_args):
if errors and change.removed_includes:
with open(path, encoding="utf-8") as f:
text = f.read()
before_sites = errors_alone_at(merge_base, clang_tidy, build_dir, path)
before = errors_alone_at(merge_base, clang_tidy, build_dir, path)
failing += [d for d in errors if d not in failing
and (error_sites([d], text) - before_sites)]
failed = bool(failing)
elif whole:
and (error_sites([d], text) - before)]
return bool(failing), output, failing
if whole:
failing = [d for d in diagnostics if d.is_compile_error or introduced(d)]
failed = bool(failing)
else:
failing, failed = diagnostics, returncode != 0
if not fixing:
return failed, output, failing, False
if whole and failing:
lines = change.lines + [[d.line, d.line] for d in failing if os.path.realpath(d.file) == real]
run_clang_tidy(clang_tidy, build_dir, path, lines, extra_args)
with open(path, "rb") as f:
if f.read() == before:
return failed, output, failing, False
# Checked again, so what is reported is what the fixes left.
return check_file(clang_tidy, build_dir, merge_base, path, change, [])[:3] + (True,)
return bool(failing), output, failing
return returncode != 0, output, diagnostics
def main():
@@ -284,14 +263,11 @@ def main():
annotate = os.environ.get("GITHUB_ACTIONS") == "true"
root = os.getcwd() + os.sep
failed = []
fixed = []
with ThreadPoolExecutor(max_workers=args.jobs) as pool:
jobs = {path: pool.submit(check_file, args.clang_tidy, args.build_dir, merge_base, path, change, args.extra_args)
for path, change in todo}
for path, job in jobs.items():
file_failed, output, diagnostics, file_fixed = job.result()
if file_fixed:
fixed.append(path)
file_failed, output, diagnostics = job.result()
if not file_failed:
continue
failed.append(path)
@@ -306,13 +282,6 @@ def main():
if len(diagnostics) > MAX_REPORTED:
print(f"... and {len(diagnostics) - MAX_REPORTED} more")
if fixed:
print(f"\nAdded includes to {len(fixed)} file(s):")
for path in fixed:
print(f" {path}")
if failed and fixed:
print(f"\nclang-tidy still fails on {len(failed)} file(s); the findings above are what --fix could not add.")
return 1
if failed:
print(f"\nclang-tidy failed on {len(failed)} file(s). Add the includes it names, or apply its "
"suggestions locally with scripts/run_clang_tidy.sh --fix (scripts\\run_clang_tidy.ps1 -Fix on Windows).")
+10 -40
View File
@@ -6,9 +6,8 @@
# scripts/run_clang_tidy.sh --fix also add the missing includes it names
#
# It configures a separate build directory (build-tidy) without the precompiled
# header, uses the clang-tidy on your system or installs the pinned one into a
# virtual environment inside it, and runs scripts/clang_tidy_diff.py the way CI
# does. Uncommitted changes are checked too.
# header, installs the pinned clang-tidy into a virtual environment inside it, and
# runs scripts/clang_tidy_diff.py the way CI does. Uncommitted changes are checked too.
set -euo pipefail
@@ -24,8 +23,7 @@ Usage: scripts/run_clang_tidy.sh [options]
deps/build/<arch> on macOS)
-j, --jobs N parallel clang-tidy runs (default: all cores)
--fix apply clang-tidy's fixes (adds the missing includes)
-y, --yes install missing tools without asking; another clang-tidy
version found on the system is then not offered
-y, --yes install missing tools without asking
-h, --help show this help
EOF
}
@@ -138,38 +136,12 @@ REQUIREMENTS="$ROOT/scripts/clang_tidy_requirements.txt"
PINNED=$(sed -n 's/^clang-tidy==//p' "$REQUIREMENTS")
VENV="$BUILD_DIR/clang-tidy-venv"
is_pinned() {
[ -x "$1" ] && "$1" --version 2>/dev/null | grep -q "version $PINNED"
}
CLANG_TIDY="${CLANG_TIDY:-}"
if [ -n "$CLANG_TIDY" ]; then
if [ -n "${CLANG_TIDY:-}" ]; then
# Set by the caller: use it as is.
is_pinned "$CLANG_TIDY" || echo "Warning: $CLANG_TIDY is not clang-tidy $PINNED, so results may differ from CI." >&2
:
else
# One already on the system comes first: the pinned version outright, another
# version if the user accepts the difference. The pinned version is installed
# into a virtual environment otherwise.
INSTALLED=""
for candidate in $(command -v clang-tidy "clang-tidy-${PINNED%%.*}" || true) \
"/usr/lib/llvm-${PINNED%%.*}/bin/clang-tidy" \
"$(brew --prefix llvm 2>/dev/null || true)/bin/clang-tidy"; do
if is_pinned "$candidate"; then
CLANG_TIDY="$candidate"
break
fi
[ -z "$INSTALLED" ] && [ -x "$candidate" ] && INSTALLED="$candidate"
done
if [ -z "$CLANG_TIDY" ] && [ -n "$INSTALLED" ] && [ "$YES" = 0 ] && ! is_pinned "$VENV/bin/clang-tidy"; then
echo "Found $INSTALLED, which is $("$INSTALLED" --version | sed -n 's/.*version \([0-9.]*\).*/\1/p' | head -n 1), not the $PINNED CI uses, so results may differ slightly."
if ask "Use it anyway?"; then
CLANG_TIDY="$INSTALLED"
fi
fi
fi
if [ -z "$CLANG_TIDY" ]; then
CLANG_TIDY="$VENV/bin/clang-tidy"
if ! is_pinned "$CLANG_TIDY"; then
if [ ! -x "$CLANG_TIDY" ] || ! "$CLANG_TIDY" --version | grep -q "version $PINNED"; then
if ask "clang-tidy $PINNED (the version CI uses) is not installed. Install it into $VENV?"; then
mkdir -p "$BUILD_DIR"
if ! python3 -m venv "$VENV"; then
@@ -186,6 +158,9 @@ if [ -z "$CLANG_TIDY" ]; then
fi
fi
fi
if ! "$CLANG_TIDY" --version | grep -q "version $PINNED"; then
echo "Warning: $CLANG_TIDY is not clang-tidy $PINNED, so results may differ from CI." >&2
fi
# --- Dependencies -------------------------------------------------------------
@@ -242,12 +217,7 @@ cmake --build "$BUILD_DIR" --target git_commit_hash_header >/dev/null
if [ -z "$BASE" ]; then
REMOTE=$(git remote -v | awk '/github\.com[:\/]OrcaSlicer\/OrcaSlicer(\.git)? \(fetch\)/ { print $1; exit }')
if [ -z "$REMOTE" ]; then
# Against a fork's main that already has the commits, nothing is checked.
echo "Warning: no remote points at github.com/OrcaSlicer/OrcaSlicer, so this compares against origin/main." >&2
echo "If origin is your fork, add the upstream remote (git remote add upstream https://github.com/OrcaSlicer/OrcaSlicer.git) or pass --base." >&2
REMOTE=origin
fi
REMOTE="${REMOTE:-origin}"
if [ "$FETCH" = 1 ]; then
git fetch --quiet "$REMOTE" main
fi
-57
View File
@@ -155,8 +155,6 @@ class TestSubprocessCalls(unittest.TestCase):
diff = "+++ b/src/libslic3r/Über.cpp\n@@ -1,0 +2 @@\n+// 打印\n"
files, call, _ = self.run_patched(clang_tidy_diff.changed_files, "base", stdout=diff)
self.assertIn("core.quotePath=false", call.args[0])
# Whatever diff.noprefix or diff.mnemonicPrefix a user has set.
self.assertIn("--dst-prefix=b/", call.args[0])
self.assertEqual(call.kwargs["encoding"], "utf-8")
self.assertEqual(files["src/libslic3r/Über.cpp"].lines, [[2, 2]])
@@ -169,60 +167,5 @@ class TestSubprocessCalls(unittest.TestCase):
self.assertEqual(call.kwargs["encoding"], "utf-8")
class TestCheckFileFix(unittest.TestCase):
"""check_file with -- --fix: what clang-tidy is run on, and what is reported afterwards."""
def setUp(self):
self.dir = tempfile.TemporaryDirectory()
self.addCleanup(self.dir.cleanup)
self.path = os.path.join(self.dir.name, "Color.cpp")
with open(self.path, "w") as f:
f.write("int x;\n")
def finding(self, line, include=""):
return clang_tidy_diff.Diagnostic(self.path, line, 1, "error",
'no header providing "x" is directly included [misc-include-cleaner]', include)
def check(self, change, results, fix_writes=None):
"""Run check_file with run_clang_tidy answering from `results` in turn; the --fix run
rewrites the file with `fix_writes` when given. Returns (result, calls)."""
calls = []
def run(clang_tidy, build_dir, path, lines, extra_args):
calls.append((lines, extra_args))
if "--fix" in extra_args and fix_writes is not None:
with open(path, "w") as f:
f.write(fix_writes)
return results[len(calls) - 1]
with mock.patch.object(clang_tidy_diff, "run_clang_tidy", side_effect=run):
result = clang_tidy_diff.check_file("clang-tidy", "build", "base", self.path, change, ["--fix"])
return result, calls
def test_a_deleted_include_is_fixed_on_the_lines_it_orphaned_only(self):
change = clang_tidy_diff.FileChange(lines=[[4, 4]], removed_includes={"libslic3r/Point.hpp"})
orphaned = self.finding(50, "<libslic3r/Point.hpp>")
unrelated = self.finding(60, "<vector>")
(failed, _, failing, fixed), calls = self.check(
change, [(1, "", [orphaned, unrelated]), (1, "", [orphaned]), (0, "", [])], fix_writes="#include <libslic3r/Point.hpp>\n")
self.assertEqual(calls, [(None, []), ([[4, 4], [50, 50]], ["--fix"]), (None, [])])
self.assertEqual((failed, failing, fixed), (False, [], True))
def test_a_file_the_fix_did_not_change_keeps_its_findings(self):
change = clang_tidy_diff.FileChange(lines=[[4, 4]])
error = clang_tidy_diff.Diagnostic(self.path, 4, 1, "error", "unknown type name 'Foo' [clang-diagnostic-error]")
(failed, _, failing, fixed), calls = self.check(change, [(1, "", [error])])
self.assertEqual(calls, [([[4, 4]], ["--fix"])])
self.assertEqual((failed, failing, fixed), (True, [error], False))
def test_a_changed_file_is_checked_again_and_reports_what_is_left(self):
change = clang_tidy_diff.FileChange(lines=[[4, 4]])
error = clang_tidy_diff.Diagnostic(self.path, 4, 1, "error", "unknown type name 'Foo' [clang-diagnostic-error]")
(failed, _, failing, fixed), calls = self.check(
change, [(1, "", [self.finding(4, "<vector>"), error]), (1, "", [error])], fix_writes="#include <vector>\n")
self.assertEqual(calls, [([[4, 4]], ["--fix"]), ([[4, 4]], [])])
self.assertEqual((failed, failing, fixed), (True, [error], True))
if __name__ == "__main__":
unittest.main()
+7
View File
@@ -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>
+105 -83
View File
@@ -751,50 +751,6 @@ template<typename T>
}
}
namespace {
// Writes G-code to a file in blocks and records in lines_ends the file offset after every '\n'
class GCodeFileWriter
{
public:
GCodeFileWriter(FilePtr& out, const std::string& out_path, std::vector<size_t>& lines_ends, const char* error_message)
: m_out(out), m_out_path(out_path), m_lines_ends(lines_ends), m_error_message(error_message)
{}
~GCodeFileWriter() { assert(m_buffer.empty() || std::uncaught_exceptions() > 0); }
void append(std::string_view text)
{
const size_t text_pos = m_file_pos + m_buffer.size();
for (size_t i = text.find('\n'); i != std::string_view::npos; i = text.find('\n', i + 1))
m_lines_ends.emplace_back(text_pos + i + 1);
m_buffer += text;
if (m_buffer.size() >= GCodeProcessor::Output_Block_Size)
flush();
}
void flush()
{
if (m_buffer.empty())
return;
fwrite(m_buffer.data(), 1, m_buffer.size(), m_out.f);
if (ferror(m_out.f)) {
m_out.close();
boost::nowide::remove(m_out_path.c_str());
throw Slic3r::RuntimeError(m_error_message);
}
m_file_pos += m_buffer.size();
m_buffer.clear();
}
private:
FilePtr& m_out;
const std::string& m_out_path;
std::vector<size_t>& m_lines_ends;
const char* m_error_message;
std::string m_buffer;
size_t m_file_pos{0};
};
} // namespace
// Helper class to modify and export gcode to file
class ExportLines
{
@@ -809,6 +765,16 @@ public:
enum class EWriteType { BySize, ByTime };
private:
static void update_lines_ends_and_out_file_pos(const std::string& out_string, std::vector<size_t>& lines_ends, size_t* out_file_pos)
{
for (size_t i = 0; i < out_string.size(); ++i) {
if (out_string[i] == '\n')
lines_ends.emplace_back((out_file_pos != nullptr) ? *out_file_pos + i + 1 : i + 1);
}
if (out_file_pos != nullptr)
*out_file_pos += out_string.size();
}
struct LineData
{
std::string line;
@@ -848,14 +814,12 @@ private:
EWriteType m_write_type{EWriteType::BySize};
// Time machines containing g1 times cache
const std::array<GCodeProcessor::TimeMachine, static_cast<size_t>(PrintEstimatedStatistics::ETimeMode::Count)>& m_machines;
// Output file writer
GCodeFileWriter& m_writer;
// Current time
std::array<float, static_cast<size_t>(PrintEstimatedStatistics::ETimeMode::Count)> m_times{0.0f, 0.0f};
// Current size of the cache in bytes
// Current size in bytes
size_t m_size{0};
// gcode lines cache, used only when writing by time
// gcode lines cache
std::deque<LineData> m_lines;
size_t m_added_lines_counter{0};
// map of gcode line ids from original to final
@@ -863,16 +827,16 @@ private:
std::vector<std::pair<size_t, size_t>> m_gcode_lines_map;
size_t m_times_cache_id{0};
size_t m_out_file_pos{0};
public:
ExportLines(EWriteType type, const std::array<GCodeProcessor::TimeMachine, static_cast<size_t>(PrintEstimatedStatistics::ETimeMode::Count)>& machines, GCodeFileWriter& writer)
ExportLines(EWriteType type, const std::array<GCodeProcessor::TimeMachine, static_cast<size_t>(PrintEstimatedStatistics::ETimeMode::Count)>& machines)
#ifndef NDEBUG
: m_statistics(*this)
, m_write_type(type)
, m_machines(machines)
, m_writer(writer){}
, m_machines(machines){}
#else
: m_write_type(type), m_machines(machines), m_writer(writer)
: m_write_type(type), m_machines(machines)
{}
#endif // NDEBUG
@@ -926,14 +890,11 @@ public:
if (line.empty())
return;
if (m_write_type == EWriteType::ByTime) {
m_lines.push_back({line, m_times});
m_lines.push_back({line, m_times});
#ifndef NDEBUG
m_statistics.add_line(line.length());
m_statistics.add_line(line.length());
#endif // NDEBUG
m_size += line.length();
} else
m_writer.append(line);
m_size += line.length();
++m_added_lines_counter;
if (!ignore_from_move) {
assert(!m_gcode_lines_map.empty());
@@ -999,35 +960,65 @@ public:
}
}
// when writing by time, pass the cached lines older than m_times[Normal] - backtrace_time to the writer
void write(float backtrace_time)
// write to file:
// m_write_type == EWriteType::ByTime - all lines older than m_time - backtrace_time
// m_write_type == EWriteType::BySize - all lines if current size is greater than 65535 bytes
void write(FilePtr& out, float backtrace_time, GCodeProcessorResult& result, const std::string& out_path)
{
if (m_write_type != EWriteType::ByTime)
if (m_lines.empty())
return;
while (!m_lines.empty() && m_lines.front().times[Normal] < m_times[Normal] - backtrace_time) {
const LineData& data = m_lines.front();
m_writer.append(data.line);
m_size -= data.line.length();
m_lines.pop_front();
// collect lines to write into a single string
std::string out_string;
if (!m_lines.empty()) {
if (m_write_type == EWriteType::ByTime) {
while (m_lines.front().times[Normal] < m_times[Normal] - backtrace_time) {
const LineData& data = m_lines.front();
out_string += data.line;
m_size -= data.line.length();
m_lines.pop_front();
#ifndef NDEBUG
m_statistics.remove_line();
m_statistics.remove_line();
#endif // NDEBUG
}
} else {
if (m_size > 65535) {
while (!m_lines.empty()) {
out_string += m_lines.front().line;
m_lines.pop_front();
}
m_size = 0;
#ifndef NDEBUG
m_statistics.remove_all_lines();
#endif // NDEBUG
}
}
}
{
write_to_file(out, out_string, result, out_path);
update_lines_ends_and_out_file_pos(out_string, result.lines_ends, &m_out_file_pos);
}
}
// flush the current content of the cache and the writer to file
void flush()
// flush the current content of the cache to file
void flush(FilePtr& out, GCodeProcessorResult& result, const std::string& out_path)
{
// collect lines to flush into a single string
std::string out_string;
while (!m_lines.empty()) {
m_writer.append(m_lines.front().line);
out_string += m_lines.front().line;
m_lines.pop_front();
}
m_size = 0;
#ifndef NDEBUG
m_statistics.remove_all_lines();
#endif // NDEBUG
m_writer.flush();
{
write_to_file(out, out_string, result, out_path);
update_lines_ends_and_out_file_pos(out_string, result.lines_ends, &m_out_file_pos);
}
}
void synchronize_moves(GCodeProcessorResult& result) const
@@ -1060,7 +1051,20 @@ public:
size_t get_size() const { return m_size; }
void reserve(size_t lines_count) { m_gcode_lines_map.reserve(lines_count); }
private:
void write_to_file(FilePtr& out, const std::string& out_string, GCodeProcessorResult& result, const std::string& out_path)
{
if (!out_string.empty()) {
if (true) {
fwrite((const void*) out_string.c_str(), 1, out_string.length(), out.f);
if (ferror(out.f)) {
out.close();
boost::nowide::remove(out_path.c_str());
throw Slic3r::RuntimeError("GCode processor post process export failed.\nIs the disk full?");
}
}
}
}
};
void GCodeProcessor::run_post_process()
@@ -1156,13 +1160,8 @@ void GCodeProcessor::run_post_process()
last_exported_stop[i] = time_in_minutes(m_time_processor.machines[i].time);
}
m_result.lines_ends.clear();
// m_result.lines_ends.emplace_back(std::vector<size_t>());
GCodeFileWriter writer(out, out_path, m_result.lines_ends, "GCode processor post process export failed.\nIs the disk full?");
ExportLines export_line(m_result.backtrace_enabled ? ExportLines::EWriteType::ByTime : ExportLines::EWriteType::BySize,
m_time_processor.machines, writer);
// The line map holds an entry for each line of the file, and the first pass counted them
export_line.reserve(m_line_id);
m_time_processor.machines);
// replace placeholder lines with the proper final value
// gcode_line is in/out parameter, to reduce expensive memory allocation
@@ -1536,6 +1535,9 @@ void GCodeProcessor::run_post_process()
}
};
m_result.lines_ends.clear();
// m_result.lines_ends.emplace_back(std::vector<size_t>());
// Orca: freshly collect SKIPPABLE ranges each post-process pass. The ranges are stored on the
// member (rather than a local) so the injection pass can consume them, hence the clear here to
// avoid stale ranges on re-invocation.
@@ -1800,7 +1802,7 @@ void GCodeProcessor::run_post_process()
if (!gcode_line.empty())
export_line.append_line(gcode_line);
export_line.write(1.1f * max_backtrace_time);
export_line.write(out, 1.1f * max_backtrace_time, m_result, out_path);
gcode_line.clear();
}
}
@@ -1842,7 +1844,7 @@ void GCodeProcessor::run_post_process()
}
}
export_line.flush();
export_line.flush(out, m_result, out_path);
out.close();
in.close();
@@ -1990,13 +1992,31 @@ void GCodeProcessor::run_second_pass_injection()
// The rewrite may shift byte positions (once the injector inserts lines), so rebuild lines_ends from scratch.
// With an empty map the scanned '\n' offsets reproduce the current lines_ends exactly.
m_result.lines_ends.clear();
GCodeFileWriter writer(out, out_path, m_result.lines_ends, "GCode processor pre-heat injection pass failed.\nIs the disk full?\n");
size_t out_file_pos = 0;
auto write_out = [&out, &out_path, this, &out_file_pos](std::string& str) {
if (str.empty())
return;
fwrite((const void*) str.c_str(), 1, str.length(), out.f);
if (ferror(out.f)) {
out.close();
boost::nowide::remove(out_path.c_str());
throw Slic3r::RuntimeError(std::string("GCode processor pre-heat injection pass failed.\nIs the disk full?\n"));
}
for (size_t i = 0; i < str.size(); ++i) {
if (str[i] == '\n')
m_result.lines_ends.emplace_back(out_file_pos + i + 1);
}
out_file_pos += str.size();
str.clear();
};
// Orca: read/split lines with EOL-preserving semantics (keep the original \r and \n bytes, and
// synthesize no trailing newline). This is required for the empty-map identity: normalizing every
// line ending to "\n" would not be byte-identical if the finished file used \r\n or lacked a
// final newline.
std::string gcode_line;
std::string export_buffer;
unsigned int line_id = 0;
auto op_it = inserted_operation_lines.begin();
std::vector<char> buffer(65536 * 10, 0);
@@ -2038,14 +2058,16 @@ void GCodeProcessor::run_second_pass_injection()
}
++op_it;
}
writer.append(gcode_line);
export_buffer += gcode_line;
gcode_line.clear();
if (export_buffer.length() >= 65536)
write_out(export_buffer);
}
}
if (eof)
break;
}
writer.flush();
write_out(export_buffer);
out.close();
in.close();
-3
View File
@@ -491,9 +491,6 @@ class Print;
static const std::string VFlush_End_Tag;
static const std::string External_Purge_Tag;
public:
// Size of the blocks the post-processing passes write the G-code in
static constexpr size_t Output_Block_Size = 65536;
// Orca: SKIPPABLE region tags, stored as static strings (the FLUSH idiom above) rather than
// a CustomETags/CustomTags array. Public so the emission sites (WipeTower / change_filament
// path) can reference them single-sourced.
+26 -125
View File
@@ -759,59 +759,6 @@ static void clip_inner_walls_over_top(std::vector<Arachne::VariableWidthLines> &
}
}
// ORCA: only_one_wall_top - widest bead of the given walls.
static coord_t widest_bead(const std::vector<Arachne::VariableWidthLines> &walls)
{
coord_t widest = 0;
for (const Arachne::VariableWidthLines &group : walls)
for (const Arachne::ExtrusionLine &el : group)
for (const Arachne::ExtrusionJunction &j : el.junctions)
widest = std::max(widest, j.w);
return widest;
}
// ORCA: only_one_wall_top - length of the walls running further than tolerance from the reference walls, outside the
// excluded area.
static double length_off_reference(const std::vector<Arachne::VariableWidthLines> &walls, const Arachne::VariableWidthLines &reference,
const ExPolygons &excluded, coord_t tolerance)
{
auto append_centerlines = [](const Arachne::VariableWidthLines &lines, Polylines &out) {
for (const Arachne::ExtrusionLine &el : lines) {
if (el.junctions.size() < 2)
continue;
Polyline &centerline = out.emplace_back();
centerline.points.reserve(el.junctions.size());
for (const Arachne::ExtrusionJunction &j : el.junctions)
centerline.points.emplace_back(j.p);
}
};
Polylines wall_centerlines;
Polylines reference_centerlines;
for (const Arachne::VariableWidthLines &group : walls)
append_centerlines(group, wall_centerlines);
append_centerlines(reference, reference_centerlines);
Polylines off_reference = diff_pl(wall_centerlines, offset(reference_centerlines, float(tolerance)));
if (! excluded.empty())
off_reference = diff_pl(off_reference, excluded);
return total_length(off_reference);
}
// ORCA: only_one_wall_top - area covered by the given walls at their local widths.
static Polygons walls_footprint(const Arachne::VariableWidthLines &walls)
{
Polygons footprint;
for (const Arachne::ExtrusionLine &el : walls)
for (size_t i = 1; i < el.junctions.size(); ++ i) {
const Arachne::ExtrusionJunction &a = el.junctions[i - 1];
const Arachne::ExtrusionJunction &b = el.junctions[i];
const coord_t width = std::max(a.w, b.w);
if (width > 0)
append(footprint, offset(Polyline(a.p, b.p), float(width) / 2.f));
}
return union_(footprint);
}
void PerimeterGenerator::split_top_surfaces(const ExPolygons &orig_polygons, ExPolygons &top_fills,
ExPolygons &non_top_polygons, ExPolygons &fill_clip) const {
// other perimeters
@@ -2599,90 +2546,44 @@ void PerimeterGenerator::process_arachne()
if (inner_loop_number >= 0) {
assert(upper_slices != nullptr);
// Infill contour bounding box.
BoundingBox infill_contour_bbox = get_extents(infill_contour);
infill_contour_bbox.offset(SCALED_EPSILON);
coord_t perimeter_width = this->perimeter_flow.scaled_width();
// Filter out areas that are too thin and expand top surface polygons a bit to hide the wall line.
// ORCA: skip if the top surface area is smaller than "min_width_top_surface"
const float top_surface_min_width = std::max<float>(float(ext_perimeter_spacing) / 4.f + scaled<float>(0.00001), float(scale_(config->min_width_top_surface.get_abs_value(unscale_(perimeter_width)))) / 4.f);
// Get top ExPolygons from current infill contour.
Polygons upper_slices_clipped;
if (object_config->interface_shells) {
auto upper_slicer_same_region = to_expolygons(this->upper_slices_same_region->surfaces);
upper_slices_clipped = ClipperUtils::clip_clipper_polygons_with_subject_bbox(upper_slicer_same_region, infill_contour_bbox);
} else
upper_slices_clipped = ClipperUtils::clip_clipper_polygons_with_subject_bbox(*upper_slices, infill_contour_bbox);
// Get top ExPolygons from the given contour. uncovered reports whether the upper layer leaves any of the
// contour uncovered, before bridges and too thin areas are filtered out.
auto get_top_expolygons = [&](const ExPolygons &contour, bool &uncovered) {
// Contour bounding box.
BoundingBox contour_bbox = get_extents(contour);
contour_bbox.offset(SCALED_EPSILON);
Polygons upper_slices_clipped;
if (object_config->interface_shells) {
auto upper_slicer_same_region = to_expolygons(this->upper_slices_same_region->surfaces);
upper_slices_clipped = ClipperUtils::clip_clipper_polygons_with_subject_bbox(upper_slicer_same_region, contour_bbox);
} else
upper_slices_clipped = ClipperUtils::clip_clipper_polygons_with_subject_bbox(*upper_slices, contour_bbox);
ExPolygons top = diff_ex(contour, upper_slices_clipped);
uncovered = !top.empty();
if (top.empty())
return top;
top_expolygons = diff_ex(infill_contour, upper_slices_clipped);
if (!top_expolygons.empty()) {
if (lower_slices != nullptr) {
const float bridge_offset = float(std::max<coord_t>(ext_perimeter_spacing, perimeter_width));
const Polygons lower_slices_clipped = ClipperUtils::clip_clipper_polygons_with_subject_bbox(*lower_slices, contour_bbox);
const ExPolygons current_slices_bridges = offset_ex(diff_ex(top, lower_slices_clipped), bridge_offset);
const Polygons lower_slices_clipped = ClipperUtils::clip_clipper_polygons_with_subject_bbox(*lower_slices, infill_contour_bbox);
const ExPolygons current_slices_bridges = offset_ex(diff_ex(top_expolygons, lower_slices_clipped), bridge_offset);
// Remove bridges from top surface polygons.
top = diff_ex(top, current_slices_bridges);
top_expolygons = diff_ex(top_expolygons, current_slices_bridges);
}
// Filter out areas that are too thin and expand top surface polygons a bit to hide the wall line.
// ORCA: skip if the top surface area is smaller than "min_width_top_surface"
const float top_surface_min_width = std::max<float>(float(ext_perimeter_spacing) / 4.f + scaled<float>(0.00001), float(scale_(config->min_width_top_surface.get_abs_value(unscale_(perimeter_width)))) / 4.f);
// Shrink the polygon to remove the small areas, then expand it back out plus a maragin to hide the wall line a little.
// ORCA: Expand the polygon with half the perimeter width in addition to the contracted amount,
// not the full perimeter width as PS does, to enable thin lettering to print on the top surface without nozzle collisions
// due to thin lines being generated
top = offset2_ex(top, -top_surface_min_width, top_surface_min_width + float(perimeter_width * 0.85));
top_expolygons = offset2_ex(top_expolygons, -top_surface_min_width, top_surface_min_width + float(perimeter_width * 0.85));
// Get final top ExPolygons (bridges were excluded above, so they stay walled).
return intersection_ex(top, contour);
};
top_expolygons = intersection_ex(top_expolygons, infill_contour);
// Walls with the full count, as generated when the single perimeter feature is disabled. Generated on first use.
std::vector<Arachne::VariableWidthLines> full_perimeters;
Polygons full_inner_contour;
bool full_perimeters_generated = false;
auto generate_full_perimeters = [&]() {
if (full_perimeters_generated)
return;
Arachne::WallToolPaths full_tool_paths(last_p, bead_width_0, perimeter_spacing, coord_t(inner_loop_number + 2), wall_0_inset, layer_height, input_params_tmp);
full_perimeters = full_tool_paths.getToolPaths();
full_inner_contour = full_tool_paths.getInnerContour();
full_perimeters_generated = true;
};
// ORCA: the single wall pass allows Arachne 2 beads across a wall, so it fills a wall narrower than 3 outer wall
// widths by widening both, where the full pass adds a middle bead. Over the top surface that is the intent;
// anywhere else it leaves no room for the inner walls. When the single wall pass's outer walls run away from
// the full pass's outside the top surface, take the full pass's outer walls and the area inside them instead.
// Walls closer than outer_wall_tolerance count as the same wall: a widened bead's centerline moves by half
// the width added, and only beads widened by more than twice the tolerance are looked for.
const coord_t outer_wall_tolerance = bead_width_0 / 10;
if (widest_bead(perimeters) > bead_width_0 + 2 * outer_wall_tolerance) {
// The single wall pass's inner contour where it widens no bead: inside nominal width outer walls.
const ExPolygons nominal_infill_contour = offset_ex(last, -float(bead_width_0 + wall_0_inset));
bool nominal_uncovered = false;
// Grown by an outer wall width to take in the outer walls bordering the top surface.
const ExPolygons top_zone = offset_ex(get_top_expolygons(nominal_infill_contour, nominal_uncovered), float(bead_width_0));
if (nominal_uncovered) {
generate_full_perimeters();
if (! full_perimeters.empty() && ! full_perimeters.front().empty() &&
length_off_reference(perimeters, full_perimeters.front(), top_zone, outer_wall_tolerance) > double(perimeter_width)) {
perimeters = { full_perimeters.front() };
infill_contour = diff_ex(nominal_infill_contour, walls_footprint(full_perimeters.front()), ApplySafetyOffset::Yes);
}
}
}
bool uncovered = false;
top_expolygons = get_top_expolygons(infill_contour, uncovered);
if (uncovered) {
// ORCA: onion the real region (inside the outer wall) so the remaining walls follow the actual
// geometry, then cut away the parts over the top surface. Re-onioning the non-top complement
// instead - the fallback when there is no top fill - walls the top/non-top interface and rings
@@ -2711,11 +2612,11 @@ void PerimeterGenerator::process_arachne()
perimeters.insert(perimeters.end(), inner_perimeters.begin(), inner_perimeters.end());
infill_contour = union_ex(top_expolygons, inner_wall_tool_paths.getInnerContour());
} else {
// There is no top surface ExPolygon, so use the walls generated like when the single perimeter
// feature is disabled.
generate_full_perimeters();
perimeters = std::move(full_perimeters);
infill_contour = union_ex(full_inner_contour);
// There is no top surface ExPolygon, so we call Arachne again with parameters
// like when the single perimeter feature is disabled.
Arachne::WallToolPaths no_single_perimeter_tool_paths(last_p, bead_width_0, perimeter_spacing, coord_t(inner_loop_number + 2), wall_0_inset, layer_height, input_params_tmp);
perimeters = no_single_perimeter_tool_paths.getToolPaths();
infill_contour = union_ex(no_single_perimeter_tool_paths.getInnerContour());
}
}
//PS
+111 -36
View File
@@ -3813,7 +3813,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()) {
@@ -3823,16 +3827,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
@@ -3843,7 +3879,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;
}
@@ -3881,6 +3921,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");
@@ -3893,42 +3934,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()) {
@@ -3968,6 +4025,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()) {
@@ -3975,6 +4033,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;
@@ -3990,7 +4049,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.") :
@@ -4001,6 +4061,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;
@@ -4440,9 +4501,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;
@@ -4464,7 +4537,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;
@@ -4477,8 +4552,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)
+3
View File
@@ -411,6 +411,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
+14 -1
View File
@@ -368,6 +368,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
@@ -457,6 +461,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
@@ -772,8 +781,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
@@ -917,7 +930,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})
+10 -1
View File
@@ -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);
+98 -43
View File
@@ -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>
@@ -472,6 +474,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();
@@ -494,6 +503,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();
@@ -543,7 +554,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;
@@ -564,7 +575,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()) {
@@ -723,6 +734,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
@@ -746,7 +772,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);
@@ -776,7 +802,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;
@@ -795,7 +821,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);
}
}
@@ -817,7 +843,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;
@@ -836,7 +862,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);
}
}
@@ -932,7 +958,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();
}
@@ -993,6 +1024,9 @@ 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;
@@ -1016,11 +1050,14 @@ 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);
@@ -1087,6 +1124,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) {
@@ -1223,7 +1266,12 @@ void AMSMaterialsSetting::Popup(wxString filament, wxString sn, wxString temp_mi
}
}
if (filament_items.IsEmpty())
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_from_printer = true;
m_comboBox_filament->SetSelection(selection_idx);
post_select_event(selection_idx);
@@ -1231,9 +1279,6 @@ void AMSMaterialsSetting::Popup(wxString filament, wxString sn, wxString temp_mi
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();
@@ -1277,50 +1322,53 @@ 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 = "";
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");
if (opt) machine_diameter = static_cast<const ConfigOptionFloats *>(opt)->values[0];
}
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()) {
if (!selected.empty() && !filament_printer_model.empty()) {
const std::string filament_id = map_filament_items[selected].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");
@@ -1359,13 +1407,16 @@ void AMSMaterialsSetting::on_select_filament(wxCommandEvent &evt)
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
@@ -1389,6 +1440,9 @@ void AMSMaterialsSetting::on_select_filament(wxCommandEvent &evt)
}
}
if (!ams_filament_id.empty())
m_clr_picker->is_empty(false);
wxArrayString items;
m_pa_profile_items.clear();
m_comboBox_cali_result->SetValue(wxEmptyString);
@@ -1444,7 +1498,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);
@@ -1461,10 +1515,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;
}
@@ -1495,7 +1550,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");
@@ -1503,7 +1558,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)
+4 -1
View File
@@ -138,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;
@@ -200,6 +200,9 @@ 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;
+7 -4
View File
@@ -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
-2
View File
@@ -58,8 +58,6 @@ public:
static bool parse_color(const std::string& scolor, unsigned char* rgb_out);
static bool parse_color4(const std::string& scolor, unsigned char* rgba_out);
// Rasterizes the SVG into a freshly generated GL texture; the caller owns it and has to delete
// it (ImGuiWrapper::svg_texture() caches the result for the whole session).
static bool load_from_svg_file_change_color(const std::string &filename, unsigned width, unsigned height, ImTextureID &texture_id, const char *hexColor);
+8 -1
View File
@@ -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);
+16 -1
View File
@@ -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>
@@ -20,6 +22,7 @@
#include "DevFilaSystem.h"
#include "json_diff.hpp"
#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
@@ -62,6 +65,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 = "";
@@ -83,6 +95,7 @@ void DevAmsTray::reset()
k = 0.0f;
n = 0.0f;
is_bbl = false;
is_empty = false;
hold_count = 0;
remain = 0;
}
@@ -682,7 +695,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")
{
@@ -782,6 +795,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");
+4 -1
View File
@@ -83,6 +83,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
@@ -91,13 +92,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
+110 -11
View File
@@ -65,6 +65,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
View File
@@ -14,6 +14,7 @@
#include <string>
#include <memory>
#include <chrono>
#include <set>
#include <unordered_set>
#include <optional>
#include <boost/thread.hpp>
@@ -73,6 +74,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
@@ -108,6 +110,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:
@@ -796,6 +803,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);
+105
View File
@@ -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
+11 -28
View File
@@ -3416,8 +3416,7 @@ void GLCanvas3D::load_sla_preview()
void GLCanvas3D::bind_event_handlers()
{
// Every view switch binds, so binding twice would run each handler twice per event.
if (m_canvas != nullptr && !m_event_handlers_bound) {
if (m_canvas != nullptr) {
m_canvas->Bind(wxEVT_SIZE, &GLCanvas3D::on_size, this);
m_canvas->Bind(wxEVT_IDLE, &GLCanvas3D::on_idle, this);
m_canvas->Bind(wxEVT_CHAR, &GLCanvas3D::on_char, this);
@@ -3427,9 +3426,9 @@ void GLCanvas3D::bind_event_handlers()
m_canvas->Bind(wxEVT_TIMER, &GLCanvas3D::on_timer, this);
m_canvas->Bind(EVT_GLCANVAS_RENDER_TIMER, &GLCanvas3D::on_render_timer, this);
m_toolbar_highlighter.set_timer_owner(m_canvas, 0);
m_canvas->Bind(EVT_GLCANVAS_TOOLBAR_HIGHLIGHTER_TIMER, &GLCanvas3D::on_toolbar_highlighter_timer, this);
m_canvas->Bind(EVT_GLCANVAS_TOOLBAR_HIGHLIGHTER_TIMER, [this](wxTimerEvent&) { m_toolbar_highlighter.blink(); });
m_gizmo_highlighter.set_timer_owner(m_canvas, 0);
m_canvas->Bind(EVT_GLCANVAS_GIZMO_HIGHLIGHTER_TIMER, &GLCanvas3D::on_gizmo_highlighter_timer, this);
m_canvas->Bind(EVT_GLCANVAS_GIZMO_HIGHLIGHTER_TIMER, [this](wxTimerEvent&) { m_gizmo_highlighter.blink(); });
m_canvas->Bind(wxEVT_LEFT_DOWN, &GLCanvas3D::on_mouse, this);
m_canvas->Bind(wxEVT_LEFT_UP, &GLCanvas3D::on_mouse, this);
m_canvas->Bind(wxEVT_MIDDLE_DOWN, &GLCanvas3D::on_mouse, this);
@@ -3444,7 +3443,14 @@ void GLCanvas3D::bind_event_handlers()
m_canvas->Bind(wxEVT_RIGHT_DCLICK, &GLCanvas3D::on_mouse, this);
m_canvas->Bind(wxEVT_PAINT, &GLCanvas3D::on_paint, this);
m_canvas->Bind(wxEVT_SET_FOCUS, &GLCanvas3D::on_set_focus, this);
m_canvas->Bind(wxEVT_KILL_FOCUS, &GLCanvas3D::on_kill_focus, this);
m_canvas->Bind(wxEVT_KILL_FOCUS, [this](wxFocusEvent& evt) {
// The key-up that would commit a keyboard edit goes to whatever took the focus.
if (m_selection_edit.kind != SelectionEdit::None)
finish_selection_edit();
ImGui::SetWindowFocus(nullptr);
render();
evt.Skip();
});
m_event_handlers_bound = true;
m_canvas->Bind(wxEVT_GESTURE_PAN, &GLCanvas3D::on_gesture, this);
@@ -3482,9 +3488,6 @@ void GLCanvas3D::unbind_event_handlers()
m_canvas->Unbind(wxEVT_RIGHT_DCLICK, &GLCanvas3D::on_mouse, this);
m_canvas->Unbind(wxEVT_PAINT, &GLCanvas3D::on_paint, this);
m_canvas->Unbind(wxEVT_SET_FOCUS, &GLCanvas3D::on_set_focus, this);
m_canvas->Unbind(wxEVT_KILL_FOCUS, &GLCanvas3D::on_kill_focus, this);
m_canvas->Unbind(EVT_GLCANVAS_TOOLBAR_HIGHLIGHTER_TIMER, &GLCanvas3D::on_toolbar_highlighter_timer, this);
m_canvas->Unbind(EVT_GLCANVAS_GIZMO_HIGHLIGHTER_TIMER, &GLCanvas3D::on_gizmo_highlighter_timer, this);
m_event_handlers_bound = false;
m_canvas->Unbind(wxEVT_GESTURE_PAN, &GLCanvas3D::on_gesture, this);
@@ -5002,26 +5005,6 @@ void GLCanvas3D::on_set_focus(wxFocusEvent& evt)
m_is_touchpad_navigation = wxGetApp().app_config->get_bool("camera_navigation_style");
}
void GLCanvas3D::on_kill_focus(wxFocusEvent& evt)
{
// The key-up that would commit a keyboard edit goes to whatever took the focus.
if (m_selection_edit.kind != SelectionEdit::None)
finish_selection_edit();
ImGui::SetWindowFocus(nullptr);
render();
evt.Skip();
}
void GLCanvas3D::on_toolbar_highlighter_timer(wxTimerEvent& evt)
{
m_toolbar_highlighter.blink();
}
void GLCanvas3D::on_gizmo_highlighter_timer(wxTimerEvent& evt)
{
m_gizmo_highlighter.blink();
}
bool GLCanvas3D::clicked_button_matches_action(const wxMouseEvent& evt, const MouseAction action, const std::map<MouseButton, MouseAction>& mappings) const
{
MouseButton clicked = MouseButton::None;
-3
View File
@@ -1203,9 +1203,6 @@ public:
void on_gesture(wxGestureEvent& evt);
void on_paint(wxPaintEvent& evt);
void on_set_focus(wxFocusEvent& evt);
void on_kill_focus(wxFocusEvent& evt);
void on_toolbar_highlighter_timer(wxTimerEvent& evt);
void on_gizmo_highlighter_timer(wxTimerEvent& evt);
void force_set_focus();
enum class MouseButton { None, Left, Middle, Right };
+16 -2
View File
@@ -1158,7 +1158,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
@@ -4130,6 +4131,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;
@@ -4201,7 +4214,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);
+3
View File
@@ -399,6 +399,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<->""
+6 -13
View File
@@ -25,7 +25,6 @@
#include <string>
#include <map>
#include <vector>
#include <memory>
#include "libslic3r/PrintConfig.hpp"
#include <wx/gdicmn.h>
#include "slic3r/GUI/wxExtensions.hpp"
@@ -2082,15 +2081,9 @@ wxMenu* MenuFactory::instance_menu()
return &m_instance_menu;
}
MenuWithSeparators* MenuFactory::new_transient_menu()
{
m_transient_menu = std::make_unique<MenuWithSeparators>();
return m_transient_menu.get();
}
wxMenu* MenuFactory::layer_menu()
{
MenuWithSeparators* menu = new_transient_menu();
MenuWithSeparators* menu = new MenuWithSeparators();
append_menu_item_settings(menu);
return menu;
@@ -2116,13 +2109,13 @@ wxMenu* MenuFactory::multi_selection_menu()
}
if (all_plates) {
wxMenu* menu = new_transient_menu();
wxMenu* menu = new MenuWithSeparators();
append_menu_item_replace_all_with_stl(menu);
return menu;
}
if (undefined_type)
return nullptr;
wxMenu* menu = new_transient_menu();
wxMenu* menu = new MenuWithSeparators();
if (!multi_volume) {
int index = 0;
if (obj_list()->can_merge_to_multipart_object()) {
@@ -2196,7 +2189,7 @@ wxMenu* MenuFactory::assemble_multi_selection_menu()
// show this menu only for Objects(Instances mixed with Objects)/Volumes selection
return nullptr;
wxMenu* menu = new_transient_menu();
wxMenu* menu = new MenuWithSeparators();
append_menu_item_set_visible(menu);
//append_menu_item_fix_through_cgal(menu);
//append_menu_item_simplify(menu);
@@ -2242,7 +2235,7 @@ wxMenu* MenuFactory::plate_menu()
wxMenu* MenuFactory::assemble_object_menu()
{
wxMenu* menu = new_transient_menu();
wxMenu* menu = new MenuWithSeparators();
// Set Visible
append_menu_item_set_visible(menu);
// Delete
@@ -2262,7 +2255,7 @@ wxMenu* MenuFactory::assemble_object_menu()
wxMenu* MenuFactory::assemble_part_menu()
{
wxMenu* menu = new_transient_menu();
wxMenu* menu = new MenuWithSeparators();
append_menu_item_set_visible(menu);
append_menu_item_delete(menu);
-10
View File
@@ -2,7 +2,6 @@
#define slic3r_GUI_Factories_hpp_
#include <map>
#include <memory>
#include <string>
#include <vector>
#include <array>
@@ -122,12 +121,6 @@ private:
MenuWithSeparators m_assemble_part_menu;
wxMenu m_filament_action_menu;
// The selection dependent menus are rebuilt for every popup, so they cannot be members that
// outlive a build like the ones above; this owns the current one and destroys the previous.
// One slot is enough because PopupMenu() is synchronous: the menu a caller was handed is gone
// from the screen before anything can ask for the next one.
std::unique_ptr<MenuWithSeparators> m_transient_menu;
// Removed/Prepended Items according to the view mode
@@ -135,9 +128,6 @@ private:
std::array<wxMenuItem*, mtCount> items_decrease;
std::array<wxMenuItem*, mtCount> items_set_number_of_copies;
// Replaces m_transient_menu with an empty menu and returns it.
MenuWithSeparators* new_transient_menu();
void create_default_menu();
void create_common_object_menu(wxMenu *menu);
void create_object_menu();
+27 -26
View File
@@ -41,20 +41,19 @@ namespace GUI
ObjectLayers::ObjectLayers(wxWindow* parent) :
OG_Settings(parent, true)
{
m_grid_sizer = new wxFlexGridSizer(5, parent ? parent->FromDIP(2) : 2, wxGetApp().em_unit()); // Title, Min Z, "to", Max Z, buttons sizer
m_grid_sizer = new wxFlexGridSizer(5, 0, wxGetApp().em_unit()); // Title, Min Z, "to", Max Z, unit & buttons sizer
m_grid_sizer->SetFlexibleDirection(wxHORIZONTAL);
m_grid_sizer->AddGrowableCol(1);
m_grid_sizer->AddGrowableCol(3);
m_og->activate();
m_og->sizer->Clear(true);
m_og->sizer->Add(m_grid_sizer, 0, wxEXPAND | wxLEFT | wxRIGHT, parent ? parent->FromDIP(2) : 2);
m_og->sizer->Add(m_grid_sizer, 0, wxEXPAND | wxLEFT | wxRIGHT, 5);
if (auto stb = dynamic_cast<LabeledStaticBox*>(m_og->stb))
stb->SetCornerRadius(0);
m_bmp_delete = ScalableBitmap(parent, "delete");
m_bmp_add = ScalableBitmap(parent, "add");
m_bmp_layer = ScalableBitmap(parent, "height_range_layer");
}
void ObjectLayers::select_editor(LayerRangeEditor* editor, const bool is_last_edited_range)
@@ -96,14 +95,10 @@ wxSizer* ObjectLayers::create_layer(const t_layer_height_range& range, PlusMinus
};
// Add text
auto title_sizer = new wxBoxSizer(wxHORIZONTAL);
auto head_text = new wxStaticText(m_og->ctrl_parent(), wxID_ANY, _L("Range"));
auto head_text = new wxStaticText(m_og->ctrl_parent(), wxID_ANY, _L("Height Range"), wxDefaultPosition, wxDefaultSize, wxST_ELLIPSIZE_END);
head_text->SetBackgroundStyle(wxBG_STYLE_PAINT);
head_text->SetFont(wxGetApp().normal_font());
auto icon = new wxStaticBitmap(m_og->ctrl_parent(), wxID_ANY, m_bmp_layer.bmp());
title_sizer->Add(icon, 0, wxALIGN_CENTER_VERTICAL);
title_sizer->Add(head_text, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, m_og->ctrl_parent()->FromDIP(5));
m_grid_sizer->Add(title_sizer, 0, wxALIGN_CENTER_VERTICAL);
m_grid_sizer->Add(head_text, 0, wxALIGN_CENTER_VERTICAL);
// Add control for the "Min Z"
@@ -131,7 +126,7 @@ wxSizer* ObjectLayers::create_layer(const t_layer_height_range& range, PlusMinus
m_grid_sizer->Add(editor, 1, wxEXPAND);
auto middle_text = new wxStaticText(m_og->ctrl_parent(), wxID_ANY, "-");
auto middle_text = new wxStaticText(m_og->ctrl_parent(), wxID_ANY, _L("to"), wxDefaultPosition, wxDefaultSize, wxST_ELLIPSIZE_END);
middle_text->SetBackgroundStyle(wxBG_STYLE_PAINT);
middle_text->SetFont(wxGetApp().normal_font());
m_grid_sizer->Add(middle_text, 0, wxALIGN_CENTER_VERTICAL);
@@ -161,6 +156,11 @@ wxSizer* ObjectLayers::create_layer(const t_layer_height_range& range, PlusMinus
m_grid_sizer->Add(editor, 1, wxEXPAND);
auto sizer2 = new wxBoxSizer(wxHORIZONTAL);
auto unit_text = new wxStaticText(m_og->ctrl_parent(), wxID_ANY, _L("mm"), wxDefaultPosition, wxDefaultSize, wxST_ELLIPSIZE_END);
unit_text->SetBackgroundStyle(wxBG_STYLE_PAINT);
unit_text->SetFont(wxGetApp().normal_font());
sizer2->Add(unit_text, 0, wxALIGN_CENTER_VERTICAL);
m_grid_sizer->Add(sizer2, 0, wxALIGN_CENTER_VERTICAL);
// BBS
@@ -205,9 +205,8 @@ void ObjectLayers::create_layers_list()
auto sizer = create_layer(range, del_btn, add_btn);
auto b_sizer = new wxBoxSizer(wxHORIZONTAL);
b_sizer->Add(del_btn, 0, wxLEFT, m_og->ctrl_parent()->FromDIP(5));
b_sizer->AddSpacer(m_og->ctrl_parent()->FromDIP(15));
b_sizer->Add(add_btn, 0, wxRIGHT, m_og->ctrl_parent()->FromDIP(5));
b_sizer->Add(del_btn, 0, wxRIGHT | wxLEFT, em_unit(m_parent));
b_sizer->Add(add_btn);
sizer->Add(b_sizer, 0, wxALIGN_CENTER_HORIZONTAL | wxTOP, m_parent->FromDIP(1)); // aligns +/- buttons vertically since we got 1px gap on bottom of icons
del_btn->Bind(wxEVT_BUTTON, [del_btn](wxEvent &) {
@@ -278,7 +277,6 @@ void ObjectLayers::msw_rescale()
{
m_bmp_delete.msw_rescale();
m_bmp_add.msw_rescale();
m_bmp_layer.msw_rescale();
m_grid_sizer->SetHGap(wxGetApp().em_unit());
@@ -369,17 +367,20 @@ LayerRangeEditor::LayerRangeEditor( ObjectLayers* parent,
m_valid_value(value),
m_type(type),
m_set_focus_data(set_focus_data_fn),
TextInput(parent->m_og->ctrl_parent(), value, _L("mm"), "", wxDefaultPosition, wxSize(em_unit(parent->m_parent), wxDefaultCoord), wxTE_PROCESS_ENTER)
wxTextCtrl(parent->m_og->ctrl_parent(), wxID_ANY, value, wxDefaultPosition,
wxSize(em_unit(parent->m_parent), wxDefaultCoord), wxTE_PROCESS_ENTER
#ifdef _WIN32
| wxBORDER_SIMPLE
#endif
)
{
this->SetFont(wxGetApp().normal_font());
wxGetApp().UpdateDarkUI(this);
wxTextCtrl* ctrl = GetTextCtrl();
// Reset m_enter_pressed flag to _false_, when value is editing
ctrl->Bind(wxEVT_TEXT, [this](wxEvent&) { m_enter_pressed = false; }, ctrl->GetId());
this->Bind(wxEVT_TEXT, [this](wxEvent&) { m_enter_pressed = false; }, this->GetId());
ctrl->Bind(wxEVT_TEXT_ENTER, [this, edit_fn](wxCommandEvent& e)
this->Bind(wxEVT_TEXT_ENTER, [this, edit_fn](wxEvent&)
{
m_enter_pressed = true;
// If LayersList wasn't updated/recreated, we can call wxEVT_KILL_FOCUS.Skip()
@@ -394,9 +395,9 @@ LayerRangeEditor::LayerRangeEditor( ObjectLayers* parent,
SetValue(m_valid_value);
m_call_kill_focus = true;
}
}, ctrl->GetId());
}, this->GetId());
ctrl->Bind(wxEVT_KILL_FOCUS, [this, edit_fn](wxFocusEvent& e)
this->Bind(wxEVT_KILL_FOCUS, [this, edit_fn](wxFocusEvent& e)
{
if (!m_enter_pressed) {
#ifndef __WXGTK__
@@ -425,14 +426,14 @@ LayerRangeEditor::LayerRangeEditor( ObjectLayers* parent,
m_call_kill_focus = false;
e.Skip();
}
}, ctrl->GetId());
}, this->GetId());
ctrl->Bind(wxEVT_SET_FOCUS, [this, parent](wxFocusEvent& e)
this->Bind(wxEVT_SET_FOCUS, [this, parent](wxFocusEvent& e)
{
set_focus_data();
parent->update_scene_from_editor_selection();
e.Skip();
}, ctrl->GetId());
}, this->GetId());
#ifdef __WXGTK__ // Workaround! To take information about selectable range
this->Bind(wxEVT_LEFT_DOWN, [this](wxEvent& e)
@@ -446,7 +447,7 @@ LayerRangeEditor::LayerRangeEditor( ObjectLayers* parent,
{
// select all text using Ctrl+A
if (wxGetKeyState(wxKeyCode('A')) && wxGetKeyState(WXK_CONTROL))
GetTextCtrl()->SetSelection(-1, -1); //select all
this->SetSelection(-1, -1); //select all
event.Skip();
}));
}
@@ -476,7 +477,7 @@ coordf_t LayerRangeEditor::get_value()
void LayerRangeEditor::msw_rescale()
{
Rescale();
SetMinSize(wxSize(wxGetApp().em_unit(), wxDefaultCoord));
}
} //namespace GUI
+1 -6
View File
@@ -9,8 +9,6 @@
#include <wx/sizer.h>
#include <wx/event.h>
#include <slic3r/GUI/Widgets/TextInput.hpp>
#ifdef __WXOSX__
#include "libslic3r/PrintConfig.hpp"
#endif
@@ -36,7 +34,7 @@ enum EditorType
etLayerHeight = 4,
};
class LayerRangeEditor : public TextInput
class LayerRangeEditor : public wxTextCtrl
{
bool m_enter_pressed { false };
bool m_call_kill_focus { false };
@@ -57,8 +55,6 @@ public:
EditorType type() const {return m_type;}
void set_focus_data() const { m_set_focus_data(m_type);}
void SetValue(const wxString& value) {GetTextCtrl()->SetValue(value);}
wxString GetValue() {return GetTextCtrl()->GetValue();}
void msw_rescale();
private:
@@ -69,7 +65,6 @@ class ObjectLayers : public OG_Settings
{
ScalableBitmap m_bmp_delete;
ScalableBitmap m_bmp_add;
ScalableBitmap m_bmp_layer;
ModelObject* m_object {nullptr};
wxFlexGridSizer* m_grid_sizer;
-4
View File
@@ -2317,10 +2317,6 @@ void GLGizmoMeasure::update_measurement_result()
void GLGizmoMeasure::reset_all_pick()
{
std::map<GLVolume*, std::shared_ptr<PickRaycaster>>().swap(m_mesh_raycaster_map);
// register_single_mesh_pick() fills both maps in lockstep, so the measurings have to go with
// the raycasters; otherwise the entries keyed on the GLVolumes of the previous selection stay
// behind for the rest of the session.
std::map<GLVolume*, std::shared_ptr<Measure::Measuring>>().swap(m_mesh_measure_map);
reset_gripper_pick(GripperType::UNDEFINE,true);
}
+3 -4
View File
@@ -37,7 +37,8 @@ enum class PainterGizmoType {
FDM_SUPPORTS,
SEAM,
MM_SEGMENTATION,
FUZZY_SKIN
FUZZY_SKIN,
TEXTURE_DISPLACEMENT
};
class TriangleSelectorGUI : public TriangleSelector {
@@ -111,9 +112,7 @@ class TriangleSelectorPatch : public TriangleSelectorGUI {
public:
explicit TriangleSelectorPatch(const TriangleMesh& mesh, const std::vector<ColorRGBA> ebt_colors, float edge_limit = 0.6f)
: TriangleSelectorGUI(mesh, edge_limit), m_ebt_colors(ebt_colors) {}
// Releases the VAO and the per-patch VBOs built by finalize_triangle_indices(). The base class
// already deletes GL buffers from its GLModel members here, so this needs no context of its own.
virtual ~TriangleSelectorPatch() { release_geometry(); }
virtual ~TriangleSelectorPatch() = default;
// Render current selection. Transformation matrices are supposed
// to be already set.
+4 -44
View File
@@ -385,7 +385,6 @@ ImGuiWrapper::~ImGuiWrapper()
{
//destroy_fonts_texture();
destroy_font();
destroy_svg_textures();
ImGui::DestroyContext();
}
@@ -567,12 +566,6 @@ bool ImGuiWrapper::update_key_data(wxKeyEvent &evt)
return ret;
}
// SVG icons rasterized into GL textures, keyed on file name, size and recolor. Cleared as a whole
// from new_frame() once it grows past MAX_SVG_TEXTURES, which is safe there: the previous frame has
// been rendered and the frame about to be recorded asks for every icon it draws again.
static std::map<std::string, ImTextureID> s_svg_textures;
static const size_t MAX_SVG_TEXTURES = 256;
void ImGuiWrapper::new_frame()
{
if (m_new_frame_open) {
@@ -583,11 +576,6 @@ void ImGuiWrapper::new_frame()
init_font(true);
}
// Recolored icons accumulate one texture per color the session has shown; drop them before
// anything references them again. This frame recreates the handful it actually draws.
if (s_svg_textures.size() > MAX_SVG_TEXTURES)
destroy_svg_textures();
ImGuiIO& io = ImGui::GetIO();
ImGui::NewFrame();
@@ -1840,7 +1828,8 @@ bool menu_item_with_icon(const char *label, const char *shortcut, ImVec2 icon_si
if (icon_color != 0)
ImGui::RenderFrame(icon_pos, icon_pos + icon_size, icon_color);
else {
ImTextureID transparent = ImGuiWrapper::svg_texture(Slic3r::resources_dir() + "/images/transparent.svg", icon_size.x, icon_size.y);
static ImTextureID transparent;
IMTexture::load_from_svg_file(Slic3r::resources_dir() + "/images/transparent.svg", icon_size.x, icon_size.y, transparent);
window->DrawList->AddImage(transparent, icon_pos, icon_pos + icon_size, { 0,0 }, { 1,1 }, ImGui::GetColorU32(ImVec4(1.f, 1.f, 1.f, 1.f)));
}
}
@@ -3420,36 +3409,6 @@ bool ImGuiWrapper::display_initialized() const
return io.DisplaySize.x >= 0.0f && io.DisplaySize.y >= 0.0f;
}
ImTextureID ImGuiWrapper::svg_texture(const std::string& filename, unsigned width, unsigned height, const char* hex_color)
{
std::string key = filename + "|" + std::to_string(width) + "x" + std::to_string(height);
if (hex_color != nullptr)
key += std::string("|") + hex_color;
const auto it = s_svg_textures.find(key);
if (it != s_svg_textures.end())
return it->second;
ImTextureID texture_id = nullptr;
const bool loaded = (hex_color != nullptr) ?
BitmapCache::load_from_svg_file_change_color(filename, width, height, texture_id, hex_color) :
IMTexture::load_from_svg_file(filename, width, height, texture_id);
if (!loaded)
return nullptr;
s_svg_textures.emplace(std::move(key), texture_id);
return texture_id;
}
void ImGuiWrapper::destroy_svg_textures()
{
for (const auto& texture : s_svg_textures) {
GLuint texture_id = (GLuint)(intptr_t)texture.second;
glsafe(::glDeleteTextures(1, &texture_id));
}
s_svg_textures.clear();
}
void ImGuiWrapper::destroy_font()
{
if (m_font_texture != 0) {
@@ -3529,6 +3488,7 @@ void ImGuiWrapper::filament_group(const std::string& filament_type, const char*
//ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0));
std::string id = std::to_string(static_cast<unsigned int> (filament_id + 1));
ImDrawList* draw_list = ImGui::GetWindowDrawList();
static ImTextureID transparent;
ImVec2 text_size = ImGui::CalcTextSize(filament_type.c_str());
// BBS image sizing based on text width (DPI scaling)
float img_width = ImGui::CalcTextSize("ABC").x;
@@ -3541,7 +3501,7 @@ void ImGuiWrapper::filament_group(const std::string& filament_type, const char*
if (rgba[3] == 0x00) {
svg_path = "/images/outlined_rect_transparent.svg";
}
ImTextureID transparent = svg_texture(Slic3r::resources_dir() + svg_path, img_size.x, img_size.y, hex_color);
BitmapCache::load_from_svg_file_change_color(Slic3r::resources_dir() + svg_path, img_size.x, img_size.y, transparent, hex_color);
ImGui::BeginGroup();
{
ImVec2 cursor_pos = ImGui::GetCursorScreenPos();
-8
View File
@@ -108,14 +108,6 @@ public:
// Hash of every draw list's vertices, indices and commands.
static ImGuiID draw_data_signature(const ImDrawData* draw_data);
// A GL texture holding an SVG icon rasterized at width x height, optionally recolored.
// Rasterizing an SVG is far too expensive to redo for every frame that draws the icon, and the
// texture the previous frame generated would leak, so the result is kept until the frame that
// finds the cache overgrown drops it (and rebuilds only what it still draws).
static ImTextureID svg_texture(const std::string& filename, unsigned width, unsigned height, const char* hex_color = nullptr);
// Deletes every texture svg_texture() handed out. Requires a current GL context.
static void destroy_svg_textures();
float scaled(float x) const { return x * m_font_size; }
ImVec2 scaled(float x, float y) const { return ImVec2(x * m_font_size, y * m_font_size); }
/// <summary>
+119 -90
View File
@@ -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)
+8 -1
View File
@@ -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;
+25 -11
View File
@@ -219,26 +219,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;
+5
View File
@@ -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;
};
+10 -10
View File
@@ -15,7 +15,6 @@
#include "slic3r/GUI/StatusPanel.hpp"
#include "slic3r/GUI/UpgradePanel.hpp"
#include "slic3r/GUI/Widgets/SideTools.hpp"
#include "slic3r/GUI/Printer/PrinterFileSystem.h"
#include <wx/app.h>
#include <wx/bookctrl.h>
#include <wx/button.h>
@@ -44,7 +43,7 @@
#include "Plater.hpp"
#include "MainFrame.hpp"
#include "MediaPlayCtrl.h"
#include "MediaFilePanel.h"
#include "StoragePanel.h"
#include "Plater.hpp"
#include "BindDialog.hpp"
@@ -200,9 +199,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())
@@ -216,8 +215,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);
@@ -266,7 +265,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()
@@ -278,7 +278,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();
@@ -425,8 +425,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())) {
+2 -2
View File
@@ -60,7 +60,7 @@
namespace Slic3r {
namespace GUI {
class MediaFilePanel;
class StoragePanel;
class AddMachinePanel : public wxPanel
{
@@ -86,7 +86,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 };
+1 -2
View File
@@ -2250,12 +2250,11 @@ void NotificationManager::close_and_delete_self(PopNotification * self)
}
void NotificationManager::remove_notification_of_type(const NotificationType type) {
// Seven notification types may have several instances alive at once, so erase every match:
// stopping at the first one leaves the rest (and the ObjectIDs they hold) behind.
for (auto it = m_pop_notifications.begin(); it != m_pop_notifications.end();) {
std::unique_ptr<PopNotification> &notification = *it;
if (notification->get_type() == type) {
it = m_pop_notifications.erase(it);
break;
} else
++it;
}
+275
View File
@@ -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);
}
}
}}
+53
View File
@@ -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;
};
}}
+2 -1
View File
@@ -6426,9 +6426,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;
};
}}
+67
View File
@@ -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_PNG);
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;
};
}}
+1 -4
View File
@@ -1816,10 +1816,7 @@ void InputIpAddressDialog::set_machine_obj(MachineObject* obj)
m_input_printer_name->GetTextCtrl()->SetLabelText(m_obj->get_dev_name());
std::string img_str = DevPrinterConfigUtil::get_printer_connect_help_img(m_obj->printer_type);
if (img_str.empty()) { img_str = "input_access_code_x1"; }
std::string language = wxGetApp().app_config->get("language");
auto diagram_bmp = create_scaled_bitmap(img_str + (language == "zh_CN" ? "_cn" : "_en"), this, 198);
auto diagram_bmp = create_scaled_bitmap(img_str + "_en", this, 198);
m_img_help->SetBitmap(diagram_bmp);
+27
View File
@@ -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>
@@ -3566,6 +3568,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";
@@ -3620,6 +3625,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;
+3 -2
View File
@@ -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)
+30
View File
@@ -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"
@@ -759,6 +763,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();
@@ -807,6 +813,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);
}
}
+6 -4
View File
@@ -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));
+112
View File
@@ -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();
}
}}
+35
View File
@@ -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;
};
}}
+6
View File
@@ -1427,6 +1427,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;
+18 -12
View File
@@ -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));
}
+2
View File
@@ -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 &&
-7
View File
@@ -456,13 +456,6 @@ wxBitmap create_scaled_bitmap( const std::string& bmp_name_in,
const vector<std::string>& array_new_color/* = vector<std::string>*/)//used for semi transparent material)
{
static Slic3r::GUI::BitmapCache cache;
// An empty name means the caller's icon lookup failed
if (bmp_name_in.empty() || bmp_name_in == ".png") {
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": empty bitmap name";
return wxNullBitmap;
}
if (bitmap2) {
return create_scaled_bitmap2(bmp_name_in, cache, win, px_cnt, grayscale, resize, array_new_color);
}
+60 -1
View File
@@ -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;
@@ -956,6 +971,50 @@ std::string Http::tls_system_cert_store()
return ret;
}
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();
+6
View File
@@ -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);
+81
View File
@@ -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.
*/
+58
View File
@@ -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
+8
View File
@@ -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 {
+11 -8
View File
@@ -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.
+298 -12
View File
@@ -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();
if (refresh_thread.joinable()) {
refresh_thread.join();
}
@@ -965,22 +978,45 @@ bool OrcaCloudServiceAgent::ensure_token_fresh(const std::string& reason) { retu
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;
}
std::string response;
unsigned int http_code = 0;
int result = http_get(ORCA_HEALTH_PATH, &response, &http_code);
bool connected = (result == BAMBU_NETWORK_SUCCESS && http_code >= 200 && http_code < 300);
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: cloud health result=" << result << " http_code=" << http_code
<< " connected=" << connected << " response_bytes=" << response.size();
// 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.
{
std::lock_guard<std::recursive_mutex> lock(state_mutex);
is_connected = connected;
}
invoke_server_connected_callback(connected ? 0 : -1, http_code);
return connected ? 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;
}
@@ -1001,13 +1037,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;
}
@@ -2042,6 +2303,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{};
@@ -2157,7 +2422,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;
@@ -2181,7 +2450,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) {
@@ -2648,19 +2917,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())
@@ -2674,15 +2952,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;
}
@@ -2767,6 +3050,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)";
+57 -2
View File
@@ -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 {
@@ -156,6 +166,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);
@@ -211,6 +223,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
@@ -246,7 +271,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;
@@ -352,7 +377,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,
@@ -376,7 +423,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();
@@ -434,6 +485,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};
@@ -447,6 +501,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
+837
View File
@@ -0,0 +1,837 @@
#include "OrcaMqttConnection.hpp"
#include "Http.hpp"
#include <atomic>
#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 <deque>
#include <cstdint>
#include <mutex>
#include <cstddef>
#include <boost/beast/websocket/rfc6455.hpp>
#include <boost/beast/websocket/stream_base.hpp>
#include <ios>
#include <exception>
#include <openssl/ssl.h>
#include <algorithm>
#include <chrono>
#include <memory>
#include <openssl/tls1.h>
#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;
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();
}
} // 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 packet = boost::beast::buffers_to_string(conn->read_buffer.data());
conn->read_buffer.consume(conn->read_buffer.size());
handle_packet(packet);
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));
}
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 malformed/not a 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));
}
// 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);
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
+159
View File
@@ -0,0 +1,159 @@
#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);
~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);
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
+255 -4
View File
@@ -2,22 +2,34 @@
#define __ORCA_PRINTER_AGENT_HPP__
#include "IPrinterAgent.hpp"
#include "ICloudServiceAgent.hpp"
#include "OrcaCloudServiceAgent.hpp"
#include "OrcaMqttConnection.hpp"
#include <atomic>
#include <chrono>
#include <cstdint>
#include <functional>
#include "bambu_networking.hpp"
#include <string>
#include <mutex>
#include <memory>
#include <thread>
#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,237 @@ 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; }
// Test-only: shrink the OPCP file-request waits (probe and per-page).
void set_file_opcp_timeouts_for_test(int probe_ms, int request_ms)
{
m_file_probe_timeout_ms.store(probe_ms);
m_file_request_timeout_ms.store(request_ms);
}
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);
static int prepare_outgoing_request(const std::string& dev_id, const std::string& payload,
std::string& command, std::string& prepared);
// 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 OrcaSonar's /server/files/list reply.
// protected static so the test Probe reaches it.
static std::vector<PrinterFileEntry> parse_file_list(const std::string& body);
// Pick the widest thumbnail path from OrcaSonar's /server/files/thumbnails
// reply (the array is smallest-first). Empty when none carry a path.
static std::string parse_thumbnail_path(const std::string& body);
// Pure JSON -> metadata normalization for OrcaSonar's /server/files/metadata
// reply. Missing or malformed fields default to 0. protected static for the Probe.
static PrinterFileMetadata parse_file_metadata(const std::string& body);
// Percent-encode each '/'-separated segment for a Moonraker URL while keeping
// the separators intact. protected static for the test Probe.
static std::string encode_file_path(const std::string& path);
// ========================================================================
// OPCP printer-file transport (Storage tab).
//
// Newer OrcaSonar builds expose files.* over the same MQTT channel the agent
// already owns; older ones only have the Moonraker HTTP façade. The manifest's
// files.list advertisement picks the transport, with a short OPCP probe and an
// HTTP fallback when the capability is not known yet.
// ========================================================================
// opcp: the manifest advertises files.list. http: the manifest is known and
// does not. probe: no manifest yet; try OPCP briefly then fall back to HTTP.
enum class FileTransport { opcp, http, probe };
static FileTransport preferred_file_transport(const std::string& dev_id);
// One files.list reply: the file entries the GUI shows, plus the subdirectory
// paths the walker still has to visit.
struct OpcpFileListPage {
std::vector<PrinterFileEntry> files;
std::vector<std::string> dirs;
bool truncated = false;
bool success = false;
};
static std::string build_files_list_request(const std::string& sequence_id, const std::string& path);
static std::string build_files_metadata_request(const std::string& sequence_id, const std::string& path);
static std::string build_files_delete_request(const std::string& sequence_id, const std::string& path);
static std::string build_files_thumbnail_request(const std::string& sequence_id, const std::string& path);
static OpcpFileListPage parse_opcp_file_list(const std::string& body);
static PrinterFileMetadata parse_opcp_file_metadata(const std::string& body);
static std::string parse_opcp_thumbnail(const std::string& body);
// True when a files.* reply is unusable, so the caller should try HTTP.
static bool file_reply_requires_http_fallback(const std::string& body);
// Consume an inbound files.* reply. Returns true for any files payload, which
// is then not forwarded to the GUI.
bool handle_files_reply(const std::string& dev_id, const std::string& payload);
// Wake every pending file request for a device (disconnect/unbind/forget).
void fail_pending_file_requests(const std::string& dev_id);
// Bounded BFS over OPCP files.list from the gcodes root. Returns false when the
// root listing itself failed, so the caller can fall back to HTTP.
static bool collect_opcp_file_entries(const std::function<int(const std::string&)>& send,
const std::string& dev_id,
std::chrono::milliseconds first_timeout,
std::chrono::milliseconds page_timeout,
std::vector<PrinterFileEntry>& out,
bool& truncated);
// HTTP fallbacks, split out of the public ops. Static so the detached workers
// that call them never capture `this`.
static void list_printer_files_http(const std::string& origin, bool use_ssl, const std::string& ca_file,
QueueOnMainFn queue, PrinterFileListFn callback);
static void get_printer_file_thumbnail_http(const std::string& path, const std::string& origin, bool use_ssl,
const std::string& ca_file, QueueOnMainFn queue, PrinterFileThumbnailFn callback);
static void delete_printer_file_http(const std::string& path, const std::string& origin, bool use_ssl,
const std::string& ca_file, QueueOnMainFn queue, PrinterFileDeleteFn callback);
static void get_printer_file_metadata_http(const std::string& path, const std::string& origin, bool use_ssl,
const std::string& ca_file, QueueOnMainFn queue, PrinterFileMetadataFn callback);
// 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;
// shared_ptr so a detached file worker can hold the connection across a
// disconnect without a use-after-free; the worker never captures `this`.
std::shared_ptr<OrcaMqttConnection> lan_mqtt_connection;
// OPCP file-request waits, in milliseconds. Probe is short so an older build
// fails over to HTTP quickly; the per-page wait covers a real transfer.
std::atomic<int> m_file_probe_timeout_ms{3000};
std::atomic<int> m_file_request_timeout_ms{15000};
// 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
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);
// Core of route_send() on an already-resolved connection. Static so detached
// file workers (which must not capture `this`) send through the same path.
static int route_send_via(bool is_lan, OrcaMqttConnection* conn, const std::string& dev_id, const std::string& json_str);
// Callbacks
OnMsgArrivedFn on_ssdp_msg_fn;

Some files were not shown because too many files have changed in this diff Show More