Compare commits

...
21 Commits
Author SHA1 Message Date
Lam Wei Lun f42173a19f clang-tidy fixes 2026-10-08 11:51:42 +08:00
Lam Wei Lun b99a9a06f3 OrcaPrinterAgent: Thumbnails over http 2026-10-08 11:18:24 +08:00
Lam Wei Lun faa64ab089 Merge main branch 2026-10-08 11:12:25 +08:00
Ian Chua 987b1b2b63 OrcaPrinterAgent - Files over MQTT (#16251)
- Add packet draining to OrcaMqttConnection
- Change files list/delete to use the Mqtt version instead of http
2026-10-07 19:20:35 +08:00
Lam Wei Lun 9bd4c71a19 List OrcaSonar printer files over LAN MQTT
The Storage tab's list/metadata/delete went over OrcaSonar's Moonraker
façade, so they failed when the façade was disabled. Send the files.* MQTT
commands on the LAN connection instead - the same transport OrcaCloud uses -
and match replies by sequence_id.

Thumbnails still use the façade (no MQTT equivalent); gcode upload is
unchanged.
2026-10-07 18:35:12 +08:00
Lam Wei Lun 3c646f305c Fix Orca MQTT: reassemble packets across WebSocket messages
OrcaSonar's /mqtt WebSocket proxy writes each ~8 KiB TCP read as its own
WebSocket binary message, so an MQTT packet larger than one read (e.g. a
files.list reply) spans several messages, and several packets can arrive in
one. OrcaMqttConnection assumed exactly one packet per message, so packets
that spanned messages were dropped and the request timed out ("Load failed").

Keep a per-connection byte buffer and extract complete packets by their
remaining-length header, dispatching each. A malformed header (or a declared
size past a 16 MiB cap) drops the connection so the worker reconnects rather
than buffering forever.
2026-10-07 18:35:12 +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
90 changed files with 13878 additions and 5307 deletions
+7
View File
@@ -131,6 +131,13 @@
<true/> <true/>
<key>NSHumanReadableCopyright</key> <key>NSHumanReadableCopyright</key>
<string>${MACOSX_BUNDLE_COPYRIGHT}</string> <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> <key>NSAppTransportSecurity</key>
<dict> <dict>
<!-- Disable App Transport Security. Resolves https://github.com/OrcaSlicer/OrcaSlicer/issues/791 --> <!-- Disable App Transport Security. Resolves https://github.com/OrcaSlicer/OrcaSlicer/issues/791 -->
+30 -161
View File
@@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-06 15:16-0300\n" "POT-Creation-Date: 2026-10-08 11:08+0800\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@@ -4046,6 +4046,9 @@ msgstr ""
msgid "You need to select the material type and color first." msgid "You need to select the material type and color first."
msgstr "" msgstr ""
msgid "The minimum temperature cannot be greater than the maximum temperature."
msgstr ""
#, possible-c-format, possible-boost-format #, possible-c-format, possible-boost-format
msgid "Please input a valid value (K in %.1f~%.1f)" msgid "Please input a valid value (K in %.1f~%.1f)"
msgstr "" msgstr ""
@@ -5779,7 +5782,7 @@ msgstr ""
msgid "Size:" msgid "Size:"
msgstr "" 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)." msgid "Conflicts of G-code paths have been found at layer %d, Z = %.2lfmm. Please separate the conflicted objects farther (%s <-> %s)."
msgstr "" msgstr ""
@@ -6618,6 +6621,22 @@ msgstr ""
msgid "File '%s' was lost! Please download it again." msgid "File '%s' was lost! Please download it again."
msgstr "" 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 #, possible-c-format, possible-boost-format
msgid "" msgid ""
"File: %s\n" "File: %s\n"
@@ -10188,6 +10207,12 @@ msgstr ""
msgid "Clean Up" msgid "Clean Up"
msgstr "" 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" msgid "Preparing print job"
msgstr "" msgstr ""
@@ -23594,165 +23619,6 @@ msgstr ""
msgid "Size: %.1f × %.1f mm" msgid "Size: %.1f × %.1f mm"
msgstr "" msgstr ""
msgid "Primitive"
msgstr ""
msgid "Empty mesh generated"
msgstr ""
#. TRN Default name of an object created from the box primitive shape.
msgid "Box"
msgstr ""
#. TRN Primitive gizmo: label in front of the preset cube size buttons
msgid "Quick:"
msgstr ""
msgid "Width (X)"
msgstr ""
msgid "Depth (Y)"
msgstr ""
msgid "Height (Z)"
msgstr ""
#. TRN Primitive gizmo: bottom radius of a cone ("Top R" is the top radius)
msgid "Bottom R"
msgstr ""
msgid "Top R"
msgstr ""
#. TRN Primitive gizmo: major radius of a torus ("Minor R" is the tube radius)
msgid "Major R"
msgstr ""
msgid "Minor R"
msgstr ""
msgid "All edges"
msgstr ""
msgid "Top edges"
msgstr ""
msgid "Bottom edges"
msgstr ""
msgid "Lateral edges"
msgstr ""
msgid "Mesh resolution"
msgstr ""
msgid "Add Shape"
msgstr ""
msgid "No outer profile"
msgstr ""
msgid "Failed to make outer face"
msgstr ""
msgid "Failed to build face with holes"
msgstr ""
msgid "Revolve failed"
msgstr ""
msgid "Profile"
msgstr ""
msgid "Start new profile (for holes)"
msgstr ""
msgid "Segments"
msgstr ""
msgid "Sides"
msgstr ""
msgid "Click on canvas to draw"
msgstr ""
msgid "Snap to grid"
msgstr ""
msgid "Grid step"
msgstr ""
msgid "Outer"
msgstr ""
msgid "closed"
msgstr ""
#. TRN Sketch gizmo profile list: "Outer" or "Hole", profile number, point count, "closed" or nothing
#, possible-boost-format
msgid "%1% %2%: %3% points %4%"
msgstr ""
msgid "XY (Top)"
msgstr ""
msgid "XZ (Front)"
msgstr ""
msgid "YZ (Side)"
msgstr ""
msgid "Revolve deg"
msgstr ""
msgid "Set to 0 for extrude, >0 for revolve"
msgstr ""
msgid "Pocket (cut)"
msgstr ""
#, possible-c-format
msgid "%zu closed profile"
msgid_plural "%zu closed profiles"
msgstr[0] ""
msgstr[1] ""
msgid "Draw a closed profile to enable"
msgstr ""
msgid "Clear all"
msgstr ""
msgid "Empty result"
msgstr ""
#, possible-boost-format
msgid "Extrude: %1%"
msgstr ""
#, possible-boost-format
msgid "Revolve: %1%"
msgstr ""
msgid "No object selected"
msgstr ""
msgid "Face failed"
msgstr ""
msgid "Face with holes failed"
msgstr ""
msgid "Tool mesh empty"
msgstr ""
msgid "Pocket added (negative volume)"
msgstr ""
#, possible-boost-format
msgid "Pocket: %1%"
msgstr ""
msgctxt "Keyboard Shortcut" msgctxt "Keyboard Shortcut"
msgid "Ins" msgid "Ins"
msgstr "" msgstr ""
@@ -26340,6 +26206,9 @@ msgstr ""
msgid "An error occurred loading %s" msgid "An error occurred loading %s"
msgstr "" msgstr ""
msgid "/"
msgstr ""
#, possible-boost-format #, possible-boost-format
msgid "Load %1% to %2%" msgid "Load %1% to %2%"
msgstr "" msgstr ""
+233 -213
View File
@@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: \n" "Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-06 15:16-0300\n" "POT-Creation-Date: 2026-10-08 11:08+0800\n"
"PO-Revision-Date: 2025-03-15 10:55+0100\n" "PO-Revision-Date: 2025-03-15 10:55+0100\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: \n" "Language-Team: \n"
@@ -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." msgid "You need to select the material type and color first."
msgstr "Primer heu de seleccionar el tipus de material i el color." 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 #, c-format, boost-format
msgid "Please input a valid value (K in %.1f~%.1f)" msgid "Please input a valid value (K in %.1f~%.1f)"
msgstr "Introduïu un valor vàlid (K a %.1f~%.1f)" msgstr "Introduïu un valor vàlid (K a %.1f~%.1f)"
@@ -6297,7 +6300,7 @@ msgid "Size:"
msgstr "Mida:" msgstr "Mida:"
# AI Translated # 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)." 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 )." 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 )."
@@ -7177,6 +7180,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." msgid "File '%s' was lost! Please download it again."
msgstr "L'arxiu '%s' s'ha perdut! Si us plau, descarregueu-lo de nou." 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 #, c-format, boost-format
msgid "" msgid ""
"File: %s\n" "File: %s\n"
@@ -11265,6 +11284,12 @@ msgstr "Cancel·lar el timelapse i imprimir"
msgid "Clean Up" msgid "Clean Up"
msgstr "Netejar" 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" msgid "Preparing print job"
msgstr "Preparant el treball d'impressió" msgstr "Preparant el treball d'impressió"
@@ -27127,214 +27152,6 @@ msgstr "Aquest tipus de lletra no té contorn per a aquest text"
msgid "Size: %.1f × %.1f mm" msgid "Size: %.1f × %.1f mm"
msgstr "Mida: %.1f × %.1f mm" msgstr "Mida: %.1f × %.1f mm"
# AI Translated
msgid "Primitive"
msgstr "Primitiva"
# AI Translated
msgid "Empty mesh generated"
msgstr "S'ha generat una malla buida"
# AI Translated
#. TRN Default name of an object created from the box primitive shape.
msgid "Box"
msgstr "Caixa"
# AI Translated
#. TRN Primitive gizmo: label in front of the preset cube size buttons
msgid "Quick:"
msgstr "Ràpid:"
# AI Translated
msgid "Width (X)"
msgstr "Amplada (X)"
# AI Translated
msgid "Depth (Y)"
msgstr "Profunditat (Y)"
# AI Translated
msgid "Height (Z)"
msgstr "Alçada (Z)"
# AI Translated
#. TRN Primitive gizmo: bottom radius of a cone ("Top R" is the top radius)
msgid "Bottom R"
msgstr "R inferior"
# AI Translated
msgid "Top R"
msgstr "R superior"
# AI Translated
#. TRN Primitive gizmo: major radius of a torus ("Minor R" is the tube radius)
msgid "Major R"
msgstr "R major"
# AI Translated
msgid "Minor R"
msgstr "R menor"
# AI Translated
msgid "All edges"
msgstr "Totes les arestes"
# AI Translated
msgid "Top edges"
msgstr "Arestes superiors"
# AI Translated
msgid "Bottom edges"
msgstr "Arestes inferiors"
# AI Translated
msgid "Lateral edges"
msgstr "Arestes laterals"
# AI Translated
msgid "Mesh resolution"
msgstr "Resolució de la malla"
# AI Translated
msgid "Add Shape"
msgstr "Afegir forma"
# AI Translated
msgid "No outer profile"
msgstr "No hi ha cap perfil exterior"
# AI Translated
msgid "Failed to make outer face"
msgstr "No s'ha pogut crear la cara exterior"
# AI Translated
msgid "Failed to build face with holes"
msgstr "No s'ha pogut construir la cara amb forats"
# AI Translated
msgid "Revolve failed"
msgstr "La revolució ha fallat"
# AI Translated
msgid "Profile"
msgstr "Perfil"
# AI Translated
msgid "Start new profile (for holes)"
msgstr "Començar un perfil nou (per a forats)"
# AI Translated
msgid "Segments"
msgstr "Segments"
# AI Translated
msgid "Sides"
msgstr "Costats"
# AI Translated
msgid "Click on canvas to draw"
msgstr "Feu clic al llenç per dibuixar"
# AI Translated
msgid "Snap to grid"
msgstr "Ajustar a la graella"
# AI Translated
msgid "Grid step"
msgstr "Pas de la graella"
# AI Translated
msgid "Outer"
msgstr "Exterior"
# AI Translated
msgid "closed"
msgstr "tancat"
# AI Translated
#. TRN Sketch gizmo profile list: "Outer" or "Hole", profile number, point count, "closed" or nothing
#, boost-format
msgid "%1% %2%: %3% points %4%"
msgstr "%1% %2%: %3% punts %4%"
# AI Translated
msgid "XY (Top)"
msgstr "XY (Superior)"
# AI Translated
msgid "XZ (Front)"
msgstr "XZ (Frontal)"
# AI Translated
msgid "YZ (Side)"
msgstr "YZ (Lateral)"
# AI Translated
msgid "Revolve deg"
msgstr "Graus de revolució"
# AI Translated
msgid "Set to 0 for extrude, >0 for revolve"
msgstr "Establiu 0 per a extrusió, >0 per a revolució"
# AI Translated
msgid "Pocket (cut)"
msgstr "Cavitat (tall)"
# AI Translated
#, c-format
msgid "%zu closed profile"
msgid_plural "%zu closed profiles"
msgstr[0] "%zu perfil tancat"
msgstr[1] "%zu perfils tancats"
# AI Translated
msgid "Draw a closed profile to enable"
msgstr "Dibuixeu un perfil tancat per activar-ho"
# AI Translated
msgid "Clear all"
msgstr "Esborrar-ho tot"
# AI Translated
msgid "Empty result"
msgstr "Resultat buit"
# AI Translated
#, boost-format
msgid "Extrude: %1%"
msgstr "Extrusió: %1%"
# AI Translated
#, boost-format
msgid "Revolve: %1%"
msgstr "Revolució: %1%"
# AI Translated
msgid "No object selected"
msgstr "No hi ha cap objecte seleccionat"
# AI Translated
msgid "Face failed"
msgstr "No s'ha pogut crear la cara"
# AI Translated
msgid "Face with holes failed"
msgstr "No s'ha pogut crear la cara amb forats"
# AI Translated
msgid "Tool mesh empty"
msgstr "La malla de l'eina és buida"
# AI Translated
msgid "Pocket added (negative volume)"
msgstr "Cavitat afegida (volum negatiu)"
# AI Translated
#, boost-format
msgid "Pocket: %1%"
msgstr "Cavitat: %1%"
# AI Translated # AI Translated
msgctxt "Keyboard Shortcut" msgctxt "Keyboard Shortcut"
msgid "Ins" msgid "Ins"
@@ -30727,6 +30544,9 @@ msgstr "Activar les eines de desenvolupament"
msgid "An error occurred loading %s" msgid "An error occurred loading %s"
msgstr "S'ha produït un error en carregar %s" msgstr "S'ha produït un error en carregar %s"
msgid "/"
msgstr "/"
# AI Translated # AI Translated
#, boost-format #, boost-format
msgid "Load %1% to %2%" msgid "Load %1% to %2%"
@@ -31407,6 +31227,209 @@ msgstr ""
"Evitar la deformació( warping )\n" "Evitar la deformació( warping )\n"
"Sabíeu que quan imprimiu materials propensos a deformar-se, com ara l'ABS, augmentar adequadament la temperatura del llit pot reduir la probabilitat de deformació?" "Sabíeu que quan imprimiu materials propensos a deformar-se, com ara l'ABS, augmentar adequadament la temperatura del llit pot reduir la probabilitat de deformació?"
# AI Translated
#~ msgid "Primitive"
#~ msgstr "Primitiva"
# AI Translated
#~ msgid "Empty mesh generated"
#~ msgstr "S'ha generat una malla buida"
# AI Translated
#~ msgid "Box"
#~ msgstr "Caixa"
# AI Translated
#~ msgid "Quick:"
#~ msgstr "Ràpid:"
# AI Translated
#~ msgid "Width (X)"
#~ msgstr "Amplada (X)"
# AI Translated
#~ msgid "Depth (Y)"
#~ msgstr "Profunditat (Y)"
# AI Translated
#~ msgid "Height (Z)"
#~ msgstr "Alçada (Z)"
# AI Translated
#~ msgid "Bottom R"
#~ msgstr "R inferior"
# AI Translated
#~ msgid "Top R"
#~ msgstr "R superior"
# AI Translated
#~ msgid "Major R"
#~ msgstr "R major"
# AI Translated
#~ msgid "Minor R"
#~ msgstr "R menor"
# AI Translated
#~ msgid "All edges"
#~ msgstr "Totes les arestes"
# AI Translated
#~ msgid "Top edges"
#~ msgstr "Arestes superiors"
# AI Translated
#~ msgid "Bottom edges"
#~ msgstr "Arestes inferiors"
# AI Translated
#~ msgid "Lateral edges"
#~ msgstr "Arestes laterals"
# AI Translated
#~ msgid "Mesh resolution"
#~ msgstr "Resolució de la malla"
# AI Translated
#~ msgid "Add Shape"
#~ msgstr "Afegir forma"
# AI Translated
#~ msgid "No outer profile"
#~ msgstr "No hi ha cap perfil exterior"
# AI Translated
#~ msgid "Failed to make outer face"
#~ msgstr "No s'ha pogut crear la cara exterior"
# AI Translated
#~ msgid "Failed to build face with holes"
#~ msgstr "No s'ha pogut construir la cara amb forats"
# AI Translated
#~ msgid "Revolve failed"
#~ msgstr "La revolució ha fallat"
# AI Translated
#~ msgid "Profile"
#~ msgstr "Perfil"
# AI Translated
#~ msgid "Start new profile (for holes)"
#~ msgstr "Començar un perfil nou (per a forats)"
# AI Translated
#~ msgid "Segments"
#~ msgstr "Segments"
# AI Translated
#~ msgid "Sides"
#~ msgstr "Costats"
# AI Translated
#~ msgid "Click on canvas to draw"
#~ msgstr "Feu clic al llenç per dibuixar"
# AI Translated
#~ msgid "Snap to grid"
#~ msgstr "Ajustar a la graella"
# AI Translated
#~ msgid "Grid step"
#~ msgstr "Pas de la graella"
# AI Translated
#~ msgid "Outer"
#~ msgstr "Exterior"
# AI Translated
#~ msgid "closed"
#~ msgstr "tancat"
# AI Translated
#, boost-format
#~ msgid "%1% %2%: %3% points %4%"
#~ msgstr "%1% %2%: %3% punts %4%"
# AI Translated
#~ msgid "XY (Top)"
#~ msgstr "XY (Superior)"
# AI Translated
#~ msgid "XZ (Front)"
#~ msgstr "XZ (Frontal)"
# AI Translated
#~ msgid "YZ (Side)"
#~ msgstr "YZ (Lateral)"
# AI Translated
#~ msgid "Revolve deg"
#~ msgstr "Graus de revolució"
# AI Translated
#~ msgid "Set to 0 for extrude, >0 for revolve"
#~ msgstr "Establiu 0 per a extrusió, >0 per a revolució"
# AI Translated
#~ msgid "Pocket (cut)"
#~ msgstr "Cavitat (tall)"
# AI Translated
#, c-format
#~ msgid "%zu closed profile"
#~ msgid_plural "%zu closed profiles"
#~ msgstr[0] "%zu perfil tancat"
#~ msgstr[1] "%zu perfils tancats"
# AI Translated
#~ msgid "Draw a closed profile to enable"
#~ msgstr "Dibuixeu un perfil tancat per activar-ho"
# AI Translated
#~ msgid "Clear all"
#~ msgstr "Esborrar-ho tot"
# AI Translated
#~ msgid "Empty result"
#~ msgstr "Resultat buit"
# AI Translated
#, boost-format
#~ msgid "Extrude: %1%"
#~ msgstr "Extrusió: %1%"
# AI Translated
#, boost-format
#~ msgid "Revolve: %1%"
#~ msgstr "Revolució: %1%"
# AI Translated
#~ msgid "No object selected"
#~ msgstr "No hi ha cap objecte seleccionat"
# AI Translated
#~ msgid "Face failed"
#~ msgstr "No s'ha pogut crear la cara"
# AI Translated
#~ msgid "Face with holes failed"
#~ msgstr "No s'ha pogut crear la cara amb forats"
# AI Translated
#~ msgid "Tool mesh empty"
#~ msgstr "La malla de l'eina és buida"
# AI Translated
#~ msgid "Pocket added (negative volume)"
#~ msgstr "Cavitat afegida (volum negatiu)"
# AI Translated
#, boost-format
#~ msgid "Pocket: %1%"
#~ msgstr "Cavitat: %1%"
# AI Translated # AI Translated
#~ msgid "(Experimental) Use printer agents instead of print hosts" #~ msgid "(Experimental) Use printer agents instead of print hosts"
#~ msgstr "(Experimental) Utilitza agents d'impressora en lloc d'amfitrions d'impressió" #~ msgstr "(Experimental) Utilitza agents d'impressora en lloc d'amfitrions d'impressió"
@@ -32463,9 +32486,6 @@ msgstr ""
#~ msgid "An error occurred loading " #~ msgid "An error occurred loading "
#~ msgstr "S'ha produït un error en carregar " #~ msgstr "S'ha produït un error en carregar "
#~ msgid "/"
#~ msgstr "/"
# AI Translated # AI Translated
#, c-format, boost-format #, c-format, boost-format
#~ msgid "Load %s to " #~ msgid "Load %s to "
+235 -215
View File
@@ -3,7 +3,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: Orca Slicer\n" "Project-Id-Version: Orca Slicer\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-06 15:16-0300\n" "POT-Creation-Date: 2026-10-08 11:08+0800\n"
"PO-Revision-Date: \n" "PO-Revision-Date: \n"
"Last-Translator: Jakub Hencl\n" "Last-Translator: Jakub Hencl\n"
"Language-Team: \n" "Language-Team: \n"
@@ -4384,6 +4384,9 @@ msgstr "Opravdu chcete vymazat informace o filamentu?"
msgid "You need to select the material type and color first." msgid "You need to select the material type and color first."
msgstr "Nejprve musíte vybrat typ materiálu a barvu." 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 #, c-format, boost-format
msgid "Please input a valid value (K in %.1f~%.1f)" msgid "Please input a valid value (K in %.1f~%.1f)"
msgstr "Zadejte platnou hodnotu (K v rozsahu %.1f~%.1f)" msgstr "Zadejte platnou hodnotu (K v rozsahu %.1f~%.1f)"
@@ -6266,7 +6269,7 @@ msgid "Size:"
msgstr "Velikost:" msgstr "Velikost:"
# AI Translated # 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)." 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)." 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)."
@@ -7156,6 +7159,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." msgid "File '%s' was lost! Please download it again."
msgstr "Soubor '%s' byl ztracen! Stáhněte jej prosím znovu." 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 #, c-format, boost-format
msgid "" msgid ""
"File: %s\n" "File: %s\n"
@@ -11229,6 +11248,12 @@ msgstr "Zrušit časosběr a tisknout"
msgid "Clean Up" msgid "Clean Up"
msgstr "Vyčistit" 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" msgid "Preparing print job"
msgstr "Příprava tiskové úlohy" msgstr "Příprava tiskové úlohy"
@@ -27135,215 +27160,6 @@ msgstr "Toto písmo nemá pro tento text žádný obrys"
msgid "Size: %.1f × %.1f mm" msgid "Size: %.1f × %.1f mm"
msgstr "Velikost: %.1f × %.1f mm" msgstr "Velikost: %.1f × %.1f mm"
# AI Translated
msgid "Primitive"
msgstr "Primitiv"
# AI Translated
msgid "Empty mesh generated"
msgstr "Byla vygenerována prázdná síť"
# AI Translated
#. TRN Default name of an object created from the box primitive shape.
msgid "Box"
msgstr "Kvádr"
# AI Translated
#. TRN Primitive gizmo: label in front of the preset cube size buttons
msgid "Quick:"
msgstr "Rychle:"
# AI Translated
msgid "Width (X)"
msgstr "Šířka (X)"
# AI Translated
msgid "Depth (Y)"
msgstr "Hloubka (Y)"
# AI Translated
msgid "Height (Z)"
msgstr "Výška (Z)"
# AI Translated
#. TRN Primitive gizmo: bottom radius of a cone ("Top R" is the top radius)
msgid "Bottom R"
msgstr "Spodní R"
# AI Translated
msgid "Top R"
msgstr "Horní R"
# AI Translated
#. TRN Primitive gizmo: major radius of a torus ("Minor R" is the tube radius)
msgid "Major R"
msgstr "Hlavní R"
# AI Translated
msgid "Minor R"
msgstr "Vedlejší R"
# AI Translated
msgid "All edges"
msgstr "Všechny hrany"
# AI Translated
msgid "Top edges"
msgstr "Horní hrany"
# AI Translated
msgid "Bottom edges"
msgstr "Spodní hrany"
# AI Translated
msgid "Lateral edges"
msgstr "Boční hrany"
# AI Translated
msgid "Mesh resolution"
msgstr "Rozlišení sítě"
# AI Translated
msgid "Add Shape"
msgstr "Přidat tvar"
# AI Translated
msgid "No outer profile"
msgstr "Chybí vnější profil"
# AI Translated
msgid "Failed to make outer face"
msgstr "Nepodařilo se vytvořit vnější plochu"
# AI Translated
msgid "Failed to build face with holes"
msgstr "Nepodařilo se vytvořit plochu s otvory"
# AI Translated
msgid "Revolve failed"
msgstr "Rotace selhala"
# AI Translated
msgid "Profile"
msgstr "Profil"
# AI Translated
msgid "Start new profile (for holes)"
msgstr "Začít nový profil (pro otvory)"
# AI Translated
msgid "Segments"
msgstr "Segmenty"
# AI Translated
msgid "Sides"
msgstr "Strany"
# AI Translated
msgid "Click on canvas to draw"
msgstr "Kreslete kliknutím na plátno"
# AI Translated
msgid "Snap to grid"
msgstr "Přichytit k mřížce"
# AI Translated
msgid "Grid step"
msgstr "Krok mřížky"
# AI Translated
msgid "Outer"
msgstr "Vnější"
# AI Translated
msgid "closed"
msgstr "uzavřený"
# AI Translated
#. TRN Sketch gizmo profile list: "Outer" or "Hole", profile number, point count, "closed" or nothing
#, boost-format
msgid "%1% %2%: %3% points %4%"
msgstr "%1% %2%: %3% b. %4%"
# AI Translated
msgid "XY (Top)"
msgstr "XY (horní)"
# AI Translated
msgid "XZ (Front)"
msgstr "XZ (přední)"
# AI Translated
msgid "YZ (Side)"
msgstr "YZ (boční)"
# AI Translated
msgid "Revolve deg"
msgstr "Rotace (°)"
# AI Translated
msgid "Set to 0 for extrude, >0 for revolve"
msgstr "0 = vysunutí, >0 = rotace"
# AI Translated
msgid "Pocket (cut)"
msgstr "Kapsa (řez)"
# AI Translated
#, c-format
msgid "%zu closed profile"
msgid_plural "%zu closed profiles"
msgstr[0] "%zu uzavřený profil"
msgstr[1] "%zu uzavřené profily"
msgstr[2] "%zu uzavřených profilů"
# AI Translated
msgid "Draw a closed profile to enable"
msgstr "Pro aktivaci nakreslete uzavřený profil"
# AI Translated
msgid "Clear all"
msgstr "Vymazat vše"
# AI Translated
msgid "Empty result"
msgstr "Prázdný výsledek"
# AI Translated
#, boost-format
msgid "Extrude: %1%"
msgstr "Vysunutí: %1%"
# AI Translated
#, boost-format
msgid "Revolve: %1%"
msgstr "Rotace: %1%"
# AI Translated
msgid "No object selected"
msgstr "Není vybrán žádný objekt"
# AI Translated
msgid "Face failed"
msgstr "Vytvoření plochy selhalo"
# AI Translated
msgid "Face with holes failed"
msgstr "Vytvoření plochy s otvory selhalo"
# AI Translated
msgid "Tool mesh empty"
msgstr "Síť nástroje je prázdná"
# AI Translated
msgid "Pocket added (negative volume)"
msgstr "Kapsa přidána (záporný objem)"
# AI Translated
#, boost-format
msgid "Pocket: %1%"
msgstr "Kapsa: %1%"
# AI Translated # AI Translated
msgctxt "Keyboard Shortcut" msgctxt "Keyboard Shortcut"
msgid "Ins" msgid "Ins"
@@ -30737,6 +30553,10 @@ msgstr "Povolit vývojářské nástroje"
msgid "An error occurred loading %s" msgid "An error occurred loading %s"
msgstr "Při načítání %s došlo k chybě" msgstr "Při načítání %s došlo k chybě"
# AI Translated
msgid "/"
msgstr "/"
# AI Translated # AI Translated
#, boost-format #, boost-format
msgid "Load %1% to %2%" msgid "Load %1% to %2%"
@@ -31418,6 +31238,210 @@ msgstr ""
"Zamezte kroucení\n" "Zamezte kroucení\n"
"Víte, že při tisku materiálů náchylných ke kroucení, jako je ABS, může vhodné zvýšení teploty vyhřívané desky snížit pravděpodobnost kroucení?" "Víte, že při tisku materiálů náchylných ke kroucení, jako je ABS, může vhodné zvýšení teploty vyhřívané desky snížit pravděpodobnost kroucení?"
# AI Translated
#~ msgid "Primitive"
#~ msgstr "Primitiv"
# AI Translated
#~ msgid "Empty mesh generated"
#~ msgstr "Byla vygenerována prázdná síť"
# AI Translated
#~ msgid "Box"
#~ msgstr "Kvádr"
# AI Translated
#~ msgid "Quick:"
#~ msgstr "Rychle:"
# AI Translated
#~ msgid "Width (X)"
#~ msgstr "Šířka (X)"
# AI Translated
#~ msgid "Depth (Y)"
#~ msgstr "Hloubka (Y)"
# AI Translated
#~ msgid "Height (Z)"
#~ msgstr "Výška (Z)"
# AI Translated
#~ msgid "Bottom R"
#~ msgstr "Spodní R"
# AI Translated
#~ msgid "Top R"
#~ msgstr "Horní R"
# AI Translated
#~ msgid "Major R"
#~ msgstr "Hlavní R"
# AI Translated
#~ msgid "Minor R"
#~ msgstr "Vedlejší R"
# AI Translated
#~ msgid "All edges"
#~ msgstr "Všechny hrany"
# AI Translated
#~ msgid "Top edges"
#~ msgstr "Horní hrany"
# AI Translated
#~ msgid "Bottom edges"
#~ msgstr "Spodní hrany"
# AI Translated
#~ msgid "Lateral edges"
#~ msgstr "Boční hrany"
# AI Translated
#~ msgid "Mesh resolution"
#~ msgstr "Rozlišení sítě"
# AI Translated
#~ msgid "Add Shape"
#~ msgstr "Přidat tvar"
# AI Translated
#~ msgid "No outer profile"
#~ msgstr "Chybí vnější profil"
# AI Translated
#~ msgid "Failed to make outer face"
#~ msgstr "Nepodařilo se vytvořit vnější plochu"
# AI Translated
#~ msgid "Failed to build face with holes"
#~ msgstr "Nepodařilo se vytvořit plochu s otvory"
# AI Translated
#~ msgid "Revolve failed"
#~ msgstr "Rotace selhala"
# AI Translated
#~ msgid "Profile"
#~ msgstr "Profil"
# AI Translated
#~ msgid "Start new profile (for holes)"
#~ msgstr "Začít nový profil (pro otvory)"
# AI Translated
#~ msgid "Segments"
#~ msgstr "Segmenty"
# AI Translated
#~ msgid "Sides"
#~ msgstr "Strany"
# AI Translated
#~ msgid "Click on canvas to draw"
#~ msgstr "Kreslete kliknutím na plátno"
# AI Translated
#~ msgid "Snap to grid"
#~ msgstr "Přichytit k mřížce"
# AI Translated
#~ msgid "Grid step"
#~ msgstr "Krok mřížky"
# AI Translated
#~ msgid "Outer"
#~ msgstr "Vnější"
# AI Translated
#~ msgid "closed"
#~ msgstr "uzavřený"
# AI Translated
#, boost-format
#~ msgid "%1% %2%: %3% points %4%"
#~ msgstr "%1% %2%: %3% b. %4%"
# AI Translated
#~ msgid "XY (Top)"
#~ msgstr "XY (horní)"
# AI Translated
#~ msgid "XZ (Front)"
#~ msgstr "XZ (přední)"
# AI Translated
#~ msgid "YZ (Side)"
#~ msgstr "YZ (boční)"
# AI Translated
#~ msgid "Revolve deg"
#~ msgstr "Rotace (°)"
# AI Translated
#~ msgid "Set to 0 for extrude, >0 for revolve"
#~ msgstr "0 = vysunutí, >0 = rotace"
# AI Translated
#~ msgid "Pocket (cut)"
#~ msgstr "Kapsa (řez)"
# AI Translated
#, c-format
#~ msgid "%zu closed profile"
#~ msgid_plural "%zu closed profiles"
#~ msgstr[0] "%zu uzavřený profil"
#~ msgstr[1] "%zu uzavřené profily"
#~ msgstr[2] "%zu uzavřených profilů"
# AI Translated
#~ msgid "Draw a closed profile to enable"
#~ msgstr "Pro aktivaci nakreslete uzavřený profil"
# AI Translated
#~ msgid "Clear all"
#~ msgstr "Vymazat vše"
# AI Translated
#~ msgid "Empty result"
#~ msgstr "Prázdný výsledek"
# AI Translated
#, boost-format
#~ msgid "Extrude: %1%"
#~ msgstr "Vysunutí: %1%"
# AI Translated
#, boost-format
#~ msgid "Revolve: %1%"
#~ msgstr "Rotace: %1%"
# AI Translated
#~ msgid "No object selected"
#~ msgstr "Není vybrán žádný objekt"
# AI Translated
#~ msgid "Face failed"
#~ msgstr "Vytvoření plochy selhalo"
# AI Translated
#~ msgid "Face with holes failed"
#~ msgstr "Vytvoření plochy s otvory selhalo"
# AI Translated
#~ msgid "Tool mesh empty"
#~ msgstr "Síť nástroje je prázdná"
# AI Translated
#~ msgid "Pocket added (negative volume)"
#~ msgstr "Kapsa přidána (záporný objem)"
# AI Translated
#, boost-format
#~ msgid "Pocket: %1%"
#~ msgstr "Kapsa: %1%"
# AI Translated # AI Translated
#~ msgid "(Experimental) Use printer agents instead of print hosts" #~ msgid "(Experimental) Use printer agents instead of print hosts"
#~ msgstr "(Experimentální) Používat agenty tiskárny místo tiskových hostů" #~ msgstr "(Experimentální) Používat agenty tiskárny místo tiskových hostů"
@@ -32479,10 +32503,6 @@ msgstr ""
#~ msgid "An error occurred loading " #~ msgid "An error occurred loading "
#~ msgstr "Při načítání došlo k chybě: " #~ msgstr "Při načítání došlo k chybě: "
# AI Translated
#~ msgid "/"
#~ msgstr "/"
# AI Translated # AI Translated
#, c-format, boost-format #, c-format, boost-format
#~ msgid "Load %s to " #~ msgid "Load %s to "
+233 -213
View File
@@ -3,7 +3,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: Orca Slicer\n" "Project-Id-Version: Orca Slicer\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-06 15:16-0300\n" "POT-Creation-Date: 2026-10-08 11:08+0800\n"
"PO-Revision-Date: \n" "PO-Revision-Date: \n"
"Last-Translator: Heiko Liebscher <hliebschergmail.com>\n" "Last-Translator: Heiko Liebscher <hliebschergmail.com>\n"
"Language-Team: \n" "Language-Team: \n"
@@ -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." msgid "You need to select the material type and color first."
msgstr "Sie müssen zuerst den Materialtyp und die Farbe auswählen." 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 #, c-format, boost-format
msgid "Please input a valid value (K in %.1f~%.1f)" msgid "Please input a valid value (K in %.1f~%.1f)"
msgstr "Bitte geben Sie einen gültigen Wert ein (K in %.1f~%.1f)" msgstr "Bitte geben Sie einen gültigen Wert ein (K in %.1f~%.1f)"
@@ -6156,7 +6159,7 @@ msgstr "Volumen:"
msgid "Size:" msgid "Size:"
msgstr "Größe:" 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)." 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)." msgstr "Konflikte von G-Code-Pfaden wurden bei Layer %d, Z = %.2lf mm gefunden.Bitte trennen Sie die konfliktbehafteten Objekte weiter voneinander (%s <-> %s)."
@@ -7024,6 +7027,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." msgid "File '%s' was lost! Please download it again."
msgstr "Die Datei '%s' ist verloren gegangen! Bitte laden Sie diese erneut herunter." 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 #, c-format, boost-format
msgid "" msgid ""
"File: %s\n" "File: %s\n"
@@ -11048,6 +11067,12 @@ msgstr "Zeitraffer abbrechen & Drucken"
msgid "Clean Up" msgid "Clean Up"
msgstr "Bereinigen" 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" msgid "Preparing print job"
msgstr "Druckauftrag vorbereiten" msgstr "Druckauftrag vorbereiten"
@@ -26532,214 +26557,6 @@ msgstr "Diese Schriftart hat keinen Umriss für diesen Text"
msgid "Size: %.1f × %.1f mm" msgid "Size: %.1f × %.1f mm"
msgstr "Größe: %.1f × %.1f mm" msgstr "Größe: %.1f × %.1f mm"
# AI Translated
msgid "Primitive"
msgstr "Primitiv"
# AI Translated
msgid "Empty mesh generated"
msgstr "Leeres Netz erzeugt"
# AI Translated
#. TRN Default name of an object created from the box primitive shape.
msgid "Box"
msgstr "Quader"
# AI Translated
#. TRN Primitive gizmo: label in front of the preset cube size buttons
msgid "Quick:"
msgstr "Schnell:"
# AI Translated
msgid "Width (X)"
msgstr "Breite (X)"
# AI Translated
msgid "Depth (Y)"
msgstr "Tiefe (Y)"
# AI Translated
msgid "Height (Z)"
msgstr "Höhe (Z)"
# AI Translated
#. TRN Primitive gizmo: bottom radius of a cone ("Top R" is the top radius)
msgid "Bottom R"
msgstr "R unten"
# AI Translated
msgid "Top R"
msgstr "R oben"
# AI Translated
#. TRN Primitive gizmo: major radius of a torus ("Minor R" is the tube radius)
msgid "Major R"
msgstr "R groß"
# AI Translated
msgid "Minor R"
msgstr "R klein"
# AI Translated
msgid "All edges"
msgstr "Alle Kanten"
# AI Translated
msgid "Top edges"
msgstr "Obere Kanten"
# AI Translated
msgid "Bottom edges"
msgstr "Untere Kanten"
# AI Translated
msgid "Lateral edges"
msgstr "Seitliche Kanten"
# AI Translated
msgid "Mesh resolution"
msgstr "Netzauflösung"
# AI Translated
msgid "Add Shape"
msgstr "Form hinzufügen"
# AI Translated
msgid "No outer profile"
msgstr "Kein Außenprofil"
# AI Translated
msgid "Failed to make outer face"
msgstr "Außenfläche konnte nicht erstellt werden"
# AI Translated
msgid "Failed to build face with holes"
msgstr "Fläche mit Löchern konnte nicht erstellt werden"
# AI Translated
msgid "Revolve failed"
msgstr "Rotieren fehlgeschlagen"
# AI Translated
msgid "Profile"
msgstr "Profil"
# AI Translated
msgid "Start new profile (for holes)"
msgstr "Neues Profil beginnen (für Löcher)"
# AI Translated
msgid "Segments"
msgstr "Segmente"
# AI Translated
msgid "Sides"
msgstr "Seiten"
# AI Translated
msgid "Click on canvas to draw"
msgstr "Zum Zeichnen in die Ansicht klicken"
# AI Translated
msgid "Snap to grid"
msgstr "Am Raster einrasten"
# AI Translated
msgid "Grid step"
msgstr "Rasterabstand"
# AI Translated
msgid "Outer"
msgstr "Außen"
# AI Translated
msgid "closed"
msgstr "geschlossen"
# AI Translated
#. TRN Sketch gizmo profile list: "Outer" or "Hole", profile number, point count, "closed" or nothing
#, boost-format
msgid "%1% %2%: %3% points %4%"
msgstr "%1% %2%: %3% Punkte %4%"
# AI Translated
msgid "XY (Top)"
msgstr "XY (Oben)"
# AI Translated
msgid "XZ (Front)"
msgstr "XZ (Vorne)"
# AI Translated
msgid "YZ (Side)"
msgstr "YZ (Seite)"
# AI Translated
msgid "Revolve deg"
msgstr "Rotieren (Grad)"
# AI Translated
msgid "Set to 0 for extrude, >0 for revolve"
msgstr "0 zum Extrudieren, >0 zum Rotieren"
# AI Translated
msgid "Pocket (cut)"
msgstr "Tasche (Ausschnitt)"
# AI Translated
#, c-format
msgid "%zu closed profile"
msgid_plural "%zu closed profiles"
msgstr[0] "%zu geschlossenes Profil"
msgstr[1] "%zu geschlossene Profile"
# AI Translated
msgid "Draw a closed profile to enable"
msgstr "Zum Aktivieren ein geschlossenes Profil zeichnen"
# AI Translated
msgid "Clear all"
msgstr "Alle löschen"
# AI Translated
msgid "Empty result"
msgstr "Leeres Ergebnis"
# AI Translated
#, boost-format
msgid "Extrude: %1%"
msgstr "Extrudieren: %1%"
# AI Translated
#, boost-format
msgid "Revolve: %1%"
msgstr "Rotieren: %1%"
# AI Translated
msgid "No object selected"
msgstr "Kein Objekt ausgewählt"
# AI Translated
msgid "Face failed"
msgstr "Fläche fehlgeschlagen"
# AI Translated
msgid "Face with holes failed"
msgstr "Fläche mit Löchern fehlgeschlagen"
# AI Translated
msgid "Tool mesh empty"
msgstr "Werkzeugnetz ist leer"
# AI Translated
msgid "Pocket added (negative volume)"
msgstr "Tasche hinzugefügt (negatives Volumen)"
# AI Translated
#, boost-format
msgid "Pocket: %1%"
msgstr "Tasche: %1%"
# AI Translated # AI Translated
msgctxt "Keyboard Shortcut" msgctxt "Keyboard Shortcut"
msgid "Ins" msgid "Ins"
@@ -30123,6 +29940,9 @@ msgstr "Entwicklertools aktivieren"
msgid "An error occurred loading %s" msgid "An error occurred loading %s"
msgstr "Fehler beim Laden von %s" msgstr "Fehler beim Laden von %s"
msgid "/"
msgstr "/"
# AI Translated # AI Translated
#, boost-format #, boost-format
msgid "Load %1% to %2%" msgid "Load %1% to %2%"
@@ -30802,6 +30622,209 @@ msgstr ""
"Verwerfungen vermeiden\n" "Verwerfungen vermeiden\n"
"Wussten Sie, dass beim Drucken von Materialien, die zu Verwerfungen neigen, wie z.B. ABS, durch eine entsprechende Erhöhung der Heizbetttemperatur die Wahrscheinlichkeit von Verwerfungen verringert werden kann?" "Wussten Sie, dass beim Drucken von Materialien, die zu Verwerfungen neigen, wie z.B. ABS, durch eine entsprechende Erhöhung der Heizbetttemperatur die Wahrscheinlichkeit von Verwerfungen verringert werden kann?"
# AI Translated
#~ msgid "Primitive"
#~ msgstr "Primitiv"
# AI Translated
#~ msgid "Empty mesh generated"
#~ msgstr "Leeres Netz erzeugt"
# AI Translated
#~ msgid "Box"
#~ msgstr "Quader"
# AI Translated
#~ msgid "Quick:"
#~ msgstr "Schnell:"
# AI Translated
#~ msgid "Width (X)"
#~ msgstr "Breite (X)"
# AI Translated
#~ msgid "Depth (Y)"
#~ msgstr "Tiefe (Y)"
# AI Translated
#~ msgid "Height (Z)"
#~ msgstr "Höhe (Z)"
# AI Translated
#~ msgid "Bottom R"
#~ msgstr "R unten"
# AI Translated
#~ msgid "Top R"
#~ msgstr "R oben"
# AI Translated
#~ msgid "Major R"
#~ msgstr "R groß"
# AI Translated
#~ msgid "Minor R"
#~ msgstr "R klein"
# AI Translated
#~ msgid "All edges"
#~ msgstr "Alle Kanten"
# AI Translated
#~ msgid "Top edges"
#~ msgstr "Obere Kanten"
# AI Translated
#~ msgid "Bottom edges"
#~ msgstr "Untere Kanten"
# AI Translated
#~ msgid "Lateral edges"
#~ msgstr "Seitliche Kanten"
# AI Translated
#~ msgid "Mesh resolution"
#~ msgstr "Netzauflösung"
# AI Translated
#~ msgid "Add Shape"
#~ msgstr "Form hinzufügen"
# AI Translated
#~ msgid "No outer profile"
#~ msgstr "Kein Außenprofil"
# AI Translated
#~ msgid "Failed to make outer face"
#~ msgstr "Außenfläche konnte nicht erstellt werden"
# AI Translated
#~ msgid "Failed to build face with holes"
#~ msgstr "Fläche mit Löchern konnte nicht erstellt werden"
# AI Translated
#~ msgid "Revolve failed"
#~ msgstr "Rotieren fehlgeschlagen"
# AI Translated
#~ msgid "Profile"
#~ msgstr "Profil"
# AI Translated
#~ msgid "Start new profile (for holes)"
#~ msgstr "Neues Profil beginnen (für Löcher)"
# AI Translated
#~ msgid "Segments"
#~ msgstr "Segmente"
# AI Translated
#~ msgid "Sides"
#~ msgstr "Seiten"
# AI Translated
#~ msgid "Click on canvas to draw"
#~ msgstr "Zum Zeichnen in die Ansicht klicken"
# AI Translated
#~ msgid "Snap to grid"
#~ msgstr "Am Raster einrasten"
# AI Translated
#~ msgid "Grid step"
#~ msgstr "Rasterabstand"
# AI Translated
#~ msgid "Outer"
#~ msgstr "Außen"
# AI Translated
#~ msgid "closed"
#~ msgstr "geschlossen"
# AI Translated
#, boost-format
#~ msgid "%1% %2%: %3% points %4%"
#~ msgstr "%1% %2%: %3% Punkte %4%"
# AI Translated
#~ msgid "XY (Top)"
#~ msgstr "XY (Oben)"
# AI Translated
#~ msgid "XZ (Front)"
#~ msgstr "XZ (Vorne)"
# AI Translated
#~ msgid "YZ (Side)"
#~ msgstr "YZ (Seite)"
# AI Translated
#~ msgid "Revolve deg"
#~ msgstr "Rotieren (Grad)"
# AI Translated
#~ msgid "Set to 0 for extrude, >0 for revolve"
#~ msgstr "0 zum Extrudieren, >0 zum Rotieren"
# AI Translated
#~ msgid "Pocket (cut)"
#~ msgstr "Tasche (Ausschnitt)"
# AI Translated
#, c-format
#~ msgid "%zu closed profile"
#~ msgid_plural "%zu closed profiles"
#~ msgstr[0] "%zu geschlossenes Profil"
#~ msgstr[1] "%zu geschlossene Profile"
# AI Translated
#~ msgid "Draw a closed profile to enable"
#~ msgstr "Zum Aktivieren ein geschlossenes Profil zeichnen"
# AI Translated
#~ msgid "Clear all"
#~ msgstr "Alle löschen"
# AI Translated
#~ msgid "Empty result"
#~ msgstr "Leeres Ergebnis"
# AI Translated
#, boost-format
#~ msgid "Extrude: %1%"
#~ msgstr "Extrudieren: %1%"
# AI Translated
#, boost-format
#~ msgid "Revolve: %1%"
#~ msgstr "Rotieren: %1%"
# AI Translated
#~ msgid "No object selected"
#~ msgstr "Kein Objekt ausgewählt"
# AI Translated
#~ msgid "Face failed"
#~ msgstr "Fläche fehlgeschlagen"
# AI Translated
#~ msgid "Face with holes failed"
#~ msgstr "Fläche mit Löchern fehlgeschlagen"
# AI Translated
#~ msgid "Tool mesh empty"
#~ msgstr "Werkzeugnetz ist leer"
# AI Translated
#~ msgid "Pocket added (negative volume)"
#~ msgstr "Tasche hinzugefügt (negatives Volumen)"
# AI Translated
#, boost-format
#~ msgid "Pocket: %1%"
#~ msgstr "Tasche: %1%"
# AI Translated # AI Translated
#~ msgid "(Experimental) Use printer agents instead of print hosts" #~ msgid "(Experimental) Use printer agents instead of print hosts"
#~ msgstr "(Experimentell) Drucker-Agenten anstelle von Druck-Hosts verwenden" #~ msgstr "(Experimentell) Drucker-Agenten anstelle von Druck-Hosts verwenden"
@@ -31857,9 +31880,6 @@ msgstr ""
#~ msgid "An error occurred loading " #~ msgid "An error occurred loading "
#~ msgstr "Fehler beim Laden von " #~ msgstr "Fehler beim Laden von "
#~ msgid "/"
#~ msgstr "/"
# AI Translated # AI Translated
#, c-format, boost-format #, c-format, boost-format
#~ msgid "Load %s to " #~ msgid "Load %s to "
+30 -161
View File
@@ -3,7 +3,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: Orca Slicer\n" "Project-Id-Version: Orca Slicer\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-06 15:16-0300\n" "POT-Creation-Date: 2026-10-08 11:08+0800\n"
"PO-Revision-Date: 2026-06-17 15:44-0300\n" "PO-Revision-Date: 2026-06-17 15:44-0300\n"
"Last-Translator: Alexandre Folle de Menezes\n" "Last-Translator: Alexandre Folle de Menezes\n"
"Language-Team: \n" "Language-Team: \n"
@@ -4042,6 +4042,9 @@ msgstr ""
msgid "You need to select the material type and color first." msgid "You need to select the material type and color first."
msgstr "" msgstr ""
msgid "The minimum temperature cannot be greater than the maximum temperature."
msgstr ""
#, c-format, boost-format #, c-format, boost-format
msgid "Please input a valid value (K in %.1f~%.1f)" msgid "Please input a valid value (K in %.1f~%.1f)"
msgstr "" msgstr ""
@@ -5775,7 +5778,7 @@ msgstr ""
msgid "Size:" msgid "Size:"
msgstr "" 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)." msgid "Conflicts of G-code paths have been found at layer %d, Z = %.2lfmm. Please separate the conflicted objects farther (%s <-> %s)."
msgstr "" msgstr ""
@@ -6614,6 +6617,22 @@ msgstr ""
msgid "File '%s' was lost! Please download it again." msgid "File '%s' was lost! Please download it again."
msgstr "" 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 #, c-format, boost-format
msgid "" msgid ""
"File: %s\n" "File: %s\n"
@@ -10184,6 +10203,12 @@ msgstr ""
msgid "Clean Up" msgid "Clean Up"
msgstr "" 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" msgid "Preparing print job"
msgstr "" msgstr ""
@@ -23590,165 +23615,6 @@ msgstr ""
msgid "Size: %.1f × %.1f mm" msgid "Size: %.1f × %.1f mm"
msgstr "" msgstr ""
msgid "Primitive"
msgstr ""
msgid "Empty mesh generated"
msgstr ""
#. TRN Default name of an object created from the box primitive shape.
msgid "Box"
msgstr ""
#. TRN Primitive gizmo: label in front of the preset cube size buttons
msgid "Quick:"
msgstr ""
msgid "Width (X)"
msgstr ""
msgid "Depth (Y)"
msgstr ""
msgid "Height (Z)"
msgstr ""
#. TRN Primitive gizmo: bottom radius of a cone ("Top R" is the top radius)
msgid "Bottom R"
msgstr ""
msgid "Top R"
msgstr ""
#. TRN Primitive gizmo: major radius of a torus ("Minor R" is the tube radius)
msgid "Major R"
msgstr ""
msgid "Minor R"
msgstr ""
msgid "All edges"
msgstr ""
msgid "Top edges"
msgstr ""
msgid "Bottom edges"
msgstr ""
msgid "Lateral edges"
msgstr ""
msgid "Mesh resolution"
msgstr ""
msgid "Add Shape"
msgstr ""
msgid "No outer profile"
msgstr ""
msgid "Failed to make outer face"
msgstr ""
msgid "Failed to build face with holes"
msgstr ""
msgid "Revolve failed"
msgstr ""
msgid "Profile"
msgstr ""
msgid "Start new profile (for holes)"
msgstr ""
msgid "Segments"
msgstr ""
msgid "Sides"
msgstr ""
msgid "Click on canvas to draw"
msgstr ""
msgid "Snap to grid"
msgstr ""
msgid "Grid step"
msgstr ""
msgid "Outer"
msgstr ""
msgid "closed"
msgstr ""
#. TRN Sketch gizmo profile list: "Outer" or "Hole", profile number, point count, "closed" or nothing
#, boost-format
msgid "%1% %2%: %3% points %4%"
msgstr ""
msgid "XY (Top)"
msgstr ""
msgid "XZ (Front)"
msgstr ""
msgid "YZ (Side)"
msgstr ""
msgid "Revolve deg"
msgstr ""
msgid "Set to 0 for extrude, >0 for revolve"
msgstr ""
msgid "Pocket (cut)"
msgstr ""
#, c-format
msgid "%zu closed profile"
msgid_plural "%zu closed profiles"
msgstr[0] ""
msgstr[1] ""
msgid "Draw a closed profile to enable"
msgstr ""
msgid "Clear all"
msgstr ""
msgid "Empty result"
msgstr ""
#, boost-format
msgid "Extrude: %1%"
msgstr ""
#, boost-format
msgid "Revolve: %1%"
msgstr ""
msgid "No object selected"
msgstr ""
msgid "Face failed"
msgstr ""
msgid "Face with holes failed"
msgstr ""
msgid "Tool mesh empty"
msgstr ""
msgid "Pocket added (negative volume)"
msgstr ""
#, boost-format
msgid "Pocket: %1%"
msgstr ""
msgctxt "Keyboard Shortcut" msgctxt "Keyboard Shortcut"
msgid "Ins" msgid "Ins"
msgstr "" msgstr ""
@@ -26336,6 +26202,9 @@ msgstr ""
msgid "An error occurred loading %s" msgid "An error occurred loading %s"
msgstr "" msgstr ""
msgid "/"
msgstr ""
#, boost-format #, boost-format
msgid "Load %1% to %2%" msgid "Load %1% to %2%"
msgstr "" msgstr ""
+232 -212
View File
@@ -3,7 +3,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: Orca Slicer\n" "Project-Id-Version: Orca Slicer\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-06 15:16-0300\n" "POT-Creation-Date: 2026-10-08 11:08+0800\n"
"PO-Revision-Date: \n" "PO-Revision-Date: \n"
"Last-Translator: Ian A. Bassi <>\n" "Last-Translator: Ian A. Bassi <>\n"
"Language-Team: \n" "Language-Team: \n"
@@ -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." msgid "You need to select the material type and color first."
msgstr "Necesitas seleccionar el tipo y el color del material primero." 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 #, c-format, boost-format
msgid "Please input a valid value (K in %.1f~%.1f)" msgid "Please input a valid value (K in %.1f~%.1f)"
msgstr "Por favor, introduzca un valor válido (K in %.1f~%.1f)" msgstr "Por favor, introduzca un valor válido (K in %.1f~%.1f)"
@@ -6009,7 +6012,7 @@ msgstr "Volumen:"
msgid "Size:" msgid "Size:"
msgstr "Tamaño:" 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)." 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)." 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)."
@@ -6876,6 +6879,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." msgid "File '%s' was lost! Please download it again."
msgstr "¡El archivo '%s' se perdió!\" Por favor, vuelva a descargárlo." 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 #, c-format, boost-format
msgid "" msgid ""
"File: %s\n" "File: %s\n"
@@ -10780,6 +10799,12 @@ msgstr "Cancelar timelapse e imprimir"
msgid "Clean Up" msgid "Clean Up"
msgstr "Limpiar" 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" msgid "Preparing print job"
msgstr "Preparando el trabajo de impresión" msgstr "Preparando el trabajo de impresión"
@@ -26095,213 +26120,6 @@ msgstr "Esta fuente no tiene contorno para este texto"
msgid "Size: %.1f × %.1f mm" msgid "Size: %.1f × %.1f mm"
msgstr "Tamaño: %.1f × %.1f mm" msgstr "Tamaño: %.1f × %.1f mm"
# AI Translated
msgid "Primitive"
msgstr "Primitivo"
# AI Translated
msgid "Empty mesh generated"
msgstr "Se generó una malla vacía"
# AI Translated
#. TRN Default name of an object created from the box primitive shape.
msgid "Box"
msgstr "Caja"
# AI Translated
#. TRN Primitive gizmo: label in front of the preset cube size buttons
msgid "Quick:"
msgstr "Rápido:"
# AI Translated
msgid "Width (X)"
msgstr "Ancho (X)"
# AI Translated
msgid "Depth (Y)"
msgstr "Profundidad (Y)"
# AI Translated
msgid "Height (Z)"
msgstr "Altura (Z)"
# AI Translated
#. TRN Primitive gizmo: bottom radius of a cone ("Top R" is the top radius)
msgid "Bottom R"
msgstr "R inferior"
# AI Translated
msgid "Top R"
msgstr "R superior"
# AI Translated
#. TRN Primitive gizmo: major radius of a torus ("Minor R" is the tube radius)
msgid "Major R"
msgstr "R mayor"
# AI Translated
msgid "Minor R"
msgstr "R menor"
# AI Translated
msgid "All edges"
msgstr "Todas las aristas"
# AI Translated
msgid "Top edges"
msgstr "Aristas superiores"
# AI Translated
msgid "Bottom edges"
msgstr "Aristas inferiores"
# AI Translated
msgid "Lateral edges"
msgstr "Aristas laterales"
# AI Translated
msgid "Mesh resolution"
msgstr "Resolución de la malla"
# AI Translated
msgid "Add Shape"
msgstr "Añadir forma"
# AI Translated
msgid "No outer profile"
msgstr "No hay perfil exterior"
# AI Translated
msgid "Failed to make outer face"
msgstr "No se pudo crear la cara exterior"
# AI Translated
msgid "Failed to build face with holes"
msgstr "No se pudo crear la cara con agujeros"
# AI Translated
msgid "Revolve failed"
msgstr "Falló la revolución"
# AI Translated
msgid "Profile"
msgstr "Perfil"
# AI Translated
msgid "Start new profile (for holes)"
msgstr "Iniciar un nuevo perfil (para agujeros)"
# AI Translated
msgid "Segments"
msgstr "Segmentos"
# AI Translated
msgid "Sides"
msgstr "Lados"
# AI Translated
msgid "Click on canvas to draw"
msgstr "Haga clic en el lienzo para dibujar"
# AI Translated
msgid "Snap to grid"
msgstr "Ajustar a la cuadrícula"
# AI Translated
msgid "Grid step"
msgstr "Paso de cuadrícula"
# AI Translated
msgid "Outer"
msgstr "Exterior"
# AI Translated
msgid "closed"
msgstr "cerrado"
# AI Translated
#. TRN Sketch gizmo profile list: "Outer" or "Hole", profile number, point count, "closed" or nothing
#, boost-format
msgid "%1% %2%: %3% points %4%"
msgstr "%1% %2%: %3% puntos %4%"
# AI Translated
msgid "XY (Top)"
msgstr "XY (superior)"
# AI Translated
msgid "XZ (Front)"
msgstr "XZ (frontal)"
# AI Translated
msgid "YZ (Side)"
msgstr "YZ (lateral)"
# AI Translated
msgid "Revolve deg"
msgstr "Grados de revolución"
# AI Translated
msgid "Set to 0 for extrude, >0 for revolve"
msgstr "Establezca 0 para extruir y >0 para revolución"
# AI Translated
msgid "Pocket (cut)"
msgstr "Cajera (corte)"
# AI Translated
#, c-format
msgid "%zu closed profile"
msgid_plural "%zu closed profiles"
msgstr[0] "%zu perfil cerrado"
msgstr[1] "%zu perfiles cerrados"
# AI Translated
msgid "Draw a closed profile to enable"
msgstr "Dibuje un perfil cerrado para activarlo"
msgid "Clear all"
msgstr "Borrar todo"
# AI Translated
msgid "Empty result"
msgstr "Resultado vacío"
# AI Translated
#, boost-format
msgid "Extrude: %1%"
msgstr "Extruir: %1%"
# AI Translated
#, boost-format
msgid "Revolve: %1%"
msgstr "Revolución: %1%"
# AI Translated
msgid "No object selected"
msgstr "No hay ningún objeto seleccionado"
# AI Translated
msgid "Face failed"
msgstr "Falló la cara"
# AI Translated
msgid "Face with holes failed"
msgstr "Falló la cara con agujeros"
# AI Translated
msgid "Tool mesh empty"
msgstr "Malla de la herramienta vacía"
# AI Translated
msgid "Pocket added (negative volume)"
msgstr "Cajera añadida (volumen negativo)"
# AI Translated
#, boost-format
msgid "Pocket: %1%"
msgstr "Cajera: %1%"
# AI Translated # AI Translated
msgctxt "Keyboard Shortcut" msgctxt "Keyboard Shortcut"
msgid "Ins" msgid "Ins"
@@ -29678,6 +29496,9 @@ msgstr "Activar herramientas de desarrollo"
msgid "An error occurred loading %s" msgid "An error occurred loading %s"
msgstr "Se produjo un error al cargar %s" msgstr "Se produjo un error al cargar %s"
msgid "/"
msgstr "/"
# AI Translated # AI Translated
#, boost-format #, boost-format
msgid "Load %1% to %2%" msgid "Load %1% to %2%"
@@ -30357,6 +30178,208 @@ msgstr ""
"Evita la deformación\n" "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?" "¿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 "Primitive"
#~ msgstr "Primitivo"
# AI Translated
#~ msgid "Empty mesh generated"
#~ msgstr "Se generó una malla vacía"
# AI Translated
#~ msgid "Box"
#~ msgstr "Caja"
# AI Translated
#~ msgid "Quick:"
#~ msgstr "Rápido:"
# AI Translated
#~ msgid "Width (X)"
#~ msgstr "Ancho (X)"
# AI Translated
#~ msgid "Depth (Y)"
#~ msgstr "Profundidad (Y)"
# AI Translated
#~ msgid "Height (Z)"
#~ msgstr "Altura (Z)"
# AI Translated
#~ msgid "Bottom R"
#~ msgstr "R inferior"
# AI Translated
#~ msgid "Top R"
#~ msgstr "R superior"
# AI Translated
#~ msgid "Major R"
#~ msgstr "R mayor"
# AI Translated
#~ msgid "Minor R"
#~ msgstr "R menor"
# AI Translated
#~ msgid "All edges"
#~ msgstr "Todas las aristas"
# AI Translated
#~ msgid "Top edges"
#~ msgstr "Aristas superiores"
# AI Translated
#~ msgid "Bottom edges"
#~ msgstr "Aristas inferiores"
# AI Translated
#~ msgid "Lateral edges"
#~ msgstr "Aristas laterales"
# AI Translated
#~ msgid "Mesh resolution"
#~ msgstr "Resolución de la malla"
# AI Translated
#~ msgid "Add Shape"
#~ msgstr "Añadir forma"
# AI Translated
#~ msgid "No outer profile"
#~ msgstr "No hay perfil exterior"
# AI Translated
#~ msgid "Failed to make outer face"
#~ msgstr "No se pudo crear la cara exterior"
# AI Translated
#~ msgid "Failed to build face with holes"
#~ msgstr "No se pudo crear la cara con agujeros"
# AI Translated
#~ msgid "Revolve failed"
#~ msgstr "Falló la revolución"
# AI Translated
#~ msgid "Profile"
#~ msgstr "Perfil"
# AI Translated
#~ msgid "Start new profile (for holes)"
#~ msgstr "Iniciar un nuevo perfil (para agujeros)"
# AI Translated
#~ msgid "Segments"
#~ msgstr "Segmentos"
# AI Translated
#~ msgid "Sides"
#~ msgstr "Lados"
# AI Translated
#~ msgid "Click on canvas to draw"
#~ msgstr "Haga clic en el lienzo para dibujar"
# AI Translated
#~ msgid "Snap to grid"
#~ msgstr "Ajustar a la cuadrícula"
# AI Translated
#~ msgid "Grid step"
#~ msgstr "Paso de cuadrícula"
# AI Translated
#~ msgid "Outer"
#~ msgstr "Exterior"
# AI Translated
#~ msgid "closed"
#~ msgstr "cerrado"
# AI Translated
#, boost-format
#~ msgid "%1% %2%: %3% points %4%"
#~ msgstr "%1% %2%: %3% puntos %4%"
# AI Translated
#~ msgid "XY (Top)"
#~ msgstr "XY (superior)"
# AI Translated
#~ msgid "XZ (Front)"
#~ msgstr "XZ (frontal)"
# AI Translated
#~ msgid "YZ (Side)"
#~ msgstr "YZ (lateral)"
# AI Translated
#~ msgid "Revolve deg"
#~ msgstr "Grados de revolución"
# AI Translated
#~ msgid "Set to 0 for extrude, >0 for revolve"
#~ msgstr "Establezca 0 para extruir y >0 para revolución"
# AI Translated
#~ msgid "Pocket (cut)"
#~ msgstr "Cajera (corte)"
# AI Translated
#, c-format
#~ msgid "%zu closed profile"
#~ msgid_plural "%zu closed profiles"
#~ msgstr[0] "%zu perfil cerrado"
#~ msgstr[1] "%zu perfiles cerrados"
# AI Translated
#~ msgid "Draw a closed profile to enable"
#~ msgstr "Dibuje un perfil cerrado para activarlo"
#~ msgid "Clear all"
#~ msgstr "Borrar todo"
# AI Translated
#~ msgid "Empty result"
#~ msgstr "Resultado vacío"
# AI Translated
#, boost-format
#~ msgid "Extrude: %1%"
#~ msgstr "Extruir: %1%"
# AI Translated
#, boost-format
#~ msgid "Revolve: %1%"
#~ msgstr "Revolución: %1%"
# AI Translated
#~ msgid "No object selected"
#~ msgstr "No hay ningún objeto seleccionado"
# AI Translated
#~ msgid "Face failed"
#~ msgstr "Falló la cara"
# AI Translated
#~ msgid "Face with holes failed"
#~ msgstr "Falló la cara con agujeros"
# AI Translated
#~ msgid "Tool mesh empty"
#~ msgstr "Malla de la herramienta vacía"
# AI Translated
#~ msgid "Pocket added (negative volume)"
#~ msgstr "Cajera añadida (volumen negativo)"
# AI Translated
#, boost-format
#~ msgid "Pocket: %1%"
#~ msgstr "Cajera: %1%"
# AI Translated # AI Translated
#~ msgid "(Experimental) Use printer agents instead of print hosts" #~ msgid "(Experimental) Use printer agents instead of print hosts"
#~ msgstr "(Experimental) Usar agentes de impresora en lugar de hosts de impresión" #~ msgstr "(Experimental) Usar agentes de impresora en lugar de hosts de impresión"
@@ -31412,9 +31435,6 @@ msgstr ""
#~ msgid "An error occurred loading " #~ msgid "An error occurred loading "
#~ msgstr "Se produjo un error al cargar " #~ msgstr "Se produjo un error al cargar "
#~ msgid "/"
#~ msgstr "/"
# AI Translated # AI Translated
#, c-format, boost-format #, c-format, boost-format
#~ msgid "Load %s to " #~ msgid "Load %s to "
+233 -213
View File
@@ -7,7 +7,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: \n" "Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-06 15:16-0300\n" "POT-Creation-Date: 2026-10-08 11:08+0800\n"
"PO-Revision-Date: 2026-07-20 13:33+0200\n" "PO-Revision-Date: 2026-07-20 13:33+0200\n"
"Last-Translator: Manu Goiogana <mgoiogana@gmail.com>\n" "Last-Translator: Manu Goiogana <mgoiogana@gmail.com>\n"
"Language-Team: \n" "Language-Team: \n"
@@ -4218,6 +4218,9 @@ msgstr "Ziur zaude filamentuaren informazioa garbitu nahi duzula?"
msgid "You need to select the material type and color first." msgid "You need to select the material type and color first."
msgstr "Lehenik eta behin, material-mota eta kolorea hautatu behar dituzu." 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 #, c-format, boost-format
msgid "Please input a valid value (K in %.1f~%.1f)" msgid "Please input a valid value (K in %.1f~%.1f)"
msgstr "Mesedez, sartu baliozko balio bat (K in %.1f~%.1f)" msgstr "Mesedez, sartu baliozko balio bat (K in %.1f~%.1f)"
@@ -6063,7 +6066,7 @@ msgstr "Bolumena:"
msgid "Size:" msgid "Size:"
msgstr "Tamaina:" 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)." 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)." msgstr "G-code ibilbideen gatazkak aurkitu dira %d geruzan, Z = %.2lf mm. Urrundu gehiago gatazkan dauden objektuak (%s <-> %s)."
@@ -6927,6 +6930,22 @@ msgstr ".gcode.3mf fitxategiak ez du G-code daturik. Mesedez, xerratu OrcaSlicer
msgid "File '%s' was lost! Please download it again." msgid "File '%s' was lost! Please download it again."
msgstr "'%s' fitxategia galdu da! Deskargatu berriro." 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 #, c-format, boost-format
msgid "" msgid ""
"File: %s\n" "File: %s\n"
@@ -10886,6 +10905,12 @@ msgstr "Utzi timelapsea eta inprimatu"
msgid "Clean Up" msgid "Clean Up"
msgstr "Garbitu" 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" msgid "Preparing print job"
msgstr "Inprimatze-lana prestatzen" msgstr "Inprimatze-lana prestatzen"
@@ -26269,214 +26294,6 @@ msgstr "Letra-tipo honek ez du ingeradarik testu honetarako"
msgid "Size: %.1f × %.1f mm" msgid "Size: %.1f × %.1f mm"
msgstr "Tamaina: %.1f × %.1f mm" msgstr "Tamaina: %.1f × %.1f mm"
# AI Translated
msgid "Primitive"
msgstr "Oinarrizko geometria"
# AI Translated
msgid "Empty mesh generated"
msgstr "Sare hutsa sortu da"
# AI Translated
#. TRN Default name of an object created from the box primitive shape.
msgid "Box"
msgstr "Kutxa"
# AI Translated
#. TRN Primitive gizmo: label in front of the preset cube size buttons
msgid "Quick:"
msgstr "Azkar:"
# AI Translated
msgid "Width (X)"
msgstr "Zabalera (X)"
# AI Translated
msgid "Depth (Y)"
msgstr "Sakonera (Y)"
# AI Translated
msgid "Height (Z)"
msgstr "Altuera (Z)"
# AI Translated
#. TRN Primitive gizmo: bottom radius of a cone ("Top R" is the top radius)
msgid "Bottom R"
msgstr "Beheko R"
# AI Translated
msgid "Top R"
msgstr "Goiko R"
# AI Translated
#. TRN Primitive gizmo: major radius of a torus ("Minor R" is the tube radius)
msgid "Major R"
msgstr "R nagusia"
# AI Translated
msgid "Minor R"
msgstr "R txikia"
# AI Translated
msgid "All edges"
msgstr "Ertz guztiak"
# AI Translated
msgid "Top edges"
msgstr "Goiko ertzak"
# AI Translated
msgid "Bottom edges"
msgstr "Beheko ertzak"
# AI Translated
msgid "Lateral edges"
msgstr "Alboko ertzak"
# AI Translated
msgid "Mesh resolution"
msgstr "Sarearen bereizmena"
# AI Translated
msgid "Add Shape"
msgstr "Gehitu forma"
# AI Translated
msgid "No outer profile"
msgstr "Ez dago kanpoko profilik"
# AI Translated
msgid "Failed to make outer face"
msgstr "Ezin izan da kanpoko aurpegia sortu"
# AI Translated
msgid "Failed to build face with holes"
msgstr "Ezin izan da zuloak dituen aurpegia eraiki"
# AI Translated
msgid "Revolve failed"
msgstr "Erreboluzioak huts egin du"
# AI Translated
msgid "Profile"
msgstr "Profila"
# AI Translated
msgid "Start new profile (for holes)"
msgstr "Hasi profil berri bat (zuloetarako)"
# AI Translated
msgid "Segments"
msgstr "Segmentuak"
# AI Translated
msgid "Sides"
msgstr "Aldeak"
# AI Translated
msgid "Click on canvas to draw"
msgstr "Egin klik mihisean marrazteko"
# AI Translated
msgid "Snap to grid"
msgstr "Atxiki saretara"
# AI Translated
msgid "Grid step"
msgstr "Sareta-urratsa"
# AI Translated
msgid "Outer"
msgstr "Kanpokoa"
# AI Translated
msgid "closed"
msgstr "itxita"
# AI Translated
#. TRN Sketch gizmo profile list: "Outer" or "Hole", profile number, point count, "closed" or nothing
#, boost-format
msgid "%1% %2%: %3% points %4%"
msgstr "%1% %2%: %3% puntu %4%"
# AI Translated
msgid "XY (Top)"
msgstr "XY (Goikoa)"
# AI Translated
msgid "XZ (Front)"
msgstr "XZ (Aurrekoa)"
# AI Translated
msgid "YZ (Side)"
msgstr "YZ (Albokoa)"
# AI Translated
msgid "Revolve deg"
msgstr "Erreboluzio-graduak"
# AI Translated
msgid "Set to 0 for extrude, >0 for revolve"
msgstr "Ezarri 0 estruitzeko, >0 erreboluzionatzeko"
# AI Translated
msgid "Pocket (cut)"
msgstr "Poltsa (ebaki)"
# AI Translated
#, c-format
msgid "%zu closed profile"
msgid_plural "%zu closed profiles"
msgstr[0] "%zu profil itxi"
msgstr[1] "%zu profil itxi"
# AI Translated
msgid "Draw a closed profile to enable"
msgstr "Marraztu profil itxi bat gaitzeko"
# AI Translated
msgid "Clear all"
msgstr "Garbitu dena"
# AI Translated
msgid "Empty result"
msgstr "Emaitza hutsa"
# AI Translated
#, boost-format
msgid "Extrude: %1%"
msgstr "Estrusioa: %1%"
# AI Translated
#, boost-format
msgid "Revolve: %1%"
msgstr "Erreboluzioa: %1%"
# AI Translated
msgid "No object selected"
msgstr "Ez dago objekturik hautatuta"
# AI Translated
msgid "Face failed"
msgstr "Aurpegiak huts egin du"
# AI Translated
msgid "Face with holes failed"
msgstr "Zuloak dituen aurpegiak huts egin du"
# AI Translated
msgid "Tool mesh empty"
msgstr "Tresnaren sarea hutsik dago"
# AI Translated
msgid "Pocket added (negative volume)"
msgstr "Poltsa gehitu da (bolumen negatiboa)"
# AI Translated
#, boost-format
msgid "Pocket: %1%"
msgstr "Poltsa: %1%"
# AI Translated # AI Translated
msgctxt "Keyboard Shortcut" msgctxt "Keyboard Shortcut"
msgid "Ins" msgid "Ins"
@@ -29870,6 +29687,9 @@ msgstr "Gaitu garapen-tresnak"
msgid "An error occurred loading %s" msgid "An error occurred loading %s"
msgstr "Errore bat gertatu da %s kargatzean" msgstr "Errore bat gertatu da %s kargatzean"
msgid "/"
msgstr "/"
# AI Translated # AI Translated
#, boost-format #, boost-format
msgid "Load %1% to %2%" msgid "Load %1% to %2%"
@@ -30549,6 +30369,209 @@ msgstr ""
"Saihestu okertzea\n" "Saihestu okertzea\n"
"Ba al zenekien ABS bezalako okertzeko joera duten materialak inprimatzean ohe beroaren tenperatura egoki igotzeak okertzeko probabilitatea murriztu dezakeela?" "Ba al zenekien ABS bezalako okertzeko joera duten materialak inprimatzean ohe beroaren tenperatura egoki igotzeak okertzeko probabilitatea murriztu dezakeela?"
# AI Translated
#~ msgid "Primitive"
#~ msgstr "Oinarrizko geometria"
# AI Translated
#~ msgid "Empty mesh generated"
#~ msgstr "Sare hutsa sortu da"
# AI Translated
#~ msgid "Box"
#~ msgstr "Kutxa"
# AI Translated
#~ msgid "Quick:"
#~ msgstr "Azkar:"
# AI Translated
#~ msgid "Width (X)"
#~ msgstr "Zabalera (X)"
# AI Translated
#~ msgid "Depth (Y)"
#~ msgstr "Sakonera (Y)"
# AI Translated
#~ msgid "Height (Z)"
#~ msgstr "Altuera (Z)"
# AI Translated
#~ msgid "Bottom R"
#~ msgstr "Beheko R"
# AI Translated
#~ msgid "Top R"
#~ msgstr "Goiko R"
# AI Translated
#~ msgid "Major R"
#~ msgstr "R nagusia"
# AI Translated
#~ msgid "Minor R"
#~ msgstr "R txikia"
# AI Translated
#~ msgid "All edges"
#~ msgstr "Ertz guztiak"
# AI Translated
#~ msgid "Top edges"
#~ msgstr "Goiko ertzak"
# AI Translated
#~ msgid "Bottom edges"
#~ msgstr "Beheko ertzak"
# AI Translated
#~ msgid "Lateral edges"
#~ msgstr "Alboko ertzak"
# AI Translated
#~ msgid "Mesh resolution"
#~ msgstr "Sarearen bereizmena"
# AI Translated
#~ msgid "Add Shape"
#~ msgstr "Gehitu forma"
# AI Translated
#~ msgid "No outer profile"
#~ msgstr "Ez dago kanpoko profilik"
# AI Translated
#~ msgid "Failed to make outer face"
#~ msgstr "Ezin izan da kanpoko aurpegia sortu"
# AI Translated
#~ msgid "Failed to build face with holes"
#~ msgstr "Ezin izan da zuloak dituen aurpegia eraiki"
# AI Translated
#~ msgid "Revolve failed"
#~ msgstr "Erreboluzioak huts egin du"
# AI Translated
#~ msgid "Profile"
#~ msgstr "Profila"
# AI Translated
#~ msgid "Start new profile (for holes)"
#~ msgstr "Hasi profil berri bat (zuloetarako)"
# AI Translated
#~ msgid "Segments"
#~ msgstr "Segmentuak"
# AI Translated
#~ msgid "Sides"
#~ msgstr "Aldeak"
# AI Translated
#~ msgid "Click on canvas to draw"
#~ msgstr "Egin klik mihisean marrazteko"
# AI Translated
#~ msgid "Snap to grid"
#~ msgstr "Atxiki saretara"
# AI Translated
#~ msgid "Grid step"
#~ msgstr "Sareta-urratsa"
# AI Translated
#~ msgid "Outer"
#~ msgstr "Kanpokoa"
# AI Translated
#~ msgid "closed"
#~ msgstr "itxita"
# AI Translated
#, boost-format
#~ msgid "%1% %2%: %3% points %4%"
#~ msgstr "%1% %2%: %3% puntu %4%"
# AI Translated
#~ msgid "XY (Top)"
#~ msgstr "XY (Goikoa)"
# AI Translated
#~ msgid "XZ (Front)"
#~ msgstr "XZ (Aurrekoa)"
# AI Translated
#~ msgid "YZ (Side)"
#~ msgstr "YZ (Albokoa)"
# AI Translated
#~ msgid "Revolve deg"
#~ msgstr "Erreboluzio-graduak"
# AI Translated
#~ msgid "Set to 0 for extrude, >0 for revolve"
#~ msgstr "Ezarri 0 estruitzeko, >0 erreboluzionatzeko"
# AI Translated
#~ msgid "Pocket (cut)"
#~ msgstr "Poltsa (ebaki)"
# AI Translated
#, c-format
#~ msgid "%zu closed profile"
#~ msgid_plural "%zu closed profiles"
#~ msgstr[0] "%zu profil itxi"
#~ msgstr[1] "%zu profil itxi"
# AI Translated
#~ msgid "Draw a closed profile to enable"
#~ msgstr "Marraztu profil itxi bat gaitzeko"
# AI Translated
#~ msgid "Clear all"
#~ msgstr "Garbitu dena"
# AI Translated
#~ msgid "Empty result"
#~ msgstr "Emaitza hutsa"
# AI Translated
#, boost-format
#~ msgid "Extrude: %1%"
#~ msgstr "Estrusioa: %1%"
# AI Translated
#, boost-format
#~ msgid "Revolve: %1%"
#~ msgstr "Erreboluzioa: %1%"
# AI Translated
#~ msgid "No object selected"
#~ msgstr "Ez dago objekturik hautatuta"
# AI Translated
#~ msgid "Face failed"
#~ msgstr "Aurpegiak huts egin du"
# AI Translated
#~ msgid "Face with holes failed"
#~ msgstr "Zuloak dituen aurpegiak huts egin du"
# AI Translated
#~ msgid "Tool mesh empty"
#~ msgstr "Tresnaren sarea hutsik dago"
# AI Translated
#~ msgid "Pocket added (negative volume)"
#~ msgstr "Poltsa gehitu da (bolumen negatiboa)"
# AI Translated
#, boost-format
#~ msgid "Pocket: %1%"
#~ msgstr "Poltsa: %1%"
# AI Translated # AI Translated
#~ msgid "(Experimental) Use printer agents instead of print hosts" #~ msgid "(Experimental) Use printer agents instead of print hosts"
#~ msgstr "(Esperimentala) Erabili inprimagailu-agenteak inprimatze-hostenen ordez" #~ msgstr "(Esperimentala) Erabili inprimagailu-agenteak inprimatze-hostenen ordez"
@@ -31608,9 +31631,6 @@ msgstr ""
#~ msgid "An error occurred loading " #~ msgid "An error occurred loading "
#~ msgstr "Errore bat gertatu da hau kargatzean: " #~ msgstr "Errore bat gertatu da hau kargatzean: "
#~ msgid "/"
#~ msgstr "/"
# AI Translated # AI Translated
#, c-format, boost-format #, c-format, boost-format
#~ msgid "Load %s to " #~ msgid "Load %s to "
+233 -213
View File
@@ -6,7 +6,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: Orca Slicer\n" "Project-Id-Version: Orca Slicer\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-06 15:16-0300\n" "POT-Creation-Date: 2026-10-08 11:08+0800\n"
"PO-Revision-Date: \n" "PO-Revision-Date: \n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Guislain Cyril, Thomas Lété\n" "Language-Team: Guislain Cyril, Thomas Lété\n"
@@ -4247,6 +4247,9 @@ msgstr "Voulez-vous vraiment effacer les informations du filament ?"
msgid "You need to select the material type and color first." 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." 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 #, c-format, boost-format
msgid "Please input a valid value (K in %.1f~%.1f)" msgid "Please input a valid value (K in %.1f~%.1f)"
msgstr "Veuillez saisir une valeur valide (K entre %.1f~%.1f)" msgstr "Veuillez saisir une valeur valide (K entre %.1f~%.1f)"
@@ -6098,7 +6101,7 @@ msgstr "Volume :"
msgid "Size:" msgid "Size:"
msgstr "Taille :" 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)." 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)." 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)."
@@ -6966,6 +6969,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." msgid "File '%s' was lost! Please download it again."
msgstr "Le fichier « %s » a été perdu ! Veuillez le télécharger à nouveau." 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 #, c-format, boost-format
msgid "" msgid ""
"File: %s\n" "File: %s\n"
@@ -10902,6 +10921,12 @@ msgstr "Annuler le timelapse et imprimer"
msgid "Clean Up" msgid "Clean Up"
msgstr "Nettoyer" 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" msgid "Preparing print job"
msgstr "Préparation du travail d'impression" msgstr "Préparation du travail d'impression"
@@ -26035,214 +26060,6 @@ msgstr "Cette police n’a pas de contour pour ce texte"
msgid "Size: %.1f × %.1f mm" msgid "Size: %.1f × %.1f mm"
msgstr "Taille : %.1f × %.1f mm" msgstr "Taille : %.1f × %.1f mm"
# AI Translated
msgid "Primitive"
msgstr "Primitive"
# AI Translated
msgid "Empty mesh generated"
msgstr "Maillage vide généré"
# AI Translated
#. TRN Default name of an object created from the box primitive shape.
msgid "Box"
msgstr "Boîte"
# AI Translated
#. TRN Primitive gizmo: label in front of the preset cube size buttons
msgid "Quick:"
msgstr "Rapide :"
# AI Translated
msgid "Width (X)"
msgstr "Largeur (X)"
# AI Translated
msgid "Depth (Y)"
msgstr "Profondeur (Y)"
# AI Translated
msgid "Height (Z)"
msgstr "Hauteur (Z)"
# AI Translated
#. TRN Primitive gizmo: bottom radius of a cone ("Top R" is the top radius)
msgid "Bottom R"
msgstr "R inférieur"
# AI Translated
msgid "Top R"
msgstr "R supérieur"
# AI Translated
#. TRN Primitive gizmo: major radius of a torus ("Minor R" is the tube radius)
msgid "Major R"
msgstr "Grand R"
# AI Translated
msgid "Minor R"
msgstr "Petit R"
# AI Translated
msgid "All edges"
msgstr "Toutes les arêtes"
# AI Translated
msgid "Top edges"
msgstr "Arêtes supérieures"
# AI Translated
msgid "Bottom edges"
msgstr "Arêtes inférieures"
# AI Translated
msgid "Lateral edges"
msgstr "Arêtes latérales"
# AI Translated
msgid "Mesh resolution"
msgstr "Résolution du maillage"
# AI Translated
msgid "Add Shape"
msgstr "Ajouter la forme"
# AI Translated
msgid "No outer profile"
msgstr "Aucun profil extérieur"
# AI Translated
msgid "Failed to make outer face"
msgstr "Échec de la création de la face extérieure"
# AI Translated
msgid "Failed to build face with holes"
msgstr "Échec de la création de la face avec trous"
# AI Translated
msgid "Revolve failed"
msgstr "Échec de la révolution"
# AI Translated
msgid "Profile"
msgstr "Profil"
# AI Translated
msgid "Start new profile (for holes)"
msgstr "Commencer un nouveau profil (pour les trous)"
# AI Translated
msgid "Segments"
msgstr "Segments"
# AI Translated
msgid "Sides"
msgstr "Côtés"
# AI Translated
msgid "Click on canvas to draw"
msgstr "Cliquez dans la vue pour dessiner"
# AI Translated
msgid "Snap to grid"
msgstr "Aimanter à la grille"
# AI Translated
msgid "Grid step"
msgstr "Pas de la grille"
# AI Translated
msgid "Outer"
msgstr "Extérieur"
# AI Translated
msgid "closed"
msgstr "fermé"
# AI Translated
#. TRN Sketch gizmo profile list: "Outer" or "Hole", profile number, point count, "closed" or nothing
#, boost-format
msgid "%1% %2%: %3% points %4%"
msgstr "%1% %2% : %3% points %4%"
# AI Translated
msgid "XY (Top)"
msgstr "XY (Haut)"
# AI Translated
msgid "XZ (Front)"
msgstr "XZ (Avant)"
# AI Translated
msgid "YZ (Side)"
msgstr "YZ (Côté)"
# AI Translated
msgid "Revolve deg"
msgstr "Révolution (°)"
# AI Translated
msgid "Set to 0 for extrude, >0 for revolve"
msgstr "0 pour une extrusion, >0 pour une révolution"
# AI Translated
msgid "Pocket (cut)"
msgstr "Poche (enlèvement)"
# AI Translated
#, c-format
msgid "%zu closed profile"
msgid_plural "%zu closed profiles"
msgstr[0] "%zu profil fermé"
msgstr[1] "%zu profils fermés"
# AI Translated
msgid "Draw a closed profile to enable"
msgstr "Dessinez un profil fermé pour activer"
# AI Translated
msgid "Clear all"
msgstr "Tout effacer"
# AI Translated
msgid "Empty result"
msgstr "Résultat vide"
# AI Translated
#, boost-format
msgid "Extrude: %1%"
msgstr "Extrusion : %1%"
# AI Translated
#, boost-format
msgid "Revolve: %1%"
msgstr "Révolution : %1%"
# AI Translated
msgid "No object selected"
msgstr "Aucun objet sélectionné"
# AI Translated
msgid "Face failed"
msgstr "Échec de la face"
# AI Translated
msgid "Face with holes failed"
msgstr "Échec de la face avec trous"
# AI Translated
msgid "Tool mesh empty"
msgstr "Maillage de l’outil vide"
# AI Translated
msgid "Pocket added (negative volume)"
msgstr "Poche ajoutée (volume négatif)"
# AI Translated
#, boost-format
msgid "Pocket: %1%"
msgstr "Poche : %1%"
# AI Translated # AI Translated
msgctxt "Keyboard Shortcut" msgctxt "Keyboard Shortcut"
msgid "Ins" msgid "Ins"
@@ -29616,6 +29433,9 @@ msgstr "Activer les outils de développement"
msgid "An error occurred loading %s" msgid "An error occurred loading %s"
msgstr "Une erreur s’est produite lors du chargement de %s" msgstr "Une erreur s’est produite lors du chargement de %s"
msgid "/"
msgstr "/"
# AI Translated # AI Translated
#, boost-format #, boost-format
msgid "Load %1% to %2%" msgid "Load %1% to %2%"
@@ -30295,6 +30115,209 @@ msgstr ""
"Éviter la déformation\n" "É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?" "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 "Primitive"
#~ msgstr "Primitive"
# AI Translated
#~ msgid "Empty mesh generated"
#~ msgstr "Maillage vide généré"
# AI Translated
#~ msgid "Box"
#~ msgstr "Boîte"
# AI Translated
#~ msgid "Quick:"
#~ msgstr "Rapide :"
# AI Translated
#~ msgid "Width (X)"
#~ msgstr "Largeur (X)"
# AI Translated
#~ msgid "Depth (Y)"
#~ msgstr "Profondeur (Y)"
# AI Translated
#~ msgid "Height (Z)"
#~ msgstr "Hauteur (Z)"
# AI Translated
#~ msgid "Bottom R"
#~ msgstr "R inférieur"
# AI Translated
#~ msgid "Top R"
#~ msgstr "R supérieur"
# AI Translated
#~ msgid "Major R"
#~ msgstr "Grand R"
# AI Translated
#~ msgid "Minor R"
#~ msgstr "Petit R"
# AI Translated
#~ msgid "All edges"
#~ msgstr "Toutes les arêtes"
# AI Translated
#~ msgid "Top edges"
#~ msgstr "Arêtes supérieures"
# AI Translated
#~ msgid "Bottom edges"
#~ msgstr "Arêtes inférieures"
# AI Translated
#~ msgid "Lateral edges"
#~ msgstr "Arêtes latérales"
# AI Translated
#~ msgid "Mesh resolution"
#~ msgstr "Résolution du maillage"
# AI Translated
#~ msgid "Add Shape"
#~ msgstr "Ajouter la forme"
# AI Translated
#~ msgid "No outer profile"
#~ msgstr "Aucun profil extérieur"
# AI Translated
#~ msgid "Failed to make outer face"
#~ msgstr "Échec de la création de la face extérieure"
# AI Translated
#~ msgid "Failed to build face with holes"
#~ msgstr "Échec de la création de la face avec trous"
# AI Translated
#~ msgid "Revolve failed"
#~ msgstr "Échec de la révolution"
# AI Translated
#~ msgid "Profile"
#~ msgstr "Profil"
# AI Translated
#~ msgid "Start new profile (for holes)"
#~ msgstr "Commencer un nouveau profil (pour les trous)"
# AI Translated
#~ msgid "Segments"
#~ msgstr "Segments"
# AI Translated
#~ msgid "Sides"
#~ msgstr "Côtés"
# AI Translated
#~ msgid "Click on canvas to draw"
#~ msgstr "Cliquez dans la vue pour dessiner"
# AI Translated
#~ msgid "Snap to grid"
#~ msgstr "Aimanter à la grille"
# AI Translated
#~ msgid "Grid step"
#~ msgstr "Pas de la grille"
# AI Translated
#~ msgid "Outer"
#~ msgstr "Extérieur"
# AI Translated
#~ msgid "closed"
#~ msgstr "fermé"
# AI Translated
#, boost-format
#~ msgid "%1% %2%: %3% points %4%"
#~ msgstr "%1% %2% : %3% points %4%"
# AI Translated
#~ msgid "XY (Top)"
#~ msgstr "XY (Haut)"
# AI Translated
#~ msgid "XZ (Front)"
#~ msgstr "XZ (Avant)"
# AI Translated
#~ msgid "YZ (Side)"
#~ msgstr "YZ (Côté)"
# AI Translated
#~ msgid "Revolve deg"
#~ msgstr "Révolution (°)"
# AI Translated
#~ msgid "Set to 0 for extrude, >0 for revolve"
#~ msgstr "0 pour une extrusion, >0 pour une révolution"
# AI Translated
#~ msgid "Pocket (cut)"
#~ msgstr "Poche (enlèvement)"
# AI Translated
#, c-format
#~ msgid "%zu closed profile"
#~ msgid_plural "%zu closed profiles"
#~ msgstr[0] "%zu profil fermé"
#~ msgstr[1] "%zu profils fermés"
# AI Translated
#~ msgid "Draw a closed profile to enable"
#~ msgstr "Dessinez un profil fermé pour activer"
# AI Translated
#~ msgid "Clear all"
#~ msgstr "Tout effacer"
# AI Translated
#~ msgid "Empty result"
#~ msgstr "Résultat vide"
# AI Translated
#, boost-format
#~ msgid "Extrude: %1%"
#~ msgstr "Extrusion : %1%"
# AI Translated
#, boost-format
#~ msgid "Revolve: %1%"
#~ msgstr "Révolution : %1%"
# AI Translated
#~ msgid "No object selected"
#~ msgstr "Aucun objet sélectionné"
# AI Translated
#~ msgid "Face failed"
#~ msgstr "Échec de la face"
# AI Translated
#~ msgid "Face with holes failed"
#~ msgstr "Échec de la face avec trous"
# AI Translated
#~ msgid "Tool mesh empty"
#~ msgstr "Maillage de l’outil vide"
# AI Translated
#~ msgid "Pocket added (negative volume)"
#~ msgstr "Poche ajoutée (volume négatif)"
# AI Translated
#, boost-format
#~ msgid "Pocket: %1%"
#~ msgstr "Poche : %1%"
# AI Translated # AI Translated
#~ msgid "(Experimental) Use printer agents instead of print hosts" #~ msgid "(Experimental) Use printer agents instead of print hosts"
#~ msgstr "(Expérimental) Utiliser les agents d’imprimante au lieu des hôtes d’impression" #~ msgstr "(Expérimental) Utiliser les agents d’imprimante au lieu des hôtes d’impression"
@@ -31343,9 +31366,6 @@ msgstr ""
#~ msgid "An error occurred loading " #~ msgid "An error occurred loading "
#~ msgstr "Une erreur s’est produite lors du chargement de " #~ msgstr "Une erreur s’est produite lors du chargement de "
#~ msgid "/"
#~ msgstr "/"
# AI Translated # AI Translated
#, c-format, boost-format #, c-format, boost-format
#~ msgid "Load %s to " #~ msgid "Load %s to "
+234 -214
View File
@@ -3,7 +3,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: Orca Slicer\n" "Project-Id-Version: Orca Slicer\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-06 15:16-0300\n" "POT-Creation-Date: 2026-10-08 11:08+0800\n"
"Language: hu\n" "Language: hu\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
@@ -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." 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." 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 #, c-format, boost-format
msgid "Please input a valid value (K in %.1f~%.1f)" msgid "Please input a valid value (K in %.1f~%.1f)"
msgstr "Adj meg érvényes értéket (K tartomány: %.1f~%.1f)" msgstr "Adj meg érvényes értéket (K tartomány: %.1f~%.1f)"
@@ -6233,7 +6236,7 @@ msgid "Size:"
msgstr "Méret:" msgstr "Méret:"
# AI Translated # 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)." 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)." 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)."
@@ -7119,6 +7122,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." msgid "File '%s' was lost! Please download it again."
msgstr "A(z) '%s' fájl elveszett! Kérlek, töltsd le újra." 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 #, c-format, boost-format
msgid "" msgid ""
"File: %s\n" "File: %s\n"
@@ -11179,6 +11198,12 @@ msgstr "Timelapse megszakítása és nyomtatás"
msgid "Clean Up" msgid "Clean Up"
msgstr "Tisztítás" 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" msgid "Preparing print job"
msgstr "Nyomtatási feladat előkészítése" msgstr "Nyomtatási feladat előkészítése"
@@ -27000,214 +27025,6 @@ msgstr "Ennek a betűtípusnak nincs körvonala ehhez a szöveghez"
msgid "Size: %.1f × %.1f mm" msgid "Size: %.1f × %.1f mm"
msgstr "Méret: %.1f × %.1f mm" msgstr "Méret: %.1f × %.1f mm"
# AI Translated
msgid "Primitive"
msgstr "Primitív"
# AI Translated
msgid "Empty mesh generated"
msgstr "Üres háló jött létre"
# AI Translated
#. TRN Default name of an object created from the box primitive shape.
msgid "Box"
msgstr "Téglatest"
# AI Translated
#. TRN Primitive gizmo: label in front of the preset cube size buttons
msgid "Quick:"
msgstr "Gyors:"
# AI Translated
msgid "Width (X)"
msgstr "Szélesség (X)"
# AI Translated
msgid "Depth (Y)"
msgstr "Mélység (Y)"
# AI Translated
msgid "Height (Z)"
msgstr "Magasság (Z)"
# AI Translated
#. TRN Primitive gizmo: bottom radius of a cone ("Top R" is the top radius)
msgid "Bottom R"
msgstr "Alsó R"
# AI Translated
msgid "Top R"
msgstr "Felső R"
# AI Translated
#. TRN Primitive gizmo: major radius of a torus ("Minor R" is the tube radius)
msgid "Major R"
msgstr "Fő R"
# AI Translated
msgid "Minor R"
msgstr "Cső R"
# AI Translated
msgid "All edges"
msgstr "Minden él"
# AI Translated
msgid "Top edges"
msgstr "Felső élek"
# AI Translated
msgid "Bottom edges"
msgstr "Alsó élek"
# AI Translated
msgid "Lateral edges"
msgstr "Oldalélek"
# AI Translated
msgid "Mesh resolution"
msgstr "Hálófelbontás"
# AI Translated
msgid "Add Shape"
msgstr "Alakzat hozzáadása"
# AI Translated
msgid "No outer profile"
msgstr "Nincs külső profil"
# AI Translated
msgid "Failed to make outer face"
msgstr "Nem sikerült létrehozni a külső lapot"
# AI Translated
msgid "Failed to build face with holes"
msgstr "Nem sikerült létrehozni a lyukas lapot"
# AI Translated
msgid "Revolve failed"
msgstr "A körbeforgatás sikertelen"
# AI Translated
msgid "Profile"
msgstr "Profil"
# AI Translated
msgid "Start new profile (for holes)"
msgstr "Új profil kezdése (lyukakhoz)"
# AI Translated
msgid "Segments"
msgstr "Szakaszok"
# AI Translated
msgid "Sides"
msgstr "Oldalak"
# AI Translated
msgid "Click on canvas to draw"
msgstr "Kattints a vászonra a rajzoláshoz"
# AI Translated
msgid "Snap to grid"
msgstr "Rácshoz illesztés"
# AI Translated
msgid "Grid step"
msgstr "Rács lépésköze"
# AI Translated
msgid "Outer"
msgstr "Külső"
# AI Translated
msgid "closed"
msgstr "zárt"
# AI Translated
#. TRN Sketch gizmo profile list: "Outer" or "Hole", profile number, point count, "closed" or nothing
#, boost-format
msgid "%1% %2%: %3% points %4%"
msgstr "%1% %2%: %3% pont %4%"
# AI Translated
msgid "XY (Top)"
msgstr "XY (felülnézet)"
# AI Translated
msgid "XZ (Front)"
msgstr "XZ (elölnézet)"
# AI Translated
msgid "YZ (Side)"
msgstr "YZ (oldalnézet)"
# AI Translated
msgid "Revolve deg"
msgstr "Körbeforgatás (°)"
# AI Translated
msgid "Set to 0 for extrude, >0 for revolve"
msgstr "0: extrudálás, >0: körbeforgatás"
# AI Translated
msgid "Pocket (cut)"
msgstr "Zseb (vágás)"
# AI Translated
#, c-format
msgid "%zu closed profile"
msgid_plural "%zu closed profiles"
msgstr[0] "%zu zárt profil"
msgstr[1] "%zu zárt profil"
# AI Translated
msgid "Draw a closed profile to enable"
msgstr "Az engedélyezéshez rajzolj egy zárt profilt"
# AI Translated
msgid "Clear all"
msgstr "Összes törlése"
# AI Translated
msgid "Empty result"
msgstr "Üres eredmény"
# AI Translated
#, boost-format
msgid "Extrude: %1%"
msgstr "Extrudálás: %1%"
# AI Translated
#, boost-format
msgid "Revolve: %1%"
msgstr "Körbeforgatás: %1%"
# AI Translated
msgid "No object selected"
msgstr "Nincs kijelölt objektum"
# AI Translated
msgid "Face failed"
msgstr "A lap létrehozása sikertelen"
# AI Translated
msgid "Face with holes failed"
msgstr "A lyukas lap létrehozása sikertelen"
# AI Translated
msgid "Tool mesh empty"
msgstr "A szerszámháló üres"
# AI Translated
msgid "Pocket added (negative volume)"
msgstr "Zseb hozzáadva (negatív térfogat)"
# AI Translated
#, boost-format
msgid "Pocket: %1%"
msgstr "Zseb: %1%"
# AI Translated # AI Translated
msgctxt "Keyboard Shortcut" msgctxt "Keyboard Shortcut"
msgid "Ins" msgid "Ins"
@@ -30600,6 +30417,10 @@ msgstr "Fejlesztői eszközök engedélyezése"
msgid "An error occurred loading %s" msgid "An error occurred loading %s"
msgstr "Hiba történt a(z) %s betöltésekor" msgstr "Hiba történt a(z) %s betöltésekor"
# AI Translated
msgid "/"
msgstr "/"
# AI Translated # AI Translated
#, boost-format #, boost-format
msgid "Load %1% to %2%" msgid "Load %1% to %2%"
@@ -31281,6 +31102,209 @@ msgstr ""
"Kunkorodás elkerülése\n" "Kunkorodás elkerülése\n"
"Tudtad, hogy a kunkorodásra hajlamos anyagok (például ABS) nyomtatásakor az asztal hőmérsékletének növelése csökkentheti a kunkorodás valószínűségét?" "Tudtad, hogy a kunkorodásra hajlamos anyagok (például ABS) nyomtatásakor az asztal hőmérsékletének növelése csökkentheti a kunkorodás valószínűségét?"
# AI Translated
#~ msgid "Primitive"
#~ msgstr "Primitív"
# AI Translated
#~ msgid "Empty mesh generated"
#~ msgstr "Üres háló jött létre"
# AI Translated
#~ msgid "Box"
#~ msgstr "Téglatest"
# AI Translated
#~ msgid "Quick:"
#~ msgstr "Gyors:"
# AI Translated
#~ msgid "Width (X)"
#~ msgstr "Szélesség (X)"
# AI Translated
#~ msgid "Depth (Y)"
#~ msgstr "Mélység (Y)"
# AI Translated
#~ msgid "Height (Z)"
#~ msgstr "Magasság (Z)"
# AI Translated
#~ msgid "Bottom R"
#~ msgstr "Alsó R"
# AI Translated
#~ msgid "Top R"
#~ msgstr "Felső R"
# AI Translated
#~ msgid "Major R"
#~ msgstr "Fő R"
# AI Translated
#~ msgid "Minor R"
#~ msgstr "Cső R"
# AI Translated
#~ msgid "All edges"
#~ msgstr "Minden él"
# AI Translated
#~ msgid "Top edges"
#~ msgstr "Felső élek"
# AI Translated
#~ msgid "Bottom edges"
#~ msgstr "Alsó élek"
# AI Translated
#~ msgid "Lateral edges"
#~ msgstr "Oldalélek"
# AI Translated
#~ msgid "Mesh resolution"
#~ msgstr "Hálófelbontás"
# AI Translated
#~ msgid "Add Shape"
#~ msgstr "Alakzat hozzáadása"
# AI Translated
#~ msgid "No outer profile"
#~ msgstr "Nincs külső profil"
# AI Translated
#~ msgid "Failed to make outer face"
#~ msgstr "Nem sikerült létrehozni a külső lapot"
# AI Translated
#~ msgid "Failed to build face with holes"
#~ msgstr "Nem sikerült létrehozni a lyukas lapot"
# AI Translated
#~ msgid "Revolve failed"
#~ msgstr "A körbeforgatás sikertelen"
# AI Translated
#~ msgid "Profile"
#~ msgstr "Profil"
# AI Translated
#~ msgid "Start new profile (for holes)"
#~ msgstr "Új profil kezdése (lyukakhoz)"
# AI Translated
#~ msgid "Segments"
#~ msgstr "Szakaszok"
# AI Translated
#~ msgid "Sides"
#~ msgstr "Oldalak"
# AI Translated
#~ msgid "Click on canvas to draw"
#~ msgstr "Kattints a vászonra a rajzoláshoz"
# AI Translated
#~ msgid "Snap to grid"
#~ msgstr "Rácshoz illesztés"
# AI Translated
#~ msgid "Grid step"
#~ msgstr "Rács lépésköze"
# AI Translated
#~ msgid "Outer"
#~ msgstr "Külső"
# AI Translated
#~ msgid "closed"
#~ msgstr "zárt"
# AI Translated
#, boost-format
#~ msgid "%1% %2%: %3% points %4%"
#~ msgstr "%1% %2%: %3% pont %4%"
# AI Translated
#~ msgid "XY (Top)"
#~ msgstr "XY (felülnézet)"
# AI Translated
#~ msgid "XZ (Front)"
#~ msgstr "XZ (elölnézet)"
# AI Translated
#~ msgid "YZ (Side)"
#~ msgstr "YZ (oldalnézet)"
# AI Translated
#~ msgid "Revolve deg"
#~ msgstr "Körbeforgatás (°)"
# AI Translated
#~ msgid "Set to 0 for extrude, >0 for revolve"
#~ msgstr "0: extrudálás, >0: körbeforgatás"
# AI Translated
#~ msgid "Pocket (cut)"
#~ msgstr "Zseb (vágás)"
# AI Translated
#, c-format
#~ msgid "%zu closed profile"
#~ msgid_plural "%zu closed profiles"
#~ msgstr[0] "%zu zárt profil"
#~ msgstr[1] "%zu zárt profil"
# AI Translated
#~ msgid "Draw a closed profile to enable"
#~ msgstr "Az engedélyezéshez rajzolj egy zárt profilt"
# AI Translated
#~ msgid "Clear all"
#~ msgstr "Összes törlése"
# AI Translated
#~ msgid "Empty result"
#~ msgstr "Üres eredmény"
# AI Translated
#, boost-format
#~ msgid "Extrude: %1%"
#~ msgstr "Extrudálás: %1%"
# AI Translated
#, boost-format
#~ msgid "Revolve: %1%"
#~ msgstr "Körbeforgatás: %1%"
# AI Translated
#~ msgid "No object selected"
#~ msgstr "Nincs kijelölt objektum"
# AI Translated
#~ msgid "Face failed"
#~ msgstr "A lap létrehozása sikertelen"
# AI Translated
#~ msgid "Face with holes failed"
#~ msgstr "A lyukas lap létrehozása sikertelen"
# AI Translated
#~ msgid "Tool mesh empty"
#~ msgstr "A szerszámháló üres"
# AI Translated
#~ msgid "Pocket added (negative volume)"
#~ msgstr "Zseb hozzáadva (negatív térfogat)"
# AI Translated
#, boost-format
#~ msgid "Pocket: %1%"
#~ msgstr "Zseb: %1%"
# AI Translated # AI Translated
#~ msgid "(Experimental) Use printer agents instead of print hosts" #~ msgid "(Experimental) Use printer agents instead of print hosts"
#~ msgstr "(Kísérleti) Nyomtatóügynökök használata nyomtatókiszolgálók helyett" #~ msgstr "(Kísérleti) Nyomtatóügynökök használata nyomtatókiszolgálók helyett"
@@ -32336,10 +32360,6 @@ msgstr ""
#~ msgid "An error occurred loading " #~ msgid "An error occurred loading "
#~ msgstr "Hiba történt a betöltés során: " #~ msgstr "Hiba történt a betöltés során: "
# AI Translated
#~ msgid "/"
#~ msgstr "/"
# AI Translated # AI Translated
#, c-format, boost-format #, c-format, boost-format
#~ msgid "Load %s to " #~ msgid "Load %s to "
+233 -213
View File
@@ -3,7 +3,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: Orca Slicer\n" "Project-Id-Version: Orca Slicer\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-06 15:16-0300\n" "POT-Creation-Date: 2026-10-08 11:08+0800\n"
"PO-Revision-Date: \n" "PO-Revision-Date: \n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: \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." msgid "You need to select the material type and color first."
msgstr "Devi prima selezionare il tipo e il colore del materiale." 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 #, c-format, boost-format
msgid "Please input a valid value (K in %.1f~%.1f)" msgid "Please input a valid value (K in %.1f~%.1f)"
msgstr "Si prega di immettere un valore valido (K in %.1f~%.1f)" msgstr "Si prega di immettere un valore valido (K in %.1f~%.1f)"
@@ -6206,7 +6209,7 @@ msgstr "Volume:"
msgid "Size:" msgid "Size:"
msgstr "Dimensione:" 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)." 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)." 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)."
@@ -7083,6 +7086,22 @@ msgstr "Il file .gcode.3mf non contiene dati G-code. Elaboralo con OrcaSlicer ed
msgid "File '%s' was lost! Please download it again." msgid "File '%s' was lost! Please download it again."
msgstr "Il file '%s' è stato perso! Si prega di scaricarlo di nuovo." 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 #, c-format, boost-format
msgid "" msgid ""
"File: %s\n" "File: %s\n"
@@ -11138,6 +11157,12 @@ msgstr "Annulla timelapse e stampa"
msgid "Clean Up" msgid "Clean Up"
msgstr "Pulisci" 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" msgid "Preparing print job"
msgstr "Preparazione della stampa" msgstr "Preparazione della stampa"
@@ -26874,214 +26899,6 @@ msgstr "Questo carattere non ha un contorno per questo testo"
msgid "Size: %.1f × %.1f mm" msgid "Size: %.1f × %.1f mm"
msgstr "Dimensioni: %.1f × %.1f mm" msgstr "Dimensioni: %.1f × %.1f mm"
# AI Translated
msgid "Primitive"
msgstr "Primitiva"
# AI Translated
msgid "Empty mesh generated"
msgstr "Generata una mesh vuota"
# AI Translated
#. TRN Default name of an object created from the box primitive shape.
msgid "Box"
msgstr "Parallelepipedo"
# AI Translated
#. TRN Primitive gizmo: label in front of the preset cube size buttons
msgid "Quick:"
msgstr "Rapido:"
# AI Translated
msgid "Width (X)"
msgstr "Larghezza (X)"
# AI Translated
msgid "Depth (Y)"
msgstr "Profondità (Y)"
# AI Translated
msgid "Height (Z)"
msgstr "Altezza (Z)"
# AI Translated
#. TRN Primitive gizmo: bottom radius of a cone ("Top R" is the top radius)
msgid "Bottom R"
msgstr "R inferiore"
# AI Translated
msgid "Top R"
msgstr "R superiore"
# AI Translated
#. TRN Primitive gizmo: major radius of a torus ("Minor R" is the tube radius)
msgid "Major R"
msgstr "R maggiore"
# AI Translated
msgid "Minor R"
msgstr "R minore"
# AI Translated
msgid "All edges"
msgstr "Tutti gli spigoli"
# AI Translated
msgid "Top edges"
msgstr "Spigoli superiori"
# AI Translated
msgid "Bottom edges"
msgstr "Spigoli inferiori"
# AI Translated
msgid "Lateral edges"
msgstr "Spigoli laterali"
# AI Translated
msgid "Mesh resolution"
msgstr "Risoluzione mesh"
# AI Translated
msgid "Add Shape"
msgstr "Aggiungi forma"
# AI Translated
msgid "No outer profile"
msgstr "Nessun profilo esterno"
# AI Translated
msgid "Failed to make outer face"
msgstr "Impossibile creare la faccia esterna"
# AI Translated
msgid "Failed to build face with holes"
msgstr "Impossibile creare la faccia con i fori"
# AI Translated
msgid "Revolve failed"
msgstr "Rivoluzione non riuscita"
# AI Translated
msgid "Profile"
msgstr "Profilo"
# AI Translated
msgid "Start new profile (for holes)"
msgstr "Inizia un nuovo profilo (per i fori)"
# AI Translated
msgid "Segments"
msgstr "Segmenti"
# AI Translated
msgid "Sides"
msgstr "Lati"
# AI Translated
msgid "Click on canvas to draw"
msgstr "Clicca nell'area di disegno per disegnare"
# AI Translated
msgid "Snap to grid"
msgstr "Aggancia alla griglia"
# AI Translated
msgid "Grid step"
msgstr "Passo griglia"
# AI Translated
msgid "Outer"
msgstr "Esterno"
# AI Translated
msgid "closed"
msgstr "chiuso"
# AI Translated
#. TRN Sketch gizmo profile list: "Outer" or "Hole", profile number, point count, "closed" or nothing
#, boost-format
msgid "%1% %2%: %3% points %4%"
msgstr "%1% %2%: %3% punti %4%"
# AI Translated
msgid "XY (Top)"
msgstr "XY (Superiore)"
# AI Translated
msgid "XZ (Front)"
msgstr "XZ (Frontale)"
# AI Translated
msgid "YZ (Side)"
msgstr "YZ (Laterale)"
# AI Translated
msgid "Revolve deg"
msgstr "Gradi rivoluzione"
# AI Translated
msgid "Set to 0 for extrude, >0 for revolve"
msgstr "Imposta 0 per estrudere, >0 per rivoluzionare"
# AI Translated
msgid "Pocket (cut)"
msgstr "Tasca (taglio)"
# AI Translated
#, c-format
msgid "%zu closed profile"
msgid_plural "%zu closed profiles"
msgstr[0] "%zu profilo chiuso"
msgstr[1] "%zu profili chiusi"
# AI Translated
msgid "Draw a closed profile to enable"
msgstr "Disegna un profilo chiuso per abilitare"
# AI Translated
msgid "Clear all"
msgstr "Cancella tutto"
# AI Translated
msgid "Empty result"
msgstr "Risultato vuoto"
# AI Translated
#, boost-format
msgid "Extrude: %1%"
msgstr "Estrusione: %1%"
# AI Translated
#, boost-format
msgid "Revolve: %1%"
msgstr "Rivoluzione: %1%"
# AI Translated
msgid "No object selected"
msgstr "Nessun oggetto selezionato"
# AI Translated
msgid "Face failed"
msgstr "Creazione della faccia non riuscita"
# AI Translated
msgid "Face with holes failed"
msgstr "Creazione della faccia con fori non riuscita"
# AI Translated
msgid "Tool mesh empty"
msgstr "Mesh dell'utensile vuota"
# AI Translated
msgid "Pocket added (negative volume)"
msgstr "Tasca aggiunta (volume negativo)"
# AI Translated
#, boost-format
msgid "Pocket: %1%"
msgstr "Tasca: %1%"
# AI Translated # AI Translated
msgctxt "Keyboard Shortcut" msgctxt "Keyboard Shortcut"
msgid "Ins" msgid "Ins"
@@ -30464,6 +30281,9 @@ msgstr "Abilita strumenti per sviluppatori"
msgid "An error occurred loading %s" msgid "An error occurred loading %s"
msgstr "Si è verificato un errore durante il caricamento di %s" msgstr "Si è verificato un errore durante il caricamento di %s"
msgid "/"
msgstr "/"
# AI Translated # AI Translated
#, boost-format #, boost-format
msgid "Load %1% to %2%" msgid "Load %1% to %2%"
@@ -31144,6 +30964,209 @@ msgstr ""
"Evita le deformazioni\n" "Evita le deformazioni\n"
"Sapevi che quando si stampano materiali soggetti a deformazioni come l'ABS, aumentare in modo appropriato la temperatura del piano riscaldato può ridurre la probabilità di deformazione?" "Sapevi che quando si stampano materiali soggetti a deformazioni come l'ABS, aumentare in modo appropriato la temperatura del piano riscaldato può ridurre la probabilità di deformazione?"
# AI Translated
#~ msgid "Primitive"
#~ msgstr "Primitiva"
# AI Translated
#~ msgid "Empty mesh generated"
#~ msgstr "Generata una mesh vuota"
# AI Translated
#~ msgid "Box"
#~ msgstr "Parallelepipedo"
# AI Translated
#~ msgid "Quick:"
#~ msgstr "Rapido:"
# AI Translated
#~ msgid "Width (X)"
#~ msgstr "Larghezza (X)"
# AI Translated
#~ msgid "Depth (Y)"
#~ msgstr "Profondità (Y)"
# AI Translated
#~ msgid "Height (Z)"
#~ msgstr "Altezza (Z)"
# AI Translated
#~ msgid "Bottom R"
#~ msgstr "R inferiore"
# AI Translated
#~ msgid "Top R"
#~ msgstr "R superiore"
# AI Translated
#~ msgid "Major R"
#~ msgstr "R maggiore"
# AI Translated
#~ msgid "Minor R"
#~ msgstr "R minore"
# AI Translated
#~ msgid "All edges"
#~ msgstr "Tutti gli spigoli"
# AI Translated
#~ msgid "Top edges"
#~ msgstr "Spigoli superiori"
# AI Translated
#~ msgid "Bottom edges"
#~ msgstr "Spigoli inferiori"
# AI Translated
#~ msgid "Lateral edges"
#~ msgstr "Spigoli laterali"
# AI Translated
#~ msgid "Mesh resolution"
#~ msgstr "Risoluzione mesh"
# AI Translated
#~ msgid "Add Shape"
#~ msgstr "Aggiungi forma"
# AI Translated
#~ msgid "No outer profile"
#~ msgstr "Nessun profilo esterno"
# AI Translated
#~ msgid "Failed to make outer face"
#~ msgstr "Impossibile creare la faccia esterna"
# AI Translated
#~ msgid "Failed to build face with holes"
#~ msgstr "Impossibile creare la faccia con i fori"
# AI Translated
#~ msgid "Revolve failed"
#~ msgstr "Rivoluzione non riuscita"
# AI Translated
#~ msgid "Profile"
#~ msgstr "Profilo"
# AI Translated
#~ msgid "Start new profile (for holes)"
#~ msgstr "Inizia un nuovo profilo (per i fori)"
# AI Translated
#~ msgid "Segments"
#~ msgstr "Segmenti"
# AI Translated
#~ msgid "Sides"
#~ msgstr "Lati"
# AI Translated
#~ msgid "Click on canvas to draw"
#~ msgstr "Clicca nell'area di disegno per disegnare"
# AI Translated
#~ msgid "Snap to grid"
#~ msgstr "Aggancia alla griglia"
# AI Translated
#~ msgid "Grid step"
#~ msgstr "Passo griglia"
# AI Translated
#~ msgid "Outer"
#~ msgstr "Esterno"
# AI Translated
#~ msgid "closed"
#~ msgstr "chiuso"
# AI Translated
#, boost-format
#~ msgid "%1% %2%: %3% points %4%"
#~ msgstr "%1% %2%: %3% punti %4%"
# AI Translated
#~ msgid "XY (Top)"
#~ msgstr "XY (Superiore)"
# AI Translated
#~ msgid "XZ (Front)"
#~ msgstr "XZ (Frontale)"
# AI Translated
#~ msgid "YZ (Side)"
#~ msgstr "YZ (Laterale)"
# AI Translated
#~ msgid "Revolve deg"
#~ msgstr "Gradi rivoluzione"
# AI Translated
#~ msgid "Set to 0 for extrude, >0 for revolve"
#~ msgstr "Imposta 0 per estrudere, >0 per rivoluzionare"
# AI Translated
#~ msgid "Pocket (cut)"
#~ msgstr "Tasca (taglio)"
# AI Translated
#, c-format
#~ msgid "%zu closed profile"
#~ msgid_plural "%zu closed profiles"
#~ msgstr[0] "%zu profilo chiuso"
#~ msgstr[1] "%zu profili chiusi"
# AI Translated
#~ msgid "Draw a closed profile to enable"
#~ msgstr "Disegna un profilo chiuso per abilitare"
# AI Translated
#~ msgid "Clear all"
#~ msgstr "Cancella tutto"
# AI Translated
#~ msgid "Empty result"
#~ msgstr "Risultato vuoto"
# AI Translated
#, boost-format
#~ msgid "Extrude: %1%"
#~ msgstr "Estrusione: %1%"
# AI Translated
#, boost-format
#~ msgid "Revolve: %1%"
#~ msgstr "Rivoluzione: %1%"
# AI Translated
#~ msgid "No object selected"
#~ msgstr "Nessun oggetto selezionato"
# AI Translated
#~ msgid "Face failed"
#~ msgstr "Creazione della faccia non riuscita"
# AI Translated
#~ msgid "Face with holes failed"
#~ msgstr "Creazione della faccia con fori non riuscita"
# AI Translated
#~ msgid "Tool mesh empty"
#~ msgstr "Mesh dell'utensile vuota"
# AI Translated
#~ msgid "Pocket added (negative volume)"
#~ msgstr "Tasca aggiunta (volume negativo)"
# AI Translated
#, boost-format
#~ msgid "Pocket: %1%"
#~ msgstr "Tasca: %1%"
# AI Translated # AI Translated
#~ msgid "(Experimental) Use printer agents instead of print hosts" #~ msgid "(Experimental) Use printer agents instead of print hosts"
#~ msgstr "(Sperimentale) Usa gli agenti stampante invece degli host di stampa" #~ msgstr "(Sperimentale) Usa gli agenti stampante invece degli host di stampa"
@@ -32200,9 +32223,6 @@ msgstr ""
#~ msgid "An error occurred loading " #~ msgid "An error occurred loading "
#~ msgstr "Si è verificato un errore durante il caricamento di " #~ msgstr "Si è verificato un errore durante il caricamento di "
#~ msgid "/"
#~ msgstr "/"
# AI Translated # AI Translated
#, c-format, boost-format #, c-format, boost-format
#~ msgid "Load %s to " #~ msgid "Load %s to "
+232 -212
View File
@@ -3,7 +3,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: Orca Slicer\n" "Project-Id-Version: Orca Slicer\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-06 15:16-0300\n" "POT-Creation-Date: 2026-10-08 11:08+0800\n"
"PO-Revision-Date: \n" "PO-Revision-Date: \n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: \n" "Language-Team: \n"
@@ -4352,6 +4352,9 @@ msgstr "フィラメント情報をクリアしてもよろしいですか?"
msgid "You need to select the material type and color first." msgid "You need to select the material type and color first."
msgstr "まず材料タイプと色を選択してください。" msgstr "まず材料タイプと色を選択してください。"
msgid "The minimum temperature cannot be greater than the maximum temperature."
msgstr ""
#, c-format, boost-format #, c-format, boost-format
msgid "Please input a valid value (K in %.1f~%.1f)" msgid "Please input a valid value (K in %.1f~%.1f)"
msgstr "有効な値を入力してください(Kは%.1f~%.1f)" msgstr "有効な値を入力してください(Kは%.1f~%.1f)"
@@ -6230,7 +6233,7 @@ msgstr "ボリューム"
msgid "Size:" msgid "Size:"
msgstr "サイズ:" 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)." 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)。" msgstr "レイヤー%d、Z = %.2lfmmでG-codeパスの衝突が検出されました。衝突するオブジェクトをもっと離してください(%s <-> %s)。"
@@ -7099,6 +7102,22 @@ msgstr ".gcode.3mfファイルにG-codeデータが含まれていません。Or
msgid "File '%s' was lost! Please download it again." msgid "File '%s' was lost! Please download it again."
msgstr "ファイル %s が見つかりませんでした、もう一度ダウンロードしてください。" 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 #, c-format, boost-format
msgid "" msgid ""
"File: %s\n" "File: %s\n"
@@ -11171,6 +11190,12 @@ msgstr "タイムラプスを中止して印刷"
msgid "Clean Up" msgid "Clean Up"
msgstr "整理する" 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" msgid "Preparing print job"
msgstr "造形タスクを準備" msgstr "造形タスクを準備"
@@ -27395,213 +27420,6 @@ msgstr "このフォントにはこのテキストのアウトラインがあり
msgid "Size: %.1f × %.1f mm" msgid "Size: %.1f × %.1f mm"
msgstr "サイズ: %.1f × %.1f mm" msgstr "サイズ: %.1f × %.1f mm"
# AI Translated
msgid "Primitive"
msgstr "プリミティブ"
# AI Translated
msgid "Empty mesh generated"
msgstr "空のメッシュが生成されました"
# AI Translated
#. TRN Default name of an object created from the box primitive shape.
msgid "Box"
msgstr "ボックス"
# AI Translated
#. TRN Primitive gizmo: label in front of the preset cube size buttons
msgid "Quick:"
msgstr "クイック:"
# AI Translated
msgid "Width (X)"
msgstr "幅 (X)"
# AI Translated
msgid "Depth (Y)"
msgstr "奥行き (Y)"
# AI Translated
msgid "Height (Z)"
msgstr "高さ (Z)"
# AI Translated
#. TRN Primitive gizmo: bottom radius of a cone ("Top R" is the top radius)
msgid "Bottom R"
msgstr "底面半径"
# AI Translated
msgid "Top R"
msgstr "上面半径"
# AI Translated
#. TRN Primitive gizmo: major radius of a torus ("Minor R" is the tube radius)
msgid "Major R"
msgstr "大半径"
# AI Translated
msgid "Minor R"
msgstr "小半径"
# AI Translated
msgid "All edges"
msgstr "すべてのエッジ"
# AI Translated
msgid "Top edges"
msgstr "上面のエッジ"
# AI Translated
msgid "Bottom edges"
msgstr "底面のエッジ"
# AI Translated
msgid "Lateral edges"
msgstr "側面のエッジ"
# AI Translated
msgid "Mesh resolution"
msgstr "メッシュ分解能"
# AI Translated
msgid "Add Shape"
msgstr "形状を追加"
# AI Translated
msgid "No outer profile"
msgstr "外側のプロファイルがありません"
# AI Translated
msgid "Failed to make outer face"
msgstr "外側の面を作成できませんでした"
# AI Translated
msgid "Failed to build face with holes"
msgstr "穴のある面を作成できませんでした"
# AI Translated
msgid "Revolve failed"
msgstr "回転押し出しに失敗しました"
# AI Translated
msgid "Profile"
msgstr "プロファイル"
# AI Translated
msgid "Start new profile (for holes)"
msgstr "新しいプロファイルを開始 (穴用)"
# AI Translated
msgid "Segments"
msgstr "セグメント数"
# AI Translated
msgid "Sides"
msgstr "辺数"
# AI Translated
msgid "Click on canvas to draw"
msgstr "キャンバスをクリックして描画"
# AI Translated
msgid "Snap to grid"
msgstr "グリッドにスナップ"
# AI Translated
msgid "Grid step"
msgstr "グリッド間隔"
# AI Translated
msgid "Outer"
msgstr "外側"
# AI Translated
msgid "closed"
msgstr "閉じています"
# AI Translated
#. TRN Sketch gizmo profile list: "Outer" or "Hole", profile number, point count, "closed" or nothing
#, boost-format
msgid "%1% %2%: %3% points %4%"
msgstr "%1% %2%: %3% 点 %4%"
# AI Translated
msgid "XY (Top)"
msgstr "XY (上面)"
# AI Translated
msgid "XZ (Front)"
msgstr "XZ (正面)"
# AI Translated
msgid "YZ (Side)"
msgstr "YZ (側面)"
# AI Translated
msgid "Revolve deg"
msgstr "回転押し出し角度"
# AI Translated
msgid "Set to 0 for extrude, >0 for revolve"
msgstr "0で押し出し、0より大きい値で回転押し出し"
# AI Translated
msgid "Pocket (cut)"
msgstr "ポケット (カット)"
# AI Translated
#, c-format
msgid "%zu closed profile"
msgid_plural "%zu closed profiles"
msgstr[0] "%zu個の閉じたプロファイル"
# AI Translated
msgid "Draw a closed profile to enable"
msgstr "有効にするには閉じたプロファイルを描画してください"
# AI Translated
msgid "Clear all"
msgstr "すべてクリア"
# AI Translated
msgid "Empty result"
msgstr "結果が空です"
# AI Translated
#, boost-format
msgid "Extrude: %1%"
msgstr "押し出し: %1%"
# AI Translated
#, boost-format
msgid "Revolve: %1%"
msgstr "回転押し出し: %1%"
# AI Translated
msgid "No object selected"
msgstr "オブジェクトが選択されていません"
# AI Translated
msgid "Face failed"
msgstr "面の作成に失敗しました"
# AI Translated
msgid "Face with holes failed"
msgstr "穴のある面の作成に失敗しました"
# AI Translated
msgid "Tool mesh empty"
msgstr "ツールメッシュが空です"
# AI Translated
msgid "Pocket added (negative volume)"
msgstr "ポケットを追加しました (マイナスボリューム)"
# AI Translated
#, boost-format
msgid "Pocket: %1%"
msgstr "ポケット: %1%"
# AI Translated # AI Translated
msgctxt "Keyboard Shortcut" msgctxt "Keyboard Shortcut"
msgid "Ins" msgid "Ins"
@@ -30986,6 +30804,9 @@ msgstr "開発者ツールを有効化"
msgid "An error occurred loading %s" msgid "An error occurred loading %s"
msgstr "%s の読み込み中にエラーが発生しました" msgstr "%s の読み込み中にエラーが発生しました"
msgid "/"
msgstr "/"
# AI Translated # AI Translated
#, boost-format #, boost-format
msgid "Load %1% to %2%" msgid "Load %1% to %2%"
@@ -31677,6 +31498,208 @@ msgstr ""
"反りを避ける\n" "反りを避ける\n"
"ABSのような反りやすい素材を印刷する場合、ヒートベッドの温度を適切に上げることで、反りが発生する確率を下げることができることをご存知ですか?" "ABSのような反りやすい素材を印刷する場合、ヒートベッドの温度を適切に上げることで、反りが発生する確率を下げることができることをご存知ですか?"
# AI Translated
#~ msgid "Primitive"
#~ msgstr "プリミティブ"
# AI Translated
#~ msgid "Empty mesh generated"
#~ msgstr "空のメッシュが生成されました"
# AI Translated
#~ msgid "Box"
#~ msgstr "ボックス"
# AI Translated
#~ msgid "Quick:"
#~ msgstr "クイック:"
# AI Translated
#~ msgid "Width (X)"
#~ msgstr "幅 (X)"
# AI Translated
#~ msgid "Depth (Y)"
#~ msgstr "奥行き (Y)"
# AI Translated
#~ msgid "Height (Z)"
#~ msgstr "高さ (Z)"
# AI Translated
#~ msgid "Bottom R"
#~ msgstr "底面半径"
# AI Translated
#~ msgid "Top R"
#~ msgstr "上面半径"
# AI Translated
#~ msgid "Major R"
#~ msgstr "大半径"
# AI Translated
#~ msgid "Minor R"
#~ msgstr "小半径"
# AI Translated
#~ msgid "All edges"
#~ msgstr "すべてのエッジ"
# AI Translated
#~ msgid "Top edges"
#~ msgstr "上面のエッジ"
# AI Translated
#~ msgid "Bottom edges"
#~ msgstr "底面のエッジ"
# AI Translated
#~ msgid "Lateral edges"
#~ msgstr "側面のエッジ"
# AI Translated
#~ msgid "Mesh resolution"
#~ msgstr "メッシュ分解能"
# AI Translated
#~ msgid "Add Shape"
#~ msgstr "形状を追加"
# AI Translated
#~ msgid "No outer profile"
#~ msgstr "外側のプロファイルがありません"
# AI Translated
#~ msgid "Failed to make outer face"
#~ msgstr "外側の面を作成できませんでした"
# AI Translated
#~ msgid "Failed to build face with holes"
#~ msgstr "穴のある面を作成できませんでした"
# AI Translated
#~ msgid "Revolve failed"
#~ msgstr "回転押し出しに失敗しました"
# AI Translated
#~ msgid "Profile"
#~ msgstr "プロファイル"
# AI Translated
#~ msgid "Start new profile (for holes)"
#~ msgstr "新しいプロファイルを開始 (穴用)"
# AI Translated
#~ msgid "Segments"
#~ msgstr "セグメント数"
# AI Translated
#~ msgid "Sides"
#~ msgstr "辺数"
# AI Translated
#~ msgid "Click on canvas to draw"
#~ msgstr "キャンバスをクリックして描画"
# AI Translated
#~ msgid "Snap to grid"
#~ msgstr "グリッドにスナップ"
# AI Translated
#~ msgid "Grid step"
#~ msgstr "グリッド間隔"
# AI Translated
#~ msgid "Outer"
#~ msgstr "外側"
# AI Translated
#~ msgid "closed"
#~ msgstr "閉じています"
# AI Translated
#, boost-format
#~ msgid "%1% %2%: %3% points %4%"
#~ msgstr "%1% %2%: %3% 点 %4%"
# AI Translated
#~ msgid "XY (Top)"
#~ msgstr "XY (上面)"
# AI Translated
#~ msgid "XZ (Front)"
#~ msgstr "XZ (正面)"
# AI Translated
#~ msgid "YZ (Side)"
#~ msgstr "YZ (側面)"
# AI Translated
#~ msgid "Revolve deg"
#~ msgstr "回転押し出し角度"
# AI Translated
#~ msgid "Set to 0 for extrude, >0 for revolve"
#~ msgstr "0で押し出し、0より大きい値で回転押し出し"
# AI Translated
#~ msgid "Pocket (cut)"
#~ msgstr "ポケット (カット)"
# AI Translated
#, c-format
#~ msgid "%zu closed profile"
#~ msgid_plural "%zu closed profiles"
#~ msgstr[0] "%zu個の閉じたプロファイル"
# AI Translated
#~ msgid "Draw a closed profile to enable"
#~ msgstr "有効にするには閉じたプロファイルを描画してください"
# AI Translated
#~ msgid "Clear all"
#~ msgstr "すべてクリア"
# AI Translated
#~ msgid "Empty result"
#~ msgstr "結果が空です"
# AI Translated
#, boost-format
#~ msgid "Extrude: %1%"
#~ msgstr "押し出し: %1%"
# AI Translated
#, boost-format
#~ msgid "Revolve: %1%"
#~ msgstr "回転押し出し: %1%"
# AI Translated
#~ msgid "No object selected"
#~ msgstr "オブジェクトが選択されていません"
# AI Translated
#~ msgid "Face failed"
#~ msgstr "面の作成に失敗しました"
# AI Translated
#~ msgid "Face with holes failed"
#~ msgstr "穴のある面の作成に失敗しました"
# AI Translated
#~ msgid "Tool mesh empty"
#~ msgstr "ツールメッシュが空です"
# AI Translated
#~ msgid "Pocket added (negative volume)"
#~ msgstr "ポケットを追加しました (マイナスボリューム)"
# AI Translated
#, boost-format
#~ msgid "Pocket: %1%"
#~ msgstr "ポケット: %1%"
# AI Translated # AI Translated
#~ msgid "(Experimental) Use printer agents instead of print hosts" #~ msgid "(Experimental) Use printer agents instead of print hosts"
#~ msgstr "(実験的) プリントホストの代わりにプリンターエージェントを使用" #~ msgstr "(実験的) プリントホストの代わりにプリンターエージェントを使用"
@@ -32759,9 +32782,6 @@ msgstr ""
#~ msgid "An error occurred loading " #~ msgid "An error occurred loading "
#~ msgstr "読み込み中にエラーが発生しました: " #~ msgstr "読み込み中にエラーが発生しました: "
#~ msgid "/"
#~ msgstr "/"
# AI Translated # AI Translated
#, c-format, boost-format #, c-format, boost-format
#~ msgid "Load %s to " #~ msgid "Load %s to "
+232 -212
View File
@@ -7,7 +7,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: Orca Slicer\n" "Project-Id-Version: Orca Slicer\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-06 15:16-0300\n" "POT-Creation-Date: 2026-10-08 11:08+0800\n"
"PO-Revision-Date: 2025-06-02 17:12+0900\n" "PO-Revision-Date: 2025-06-02 17:12+0900\n"
"Last-Translator: crwusiz <crwusiz@gmail.com>\n" "Last-Translator: crwusiz <crwusiz@gmail.com>\n"
"Language-Team: \n" "Language-Team: \n"
@@ -4377,6 +4377,9 @@ msgstr "정말 필라멘트 정보를 삭제하시겠습니까?"
msgid "You need to select the material type and color first." msgid "You need to select the material type and color first."
msgstr "재료 유형과 색상을 먼저 선택해야 합니다." msgstr "재료 유형과 색상을 먼저 선택해야 합니다."
msgid "The minimum temperature cannot be greater than the maximum temperature."
msgstr ""
#, c-format, boost-format #, c-format, boost-format
msgid "Please input a valid value (K in %.1f~%.1f)" msgid "Please input a valid value (K in %.1f~%.1f)"
msgstr "유효한 값을 입력하세요 (%.1f~%.1f 사이의 K)" msgstr "유효한 값을 입력하세요 (%.1f~%.1f 사이의 K)"
@@ -6264,7 +6267,7 @@ msgid "Size:"
msgstr "크기:" msgstr "크기:"
# AI Translated # 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)." 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)." msgstr "레이어 %d, Z = %.2lf mm에서 G-code 경로 충돌이 발견되었습니다. 충돌하는 객체를 더 멀리 분리하세요 (%s <-> %s)."
@@ -7144,6 +7147,22 @@ msgstr ".gcode.3mf 파일에는 G-code 데이터가 없습니다. OrcaSlicer에
msgid "File '%s' was lost! Please download it again." msgid "File '%s' was lost! Please download it again."
msgstr "'%s' 파일이 손실되었습니다! 다시 다운로드하세요." 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 #, c-format, boost-format
msgid "" msgid ""
"File: %s\n" "File: %s\n"
@@ -11313,6 +11332,12 @@ msgstr "타임랩스 취소 후 출력"
msgid "Clean Up" msgid "Clean Up"
msgstr "정리" 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" msgid "Preparing print job"
msgstr "출력 작업 준비 중" msgstr "출력 작업 준비 중"
@@ -27412,213 +27437,6 @@ msgstr "이 글꼴에는 이 텍스트의 외곽선이 없습니다"
msgid "Size: %.1f × %.1f mm" msgid "Size: %.1f × %.1f mm"
msgstr "크기: %.1f × %.1f mm" msgstr "크기: %.1f × %.1f mm"
# AI Translated
msgid "Primitive"
msgstr "기본 모델"
# AI Translated
msgid "Empty mesh generated"
msgstr "빈 메시가 생성되었습니다"
# AI Translated
#. TRN Default name of an object created from the box primitive shape.
msgid "Box"
msgstr "상자"
# AI Translated
#. TRN Primitive gizmo: label in front of the preset cube size buttons
msgid "Quick:"
msgstr "빠른 선택:"
# AI Translated
msgid "Width (X)"
msgstr "너비 (X)"
# AI Translated
msgid "Depth (Y)"
msgstr "깊이 (Y)"
# AI Translated
msgid "Height (Z)"
msgstr "높이 (Z)"
# AI Translated
#. TRN Primitive gizmo: bottom radius of a cone ("Top R" is the top radius)
msgid "Bottom R"
msgstr "하단 R"
# AI Translated
msgid "Top R"
msgstr "상단 R"
# AI Translated
#. TRN Primitive gizmo: major radius of a torus ("Minor R" is the tube radius)
msgid "Major R"
msgstr "주 R"
# AI Translated
msgid "Minor R"
msgstr "부 R"
# AI Translated
msgid "All edges"
msgstr "모든 모서리"
# AI Translated
msgid "Top edges"
msgstr "상단 모서리"
# AI Translated
msgid "Bottom edges"
msgstr "하단 모서리"
# AI Translated
msgid "Lateral edges"
msgstr "측면 모서리"
# AI Translated
msgid "Mesh resolution"
msgstr "메시 해상도"
# AI Translated
msgid "Add Shape"
msgstr "형상 추가"
# AI Translated
msgid "No outer profile"
msgstr "외곽 프로파일 없음"
# AI Translated
msgid "Failed to make outer face"
msgstr "외곽 면을 만들지 못했습니다"
# AI Translated
msgid "Failed to build face with holes"
msgstr "구멍이 있는 면을 만들지 못했습니다"
# AI Translated
msgid "Revolve failed"
msgstr "회전 실패"
# AI Translated
msgid "Profile"
msgstr "프로파일"
# AI Translated
msgid "Start new profile (for holes)"
msgstr "새 프로파일 시작(구멍용)"
# AI Translated
msgid "Segments"
msgstr "세그먼트 수"
# AI Translated
msgid "Sides"
msgstr "변 수"
# AI Translated
msgid "Click on canvas to draw"
msgstr "캔버스를 클릭하여 그리기"
# AI Translated
msgid "Snap to grid"
msgstr "격자에 스냅"
# AI Translated
msgid "Grid step"
msgstr "격자 간격"
# AI Translated
msgid "Outer"
msgstr "외곽"
# AI Translated
msgid "closed"
msgstr "닫힘"
# AI Translated
#. TRN Sketch gizmo profile list: "Outer" or "Hole", profile number, point count, "closed" or nothing
#, boost-format
msgid "%1% %2%: %3% points %4%"
msgstr "%1% %2%: 점 %3%개 %4%"
# AI Translated
msgid "XY (Top)"
msgstr "XY (위)"
# AI Translated
msgid "XZ (Front)"
msgstr "XZ (앞)"
# AI Translated
msgid "YZ (Side)"
msgstr "YZ (측면)"
# AI Translated
msgid "Revolve deg"
msgstr "회전 각도"
# AI Translated
msgid "Set to 0 for extrude, >0 for revolve"
msgstr "돌출은 0, 회전은 0보다 큰 값으로 설정"
# AI Translated
msgid "Pocket (cut)"
msgstr "포켓(잘라내기)"
# AI Translated
#, c-format
msgid "%zu closed profile"
msgid_plural "%zu closed profiles"
msgstr[0] "%zu개의 닫힌 프로파일"
# AI Translated
msgid "Draw a closed profile to enable"
msgstr "활성화하려면 닫힌 프로파일을 그리십시오"
# AI Translated
msgid "Clear all"
msgstr "모두 지우기"
# AI Translated
msgid "Empty result"
msgstr "결과가 비어 있음"
# AI Translated
#, boost-format
msgid "Extrude: %1%"
msgstr "돌출: %1%"
# AI Translated
#, boost-format
msgid "Revolve: %1%"
msgstr "회전: %1%"
# AI Translated
msgid "No object selected"
msgstr "선택한 객체 없음"
# AI Translated
msgid "Face failed"
msgstr "면 생성 실패"
# AI Translated
msgid "Face with holes failed"
msgstr "구멍이 있는 면 생성 실패"
# AI Translated
msgid "Tool mesh empty"
msgstr "도구 메시가 비어 있음"
# AI Translated
msgid "Pocket added (negative volume)"
msgstr "포켓 추가됨(비우기 부품)"
# AI Translated
#, boost-format
msgid "Pocket: %1%"
msgstr "포켓: %1%"
# AI Translated # AI Translated
msgctxt "Keyboard Shortcut" msgctxt "Keyboard Shortcut"
msgid "Ins" msgid "Ins"
@@ -31004,6 +30822,9 @@ msgstr "개발자 도구 사용"
msgid "An error occurred loading %s" msgid "An error occurred loading %s"
msgstr "%s 로드 중 오류가 발생했습니다" msgstr "%s 로드 중 오류가 발생했습니다"
msgid "/"
msgstr "/"
# AI Translated # AI Translated
#, boost-format #, boost-format
msgid "Load %1% to %2%" msgid "Load %1% to %2%"
@@ -31686,6 +31507,208 @@ msgstr ""
"뒤틀림 방지\n" "뒤틀림 방지\n"
"ABS와 같이 뒤틀림이 발생하기 쉬운 소재를 출력할 때, 히트베드 온도를 적절하게 높이면 뒤틀림 가능성을 줄일 수 있다는 사실을 알고 계셨나요?" "ABS와 같이 뒤틀림이 발생하기 쉬운 소재를 출력할 때, 히트베드 온도를 적절하게 높이면 뒤틀림 가능성을 줄일 수 있다는 사실을 알고 계셨나요?"
# AI Translated
#~ msgid "Primitive"
#~ msgstr "기본 모델"
# AI Translated
#~ msgid "Empty mesh generated"
#~ msgstr "빈 메시가 생성되었습니다"
# AI Translated
#~ msgid "Box"
#~ msgstr "상자"
# AI Translated
#~ msgid "Quick:"
#~ msgstr "빠른 선택:"
# AI Translated
#~ msgid "Width (X)"
#~ msgstr "너비 (X)"
# AI Translated
#~ msgid "Depth (Y)"
#~ msgstr "깊이 (Y)"
# AI Translated
#~ msgid "Height (Z)"
#~ msgstr "높이 (Z)"
# AI Translated
#~ msgid "Bottom R"
#~ msgstr "하단 R"
# AI Translated
#~ msgid "Top R"
#~ msgstr "상단 R"
# AI Translated
#~ msgid "Major R"
#~ msgstr "주 R"
# AI Translated
#~ msgid "Minor R"
#~ msgstr "부 R"
# AI Translated
#~ msgid "All edges"
#~ msgstr "모든 모서리"
# AI Translated
#~ msgid "Top edges"
#~ msgstr "상단 모서리"
# AI Translated
#~ msgid "Bottom edges"
#~ msgstr "하단 모서리"
# AI Translated
#~ msgid "Lateral edges"
#~ msgstr "측면 모서리"
# AI Translated
#~ msgid "Mesh resolution"
#~ msgstr "메시 해상도"
# AI Translated
#~ msgid "Add Shape"
#~ msgstr "형상 추가"
# AI Translated
#~ msgid "No outer profile"
#~ msgstr "외곽 프로파일 없음"
# AI Translated
#~ msgid "Failed to make outer face"
#~ msgstr "외곽 면을 만들지 못했습니다"
# AI Translated
#~ msgid "Failed to build face with holes"
#~ msgstr "구멍이 있는 면을 만들지 못했습니다"
# AI Translated
#~ msgid "Revolve failed"
#~ msgstr "회전 실패"
# AI Translated
#~ msgid "Profile"
#~ msgstr "프로파일"
# AI Translated
#~ msgid "Start new profile (for holes)"
#~ msgstr "새 프로파일 시작(구멍용)"
# AI Translated
#~ msgid "Segments"
#~ msgstr "세그먼트 수"
# AI Translated
#~ msgid "Sides"
#~ msgstr "변 수"
# AI Translated
#~ msgid "Click on canvas to draw"
#~ msgstr "캔버스를 클릭하여 그리기"
# AI Translated
#~ msgid "Snap to grid"
#~ msgstr "격자에 스냅"
# AI Translated
#~ msgid "Grid step"
#~ msgstr "격자 간격"
# AI Translated
#~ msgid "Outer"
#~ msgstr "외곽"
# AI Translated
#~ msgid "closed"
#~ msgstr "닫힘"
# AI Translated
#, boost-format
#~ msgid "%1% %2%: %3% points %4%"
#~ msgstr "%1% %2%: 점 %3%개 %4%"
# AI Translated
#~ msgid "XY (Top)"
#~ msgstr "XY (위)"
# AI Translated
#~ msgid "XZ (Front)"
#~ msgstr "XZ (앞)"
# AI Translated
#~ msgid "YZ (Side)"
#~ msgstr "YZ (측면)"
# AI Translated
#~ msgid "Revolve deg"
#~ msgstr "회전 각도"
# AI Translated
#~ msgid "Set to 0 for extrude, >0 for revolve"
#~ msgstr "돌출은 0, 회전은 0보다 큰 값으로 설정"
# AI Translated
#~ msgid "Pocket (cut)"
#~ msgstr "포켓(잘라내기)"
# AI Translated
#, c-format
#~ msgid "%zu closed profile"
#~ msgid_plural "%zu closed profiles"
#~ msgstr[0] "%zu개의 닫힌 프로파일"
# AI Translated
#~ msgid "Draw a closed profile to enable"
#~ msgstr "활성화하려면 닫힌 프로파일을 그리십시오"
# AI Translated
#~ msgid "Clear all"
#~ msgstr "모두 지우기"
# AI Translated
#~ msgid "Empty result"
#~ msgstr "결과가 비어 있음"
# AI Translated
#, boost-format
#~ msgid "Extrude: %1%"
#~ msgstr "돌출: %1%"
# AI Translated
#, boost-format
#~ msgid "Revolve: %1%"
#~ msgstr "회전: %1%"
# AI Translated
#~ msgid "No object selected"
#~ msgstr "선택한 객체 없음"
# AI Translated
#~ msgid "Face failed"
#~ msgstr "면 생성 실패"
# AI Translated
#~ msgid "Face with holes failed"
#~ msgstr "구멍이 있는 면 생성 실패"
# AI Translated
#~ msgid "Tool mesh empty"
#~ msgstr "도구 메시가 비어 있음"
# AI Translated
#~ msgid "Pocket added (negative volume)"
#~ msgstr "포켓 추가됨(비우기 부품)"
# AI Translated
#, boost-format
#~ msgid "Pocket: %1%"
#~ msgstr "포켓: %1%"
# AI Translated # AI Translated
#~ msgid "(Experimental) Use printer agents instead of print hosts" #~ msgid "(Experimental) Use printer agents instead of print hosts"
#~ msgstr "(실험적) 출력 호스트 대신 프린터 에이전트 사용" #~ msgstr "(실험적) 출력 호스트 대신 프린터 에이전트 사용"
@@ -32749,9 +32772,6 @@ msgstr ""
#~ msgid "An error occurred loading " #~ msgid "An error occurred loading "
#~ msgstr "로드 중 오류가 발생했습니다: " #~ msgstr "로드 중 오류가 발생했습니다: "
#~ msgid "/"
#~ msgstr "/"
# AI Translated # AI Translated
#, c-format, boost-format #, c-format, boost-format
#~ msgid "Load %s to " #~ msgid "Load %s to "
+1
View File
@@ -147,6 +147,7 @@ src/slic3r/GUI/HMSPanel.cpp
src/slic3r/GUI/MainFrame.cpp src/slic3r/GUI/MainFrame.cpp
src/slic3r/GUI/MediaPlayCtrl.cpp src/slic3r/GUI/MediaPlayCtrl.cpp
src/slic3r/GUI/MediaFilePanel.cpp src/slic3r/GUI/MediaFilePanel.cpp
src/slic3r/GUI/OrcaFilesPanel.cpp
src/slic3r/GUI/ImageGrid.cpp src/slic3r/GUI/ImageGrid.cpp
src/slic3r/GUI/Widgets/ImageSwitchButton.cpp src/slic3r/GUI/Widgets/ImageSwitchButton.cpp
src/slic3r/GUI/Printer/PrinterFileSystem.cpp src/slic3r/GUI/Printer/PrinterFileSystem.cpp
+235 -215
View File
@@ -7,7 +7,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: \n" "Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-06 15:16-0300\n" "POT-Creation-Date: 2026-10-08 11:08+0800\n"
"PO-Revision-Date: 2026-07-02 14:13+0300\n" "PO-Revision-Date: 2026-07-02 14:13+0300\n"
"Last-Translator: Gintaras Kučinskas <sharanchius@gmail.com>\n" "Last-Translator: Gintaras Kučinskas <sharanchius@gmail.com>\n"
"Language-Team: \n" "Language-Team: \n"
@@ -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." msgid "You need to select the material type and color first."
msgstr "Pradžioje jums reikia pasirinkti medžiagos tipą ir spalvą." 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 #, c-format, boost-format
msgid "Please input a valid value (K in %.1f~%.1f)" msgid "Please input a valid value (K in %.1f~%.1f)"
msgstr "Įveskite tinkamą reikšmę (K %.1f~%.1f)" msgstr "Įveskite tinkamą reikšmę (K %.1f~%.1f)"
@@ -6219,7 +6222,7 @@ msgid "Size:"
msgstr "Dydis:" msgstr "Dydis:"
# AI Translated # 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)." 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)." msgstr "Rasta G-code trajektorijų konfliktų %d sluoksnyje, Z = %.2lfmm. Prašome labiau atskirti konfliktuojančius objektus (%s <-> %s)."
@@ -7104,6 +7107,22 @@ msgstr "Failas „.gcode.3mf“ neturi G-code duomenų. Supjaustykite jį su „
msgid "File '%s' was lost! Please download it again." msgid "File '%s' was lost! Please download it again."
msgstr "Failas „%s“ nerastas! Atsisiųskite jį iš naujo." 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 #, c-format, boost-format
msgid "" msgid ""
"File: %s\n" "File: %s\n"
@@ -11134,6 +11153,12 @@ msgstr "Atšaukti intervalinį fotografavimą ir spausdinti"
msgid "Clean Up" msgid "Clean Up"
msgstr "Išvalyti" 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" msgid "Preparing print job"
msgstr "Spausdinimo užduoties rengimas" msgstr "Spausdinimo užduoties rengimas"
@@ -26733,215 +26758,6 @@ msgstr "Šis šriftas neturi šio teksto kontūro"
msgid "Size: %.1f × %.1f mm" msgid "Size: %.1f × %.1f mm"
msgstr "Dydis: %.1f × %.1f mm" msgstr "Dydis: %.1f × %.1f mm"
# AI Translated
msgid "Primitive"
msgstr "Primityvas"
# AI Translated
msgid "Empty mesh generated"
msgstr "Sugeneruotas tuščias poligoninis tinklas"
# AI Translated
#. TRN Default name of an object created from the box primitive shape.
msgid "Box"
msgstr "Dėžė"
# AI Translated
#. TRN Primitive gizmo: label in front of the preset cube size buttons
msgid "Quick:"
msgstr "Greitai:"
# AI Translated
msgid "Width (X)"
msgstr "Plotis (X)"
# AI Translated
msgid "Depth (Y)"
msgstr "Gylis (Y)"
# AI Translated
msgid "Height (Z)"
msgstr "Aukštis (Z)"
# AI Translated
#. TRN Primitive gizmo: bottom radius of a cone ("Top R" is the top radius)
msgid "Bottom R"
msgstr "Apat. R"
# AI Translated
msgid "Top R"
msgstr "Viršut. R"
# AI Translated
#. TRN Primitive gizmo: major radius of a torus ("Minor R" is the tube radius)
msgid "Major R"
msgstr "Didysis R"
# AI Translated
msgid "Minor R"
msgstr "Mažasis R"
# AI Translated
msgid "All edges"
msgstr "Visos briaunos"
# AI Translated
msgid "Top edges"
msgstr "Viršutinės briaunos"
# AI Translated
msgid "Bottom edges"
msgstr "Apatinės briaunos"
# AI Translated
msgid "Lateral edges"
msgstr "Šoninės briaunos"
# AI Translated
msgid "Mesh resolution"
msgstr "Tinklelio raiška"
# AI Translated
msgid "Add Shape"
msgstr "Pridėti formą"
# AI Translated
msgid "No outer profile"
msgstr "Nėra išorinio profilio"
# AI Translated
msgid "Failed to make outer face"
msgstr "Nepavyko sukurti išorinio paviršiaus"
# AI Translated
msgid "Failed to build face with holes"
msgstr "Nepavyko sukurti paviršiaus su skylėmis"
# AI Translated
msgid "Revolve failed"
msgstr "Apsukimas nepavyko"
# AI Translated
msgid "Profile"
msgstr "Profilis"
# AI Translated
msgid "Start new profile (for holes)"
msgstr "Pradėti naują profilį (skylėms)"
# AI Translated
msgid "Segments"
msgstr "Segmentai"
# AI Translated
msgid "Sides"
msgstr "Kraštinės"
# AI Translated
msgid "Click on canvas to draw"
msgstr "Spustelėkite drobėje, kad pieštumėte"
# AI Translated
msgid "Snap to grid"
msgstr "Pritraukti prie tinklelio"
# AI Translated
msgid "Grid step"
msgstr "Tinklelio žingsnis"
# AI Translated
msgid "Outer"
msgstr "Išorinis"
# AI Translated
msgid "closed"
msgstr "uždaras"
# AI Translated
#. TRN Sketch gizmo profile list: "Outer" or "Hole", profile number, point count, "closed" or nothing
#, boost-format
msgid "%1% %2%: %3% points %4%"
msgstr "%1% %2%: %3% tšk. %4%"
# AI Translated
msgid "XY (Top)"
msgstr "XY (viršus)"
# AI Translated
msgid "XZ (Front)"
msgstr "XZ (priekis)"
# AI Translated
msgid "YZ (Side)"
msgstr "YZ (šonas)"
# AI Translated
msgid "Revolve deg"
msgstr "Apsukimo kampas"
# AI Translated
msgid "Set to 0 for extrude, >0 for revolve"
msgstr "0 – išspaudimas, >0 – apsukimas"
# AI Translated
msgid "Pocket (cut)"
msgstr "Įduba (pjūvis)"
# AI Translated
#, c-format
msgid "%zu closed profile"
msgid_plural "%zu closed profiles"
msgstr[0] "%zu uždaras profilis"
msgstr[1] "%zu uždari profiliai"
msgstr[2] "%zu uždarų profilių"
# AI Translated
msgid "Draw a closed profile to enable"
msgstr "Nubrėžkite uždarą profilį, kad įjungtumėte"
# AI Translated
msgid "Clear all"
msgstr "Išvalyti viską"
# AI Translated
msgid "Empty result"
msgstr "Tuščias rezultatas"
# AI Translated
#, boost-format
msgid "Extrude: %1%"
msgstr "Išspaudimas: %1%"
# AI Translated
#, boost-format
msgid "Revolve: %1%"
msgstr "Apsukimas: %1%"
# AI Translated
msgid "No object selected"
msgstr "Nepasirinktas joks objektas"
# AI Translated
msgid "Face failed"
msgstr "Nepavyko sukurti paviršiaus"
# AI Translated
msgid "Face with holes failed"
msgstr "Nepavyko sukurti paviršiaus su skylėmis"
# AI Translated
msgid "Tool mesh empty"
msgstr "Įrankio tinklelis tuščias"
# AI Translated
msgid "Pocket added (negative volume)"
msgstr "Įduba pridėta (neigiamas tūris)"
# AI Translated
#, boost-format
msgid "Pocket: %1%"
msgstr "Įduba: %1%"
# AI Translated # AI Translated
msgctxt "Keyboard Shortcut" msgctxt "Keyboard Shortcut"
msgid "Ins" msgid "Ins"
@@ -30336,6 +30152,10 @@ msgstr "Įjungti kūrėjo įrankius"
msgid "An error occurred loading %s" msgid "An error occurred loading %s"
msgstr "Įvyko klaida įkeliant %s" msgstr "Įvyko klaida įkeliant %s"
# AI Translated
msgid "/"
msgstr "/"
# AI Translated # AI Translated
#, boost-format #, boost-format
msgid "Load %1% to %2%" msgid "Load %1% to %2%"
@@ -31016,6 +30836,210 @@ msgstr ""
"Venkite deformacijų (warping)\n" "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ę?" "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 "Primitive"
#~ msgstr "Primityvas"
# AI Translated
#~ msgid "Empty mesh generated"
#~ msgstr "Sugeneruotas tuščias poligoninis tinklas"
# AI Translated
#~ msgid "Box"
#~ msgstr "Dėžė"
# AI Translated
#~ msgid "Quick:"
#~ msgstr "Greitai:"
# AI Translated
#~ msgid "Width (X)"
#~ msgstr "Plotis (X)"
# AI Translated
#~ msgid "Depth (Y)"
#~ msgstr "Gylis (Y)"
# AI Translated
#~ msgid "Height (Z)"
#~ msgstr "Aukštis (Z)"
# AI Translated
#~ msgid "Bottom R"
#~ msgstr "Apat. R"
# AI Translated
#~ msgid "Top R"
#~ msgstr "Viršut. R"
# AI Translated
#~ msgid "Major R"
#~ msgstr "Didysis R"
# AI Translated
#~ msgid "Minor R"
#~ msgstr "Mažasis R"
# AI Translated
#~ msgid "All edges"
#~ msgstr "Visos briaunos"
# AI Translated
#~ msgid "Top edges"
#~ msgstr "Viršutinės briaunos"
# AI Translated
#~ msgid "Bottom edges"
#~ msgstr "Apatinės briaunos"
# AI Translated
#~ msgid "Lateral edges"
#~ msgstr "Šoninės briaunos"
# AI Translated
#~ msgid "Mesh resolution"
#~ msgstr "Tinklelio raiška"
# AI Translated
#~ msgid "Add Shape"
#~ msgstr "Pridėti formą"
# AI Translated
#~ msgid "No outer profile"
#~ msgstr "Nėra išorinio profilio"
# AI Translated
#~ msgid "Failed to make outer face"
#~ msgstr "Nepavyko sukurti išorinio paviršiaus"
# AI Translated
#~ msgid "Failed to build face with holes"
#~ msgstr "Nepavyko sukurti paviršiaus su skylėmis"
# AI Translated
#~ msgid "Revolve failed"
#~ msgstr "Apsukimas nepavyko"
# AI Translated
#~ msgid "Profile"
#~ msgstr "Profilis"
# AI Translated
#~ msgid "Start new profile (for holes)"
#~ msgstr "Pradėti naują profilį (skylėms)"
# AI Translated
#~ msgid "Segments"
#~ msgstr "Segmentai"
# AI Translated
#~ msgid "Sides"
#~ msgstr "Kraštinės"
# AI Translated
#~ msgid "Click on canvas to draw"
#~ msgstr "Spustelėkite drobėje, kad pieštumėte"
# AI Translated
#~ msgid "Snap to grid"
#~ msgstr "Pritraukti prie tinklelio"
# AI Translated
#~ msgid "Grid step"
#~ msgstr "Tinklelio žingsnis"
# AI Translated
#~ msgid "Outer"
#~ msgstr "Išorinis"
# AI Translated
#~ msgid "closed"
#~ msgstr "uždaras"
# AI Translated
#, boost-format
#~ msgid "%1% %2%: %3% points %4%"
#~ msgstr "%1% %2%: %3% tšk. %4%"
# AI Translated
#~ msgid "XY (Top)"
#~ msgstr "XY (viršus)"
# AI Translated
#~ msgid "XZ (Front)"
#~ msgstr "XZ (priekis)"
# AI Translated
#~ msgid "YZ (Side)"
#~ msgstr "YZ (šonas)"
# AI Translated
#~ msgid "Revolve deg"
#~ msgstr "Apsukimo kampas"
# AI Translated
#~ msgid "Set to 0 for extrude, >0 for revolve"
#~ msgstr "0 – išspaudimas, >0 – apsukimas"
# AI Translated
#~ msgid "Pocket (cut)"
#~ msgstr "Įduba (pjūvis)"
# AI Translated
#, c-format
#~ msgid "%zu closed profile"
#~ msgid_plural "%zu closed profiles"
#~ msgstr[0] "%zu uždaras profilis"
#~ msgstr[1] "%zu uždari profiliai"
#~ msgstr[2] "%zu uždarų profilių"
# AI Translated
#~ msgid "Draw a closed profile to enable"
#~ msgstr "Nubrėžkite uždarą profilį, kad įjungtumėte"
# AI Translated
#~ msgid "Clear all"
#~ msgstr "Išvalyti viską"
# AI Translated
#~ msgid "Empty result"
#~ msgstr "Tuščias rezultatas"
# AI Translated
#, boost-format
#~ msgid "Extrude: %1%"
#~ msgstr "Išspaudimas: %1%"
# AI Translated
#, boost-format
#~ msgid "Revolve: %1%"
#~ msgstr "Apsukimas: %1%"
# AI Translated
#~ msgid "No object selected"
#~ msgstr "Nepasirinktas joks objektas"
# AI Translated
#~ msgid "Face failed"
#~ msgstr "Nepavyko sukurti paviršiaus"
# AI Translated
#~ msgid "Face with holes failed"
#~ msgstr "Nepavyko sukurti paviršiaus su skylėmis"
# AI Translated
#~ msgid "Tool mesh empty"
#~ msgstr "Įrankio tinklelis tuščias"
# AI Translated
#~ msgid "Pocket added (negative volume)"
#~ msgstr "Įduba pridėta (neigiamas tūris)"
# AI Translated
#, boost-format
#~ msgid "Pocket: %1%"
#~ msgstr "Įduba: %1%"
# AI Translated # AI Translated
#~ msgid "(Experimental) Use printer agents instead of print hosts" #~ msgid "(Experimental) Use printer agents instead of print hosts"
#~ msgstr "(Eksperimentinė) Naudoti spausdintuvo agentus vietoj spausdinimo serverių" #~ msgstr "(Eksperimentinė) Naudoti spausdintuvo agentus vietoj spausdinimo serverių"
@@ -32077,10 +32101,6 @@ msgstr ""
#~ msgid "An error occurred loading " #~ msgid "An error occurred loading "
#~ msgstr "Įvyko klaida įkeliant " #~ msgstr "Įvyko klaida įkeliant "
# AI Translated
#~ msgid "/"
#~ msgstr "/"
# AI Translated # AI Translated
#, c-format, boost-format #, c-format, boost-format
#~ msgid "Load %s to " #~ msgid "Load %s to "
+233 -213
View File
@@ -3,7 +3,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: Orca Slicer\n" "Project-Id-Version: Orca Slicer\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-06 15:16-0300\n" "POT-Creation-Date: 2026-10-08 11:08+0800\n"
"PO-Revision-Date: \n" "PO-Revision-Date: \n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: \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." msgid "You need to select the material type and color first."
msgstr "U moet eerst het materiaaltype en de kleur selecteren." msgstr "U moet eerst het materiaaltype en de kleur selecteren."
msgid "The minimum temperature cannot be greater than the maximum temperature."
msgstr ""
# AI Translated # AI Translated
#, c-format, boost-format #, c-format, boost-format
msgid "Please input a valid value (K in %.1f~%.1f)" msgid "Please input a valid value (K in %.1f~%.1f)"
@@ -6743,7 +6746,7 @@ msgid "Size:"
msgstr "Maat:" msgstr "Maat:"
# AI Translated # 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)." 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)." msgstr "Er zijn conflicten tussen G-code-paden gevonden op laag %d, Z = %.2lfmm. Plaats de conflicterende objecten verder uit elkaar (%s <-> %s)."
@@ -7683,6 +7686,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." msgid "File '%s' was lost! Please download it again."
msgstr "Bestand '%s' is verloren gegaan! Download het opnieuw." 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 #, c-format, boost-format
msgid "" msgid ""
"File: %s\n" "File: %s\n"
@@ -12028,6 +12047,12 @@ msgstr "Timelapse annuleren en printen"
msgid "Clean Up" msgid "Clean Up"
msgstr "Opruimen" 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" msgid "Preparing print job"
msgstr "Print opdracht voorbereiden" msgstr "Print opdracht voorbereiden"
@@ -28928,214 +28953,6 @@ msgstr "Dit lettertype heeft geen omtrek voor deze tekst"
msgid "Size: %.1f × %.1f mm" msgid "Size: %.1f × %.1f mm"
msgstr "Grootte: %.1f × %.1f mm" msgstr "Grootte: %.1f × %.1f mm"
# AI Translated
msgid "Primitive"
msgstr "Primitief"
# AI Translated
msgid "Empty mesh generated"
msgstr "Lege mesh gegenereerd"
# AI Translated
#. TRN Default name of an object created from the box primitive shape.
msgid "Box"
msgstr "Blok"
# AI Translated
#. TRN Primitive gizmo: label in front of the preset cube size buttons
msgid "Quick:"
msgstr "Snel:"
# AI Translated
msgid "Width (X)"
msgstr "Breedte (X)"
# AI Translated
msgid "Depth (Y)"
msgstr "Diepte (Y)"
# AI Translated
msgid "Height (Z)"
msgstr "Hoogte (Z)"
# AI Translated
#. TRN Primitive gizmo: bottom radius of a cone ("Top R" is the top radius)
msgid "Bottom R"
msgstr "R onder"
# AI Translated
msgid "Top R"
msgstr "R boven"
# AI Translated
#. TRN Primitive gizmo: major radius of a torus ("Minor R" is the tube radius)
msgid "Major R"
msgstr "Grote R"
# AI Translated
msgid "Minor R"
msgstr "Kleine R"
# AI Translated
msgid "All edges"
msgstr "Alle randen"
# AI Translated
msgid "Top edges"
msgstr "Bovenranden"
# AI Translated
msgid "Bottom edges"
msgstr "Onderranden"
# AI Translated
msgid "Lateral edges"
msgstr "Zijranden"
# AI Translated
msgid "Mesh resolution"
msgstr "Meshresolutie"
# AI Translated
msgid "Add Shape"
msgstr "Vorm toevoegen"
# AI Translated
msgid "No outer profile"
msgstr "Geen buitenprofiel"
# AI Translated
msgid "Failed to make outer face"
msgstr "Kan het buitenvlak niet maken"
# AI Translated
msgid "Failed to build face with holes"
msgstr "Kan het vlak met gaten niet opbouwen"
# AI Translated
msgid "Revolve failed"
msgstr "Wentelen mislukt"
# AI Translated
msgid "Profile"
msgstr "Profiel"
# AI Translated
msgid "Start new profile (for holes)"
msgstr "Nieuw profiel beginnen (voor gaten)"
# AI Translated
msgid "Segments"
msgstr "Segmenten"
# AI Translated
msgid "Sides"
msgstr "Zijden"
# AI Translated
msgid "Click on canvas to draw"
msgstr "Klik op het canvas om te tekenen"
# AI Translated
msgid "Snap to grid"
msgstr "Uitlijnen op raster"
# AI Translated
msgid "Grid step"
msgstr "Rasterstap"
# AI Translated
msgid "Outer"
msgstr "Buiten"
# AI Translated
msgid "closed"
msgstr "gesloten"
# AI Translated
#. TRN Sketch gizmo profile list: "Outer" or "Hole", profile number, point count, "closed" or nothing
#, boost-format
msgid "%1% %2%: %3% points %4%"
msgstr "%1% %2%: %3% punten %4%"
# AI Translated
msgid "XY (Top)"
msgstr "XY (boven)"
# AI Translated
msgid "XZ (Front)"
msgstr "XZ (voor)"
# AI Translated
msgid "YZ (Side)"
msgstr "YZ (zijkant)"
# AI Translated
msgid "Revolve deg"
msgstr "Wentelhoek (°)"
# AI Translated
msgid "Set to 0 for extrude, >0 for revolve"
msgstr "Zet op 0 om te extruderen, >0 om te wentelen"
# AI Translated
msgid "Pocket (cut)"
msgstr "Uitsparing (snijden)"
# AI Translated
#, c-format
msgid "%zu closed profile"
msgid_plural "%zu closed profiles"
msgstr[0] "%zu gesloten profiel"
msgstr[1] "%zu gesloten profielen"
# AI Translated
msgid "Draw a closed profile to enable"
msgstr "Teken een gesloten profiel om dit in te schakelen"
# AI Translated
msgid "Clear all"
msgstr "Alles wissen"
# AI Translated
msgid "Empty result"
msgstr "Leeg resultaat"
# AI Translated
#, boost-format
msgid "Extrude: %1%"
msgstr "Extruderen: %1%"
# AI Translated
#, boost-format
msgid "Revolve: %1%"
msgstr "Wentelen: %1%"
# AI Translated
msgid "No object selected"
msgstr "Geen object geselecteerd"
# AI Translated
msgid "Face failed"
msgstr "Vlak mislukt"
# AI Translated
msgid "Face with holes failed"
msgstr "Vlak met gaten mislukt"
# AI Translated
msgid "Tool mesh empty"
msgstr "Gereedschapsmesh leeg"
# AI Translated
msgid "Pocket added (negative volume)"
msgstr "Uitsparing toegevoegd (negatief volume)"
# AI Translated
#, boost-format
msgid "Pocket: %1%"
msgstr "Uitsparing: %1%"
# AI Translated # AI Translated
msgctxt "Keyboard Shortcut" msgctxt "Keyboard Shortcut"
msgid "Ins" msgid "Ins"
@@ -32534,6 +32351,9 @@ msgstr "Ontwikkelaarstools inschakelen"
msgid "An error occurred loading %s" msgid "An error occurred loading %s"
msgstr "Er is een fout opgetreden bij het laden van %s" msgstr "Er is een fout opgetreden bij het laden van %s"
msgid "/"
msgstr "/"
# AI Translated # AI Translated
#, boost-format #, boost-format
msgid "Load %1% to %2%" msgid "Load %1% to %2%"
@@ -33214,6 +33034,209 @@ msgstr ""
"Kromtrekken voorkomen\n" "Kromtrekken voorkomen\n"
"Wist je dat bij het printen van materialen die gevoelig zijn voor kromtrekken, zoals ABS, een juiste verhoging van de temperatuur van het warmtebed de kans op kromtrekken kan verkleinen?" "Wist je dat bij het printen van materialen die gevoelig zijn voor kromtrekken, zoals ABS, een juiste verhoging van de temperatuur van het warmtebed de kans op kromtrekken kan verkleinen?"
# AI Translated
#~ msgid "Primitive"
#~ msgstr "Primitief"
# AI Translated
#~ msgid "Empty mesh generated"
#~ msgstr "Lege mesh gegenereerd"
# AI Translated
#~ msgid "Box"
#~ msgstr "Blok"
# AI Translated
#~ msgid "Quick:"
#~ msgstr "Snel:"
# AI Translated
#~ msgid "Width (X)"
#~ msgstr "Breedte (X)"
# AI Translated
#~ msgid "Depth (Y)"
#~ msgstr "Diepte (Y)"
# AI Translated
#~ msgid "Height (Z)"
#~ msgstr "Hoogte (Z)"
# AI Translated
#~ msgid "Bottom R"
#~ msgstr "R onder"
# AI Translated
#~ msgid "Top R"
#~ msgstr "R boven"
# AI Translated
#~ msgid "Major R"
#~ msgstr "Grote R"
# AI Translated
#~ msgid "Minor R"
#~ msgstr "Kleine R"
# AI Translated
#~ msgid "All edges"
#~ msgstr "Alle randen"
# AI Translated
#~ msgid "Top edges"
#~ msgstr "Bovenranden"
# AI Translated
#~ msgid "Bottom edges"
#~ msgstr "Onderranden"
# AI Translated
#~ msgid "Lateral edges"
#~ msgstr "Zijranden"
# AI Translated
#~ msgid "Mesh resolution"
#~ msgstr "Meshresolutie"
# AI Translated
#~ msgid "Add Shape"
#~ msgstr "Vorm toevoegen"
# AI Translated
#~ msgid "No outer profile"
#~ msgstr "Geen buitenprofiel"
# AI Translated
#~ msgid "Failed to make outer face"
#~ msgstr "Kan het buitenvlak niet maken"
# AI Translated
#~ msgid "Failed to build face with holes"
#~ msgstr "Kan het vlak met gaten niet opbouwen"
# AI Translated
#~ msgid "Revolve failed"
#~ msgstr "Wentelen mislukt"
# AI Translated
#~ msgid "Profile"
#~ msgstr "Profiel"
# AI Translated
#~ msgid "Start new profile (for holes)"
#~ msgstr "Nieuw profiel beginnen (voor gaten)"
# AI Translated
#~ msgid "Segments"
#~ msgstr "Segmenten"
# AI Translated
#~ msgid "Sides"
#~ msgstr "Zijden"
# AI Translated
#~ msgid "Click on canvas to draw"
#~ msgstr "Klik op het canvas om te tekenen"
# AI Translated
#~ msgid "Snap to grid"
#~ msgstr "Uitlijnen op raster"
# AI Translated
#~ msgid "Grid step"
#~ msgstr "Rasterstap"
# AI Translated
#~ msgid "Outer"
#~ msgstr "Buiten"
# AI Translated
#~ msgid "closed"
#~ msgstr "gesloten"
# AI Translated
#, boost-format
#~ msgid "%1% %2%: %3% points %4%"
#~ msgstr "%1% %2%: %3% punten %4%"
# AI Translated
#~ msgid "XY (Top)"
#~ msgstr "XY (boven)"
# AI Translated
#~ msgid "XZ (Front)"
#~ msgstr "XZ (voor)"
# AI Translated
#~ msgid "YZ (Side)"
#~ msgstr "YZ (zijkant)"
# AI Translated
#~ msgid "Revolve deg"
#~ msgstr "Wentelhoek (°)"
# AI Translated
#~ msgid "Set to 0 for extrude, >0 for revolve"
#~ msgstr "Zet op 0 om te extruderen, >0 om te wentelen"
# AI Translated
#~ msgid "Pocket (cut)"
#~ msgstr "Uitsparing (snijden)"
# AI Translated
#, c-format
#~ msgid "%zu closed profile"
#~ msgid_plural "%zu closed profiles"
#~ msgstr[0] "%zu gesloten profiel"
#~ msgstr[1] "%zu gesloten profielen"
# AI Translated
#~ msgid "Draw a closed profile to enable"
#~ msgstr "Teken een gesloten profiel om dit in te schakelen"
# AI Translated
#~ msgid "Clear all"
#~ msgstr "Alles wissen"
# AI Translated
#~ msgid "Empty result"
#~ msgstr "Leeg resultaat"
# AI Translated
#, boost-format
#~ msgid "Extrude: %1%"
#~ msgstr "Extruderen: %1%"
# AI Translated
#, boost-format
#~ msgid "Revolve: %1%"
#~ msgstr "Wentelen: %1%"
# AI Translated
#~ msgid "No object selected"
#~ msgstr "Geen object geselecteerd"
# AI Translated
#~ msgid "Face failed"
#~ msgstr "Vlak mislukt"
# AI Translated
#~ msgid "Face with holes failed"
#~ msgstr "Vlak met gaten mislukt"
# AI Translated
#~ msgid "Tool mesh empty"
#~ msgstr "Gereedschapsmesh leeg"
# AI Translated
#~ msgid "Pocket added (negative volume)"
#~ msgstr "Uitsparing toegevoegd (negatief volume)"
# AI Translated
#, boost-format
#~ msgid "Pocket: %1%"
#~ msgstr "Uitsparing: %1%"
# AI Translated # AI Translated
#~ msgid "(Experimental) Use printer agents instead of print hosts" #~ msgid "(Experimental) Use printer agents instead of print hosts"
#~ msgstr "(Experimenteel) Printeragents gebruiken in plaats van printhosts" #~ msgstr "(Experimenteel) Printeragents gebruiken in plaats van printhosts"
@@ -34333,9 +34356,6 @@ msgstr ""
#~ msgid "An error occurred loading " #~ msgid "An error occurred loading "
#~ msgstr "Er is een fout opgetreden bij het laden van " #~ msgstr "Er is een fout opgetreden bij het laden van "
#~ msgid "/"
#~ msgstr "/"
# AI Translated # AI Translated
#, c-format, boost-format #, c-format, boost-format
#~ msgid "Load %s to " #~ msgid "Load %s to "
+234 -214
View File
@@ -3,7 +3,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: OrcaSlicer 2.3.0-rc\n" "Project-Id-Version: OrcaSlicer 2.3.0-rc\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-06 15:16-0300\n" "POT-Creation-Date: 2026-10-08 11:08+0800\n"
"PO-Revision-Date: \n" "PO-Revision-Date: \n"
"Last-Translator: Krzysztof Morga <<tlumaczeniebs@gmail.com>>\n" "Last-Translator: Krzysztof Morga <<tlumaczeniebs@gmail.com>>\n"
"Language-Team: \n" "Language-Team: \n"
@@ -4438,6 +4438,9 @@ msgstr "Czy na pewno usunąć informacje o filamentach?"
msgid "You need to select the material type and color first." msgid "You need to select the material type and color first."
msgstr "Najpierw należy wybrać rodzaj i kolor filamentu." 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 #, c-format, boost-format
msgid "Please input a valid value (K in %.1f~%.1f)" msgid "Please input a valid value (K in %.1f~%.1f)"
msgstr "Proszę podać prawidłową wartość (K w zakresie %.1f~%.1f)" msgstr "Proszę podać prawidłową wartość (K w zakresie %.1f~%.1f)"
@@ -6338,7 +6341,7 @@ msgstr "Objętość:"
msgid "Size:" msgid "Size:"
msgstr "Rozmiar:" 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)." 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)." msgstr "Wykryto konflikty ścieżek G-code na warstwie %d, Z = %.2lfmm. Proszę oddalić od siebie obiekty będące w konflikcie (%s <-> %s)."
@@ -7235,6 +7238,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." msgid "File '%s' was lost! Please download it again."
msgstr "Plik „%s” został utracony! Proszę pobrać go ponownie." 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 #, c-format, boost-format
msgid "" msgid ""
"File: %s\n" "File: %s\n"
@@ -11413,6 +11432,12 @@ msgstr "Anuluj timelapse i drukuj"
msgid "Clean Up" msgid "Clean Up"
msgstr "Wyczyść" 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" msgid "Preparing print job"
msgstr "Przygotowywanie zadania do druku" msgstr "Przygotowywanie zadania do druku"
@@ -27480,215 +27505,6 @@ msgstr "Ta czcionka nie ma konturu dla tego tekstu"
msgid "Size: %.1f × %.1f mm" msgid "Size: %.1f × %.1f mm"
msgstr "Rozmiar: %.1f × %.1f mm" msgstr "Rozmiar: %.1f × %.1f mm"
# AI Translated
msgid "Primitive"
msgstr "Prymityw"
# AI Translated
msgid "Empty mesh generated"
msgstr "Wygenerowano pustą siatkę"
# AI Translated
#. TRN Default name of an object created from the box primitive shape.
msgid "Box"
msgstr "Prostopadłościan"
# AI Translated
#. TRN Primitive gizmo: label in front of the preset cube size buttons
msgid "Quick:"
msgstr "Szybki wybór:"
# AI Translated
msgid "Width (X)"
msgstr "Szerokość (X)"
# AI Translated
msgid "Depth (Y)"
msgstr "Głębokość (Y)"
# AI Translated
msgid "Height (Z)"
msgstr "Wysokość (Z)"
# AI Translated
#. TRN Primitive gizmo: bottom radius of a cone ("Top R" is the top radius)
msgid "Bottom R"
msgstr "R dolny"
# AI Translated
msgid "Top R"
msgstr "R górny"
# AI Translated
#. TRN Primitive gizmo: major radius of a torus ("Minor R" is the tube radius)
msgid "Major R"
msgstr "R większy"
# AI Translated
msgid "Minor R"
msgstr "R mniejszy"
# AI Translated
msgid "All edges"
msgstr "Wszystkie krawędzie"
# AI Translated
msgid "Top edges"
msgstr "Górne krawędzie"
# AI Translated
msgid "Bottom edges"
msgstr "Dolne krawędzie"
# AI Translated
msgid "Lateral edges"
msgstr "Boczne krawędzie"
# AI Translated
msgid "Mesh resolution"
msgstr "Rozdzielczość siatki"
# AI Translated
msgid "Add Shape"
msgstr "Dodaj kształt"
# AI Translated
msgid "No outer profile"
msgstr "Brak profilu zewnętrznego"
# AI Translated
msgid "Failed to make outer face"
msgstr "Nie udało się utworzyć powierzchni zewnętrznej"
# AI Translated
msgid "Failed to build face with holes"
msgstr "Nie udało się zbudować powierzchni z otworami"
# AI Translated
msgid "Revolve failed"
msgstr "Obrót nie powiódł się"
# AI Translated
msgid "Profile"
msgstr "Profil"
# AI Translated
msgid "Start new profile (for holes)"
msgstr "Rozpocznij nowy profil (dla otworów)"
# AI Translated
msgid "Segments"
msgstr "Segmenty"
# AI Translated
msgid "Sides"
msgstr "Boki"
# AI Translated
msgid "Click on canvas to draw"
msgstr "Kliknij w widoku, aby rysować"
# AI Translated
msgid "Snap to grid"
msgstr "Przyciągaj do siatki"
# AI Translated
msgid "Grid step"
msgstr "Krok siatki"
# AI Translated
msgid "Outer"
msgstr "Zewnętrzny"
# AI Translated
msgid "closed"
msgstr "zamknięty"
# AI Translated
#. TRN Sketch gizmo profile list: "Outer" or "Hole", profile number, point count, "closed" or nothing
#, boost-format
msgid "%1% %2%: %3% points %4%"
msgstr "%1% %2%: %3% pkt %4%"
# AI Translated
msgid "XY (Top)"
msgstr "XY (góra)"
# AI Translated
msgid "XZ (Front)"
msgstr "XZ (przód)"
# AI Translated
msgid "YZ (Side)"
msgstr "YZ (bok)"
# AI Translated
msgid "Revolve deg"
msgstr "Obrót (°)"
# AI Translated
msgid "Set to 0 for extrude, >0 for revolve"
msgstr "Ustaw 0 dla wyciągnięcia, >0 dla obrotu"
# AI Translated
msgid "Pocket (cut)"
msgstr "Kieszeń (wycięcie)"
# AI Translated
#, c-format
msgid "%zu closed profile"
msgid_plural "%zu closed profiles"
msgstr[0] "%zu zamknięty profil"
msgstr[1] "%zu zamknięte profile"
msgstr[2] "%zu zamkniętych profili"
# AI Translated
msgid "Draw a closed profile to enable"
msgstr "Narysuj zamknięty profil, aby włączyć"
# AI Translated
msgid "Clear all"
msgstr "Wyczyść wszystko"
# AI Translated
msgid "Empty result"
msgstr "Pusty wynik"
# AI Translated
#, boost-format
msgid "Extrude: %1%"
msgstr "Wyciągnięcie: %1%"
# AI Translated
#, boost-format
msgid "Revolve: %1%"
msgstr "Obrót: %1%"
# AI Translated
msgid "No object selected"
msgstr "Nie wybrano obiektu"
# AI Translated
msgid "Face failed"
msgstr "Nie udało się utworzyć powierzchni"
# AI Translated
msgid "Face with holes failed"
msgstr "Nie udało się utworzyć powierzchni z otworami"
# AI Translated
msgid "Tool mesh empty"
msgstr "Siatka narzędzia jest pusta"
# AI Translated
msgid "Pocket added (negative volume)"
msgstr "Dodano kieszeń (objętość ujemna)"
# AI Translated
#, boost-format
msgid "Pocket: %1%"
msgstr "Kieszeń: %1%"
# AI Translated # AI Translated
msgctxt "Keyboard Shortcut" msgctxt "Keyboard Shortcut"
msgid "Ins" msgid "Ins"
@@ -30988,6 +30804,9 @@ msgstr "Włącz narzędzia deweloperskie"
msgid "An error occurred loading %s" msgid "An error occurred loading %s"
msgstr "Wystąpił błąd podczas ładowania %s" msgstr "Wystąpił błąd podczas ładowania %s"
msgid "/"
msgstr "/"
# AI Translated # AI Translated
#, boost-format #, boost-format
msgid "Load %1% to %2%" msgid "Load %1% to %2%"
@@ -31667,6 +31486,210 @@ msgstr ""
"Unikaj odkształceń\n" "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ń?" "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 "Primitive"
#~ msgstr "Prymityw"
# AI Translated
#~ msgid "Empty mesh generated"
#~ msgstr "Wygenerowano pustą siatkę"
# AI Translated
#~ msgid "Box"
#~ msgstr "Prostopadłościan"
# AI Translated
#~ msgid "Quick:"
#~ msgstr "Szybki wybór:"
# AI Translated
#~ msgid "Width (X)"
#~ msgstr "Szerokość (X)"
# AI Translated
#~ msgid "Depth (Y)"
#~ msgstr "Głębokość (Y)"
# AI Translated
#~ msgid "Height (Z)"
#~ msgstr "Wysokość (Z)"
# AI Translated
#~ msgid "Bottom R"
#~ msgstr "R dolny"
# AI Translated
#~ msgid "Top R"
#~ msgstr "R górny"
# AI Translated
#~ msgid "Major R"
#~ msgstr "R większy"
# AI Translated
#~ msgid "Minor R"
#~ msgstr "R mniejszy"
# AI Translated
#~ msgid "All edges"
#~ msgstr "Wszystkie krawędzie"
# AI Translated
#~ msgid "Top edges"
#~ msgstr "Górne krawędzie"
# AI Translated
#~ msgid "Bottom edges"
#~ msgstr "Dolne krawędzie"
# AI Translated
#~ msgid "Lateral edges"
#~ msgstr "Boczne krawędzie"
# AI Translated
#~ msgid "Mesh resolution"
#~ msgstr "Rozdzielczość siatki"
# AI Translated
#~ msgid "Add Shape"
#~ msgstr "Dodaj kształt"
# AI Translated
#~ msgid "No outer profile"
#~ msgstr "Brak profilu zewnętrznego"
# AI Translated
#~ msgid "Failed to make outer face"
#~ msgstr "Nie udało się utworzyć powierzchni zewnętrznej"
# AI Translated
#~ msgid "Failed to build face with holes"
#~ msgstr "Nie udało się zbudować powierzchni z otworami"
# AI Translated
#~ msgid "Revolve failed"
#~ msgstr "Obrót nie powiódł się"
# AI Translated
#~ msgid "Profile"
#~ msgstr "Profil"
# AI Translated
#~ msgid "Start new profile (for holes)"
#~ msgstr "Rozpocznij nowy profil (dla otworów)"
# AI Translated
#~ msgid "Segments"
#~ msgstr "Segmenty"
# AI Translated
#~ msgid "Sides"
#~ msgstr "Boki"
# AI Translated
#~ msgid "Click on canvas to draw"
#~ msgstr "Kliknij w widoku, aby rysować"
# AI Translated
#~ msgid "Snap to grid"
#~ msgstr "Przyciągaj do siatki"
# AI Translated
#~ msgid "Grid step"
#~ msgstr "Krok siatki"
# AI Translated
#~ msgid "Outer"
#~ msgstr "Zewnętrzny"
# AI Translated
#~ msgid "closed"
#~ msgstr "zamknięty"
# AI Translated
#, boost-format
#~ msgid "%1% %2%: %3% points %4%"
#~ msgstr "%1% %2%: %3% pkt %4%"
# AI Translated
#~ msgid "XY (Top)"
#~ msgstr "XY (góra)"
# AI Translated
#~ msgid "XZ (Front)"
#~ msgstr "XZ (przód)"
# AI Translated
#~ msgid "YZ (Side)"
#~ msgstr "YZ (bok)"
# AI Translated
#~ msgid "Revolve deg"
#~ msgstr "Obrót (°)"
# AI Translated
#~ msgid "Set to 0 for extrude, >0 for revolve"
#~ msgstr "Ustaw 0 dla wyciągnięcia, >0 dla obrotu"
# AI Translated
#~ msgid "Pocket (cut)"
#~ msgstr "Kieszeń (wycięcie)"
# AI Translated
#, c-format
#~ msgid "%zu closed profile"
#~ msgid_plural "%zu closed profiles"
#~ msgstr[0] "%zu zamknięty profil"
#~ msgstr[1] "%zu zamknięte profile"
#~ msgstr[2] "%zu zamkniętych profili"
# AI Translated
#~ msgid "Draw a closed profile to enable"
#~ msgstr "Narysuj zamknięty profil, aby włączyć"
# AI Translated
#~ msgid "Clear all"
#~ msgstr "Wyczyść wszystko"
# AI Translated
#~ msgid "Empty result"
#~ msgstr "Pusty wynik"
# AI Translated
#, boost-format
#~ msgid "Extrude: %1%"
#~ msgstr "Wyciągnięcie: %1%"
# AI Translated
#, boost-format
#~ msgid "Revolve: %1%"
#~ msgstr "Obrót: %1%"
# AI Translated
#~ msgid "No object selected"
#~ msgstr "Nie wybrano obiektu"
# AI Translated
#~ msgid "Face failed"
#~ msgstr "Nie udało się utworzyć powierzchni"
# AI Translated
#~ msgid "Face with holes failed"
#~ msgstr "Nie udało się utworzyć powierzchni z otworami"
# AI Translated
#~ msgid "Tool mesh empty"
#~ msgstr "Siatka narzędzia jest pusta"
# AI Translated
#~ msgid "Pocket added (negative volume)"
#~ msgstr "Dodano kieszeń (objętość ujemna)"
# AI Translated
#, boost-format
#~ msgid "Pocket: %1%"
#~ msgstr "Kieszeń: %1%"
# AI Translated # AI Translated
#~ msgid "(Experimental) Use printer agents instead of print hosts" #~ msgid "(Experimental) Use printer agents instead of print hosts"
#~ msgstr "(Eksperymentalne) Używaj agentów drukarki zamiast serwerów druku" #~ msgstr "(Eksperymentalne) Używaj agentów drukarki zamiast serwerów druku"
@@ -32698,9 +32721,6 @@ msgstr ""
#~ msgid "An error occurred loading " #~ msgid "An error occurred loading "
#~ msgstr "Wystąpił błąd podczas ładowania" #~ msgstr "Wystąpił błąd podczas ładowania"
#~ msgid "/"
#~ msgstr "/"
# AI Translated # AI Translated
#, c-format, boost-format #, c-format, boost-format
#~ msgid "Load %s to " #~ msgid "Load %s to "
+234 -214
View File
@@ -3,7 +3,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: Orca Slicer\n" "Project-Id-Version: Orca Slicer\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-06 15:16-0300\n" "POT-Creation-Date: 2026-10-08 11:08+0800\n"
"PO-Revision-Date: 2026-07-26 11:14-0300\n" "PO-Revision-Date: 2026-07-26 11:14-0300\n"
"Last-Translator: Alexandre Folle de Menezes\n" "Last-Translator: Alexandre Folle de Menezes\n"
"Language-Team: Portuguese, Brazilian\n" "Language-Team: Portuguese, Brazilian\n"
@@ -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." msgid "You need to select the material type and color first."
msgstr "Você precisa selecionar o tipo e a cor do material primeiro." 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 #, c-format, boost-format
msgid "Please input a valid value (K in %.1f~%.1f)" msgid "Please input a valid value (K in %.1f~%.1f)"
msgstr "Por favor insira um valor válido (K entre %.1f~%.1f)" msgstr "Por favor insira um valor válido (K entre %.1f~%.1f)"
@@ -6012,7 +6015,7 @@ msgstr "Volume:"
msgid "Size:" msgid "Size:"
msgstr "Tamanho:" 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)." 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)." msgstr "Foram encontrados conflitos de caminhos de G-code na camada %d, Z = %.2lfmm. Por favor, separe mais os objetos em conflito (%s <-> %s)."
@@ -6875,6 +6878,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." msgid "File '%s' was lost! Please download it again."
msgstr "O arquivo '%s' foi perdido! Por favor, baixe-o novamente." 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 #, c-format, boost-format
msgid "" msgid ""
"File: %s\n" "File: %s\n"
@@ -10722,6 +10741,12 @@ msgstr "Cancelar Timelapse e Imprimir"
msgid "Clean Up" msgid "Clean Up"
msgstr "Limpar" 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" msgid "Preparing print job"
msgstr "Preparando trabalho de impressão" msgstr "Preparando trabalho de impressão"
@@ -25968,214 +25993,6 @@ msgstr "Esta fonte não tem contorno para este texto"
msgid "Size: %.1f × %.1f mm" msgid "Size: %.1f × %.1f mm"
msgstr "Tamanho: %.1f × %.1f mm" msgstr "Tamanho: %.1f × %.1f mm"
# AI Translated
msgid "Primitive"
msgstr "Primitivo"
# AI Translated
msgid "Empty mesh generated"
msgstr "Malha vazia gerada"
# AI Translated
#. TRN Default name of an object created from the box primitive shape.
msgid "Box"
msgstr "Caixa"
# AI Translated
#. TRN Primitive gizmo: label in front of the preset cube size buttons
msgid "Quick:"
msgstr "Rápido:"
# AI Translated
msgid "Width (X)"
msgstr "Largura (X)"
# AI Translated
msgid "Depth (Y)"
msgstr "Profundidade (Y)"
# AI Translated
msgid "Height (Z)"
msgstr "Altura (Z)"
# AI Translated
#. TRN Primitive gizmo: bottom radius of a cone ("Top R" is the top radius)
msgid "Bottom R"
msgstr "R inferior"
# AI Translated
msgid "Top R"
msgstr "R superior"
# AI Translated
#. TRN Primitive gizmo: major radius of a torus ("Minor R" is the tube radius)
msgid "Major R"
msgstr "R maior"
# AI Translated
msgid "Minor R"
msgstr "R menor"
# AI Translated
msgid "All edges"
msgstr "Todas as arestas"
# AI Translated
msgid "Top edges"
msgstr "Arestas superiores"
# AI Translated
msgid "Bottom edges"
msgstr "Arestas inferiores"
# AI Translated
msgid "Lateral edges"
msgstr "Arestas laterais"
# AI Translated
msgid "Mesh resolution"
msgstr "Resolução da malha"
# AI Translated
msgid "Add Shape"
msgstr "Adicionar forma"
# AI Translated
msgid "No outer profile"
msgstr "Nenhum perfil externo"
# AI Translated
msgid "Failed to make outer face"
msgstr "Falha ao criar a face externa"
# AI Translated
msgid "Failed to build face with holes"
msgstr "Falha ao construir a face com furos"
# AI Translated
msgid "Revolve failed"
msgstr "Falha na revolução"
# AI Translated
msgid "Profile"
msgstr "Perfil"
# AI Translated
msgid "Start new profile (for holes)"
msgstr "Iniciar novo perfil (para furos)"
# AI Translated
msgid "Segments"
msgstr "Segmentos"
# AI Translated
msgid "Sides"
msgstr "Lados"
# AI Translated
msgid "Click on canvas to draw"
msgstr "Clique na tela para desenhar"
# AI Translated
msgid "Snap to grid"
msgstr "Ajustar à grade"
# AI Translated
msgid "Grid step"
msgstr "Passo da grade"
# AI Translated
msgid "Outer"
msgstr "Externo"
# AI Translated
msgid "closed"
msgstr "fechado"
# AI Translated
#. TRN Sketch gizmo profile list: "Outer" or "Hole", profile number, point count, "closed" or nothing
#, boost-format
msgid "%1% %2%: %3% points %4%"
msgstr "%1% %2%: %3% pontos %4%"
# AI Translated
msgid "XY (Top)"
msgstr "XY (Topo)"
# AI Translated
msgid "XZ (Front)"
msgstr "XZ (Frente)"
# AI Translated
msgid "YZ (Side)"
msgstr "YZ (Lateral)"
# AI Translated
msgid "Revolve deg"
msgstr "Revolução (graus)"
# AI Translated
msgid "Set to 0 for extrude, >0 for revolve"
msgstr "Defina como 0 para extrusão, >0 para revolução"
# AI Translated
msgid "Pocket (cut)"
msgstr "Cavidade (corte)"
# AI Translated
#, c-format
msgid "%zu closed profile"
msgid_plural "%zu closed profiles"
msgstr[0] "%zu perfil fechado"
msgstr[1] "%zu perfis fechados"
# AI Translated
msgid "Draw a closed profile to enable"
msgstr "Desenhe um perfil fechado para ativar"
# AI Translated
msgid "Clear all"
msgstr "Limpar tudo"
# AI Translated
msgid "Empty result"
msgstr "Resultado vazio"
# AI Translated
#, boost-format
msgid "Extrude: %1%"
msgstr "Extrusão: %1%"
# AI Translated
#, boost-format
msgid "Revolve: %1%"
msgstr "Revolução: %1%"
# AI Translated
msgid "No object selected"
msgstr "Nenhum objeto selecionado"
# AI Translated
msgid "Face failed"
msgstr "Falha na face"
# AI Translated
msgid "Face with holes failed"
msgstr "Falha na face com furos"
# AI Translated
msgid "Tool mesh empty"
msgstr "Malha da ferramenta vazia"
# AI Translated
msgid "Pocket added (negative volume)"
msgstr "Cavidade adicionada (volume negativo)"
# AI Translated
#, boost-format
msgid "Pocket: %1%"
msgstr "Cavidade: %1%"
# AI Translated # AI Translated
msgctxt "Keyboard Shortcut" msgctxt "Keyboard Shortcut"
msgid "Ins" msgid "Ins"
@@ -29560,6 +29377,10 @@ msgstr "Ativar Ferramentas de Desenvolvedor"
msgid "An error occurred loading %s" msgid "An error occurred loading %s"
msgstr "Ocorreu um erro ao carregar %s" msgstr "Ocorreu um erro ao carregar %s"
# AI Translated
msgid "/"
msgstr "/"
# AI Translated # AI Translated
#, boost-format #, boost-format
msgid "Load %1% to %2%" msgid "Load %1% to %2%"
@@ -30239,6 +30060,209 @@ msgstr ""
"Evitar empenamento\n" "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?" "Você sabia que ao imprimir materiais propensos ao empenamento como ABS, aumentar adequadamente a temperatura da mesa aquecida pode reduzir a probabilidade de empenamento?"
# AI Translated
#~ msgid "Primitive"
#~ msgstr "Primitivo"
# AI Translated
#~ msgid "Empty mesh generated"
#~ msgstr "Malha vazia gerada"
# AI Translated
#~ msgid "Box"
#~ msgstr "Caixa"
# AI Translated
#~ msgid "Quick:"
#~ msgstr "Rápido:"
# AI Translated
#~ msgid "Width (X)"
#~ msgstr "Largura (X)"
# AI Translated
#~ msgid "Depth (Y)"
#~ msgstr "Profundidade (Y)"
# AI Translated
#~ msgid "Height (Z)"
#~ msgstr "Altura (Z)"
# AI Translated
#~ msgid "Bottom R"
#~ msgstr "R inferior"
# AI Translated
#~ msgid "Top R"
#~ msgstr "R superior"
# AI Translated
#~ msgid "Major R"
#~ msgstr "R maior"
# AI Translated
#~ msgid "Minor R"
#~ msgstr "R menor"
# AI Translated
#~ msgid "All edges"
#~ msgstr "Todas as arestas"
# AI Translated
#~ msgid "Top edges"
#~ msgstr "Arestas superiores"
# AI Translated
#~ msgid "Bottom edges"
#~ msgstr "Arestas inferiores"
# AI Translated
#~ msgid "Lateral edges"
#~ msgstr "Arestas laterais"
# AI Translated
#~ msgid "Mesh resolution"
#~ msgstr "Resolução da malha"
# AI Translated
#~ msgid "Add Shape"
#~ msgstr "Adicionar forma"
# AI Translated
#~ msgid "No outer profile"
#~ msgstr "Nenhum perfil externo"
# AI Translated
#~ msgid "Failed to make outer face"
#~ msgstr "Falha ao criar a face externa"
# AI Translated
#~ msgid "Failed to build face with holes"
#~ msgstr "Falha ao construir a face com furos"
# AI Translated
#~ msgid "Revolve failed"
#~ msgstr "Falha na revolução"
# AI Translated
#~ msgid "Profile"
#~ msgstr "Perfil"
# AI Translated
#~ msgid "Start new profile (for holes)"
#~ msgstr "Iniciar novo perfil (para furos)"
# AI Translated
#~ msgid "Segments"
#~ msgstr "Segmentos"
# AI Translated
#~ msgid "Sides"
#~ msgstr "Lados"
# AI Translated
#~ msgid "Click on canvas to draw"
#~ msgstr "Clique na tela para desenhar"
# AI Translated
#~ msgid "Snap to grid"
#~ msgstr "Ajustar à grade"
# AI Translated
#~ msgid "Grid step"
#~ msgstr "Passo da grade"
# AI Translated
#~ msgid "Outer"
#~ msgstr "Externo"
# AI Translated
#~ msgid "closed"
#~ msgstr "fechado"
# AI Translated
#, boost-format
#~ msgid "%1% %2%: %3% points %4%"
#~ msgstr "%1% %2%: %3% pontos %4%"
# AI Translated
#~ msgid "XY (Top)"
#~ msgstr "XY (Topo)"
# AI Translated
#~ msgid "XZ (Front)"
#~ msgstr "XZ (Frente)"
# AI Translated
#~ msgid "YZ (Side)"
#~ msgstr "YZ (Lateral)"
# AI Translated
#~ msgid "Revolve deg"
#~ msgstr "Revolução (graus)"
# AI Translated
#~ msgid "Set to 0 for extrude, >0 for revolve"
#~ msgstr "Defina como 0 para extrusão, >0 para revolução"
# AI Translated
#~ msgid "Pocket (cut)"
#~ msgstr "Cavidade (corte)"
# AI Translated
#, c-format
#~ msgid "%zu closed profile"
#~ msgid_plural "%zu closed profiles"
#~ msgstr[0] "%zu perfil fechado"
#~ msgstr[1] "%zu perfis fechados"
# AI Translated
#~ msgid "Draw a closed profile to enable"
#~ msgstr "Desenhe um perfil fechado para ativar"
# AI Translated
#~ msgid "Clear all"
#~ msgstr "Limpar tudo"
# AI Translated
#~ msgid "Empty result"
#~ msgstr "Resultado vazio"
# AI Translated
#, boost-format
#~ msgid "Extrude: %1%"
#~ msgstr "Extrusão: %1%"
# AI Translated
#, boost-format
#~ msgid "Revolve: %1%"
#~ msgstr "Revolução: %1%"
# AI Translated
#~ msgid "No object selected"
#~ msgstr "Nenhum objeto selecionado"
# AI Translated
#~ msgid "Face failed"
#~ msgstr "Falha na face"
# AI Translated
#~ msgid "Face with holes failed"
#~ msgstr "Falha na face com furos"
# AI Translated
#~ msgid "Tool mesh empty"
#~ msgstr "Malha da ferramenta vazia"
# AI Translated
#~ msgid "Pocket added (negative volume)"
#~ msgstr "Cavidade adicionada (volume negativo)"
# AI Translated
#, boost-format
#~ msgid "Pocket: %1%"
#~ msgstr "Cavidade: %1%"
#~ msgid "(Experimental) Use printer agents instead of print hosts" #~ msgid "(Experimental) Use printer agents instead of print hosts"
#~ msgstr "(Experimental) Usar agentes de impressora em vez de hosts de impressão" #~ msgstr "(Experimental) Usar agentes de impressora em vez de hosts de impressão"
@@ -31292,10 +31316,6 @@ msgstr ""
#~ msgid "An error occurred loading " #~ msgid "An error occurred loading "
#~ msgstr "Ocorreu um erro ao carregar " #~ msgstr "Ocorreu um erro ao carregar "
# AI Translated
#~ msgid "/"
#~ msgstr "/"
# AI Translated # AI Translated
#, c-format, boost-format #, c-format, boost-format
#~ msgid "Load %s to " #~ msgid "Load %s to "
+234 -214
View File
@@ -7,7 +7,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: OrcaSlicer V2.5.0\n" "Project-Id-Version: OrcaSlicer V2.5.0\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-06 15:16-0300\n" "POT-Creation-Date: 2026-10-08 11:08+0800\n"
"PO-Revision-Date: 2026-02-25 13:38+0300\n" "PO-Revision-Date: 2026-02-25 13:38+0300\n"
"Last-Translator: Felix14_v2\n" "Last-Translator: Felix14_v2\n"
"Language-Team: Felix14_v2 (ДС/ТГ: @felix14_v2, почта: aleks111001@list.ru), Andylg <andylg@yandex.ru>\n" "Language-Team: Felix14_v2 (ДС/ТГ: @felix14_v2, почта: aleks111001@list.ru), Andylg <andylg@yandex.ru>\n"
@@ -4319,6 +4319,9 @@ msgstr "Вы действительно хотите удалить информ
msgid "You need to select the material type and color first." msgid "You need to select the material type and color first."
msgstr "Сначала нужно выбрать тип материала и цвет." msgstr "Сначала нужно выбрать тип материала и цвет."
msgid "The minimum temperature cannot be greater than the maximum temperature."
msgstr ""
#, c-format, boost-format #, c-format, boost-format
msgid "Please input a valid value (K in %.1f~%.1f)" msgid "Please input a valid value (K in %.1f~%.1f)"
msgstr "Пожалуйста, введите допустимое значение (K в диапазоне %.1f~%.1f)" msgstr "Пожалуйста, введите допустимое значение (K в диапазоне %.1f~%.1f)"
@@ -6230,7 +6233,7 @@ msgstr "Объём:"
msgid "Size:" msgid "Size:"
msgstr "Размер:" 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)." 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)." msgstr "В G-code на %d слое (z = %.2lf мм) обнаружен конфликт путей. Пожалуйста, разместите конфликтующие модели дальше друг от друга (%s <-> %s)."
@@ -7141,6 +7144,22 @@ msgstr "Файл .gcode.3mf не содержит данных G-code. Испо
msgid "File '%s' was lost! Please download it again." msgid "File '%s' was lost! Please download it again."
msgstr "Файл '%s' потерян! Пожалуйста, загрузите его снова." 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 #, c-format, boost-format
msgid "" msgid ""
"File: %s\n" "File: %s\n"
@@ -11147,6 +11166,12 @@ msgstr "Отключить таймлапс и продолжить"
msgid "Clean Up" msgid "Clean Up"
msgstr "Очистить" 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" msgid "Preparing print job"
msgstr "Подготовка задания на печать" msgstr "Подготовка задания на печать"
@@ -27231,215 +27256,6 @@ msgstr "В этом шрифте нет контуров символов это
msgid "Size: %.1f × %.1f mm" msgid "Size: %.1f × %.1f mm"
msgstr "Размер: %.1f × %.1f мм" msgstr "Размер: %.1f × %.1f мм"
# AI Translated
msgid "Primitive"
msgstr "Фигура"
# AI Translated
msgid "Empty mesh generated"
msgstr "Создана пустая сетка"
# AI Translated
#. TRN Default name of an object created from the box primitive shape.
msgid "Box"
msgstr "Параллелепипед"
# AI Translated
#. TRN Primitive gizmo: label in front of the preset cube size buttons
msgid "Quick:"
msgstr "Быстрый выбор:"
# AI Translated
msgid "Width (X)"
msgstr "Ширина (X)"
# AI Translated
msgid "Depth (Y)"
msgstr "Глубина (Y)"
# AI Translated
msgid "Height (Z)"
msgstr "Высота (Z)"
# AI Translated
#. TRN Primitive gizmo: bottom radius of a cone ("Top R" is the top radius)
msgid "Bottom R"
msgstr "Нижний R"
# AI Translated
msgid "Top R"
msgstr "Верхний R"
# AI Translated
#. TRN Primitive gizmo: major radius of a torus ("Minor R" is the tube radius)
msgid "Major R"
msgstr "Большой R"
# AI Translated
msgid "Minor R"
msgstr "Малый R"
# AI Translated
msgid "All edges"
msgstr "Все рёбра"
# AI Translated
msgid "Top edges"
msgstr "Верхние рёбра"
# AI Translated
msgid "Bottom edges"
msgstr "Нижние рёбра"
# AI Translated
msgid "Lateral edges"
msgstr "Боковые рёбра"
# AI Translated
msgid "Mesh resolution"
msgstr "Разрешение сетки"
# AI Translated
msgid "Add Shape"
msgstr "Добавить фигуру"
# AI Translated
msgid "No outer profile"
msgstr "Нет внешнего профиля"
# AI Translated
msgid "Failed to make outer face"
msgstr "Не удалось создать внешнюю грань"
# AI Translated
msgid "Failed to build face with holes"
msgstr "Не удалось построить грань с отверстиями"
# AI Translated
msgid "Revolve failed"
msgstr "Ошибка вращения"
# AI Translated
msgid "Profile"
msgstr "Профиль"
# AI Translated
msgid "Start new profile (for holes)"
msgstr "Начать новый профиль (для отверстий)"
# AI Translated
msgid "Segments"
msgstr "Сегменты"
# AI Translated
msgid "Sides"
msgstr "Стороны"
# AI Translated
msgid "Click on canvas to draw"
msgstr "Щёлкайте в рабочем пространстве, чтобы рисовать"
# AI Translated
msgid "Snap to grid"
msgstr "Привязка к сетке"
# AI Translated
msgid "Grid step"
msgstr "Шаг сетки"
# AI Translated
msgid "Outer"
msgstr "Внешний"
# AI Translated
msgid "closed"
msgstr "замкнутый"
# AI Translated
#. TRN Sketch gizmo profile list: "Outer" or "Hole", profile number, point count, "closed" or nothing
#, boost-format
msgid "%1% %2%: %3% points %4%"
msgstr "%1% %2% (точек: %3%) %4%"
# AI Translated
msgid "XY (Top)"
msgstr "XY (сверху)"
# AI Translated
msgid "XZ (Front)"
msgstr "XZ (спереди)"
# AI Translated
msgid "YZ (Side)"
msgstr "YZ (сбоку)"
# AI Translated
msgid "Revolve deg"
msgstr "Угол вращения, °"
# AI Translated
msgid "Set to 0 for extrude, >0 for revolve"
msgstr "0 — выдавливание, >0 — вращение"
# AI Translated
msgid "Pocket (cut)"
msgstr "Карман (вырез)"
# AI Translated
#, c-format
msgid "%zu closed profile"
msgid_plural "%zu closed profiles"
msgstr[0] "%zu замкнутый профиль"
msgstr[1] "%zu замкнутых профиля"
msgstr[2] "%zu замкнутых профилей"
# AI Translated
msgid "Draw a closed profile to enable"
msgstr "Нарисуйте замкнутый профиль, чтобы включить"
# AI Translated
msgid "Clear all"
msgstr "Очистить всё"
# AI Translated
msgid "Empty result"
msgstr "Пустой результат"
# AI Translated
#, boost-format
msgid "Extrude: %1%"
msgstr "Выдавливание: %1%"
# AI Translated
#, boost-format
msgid "Revolve: %1%"
msgstr "Вращение: %1%"
# AI Translated
msgid "No object selected"
msgstr "Модель не выбрана"
# AI Translated
msgid "Face failed"
msgstr "Ошибка построения грани"
# AI Translated
msgid "Face with holes failed"
msgstr "Ошибка построения грани с отверстиями"
# AI Translated
msgid "Tool mesh empty"
msgstr "Сетка инструмента пуста"
# AI Translated
msgid "Pocket added (negative volume)"
msgstr "Карман добавлен (вырез)"
# AI Translated
#, boost-format
msgid "Pocket: %1%"
msgstr "Карман: %1%"
# AI Translated # AI Translated
msgctxt "Keyboard Shortcut" msgctxt "Keyboard Shortcut"
msgid "Ins" msgid "Ins"
@@ -30827,6 +30643,9 @@ msgstr "Включить инструменты разработчика"
msgid "An error occurred loading %s" msgid "An error occurred loading %s"
msgstr "Ошибка при загрузке %s" msgstr "Ошибка при загрузке %s"
msgid "/"
msgstr "/"
# AI Translated # AI Translated
#, boost-format #, boost-format
msgid "Load %1% to %2%" msgid "Load %1% to %2%"
@@ -31504,6 +31323,210 @@ msgstr ""
"Предотвращение коробления материала\n" "Предотвращение коробления материала\n"
"Знаете ли вы, что при печати материалами, склонными к короблению, таких как ABS, повышение температуры подогреваемого стола может снизить эту вероятность?" "Знаете ли вы, что при печати материалами, склонными к короблению, таких как ABS, повышение температуры подогреваемого стола может снизить эту вероятность?"
# AI Translated
#~ msgid "Primitive"
#~ msgstr "Фигура"
# AI Translated
#~ msgid "Empty mesh generated"
#~ msgstr "Создана пустая сетка"
# AI Translated
#~ msgid "Box"
#~ msgstr "Параллелепипед"
# AI Translated
#~ msgid "Quick:"
#~ msgstr "Быстрый выбор:"
# AI Translated
#~ msgid "Width (X)"
#~ msgstr "Ширина (X)"
# AI Translated
#~ msgid "Depth (Y)"
#~ msgstr "Глубина (Y)"
# AI Translated
#~ msgid "Height (Z)"
#~ msgstr "Высота (Z)"
# AI Translated
#~ msgid "Bottom R"
#~ msgstr "Нижний R"
# AI Translated
#~ msgid "Top R"
#~ msgstr "Верхний R"
# AI Translated
#~ msgid "Major R"
#~ msgstr "Большой R"
# AI Translated
#~ msgid "Minor R"
#~ msgstr "Малый R"
# AI Translated
#~ msgid "All edges"
#~ msgstr "Все рёбра"
# AI Translated
#~ msgid "Top edges"
#~ msgstr "Верхние рёбра"
# AI Translated
#~ msgid "Bottom edges"
#~ msgstr "Нижние рёбра"
# AI Translated
#~ msgid "Lateral edges"
#~ msgstr "Боковые рёбра"
# AI Translated
#~ msgid "Mesh resolution"
#~ msgstr "Разрешение сетки"
# AI Translated
#~ msgid "Add Shape"
#~ msgstr "Добавить фигуру"
# AI Translated
#~ msgid "No outer profile"
#~ msgstr "Нет внешнего профиля"
# AI Translated
#~ msgid "Failed to make outer face"
#~ msgstr "Не удалось создать внешнюю грань"
# AI Translated
#~ msgid "Failed to build face with holes"
#~ msgstr "Не удалось построить грань с отверстиями"
# AI Translated
#~ msgid "Revolve failed"
#~ msgstr "Ошибка вращения"
# AI Translated
#~ msgid "Profile"
#~ msgstr "Профиль"
# AI Translated
#~ msgid "Start new profile (for holes)"
#~ msgstr "Начать новый профиль (для отверстий)"
# AI Translated
#~ msgid "Segments"
#~ msgstr "Сегменты"
# AI Translated
#~ msgid "Sides"
#~ msgstr "Стороны"
# AI Translated
#~ msgid "Click on canvas to draw"
#~ msgstr "Щёлкайте в рабочем пространстве, чтобы рисовать"
# AI Translated
#~ msgid "Snap to grid"
#~ msgstr "Привязка к сетке"
# AI Translated
#~ msgid "Grid step"
#~ msgstr "Шаг сетки"
# AI Translated
#~ msgid "Outer"
#~ msgstr "Внешний"
# AI Translated
#~ msgid "closed"
#~ msgstr "замкнутый"
# AI Translated
#, boost-format
#~ msgid "%1% %2%: %3% points %4%"
#~ msgstr "%1% %2% (точек: %3%) %4%"
# AI Translated
#~ msgid "XY (Top)"
#~ msgstr "XY (сверху)"
# AI Translated
#~ msgid "XZ (Front)"
#~ msgstr "XZ (спереди)"
# AI Translated
#~ msgid "YZ (Side)"
#~ msgstr "YZ (сбоку)"
# AI Translated
#~ msgid "Revolve deg"
#~ msgstr "Угол вращения, °"
# AI Translated
#~ msgid "Set to 0 for extrude, >0 for revolve"
#~ msgstr "0 — выдавливание, >0 — вращение"
# AI Translated
#~ msgid "Pocket (cut)"
#~ msgstr "Карман (вырез)"
# AI Translated
#, c-format
#~ msgid "%zu closed profile"
#~ msgid_plural "%zu closed profiles"
#~ msgstr[0] "%zu замкнутый профиль"
#~ msgstr[1] "%zu замкнутых профиля"
#~ msgstr[2] "%zu замкнутых профилей"
# AI Translated
#~ msgid "Draw a closed profile to enable"
#~ msgstr "Нарисуйте замкнутый профиль, чтобы включить"
# AI Translated
#~ msgid "Clear all"
#~ msgstr "Очистить всё"
# AI Translated
#~ msgid "Empty result"
#~ msgstr "Пустой результат"
# AI Translated
#, boost-format
#~ msgid "Extrude: %1%"
#~ msgstr "Выдавливание: %1%"
# AI Translated
#, boost-format
#~ msgid "Revolve: %1%"
#~ msgstr "Вращение: %1%"
# AI Translated
#~ msgid "No object selected"
#~ msgstr "Модель не выбрана"
# AI Translated
#~ msgid "Face failed"
#~ msgstr "Ошибка построения грани"
# AI Translated
#~ msgid "Face with holes failed"
#~ msgstr "Ошибка построения грани с отверстиями"
# AI Translated
#~ msgid "Tool mesh empty"
#~ msgstr "Сетка инструмента пуста"
# AI Translated
#~ msgid "Pocket added (negative volume)"
#~ msgstr "Карман добавлен (вырез)"
# AI Translated
#, boost-format
#~ msgid "Pocket: %1%"
#~ msgstr "Карман: %1%"
# AI Translated # AI Translated
#~ msgid "(Experimental) Use printer agents instead of print hosts" #~ msgid "(Experimental) Use printer agents instead of print hosts"
#~ msgstr "(Экспериментально) Использовать агентов принтера вместо хостов печати" #~ msgstr "(Экспериментально) Использовать агентов принтера вместо хостов печати"
@@ -32571,9 +32594,6 @@ msgstr ""
#~ msgid "An error occurred loading " #~ msgid "An error occurred loading "
#~ msgstr "Ошибка при загрузке " #~ msgstr "Ошибка при загрузке "
#~ msgid "/"
#~ msgstr "/"
# AI Translated # AI Translated
#, c-format, boost-format #, c-format, boost-format
#~ msgid "Load %s to " #~ msgid "Load %s to "
+233 -213
View File
@@ -3,7 +3,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: Orca Slicer\n" "Project-Id-Version: Orca Slicer\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-06 15:16-0300\n" "POT-Creation-Date: 2026-10-08 11:08+0800\n"
"Language: sv\n" "Language: sv\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
@@ -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." msgid "You need to select the material type and color first."
msgstr "Du måste först välja materialtyp och färg." 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 # AI Translated
#, c-format, boost-format #, c-format, boost-format
msgid "Please input a valid value (K in %.1f~%.1f)" msgid "Please input a valid value (K in %.1f~%.1f)"
@@ -6837,7 +6840,7 @@ msgid "Size:"
msgstr "Storlek:" msgstr "Storlek:"
# AI Translated # 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)." 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)." msgstr "Konflikter mellan G-code-banor hittades på lager %d, Z = %.2lfmm. Placera de objekt som krockar längre ifrån varandra (%s <-> %s)."
@@ -7778,6 +7781,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." msgid "File '%s' was lost! Please download it again."
msgstr "Filen '%s' förlorades! Ladda ner den igen." 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 # AI Translated
#, c-format, boost-format #, c-format, boost-format
msgid "" msgid ""
@@ -12177,6 +12196,12 @@ msgstr "Avbryt timelapse och skriv ut"
msgid "Clean Up" msgid "Clean Up"
msgstr "Rensa" 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" msgid "Preparing print job"
msgstr "Förbereder utskriftsjobb" msgstr "Förbereder utskriftsjobb"
@@ -29271,214 +29296,6 @@ msgstr "Det här teckensnittet har ingen kontur för den här texten"
msgid "Size: %.1f × %.1f mm" msgid "Size: %.1f × %.1f mm"
msgstr "Storlek: %.1f × %.1f mm" msgstr "Storlek: %.1f × %.1f mm"
# AI Translated
msgid "Primitive"
msgstr "Primitiv"
# AI Translated
msgid "Empty mesh generated"
msgstr "Tom mesh genererades"
# AI Translated
#. TRN Default name of an object created from the box primitive shape.
msgid "Box"
msgstr "Låda"
# AI Translated
#. TRN Primitive gizmo: label in front of the preset cube size buttons
msgid "Quick:"
msgstr "Förval:"
# AI Translated
msgid "Width (X)"
msgstr "Bredd (X)"
# AI Translated
msgid "Depth (Y)"
msgstr "Djup (Y)"
# AI Translated
msgid "Height (Z)"
msgstr "Höjd (Z)"
# AI Translated
#. TRN Primitive gizmo: bottom radius of a cone ("Top R" is the top radius)
msgid "Bottom R"
msgstr "Nedre R"
# AI Translated
msgid "Top R"
msgstr "Övre R"
# AI Translated
#. TRN Primitive gizmo: major radius of a torus ("Minor R" is the tube radius)
msgid "Major R"
msgstr "Stor R"
# AI Translated
msgid "Minor R"
msgstr "Liten R"
# AI Translated
msgid "All edges"
msgstr "Alla kanter"
# AI Translated
msgid "Top edges"
msgstr "Övre kanter"
# AI Translated
msgid "Bottom edges"
msgstr "Nedre kanter"
# AI Translated
msgid "Lateral edges"
msgstr "Sidokanter"
# AI Translated
msgid "Mesh resolution"
msgstr "Meshupplösning"
# AI Translated
msgid "Add Shape"
msgstr "Lägg till form"
# AI Translated
msgid "No outer profile"
msgstr "Ingen yttre profil"
# AI Translated
msgid "Failed to make outer face"
msgstr "Det gick inte att skapa den yttre ytan"
# AI Translated
msgid "Failed to build face with holes"
msgstr "Det gick inte att skapa en yta med hål"
# AI Translated
msgid "Revolve failed"
msgstr "Rotationen misslyckades"
# AI Translated
msgid "Profile"
msgstr "Profil"
# AI Translated
msgid "Start new profile (for holes)"
msgstr "Starta ny profil (för hål)"
# AI Translated
msgid "Segments"
msgstr "Segment"
# AI Translated
msgid "Sides"
msgstr "Sidor"
# AI Translated
msgid "Click on canvas to draw"
msgstr "Klicka i vyn för att rita"
# AI Translated
msgid "Snap to grid"
msgstr "Fäst mot rutnät"
# AI Translated
msgid "Grid step"
msgstr "Rutnätssteg"
# AI Translated
msgid "Outer"
msgstr "Yttre"
# AI Translated
msgid "closed"
msgstr "sluten"
# AI Translated
#. TRN Sketch gizmo profile list: "Outer" or "Hole", profile number, point count, "closed" or nothing
#, boost-format
msgid "%1% %2%: %3% points %4%"
msgstr "%1% %2%: %3% punkter %4%"
# AI Translated
msgid "XY (Top)"
msgstr "XY (Topp)"
# AI Translated
msgid "XZ (Front)"
msgstr "XZ (Front)"
# AI Translated
msgid "YZ (Side)"
msgstr "YZ (Sida)"
# AI Translated
msgid "Revolve deg"
msgstr "Rotationsvinkel"
# AI Translated
msgid "Set to 0 for extrude, >0 for revolve"
msgstr "Ange 0 för extrudering, >0 för rotation"
# AI Translated
msgid "Pocket (cut)"
msgstr "Ficka (skär)"
# AI Translated
#, c-format
msgid "%zu closed profile"
msgid_plural "%zu closed profiles"
msgstr[0] "%zu sluten profil"
msgstr[1] "%zu slutna profiler"
# AI Translated
msgid "Draw a closed profile to enable"
msgstr "Rita en sluten profil för att aktivera"
# AI Translated
msgid "Clear all"
msgstr "Rensa alla"
# AI Translated
msgid "Empty result"
msgstr "Tomt resultat"
# AI Translated
#, boost-format
msgid "Extrude: %1%"
msgstr "Extrudera: %1%"
# AI Translated
#, boost-format
msgid "Revolve: %1%"
msgstr "Rotera: %1%"
# AI Translated
msgid "No object selected"
msgstr "Inget objekt valt"
# AI Translated
msgid "Face failed"
msgstr "Det gick inte att skapa ytan"
# AI Translated
msgid "Face with holes failed"
msgstr "Det gick inte att skapa ytan med hål"
# AI Translated
msgid "Tool mesh empty"
msgstr "Verktygets mesh är tom"
# AI Translated
msgid "Pocket added (negative volume)"
msgstr "Ficka tillagd (negativ volym)"
# AI Translated
#, boost-format
msgid "Pocket: %1%"
msgstr "Ficka: %1%"
# AI Translated # AI Translated
msgctxt "Keyboard Shortcut" msgctxt "Keyboard Shortcut"
msgid "Ins" msgid "Ins"
@@ -32868,6 +32685,9 @@ msgstr "Aktivera utvecklarverktyg"
msgid "An error occurred loading %s" msgid "An error occurred loading %s"
msgstr "Ett fel uppstod vid inläsning av %s" msgstr "Ett fel uppstod vid inläsning av %s"
msgid "/"
msgstr "/"
# AI Translated # AI Translated
#, boost-format #, boost-format
msgid "Load %1% to %2%" msgid "Load %1% to %2%"
@@ -33562,6 +33382,209 @@ msgstr ""
"Undvik vridning\n" "Undvik vridning\n"
"Visste du att när du skriver ut material som är benägna att vrida, såsom ABS, kan en lämplig ökning av värmebäddens temperatur minska sannolikheten för vridning?" "Visste du att när du skriver ut material som är benägna att vrida, såsom ABS, kan en lämplig ökning av värmebäddens temperatur minska sannolikheten för vridning?"
# AI Translated
#~ msgid "Primitive"
#~ msgstr "Primitiv"
# AI Translated
#~ msgid "Empty mesh generated"
#~ msgstr "Tom mesh genererades"
# AI Translated
#~ msgid "Box"
#~ msgstr "Låda"
# AI Translated
#~ msgid "Quick:"
#~ msgstr "Förval:"
# AI Translated
#~ msgid "Width (X)"
#~ msgstr "Bredd (X)"
# AI Translated
#~ msgid "Depth (Y)"
#~ msgstr "Djup (Y)"
# AI Translated
#~ msgid "Height (Z)"
#~ msgstr "Höjd (Z)"
# AI Translated
#~ msgid "Bottom R"
#~ msgstr "Nedre R"
# AI Translated
#~ msgid "Top R"
#~ msgstr "Övre R"
# AI Translated
#~ msgid "Major R"
#~ msgstr "Stor R"
# AI Translated
#~ msgid "Minor R"
#~ msgstr "Liten R"
# AI Translated
#~ msgid "All edges"
#~ msgstr "Alla kanter"
# AI Translated
#~ msgid "Top edges"
#~ msgstr "Övre kanter"
# AI Translated
#~ msgid "Bottom edges"
#~ msgstr "Nedre kanter"
# AI Translated
#~ msgid "Lateral edges"
#~ msgstr "Sidokanter"
# AI Translated
#~ msgid "Mesh resolution"
#~ msgstr "Meshupplösning"
# AI Translated
#~ msgid "Add Shape"
#~ msgstr "Lägg till form"
# AI Translated
#~ msgid "No outer profile"
#~ msgstr "Ingen yttre profil"
# AI Translated
#~ msgid "Failed to make outer face"
#~ msgstr "Det gick inte att skapa den yttre ytan"
# AI Translated
#~ msgid "Failed to build face with holes"
#~ msgstr "Det gick inte att skapa en yta med hål"
# AI Translated
#~ msgid "Revolve failed"
#~ msgstr "Rotationen misslyckades"
# AI Translated
#~ msgid "Profile"
#~ msgstr "Profil"
# AI Translated
#~ msgid "Start new profile (for holes)"
#~ msgstr "Starta ny profil (för hål)"
# AI Translated
#~ msgid "Segments"
#~ msgstr "Segment"
# AI Translated
#~ msgid "Sides"
#~ msgstr "Sidor"
# AI Translated
#~ msgid "Click on canvas to draw"
#~ msgstr "Klicka i vyn för att rita"
# AI Translated
#~ msgid "Snap to grid"
#~ msgstr "Fäst mot rutnät"
# AI Translated
#~ msgid "Grid step"
#~ msgstr "Rutnätssteg"
# AI Translated
#~ msgid "Outer"
#~ msgstr "Yttre"
# AI Translated
#~ msgid "closed"
#~ msgstr "sluten"
# AI Translated
#, boost-format
#~ msgid "%1% %2%: %3% points %4%"
#~ msgstr "%1% %2%: %3% punkter %4%"
# AI Translated
#~ msgid "XY (Top)"
#~ msgstr "XY (Topp)"
# AI Translated
#~ msgid "XZ (Front)"
#~ msgstr "XZ (Front)"
# AI Translated
#~ msgid "YZ (Side)"
#~ msgstr "YZ (Sida)"
# AI Translated
#~ msgid "Revolve deg"
#~ msgstr "Rotationsvinkel"
# AI Translated
#~ msgid "Set to 0 for extrude, >0 for revolve"
#~ msgstr "Ange 0 för extrudering, >0 för rotation"
# AI Translated
#~ msgid "Pocket (cut)"
#~ msgstr "Ficka (skär)"
# AI Translated
#, c-format
#~ msgid "%zu closed profile"
#~ msgid_plural "%zu closed profiles"
#~ msgstr[0] "%zu sluten profil"
#~ msgstr[1] "%zu slutna profiler"
# AI Translated
#~ msgid "Draw a closed profile to enable"
#~ msgstr "Rita en sluten profil för att aktivera"
# AI Translated
#~ msgid "Clear all"
#~ msgstr "Rensa alla"
# AI Translated
#~ msgid "Empty result"
#~ msgstr "Tomt resultat"
# AI Translated
#, boost-format
#~ msgid "Extrude: %1%"
#~ msgstr "Extrudera: %1%"
# AI Translated
#, boost-format
#~ msgid "Revolve: %1%"
#~ msgstr "Rotera: %1%"
# AI Translated
#~ msgid "No object selected"
#~ msgstr "Inget objekt valt"
# AI Translated
#~ msgid "Face failed"
#~ msgstr "Det gick inte att skapa ytan"
# AI Translated
#~ msgid "Face with holes failed"
#~ msgstr "Det gick inte att skapa ytan med hål"
# AI Translated
#~ msgid "Tool mesh empty"
#~ msgstr "Verktygets mesh är tom"
# AI Translated
#~ msgid "Pocket added (negative volume)"
#~ msgstr "Ficka tillagd (negativ volym)"
# AI Translated
#, boost-format
#~ msgid "Pocket: %1%"
#~ msgstr "Ficka: %1%"
# AI Translated # AI Translated
#~ msgid "(Experimental) Use printer agents instead of print hosts" #~ msgid "(Experimental) Use printer agents instead of print hosts"
#~ msgstr "(Experimentellt) Använd skrivaragenter i stället för utskriftsvärdar" #~ msgstr "(Experimentellt) Använd skrivaragenter i stället för utskriftsvärdar"
@@ -34687,9 +34710,6 @@ msgstr ""
#~ msgid "An error occurred loading " #~ msgid "An error occurred loading "
#~ msgstr "Ett fel uppstod vid inläsning av " #~ msgstr "Ett fel uppstod vid inläsning av "
#~ msgid "/"
#~ msgstr "/"
# AI Translated # AI Translated
#, c-format, boost-format #, c-format, boost-format
#~ msgid "Load %s to " #~ msgid "Load %s to "
+233 -213
View File
@@ -7,7 +7,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: Orca Slicer\n" "Project-Id-Version: Orca Slicer\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-06 15:16-0300\n" "POT-Creation-Date: 2026-10-08 11:08+0800\n"
"PO-Revision-Date: 2026-06-19 13:40+0700\n" "PO-Revision-Date: 2026-06-19 13:40+0700\n"
"Last-Translator: Icezaza\n" "Last-Translator: Icezaza\n"
"Language-Team: Thai\n" "Language-Team: Thai\n"
@@ -4332,6 +4332,9 @@ msgstr "คุณแน่ใจหรือไม่ว่าต้องกา
msgid "You need to select the material type and color first." msgid "You need to select the material type and color first."
msgstr "คุณต้องเลือกประเภทวัสดุและสีก่อน" msgstr "คุณต้องเลือกประเภทวัสดุและสีก่อน"
msgid "The minimum temperature cannot be greater than the maximum temperature."
msgstr ""
#, c-format, boost-format #, c-format, boost-format
msgid "Please input a valid value (K in %.1f~%.1f)" msgid "Please input a valid value (K in %.1f~%.1f)"
msgstr "โปรดป้อนค่าที่ถูกต้อง (K อยู่ในช่วง %.1f~%.1f)" msgstr "โปรดป้อนค่าที่ถูกต้อง (K อยู่ในช่วง %.1f~%.1f)"
@@ -6189,7 +6192,7 @@ msgid "Size:"
msgstr "ขนาด:" msgstr "ขนาด:"
# AI Translated # 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)." 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)" msgstr "พบความขัดแย้งของเส้นทาง G-code ที่เลเยอร์ %d, Z = %.2lfmm โปรดแยกวัตถุที่ขัดแย้งกันให้ไกลออกไป (%s <-> %s)"
@@ -7058,6 +7061,22 @@ msgstr "ไฟล์ .gcode.3mf ไม่มีข้อมูล G-code โป
msgid "File '%s' was lost! Please download it again." msgid "File '%s' was lost! Please download it again."
msgstr "ไฟล์ '%s' สูญหาย! กรุณาดาวน์โหลดอีกครั้ง." 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 #, c-format, boost-format
msgid "" msgid ""
"File: %s\n" "File: %s\n"
@@ -11065,6 +11084,12 @@ msgstr "ยกเลิกไทม์แลปส์และพิมพ์"
msgid "Clean Up" msgid "Clean Up"
msgstr "ล้าง" 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" msgid "Preparing print job"
msgstr "กำลังเตรียมงานพิมพ์" msgstr "กำลังเตรียมงานพิมพ์"
@@ -26617,213 +26642,6 @@ msgstr "แบบอักษรนี้ไม่มีเส้นขอบส
msgid "Size: %.1f × %.1f mm" msgid "Size: %.1f × %.1f mm"
msgstr "ขนาด: %.1f × %.1f มม." msgstr "ขนาด: %.1f × %.1f มม."
# AI Translated
msgid "Primitive"
msgstr "รูปทรงพื้นฐาน"
# AI Translated
msgid "Empty mesh generated"
msgstr "สร้างเมชว่างเปล่า"
# AI Translated
#. TRN Default name of an object created from the box primitive shape.
msgid "Box"
msgstr "กล่อง"
# AI Translated
#. TRN Primitive gizmo: label in front of the preset cube size buttons
msgid "Quick:"
msgstr "ขนาดด่วน:"
# AI Translated
msgid "Width (X)"
msgstr "ความกว้าง (X)"
# AI Translated
msgid "Depth (Y)"
msgstr "ความลึก (Y)"
# AI Translated
msgid "Height (Z)"
msgstr "ความสูง (Z)"
# AI Translated
#. TRN Primitive gizmo: bottom radius of a cone ("Top R" is the top radius)
msgid "Bottom R"
msgstr "R ล่าง"
# AI Translated
msgid "Top R"
msgstr "R บน"
# AI Translated
#. TRN Primitive gizmo: major radius of a torus ("Minor R" is the tube radius)
msgid "Major R"
msgstr "R หลัก"
# AI Translated
msgid "Minor R"
msgstr "R รอง"
# AI Translated
msgid "All edges"
msgstr "ขอบทั้งหมด"
# AI Translated
msgid "Top edges"
msgstr "ขอบบน"
# AI Translated
msgid "Bottom edges"
msgstr "ขอบล่าง"
# AI Translated
msgid "Lateral edges"
msgstr "ขอบด้านข้าง"
# AI Translated
msgid "Mesh resolution"
msgstr "ความละเอียดเมช"
# AI Translated
msgid "Add Shape"
msgstr "เพิ่มรูปทรง"
# AI Translated
msgid "No outer profile"
msgstr "ไม่มีโปรไฟล์ด้านนอก"
# AI Translated
msgid "Failed to make outer face"
msgstr "ไม่สามารถสร้างผิวหน้าด้านนอกได้"
# AI Translated
msgid "Failed to build face with holes"
msgstr "ไม่สามารถสร้างผิวหน้าที่มีรูได้"
# AI Translated
msgid "Revolve failed"
msgstr "หมุนขึ้นรูปล้มเหลว"
# AI Translated
msgid "Profile"
msgstr "โปรไฟล์"
# AI Translated
msgid "Start new profile (for holes)"
msgstr "เริ่มโปรไฟล์ใหม่ (สำหรับรู)"
# AI Translated
msgid "Segments"
msgstr "เซกเมนต์"
# AI Translated
msgid "Sides"
msgstr "ด้าน"
# AI Translated
msgid "Click on canvas to draw"
msgstr "คลิกบนพื้นที่วาดเพื่อวาด"
# AI Translated
msgid "Snap to grid"
msgstr "สแน็ปกับกริด"
# AI Translated
msgid "Grid step"
msgstr "ระยะกริด"
# AI Translated
msgid "Outer"
msgstr "ด้านนอก"
# AI Translated
msgid "closed"
msgstr "แบบปิด"
# AI Translated
#. TRN Sketch gizmo profile list: "Outer" or "Hole", profile number, point count, "closed" or nothing
#, boost-format
msgid "%1% %2%: %3% points %4%"
msgstr "%1% %2%: %3% จุด %4%"
# AI Translated
msgid "XY (Top)"
msgstr "XY (บน)"
# AI Translated
msgid "XZ (Front)"
msgstr "XZ (ด้านหน้า)"
# AI Translated
msgid "YZ (Side)"
msgstr "YZ (ด้านข้าง)"
# AI Translated
msgid "Revolve deg"
msgstr "องศาหมุนขึ้นรูป"
# AI Translated
msgid "Set to 0 for extrude, >0 for revolve"
msgstr "ตั้งเป็น 0 เพื่อยืดขึ้นรูป, >0 เพื่อหมุนขึ้นรูป"
# AI Translated
msgid "Pocket (cut)"
msgstr "ช่องเว้า (ตัด)"
# AI Translated
#, c-format
msgid "%zu closed profile"
msgid_plural "%zu closed profiles"
msgstr[0] "โปรไฟล์ปิด %zu รายการ"
# AI Translated
msgid "Draw a closed profile to enable"
msgstr "วาดโปรไฟล์ปิดเพื่อเปิดใช้งาน"
# AI Translated
msgid "Clear all"
msgstr "ล้างทั้งหมด"
# AI Translated
msgid "Empty result"
msgstr "ผลลัพธ์ว่างเปล่า"
# AI Translated
#, boost-format
msgid "Extrude: %1%"
msgstr "ยืดขึ้นรูป: %1%"
# AI Translated
#, boost-format
msgid "Revolve: %1%"
msgstr "หมุนขึ้นรูป: %1%"
# AI Translated
msgid "No object selected"
msgstr "ไม่ได้เลือกวัตถุ"
# AI Translated
msgid "Face failed"
msgstr "สร้างผิวหน้าล้มเหลว"
# AI Translated
msgid "Face with holes failed"
msgstr "สร้างผิวหน้าที่มีรูล้มเหลว"
# AI Translated
msgid "Tool mesh empty"
msgstr "เมชเครื่องมือว่างเปล่า"
# AI Translated
msgid "Pocket added (negative volume)"
msgstr "เพิ่มช่องเว้าแล้ว (วอลลุ่มเชิงลบ)"
# AI Translated
#, boost-format
msgid "Pocket: %1%"
msgstr "ช่องเว้า: %1%"
# AI Translated # AI Translated
msgctxt "Keyboard Shortcut" msgctxt "Keyboard Shortcut"
msgid "Ins" msgid "Ins"
@@ -30216,6 +30034,10 @@ msgstr "เปิดใช้งานเครื่องมือนักพ
msgid "An error occurred loading %s" msgid "An error occurred loading %s"
msgstr "เกิดข้อผิดพลาดในการโหลด %s" msgstr "เกิดข้อผิดพลาดในการโหลด %s"
# AI Translated
msgid "/"
msgstr "/"
# AI Translated # AI Translated
#, boost-format #, boost-format
msgid "Load %1% to %2%" msgid "Load %1% to %2%"
@@ -30895,6 +30717,208 @@ msgstr ""
"หลีกเลี่ยงการบิดเบี้ยว\n" "หลีกเลี่ยงการบิดเบี้ยว\n"
"คุณรู้หรือไม่ว่าเมื่อพิมพ์วัสดุที่มีแนวโน้มที่จะเกิดการบิดเบี้ยว เช่น ABS การเพิ่มอุณหภูมิฐานพิมพ์อย่างเหมาะสมสามารถลดความน่าจะเป็นของการบิดเบี้ยวได้" "คุณรู้หรือไม่ว่าเมื่อพิมพ์วัสดุที่มีแนวโน้มที่จะเกิดการบิดเบี้ยว เช่น ABS การเพิ่มอุณหภูมิฐานพิมพ์อย่างเหมาะสมสามารถลดความน่าจะเป็นของการบิดเบี้ยวได้"
# AI Translated
#~ msgid "Primitive"
#~ msgstr "รูปทรงพื้นฐาน"
# AI Translated
#~ msgid "Empty mesh generated"
#~ msgstr "สร้างเมชว่างเปล่า"
# AI Translated
#~ msgid "Box"
#~ msgstr "กล่อง"
# AI Translated
#~ msgid "Quick:"
#~ msgstr "ขนาดด่วน:"
# AI Translated
#~ msgid "Width (X)"
#~ msgstr "ความกว้าง (X)"
# AI Translated
#~ msgid "Depth (Y)"
#~ msgstr "ความลึก (Y)"
# AI Translated
#~ msgid "Height (Z)"
#~ msgstr "ความสูง (Z)"
# AI Translated
#~ msgid "Bottom R"
#~ msgstr "R ล่าง"
# AI Translated
#~ msgid "Top R"
#~ msgstr "R บน"
# AI Translated
#~ msgid "Major R"
#~ msgstr "R หลัก"
# AI Translated
#~ msgid "Minor R"
#~ msgstr "R รอง"
# AI Translated
#~ msgid "All edges"
#~ msgstr "ขอบทั้งหมด"
# AI Translated
#~ msgid "Top edges"
#~ msgstr "ขอบบน"
# AI Translated
#~ msgid "Bottom edges"
#~ msgstr "ขอบล่าง"
# AI Translated
#~ msgid "Lateral edges"
#~ msgstr "ขอบด้านข้าง"
# AI Translated
#~ msgid "Mesh resolution"
#~ msgstr "ความละเอียดเมช"
# AI Translated
#~ msgid "Add Shape"
#~ msgstr "เพิ่มรูปทรง"
# AI Translated
#~ msgid "No outer profile"
#~ msgstr "ไม่มีโปรไฟล์ด้านนอก"
# AI Translated
#~ msgid "Failed to make outer face"
#~ msgstr "ไม่สามารถสร้างผิวหน้าด้านนอกได้"
# AI Translated
#~ msgid "Failed to build face with holes"
#~ msgstr "ไม่สามารถสร้างผิวหน้าที่มีรูได้"
# AI Translated
#~ msgid "Revolve failed"
#~ msgstr "หมุนขึ้นรูปล้มเหลว"
# AI Translated
#~ msgid "Profile"
#~ msgstr "โปรไฟล์"
# AI Translated
#~ msgid "Start new profile (for holes)"
#~ msgstr "เริ่มโปรไฟล์ใหม่ (สำหรับรู)"
# AI Translated
#~ msgid "Segments"
#~ msgstr "เซกเมนต์"
# AI Translated
#~ msgid "Sides"
#~ msgstr "ด้าน"
# AI Translated
#~ msgid "Click on canvas to draw"
#~ msgstr "คลิกบนพื้นที่วาดเพื่อวาด"
# AI Translated
#~ msgid "Snap to grid"
#~ msgstr "สแน็ปกับกริด"
# AI Translated
#~ msgid "Grid step"
#~ msgstr "ระยะกริด"
# AI Translated
#~ msgid "Outer"
#~ msgstr "ด้านนอก"
# AI Translated
#~ msgid "closed"
#~ msgstr "แบบปิด"
# AI Translated
#, boost-format
#~ msgid "%1% %2%: %3% points %4%"
#~ msgstr "%1% %2%: %3% จุด %4%"
# AI Translated
#~ msgid "XY (Top)"
#~ msgstr "XY (บน)"
# AI Translated
#~ msgid "XZ (Front)"
#~ msgstr "XZ (ด้านหน้า)"
# AI Translated
#~ msgid "YZ (Side)"
#~ msgstr "YZ (ด้านข้าง)"
# AI Translated
#~ msgid "Revolve deg"
#~ msgstr "องศาหมุนขึ้นรูป"
# AI Translated
#~ msgid "Set to 0 for extrude, >0 for revolve"
#~ msgstr "ตั้งเป็น 0 เพื่อยืดขึ้นรูป, >0 เพื่อหมุนขึ้นรูป"
# AI Translated
#~ msgid "Pocket (cut)"
#~ msgstr "ช่องเว้า (ตัด)"
# AI Translated
#, c-format
#~ msgid "%zu closed profile"
#~ msgid_plural "%zu closed profiles"
#~ msgstr[0] "โปรไฟล์ปิด %zu รายการ"
# AI Translated
#~ msgid "Draw a closed profile to enable"
#~ msgstr "วาดโปรไฟล์ปิดเพื่อเปิดใช้งาน"
# AI Translated
#~ msgid "Clear all"
#~ msgstr "ล้างทั้งหมด"
# AI Translated
#~ msgid "Empty result"
#~ msgstr "ผลลัพธ์ว่างเปล่า"
# AI Translated
#, boost-format
#~ msgid "Extrude: %1%"
#~ msgstr "ยืดขึ้นรูป: %1%"
# AI Translated
#, boost-format
#~ msgid "Revolve: %1%"
#~ msgstr "หมุนขึ้นรูป: %1%"
# AI Translated
#~ msgid "No object selected"
#~ msgstr "ไม่ได้เลือกวัตถุ"
# AI Translated
#~ msgid "Face failed"
#~ msgstr "สร้างผิวหน้าล้มเหลว"
# AI Translated
#~ msgid "Face with holes failed"
#~ msgstr "สร้างผิวหน้าที่มีรูล้มเหลว"
# AI Translated
#~ msgid "Tool mesh empty"
#~ msgstr "เมชเครื่องมือว่างเปล่า"
# AI Translated
#~ msgid "Pocket added (negative volume)"
#~ msgstr "เพิ่มช่องเว้าแล้ว (วอลลุ่มเชิงลบ)"
# AI Translated
#, boost-format
#~ msgid "Pocket: %1%"
#~ msgstr "ช่องเว้า: %1%"
# AI Translated # AI Translated
#~ msgid "(Experimental) Use printer agents instead of print hosts" #~ msgid "(Experimental) Use printer agents instead of print hosts"
#~ msgstr "(ทดลอง) ใช้เอเจนต์เครื่องพิมพ์แทนโฮสต์การพิมพ์" #~ msgstr "(ทดลอง) ใช้เอเจนต์เครื่องพิมพ์แทนโฮสต์การพิมพ์"
@@ -31952,10 +31976,6 @@ msgstr ""
#~ msgid "An error occurred loading " #~ msgid "An error occurred loading "
#~ msgstr "เกิดข้อผิดพลาดในการโหลด " #~ msgstr "เกิดข้อผิดพลาดในการโหลด "
# AI Translated
#~ msgid "/"
#~ msgstr "/"
# AI Translated # AI Translated
#, c-format, boost-format #, c-format, boost-format
#~ msgid "Load %s to " #~ msgid "Load %s to "
+234 -214
View File
@@ -3,7 +3,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: Orca Slicer\n" "Project-Id-Version: Orca Slicer\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-06 15:16-0300\n" "POT-Creation-Date: 2026-10-08 11:08+0800\n"
"PO-Revision-Date: 2026-08-21 23:18+0300\n" "PO-Revision-Date: 2026-08-21 23:18+0300\n"
"Last-Translator: GlauTech\n" "Last-Translator: GlauTech\n"
"Language-Team: \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." msgid "You need to select the material type and color first."
msgstr "Önce malzeme türünü ve rengini seçmeniz gerekir." 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 #, c-format, boost-format
msgid "Please input a valid value (K in %.1f~%.1f)" msgid "Please input a valid value (K in %.1f~%.1f)"
msgstr "Lütfen geçerli bir değer girin (K %.1f~%.1f içinde)" msgstr "Lütfen geçerli bir değer girin (K %.1f~%.1f içinde)"
@@ -6255,7 +6258,7 @@ msgstr "Hacim:"
msgid "Size:" msgid "Size:"
msgstr "Boyut:" 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)." 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)." 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)."
@@ -7131,6 +7134,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." msgid "File '%s' was lost! Please download it again."
msgstr "'%s' dosyası kayboldu! Lütfen tekrar indirin." 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 #, c-format, boost-format
msgid "" msgid ""
"File: %s\n" "File: %s\n"
@@ -11201,6 +11220,12 @@ msgstr "Timelapse'i İptal Et ve Yazdır"
msgid "Clean Up" msgid "Clean Up"
msgstr "Temizle" 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" msgid "Preparing print job"
msgstr "Yazdırma için hazırlanıyor" msgstr "Yazdırma için hazırlanıyor"
@@ -27062,214 +27087,6 @@ msgstr "Bu yazı tipinde bu metin için dış hat yok"
msgid "Size: %.1f × %.1f mm" msgid "Size: %.1f × %.1f mm"
msgstr "Boyut: %.1f × %.1f mm" msgstr "Boyut: %.1f × %.1f mm"
# AI Translated
msgid "Primitive"
msgstr "Şekil"
# AI Translated
msgid "Empty mesh generated"
msgstr "Boş mesh oluşturuldu"
# AI Translated
#. TRN Default name of an object created from the box primitive shape.
msgid "Box"
msgstr "Kutu"
# AI Translated
#. TRN Primitive gizmo: label in front of the preset cube size buttons
msgid "Quick:"
msgstr "Hızlı:"
# AI Translated
msgid "Width (X)"
msgstr "Genişlik (X)"
# AI Translated
msgid "Depth (Y)"
msgstr "Derinlik (Y)"
# AI Translated
msgid "Height (Z)"
msgstr "Yükseklik (Z)"
# AI Translated
#. TRN Primitive gizmo: bottom radius of a cone ("Top R" is the top radius)
msgid "Bottom R"
msgstr "Alt R"
# AI Translated
msgid "Top R"
msgstr "Üst R"
# AI Translated
#. TRN Primitive gizmo: major radius of a torus ("Minor R" is the tube radius)
msgid "Major R"
msgstr "Büyük R"
# AI Translated
msgid "Minor R"
msgstr "Küçük R"
# AI Translated
msgid "All edges"
msgstr "Tüm kenarlar"
# AI Translated
msgid "Top edges"
msgstr "Üst kenarlar"
# AI Translated
msgid "Bottom edges"
msgstr "Alt kenarlar"
# AI Translated
msgid "Lateral edges"
msgstr "Yan kenarlar"
# AI Translated
msgid "Mesh resolution"
msgstr "Mesh çözünürlüğü"
# AI Translated
msgid "Add Shape"
msgstr "Şekil ekle"
# AI Translated
msgid "No outer profile"
msgstr "Dış profil yok"
# AI Translated
msgid "Failed to make outer face"
msgstr "Dış yüz oluşturulamadı"
# AI Translated
msgid "Failed to build face with holes"
msgstr "Delikli yüz oluşturulamadı"
# AI Translated
msgid "Revolve failed"
msgstr "Döndürme başarısız oldu"
# AI Translated
msgid "Profile"
msgstr "Profil"
# AI Translated
msgid "Start new profile (for holes)"
msgstr "Yeni profil başlat (delikler için)"
# AI Translated
msgid "Segments"
msgstr "Segmentler"
# AI Translated
msgid "Sides"
msgstr "Kenar sayısı"
# AI Translated
msgid "Click on canvas to draw"
msgstr "Çizmek için tuvale tıklayın"
# AI Translated
msgid "Snap to grid"
msgstr "Izgaraya yasla"
# AI Translated
msgid "Grid step"
msgstr "Izgara adımı"
# AI Translated
msgid "Outer"
msgstr "Dış"
# AI Translated
msgid "closed"
msgstr "kapalı"
# AI Translated
#. TRN Sketch gizmo profile list: "Outer" or "Hole", profile number, point count, "closed" or nothing
#, boost-format
msgid "%1% %2%: %3% points %4%"
msgstr "%1% %2%: %3% nokta %4%"
# AI Translated
msgid "XY (Top)"
msgstr "XY (Üst)"
# AI Translated
msgid "XZ (Front)"
msgstr "XZ (Ön)"
# AI Translated
msgid "YZ (Side)"
msgstr "YZ (Yan)"
# AI Translated
msgid "Revolve deg"
msgstr "Döndürme açısı"
# AI Translated
msgid "Set to 0 for extrude, >0 for revolve"
msgstr "Ekstrüzyon için 0, döndürme için >0 girin"
# AI Translated
msgid "Pocket (cut)"
msgstr "Cep (kes)"
# AI Translated
#, c-format
msgid "%zu closed profile"
msgid_plural "%zu closed profiles"
msgstr[0] "%zu kapalı profil"
msgstr[1] "%zu kapalı profil"
# AI Translated
msgid "Draw a closed profile to enable"
msgstr "Etkinleştirmek için kapalı bir profil çizin"
# AI Translated
msgid "Clear all"
msgstr "Tümünü temizle"
# AI Translated
msgid "Empty result"
msgstr "Boş sonuç"
# AI Translated
#, boost-format
msgid "Extrude: %1%"
msgstr "Ekstrüzyon: %1%"
# AI Translated
#, boost-format
msgid "Revolve: %1%"
msgstr "Döndürme: %1%"
# AI Translated
msgid "No object selected"
msgstr "Nesne seçilmedi"
# AI Translated
msgid "Face failed"
msgstr "Yüz oluşturulamadı"
# AI Translated
msgid "Face with holes failed"
msgstr "Delikli yüz oluşturulamadı"
# AI Translated
msgid "Tool mesh empty"
msgstr "Araç mesh'i boş"
# AI Translated
msgid "Pocket added (negative volume)"
msgstr "Cep eklendi (negatif hacim)"
# AI Translated
#, boost-format
msgid "Pocket: %1%"
msgstr "Cep: %1%"
# AI Translated # AI Translated
msgctxt "Keyboard Shortcut" msgctxt "Keyboard Shortcut"
msgid "Ins" msgid "Ins"
@@ -30665,6 +30482,10 @@ msgstr "Geliştirici Araçlarını Etkinleştir"
msgid "An error occurred loading %s" msgid "An error occurred loading %s"
msgstr "%s yüklenirken bir hata oluştu" msgstr "%s yüklenirken bir hata oluştu"
# AI Translated
msgid "/"
msgstr "/"
# AI Translated # AI Translated
#, boost-format #, boost-format
msgid "Load %1% to %2%" msgid "Load %1% to %2%"
@@ -31346,6 +31167,209 @@ msgstr ""
"Eğilmeyi önleyin\n" "Eğilmeyi önleyin\n"
"ABS gibi bükülmeye yatkın malzemelere baskı yaparken, ısıtma yatağı sıcaklığının uygun şekilde arttırılmasının bükülme olasılığını azaltabileceğini biliyor muydunuz?" "ABS gibi bükülmeye yatkın malzemelere baskı yaparken, ısıtma yatağı sıcaklığının uygun şekilde arttırılmasının bükülme olasılığını azaltabileceğini biliyor muydunuz?"
# AI Translated
#~ msgid "Primitive"
#~ msgstr "Şekil"
# AI Translated
#~ msgid "Empty mesh generated"
#~ msgstr "Boş mesh oluşturuldu"
# AI Translated
#~ msgid "Box"
#~ msgstr "Kutu"
# AI Translated
#~ msgid "Quick:"
#~ msgstr "Hızlı:"
# AI Translated
#~ msgid "Width (X)"
#~ msgstr "Genişlik (X)"
# AI Translated
#~ msgid "Depth (Y)"
#~ msgstr "Derinlik (Y)"
# AI Translated
#~ msgid "Height (Z)"
#~ msgstr "Yükseklik (Z)"
# AI Translated
#~ msgid "Bottom R"
#~ msgstr "Alt R"
# AI Translated
#~ msgid "Top R"
#~ msgstr "Üst R"
# AI Translated
#~ msgid "Major R"
#~ msgstr "Büyük R"
# AI Translated
#~ msgid "Minor R"
#~ msgstr "Küçük R"
# AI Translated
#~ msgid "All edges"
#~ msgstr "Tüm kenarlar"
# AI Translated
#~ msgid "Top edges"
#~ msgstr "Üst kenarlar"
# AI Translated
#~ msgid "Bottom edges"
#~ msgstr "Alt kenarlar"
# AI Translated
#~ msgid "Lateral edges"
#~ msgstr "Yan kenarlar"
# AI Translated
#~ msgid "Mesh resolution"
#~ msgstr "Mesh çözünürlüğü"
# AI Translated
#~ msgid "Add Shape"
#~ msgstr "Şekil ekle"
# AI Translated
#~ msgid "No outer profile"
#~ msgstr "Dış profil yok"
# AI Translated
#~ msgid "Failed to make outer face"
#~ msgstr "Dış yüz oluşturulamadı"
# AI Translated
#~ msgid "Failed to build face with holes"
#~ msgstr "Delikli yüz oluşturulamadı"
# AI Translated
#~ msgid "Revolve failed"
#~ msgstr "Döndürme başarısız oldu"
# AI Translated
#~ msgid "Profile"
#~ msgstr "Profil"
# AI Translated
#~ msgid "Start new profile (for holes)"
#~ msgstr "Yeni profil başlat (delikler için)"
# AI Translated
#~ msgid "Segments"
#~ msgstr "Segmentler"
# AI Translated
#~ msgid "Sides"
#~ msgstr "Kenar sayısı"
# AI Translated
#~ msgid "Click on canvas to draw"
#~ msgstr "Çizmek için tuvale tıklayın"
# AI Translated
#~ msgid "Snap to grid"
#~ msgstr "Izgaraya yasla"
# AI Translated
#~ msgid "Grid step"
#~ msgstr "Izgara adımı"
# AI Translated
#~ msgid "Outer"
#~ msgstr "Dış"
# AI Translated
#~ msgid "closed"
#~ msgstr "kapalı"
# AI Translated
#, boost-format
#~ msgid "%1% %2%: %3% points %4%"
#~ msgstr "%1% %2%: %3% nokta %4%"
# AI Translated
#~ msgid "XY (Top)"
#~ msgstr "XY (Üst)"
# AI Translated
#~ msgid "XZ (Front)"
#~ msgstr "XZ (Ön)"
# AI Translated
#~ msgid "YZ (Side)"
#~ msgstr "YZ (Yan)"
# AI Translated
#~ msgid "Revolve deg"
#~ msgstr "Döndürme açısı"
# AI Translated
#~ msgid "Set to 0 for extrude, >0 for revolve"
#~ msgstr "Ekstrüzyon için 0, döndürme için >0 girin"
# AI Translated
#~ msgid "Pocket (cut)"
#~ msgstr "Cep (kes)"
# AI Translated
#, c-format
#~ msgid "%zu closed profile"
#~ msgid_plural "%zu closed profiles"
#~ msgstr[0] "%zu kapalı profil"
#~ msgstr[1] "%zu kapalı profil"
# AI Translated
#~ msgid "Draw a closed profile to enable"
#~ msgstr "Etkinleştirmek için kapalı bir profil çizin"
# AI Translated
#~ msgid "Clear all"
#~ msgstr "Tümünü temizle"
# AI Translated
#~ msgid "Empty result"
#~ msgstr "Boş sonuç"
# AI Translated
#, boost-format
#~ msgid "Extrude: %1%"
#~ msgstr "Ekstrüzyon: %1%"
# AI Translated
#, boost-format
#~ msgid "Revolve: %1%"
#~ msgstr "Döndürme: %1%"
# AI Translated
#~ msgid "No object selected"
#~ msgstr "Nesne seçilmedi"
# AI Translated
#~ msgid "Face failed"
#~ msgstr "Yüz oluşturulamadı"
# AI Translated
#~ msgid "Face with holes failed"
#~ msgstr "Delikli yüz oluşturulamadı"
# AI Translated
#~ msgid "Tool mesh empty"
#~ msgstr "Araç mesh'i boş"
# AI Translated
#~ msgid "Pocket added (negative volume)"
#~ msgstr "Cep eklendi (negatif hacim)"
# AI Translated
#, boost-format
#~ msgid "Pocket: %1%"
#~ msgstr "Cep: %1%"
# AI Translated # AI Translated
#~ msgid "(Experimental) Use printer agents instead of print hosts" #~ msgid "(Experimental) Use printer agents instead of print hosts"
#~ msgstr "(Deneysel) Baskı sunucuları yerine yazıcı aracılarını kullan" #~ msgstr "(Deneysel) Baskı sunucuları yerine yazıcı aracılarını kullan"
@@ -32401,10 +32425,6 @@ msgstr ""
#~ msgid "An error occurred loading " #~ msgid "An error occurred loading "
#~ msgstr "Yüklenirken bir hata oluştu: " #~ msgstr "Yüklenirken bir hata oluştu: "
# AI Translated
#~ msgid "/"
#~ msgstr "/"
# AI Translated # AI Translated
#, c-format, boost-format #, c-format, boost-format
#~ msgid "Load %s to " #~ msgid "Load %s to "
+235 -215
View File
@@ -3,7 +3,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: orcaslicerua\n" "Project-Id-Version: orcaslicerua\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-06 15:16-0300\n" "POT-Creation-Date: 2026-10-08 11:08+0800\n"
"PO-Revision-Date: 2026-07-17 16:25+0300\n" "PO-Revision-Date: 2026-07-17 16:25+0300\n"
"Last-Translator: Andrij Mizyk <andm1zyk@proton.me>\n" "Last-Translator: Andrij Mizyk <andm1zyk@proton.me>\n"
"Language-Team: Ukrainian\n" "Language-Team: Ukrainian\n"
@@ -4298,6 +4298,9 @@ msgstr "Ви впевнені, що хочете видалити інформа
msgid "You need to select the material type and color first." msgid "You need to select the material type and color first."
msgstr "Спочатку потрібно вибрати тип матеріалу та колір." msgstr "Спочатку потрібно вибрати тип матеріалу та колір."
msgid "The minimum temperature cannot be greater than the maximum temperature."
msgstr ""
#, c-format, boost-format #, c-format, boost-format
msgid "Please input a valid value (K in %.1f~%.1f)" msgid "Please input a valid value (K in %.1f~%.1f)"
msgstr "Будь ласка введіть коректне значення (K в %.1f~%.1f)" msgstr "Будь ласка введіть коректне значення (K в %.1f~%.1f)"
@@ -6209,7 +6212,7 @@ msgid "Size:"
msgstr "Розмір:" msgstr "Розмір:"
# AI Translated # 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)." 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)." msgstr "Виявлено конфлікти шляхів G-code на шарі %d, Z = %.2lf мм. Будь ласка, рознесіть конфліктуючі обʼєкти далі один від одного (%s <-> %s)."
@@ -7100,6 +7103,22 @@ msgstr "Файл .gcode.3mf не містить даних G-code. Будь ла
msgid "File '%s' was lost! Please download it again." msgid "File '%s' was lost! Please download it again."
msgstr "Файл '%s' втрачений! Завантажте його знову." 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 #, c-format, boost-format
msgid "" msgid ""
"File: %s\n" "File: %s\n"
@@ -11218,6 +11237,12 @@ msgstr "Скасувати таймлапс і друкувати"
msgid "Clean Up" msgid "Clean Up"
msgstr "Очистити" 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" msgid "Preparing print job"
msgstr "Підготовка завдання для друку" msgstr "Підготовка завдання для друку"
@@ -27229,216 +27254,6 @@ msgstr "У цьому шрифті немає контурів символів
msgid "Size: %.1f × %.1f mm" msgid "Size: %.1f × %.1f mm"
msgstr "Розмір: %.1f × %.1f мм" msgstr "Розмір: %.1f × %.1f мм"
# AI Translated
msgid "Primitive"
msgstr "Примітив"
# AI Translated
msgid "Empty mesh generated"
msgstr "Згенеровано порожню сітку"
# AI Translated
#. TRN Default name of an object created from the box primitive shape.
msgid "Box"
msgstr "Паралелепіпед"
# AI Translated
#. TRN Primitive gizmo: label in front of the preset cube size buttons
msgid "Quick:"
msgstr "Швидкий вибір:"
# AI Translated
msgid "Width (X)"
msgstr "Ширина (X)"
# AI Translated
msgid "Depth (Y)"
msgstr "Глибина (Y)"
# AI Translated
msgid "Height (Z)"
msgstr "Висота (Z)"
# AI Translated
#. TRN Primitive gizmo: bottom radius of a cone ("Top R" is the top radius)
msgid "Bottom R"
msgstr "Нижній R"
# AI Translated
msgid "Top R"
msgstr "Верхній R"
# AI Translated
#. TRN Primitive gizmo: major radius of a torus ("Minor R" is the tube radius)
msgid "Major R"
msgstr "Великий R"
# AI Translated
msgid "Minor R"
msgstr "Малий R"
# AI Translated
msgid "All edges"
msgstr "Усі ребра"
# AI Translated
msgid "Top edges"
msgstr "Верхні ребра"
# AI Translated
msgid "Bottom edges"
msgstr "Нижні ребра"
# AI Translated
msgid "Lateral edges"
msgstr "Бічні ребра"
# AI Translated
msgid "Mesh resolution"
msgstr "Роздільна здатність сітки"
# AI Translated
msgid "Add Shape"
msgstr "Додати форму"
# AI Translated
msgid "No outer profile"
msgstr "Немає зовнішнього профілю"
# AI Translated
msgid "Failed to make outer face"
msgstr "Не вдалося створити зовнішню грань"
# AI Translated
msgid "Failed to build face with holes"
msgstr "Не вдалося побудувати грань з отворами"
# AI Translated
msgid "Revolve failed"
msgstr "Не вдалося виконати обертання"
# AI Translated
msgid "Profile"
msgstr "Профіль"
# AI Translated
msgid "Start new profile (for holes)"
msgstr "Почати новий профіль (для отворів)"
# AI Translated
msgid "Segments"
msgstr "Сегменти"
# AI Translated
msgid "Sides"
msgstr "Сторони"
# AI Translated
msgid "Click on canvas to draw"
msgstr "Клацніть на полотні, щоб малювати"
# AI Translated
msgid "Snap to grid"
msgstr "Привʼязка до сітки"
# AI Translated
msgid "Grid step"
msgstr "Крок сітки"
# AI Translated
msgid "Outer"
msgstr "Зовнішній"
# AI Translated
msgid "closed"
msgstr "замкнений"
# AI Translated
#. TRN Sketch gizmo profile list: "Outer" or "Hole", profile number, point count, "closed" or nothing
#, boost-format
msgid "%1% %2%: %3% points %4%"
msgstr "%1% %2% (точок: %3%) %4%"
# AI Translated
msgid "XY (Top)"
msgstr "XY (Верх)"
# AI Translated
msgid "XZ (Front)"
msgstr "XZ (Перед)"
# AI Translated
msgid "YZ (Side)"
msgstr "YZ (Бік)"
# AI Translated
msgid "Revolve deg"
msgstr "Обертання, °"
# AI Translated
msgid "Set to 0 for extrude, >0 for revolve"
msgstr "0 — видавлювання, >0 — обертання"
# AI Translated
msgid "Pocket (cut)"
msgstr "Кишеня (вирізання)"
# AI Translated
#, c-format
msgid "%zu closed profile"
msgid_plural "%zu closed profiles"
msgstr[0] "%zu замкнений профіль"
msgstr[1] "%zu замкнені профілі"
msgstr[2] "%zu замкнених профілів"
msgstr[3] "%zu замкненого профілю"
# AI Translated
msgid "Draw a closed profile to enable"
msgstr "Намалюйте замкнений профіль, щоб увімкнути"
# AI Translated
msgid "Clear all"
msgstr "Очистити все"
# AI Translated
msgid "Empty result"
msgstr "Порожній результат"
# AI Translated
#, boost-format
msgid "Extrude: %1%"
msgstr "Видавлювання: %1%"
# AI Translated
#, boost-format
msgid "Revolve: %1%"
msgstr "Обертання: %1%"
# AI Translated
msgid "No object selected"
msgstr "Не вибрано обʼєкт"
# AI Translated
msgid "Face failed"
msgstr "Не вдалося створити грань"
# AI Translated
msgid "Face with holes failed"
msgstr "Не вдалося створити грань з отворами"
# AI Translated
msgid "Tool mesh empty"
msgstr "Сітка інструмента порожня"
# AI Translated
msgid "Pocket added (negative volume)"
msgstr "Кишеню додано (відʼємний обʼєм)"
# AI Translated
#, boost-format
msgid "Pocket: %1%"
msgstr "Кишеня: %1%"
# AI Translated # AI Translated
msgctxt "Keyboard Shortcut" msgctxt "Keyboard Shortcut"
msgid "Ins" msgid "Ins"
@@ -30832,6 +30647,9 @@ msgstr "Увімкнути інструменти розробника"
msgid "An error occurred loading %s" msgid "An error occurred loading %s"
msgstr "Помилка під час завантаження %s" msgstr "Помилка під час завантаження %s"
msgid "/"
msgstr "/"
# AI Translated # AI Translated
#, boost-format #, boost-format
msgid "Load %1% to %2%" msgid "Load %1% to %2%"
@@ -31516,6 +31334,211 @@ msgstr ""
"Уникнення деформації\n" "Уникнення деформації\n"
"Чи знаєте ви, що при друку матеріалами, схильними до деформації, такими як ABS, відповідне підвищення температури столу може зменшити ймовірність деформації?" "Чи знаєте ви, що при друку матеріалами, схильними до деформації, такими як ABS, відповідне підвищення температури столу може зменшити ймовірність деформації?"
# AI Translated
#~ msgid "Primitive"
#~ msgstr "Примітив"
# AI Translated
#~ msgid "Empty mesh generated"
#~ msgstr "Згенеровано порожню сітку"
# AI Translated
#~ msgid "Box"
#~ msgstr "Паралелепіпед"
# AI Translated
#~ msgid "Quick:"
#~ msgstr "Швидкий вибір:"
# AI Translated
#~ msgid "Width (X)"
#~ msgstr "Ширина (X)"
# AI Translated
#~ msgid "Depth (Y)"
#~ msgstr "Глибина (Y)"
# AI Translated
#~ msgid "Height (Z)"
#~ msgstr "Висота (Z)"
# AI Translated
#~ msgid "Bottom R"
#~ msgstr "Нижній R"
# AI Translated
#~ msgid "Top R"
#~ msgstr "Верхній R"
# AI Translated
#~ msgid "Major R"
#~ msgstr "Великий R"
# AI Translated
#~ msgid "Minor R"
#~ msgstr "Малий R"
# AI Translated
#~ msgid "All edges"
#~ msgstr "Усі ребра"
# AI Translated
#~ msgid "Top edges"
#~ msgstr "Верхні ребра"
# AI Translated
#~ msgid "Bottom edges"
#~ msgstr "Нижні ребра"
# AI Translated
#~ msgid "Lateral edges"
#~ msgstr "Бічні ребра"
# AI Translated
#~ msgid "Mesh resolution"
#~ msgstr "Роздільна здатність сітки"
# AI Translated
#~ msgid "Add Shape"
#~ msgstr "Додати форму"
# AI Translated
#~ msgid "No outer profile"
#~ msgstr "Немає зовнішнього профілю"
# AI Translated
#~ msgid "Failed to make outer face"
#~ msgstr "Не вдалося створити зовнішню грань"
# AI Translated
#~ msgid "Failed to build face with holes"
#~ msgstr "Не вдалося побудувати грань з отворами"
# AI Translated
#~ msgid "Revolve failed"
#~ msgstr "Не вдалося виконати обертання"
# AI Translated
#~ msgid "Profile"
#~ msgstr "Профіль"
# AI Translated
#~ msgid "Start new profile (for holes)"
#~ msgstr "Почати новий профіль (для отворів)"
# AI Translated
#~ msgid "Segments"
#~ msgstr "Сегменти"
# AI Translated
#~ msgid "Sides"
#~ msgstr "Сторони"
# AI Translated
#~ msgid "Click on canvas to draw"
#~ msgstr "Клацніть на полотні, щоб малювати"
# AI Translated
#~ msgid "Snap to grid"
#~ msgstr "Привʼязка до сітки"
# AI Translated
#~ msgid "Grid step"
#~ msgstr "Крок сітки"
# AI Translated
#~ msgid "Outer"
#~ msgstr "Зовнішній"
# AI Translated
#~ msgid "closed"
#~ msgstr "замкнений"
# AI Translated
#, boost-format
#~ msgid "%1% %2%: %3% points %4%"
#~ msgstr "%1% %2% (точок: %3%) %4%"
# AI Translated
#~ msgid "XY (Top)"
#~ msgstr "XY (Верх)"
# AI Translated
#~ msgid "XZ (Front)"
#~ msgstr "XZ (Перед)"
# AI Translated
#~ msgid "YZ (Side)"
#~ msgstr "YZ (Бік)"
# AI Translated
#~ msgid "Revolve deg"
#~ msgstr "Обертання, °"
# AI Translated
#~ msgid "Set to 0 for extrude, >0 for revolve"
#~ msgstr "0 — видавлювання, >0 — обертання"
# AI Translated
#~ msgid "Pocket (cut)"
#~ msgstr "Кишеня (вирізання)"
# AI Translated
#, c-format
#~ msgid "%zu closed profile"
#~ msgid_plural "%zu closed profiles"
#~ msgstr[0] "%zu замкнений профіль"
#~ msgstr[1] "%zu замкнені профілі"
#~ msgstr[2] "%zu замкнених профілів"
#~ msgstr[3] "%zu замкненого профілю"
# AI Translated
#~ msgid "Draw a closed profile to enable"
#~ msgstr "Намалюйте замкнений профіль, щоб увімкнути"
# AI Translated
#~ msgid "Clear all"
#~ msgstr "Очистити все"
# AI Translated
#~ msgid "Empty result"
#~ msgstr "Порожній результат"
# AI Translated
#, boost-format
#~ msgid "Extrude: %1%"
#~ msgstr "Видавлювання: %1%"
# AI Translated
#, boost-format
#~ msgid "Revolve: %1%"
#~ msgstr "Обертання: %1%"
# AI Translated
#~ msgid "No object selected"
#~ msgstr "Не вибрано обʼєкт"
# AI Translated
#~ msgid "Face failed"
#~ msgstr "Не вдалося створити грань"
# AI Translated
#~ msgid "Face with holes failed"
#~ msgstr "Не вдалося створити грань з отворами"
# AI Translated
#~ msgid "Tool mesh empty"
#~ msgstr "Сітка інструмента порожня"
# AI Translated
#~ msgid "Pocket added (negative volume)"
#~ msgstr "Кишеню додано (відʼємний обʼєм)"
# AI Translated
#, boost-format
#~ msgid "Pocket: %1%"
#~ msgstr "Кишеня: %1%"
# AI Translated # AI Translated
#~ msgid "(Experimental) Use printer agents instead of print hosts" #~ msgid "(Experimental) Use printer agents instead of print hosts"
#~ msgstr "(Експериментально) Використовувати агентів принтера замість хостів друку" #~ msgstr "(Експериментально) Використовувати агентів принтера замість хостів друку"
@@ -32595,9 +32618,6 @@ msgstr ""
#~ msgid "An error occurred loading " #~ msgid "An error occurred loading "
#~ msgstr "Помилка під час завантаження " #~ msgstr "Помилка під час завантаження "
#~ msgid "/"
#~ msgstr "/"
# AI Translated # AI Translated
#, c-format, boost-format #, c-format, boost-format
#~ msgid "Load %s to " #~ msgid "Load %s to "
+233 -213
View File
@@ -3,7 +3,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: Orca Slicer\n" "Project-Id-Version: Orca Slicer\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-06 15:16-0300\n" "POT-Creation-Date: 2026-10-08 11:08+0800\n"
"PO-Revision-Date: 2025-10-02 17:43+0700\n" "PO-Revision-Date: 2025-10-02 17:43+0700\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: hainguyen.ts13@gmail.com\n" "Language-Team: hainguyen.ts13@gmail.com\n"
@@ -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." 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." 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 #, c-format, boost-format
msgid "Please input a valid value (K in %.1f~%.1f)" 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)" msgstr "Vui lòng nhập giá trị hợp lệ (K trong %.1f~%.1f)"
@@ -6562,7 +6565,7 @@ msgstr "Thể tích:"
msgid "Size:" msgid "Size:"
msgstr "Kích thước:" 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)." 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)." 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)."
@@ -7466,6 +7469,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." msgid "File '%s' was lost! Please download it again."
msgstr "File '%s' đã bị mất! Vui lòng tải lại." 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 #, c-format, boost-format
msgid "" msgid ""
"File: %s\n" "File: %s\n"
@@ -11744,6 +11763,12 @@ msgstr "Hủy timelapse & In"
msgid "Clean Up" msgid "Clean Up"
msgstr "Dọn dẹp" 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" msgid "Preparing print job"
msgstr "Đang chuẩn bị công việc in" msgstr "Đang chuẩn bị công việc in"
@@ -27905,213 +27930,6 @@ msgstr "Font này không có đường viền cho chữ này"
msgid "Size: %.1f × %.1f mm" msgid "Size: %.1f × %.1f mm"
msgstr "Kích thước: %.1f × %.1f mm" msgstr "Kích thước: %.1f × %.1f mm"
# AI Translated
msgid "Primitive"
msgstr "Hình nguyên thủy"
# AI Translated
msgid "Empty mesh generated"
msgstr "Đã tạo lưới rỗng"
# AI Translated
#. TRN Default name of an object created from the box primitive shape.
msgid "Box"
msgstr "Hộp"
# AI Translated
#. TRN Primitive gizmo: label in front of the preset cube size buttons
msgid "Quick:"
msgstr "Nhanh:"
# AI Translated
msgid "Width (X)"
msgstr "Độ rộng (X)"
# AI Translated
msgid "Depth (Y)"
msgstr "Độ sâu (Y)"
# AI Translated
msgid "Height (Z)"
msgstr "Chiều cao (Z)"
# AI Translated
#. TRN Primitive gizmo: bottom radius of a cone ("Top R" is the top radius)
msgid "Bottom R"
msgstr "R đáy"
# AI Translated
msgid "Top R"
msgstr "R đỉnh"
# AI Translated
#. TRN Primitive gizmo: major radius of a torus ("Minor R" is the tube radius)
msgid "Major R"
msgstr "R lớn"
# AI Translated
msgid "Minor R"
msgstr "R nhỏ"
# AI Translated
msgid "All edges"
msgstr "Tất cả cạnh"
# AI Translated
msgid "Top edges"
msgstr "Cạnh trên"
# AI Translated
msgid "Bottom edges"
msgstr "Cạnh dưới"
# AI Translated
msgid "Lateral edges"
msgstr "Cạnh bên"
# AI Translated
msgid "Mesh resolution"
msgstr "Độ phân giải lưới"
# AI Translated
msgid "Add Shape"
msgstr "Thêm hình"
# AI Translated
msgid "No outer profile"
msgstr "Không có biên dạng ngoài"
# AI Translated
msgid "Failed to make outer face"
msgstr "Không thể tạo mặt ngoài"
# AI Translated
msgid "Failed to build face with holes"
msgstr "Không thể tạo mặt có lỗ"
# AI Translated
msgid "Revolve failed"
msgstr "Tròn xoay thất bại"
# AI Translated
msgid "Profile"
msgstr "Biên dạng"
# AI Translated
msgid "Start new profile (for holes)"
msgstr "Bắt đầu biên dạng mới (cho lỗ)"
# AI Translated
msgid "Segments"
msgstr "Số đoạn"
# AI Translated
msgid "Sides"
msgstr "Số cạnh"
# AI Translated
msgid "Click on canvas to draw"
msgstr "Nhấn vào khung vẽ để vẽ"
# AI Translated
msgid "Snap to grid"
msgstr "Bám theo lưới"
# AI Translated
msgid "Grid step"
msgstr "Bước lưới"
# AI Translated
msgid "Outer"
msgstr "Ngoài"
# AI Translated
msgid "closed"
msgstr "khép kín"
# AI Translated
#. TRN Sketch gizmo profile list: "Outer" or "Hole", profile number, point count, "closed" or nothing
#, boost-format
msgid "%1% %2%: %3% points %4%"
msgstr "%1% %2%: %3% điểm %4%"
# AI Translated
msgid "XY (Top)"
msgstr "XY (Trên)"
# AI Translated
msgid "XZ (Front)"
msgstr "XZ (Trước)"
# AI Translated
msgid "YZ (Side)"
msgstr "YZ (Bên)"
# AI Translated
msgid "Revolve deg"
msgstr "Góc tròn xoay"
# AI Translated
msgid "Set to 0 for extrude, >0 for revolve"
msgstr "Đặt 0 để đùn, >0 để tạo khối tròn xoay"
# AI Translated
msgid "Pocket (cut)"
msgstr "Khoét hốc (cắt)"
# AI Translated
#, c-format
msgid "%zu closed profile"
msgid_plural "%zu closed profiles"
msgstr[0] "%zu biên dạng khép kín"
# AI Translated
msgid "Draw a closed profile to enable"
msgstr "Vẽ một biên dạng khép kín để bật"
# AI Translated
msgid "Clear all"
msgstr "Xóa tất cả"
# AI Translated
msgid "Empty result"
msgstr "Kết quả rỗng"
# AI Translated
#, boost-format
msgid "Extrude: %1%"
msgstr "Đùn: %1%"
# AI Translated
#, boost-format
msgid "Revolve: %1%"
msgstr "Tròn xoay: %1%"
# AI Translated
msgid "No object selected"
msgstr "Chưa chọn vật thể nào"
# AI Translated
msgid "Face failed"
msgstr "Tạo mặt thất bại"
# AI Translated
msgid "Face with holes failed"
msgstr "Tạo mặt có lỗ thất bại"
# AI Translated
msgid "Tool mesh empty"
msgstr "Lưới công cụ rỗng"
# AI Translated
msgid "Pocket added (negative volume)"
msgstr "Đã thêm hốc (thể tích âm)"
# AI Translated
#, boost-format
msgid "Pocket: %1%"
msgstr "Khoét hốc: %1%"
# AI Translated # AI Translated
msgctxt "Keyboard Shortcut" msgctxt "Keyboard Shortcut"
msgid "Ins" msgid "Ins"
@@ -31505,6 +31323,10 @@ msgstr "Bật công cụ nhà phát triển"
msgid "An error occurred loading %s" msgid "An error occurred loading %s"
msgstr "Đã xảy ra lỗi khi tải %s" msgstr "Đã xảy ra lỗi khi tải %s"
# AI Translated
msgid "/"
msgstr "/"
# AI Translated # AI Translated
#, boost-format #, boost-format
msgid "Load %1% to %2%" msgid "Load %1% to %2%"
@@ -32185,6 +32007,208 @@ msgstr ""
"Tránh cong vênh\n" "Tránh cong vênh\n"
"Bạn có biết rằng khi in vật liệu dễ cong vênh như ABS, tăng nhiệt độ bàn nóng một cách thích hợp có thể giảm xác suất cong vênh không?" "Bạn có biết rằng khi in vật liệu dễ cong vênh như ABS, tăng nhiệt độ bàn nóng một cách thích hợp có thể giảm xác suất cong vênh không?"
# AI Translated
#~ msgid "Primitive"
#~ msgstr "Hình nguyên thủy"
# AI Translated
#~ msgid "Empty mesh generated"
#~ msgstr "Đã tạo lưới rỗng"
# AI Translated
#~ msgid "Box"
#~ msgstr "Hộp"
# AI Translated
#~ msgid "Quick:"
#~ msgstr "Nhanh:"
# AI Translated
#~ msgid "Width (X)"
#~ msgstr "Độ rộng (X)"
# AI Translated
#~ msgid "Depth (Y)"
#~ msgstr "Độ sâu (Y)"
# AI Translated
#~ msgid "Height (Z)"
#~ msgstr "Chiều cao (Z)"
# AI Translated
#~ msgid "Bottom R"
#~ msgstr "R đáy"
# AI Translated
#~ msgid "Top R"
#~ msgstr "R đỉnh"
# AI Translated
#~ msgid "Major R"
#~ msgstr "R lớn"
# AI Translated
#~ msgid "Minor R"
#~ msgstr "R nhỏ"
# AI Translated
#~ msgid "All edges"
#~ msgstr "Tất cả cạnh"
# AI Translated
#~ msgid "Top edges"
#~ msgstr "Cạnh trên"
# AI Translated
#~ msgid "Bottom edges"
#~ msgstr "Cạnh dưới"
# AI Translated
#~ msgid "Lateral edges"
#~ msgstr "Cạnh bên"
# AI Translated
#~ msgid "Mesh resolution"
#~ msgstr "Độ phân giải lưới"
# AI Translated
#~ msgid "Add Shape"
#~ msgstr "Thêm hình"
# AI Translated
#~ msgid "No outer profile"
#~ msgstr "Không có biên dạng ngoài"
# AI Translated
#~ msgid "Failed to make outer face"
#~ msgstr "Không thể tạo mặt ngoài"
# AI Translated
#~ msgid "Failed to build face with holes"
#~ msgstr "Không thể tạo mặt có lỗ"
# AI Translated
#~ msgid "Revolve failed"
#~ msgstr "Tròn xoay thất bại"
# AI Translated
#~ msgid "Profile"
#~ msgstr "Biên dạng"
# AI Translated
#~ msgid "Start new profile (for holes)"
#~ msgstr "Bắt đầu biên dạng mới (cho lỗ)"
# AI Translated
#~ msgid "Segments"
#~ msgstr "Số đoạn"
# AI Translated
#~ msgid "Sides"
#~ msgstr "Số cạnh"
# AI Translated
#~ msgid "Click on canvas to draw"
#~ msgstr "Nhấn vào khung vẽ để vẽ"
# AI Translated
#~ msgid "Snap to grid"
#~ msgstr "Bám theo lưới"
# AI Translated
#~ msgid "Grid step"
#~ msgstr "Bước lưới"
# AI Translated
#~ msgid "Outer"
#~ msgstr "Ngoài"
# AI Translated
#~ msgid "closed"
#~ msgstr "khép kín"
# AI Translated
#, boost-format
#~ msgid "%1% %2%: %3% points %4%"
#~ msgstr "%1% %2%: %3% điểm %4%"
# AI Translated
#~ msgid "XY (Top)"
#~ msgstr "XY (Trên)"
# AI Translated
#~ msgid "XZ (Front)"
#~ msgstr "XZ (Trước)"
# AI Translated
#~ msgid "YZ (Side)"
#~ msgstr "YZ (Bên)"
# AI Translated
#~ msgid "Revolve deg"
#~ msgstr "Góc tròn xoay"
# AI Translated
#~ msgid "Set to 0 for extrude, >0 for revolve"
#~ msgstr "Đặt 0 để đùn, >0 để tạo khối tròn xoay"
# AI Translated
#~ msgid "Pocket (cut)"
#~ msgstr "Khoét hốc (cắt)"
# AI Translated
#, c-format
#~ msgid "%zu closed profile"
#~ msgid_plural "%zu closed profiles"
#~ msgstr[0] "%zu biên dạng khép kín"
# AI Translated
#~ msgid "Draw a closed profile to enable"
#~ msgstr "Vẽ một biên dạng khép kín để bật"
# AI Translated
#~ msgid "Clear all"
#~ msgstr "Xóa tất cả"
# AI Translated
#~ msgid "Empty result"
#~ msgstr "Kết quả rỗng"
# AI Translated
#, boost-format
#~ msgid "Extrude: %1%"
#~ msgstr "Đùn: %1%"
# AI Translated
#, boost-format
#~ msgid "Revolve: %1%"
#~ msgstr "Tròn xoay: %1%"
# AI Translated
#~ msgid "No object selected"
#~ msgstr "Chưa chọn vật thể nào"
# AI Translated
#~ msgid "Face failed"
#~ msgstr "Tạo mặt thất bại"
# AI Translated
#~ msgid "Face with holes failed"
#~ msgstr "Tạo mặt có lỗ thất bại"
# AI Translated
#~ msgid "Tool mesh empty"
#~ msgstr "Lưới công cụ rỗng"
# AI Translated
#~ msgid "Pocket added (negative volume)"
#~ msgstr "Đã thêm hốc (thể tích âm)"
# AI Translated
#, boost-format
#~ msgid "Pocket: %1%"
#~ msgstr "Khoét hốc: %1%"
# AI Translated # AI Translated
#~ msgid "(Experimental) Use printer agents instead of print hosts" #~ 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" #~ msgstr "(Thử nghiệm) Dùng tác nhân máy in thay cho máy chủ in"
@@ -33275,10 +33299,6 @@ msgstr ""
#~ msgid "An error occurred loading " #~ msgid "An error occurred loading "
#~ msgstr "Đã xảy ra lỗi khi tải " #~ msgstr "Đã xảy ra lỗi khi tải "
# AI Translated
#~ msgid "/"
#~ msgstr "/"
# AI Translated # AI Translated
#, c-format, boost-format #, c-format, boost-format
#~ msgid "Load %s to " #~ msgid "Load %s to "
+233 -213
View File
@@ -6,7 +6,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: Slic3rPE\n" "Project-Id-Version: Slic3rPE\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-06 15:16-0300\n" "POT-Creation-Date: 2026-10-08 11:08+0800\n"
"PO-Revision-Date: 2026-06-11 12:37-0300\n" "PO-Revision-Date: 2026-06-11 12:37-0300\n"
"Last-Translator: Handle <mail@bysb.net>\n" "Last-Translator: Handle <mail@bysb.net>\n"
"Language-Team: \n" "Language-Team: \n"
@@ -4185,6 +4185,9 @@ msgstr "您确定要清除耗材丝信息吗?"
msgid "You need to select the material type and color first." msgid "You need to select the material type and color first."
msgstr "您需要先选择材料类型和颜色。" msgstr "您需要先选择材料类型和颜色。"
msgid "The minimum temperature cannot be greater than the maximum temperature."
msgstr ""
#, c-format, boost-format #, c-format, boost-format
msgid "Please input a valid value (K in %.1f~%.1f)" msgid "Please input a valid value (K in %.1f~%.1f)"
msgstr "请输入有效的数值(K的范围为%.1f~%.1f)" msgstr "请输入有效的数值(K的范围为%.1f~%.1f)"
@@ -6039,7 +6042,7 @@ msgstr "体积:"
msgid "Size:" msgid "Size:"
msgstr "尺寸:" 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)." 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)。" msgstr "发现G-code路径在层%d,高度为%.2lf mm处有冲突。请将有冲突的对象分离得更远(%s <-> %s)。"
@@ -6903,6 +6906,22 @@ msgstr ".gcode.3mf文件中不包含G-code数据。请使用Orca Slicer进行切
msgid "File '%s' was lost! Please download it again." msgid "File '%s' was lost! Please download it again."
msgstr "文件%s丢失,请重新下载。" 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 #, c-format, boost-format
msgid "" msgid ""
"File: %s\n" "File: %s\n"
@@ -10853,6 +10872,12 @@ msgstr "取消延时摄影,继续打印"
msgid "Clean Up" msgid "Clean Up"
msgstr "清理" 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" msgid "Preparing print job"
msgstr "正在准备打印任务" msgstr "正在准备打印任务"
@@ -26335,213 +26360,6 @@ msgstr "此字体中没有该文字的字形轮廓"
msgid "Size: %.1f × %.1f mm" msgid "Size: %.1f × %.1f mm"
msgstr "大小:%.1f × %.1f mm" msgstr "大小:%.1f × %.1f mm"
# AI Translated
msgid "Primitive"
msgstr "标准模型"
# AI Translated
msgid "Empty mesh generated"
msgstr "生成了空网格"
# AI Translated
#. TRN Default name of an object created from the box primitive shape.
msgid "Box"
msgstr "长方体"
# AI Translated
#. TRN Primitive gizmo: label in front of the preset cube size buttons
msgid "Quick:"
msgstr "快速:"
# AI Translated
msgid "Width (X)"
msgstr "宽度 (X)"
# AI Translated
msgid "Depth (Y)"
msgstr "深度 (Y)"
# AI Translated
msgid "Height (Z)"
msgstr "高度 (Z)"
# AI Translated
#. TRN Primitive gizmo: bottom radius of a cone ("Top R" is the top radius)
msgid "Bottom R"
msgstr "底面半径"
# AI Translated
msgid "Top R"
msgstr "顶面半径"
# AI Translated
#. TRN Primitive gizmo: major radius of a torus ("Minor R" is the tube radius)
msgid "Major R"
msgstr "大半径"
# AI Translated
msgid "Minor R"
msgstr "小半径"
# AI Translated
msgid "All edges"
msgstr "所有边"
# AI Translated
msgid "Top edges"
msgstr "顶部边"
# AI Translated
msgid "Bottom edges"
msgstr "底部边"
# AI Translated
msgid "Lateral edges"
msgstr "侧边"
# AI Translated
msgid "Mesh resolution"
msgstr "网格精度"
# AI Translated
msgid "Add Shape"
msgstr "添加形状"
# AI Translated
msgid "No outer profile"
msgstr "没有外轮廓"
# AI Translated
msgid "Failed to make outer face"
msgstr "无法生成外轮廓面"
# AI Translated
msgid "Failed to build face with holes"
msgstr "无法生成带孔的面"
# AI Translated
msgid "Revolve failed"
msgstr "旋转失败"
# AI Translated
msgid "Profile"
msgstr "轮廓"
# AI Translated
msgid "Start new profile (for holes)"
msgstr "开始新轮廓(用于孔)"
# AI Translated
msgid "Segments"
msgstr "段数"
# AI Translated
msgid "Sides"
msgstr "边数"
# AI Translated
msgid "Click on canvas to draw"
msgstr "单击画布进行绘制"
# AI Translated
msgid "Snap to grid"
msgstr "吸附到网格"
# AI Translated
msgid "Grid step"
msgstr "网格间距"
# AI Translated
msgid "Outer"
msgstr "外轮廓"
# AI Translated
msgid "closed"
msgstr "已闭合"
# AI Translated
#. TRN Sketch gizmo profile list: "Outer" or "Hole", profile number, point count, "closed" or nothing
#, boost-format
msgid "%1% %2%: %3% points %4%"
msgstr "%1% %2%:%3% 个点 %4%"
# AI Translated
msgid "XY (Top)"
msgstr "XY(顶部)"
# AI Translated
msgid "XZ (Front)"
msgstr "XZ(前面)"
# AI Translated
msgid "YZ (Side)"
msgstr "YZ(侧面)"
# AI Translated
msgid "Revolve deg"
msgstr "旋转角度"
# AI Translated
msgid "Set to 0 for extrude, >0 for revolve"
msgstr "设为 0 则拉伸,大于 0 则旋转"
# AI Translated
msgid "Pocket (cut)"
msgstr "凹槽(切除)"
# AI Translated
#, c-format
msgid "%zu closed profile"
msgid_plural "%zu closed profiles"
msgstr[0] "%zu 个闭合轮廓"
# AI Translated
msgid "Draw a closed profile to enable"
msgstr "绘制一个闭合轮廓以启用"
# AI Translated
msgid "Clear all"
msgstr "全部清除"
# AI Translated
msgid "Empty result"
msgstr "结果为空"
# AI Translated
#, boost-format
msgid "Extrude: %1%"
msgstr "拉伸:%1%"
# AI Translated
#, boost-format
msgid "Revolve: %1%"
msgstr "旋转:%1%"
# AI Translated
msgid "No object selected"
msgstr "未选择对象"
# AI Translated
msgid "Face failed"
msgstr "生成面失败"
# AI Translated
msgid "Face with holes failed"
msgstr "生成带孔的面失败"
# AI Translated
msgid "Tool mesh empty"
msgstr "工具网格为空"
# AI Translated
msgid "Pocket added (negative volume)"
msgstr "已添加凹槽(负体积)"
# AI Translated
#, boost-format
msgid "Pocket: %1%"
msgstr "凹槽:%1%"
# AI Translated # AI Translated
msgctxt "Keyboard Shortcut" msgctxt "Keyboard Shortcut"
msgid "Ins" msgid "Ins"
@@ -29944,6 +29762,10 @@ msgstr "启用开发者工具"
msgid "An error occurred loading %s" msgid "An error occurred loading %s"
msgstr "加载 %s 时出错" msgstr "加载 %s 时出错"
# AI Translated
msgid "/"
msgstr "/"
# AI Translated # AI Translated
#, boost-format #, boost-format
msgid "Load %1% to %2%" msgid "Load %1% to %2%"
@@ -30623,6 +30445,208 @@ msgstr ""
"避免翘曲\n" "避免翘曲\n"
"您知道吗?打印ABS这类易翘曲材料时,适当提高热床温度可以降低翘曲的概率。" "您知道吗?打印ABS这类易翘曲材料时,适当提高热床温度可以降低翘曲的概率。"
# AI Translated
#~ msgid "Primitive"
#~ msgstr "标准模型"
# AI Translated
#~ msgid "Empty mesh generated"
#~ msgstr "生成了空网格"
# AI Translated
#~ msgid "Box"
#~ msgstr "长方体"
# AI Translated
#~ msgid "Quick:"
#~ msgstr "快速:"
# AI Translated
#~ msgid "Width (X)"
#~ msgstr "宽度 (X)"
# AI Translated
#~ msgid "Depth (Y)"
#~ msgstr "深度 (Y)"
# AI Translated
#~ msgid "Height (Z)"
#~ msgstr "高度 (Z)"
# AI Translated
#~ msgid "Bottom R"
#~ msgstr "底面半径"
# AI Translated
#~ msgid "Top R"
#~ msgstr "顶面半径"
# AI Translated
#~ msgid "Major R"
#~ msgstr "大半径"
# AI Translated
#~ msgid "Minor R"
#~ msgstr "小半径"
# AI Translated
#~ msgid "All edges"
#~ msgstr "所有边"
# AI Translated
#~ msgid "Top edges"
#~ msgstr "顶部边"
# AI Translated
#~ msgid "Bottom edges"
#~ msgstr "底部边"
# AI Translated
#~ msgid "Lateral edges"
#~ msgstr "侧边"
# AI Translated
#~ msgid "Mesh resolution"
#~ msgstr "网格精度"
# AI Translated
#~ msgid "Add Shape"
#~ msgstr "添加形状"
# AI Translated
#~ msgid "No outer profile"
#~ msgstr "没有外轮廓"
# AI Translated
#~ msgid "Failed to make outer face"
#~ msgstr "无法生成外轮廓面"
# AI Translated
#~ msgid "Failed to build face with holes"
#~ msgstr "无法生成带孔的面"
# AI Translated
#~ msgid "Revolve failed"
#~ msgstr "旋转失败"
# AI Translated
#~ msgid "Profile"
#~ msgstr "轮廓"
# AI Translated
#~ msgid "Start new profile (for holes)"
#~ msgstr "开始新轮廓(用于孔)"
# AI Translated
#~ msgid "Segments"
#~ msgstr "段数"
# AI Translated
#~ msgid "Sides"
#~ msgstr "边数"
# AI Translated
#~ msgid "Click on canvas to draw"
#~ msgstr "单击画布进行绘制"
# AI Translated
#~ msgid "Snap to grid"
#~ msgstr "吸附到网格"
# AI Translated
#~ msgid "Grid step"
#~ msgstr "网格间距"
# AI Translated
#~ msgid "Outer"
#~ msgstr "外轮廓"
# AI Translated
#~ msgid "closed"
#~ msgstr "已闭合"
# AI Translated
#, boost-format
#~ msgid "%1% %2%: %3% points %4%"
#~ msgstr "%1% %2%:%3% 个点 %4%"
# AI Translated
#~ msgid "XY (Top)"
#~ msgstr "XY(顶部)"
# AI Translated
#~ msgid "XZ (Front)"
#~ msgstr "XZ(前面)"
# AI Translated
#~ msgid "YZ (Side)"
#~ msgstr "YZ(侧面)"
# AI Translated
#~ msgid "Revolve deg"
#~ msgstr "旋转角度"
# AI Translated
#~ msgid "Set to 0 for extrude, >0 for revolve"
#~ msgstr "设为 0 则拉伸,大于 0 则旋转"
# AI Translated
#~ msgid "Pocket (cut)"
#~ msgstr "凹槽(切除)"
# AI Translated
#, c-format
#~ msgid "%zu closed profile"
#~ msgid_plural "%zu closed profiles"
#~ msgstr[0] "%zu 个闭合轮廓"
# AI Translated
#~ msgid "Draw a closed profile to enable"
#~ msgstr "绘制一个闭合轮廓以启用"
# AI Translated
#~ msgid "Clear all"
#~ msgstr "全部清除"
# AI Translated
#~ msgid "Empty result"
#~ msgstr "结果为空"
# AI Translated
#, boost-format
#~ msgid "Extrude: %1%"
#~ msgstr "拉伸:%1%"
# AI Translated
#, boost-format
#~ msgid "Revolve: %1%"
#~ msgstr "旋转:%1%"
# AI Translated
#~ msgid "No object selected"
#~ msgstr "未选择对象"
# AI Translated
#~ msgid "Face failed"
#~ msgstr "生成面失败"
# AI Translated
#~ msgid "Face with holes failed"
#~ msgstr "生成带孔的面失败"
# AI Translated
#~ msgid "Tool mesh empty"
#~ msgstr "工具网格为空"
# AI Translated
#~ msgid "Pocket added (negative volume)"
#~ msgstr "已添加凹槽(负体积)"
# AI Translated
#, boost-format
#~ msgid "Pocket: %1%"
#~ msgstr "凹槽:%1%"
# AI Translated # AI Translated
#~ msgid "(Experimental) Use printer agents instead of print hosts" #~ msgid "(Experimental) Use printer agents instead of print hosts"
#~ msgstr "(实验性)使用打印机代理替代打印主机" #~ msgstr "(实验性)使用打印机代理替代打印主机"
@@ -31675,10 +31699,6 @@ msgstr ""
#~ msgid "An error occurred loading " #~ msgid "An error occurred loading "
#~ msgstr "加载时出错: " #~ msgstr "加载时出错: "
# AI Translated
#~ msgid "/"
#~ msgstr "/"
# AI Translated # AI Translated
#, c-format, boost-format #, c-format, boost-format
#~ msgid "Load %s to " #~ msgid "Load %s to "
+233 -213
View File
@@ -9,7 +9,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: Orca Slicer\n" "Project-Id-Version: Orca Slicer\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-06 15:16-0300\n" "POT-Creation-Date: 2026-10-08 11:08+0800\n"
"PO-Revision-Date: 2025-11-28 13:48-0600\n" "PO-Revision-Date: 2025-11-28 13:48-0600\n"
"Last-Translator: tntchn <15895303+tntchn@users.noreply.github.com>\n" "Last-Translator: tntchn <15895303+tntchn@users.noreply.github.com>\n"
"Language-Team: \n" "Language-Team: \n"
@@ -4290,6 +4290,9 @@ msgstr "您確認要清除線材資訊嗎?"
msgid "You need to select the material type and color first." msgid "You need to select the material type and color first."
msgstr "您需要先選擇線材類型和顏色。" msgstr "您需要先選擇線材類型和顏色。"
msgid "The minimum temperature cannot be greater than the maximum temperature."
msgstr ""
#, c-format, boost-format #, c-format, boost-format
msgid "Please input a valid value (K in %.1f~%.1f)" msgid "Please input a valid value (K in %.1f~%.1f)"
msgstr "請輸入有效的數值 (K於 %.1f~%.1f 之間)" msgstr "請輸入有效的數值 (K於 %.1f~%.1f 之間)"
@@ -6169,7 +6172,7 @@ msgstr "體積:"
msgid "Size:" msgid "Size:"
msgstr "尺寸:" 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)." 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)。" msgstr "發現 G-code 路徑在 %d 層,Z = %.2lf mm 處的衝突。請將有衝突的物件分離得更遠(%s <-> %s)。"
@@ -7038,6 +7041,22 @@ msgstr ".gcode.3mf 檔案不包含 G-code 資料。請使用 OrcaSlicer 切片
msgid "File '%s' was lost! Please download it again." msgid "File '%s' was lost! Please download it again."
msgstr "檔案 %s 遺失,請重新下載。" 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 #, c-format, boost-format
msgid "" msgid ""
"File: %s\n" "File: %s\n"
@@ -11051,6 +11070,12 @@ msgstr "取消縮時攝影並列印"
msgid "Clean Up" msgid "Clean Up"
msgstr "清理" 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" msgid "Preparing print job"
msgstr "正在準備列印作業" msgstr "正在準備列印作業"
@@ -26533,213 +26558,6 @@ msgstr "此字型中沒有該文字的字形輪廓"
msgid "Size: %.1f × %.1f mm" msgid "Size: %.1f × %.1f mm"
msgstr "尺寸:%.1f × %.1f mm" msgstr "尺寸:%.1f × %.1f mm"
# AI Translated
msgid "Primitive"
msgstr "標準模型"
# AI Translated
msgid "Empty mesh generated"
msgstr "產生了空的網格"
# AI Translated
#. TRN Default name of an object created from the box primitive shape.
msgid "Box"
msgstr "長方體"
# AI Translated
#. TRN Primitive gizmo: label in front of the preset cube size buttons
msgid "Quick:"
msgstr "快速:"
# AI Translated
msgid "Width (X)"
msgstr "寬度 (X)"
# AI Translated
msgid "Depth (Y)"
msgstr "深度 (Y)"
# AI Translated
msgid "Height (Z)"
msgstr "高度 (Z)"
# AI Translated
#. TRN Primitive gizmo: bottom radius of a cone ("Top R" is the top radius)
msgid "Bottom R"
msgstr "底部半徑"
# AI Translated
msgid "Top R"
msgstr "頂部半徑"
# AI Translated
#. TRN Primitive gizmo: major radius of a torus ("Minor R" is the tube radius)
msgid "Major R"
msgstr "主半徑"
# AI Translated
msgid "Minor R"
msgstr "次半徑"
# AI Translated
msgid "All edges"
msgstr "所有邊"
# AI Translated
msgid "Top edges"
msgstr "頂部邊"
# AI Translated
msgid "Bottom edges"
msgstr "底部邊"
# AI Translated
msgid "Lateral edges"
msgstr "側邊"
# AI Translated
msgid "Mesh resolution"
msgstr "網格解析度"
# AI Translated
msgid "Add Shape"
msgstr "新增形狀"
# AI Translated
msgid "No outer profile"
msgstr "沒有外輪廓"
# AI Translated
msgid "Failed to make outer face"
msgstr "無法建立外部面"
# AI Translated
msgid "Failed to build face with holes"
msgstr "無法建立帶孔的面"
# AI Translated
msgid "Revolve failed"
msgstr "迴轉失敗"
# AI Translated
msgid "Profile"
msgstr "輪廓"
# AI Translated
msgid "Start new profile (for holes)"
msgstr "開始新輪廓(用於孔洞)"
# AI Translated
msgid "Segments"
msgstr "段數"
# AI Translated
msgid "Sides"
msgstr "邊數"
# AI Translated
msgid "Click on canvas to draw"
msgstr "在畫布上點選以繪製"
# AI Translated
msgid "Snap to grid"
msgstr "貼齊格線"
# AI Translated
msgid "Grid step"
msgstr "格線間距"
# AI Translated
msgid "Outer"
msgstr "外輪廓"
# AI Translated
msgid "closed"
msgstr "已封閉"
# AI Translated
#. TRN Sketch gizmo profile list: "Outer" or "Hole", profile number, point count, "closed" or nothing
#, boost-format
msgid "%1% %2%: %3% points %4%"
msgstr "%1% %2%:%3% 個點 %4%"
# AI Translated
msgid "XY (Top)"
msgstr "XY(頂部)"
# AI Translated
msgid "XZ (Front)"
msgstr "XZ(前面)"
# AI Translated
msgid "YZ (Side)"
msgstr "YZ(側面)"
# AI Translated
msgid "Revolve deg"
msgstr "迴轉角度"
# AI Translated
msgid "Set to 0 for extrude, >0 for revolve"
msgstr "設為 0 表示擠出,大於 0 表示迴轉"
# AI Translated
msgid "Pocket (cut)"
msgstr "挖槽(切除)"
# AI Translated
#, c-format
msgid "%zu closed profile"
msgid_plural "%zu closed profiles"
msgstr[0] "%zu 個封閉輪廓"
# AI Translated
msgid "Draw a closed profile to enable"
msgstr "請繪製封閉輪廓以啟用"
# AI Translated
msgid "Clear all"
msgstr "全部清除"
# AI Translated
msgid "Empty result"
msgstr "結果為空"
# AI Translated
#, boost-format
msgid "Extrude: %1%"
msgstr "擠出:%1%"
# AI Translated
#, boost-format
msgid "Revolve: %1%"
msgstr "迴轉:%1%"
# AI Translated
msgid "No object selected"
msgstr "未選取物件"
# AI Translated
msgid "Face failed"
msgstr "建立面失敗"
# AI Translated
msgid "Face with holes failed"
msgstr "建立帶孔的面失敗"
# AI Translated
msgid "Tool mesh empty"
msgstr "工具網格為空"
# AI Translated
msgid "Pocket added (negative volume)"
msgstr "已新增挖槽(負體積)"
# AI Translated
#, boost-format
msgid "Pocket: %1%"
msgstr "挖槽:%1%"
# AI Translated # AI Translated
msgctxt "Keyboard Shortcut" msgctxt "Keyboard Shortcut"
msgid "Ins" msgid "Ins"
@@ -30137,6 +29955,10 @@ msgstr "啟用開發者工具"
msgid "An error occurred loading %s" msgid "An error occurred loading %s"
msgstr "載入 %s 時發生錯誤" msgstr "載入 %s 時發生錯誤"
# AI Translated
msgid "/"
msgstr "/"
# AI Translated # AI Translated
#, boost-format #, boost-format
msgid "Load %1% to %2%" msgid "Load %1% to %2%"
@@ -30837,6 +30659,208 @@ msgstr ""
"避免翹曲\n" "避免翹曲\n"
"您知道嗎?當列印容易翹曲的材料(如 ABS)時,適當提高熱床溫度可以降低翹曲的機率。" "您知道嗎?當列印容易翹曲的材料(如 ABS)時,適當提高熱床溫度可以降低翹曲的機率。"
# AI Translated
#~ msgid "Primitive"
#~ msgstr "標準模型"
# AI Translated
#~ msgid "Empty mesh generated"
#~ msgstr "產生了空的網格"
# AI Translated
#~ msgid "Box"
#~ msgstr "長方體"
# AI Translated
#~ msgid "Quick:"
#~ msgstr "快速:"
# AI Translated
#~ msgid "Width (X)"
#~ msgstr "寬度 (X)"
# AI Translated
#~ msgid "Depth (Y)"
#~ msgstr "深度 (Y)"
# AI Translated
#~ msgid "Height (Z)"
#~ msgstr "高度 (Z)"
# AI Translated
#~ msgid "Bottom R"
#~ msgstr "底部半徑"
# AI Translated
#~ msgid "Top R"
#~ msgstr "頂部半徑"
# AI Translated
#~ msgid "Major R"
#~ msgstr "主半徑"
# AI Translated
#~ msgid "Minor R"
#~ msgstr "次半徑"
# AI Translated
#~ msgid "All edges"
#~ msgstr "所有邊"
# AI Translated
#~ msgid "Top edges"
#~ msgstr "頂部邊"
# AI Translated
#~ msgid "Bottom edges"
#~ msgstr "底部邊"
# AI Translated
#~ msgid "Lateral edges"
#~ msgstr "側邊"
# AI Translated
#~ msgid "Mesh resolution"
#~ msgstr "網格解析度"
# AI Translated
#~ msgid "Add Shape"
#~ msgstr "新增形狀"
# AI Translated
#~ msgid "No outer profile"
#~ msgstr "沒有外輪廓"
# AI Translated
#~ msgid "Failed to make outer face"
#~ msgstr "無法建立外部面"
# AI Translated
#~ msgid "Failed to build face with holes"
#~ msgstr "無法建立帶孔的面"
# AI Translated
#~ msgid "Revolve failed"
#~ msgstr "迴轉失敗"
# AI Translated
#~ msgid "Profile"
#~ msgstr "輪廓"
# AI Translated
#~ msgid "Start new profile (for holes)"
#~ msgstr "開始新輪廓(用於孔洞)"
# AI Translated
#~ msgid "Segments"
#~ msgstr "段數"
# AI Translated
#~ msgid "Sides"
#~ msgstr "邊數"
# AI Translated
#~ msgid "Click on canvas to draw"
#~ msgstr "在畫布上點選以繪製"
# AI Translated
#~ msgid "Snap to grid"
#~ msgstr "貼齊格線"
# AI Translated
#~ msgid "Grid step"
#~ msgstr "格線間距"
# AI Translated
#~ msgid "Outer"
#~ msgstr "外輪廓"
# AI Translated
#~ msgid "closed"
#~ msgstr "已封閉"
# AI Translated
#, boost-format
#~ msgid "%1% %2%: %3% points %4%"
#~ msgstr "%1% %2%:%3% 個點 %4%"
# AI Translated
#~ msgid "XY (Top)"
#~ msgstr "XY(頂部)"
# AI Translated
#~ msgid "XZ (Front)"
#~ msgstr "XZ(前面)"
# AI Translated
#~ msgid "YZ (Side)"
#~ msgstr "YZ(側面)"
# AI Translated
#~ msgid "Revolve deg"
#~ msgstr "迴轉角度"
# AI Translated
#~ msgid "Set to 0 for extrude, >0 for revolve"
#~ msgstr "設為 0 表示擠出,大於 0 表示迴轉"
# AI Translated
#~ msgid "Pocket (cut)"
#~ msgstr "挖槽(切除)"
# AI Translated
#, c-format
#~ msgid "%zu closed profile"
#~ msgid_plural "%zu closed profiles"
#~ msgstr[0] "%zu 個封閉輪廓"
# AI Translated
#~ msgid "Draw a closed profile to enable"
#~ msgstr "請繪製封閉輪廓以啟用"
# AI Translated
#~ msgid "Clear all"
#~ msgstr "全部清除"
# AI Translated
#~ msgid "Empty result"
#~ msgstr "結果為空"
# AI Translated
#, boost-format
#~ msgid "Extrude: %1%"
#~ msgstr "擠出:%1%"
# AI Translated
#, boost-format
#~ msgid "Revolve: %1%"
#~ msgstr "迴轉:%1%"
# AI Translated
#~ msgid "No object selected"
#~ msgstr "未選取物件"
# AI Translated
#~ msgid "Face failed"
#~ msgstr "建立面失敗"
# AI Translated
#~ msgid "Face with holes failed"
#~ msgstr "建立帶孔的面失敗"
# AI Translated
#~ msgid "Tool mesh empty"
#~ msgstr "工具網格為空"
# AI Translated
#~ msgid "Pocket added (negative volume)"
#~ msgstr "已新增挖槽(負體積)"
# AI Translated
#, boost-format
#~ msgid "Pocket: %1%"
#~ msgstr "挖槽:%1%"
# AI Translated # AI Translated
#~ msgid "(Experimental) Use printer agents instead of print hosts" #~ msgid "(Experimental) Use printer agents instead of print hosts"
#~ msgstr "(實驗性)使用列印裝置代理程式取代列印主機" #~ msgstr "(實驗性)使用列印裝置代理程式取代列印主機"
@@ -31890,10 +31914,6 @@ msgstr ""
#~ msgid "An error occurred loading " #~ msgid "An error occurred loading "
#~ msgstr "載入時發生錯誤: " #~ msgstr "載入時發生錯誤: "
# AI Translated
#~ msgid "/"
#~ msgstr "/"
# AI Translated # AI Translated
#, c-format, boost-format #, c-format, boost-format
#~ msgid "Load %s to " #~ msgid "Load %s to "
+7
View File
@@ -126,6 +126,13 @@
</array> </array>
<key>LSMinimumSystemVersion</key> <key>LSMinimumSystemVersion</key>
<string>10.10</string> <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> <key>NSPrincipalClass</key>
<string>NSApplication</string> <string>NSApplication</string>
<key>NSHighResolutionCapable</key> <key>NSHighResolutionCapable</key>
+111 -36
View File
@@ -3808,7 +3808,11 @@ void PresetBundle::get_ams_cobox_infos(AMSComboInfo& combox_info)
auto ams_name = ams.opt_string("tray_name", 0u); auto ams_name = ams.opt_string("tray_name", 0u);
auto filament_changed = !ams.has("filament_changed") || ams.opt_bool("filament_changed"); auto filament_changed = !ams.has("filament_changed") || ams.opt_bool("filament_changed");
auto filament_multi_color = ams.opt<ConfigOptionStrings>("filament_multi_colour")->values; 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; continue;
} }
if (!filament_changed && this->filament_presets.size() > combox_info.ams_filament_presets.size()) { if (!filament_changed && this->filament_presets.size() > combox_info.ams_filament_presets.size()) {
@@ -3818,16 +3822,48 @@ void PresetBundle::get_ams_cobox_infos(AMSComboInfo& combox_info)
combox_info.ams_names.push_back(ams_name); combox_info.ams_names.push_back(ams_name);
continue; continue;
} }
auto iter = std::find_if(filaments.begin(), filaments.end(), auto iter = filaments.end();
[this, &filament_id](auto &f) { return f.is_compatible && filaments.get_preset_base(f) == &f && f.filament_id == filament_id; }); if (!filament_id.empty()) {
warn_ambiguous_filament_id_match(filaments, iter, filament_id); 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()) { if (iter == filaments.end()) {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": filament_id %1% not found or system or compatible") % filament_id; 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()) { if (!filament_type.empty()) {
auto original_type = filament_type;
filament_type = "Generic " + filament_type; filament_type = "Generic " + filament_type;
iter = std::find_if(filaments.begin(), filaments.end(), 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); }); [&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()) { if (iter == filaments.end()) {
// Prefer old selection // Prefer old selection
@@ -3838,7 +3874,11 @@ void PresetBundle::get_ams_cobox_infos(AMSComboInfo& combox_info)
combox_info.ams_names.push_back(ams_name); combox_info.ams_names.push_back(ams_name);
continue; 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()) if (iter == filaments.end())
continue; continue;
} }
@@ -3876,6 +3916,7 @@ unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfi
for (auto &entry : filament_ams_list) { for (auto &entry : filament_ams_list) {
auto & ams = entry.second; auto & ams = entry.second;
auto filament_id = ams.opt_string("filament_id", 0u); 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 = ams.opt_string("filament_colour", 0u);
auto filament_color_type = ams.opt_string("filament_colour_type", 0u); auto filament_color_type = ams.opt_string("filament_colour_type", 0u);
auto filament_changed = !ams.has("filament_changed") || ams.opt_bool("filament_changed"); auto filament_changed = !ams.has("filament_changed") || ams.opt_bool("filament_changed");
@@ -3888,42 +3929,58 @@ unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfi
ams_array_maps.push_back(temp); ams_array_maps.push_back(temp);
index++; index++;
if (filament_id.empty()) { if (filament_id.empty()) {
if (use_map) { // A tray written by the printer UI carries a material type but no
for (int j = maps.size() - 1; j >= 0; j--) { // OrcaSlicer preset id. Resolve it to the matching Generic preset with
if (maps[j].slot_id == slot_id && maps[j].ams_id == ams_id) { // the tray's own color instead of dropping it (direct sync) or forcing
maps.erase(j); // "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 continue;
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;
} }
if (!filament_changed && this->filament_presets.size() > ams_filament_presets.size()) { 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_presets.push_back(this->filament_presets[ams_filament_presets.size()]);
ams_filament_colors.push_back(filament_color); ams_filament_colors.push_back(filament_color);
ams_filament_color_types.push_back(filament_color_type); ams_filament_color_types.push_back(filament_color_type);
ams_multi_color_filment.push_back(filament_multi_color); ams_multi_color_filment.push_back(filament_multi_color);
ams_infos.back().valid = true;
continue; continue;
} }
bool has_type = false; bool has_type = false;
auto filament_type = ams.opt_string("filament_type", 0u); 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) { auto iter = filaments.end();
has_type |= f.config.opt_string("filament_type", 0u) == filament_type; if (!filament_id.empty()) {
return f.is_compatible && filaments.get_preset_base(f) == &f && f.filament_id == filament_id; }); iter = std::find_if(filaments.begin(), filaments.end(), [this, &filament_id, &has_type, filament_type](auto &f) {
warn_ambiguous_filament_id_match(filaments, iter, filament_id); 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()) { if (iter == filaments.end()) {
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": filament_id %1% not found or system or compatible") % filament_id; BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": filament_id %1% not found or system or compatible") % filament_id;
if (!filament_type.empty()) { if (!filament_type.empty()) {
@@ -3963,6 +4020,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()) { if (iter == filaments.end()) {
// Prefer old selection // Prefer old selection
if (ams_filament_presets.size() < this->filament_presets.size()) { if (ams_filament_presets.size() < this->filament_presets.size()) {
@@ -3970,6 +4028,7 @@ unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfi
ams_filament_colors.push_back(filament_color); ams_filament_colors.push_back(filament_color);
ams_filament_color_types.push_back(filament_color_type); ams_filament_color_types.push_back(filament_color_type);
ams_multi_color_filment.push_back(filament_multi_color); 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.") : 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.")); L("The filament model is unknown. Still using the previous filament preset."));
continue; continue;
@@ -3985,7 +4044,8 @@ unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfi
if (iter == filaments.end()) if (iter == filaments.end())
continue; 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.") : (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.")) : 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.") : (has_type ? L("The filament may not be compatible with the current machine settings. A random filament preset will be used.") :
@@ -3996,6 +4056,7 @@ unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfi
ams_filament_colors.push_back(filament_color); ams_filament_colors.push_back(filament_color);
ams_filament_color_types.push_back(filament_color_type); ams_filament_color_types.push_back(filament_color_type);
ams_multi_color_filment.push_back(filament_multi_color); ams_multi_color_filment.push_back(filament_multi_color);
ams_infos.back().valid = true;
} }
if (ams_filament_presets.empty()) if (ams_filament_presets.empty())
return 0; return 0;
@@ -4435,9 +4496,21 @@ std::vector<std::vector<DynamicPrintConfig>> PresetBundle::get_extruder_filament
return filament_infos; return filament_infos;
} }
// ORCA TODO: currently, this function assumes the printer name follows the pattern of "<printer_model> <nozzle_diameter>", e.g. std::string PresetBundle::get_printer_model_display_name(const std::string &model_id) const
// 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. 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> 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; std::set<std::string> printer_names;
@@ -4459,7 +4532,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); 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) { for (auto& printer_name : printer_names) {
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " " << __LINE__ << " printer name: " << printer_name; BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " " << __LINE__ << " printer name: " << printer_name;
@@ -4472,8 +4547,8 @@ std::vector<Preset *> PresetBundle::get_filament_presets_for_machine(const std::
const std::string &nozzle_diameter_str, const std::string &nozzle_diameter_str,
bool include_user_presets) bool include_user_presets)
{ {
// Printer model plus nozzle diameter is expected to resolve to a single system printer preset; // Printer model plus nozzle diameter normally resolves to a single system printer preset.
// get_printer_names_by_printer_type_and_nozzle asserts as much in debug builds. // 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 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()); const Preset *printer = printer_names.empty() ? nullptr : printers.find_preset(*printer_names.begin());
if (printer == nullptr) if (printer == nullptr)
+3
View File
@@ -411,6 +411,9 @@ public:
std::vector<std::vector<DynamicPrintConfig>> get_extruder_filament_info() const; 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); 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 // 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 // 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/MarkdownTip.hpp
GUI/MediaFilePanel.cpp GUI/MediaFilePanel.cpp
GUI/MediaFilePanel.h GUI/MediaFilePanel.h
GUI/OrcaFilesPanel.cpp
GUI/OrcaFilesPanel.h
GUI/StoragePanel.cpp
GUI/StoragePanel.h
GUI/MediaPlayCtrl.cpp GUI/MediaPlayCtrl.cpp
GUI/MediaPlayCtrl.h GUI/MediaPlayCtrl.h
GUI/WebRtcMediaController.cpp GUI/WebRtcMediaController.cpp
@@ -457,6 +461,11 @@ set(SLIC3R_GUI_SOURCES
GUI/PresetHints.hpp GUI/PresetHints.hpp
GUI/PrinterCloudAuthDialog.cpp GUI/PrinterCloudAuthDialog.cpp
GUI/PrinterCloudAuthDialog.hpp 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.cpp
GUI/Printer/PrinterFileSystem.h GUI/Printer/PrinterFileSystem.h
GUI/PrinterWebView.cpp GUI/PrinterWebView.cpp
@@ -772,8 +781,12 @@ set(SLIC3R_GUI_SOURCES
Utils/ICameraSignalingChannel.hpp Utils/ICameraSignalingChannel.hpp
Utils/OrcaCloudServiceAgent.cpp Utils/OrcaCloudServiceAgent.cpp
Utils/OrcaCloudServiceAgent.hpp Utils/OrcaCloudServiceAgent.hpp
Utils/OrcaMqttConnection.cpp
Utils/OrcaMqttConnection.hpp
Utils/OrcaPrinterAgent.cpp Utils/OrcaPrinterAgent.cpp
Utils/OrcaPrinterAgent.hpp Utils/OrcaPrinterAgent.hpp
Utils/OrcaCloudSignalingChannel.cpp
Utils/OrcaCloudSignalingChannel.hpp
Utils/QidiPrinterAgent.cpp Utils/QidiPrinterAgent.cpp
Utils/QidiPrinterAgent.hpp Utils/QidiPrinterAgent.hpp
Utils/SnapmakerPrinterAgent.cpp Utils/SnapmakerPrinterAgent.cpp
@@ -912,7 +925,7 @@ target_include_directories(libslic3r_gui PRIVATE Utils ${CMAKE_CURRENT_BINARY_DI
if (WIN32) if (WIN32)
target_include_directories(libslic3r_gui SYSTEM PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../../deps/WebView2/include) 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() endif()
source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} FILES ${SLIC3R_GUI_SOURCES}) 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/DevExtruderSystem.h"
#include "slic3r/GUI/DeviceCore/DevManager.h" #include "slic3r/GUI/DeviceCore/DevManager.h"
#include "slic3r/GUI/DeviceManager.hpp"
#include "slic3r/GUI/MsgDialog.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); stream << std::fixed << std::setprecision(1) << obj->GetExtderSystem()->GetNozzleDiameter(extruder_id);
std::string nozzle_diameter_str = stream.str(); 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( 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) if (!filament_id_set.insert(filament_it->filament_id).second)
continue; continue;
const std::string filament_alias = filaments.get_preset_alias(*filament_it, true); 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_App.hpp"
#include "GUI.hpp" #include "GUI.hpp"
#include "slic3r/Utils/NetworkAgent.hpp" #include "slic3r/Utils/NetworkAgent.hpp"
#include "slic3r/Utils/NetworkAgentFactory.hpp"
#include "libslic3r/Preset.hpp" #include "libslic3r/Preset.hpp"
#include "I18N.hpp" #include "I18N.hpp"
#include <algorithm> #include <algorithm>
#include <boost/log/trivial.hpp> #include <boost/log/trivial.hpp>
#include <slic3r/GUI/DeviceManager.hpp>
#include <string> #include <string>
#include <sstream> #include <sstream>
#include <ios> #include <ios>
@@ -472,6 +474,13 @@ void AMSMaterialsSetting::update_filament_editing(bool is_printing)
m_button_confirm->Show(true); 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) { if (!m_is_third) {
m_tip_readonly->SetLabelText(wxEmptyString); m_tip_readonly->SetLabelText(wxEmptyString);
m_tip_readonly->Hide(); 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 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_filament->Enable(false);
m_comboBox_cali_result->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_k_val->Enable(false);
m_input_n_val->Enable(false); m_input_n_val->Enable(false);
m_button_confirm->Hide(); m_button_confirm->Hide();
@@ -543,7 +554,7 @@ void AMSMaterialsSetting::on_select_reset(wxCommandEvent& event) {
} }
// set k / n value // 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 // set extrusion cali ratio
int cali_tray_id = ams_id * 4 + slot_id; 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); obj->command_extrusion_cali_set(cali_tray_id, "", "", k, n);
} }
else { else if (obj->supports_extrusion_cali()) {
PACalibIndexInfo select_index_info; PACalibIndexInfo select_index_info;
int tray_id = ams_id * 4 + slot_id; int tray_id = ams_id * 4 + slot_id;
if (is_virtual_tray()) { if (is_virtual_tray()) {
@@ -723,6 +734,21 @@ void AMSMaterialsSetting::on_select_ok(wxCommandEvent &event)
return; 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 // 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 // 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 k_text = m_input_k_val->GetTextCtrl()->GetValue();
wxString n_text = m_input_n_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 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); 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); 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; vt_tray = VIRTUAL_TRAY_DEPUTY_ID;
} }
if (obj->cali_version >= 0) { if (obj->supports_extrusion_cali() && obj->cali_version >= 0) {
PACalibIndexInfo select_index_info; PACalibIndexInfo select_index_info;
select_index_info.tray_id = vt_tray; select_index_info.tray_id = vt_tray;
select_index_info.ams_id = ams_id; 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); CalibUtils::select_PA_calib_result(select_index_info);
} }
else { else if (obj->supports_extrusion_cali()) {
obj->command_extrusion_cali_set(vt_tray, "", "", k, n); 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; PACalibIndexInfo select_index_info;
select_index_info.tray_id = cali_tray_id; select_index_info.tray_id = cali_tray_id;
select_index_info.ams_id = ams_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); 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); obj->command_extrusion_cali_set(cali_tray_id, "", "", k, n);
} }
} }
@@ -932,7 +958,12 @@ bool AMSMaterialsSetting::is_virtual_tray()
void AMSMaterialsSetting::update_widgets() 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 // Low version firmware does not display k value
m_panel_kn->Hide(); 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_k_val->GetTextCtrl()->SetValue(k);
m_input_n_val->GetTextCtrl()->SetValue(n); 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; wxArrayString filament_items;
wxString bambu_filament_name; 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; stream << std::fixed << std::setprecision(1) << machine_diameter;
std::string nozzle_diameter_str = stream.str(); 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(); BOOST_LOG_TRIVIAL(trace) << "system_preset_bundle filament number=" << preset_bundle->filaments.size();
for (Preset *filament_it : preset_bundle->get_filament_presets_for_machine( 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) if (!filament_id_set.insert(filament_it->filament_id).second)
continue; continue;
const std::string alias = preset_bundle->filaments.get_preset_alias(*filament_it, true); 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 { else {
m_comboBox_filament->Show(); m_comboBox_filament->Show();
m_readonly_filament->Hide(); 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) { 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_filament->Set(filament_items);
m_comboBox_from_printer = true;
m_comboBox_filament->SetSelection(selection_idx); m_comboBox_filament->SetSelection(selection_idx);
post_select_event(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); 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(); update();
Layout(); Layout();
Fit(); Fit();
@@ -1277,50 +1322,53 @@ int AMSMaterialsSetting::get_filament_variant_index(const Preset &filament, cons
void AMSMaterialsSetting::on_select_filament(wxCommandEvent &evt) void AMSMaterialsSetting::on_select_filament(wxCommandEvent &evt)
{ {
// Get the flag whether to open the filament setting dialog from the device page // True for the popup's own initial selection: the dialog pre-filled the tray's
int* from_printer = static_cast<int*>(m_comboBox_filament->GetClientData()); // temps, so the matching preset must not overwrite them.
const bool initial_printer_selection = m_comboBox_from_printer;
m_filament_type = ""; m_filament_type = "";
PresetBundle* preset_bundle = wxGetApp().preset_bundle; PresetBundle* preset_bundle = wxGetApp().preset_bundle;
std::string filament_printer_model;
if (preset_bundle) { if (preset_bundle) {
std::ostringstream stream; std::ostringstream stream;
if (obj) { if (obj) {
// Defensive: this dialog is opened only from StatusPanel (BBL-only) today, so the fallback fires // Use the selected profile's nozzle diameter until the connected device reports one.
// 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.
float machine_diameter = obj->GetExtderSystem()->GetNozzleDiameter(0); float machine_diameter = obj->GetExtderSystem()->GetNozzleDiameter(0);
if (machine_diameter == 0.0f) { if (machine_diameter == 0.0f) {
const ConfigOption *opt = preset_bundle->printers.get_selected_preset().config.option("nozzle_diameter"); const ConfigOption *opt = preset_bundle->printers.get_selected_preset().config.option("nozzle_diameter");
if (opt) machine_diameter = static_cast<const ConfigOptionFloats *>(opt)->values[0]; if (opt) machine_diameter = static_cast<const ConfigOptionFloats *>(opt)->values[0];
} }
stream << std::fixed << std::setprecision(1) << machine_diameter; 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(); std::string nozzle_diameter_str = stream.str();
// Resolve the selection against the same list Popup() built the dropdown from, so the two // 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. // halves of the dialog cannot disagree about which filaments this machine can use.
const std::string selected = m_comboBox_filament->GetValue().ToStdString(); 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; const std::string filament_id = map_filament_items[selected].filament_id;
for (Preset *it : preset_bundle->get_filament_presets_for_machine( 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) if (it->filament_id != filament_id)
continue; continue;
// ) if nozzle_temperature_range is found if (!initial_printer_selection) {
const int variant_index = get_filament_variant_index(*it, nozzle_diameter_str); // ) if nozzle_temperature_range is found
ConfigOption* opt_min = it->config.option("nozzle_temperature_range_low"); const int variant_index = get_filament_variant_index(*it, nozzle_diameter_str);
if (opt_min) { ConfigOption* opt_min = it->config.option("nozzle_temperature_range_low");
ConfigOptionInts* opt_min_ints = dynamic_cast<ConfigOptionInts*>(opt_min); if (opt_min) {
if (opt_min_ints) { ConfigOptionInts* opt_min_ints = dynamic_cast<ConfigOptionInts*>(opt_min);
wxString text_nozzle_temp_min = wxString::Format("%d", opt_min_ints->get_at(variant_index)); if (opt_min_ints) {
m_input_nozzle_min->GetTextCtrl()->SetValue(text_nozzle_temp_min); 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");
ConfigOption* opt_max = it->config.option("nozzle_temperature_range_high"); if (opt_max) {
if (opt_max) { ConfigOptionInts* opt_max_ints = dynamic_cast<ConfigOptionInts*>(opt_max);
ConfigOptionInts* opt_max_ints = dynamic_cast<ConfigOptionInts*>(opt_max); if (opt_max_ints) {
if (opt_max_ints) { wxString text_nozzle_temp_max = wxString::Format("%d", opt_max_ints->get_at(variant_index));
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);
m_input_nozzle_max->GetTextCtrl()->SetValue(text_nozzle_temp_max); }
} }
} }
ConfigOption* opt_type = it->config.option("filament_type"); 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_button_confirm->Disable(); // ORCA No need to change style
m_comboBox_cali_result->Clear(); m_comboBox_cali_result->Clear();
m_comboBox_cali_result->SetValue(wxEmptyString); m_comboBox_cali_result->SetValue(wxEmptyString);
m_input_k_val->GetTextCtrl()->SetValue(wxEmptyString); if (!initial_printer_selection) {
m_input_n_val->GetTextCtrl()->SetValue(wxEmptyString); m_input_k_val->GetTextCtrl()->SetValue(wxEmptyString);
m_comboBox_filament->SetClientData(new int(0)); m_input_n_val->GetTextCtrl()->SetValue(wxEmptyString);
}
m_comboBox_from_printer = false;
return; return;
} }
else { 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 //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; wxArrayString items;
m_pa_profile_items.clear(); m_pa_profile_items.clear();
m_comboBox_cali_result->SetValue(wxEmptyString); m_comboBox_cali_result->SetValue(wxEmptyString);
@@ -1444,7 +1498,7 @@ void AMSMaterialsSetting::on_select_filament(wxCommandEvent &evt)
m_comboBox_cali_result->Set(items); m_comboBox_cali_result->Set(items);
if (ams_id == VIRTUAL_TRAY_MAIN_ID || ams_id == VIRTUAL_TRAY_DEPUTY_ID) { 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) { for (auto slot : obj->vt_slot) {
if (slot.id == std::to_string(ams_id)) if (slot.id == std::to_string(ams_id))
cali_select_idx = CalibUtils::get_selected_calib_idx(m_pa_profile_items, slot.cali_idx); 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 { 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)); DevAmsTray* selected_tray = this->obj->GetFilaSystem()->GetAmsTray(std::to_string(ams_id), std::to_string(slot_id));
if (!selected_tray) if (!selected_tray)
{ {
m_comboBox_from_printer = false;
return; return;
} }
@@ -1495,7 +1550,7 @@ void AMSMaterialsSetting::on_select_filament(wxCommandEvent &evt)
else { else {
if (!ams_filament_id.empty()) { if (!ams_filament_id.empty()) {
//m_input_k_val->GetTextCtrl()->SetValue("0.00"); //m_input_k_val->GetTextCtrl()->SetValue("0.00");
m_input_k_val->Enable(true); m_input_k_val->Enable(!m_view_only);
} }
else { else {
//m_input_k_val->GetTextCtrl()->SetValue("0.00"); //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) 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_filament_id;
std::string ams_setting_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 // Orca: view-only mode (laser/cut). When set, the dialog is inspectable but every
// editing control is disabled and no command is sent. // editing control is disabled and no command is sent.
bool m_view_only = false; bool m_view_only = false;
@@ -200,6 +200,9 @@ protected:
wxStaticText* m_n_param; wxStaticText* m_n_param;
TextInput* m_input_n_val; TextInput* m_input_n_val;
int m_filament_selection; 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; 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 tray_read_opt = m_checkbox_Insert_material_auto_read->GetValue();
bool remain_opt = m_checkbox_remain->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(); m_sizer_Insert_material_tip_inline->Layout();
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 tray_read_opt = m_checkbox_Insert_material_auto_read->GetValue();
bool remain_opt = m_checkbox_remain->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(); m_sizer_starting_tip_inline->Layout();
Layout(); Layout();
@@ -570,7 +572,8 @@ void AMSSetting::on_remain(wxCommandEvent& event)
bool start_read_opt = m_checkbox_starting_auto_read->GetValue(); bool start_read_opt = m_checkbox_starting_auto_read->GetValue();
bool tray_read_opt = m_checkbox_Insert_material_auto_read->GetValue(); bool tray_read_opt = m_checkbox_Insert_material_auto_read->GetValue();
bool remain_opt = m_checkbox_remain->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(); event.Skip();
} }
@@ -806,4 +809,4 @@ void AMSSettingArrangeAMSOrder::OnBtnRearrangeClicked(wxCommandEvent& event)
} }
#endif #endif
}} // namespace Slic3r::GUI }} // namespace Slic3r::GUI
+8 -1
View File
@@ -10,6 +10,7 @@
#include "Widgets/DialogButtons.hpp" #include "Widgets/DialogButtons.hpp"
#include "libslic3r/Config.hpp" #include "libslic3r/Config.hpp"
#include <boost/log/trivial.hpp> #include <boost/log/trivial.hpp>
#include <slic3r/GUI/DeviceManager.hpp>
#include <string> #include <string>
#include <vector> #include <vector>
#include "libslic3r/calib.hpp" #include "libslic3r/calib.hpp"
@@ -745,9 +746,15 @@ wxArrayString NewCalibrationHistoryDialog::get_all_filaments(const MachineObject
std::string nozzle_diameter_str = stream.str(); std::string nozzle_diameter_str = stream.str();
if (preset_bundle) { 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(); BOOST_LOG_TRIVIAL(trace) << "system_preset_bundle filament number=" << preset_bundle->filaments.size();
for (Preset *filament_it : preset_bundle->get_filament_presets_for_machine( 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) if (!filament_id_set.insert(filament_it->filament_id).second)
continue; continue;
const std::string alias = preset_bundle->filaments.get_preset_alias(*filament_it, true); 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 <array>
#include <cstddef> #include <cstddef>
#include <cassert> #include <cassert>
@@ -8,6 +9,7 @@
#include <cstdlib> #include <cstdlib>
#include <chrono> #include <chrono>
#include <nlohmann/json.hpp> #include <nlohmann/json.hpp>
#include <wx/app.h>
#include <wx/colour.h> #include <wx/colour.h>
#include <string> #include <string>
#include <wx/string.h> #include <wx/string.h>
@@ -19,6 +21,7 @@
#include <utility> #include <utility>
#include "DevFilaSystem.h" #include "DevFilaSystem.h"
#include "slic3r/Utils/NetworkAgent.hpp" #include "slic3r/Utils/NetworkAgent.hpp"
#include "slic3r/Utils/NetworkAgentFactory.hpp" // ORCA_PRINTER_AGENT_ID
#include "DevNozzleSystem.h" // DevNozzle / DevNozzleSystem for GetNozzleFlowStringByAmsId #include "DevNozzleSystem.h" // DevNozzle / DevNozzleSystem for GetNozzleFlowStringByAmsId
// TODO: remove this include // TODO: remove this include
@@ -61,6 +64,15 @@ void DevAmsTray::UpdateColorFromStr(const std::string& color)
} }
} }
void DevAmsTray::UpdateEmptyState(bool material_fields_present, bool classify_empty)
{
const auto is_zero_or_empty = [](const std::string& value) {
return value.empty() || std::all_of(value.begin(), value.end(), [](char c) { return c == '0'; });
};
is_empty = material_fields_present && classify_empty && setting_id.empty() && m_fila_type.empty() &&
is_zero_or_empty(color) && is_zero_or_empty(tag_uid);
}
void DevAmsTray::reset() void DevAmsTray::reset()
{ {
tag_uid = ""; tag_uid = "";
@@ -82,6 +94,7 @@ void DevAmsTray::reset()
k = 0.0f; k = 0.0f;
n = 0.0f; n = 0.0f;
is_bbl = false; is_bbl = false;
is_empty = false;
hold_count = 0; hold_count = 0;
remain = 0; remain = 0;
} }
@@ -681,7 +694,7 @@ void DevFilaSystemParser::ParseV1_0(const json& jj, MachineObject* obj, DevFilaS
//std::string type = (*tray_it)["tray_type"].get<std::string>(); //std::string type = (*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>()); 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. // 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; 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") if (printer_filament_id == "GFS00")
{ {
@@ -781,6 +794,8 @@ void DevFilaSystemParser::ParseV1_0(const json& jj, MachineObject* obj, DevFilaS
{ {
curr_tray->remain = -1; curr_tray->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 // The tray objects are reused across status updates. Reset this
// state when a previously empty slot receives a filament again. // state when a previously empty slot receives a filament again.
curr_tray->is_slot_placeholder = tray_it->contains("tray_slot_placeholder"); curr_tray->is_slot_placeholder = tray_it->contains("tray_slot_placeholder");
+4 -1
View File
@@ -83,6 +83,7 @@ public:
wxColour wx_color; wxColour wx_color;
bool is_bbl; bool is_bbl;
bool is_exists = false; 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 bool is_slot_placeholder = false; // Orca: True for empty tray slots from pull-mode agents
int hold_count = 0; int hold_count = 0;
int remain = 0; // filament remain: 0 ~ 100 int remain = 0; // filament remain: 0 ~ 100
@@ -91,13 +92,15 @@ public:
// operators // operators
bool operator==(DevAmsTray const& o) const 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); } bool operator!=(DevAmsTray const& o) const { return !operator==(o); }
// setters // setters
void reset(); void reset();
void UpdateColorFromStr(const std::string& color); void UpdateColorFromStr(const std::string& color);
void UpdateEmptyState(bool material_fields_present, bool classify_empty);
void set_hold_count() { hold_count = HOLD_COUNT_MAX; } void set_hold_count() { hold_count = HOLD_COUNT_MAX; }
// getter // getter
+110 -11
View File
@@ -65,6 +65,7 @@
#include <unordered_set> #include <unordered_set>
#include <utility> #include <utility>
#include <vector> #include <vector>
#include <wx/app.h>
#include <wx/colour.h> #include <wx/colour.h>
#include <tuple> #include <tuple>
#include <wx/dir.h> #include <wx/dir.h>
@@ -432,6 +433,32 @@ NozzleVolumeType convert_to_nozzle_type(const std::string &str)
return res; 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 wxString MachineObject::get_printer_type_display_str() const
{ {
std::string display_name = DevPrinterConfigUtil::get_printer_display_name(printer_type); std::string display_name = DevPrinterConfigUtil::get_printer_display_name(printer_type);
@@ -1734,6 +1761,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) 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; json j;
try { try {
auto tray_id = 0; auto tray_id = 0;
@@ -1777,6 +1808,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) 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; json j;
j["print"]["command"] = "ams_user_setting"; j["print"]["command"] = "ams_user_setting";
j["print"]["sequence_id"] = std::to_string(MachineObject::m_sequence_id++); j["print"]["sequence_id"] = std::to_string(MachineObject::m_sequence_id++);
@@ -1785,22 +1820,59 @@ int MachineObject::command_ams_user_settings(bool start_read_opt, bool tray_read
j["print"]["tray_read_option"] = tray_read_opt; j["print"]["tray_read_option"] = tray_read_opt;
j["print"]["calibrate_remain_flag"] = remain_flag; j["print"]["calibrate_remain_flag"] = remain_flag;
m_fila_system->GetAmsSystemSetting().SetDetectOnInsertEnabled(tray_read_opt); const int rc = this->publish_json(j);
m_fila_system->GetAmsSystemSetting().SetDetectOnPowerupEnabled(start_read_opt); if (rc == 0) {
m_fila_system->GetAmsSystemSetting().SetDetectRemainEnabled(remain_flag); m_fila_system->GetAmsSystemSetting().SetDetectOnInsertEnabled(tray_read_opt);
ams_user_setting_start = time(nullptr); m_fila_system->GetAmsSystemSetting().SetDetectOnPowerupEnabled(start_read_opt);
m_fila_system->GetAmsSystemSetting().SetDetectRemainEnabled(remain_flag);
return this->publish_json(j); ams_user_setting_start = time(nullptr);
}
return rc;
} }
int MachineObject::command_ams_calibrate(int ams_id) int MachineObject::command_ams_calibrate(int ams_id)
{ {
if (!m_agent) return -1; 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())); 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) 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_tray_id = 0;
int tag_ams_id = ams_id; int tag_ams_id = ams_id;
int tag_slot_id = slot_id; int tag_slot_id = slot_id;
@@ -1835,6 +1907,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) int MachineObject::command_ams_refresh_rfid(int ams_id, int slot_id)
{ {
if (!m_agent) return -1; 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())); 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()));
} }
@@ -1848,11 +1924,19 @@ int MachineObject::command_start_camera()
int MachineObject::command_ams_select_tray(std::string tray_id) int MachineObject::command_ams_select_tray(std::string tray_id)
{ {
if (!m_agent) return -1; 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())); 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) 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; if (action == "resume" && check_resume_condition()) return 0;
//valid actions //valid actions
@@ -1868,6 +1952,10 @@ int MachineObject::command_ams_control(std::string action)
int MachineObject::command_ams_drying_stop() 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; json j;
j["print"]["command"] = "auto_stop_ams_dry"; j["print"]["command"] = "auto_stop_ams_dry";
j["print"]["sequence_id"] = std::to_string(MachineObject::m_sequence_id++); j["print"]["sequence_id"] = std::to_string(MachineObject::m_sequence_id++);
@@ -2818,7 +2906,9 @@ bool MachineObject::is_camera_busy_off()
int MachineObject::publish_json(const json& json_item, int qos, int flag) int MachineObject::publish_json(const json& json_item, int qos, int flag)
{ {
int rtn = 0; 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); rtn = local_publish_json(json_item.dump(), qos, flag);
} else { } else {
rtn = cloud_publish_json(json_item.dump(), qos, flag); rtn = cloud_publish_json(json_item.dump(), qos, flag);
@@ -2862,6 +2952,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 MachineObject::setting_id_to_type(std::string setting_id, std::string tray_type)
{ {
std::string type; std::string type;
if (wxTheApp == nullptr) return tray_type;
PresetBundle* preset_bundle = GUI::wxGetApp().preset_bundle; PresetBundle* preset_bundle = GUI::wxGetApp().preset_bundle;
if (preset_bundle) { if (preset_bundle) {
for (auto it = preset_bundle->filaments.begin(); it != preset_bundle->filaments.end(); it++) { for (auto it = preset_bundle->filaments.begin(); it != preset_bundle->filaments.end(); it++) {
@@ -4198,6 +4289,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_slot[0].setting_id = jj["tray_info_idx"].get<std::string>();
//vt_tray.type = jj["tray_type"].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>()); 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 // delay update
vt_slot[0].set_hold_count(); vt_slot[0].set_hold_count();
} else { } else {
@@ -4223,6 +4317,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->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>()); 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 // delay update
tray_it->second->set_hold_count(); tray_it->second->set_hold_count();
} else { } else {
@@ -5161,7 +5258,7 @@ DevAmsTray MachineObject::parse_vt_tray(json vtray)
//std::string type = vtray["tray_type"].get<std::string>(); //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>()); 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. // 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; 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") { if (printer_filament_id == "GFS00") {
vt_tray.m_fila_type = "PLA-S"; vt_tray.m_fila_type = "PLA-S";
@@ -5256,6 +5353,8 @@ DevAmsTray MachineObject::parse_vt_tray(json vtray)
else { else {
vt_tray.remain = -1; 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; return vt_tray;
@@ -5549,11 +5648,11 @@ void MachineObject::parse_new_info2(const json& info)
if (capabilities_it == info.end() || !capabilities_it->is_object()) if (capabilities_it == info.end() || !capabilities_it->is_object())
return; return;
const auto flags_it = capabilities_it->find("flags"); 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; return;
const json& flags = *flags_it; 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) { auto parse_bool = [&flags](const char* name, bool& target) {
const auto it = flags.find(name); const auto it = flags.find(name);
+14
View File
@@ -14,6 +14,7 @@
#include <string> #include <string>
#include <memory> #include <memory>
#include <chrono> #include <chrono>
#include <set>
#include <unordered_set> #include <unordered_set>
#include <optional> #include <optional>
#include <boost/thread.hpp> #include <boost/thread.hpp>
@@ -72,6 +73,7 @@ class DeviceErrorDialog; // Previous definitions
class NetworkAgent; class NetworkAgent;
// Orca: ManualPaCaliMethod now provided by DeviceCore/DevCalib.h (enum class) // Orca: ManualPaCaliMethod now provided by DeviceCore/DevCalib.h (enum class)
class PresetBundle;
#define UpgradeNoError 0 #define UpgradeNoError 0
#define UpgradeDownloadFailed -1 #define UpgradeDownloadFailed -1
@@ -107,6 +109,11 @@ struct DevPrintTaskRatingInfo;
// given nozzle diameter (mm), bucketed per nozzle size to mirror the printer firmware. // 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); 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 class MachineObject
{ {
private: private:
@@ -795,6 +802,13 @@ public:
int command_refresh_nozzle(); int command_refresh_nozzle();
int command_set_chamber(int temp); int command_set_chamber(int temp);
int check_resume_condition(); 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 // ams controls
//int command_ams_switch(int tray_index, int old_temp = 210, int new_temp = 210); //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); 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]; const auto& can = ams.cans[j];
auto id = getTrayID(obj, ams.ams_id, can.can_id); auto id = getTrayID(obj, ams.ams_id, can.can_id);
auto material = can.material_name; 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_BRAND ||
can.material_state == AMSCanType::AMS_CAN_TYPE_VIRTUAL) can.material_state == AMSCanType::AMS_CAN_TYPE_VIRTUAL)
{ {
@@ -979,10 +983,6 @@ int ReselectMachineDialog::CaculateSwitcherDistribution(MachineObject* obj, cons
material = L("?"); 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 itOK = std::find_if(posOK.begin(), posOK.end(), [&](const trayHelper& tray){
auto amsID = std::get<0>(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
+16 -2
View File
@@ -1160,7 +1160,8 @@ void GUI_App::post_init()
m_agent->set_on_http_error_fn([this](CloudEvent event, unsigned int status, std::string body) { m_agent->set_on_http_error_fn([this](CloudEvent event, unsigned int status, std::string body) {
this->handle_http_error(status, body, event.provider); 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 //update the plugin tips
@@ -4135,6 +4136,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; 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) std::string GUI_App::canonical_printer_agent_id(const std::string& picked_id)
{ {
return picked_id == resolve_printer_agent_id("") ? std::string() : picked_id; return picked_id == resolve_printer_agent_id("") ? std::string() : picked_id;
@@ -4206,7 +4219,8 @@ void GUI_App::switch_printer_agent()
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": printer agent switched to " << effective_agent_id; 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 // 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) // Auto-switch MachineObject (new agent has empty device_info, so always re-select)
select_machine(effective_agent_id); select_machine(effective_agent_id);
+3
View File
@@ -399,6 +399,9 @@ public:
// Reconcile the live printer agent with the stored preset selection. // Reconcile the live printer agent with the stored preset selection.
void switch_printer_agent(); 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; 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 // ORCA TODO: in the future, bbl presets should specify "bbl" printer agent id
// then, all resolve and canonical would just be ORCA<->"" // then, all resolve and canonical would just be ORCA<->""
+119 -90
View File
@@ -1,13 +1,16 @@
#include "ImageGrid.h" #include "ImageGrid.h"
#include "Printer/PrinterFileSystem.h" #include "Printer/PrinterFileSystem.h"
#include "Printer/BambuFileGridModel.h"
#include "wxExtensions.hpp" #include "wxExtensions.hpp"
#include "Widgets/Label.hpp" #include "Widgets/Label.hpp"
#include "I18N.hpp" #include "I18N.hpp"
#include "GUI_App.hpp" #include "GUI_App.hpp"
#include "GUI.hpp" #include "GUI.hpp"
#include <slic3r/GUI/Printer/IFileGridModel.h>
#include <utility> #include <utility>
#include <boost/smart_ptr/shared_ptr.hpp> #include <boost/smart_ptr/shared_ptr.hpp>
#include <boost/smart_ptr/make_shared_object.hpp>
#include <wx/colour.h> #include <wx/colour.h>
#include <cstddef> #include <cstddef>
#include <wx/dcclient.h> #include <wx/dcclient.h>
@@ -76,18 +79,11 @@ ImageGrid::ImageGrid(wxWindow * parent)
void ImageGrid::SetFileSystem(boost::shared_ptr<PrinterFileSystem> file_sys) 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; m_file_sys = file_sys;
if (m_file_sys) { m_model.reset();
m_file_sys->Bind(EVT_MODE_CHANGED, &ImageGrid::changedEvent, this); if (file_sys) {
m_file_sys->Bind(EVT_FILE_CHANGED, &ImageGrid::changedEvent, this); m_model = boost::make_shared<BambuFileGridModel>(file_sys);
m_file_sys->Bind(EVT_THUMBNAIL, &ImageGrid::changedEvent, this); m_model->SetChangeHandler([this](FileGridChange c) { onModelChange(c); });
m_file_sys->Bind(EVT_DOWNLOAD, &ImageGrid::changedEvent, this);
} }
m_row_count = 0; m_row_count = 0;
m_col_count = 1; m_col_count = 1;
@@ -96,9 +92,22 @@ void ImageGrid::SetFileSystem(boost::shared_ptr<PrinterFileSystem> file_sys)
UpdateFileSystem(); 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) 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_icon = icon;
m_status_msg = wxString::Format(msg, code); m_status_msg = wxString::Format(msg, code);
BOOST_LOG_TRIVIAL(info) << "ImageGrid::SetStatus: " << m_status_msg.ToUTF8().data(); 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) void Slic3r::GUI::ImageGrid::SetFileType(int type, std::string const &storage)
{ {
if (!m_file_sys) if (!m_model)
return; return;
m_file_sys->SetFileType((PrinterFileSystem::FileType) type, storage); m_model->SetFileType((FileGridType) type, storage);
} }
void Slic3r::GUI::ImageGrid::SetGroupMode(int mode) void Slic3r::GUI::ImageGrid::SetGroupMode(int mode)
{ {
if (!m_file_sys) if (!m_model)
return; return;
if (m_file_sys->GetCount() == 0) { if (m_model->GetCount() == 0) {
m_file_sys->SetGroupMode((PrinterFileSystem::GroupMode) mode); m_model->SetGroupMode((FileGridGroup) mode);
return; return;
} }
wxSize size = GetClientSize(); wxSize size = GetClientSize();
int index = (m_row_offset + 1 < m_row_count || m_row_count == 0) int index = (m_row_offset + 1 < m_row_count || m_row_count == 0)
? m_row_offset / 4 * m_col_count ? 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; : ((m_model->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); time_t time = m_model->GetFile(index).time;
m_file_sys->SetGroupMode((PrinterFileSystem::GroupMode) mode); m_model->SetGroupMode((FileGridGroup) mode);
index = m_file_sys->GetIndexAtTime(file.time); index = m_model->GetIndexAtTime(time);
// UpdateFileSystem(); call by changed event // UpdateFileSystem(); call by changed event
m_row_offset = index / m_col_count * 4; m_row_offset = index / m_col_count * 4;
if (m_row_offset >= m_row_count) 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) void Slic3r::GUI::ImageGrid::SetSelecting(bool selecting)
{ {
m_selecting = selecting; m_selecting = selecting;
if (m_file_sys) if (m_model)
m_file_sys->SelectAll(false); m_model->SelectAll(false);
Refresh(); Refresh();
} }
@@ -166,14 +175,14 @@ void Slic3r::GUI::ImageGrid::Rescale()
void Slic3r::GUI::ImageGrid::Select(size_t index) void Slic3r::GUI::ImageGrid::Select(size_t index)
{ {
if (m_selecting) { if (m_selecting) {
m_file_sys->ToggleSelect(index); m_model->ToggleSelect(index);
Refresh(); Refresh();
return; return;
} }
if (m_file_sys->GetGroupMode() == PrinterFileSystem::G_NONE) { if (m_model->GetGroupMode() == FileGridGroup::All) {
return; return;
} }
index = m_file_sys->EnterSubGroup(index); index = m_model->EnterSubGroup(index);
// UpdateFileSystem(); call by changed event // UpdateFileSystem(); call by changed event
m_row_offset = index / m_col_count * 4; m_row_offset = index / m_col_count * 4;
if (m_row_offset >= m_row_count) 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() void Slic3r::GUI::ImageGrid::UpdateFileSystem()
{ {
if (!m_file_sys) return; if (!m_model) return;
if (m_file_sys->GetFileType() < PrinterFileSystem::F_MODEL) { if (m_model->GetFileType() < FileGridType::Model) {
if (m_file_sys->GetGroupMode() == PrinterFileSystem::G_NONE) { if (m_model->GetGroupMode() == FileGridGroup::All) {
m_cell_size.Set(396, 228); m_cell_size.Set(396, 228);
m_border_size.Set(384, 216); m_border_size.Set(384, 216);
} }
@@ -216,10 +225,10 @@ void Slic3r::GUI::ImageGrid::UpdateFileSystem()
void ImageGrid::UpdateLayout() void ImageGrid::UpdateLayout()
{ {
if (!m_file_sys) return; if (!m_model) return;
wxSize size = GetClientSize(); wxSize size = GetClientSize();
wxSize title_mask_size{0, 60 * em_unit(this) / 10}; 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; title_mask_size.y = 20 * em_unit(this) / 10;
size.y -= title_mask_size.y; size.y -= title_mask_size.y;
} }
@@ -227,7 +236,7 @@ void ImageGrid::UpdateLayout()
int cell_height = m_cell_size.GetHeight(); int cell_height = m_cell_size.GetHeight();
int ncol = (size.GetWidth() - cell_width + m_border_size.GetWidth()) / cell_width; int ncol = (size.GetWidth() - cell_width + m_border_size.GetWidth()) / cell_width;
if (ncol <= 0) ncol = 1; 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); int nrow = (total_height - size.GetHeight() + cell_height / 4 - 1) / (cell_height / 4);
m_row_offset = m_row_offset * m_col_count / ncol; m_row_offset = m_row_offset * m_col_count / ncol;
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_row_offset = m_row_count == 0 ? 0 : m_row_count - 1;
m_scroll_offset = 0; m_scroll_offset = 0;
// create mask // 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(); title_mask_size.x = (m_col_count - 1) * m_cell_size.GetWidth() + m_border_size.GetWidth();
} }
else { else {
@@ -252,7 +261,7 @@ void ImageGrid::UpdateLayout()
void Slic3r::GUI::ImageGrid::UpdateFocusRange() void Slic3r::GUI::ImageGrid::UpdateFocusRange()
{ {
if (!m_file_sys) return; if (!m_model) return;
wxSize size = GetClientSize(); wxSize size = GetClientSize();
wxPoint off; wxPoint off;
int index = firstItem(size, off); int index = firstItem(size, off);
@@ -261,15 +270,21 @@ void Slic3r::GUI::ImageGrid::UpdateFocusRange()
count += m_col_count; count += m_col_count;
off.y += m_cell_size.y; 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) std::pair<int, size_t> Slic3r::GUI::ImageGrid::HitTest(wxPoint const &pt)
{ {
if (!m_file_sys) if (!m_model)
return {HIT_NONE, -1}; return {HIT_NONE, -1};
wxSize size = GetClientSize(); 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)) if (wxRect({0, 0}, m_border_size).CenterIn(wxRect({0, 0}, size)).Contains(pt))
return {HIT_STATUS, 0}; return {HIT_STATUS, 0};
return {HIT_NONE, -1}; return {HIT_NONE, -1};
@@ -287,17 +302,19 @@ std::pair<int, size_t> Slic3r::GUI::ImageGrid::HitTest(wxPoint const &pt)
index += m_col_count; index += m_col_count;
off.y -= m_cell_size.GetHeight(); 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_content_rect.Contains(off)) { return {HIT_NONE, -1}; }
if (!m_selecting) { if (!m_selecting) {
wxRect hover_rect{0, m_content_rect.GetHeight() - m_buttons_background.GetHeight(), m_content_rect.GetWidth(), m_buttons_background.GetHeight()}; 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); const FileGridCard &card = m_model->GetFile(index);
int btn = file.IsDownload() && file.DownloadProgress() >= 0 ? 3 : 2; int btn = card.downloading && card.download_progress >= 0 ? 3 : 2;
if (m_file_sys->GetFileType() == PrinterFileSystem::F_MODEL) { if (m_model->GetFileType() == FileGridType::Model) {
if (m_show_download) if (m_show_download && card.printable)
btn = 3; btn = 3;
hover_rect.y -= m_content_rect.GetHeight() * 64 / 264; hover_rect.y -= m_content_rect.GetHeight() * 64 / 264;
} }
if (!hasSecondAction(card))
btn = 1;
if (hover_rect.Contains(off.x, off.y)) { if (hover_rect.Contains(off.x, off.y)) {
return {HIT_ACTION, index * 4 + off.x * btn / hover_rect.GetWidth()}; return {HIT_ACTION, index * 4 + off.x * btn / hover_rect.GetWidth()};
} // Two buttons } // Two buttons
@@ -315,12 +332,12 @@ void ImageGrid::mouseMoved(wxMouseEvent& event)
m_hit_item = hit.second; m_hit_item = hit.second;
if (hit.first == HIT_ITEM) { if (hit.first == HIT_ITEM) {
SetToolTip({}); SetToolTip({});
auto & file = m_file_sys->GetFile(hit.second); const FileGridCard &card = m_model->GetFile(hit.second);
if (auto title = file.Title(); !title.empty()) { if (auto title = card.title; !title.empty()) {
auto tip = wxString::Format(_L("File: %s\nTitle: %s\n"), from_u8(file.name), from_u8(title)); auto tip = wxString::Format(_L("File: %s\nTitle: %s\n"), from_u8(card.name), from_u8(title));
SetToolTip(tip); SetToolTip(tip);
} else { } else {
SetToolTip(from_u8(file.name)); SetToolTip(from_u8(card.name));
} }
} else } else
SetToolTip({}); SetToolTip({});
@@ -372,9 +389,9 @@ void ImageGrid::mouseReleased(wxMouseEvent& event)
else if (m_hit_type == HIT_ACTION) else if (m_hit_type == HIT_ACTION)
DoAction(m_hit_item / 4, m_hit_item & 3); DoAction(m_hit_item / 4, m_hit_item & 3);
else if (m_hit_type == HIT_MODE) 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) else if (m_hit_type == HIT_STATUS)
m_file_sys->Retry(); m_model->Retry();
else else
Refresh(); Refresh();
} else { } else {
@@ -403,20 +420,11 @@ void ImageGrid::mouseWheelMoved(wxMouseEvent &event)
Refresh(); Refresh();
} }
void Slic3r::GUI::ImageGrid::changedEvent(wxCommandEvent& evt) void Slic3r::GUI::ImageGrid::onModelChange(FileGridChange change)
{ {
evt.Skip(); BOOST_LOG_TRIVIAL(debug) << "ImageGrid::onModelChange: " << (int) change;
BOOST_LOG_TRIVIAL(debug) << "ImageGrid::changedEvent: " << evt.GetEventType() << " index: " << evt.GetInt() if (change == FileGridChange::Files || change == FileGridChange::Mode)
<< " 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"));
UpdateFileSystem(); UpdateFileSystem();
}
else if (evt.GetEventType() == EVT_MODE_CHANGED)
UpdateFileSystem();
//else if (evt.GetEventType() == EVT_THUMBNAIL)
// RefreshRect(itemRect(evt.GetInt()), false);
else else
Refresh(); Refresh();
} }
@@ -431,7 +439,7 @@ void ImageGrid::paintEvent(wxPaintEvent& evt)
size_t Slic3r::GUI::ImageGrid::firstItem(wxSize const &size, wxPoint &off) size_t Slic3r::GUI::ImageGrid::firstItem(wxSize const &size, wxPoint &off)
{ {
int size_y = size.y; 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(); size_y -= m_title_mask.GetHeight();
int offx = (size.x - (m_col_count - 1) * m_cell_size.GetWidth() - m_border_size.GetWidth()) / 2; 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) ? 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(); 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) ? int index = (m_row_offset + 1 < m_row_count || m_row_count == 0) ?
m_row_offset / 4 * m_col_count : 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; ((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_file_sys->GetGroupMode() == PrinterFileSystem::G_NONE) if (m_model->GetGroupMode() == FileGridGroup::All)
offy += m_title_mask.GetHeight(); offy += m_title_mask.GetHeight();
off = wxPoint{offx, offy}; off = wxPoint{offx, offy};
return index; return index;
@@ -534,7 +542,7 @@ void ImageGrid::render(wxDC& dc)
wxSize size = GetClientSize(); wxSize size = GetClientSize();
dc.SetPen(wxPen(GetBackgroundColour())); dc.SetPen(wxPen(GetBackgroundColour()));
dc.SetBrush(wxBrush(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 }); dc.DrawRectangle({ 0, 0, size.x, size.y });
if (!m_status_msg.IsEmpty()) { if (!m_status_msg.IsEmpty()) {
auto si = m_status_icon.GetBmpSize(); auto si = m_status_icon.GetBmpSize();
@@ -562,7 +570,7 @@ void ImageGrid::render(wxDC& dc)
{ {
// Draw one line // Draw one line
wxPoint pt{off.x, off.y}; 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) { while (index < end) {
pt += m_content_rect.GetTopLeft(); pt += m_content_rect.GetTopLeft();
// Draw content // Draw content
@@ -571,7 +579,7 @@ void ImageGrid::render(wxDC& dc)
&ImageGrid::renderContent1, &ImageGrid::renderContent1,
&ImageGrid::renderContent2 &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(); pt -= m_content_rect.GetTopLeft();
// Draw colume spacing at right // 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()}); 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(); off.y += m_cell_size.GetHeight();
} }
// Draw floating date range for non-group list // 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.DrawBitmap(m_title_mask, {off.x, 0});
dc.DrawRectangle({off.x, 0}, m_title_mask.GetSize()); dc.DrawRectangle({off.x, 0}, m_title_mask.GetSize());
auto & file1 = m_file_sys->GetFile(start); const FileGridCard & file1 = m_model->GetFile(start);
auto & file2 = m_file_sys->GetFile(end - 1); const FileGridCard & file2 = m_model->GetFile(end - 1);
auto date1 = wxDateTime((time_t) file1.time).Format(_L(TIME_FORMATS[m_file_sys->GetGroupMode()])); 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[m_file_sys->GetGroupMode()])); auto date2 = wxDateTime((time_t) file2.time).Format(_L(TIME_FORMATS[static_cast<int>(m_model->GetGroupMode())]));
dc.SetFont(Label::Head_16); dc.SetFont(Label::Head_16);
dc.SetTextForeground(StateColor::darkModeColorFor("#262E30")); dc.SetTextForeground(StateColor::darkModeColorFor("#262E30"));
dc.DrawText(date1 + " - " + date2, wxPoint{off.x, 2}); 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}); dc.DrawRectangle({off.x, off.y, size.x - off.x * 2, size.y - off.y});
// Draw position bar // Draw position bar
if (m_timer.IsRunning()) { 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) { 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; 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, 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) void Slic3r::GUI::ImageGrid::renderContent1(wxDC &dc, wxPoint const &pt, int index, bool hit)
{ {
bool selected = false; bool selected = false;
auto &file = m_file_sys->GetFile(index, selected); const FileGridCard &card = m_model->GetFile(index, selected);
// Draw thumbnail // Draw thumbnail
if (file.thumbnail.IsOk()) { if (card.thumbnail.IsOk()) {
float hs = (float) m_content_rect.GetWidth() / file.thumbnail.GetWidth(); if (m_model->preserve_thumbnail_aspect()) {
float vs = (float) m_content_rect.GetHeight() / file.thumbnail.GetHeight(); // Contain: fit the whole thumbnail and letterbox against the tile background.
dc.SetUserScale(hs, vs); float scale = std::min((float) m_content_rect.GetWidth() / card.thumbnail.GetWidth(),
dc.DrawBitmap(file.thumbnail, {(int) (pt.x / hs), (int) (pt.y / vs)}); (float) m_content_rect.GetHeight() / card.thumbnail.GetHeight());
dc.SetUserScale(1, 1); int thumb_w = (int) (card.thumbnail.GetWidth() * scale);
if (m_file_sys->GetGroupMode() != PrinterFileSystem::G_NONE) { dc.DrawBitmap(m_title_mask, pt); } 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; bool show_download_state_always = true;
// Draw checked icon // 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}); dc.DrawBitmap(selected ? m_checked_icon.bmp() : m_unchecked_icon.bmp(), pt + wxPoint{10, 10});
// can't handle alpha // 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); // 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 nonHoverText;
wxString secondAction = m_show_download ? _L("Download") : ""; wxString secondAction = m_show_download ? _L("Download") : "";
wxString thirdAction; wxString thirdAction;
int states = 0; int states = 0;
// Draw download progress // Draw download progress
if (file.IsDownload()) { if (card.downloading) {
int progress = file.DownloadProgress(); int progress = card.download_progress;
if (progress == -1) { if (progress == -1) {
secondAction = _L("Cancel"); secondAction = _L("Cancel");
nonHoverText = _L("Download waiting..."); 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); 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")) if (secondAction != _L("Play"))
thirdAction = secondAction; thirdAction = secondAction;
secondAction = _L_CONTEXT("Print", "Verb"); secondAction = _L_CONTEXT("Print", "Verb");
@@ -669,7 +697,8 @@ void Slic3r::GUI::ImageGrid::renderContent1(wxDC &dc, wxPoint const &pt, int ind
wxArrayString texts; wxArrayString texts;
if (hit) { if (hit) {
texts.Add(_L("Delete")); texts.Add(_L("Delete"));
texts.Add(secondAction); if (hasSecondAction(card))
texts.Add(secondAction);
if (!thirdAction.IsEmpty()) if (!thirdAction.IsEmpty())
texts.Add(thirdAction); texts.Add(thirdAction);
renderButtons(dc, texts, rect, m_hit_type == HIT_ACTION ? m_hit_item & 3 : -1, states); 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 { } else {
dc.SetTextForeground(*wxWHITE); // time text color 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}); dc.DrawText(date, pt + wxPoint{24, 16});
} }
if (m_selecting && show_download_state_always) 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) 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 // Draw thumbnail & buttons
int h = m_content_rect.GetHeight() * 64 / 264; int h = m_content_rect.GetHeight() * 64 / 264;
m_content_rect.SetHeight(m_content_rect.GetHeight() - h); 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); auto em = em_unit(this);
wxRect rect{pt.x, pt.y + m_content_rect.GetHeight() - h, m_content_rect.GetWidth(), h / 2}; wxRect rect{pt.x, pt.y + m_content_rect.GetHeight() - h, m_content_rect.GetWidth(), h / 2};
rect.Deflate(em, 0); rect.Deflate(em, 0);
renderText2(dc, from_u8(file.name), rect); renderText2(dc, from_u8(card.name), rect);
rect.Offset(0, h / 2); rect.Offset(0, h / 2);
rect.SetWidth(rect.GetWidth() / 2 - em); rect.SetWidth(rect.GetWidth() / 2 - em);
dc.SetFont(Label::Body_13); dc.SetFont(Label::Body_13);
dc.SetTextForeground(StateColor::darkModeColorFor("#6B6B6B")); 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); 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) 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 "Widgets/StateColor.hpp"
#include "wxExtensions.hpp" #include "wxExtensions.hpp"
#include "Printer/IFileGridModel.h"
class Button; class Button;
class Label; class Label;
@@ -44,6 +45,8 @@ public:
void SetFileSystem(boost::shared_ptr<PrinterFileSystem> file_sys); void SetFileSystem(boost::shared_ptr<PrinterFileSystem> file_sys);
void SetModel(boost::shared_ptr<IFileGridModel> model);
void SetStatus(ScalableBitmap const & icon, wxString const &msg); void SetStatus(ScalableBitmap const & icon, wxString const &msg);
boost::shared_ptr<PrinterFileSystem> GetFileSystem() { return m_file_sys; } boost::shared_ptr<PrinterFileSystem> GetFileSystem() { return m_file_sys; }
@@ -76,9 +79,12 @@ protected:
std::pair<int, size_t> HitTest(wxPoint const &pt); 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: protected:
void changedEvent(wxCommandEvent& evt); void onModelChange(FileGridChange change);
void paintEvent(wxPaintEvent& evt); void paintEvent(wxPaintEvent& evt);
@@ -117,6 +123,7 @@ protected:
private: private:
boost::shared_ptr<PrinterFileSystem> m_file_sys; boost::shared_ptr<PrinterFileSystem> m_file_sys;
boost::shared_ptr<IFileGridModel> m_model;
ScalableBitmap m_status_icon; ScalableBitmap m_status_icon;
wxString m_status_msg; wxString m_status_msg;
+25 -11
View File
@@ -221,26 +221,40 @@ MediaFilePanel::MediaFilePanel(wxWindow * parent)
SetSelecting(false); SetSelecting(false);
}); });
auto onShowHide = [this](auto &e) { Bind(wxEVT_SHOW, &MediaFilePanel::on_show_hide, this);
e.Skip(); // A Device sub-tab switch hides the page container (StoragePanel); a main tab
if (auto w = dynamic_cast<wxWindow *>(e.GetEventObject()); !w || w->IsBeingDeleted()) // switch hides MonitorPanel inside LazyPage. Neither event reaches the other,
return; // so watch every ancestor below the top-level frame.
CallAfter([this] { for (wxWindow* w = GetParent(); w && !w->IsTopLevel(); w = w->GetParent()) {
auto fs = m_image_grid ? m_image_grid->GetFileSystem() : nullptr; w->Bind(wxEVT_SHOW, &MediaFilePanel::on_show_hide, this);
if (fs) IsShownOnScreen() ? fs->Start() : fs->Stop(); m_show_hosts.push_back(w);
}); }
};
Bind(wxEVT_SHOW, onShowHide);
parent->GetParent()->Bind(wxEVT_SHOW, onShowHide);
m_lan_user = "bblp"; m_lan_user = "bblp";
} }
MediaFilePanel::~MediaFilePanel() 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); 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) void MediaFilePanel::UpdateByObj(MachineObject* obj)
{ {
bool sdcard_state_changed = false; bool sdcard_state_changed = false;
+5
View File
@@ -11,6 +11,7 @@
#include <boost/smart_ptr/shared_ptr.hpp> #include <boost/smart_ptr/shared_ptr.hpp>
#include <cstddef> #include <cstddef>
#include <set> #include <set>
#include <vector>
#include "GUI_Utils.hpp" #include "GUI_Utils.hpp"
#include "wxExtensions.hpp" #include "wxExtensions.hpp"
@@ -53,6 +54,8 @@ public:
private: private:
void modeChanged(wxCommandEvent & e); void modeChanged(wxCommandEvent & e);
void on_show_hide(wxShowEvent & e);
void fetchUrl(boost::weak_ptr<PrinterFileSystem> fs); void fetchUrl(boost::weak_ptr<PrinterFileSystem> fs);
void doAction(size_t index, int action); void doAction(size_t index, int action);
@@ -99,6 +102,8 @@ private:
int m_last_mode = 0; int m_last_mode = 0;
int m_last_type = 0; int m_last_type = 0;
std::set<int> m_last_errors; std::set<int> m_last_errors;
std::vector<wxWindow*> m_show_hosts;
}; };
+10 -9
View File
@@ -44,7 +44,7 @@
#include "Plater.hpp" #include "Plater.hpp"
#include "MainFrame.hpp" #include "MainFrame.hpp"
#include "MediaPlayCtrl.h" #include "MediaPlayCtrl.h"
#include "MediaFilePanel.h" #include "StoragePanel.h"
#include "Plater.hpp" #include "Plater.hpp"
#include "BindDialog.hpp" #include "BindDialog.hpp"
@@ -202,9 +202,9 @@ void MonitorPanel::init_tabpanel()
m_tabpanel->SetBackgroundColour(wxColour("#FEFFFF")); m_tabpanel->SetBackgroundColour(wxColour("#FEFFFF"));
m_tabpanel->Bind(wxEVT_BOOKCTRL_PAGE_CHANGED, [this](wxBookCtrlEvent& e) { m_tabpanel->Bind(wxEVT_BOOKCTRL_PAGE_CHANGED, [this](wxBookCtrlEvent& e) {
auto page = m_tabpanel->GetCurrentPage(); auto page = m_tabpanel->GetCurrentPage();
if (page == m_media_file_panel) { if (page == m_storage_panel) {
auto title = m_tabpanel->GetPageText(m_tabpanel->GetSelection()); 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. // The first page is selected while the panel is built off screen.
if (page->IsShownOnScreen()) if (page->IsShownOnScreen())
@@ -218,8 +218,8 @@ void MonitorPanel::init_tabpanel()
add_build_steps_of(*m_status_info_panel); add_build_steps_of(*m_status_info_panel);
m_tabpanel->AddPage(m_status_info_panel, _L("Status"), true); m_tabpanel->AddPage(m_status_info_panel, _L("Status"), true);
add_build_step([this] { add_build_step([this] {
m_media_file_panel = new MediaFilePanel(m_tabpanel); m_storage_panel = new StoragePanel(m_tabpanel);
m_tabpanel->AddPage(m_media_file_panel, _L("Storage"), false); m_tabpanel->AddPage(m_storage_panel, _L("Storage"), false);
}); });
add_build_step([this] { add_build_step([this] {
m_upgrade_panel = new UpgradePanel(m_tabpanel); m_upgrade_panel = new UpgradePanel(m_tabpanel);
@@ -268,7 +268,8 @@ void MonitorPanel::on_sys_color_changed()
{ {
m_status_info_panel->on_sys_color_changed(); m_status_info_panel->on_sys_color_changed();
m_upgrade_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() void MonitorPanel::msw_rescale()
@@ -280,7 +281,7 @@ void MonitorPanel::msw_rescale()
m_tabpanel->Rescale(); m_tabpanel->Rescale();
//m_status_add_machine_panel->msw_rescale(); //m_status_add_machine_panel->msw_rescale();
m_status_info_panel->msw_rescale(); m_status_info_panel->msw_rescale();
m_media_file_panel->Rescale(); m_storage_panel->Rescale();
m_upgrade_panel->msw_rescale(); m_upgrade_panel->msw_rescale();
m_hms_panel->msw_rescale(); m_hms_panel->msw_rescale();
@@ -427,8 +428,8 @@ void MonitorPanel::update_all()
} }
} else if (current_page == m_upgrade_panel) { } else if (current_page == m_upgrade_panel) {
m_upgrade_panel->update(obj); m_upgrade_panel->update(obj);
} else if (current_page == m_media_file_panel) { } else if (current_page == m_storage_panel) {
m_media_file_panel->UpdateByObj(obj); m_storage_panel->UpdateByObj(obj);
} }
if (current_page == m_hms_panel || (obj->GetHMS()->GetHMSItems().size() != m_hms_panel->temp_hms_list.size())) { 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 Slic3r {
namespace GUI { namespace GUI {
class MediaFilePanel; class StoragePanel;
class AddMachinePanel : public wxPanel class AddMachinePanel : public wxPanel
{ {
@@ -86,7 +86,7 @@ private:
wxSizer* m_main_sizer{ nullptr }; wxSizer* m_main_sizer{ nullptr };
StatusPanel* m_status_info_panel{ nullptr }; StatusPanel* m_status_info_panel{ nullptr };
MediaFilePanel* m_media_file_panel{ nullptr }; StoragePanel* m_storage_panel{ nullptr };
UpgradePanel* m_upgrade_panel{ nullptr }; UpgradePanel* m_upgrade_panel{ nullptr };
HMSPanel* m_hms_panel{ nullptr }; HMSPanel* m_hms_panel{ nullptr };
+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
@@ -6422,9 +6422,10 @@ void Sidebar::sync_ams_list(bool is_from_big_sync_btn)
for (const auto &entry : wxGetApp().preset_bundle->filament_ams_list) { for (const auto &entry : wxGetApp().preset_bundle->filament_ams_list) {
const auto &tray = entry.second; const auto &tray = entry.second;
const bool has_filament = !tray.opt_string("filament_id", 0u).empty(); 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") && const bool is_placeholder = tray.has("filament_slot_placeholder") &&
tray.opt_bool("filament_slot_placeholder", 0u); tray.opt_bool("filament_slot_placeholder", 0u);
if (!has_filament && !is_placeholder) { if (!has_filament && !is_placeholder && !has_type) {
continue; continue;
} }
if (combo_index >= p->combos_filament.size()) { 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;
};
}}
+27
View File
@@ -36,6 +36,8 @@
#include "libslic3r/MultiNozzleUtils.hpp" // filament-change-gap model for the best-position popup #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 "BackgroundSlicingProcess.hpp" // complete type for background_process().get_current_gcode_result()
#include "DeviceCore/DevStorage.h" #include "DeviceCore/DevStorage.h"
#include "slic3r/Utils/NetworkAgentFactory.hpp"
#include "FilamentMappingUtils.hpp"
#include <wx/event.h> #include <wx/event.h>
#include <string> #include <string>
@@ -3568,6 +3570,9 @@ void SelectMachineDialog::navigate_to_timelapse_page()
this->EndModal(wxID_CANCEL); this->EndModal(wxID_CANCEL);
} }
// Mapping helpers live in FilamentMappingUtils.hpp (shared with
// SendMultiMachinePage); they mirror the agent serializer exactly.
void SelectMachineDialog::on_send_print() void SelectMachineDialog::on_send_print()
{ {
BOOST_LOG_TRIVIAL(info) << "print_job: on_ok to send"; BOOST_LOG_TRIVIAL(info) << "print_job: on_ok to send";
@@ -3622,6 +3627,28 @@ void SelectMachineDialog::on_send_print()
get_ams_mapping_result(ams_mapping_array,ams_mapping_array2, ams_mapping_info); 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) { 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) { 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; if (this->m_is_canceled) return;
+3 -2
View File
@@ -8,6 +8,7 @@
#include "GUI.hpp" #include "GUI.hpp"
#include "GUI_App.hpp" #include "GUI_App.hpp"
#include "slic3r/Utils/NetworkAgent.hpp"
#include "GUI_Preview.hpp" #include "GUI_Preview.hpp"
#include "MainFrame.hpp" #include "MainFrame.hpp"
#include "Widgets/ProgressDialog.hpp" #include "Widgets/ProgressDialog.hpp"
@@ -847,8 +848,8 @@ void SelectMachinePopup::update_machine_list(wxCommandEvent &event)
void SelectMachinePopup::start_ssdp(bool start) void SelectMachinePopup::start_ssdp(bool start)
{ {
return; if (auto* agent = wxGetApp().getAgent())
//if (wxGetApp().getAgent()) { wxGetApp().getAgent()->start_discovery(true, start); } agent->start_discovery(true, start);
} }
void SelectMachinePopup::OnLeftUp(wxMouseEvent &event) void SelectMachinePopup::OnLeftUp(wxMouseEvent &event)
+30
View File
@@ -61,6 +61,10 @@
#include "DeviceCore/DevManager.h" #include "DeviceCore/DevManager.h"
#include "DeviceCore/DevStorage.h" #include "DeviceCore/DevStorage.h"
#include "slic3r/Utils/NetworkAgentFactory.hpp"
#include "FilamentMappingUtils.hpp"
#include "libslic3r/AppConfig.hpp" #include "libslic3r/AppConfig.hpp"
#include "libslic3r/GCode/GCodeProcessor.hpp" #include "libslic3r/GCode/GCodeProcessor.hpp"
#include "libslic3r/Model.hpp" #include "libslic3r/Model.hpp"
@@ -760,6 +764,8 @@ bool SendMultiMachinePage::get_ams_mapping_result(std::string &mapping_array_str
return true; return true;
} }
// Mapping helpers live in FilamentMappingUtils.hpp, shared with SelectMachine.
void SendMultiMachinePage::on_send(wxCommandEvent& event) void SendMultiMachinePage::on_send(wxCommandEvent& event)
{ {
event.Skip(); event.Skip();
@@ -808,6 +814,30 @@ void SendMultiMachinePage::on_send(wxCommandEvent& event)
if (!wxGetApp().is_blocking_printing(obj)) { if (!wxGetApp().is_blocking_printing(obj)) {
PrintParams params = request_params(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); 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); 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) if (!m_filament_setting_dlg->m_is_third)
{ {
sn_number = tray->uuid; sn_number = tray->uuid;
filament = tray->sub_brands; 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); 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) { if (!m_filament_setting_dlg->m_is_third) {
sn_number = obj->vt_slot[nozzle_index].uuid; sn_number = obj->vt_slot[nozzle_index].uuid;
filament = obj->vt_slot[nozzle_index].sub_brands; 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)); 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
@@ -1425,6 +1425,12 @@ bool SyncAmsInfoDialog::get_ams_mapping_result(std::string &mapping_array_str, s
BOOST_LOG_TRIVIAL(error) << "get_ams_mapping_result, plater is nullptr"; 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++) { for (int i = 0; i < wxGetApp().preset_bundle->filament_presets.size(); i++) {
int tray_id = -1; int tray_id = -1;
json mapping_item_v1; 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; Caninfo info;
// tray is exists // tray is exists
if (it != ams->GetTrays().end() && it->second->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()) { if (it->second->is_tray_info_ready()) {
info.can_id = it->second->id; info.can_id = it->second->id;
info.ctype = it->second->ctype; info.ctype = it->second->ctype;
@@ -168,6 +169,7 @@ void AMSinfo::parse_ext_info(MachineObject* obj, DevAmsTray tray) {
this->ams_type = AMSModel::EXT_AMS; this->ams_type = AMSModel::EXT_AMS;
Caninfo info; Caninfo info;
info.can_id = std::to_string(0); info.can_id = std::to_string(0);
info.is_empty = tray.is_empty;
this->cans.clear(); this->cans.clear();
if (tray.id == std::to_string(VIRTUAL_TRAY_MAIN_ID)) 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); dc.SetTextForeground(temp_text_colour);
auto libsize = GetSize(); 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_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()) { if (m_info.material_name.empty()) {
auto tsize = dc.GetMultiLineTextExtent("?"); 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) { if (m_info.material_state == AMSCanType::AMS_CAN_TYPE_EMPTY || m_info.is_empty) {
auto tsize = dc.GetMultiLineTextExtent("/"); auto tsize = dc.GetMultiLineTextExtent(_L("/"));
auto pot = wxPoint((libsize.x - tsize.x) / 2 + FromDIP(2), (libsize.y - tsize.y) / 2 + FromDIP(3)); auto pot = wxPoint((libsize.x - tsize.x) / 2 + FromDIP(2), (libsize.y - tsize.y) / 2 + FromDIP(3));
dc.DrawText("/", pot); dc.DrawText("/", pot);
} }
@@ -1189,8 +1191,12 @@ void AMSLib::render_lite_text(wxDC& dc)
void AMSLib::render_generic_text(wxDC &dc) void AMSLib::render_generic_text(wxDC &dc)
{ {
bool show_k_value = true; // K/N is Bambu firmware's flow-dynamics calibration; agents with no printer-side
if (m_info.material_name.empty()) { // 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; 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))) { 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(); 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_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*/) { if (m_info.material_name.empty() /*&& m_info.material_state != AMSCanType::AMS_CAN_TYPE_VIRTUAL*/) {
auto tsize = dc.GetMultiLineTextExtent("?"); 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_top_tsize = dc.GetMultiLineTextExtent(line_top);
auto line_bottom_tsize = dc.GetMultiLineTextExtent(line_bottom); 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)); 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); dc.DrawText(line_top, pot_top);
@@ -1302,7 +1308,7 @@ void AMSLib::render_generic_text(wxDC &dc)
//draw k&n //draw k&n
if (m_obj && show_k_value) { 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_k = wxString::Format("K %1.3f", m_info.k);
wxString str_n = wxString::Format("N %1.3f", m_info.n); wxString str_n = wxString::Format("N %1.3f", m_info.n);
dc.SetFont(::Label::Body_11); 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 tsize = dc.GetMultiLineTextExtent(_L("Empty"));
auto pot = wxPoint((libsize.x - tsize.x) / 2, (libsize.y - tsize.y) / 2 + FromDIP(3)); auto pot = wxPoint((libsize.x - tsize.x) / 2, (libsize.y - tsize.y) / 2 + FromDIP(3));
dc.DrawText(_L("Empty"), pot); dc.DrawText(_L("Empty"), pot);
@@ -2810,7 +2816,7 @@ void AMSPreview::doRender(wxDC &dc)
} }
else { else {
wxRect rect(left, (size.y - AMS_ITEM_CUBE_SIZE.y) / 2, AMS_ITEM_CUBE_SIZE.x, AMS_ITEM_CUBE_SIZE.y); 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.SetPen(wxPen(wxColor(0, 0, 0)));
dc.DrawLine(rect.GetRight() - FromDIP(1), rect.GetTop() + FromDIP(1), rect.GetLeft() + FromDIP(1), rect.GetBottom() - FromDIP(1)); 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; wxString material_name;
wxColour material_colour = {*wxWHITE}; wxColour material_colour = {*wxWHITE};
AMSCanType material_state; AMSCanType material_state;
bool is_empty = false;
int ctype=0; int ctype=0;
int material_remain = 100; int material_remain = 100;
int cali_idx = -1; int cali_idx = -1;
@@ -215,6 +216,7 @@ public:
material_name == other.material_name && material_name == other.material_name &&
material_colour == other.material_colour && material_colour == other.material_colour &&
material_state == other.material_state && material_state == other.material_state &&
is_empty == other.is_empty &&
ctype == other.ctype && ctype == other.ctype &&
material_remain == other.material_remain && material_remain == other.material_remain &&
cali_idx == other.cali_idx && cali_idx == other.cali_idx &&
+60 -1
View File
@@ -31,8 +31,23 @@
#include <curl/curl.h> #include <curl/curl.h>
#include <utility> #include <utility>
#ifdef OPENSSL_CERT_OVERRIDE #include <openssl/err.h>
#include <openssl/ssl.h>
#include <openssl/x509.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 #endif
namespace fs = boost::filesystem; namespace fs = boost::filesystem;
@@ -956,6 +971,50 @@ std::string Http::tls_system_cert_store()
return ret; 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) std::string Http::url_encode(const std::string &str)
{ {
::CURL *curl = ::curl_easy_init(); ::CURL *curl = ::curl_easy_init();
+6
View File
@@ -13,6 +13,8 @@
#include "libslic3r/Exception.hpp" #include "libslic3r/Exception.hpp"
#include "libslic3r_version.h" #include "libslic3r_version.h"
typedef struct ssl_ctx_st SSL_CTX;
#define MAX_SIZE_TO_FILE 3*1024 #define MAX_SIZE_TO_FILE 3*1024
namespace Slic3r { namespace Slic3r {
@@ -200,6 +202,10 @@ public:
// Return empty string on success or error message on fail. // Return empty string on success or error message on fail.
static std::string tls_global_init(); static std::string tls_global_init();
static std::string tls_system_cert_store(); 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 // converts the given string to an url_encoded_string
static std::string url_encode(const std::string &str); static std::string url_encode(const std::string &str);
+81
View File
@@ -13,6 +13,7 @@
#include <memory> #include <memory>
#include <vector> #include <vector>
#include <functional> #include <functional>
#include <cstdint>
#include <cmath> #include <cmath>
#include <nlohmann/json.hpp> #include <nlohmann/json.hpp>
#include <boost/format.hpp> #include <boost/format.hpp>
@@ -68,6 +69,34 @@ enum class CameraStreamMode {
http_snapshot // HTTP endpoint returning one image per request 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. * 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; 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 // 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 // gcode live on the agent that speaks it; the default is an honest refusal that MachineObject's
// publish funnel turns into a dialog. // publish funnel turns into a dialog.
@@ -316,6 +355,48 @@ public:
return -1; 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. * 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; 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) 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) if (m_printer_agent)
@@ -1106,4 +1136,32 @@ int NetworkAgent::get_hms_snapshot(std::string dev_id, std::string file_name, st
return -1; 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 } // namespace Slic3r
+8
View File
@@ -153,6 +153,10 @@ public:
int set_on_local_message_fn(OnMessageFn fn); int set_on_local_message_fn(OnMessageFn fn);
int set_server_callback(OnServerErrFn fn); int set_server_callback(OnServerErrFn fn);
int send_message(std::string dev_id, std::string json_str, int qos, int flag); 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_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_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); 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; std::string from_orca_filament_id(const std::string& orca_filament_id) const;
int request_bind_ticket(std::string* ticket); 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 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: private:
struct PrinterCallbacks { struct PrinterCallbacks {
+11 -8
View File
@@ -17,7 +17,7 @@
namespace Slic3r { namespace Slic3r {
static constexpr char ORCA_PRINTER_AGENT_ID[] = "orca"; 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 // Factory function type for creating printer agents
using PrinterAgentFactory = using PrinterAgentFactory =
@@ -26,17 +26,20 @@ using PrinterAgentFactory =
// Information about a registered printer agent // Information about a registered printer agent
struct PrinterAgentInfo struct PrinterAgentInfo
{ {
std::string id; // Registry/config key, e.g. "orca" or a plugin AgentInfo::id 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 display_name; // e.g., "Orca Native", "Bambu Lab"
std::string plugin_identifier; // Empty for built-ins, otherwise <plugin_key>;<uuid>;<capability_name> std::string plugin_identifier; // Empty for built-ins, otherwise <plugin_key>;<uuid>;<capability_name>
PrinterAgentFactory factory; // Function to create the agent PrinterAgentFactory factory; // Function to create the agent
bool is_plugin() const { return !plugin_identifier.empty(); } bool is_plugin() const { return !plugin_identifier.empty(); }
PrinterAgentInfo(const std::string& id_, const std::string& display_name_, PrinterAgentFactory factory_) PrinterAgentInfo(const std::string& id_, const std::string& display_name_, PrinterAgentFactory factory_)
: id(id_), display_name(display_name_), factory(std::move(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_)) : 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 * @param log_dir Directory for log files
* @return Shared pointer to IPrinterAgent, or nullptr if ID not found * @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, std::shared_ptr<ICloudServiceAgent> cloud_agent,
const std::string& log_dir); const std::string& log_dir);
/** /**
* Clear the printer agent cache. * Clear the printer agent cache.
+298 -12
View File
@@ -1,6 +1,9 @@
#include "OrcaCloudServiceAgent.hpp" #include "OrcaCloudServiceAgent.hpp"
#include "CloudProvider.hpp" #include "CloudProvider.hpp"
#include "Http.hpp" #include "Http.hpp"
#include "ICameraSignalingChannel.hpp"
#include "OrcaCloudSignalingChannel.hpp"
#include "OrcaMqttConnection.hpp"
#include "bambu_networking.hpp" #include "bambu_networking.hpp"
#include "ICloudServiceAgent.hpp" #include "ICloudServiceAgent.hpp"
#include "libslic3r/Utils.hpp" #include "libslic3r/Utils.hpp"
@@ -12,8 +15,11 @@
#include <boost/asio/ip/tcp.hpp> #include <boost/asio/ip/tcp.hpp>
#include <boost/asio/io_context.hpp> #include <boost/asio/io_context.hpp>
#include <boost/beast/core/detail/base64.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.hpp>
#include <boost/filesystem/operations.hpp> #include <boost/filesystem/operations.hpp>
#include <boost/filesystem/path.hpp>
#include <boost/log/trivial.hpp> #include <boost/log/trivial.hpp>
#include <boost/uuid/name_generator_sha1.hpp> #include <boost/uuid/name_generator_sha1.hpp>
#include <boost/uuid/uuid.hpp> #include <boost/uuid/uuid.hpp>
@@ -37,14 +43,18 @@
#include <openssl/hmac.h> #include <openssl/hmac.h>
#include <openssl/rand.h> #include <openssl/rand.h>
#include <openssl/sha.h> #include <openssl/sha.h>
#include <openssl/ssl.h>
#include <algorithm> #include <algorithm>
#include <cctype> #include <cctype>
#include <condition_variable>
#include <cstdint>
#include <cstdlib> #include <cstdlib>
#include <fstream> #include <fstream>
#include <iomanip> #include <iomanip>
#include <optional> #include <optional>
#include <random> #include <random>
#include <set>
#include <sstream> #include <sstream>
#include <string> #include <string>
@@ -513,6 +523,7 @@ OrcaCloudServiceAgent::OrcaCloudServiceAgent(std::string log_dir)
, api_base_url(ORCA_DEFAULT_API_URL) , api_base_url(ORCA_DEFAULT_API_URL)
, auth_base_url(ORCA_DEFAULT_AUTH_URL) , auth_base_url(ORCA_DEFAULT_AUTH_URL)
, cloud_base_url(ORCA_DEFAULT_CLOUD_URL) , cloud_base_url(ORCA_DEFAULT_CLOUD_URL)
, mqtt_connection(std::make_unique<OrcaMqttConnection>())
{ {
auth_headers["apikey"] = ORCA_DEFAULT_PUB_KEY; auth_headers["apikey"] = ORCA_DEFAULT_PUB_KEY;
pkce_bundle.loopback_port = choose_loopback_port(); pkce_bundle.loopback_port = choose_loopback_port();
@@ -523,6 +534,8 @@ OrcaCloudServiceAgent::OrcaCloudServiceAgent(std::string log_dir)
OrcaCloudServiceAgent::~OrcaCloudServiceAgent() OrcaCloudServiceAgent::~OrcaCloudServiceAgent()
{ {
if (mqtt_connection)
mqtt_connection->stop();
if (refresh_thread.joinable()) { if (refresh_thread.joinable()) {
refresh_thread.join(); refresh_thread.join();
} }
@@ -965,22 +978,45 @@ bool OrcaCloudServiceAgent::ensure_token_fresh(const std::string& reason) { retu
int OrcaCloudServiceAgent::connect_server() 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; std::string response;
unsigned int http_code = 0; unsigned int http_code = 0;
int result = http_get(ORCA_HEALTH_PATH, &response, &http_code); int result = http_get(ORCA_HEALTH_PATH, &response, &http_code);
bool connected = (result == BAMBU_NETWORK_SUCCESS && http_code >= 200 && http_code < 300); 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); std::lock_guard<std::recursive_mutex> lock(state_mutex);
is_connected = connected; is_connected = connected;
} }
invoke_server_connected_callback(connected ? 0 : -1, http_code); invoke_server_connected_callback(connected ? 0 : -1, http_code);
return connected ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_CONNECTION_TO_SERVER_FAILED; return connected ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_CONNECTION_TO_SERVER_FAILED;
} }
bool OrcaCloudServiceAgent::is_server_connected() 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); std::lock_guard<std::recursive_mutex> lock(state_mutex);
return is_connected; return is_connected;
} }
@@ -1001,13 +1037,238 @@ int OrcaCloudServiceAgent::stop_subscribe(std::string module)
int OrcaCloudServiceAgent::add_subscribe(std::vector<std::string> dev_list) int OrcaCloudServiceAgent::add_subscribe(std::vector<std::string> dev_list)
{ {
(void) dev_list; BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: OrcaCloudServiceAgent::add_subscribe count=" << dev_list.size()
return BAMBU_NETWORK_SUCCESS; << " 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) 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; 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) 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); std::lock_guard<std::mutex> lock(session_mutex);
session = SessionInfo{}; 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; 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; std::string url = api_base_url + path;
BOOST_LOG_TRIVIAL(trace) << "OrcaCloudServiceAgent: POST " << url; 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("Authorization", "Bearer " + token);
} }
http.header("Content-Type", "application/json"); http.header("Content-Type", content_type);
http.set_post_body(body); http.set_post_body(body);
http.on_complete([&](std::string resp_body, unsigned resp_status) { 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) if (http_code)
*http_code = 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; 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 { try {
auto resp_json = nlohmann::json::parse(response); auto resp_json = nlohmann::json::parse(response);
nlohmann::json devices = nlohmann::json::array(); nlohmann::json devices = nlohmann::json::array();
for (const auto& printer : resp_json.value("data", 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") if (role.empty() || role == "viewer")
continue; continue;
nlohmann::json device;
device["dev_id"] = printer.value("id", ""); device["dev_id"] = printer.value("id", "");
device["dev_name"] = printer.value("name", ""); device["dev_name"] = printer.value("name", "");
if (printer.contains("model") && printer["model"].is_string()) 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["task_status"] = status["job"].value("state", "");
} }
device["dev_online"] = online; device["dev_online"] = online;
devices.push_back(std::move(device));
devices.push_back(device);
} }
if (http_body) { if (http_body) {
nlohmann::json out; nlohmann::json out;
out["devices"] = std::move(devices); out["devices"] = devices;
*http_body = out.dump(); *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; 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; 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) int OrcaCloudServiceAgent::get_design_staffpick(int offset, int limit, std::function<void(std::string)> callback)
{ {
BOOST_LOG_TRIVIAL(debug) << "OrcaCloudServiceAgent: get_design_staffpick (stub)"; BOOST_LOG_TRIVIAL(debug) << "OrcaCloudServiceAgent: get_design_staffpick (stub)";
+57 -2
View File
@@ -4,6 +4,11 @@
#include "CloudProvider.hpp" #include "CloudProvider.hpp"
#include "ICameraSignalingChannel.hpp" #include "ICameraSignalingChannel.hpp"
#include "ICloudServiceAgent.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 "bambu_networking.hpp"
#include <cstdlib> #include <cstdlib>
#include "libslic3r/ProjectTask.hpp" #include "libslic3r/ProjectTask.hpp"
@@ -13,6 +18,9 @@
#include <atomic> #include <atomic>
#include <chrono> #include <chrono>
#include <functional> #include <functional>
#include <condition_variable>
#include <cstdint>
#include <set>
#include <memory> #include <memory>
#include <thread> #include <thread>
#include <unordered_map> #include <unordered_map>
@@ -20,6 +28,8 @@
#include <vector> #include <vector>
#include <nlohmann/json.hpp> #include <nlohmann/json.hpp>
#include "OrcaMqttConnection.hpp"
class wxSecretStore; class wxSecretStore;
namespace Slic3r { namespace Slic3r {
@@ -156,6 +166,8 @@ public:
// Configuration // Configuration
void configure_urls(AppConfig* app_config); 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_api_base_url(const std::string& url);
void set_auth_base_url(const std::string& url); void set_auth_base_url(const std::string& url);
void set_cloud_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 add_subscribe(std::vector<std::string> dev_list) override;
int del_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; 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 // ICloudServiceAgent Interface Implementation - Settings Synchronization
@@ -246,7 +271,7 @@ public:
// ICloudServiceAgent Interface Implementation - Model Mall & Publishing // ICloudServiceAgent Interface Implementation - Model Mall & Publishing
// ======================================================================== // ========================================================================
int get_camera_url(std::string dev_id, std::function<void(std::string)> callback) override; 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 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 start_publish(PublishParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, std::string* out) override;
int get_model_publish_url(std::string* url) 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 = ""); 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: 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 // Sync protocol helpers
int sync_pull( int sync_pull(
std::function<void(const SyncPullResponse&)> on_success, std::function<void(const SyncPullResponse&)> on_success,
@@ -376,7 +423,11 @@ private:
// HTTP request helpers // HTTP request helpers
int http_get(const std::string& path, std::string* response_body, unsigned int* http_code); 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_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); int http_delete(const std::string& path, std::string* response_body, unsigned int* http_code);
std::map<std::string, std::string> data_headers(); std::map<std::string, std::string> data_headers();
@@ -434,6 +485,9 @@ private:
std::chrono::system_clock::now().time_since_epoch()).count()}; std::chrono::system_clock::now().time_since_epoch()).count()};
// Member variables - connection state // 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 is_connected{false};
bool enable_track{false}; bool enable_track{false};
bool multi_machine_enabled{false}; bool multi_machine_enabled{false};
@@ -447,6 +501,7 @@ private:
AppOnHttpErrorFn on_http_error_fn; AppOnHttpErrorFn on_http_error_fn;
GetCountryCodeFn get_country_code_fn; GetCountryCodeFn get_country_code_fn;
QueueOnMainFn queue_on_main_fn; QueueOnMainFn queue_on_main_fn;
OnMessageFn printer_status_callback;
mutable std::mutex callback_mutex; mutable std::mutex callback_mutex;
// Thread safety // 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
+920
View File
@@ -0,0 +1,920 @@
#include "OrcaMqttConnection.hpp"
#include "Http.hpp"
#include <boost/asio.hpp>
#include <boost/asio/io_context.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <boost/asio/post.hpp>
#include <boost/asio/buffer.hpp>
#include <boost/asio/error.hpp>
#include <boost/asio/ssl.hpp>
#include <boost/asio/ssl/context.hpp>
#include <boost/asio/steady_timer.hpp>
#include <boost/asio/ssl/verify_mode.hpp>
#include <boost/asio/ssl/host_name_verification.hpp>
#include <boost/asio/ssl/stream_base.hpp>
#include <boost/beast/core.hpp>
#include <boost/beast/core/flat_buffer.hpp>
#include <boost/beast/core/stream_traits.hpp>
#include <boost/beast/core/buffers_to_string.hpp>
#include <boost/beast/http/field.hpp>
#include <boost/beast/http/message.hpp>
#include <boost/beast/http/string_body.hpp>
#include <boost/beast/ssl.hpp>
#include <boost/beast/websocket.hpp>
#include <boost/beast/websocket/rfc6455.hpp>
#include <boost/beast/websocket/stream_base.hpp>
#include <boost/system/errc.hpp>
#include <boost/system/error_code.hpp>
#include <openssl/ssl.h>
#include <openssl/tls1.h>
#include <algorithm>
#include <atomic>
#include <chrono>
#include <cstddef>
#include <cstdint>
#include <deque>
#include <exception>
#include <ios>
#include <memory>
#include <mutex>
#include <optional>
#include <sstream>
#include <stdexcept>
#include <string>
#include <utility>
#include <vector>
namespace Slic3r {
struct OrcaMqttConnection::Connection {
boost::asio::io_context io_context;
boost::asio::ssl::context ssl_context;
boost::asio::ip::tcp::resolver resolver;
boost::asio::steady_timer keepalive_timer;
boost::beast::flat_buffer read_buffer;
// Reassembly buffer for the MQTT byte stream: each WebSocket message carries an
// arbitrary byte range of that stream, so partial packets accumulate here until
// complete. Only the MQTT worker thread touches it.
std::string mqtt_rx;
std::deque<std::shared_ptr<std::vector<uint8_t>>> outbound_packets;
boost::system::error_code terminal_error;
std::atomic_bool async_session_started{false};
bool write_in_progress{false};
// Exactly one of these is engaged once ws_handshake() has run: wss for
// wss:// endpoints, ws for plaintext ws://.
std::optional<TlsWebSocket> wss;
std::optional<PlainWebSocket> ws;
Connection()
: ssl_context(boost::asio::ssl::context::tls_client)
, resolver(io_context)
, keepalive_timer(io_context)
{}
};
namespace {
// Apply / clear a tcp_stream timeout on whichever websocket is engaged.
// Templated on the connection type only because Connection is a private nested
// type: a deduced parameter needs no (inaccessible) name for it.
template<class Conn> void expires_after(Conn& conn, std::chrono::seconds timeout) {
if (conn.wss) boost::beast::get_lowest_layer(*conn.wss).expires_after(timeout);
else if (conn.ws) boost::beast::get_lowest_layer(*conn.ws).expires_after(timeout);
}
template<class Conn> void expires_never(Conn& conn) {
if (conn.wss) boost::beast::get_lowest_layer(*conn.wss).expires_never();
else if (conn.ws) boost::beast::get_lowest_layer(*conn.ws).expires_never();
}
// Total byte length of the MQTT packet starting at `offset` in `stream`. Returns
// kMqttPacketIncomplete when the fixed header's remaining-length varint is not yet
// complete, or kMqttPacketMalformed when it cannot be valid (more than four length
// bytes, or a declared size past the sanity cap). The caller compares a real length
// against the bytes available.
constexpr std::size_t kMqttPacketIncomplete = 0;
constexpr std::size_t kMqttPacketMalformed = static_cast<std::size_t>(-1);
// MQTT permits ~256 MiB; nothing this client receives is close, so a larger
// declared length is treated as malformed rather than buffered.
constexpr std::size_t kMqttMaxPacketBytes = 16 * 1024 * 1024;
std::size_t mqtt_packet_length(const std::string& stream, std::size_t offset) {
if (offset >= stream.size())
return kMqttPacketIncomplete;
std::size_t multiplier = 1;
std::size_t remaining = 0;
std::size_t i = offset + 1;
for (int len_bytes = 0; len_bytes < 4; ++len_bytes) {
if (i >= stream.size())
return kMqttPacketIncomplete; // varint not complete yet
const std::uint8_t byte = static_cast<std::uint8_t>(stream[i++]);
remaining += static_cast<std::size_t>(byte & 0x7f) * multiplier;
if ((byte & 0x80) == 0) {
const std::size_t total = (i - offset) + remaining;
return total > kMqttMaxPacketBytes ? kMqttPacketMalformed : total;
}
multiplier *= 128;
}
return kMqttPacketMalformed; // more than four length bytes
}
} // namespace
OrcaMqttConnection::~OrcaMqttConnection() { stop(); }
bool OrcaMqttConnection::start(const Config& config, MessageHandler on_message, StateHandler on_state) {
{
std::lock_guard<std::recursive_mutex> lifecycle_lock(lifecycle_mutex);
stop();
{
std::lock_guard<std::mutex> lock(mutex);
current_config = config;
this->on_message = std::move(on_message);
this->on_state = std::move(on_state);
initial_result = false;
initial_completed = false;
connected = false;
m_last_connack_rc.store(-1);
}
stopping.store(false);
worker = std::thread(&OrcaMqttConnection::run, this);
}
// Wait without holding lifecycle_mutex so a concurrent stop() can cancel the
// attempt and signal completion instead of blocking until the timeout.
std::unique_lock<std::mutex> lock(mutex);
if (!initial_cv.wait_for(lock, std::chrono::seconds(10), [this] { return initial_completed; })) {
initial_completed = true;
initial_result = false;
}
return initial_result;
}
void OrcaMqttConnection::stop() {
std::lock_guard<std::recursive_mutex> lifecycle_lock(lifecycle_mutex);
stopping.store(true);
state_cv.notify_all();
{
std::lock_guard<std::mutex> lock(connection_mutex);
if (active_connection) {
if (active_connection->async_session_started.load()) {
// The worker owns the live WebSocket. Stop dispatching its
// asynchronous operations; the worker closes the socket after
// leaving the event loop.
active_connection->io_context.stop();
} else {
// Setup still uses synchronous operations on the worker. Wake a
// pending resolve/connect/CONNACK read without competing with a
// live asynchronous session.
auto shutdown_socket = [](auto& websocket) {
auto& socket = boost::beast::get_lowest_layer(websocket).socket();
boost::system::error_code socket_error;
if (socket.cancel(socket_error))
return;
if (socket.shutdown(boost::asio::ip::tcp::socket::shutdown_both, socket_error))
return;
if (socket.close(socket_error))
return;
};
if (active_connection->wss)
shutdown_socket(*active_connection->wss);
else if (active_connection->ws)
shutdown_socket(*active_connection->ws);
active_connection->resolver.cancel();
}
}
}
if (worker.joinable())
worker.join();
{
std::lock_guard<std::mutex> lock(mutex);
connected = false;
acknowledged_subscriptions.clear();
pending_subscribe_packets.clear();
pending_requests.clear();
if (!initial_completed) {
initial_completed = true;
initial_result = false;
}
}
initial_cv.notify_all();
}
bool OrcaMqttConnection::is_running() const {
return worker.joinable() && !stopping.load();
}
void OrcaMqttConnection::flush_subscription_change() {
std::shared_ptr<Connection> conn;
{
std::lock_guard<std::mutex> lock(connection_mutex);
conn = active_connection;
}
bool connacked;
{
std::lock_guard<std::mutex> lock(mutex);
connacked = connected;
}
if (!conn || !connacked)
return; // no live MQTT session yet — the worker sends the set on CONNACK
boost::asio::post(conn->io_context, [this, conn] {
if (!stopping.load() && connected.load())
send_pending_subscriptions(conn);
});
}
bool OrcaMqttConnection::subscribe(const std::string& dev_id) {
if (dev_id.empty()) {
return false;
}
const std::string topic = report_topic(dev_id);
if (topic.size() > 96) { // MQTT topic filter cap enforced by the service
return false;
}
{
std::lock_guard<std::mutex> lock(mutex);
if (subscriptions.count(topic) != 0 && pending_unsubscriptions.count(topic) == 0) {
return true;
}
subscriptions.insert(topic);
pending_unsubscriptions.erase(topic);
pending_subscriptions.insert(topic);
}
state_cv.notify_all();
flush_subscription_change(); // ask the worker to emit SUBSCRIBE now (no reconnect)
return true;
}
bool OrcaMqttConnection::unsubscribe(const std::string& dev_id) {
const std::string topic = report_topic(dev_id);
{
std::lock_guard<std::mutex> lock(mutex);
subscriptions.erase(topic);
acknowledged_subscriptions.erase(topic);
pending_subscriptions.erase(topic);
pending_unsubscriptions.insert(topic);
for (auto it = pending_subscribe_packets.begin(); it != pending_subscribe_packets.end();) {
if (it->second == topic)
it = pending_subscribe_packets.erase(it);
else
++it;
}
for (auto it = pending_requests.begin(); it != pending_requests.end();) {
if (it->first == dev_id)
it = pending_requests.erase(it);
else
++it;
}
}
state_cv.notify_all();
flush_subscription_change(); // ask the worker to emit UNSUBSCRIBE now (no reconnect)
return true;
}
void OrcaMqttConnection::clear_subscriptions() {
std::lock_guard<std::mutex> lock(mutex);
subscriptions.clear();
pending_subscriptions.clear();
pending_unsubscriptions.clear();
acknowledged_subscriptions.clear();
pending_subscribe_packets.clear();
pending_requests.clear();
}
bool OrcaMqttConnection::parse_endpoint(const std::string& url, Endpoint& endpoint) {
std::string rest;
std::string default_port;
if (url.rfind("wss://", 0) == 0) { rest = url.substr(6); default_port = "443"; }
else if (url.rfind("ws://", 0) == 0) { rest = url.substr(5); default_port = "80"; }
else return false;
const auto slash = rest.find('/');
const std::string authority = rest.substr(0, slash);
endpoint.target = (slash == std::string::npos) ? "/" : rest.substr(slash);
// host[:port] — leave an unbracketed IPv6 literal alone
const auto colon = authority.rfind(':');
if (colon != std::string::npos && authority.find(']') == std::string::npos) {
endpoint.host = authority.substr(0, colon);
endpoint.port = authority.substr(colon + 1);
} else {
endpoint.host = authority;
endpoint.port = default_port;
}
return !endpoint.host.empty() && !endpoint.port.empty() && !endpoint.target.empty();
}
void OrcaMqttConnection::append_string(std::vector<uint8_t>& packet, const std::string& value) {
if (value.size() > 0xffff)
throw std::runtime_error("MQTT string is too long");
packet.push_back(static_cast<uint8_t>(value.size() >> 8));
packet.push_back(static_cast<uint8_t>(value.size() & 0xff));
packet.insert(packet.end(), value.begin(), value.end());
}
void OrcaMqttConnection::prepend_remaining_length(std::vector<uint8_t>& packet, size_t length) {
std::vector<uint8_t> encoded;
do {
uint8_t byte = static_cast<uint8_t>(length % 128);
length /= 128;
if (length != 0)
byte |= 0x80;
encoded.push_back(byte);
} while (length != 0);
packet.insert(packet.begin() + 1, encoded.begin(), encoded.end());
}
std::vector<uint8_t> OrcaMqttConnection::make_connect_packet(
const std::string& client_id, const std::string& username,
const std::string& password, int keepalive_seconds) {
std::vector<uint8_t> packet{0x10};
append_string(packet, "MQTT");
packet.push_back(4); // protocol level 3.1.1
uint8_t flags = 0x02; // clean session
if (!username.empty()) { flags |= 0x80; if (!password.empty()) flags |= 0x40; }
packet.push_back(flags);
packet.push_back(static_cast<uint8_t>(keepalive_seconds >> 8));
packet.push_back(static_cast<uint8_t>(keepalive_seconds & 0xff));
append_string(packet, client_id.empty() ? "OrcaSlicer" : client_id);
if (!username.empty()) {
append_string(packet, username);
if (!password.empty()) append_string(packet, password);
}
prepend_remaining_length(packet, packet.size() - 1);
return packet;
}
std::string OrcaMqttConnection::report_topic(const std::string& device_id) { return "device/" + device_id + "/report"; }
std::string OrcaMqttConnection::request_topic(const std::string& id) { return "device/" + id + "/request"; }
std::vector<uint8_t> OrcaMqttConnection::make_publish_packet(const std::string& topic, const std::string& payload) {
std::vector<uint8_t> packet{0x30}; // PUBLISH, QoS 0, no retain
append_string(packet, topic); // no packet id at QoS 0
packet.insert(packet.end(), payload.begin(), payload.end());
prepend_remaining_length(packet, packet.size() - 1);
return packet;
}
std::vector<uint8_t> OrcaMqttConnection::make_subscribe_packet(uint16_t id, const std::string& topic, uint8_t qos) {
std::vector<uint8_t> packet{0x82};
packet.push_back(id >> 8); packet.push_back(id & 0xff);
append_string(packet, topic);
packet.push_back(qos);
prepend_remaining_length(packet, packet.size() - 1);
return packet;
}
std::vector<uint8_t> OrcaMqttConnection::make_unsubscribe_packet(uint16_t id, const std::string& topic) {
std::vector<uint8_t> packet{0xA2};
packet.push_back(id >> 8); packet.push_back(id & 0xff);
append_string(packet, topic);
prepend_remaining_length(packet, packet.size() - 1);
return packet;
}
std::vector<uint8_t> OrcaMqttConnection::make_ping_packet() { return {0xc0, 0}; }
void OrcaMqttConnection::ws_write(Connection& conn, const std::vector<uint8_t>& packet) {
if (packet.empty()) {
return;
}
if (conn.wss) {
conn.wss->binary(true);
conn.wss->write(boost::asio::buffer(packet));
} else if (conn.ws) {
conn.ws->binary(true);
conn.ws->write(boost::asio::buffer(packet));
}
}
void OrcaMqttConnection::close_connection(Connection& conn) {
boost::system::error_code error;
if (conn.wss) {
auto& socket = boost::beast::get_lowest_layer(*conn.wss).socket();
if (socket.cancel(error))
return;
if (socket.shutdown(boost::asio::ip::tcp::socket::shutdown_both, error))
return;
if (socket.close(error))
return;
} else if (conn.ws) {
auto& socket = boost::beast::get_lowest_layer(*conn.ws).socket();
if (socket.cancel(error))
return;
if (socket.shutdown(boost::asio::ip::tcp::socket::shutdown_both, error))
return;
if (socket.close(error))
return;
}
}
void OrcaMqttConnection::enqueue_packet(const std::shared_ptr<Connection>& conn,
std::vector<uint8_t> packet) {
if (!conn || packet.empty())
return;
conn->outbound_packets.emplace_back(std::make_shared<std::vector<uint8_t>>(std::move(packet)));
start_async_write(conn);
}
void OrcaMqttConnection::start_async_write(const std::shared_ptr<Connection>& conn) {
if (!conn || conn->write_in_progress || conn->outbound_packets.empty() || stopping.load())
return;
conn->write_in_progress = true;
const auto packet = conn->outbound_packets.front();
auto on_write = [this, conn](const boost::system::error_code& error, std::size_t) {
conn->write_in_progress = false;
if (error) {
if (!stopping.load())
conn->terminal_error = error;
conn->io_context.stop();
return;
}
conn->outbound_packets.pop_front();
start_async_write(conn);
};
if (conn->wss) {
conn->wss->binary(true);
conn->wss->async_write(boost::asio::buffer(*packet), std::move(on_write));
} else if (conn->ws) {
conn->ws->binary(true);
conn->ws->async_write(boost::asio::buffer(*packet), std::move(on_write));
} else {
conn->write_in_progress = false;
conn->outbound_packets.pop_front();
}
}
void OrcaMqttConnection::start_async_read(const std::shared_ptr<Connection>& conn) {
if (!conn || stopping.load())
return;
auto on_read = [this, conn](const boost::system::error_code& error, std::size_t) {
if (error) {
if (!stopping.load())
conn->terminal_error = error;
conn->io_context.stop();
return;
}
const std::string message = boost::beast::buffers_to_string(conn->read_buffer.data());
conn->read_buffer.consume(conn->read_buffer.size());
feed_mqtt(*conn, message);
start_async_read(conn);
};
if (conn->wss)
conn->wss->async_read(conn->read_buffer, std::move(on_read));
else if (conn->ws)
conn->ws->async_read(conn->read_buffer, std::move(on_read));
}
bool OrcaMqttConnection::drain_mqtt_packets(std::string& stream, std::vector<std::string>& packets) {
std::size_t consumed = 0;
bool ok = true;
while (consumed < stream.size()) {
const std::size_t total = mqtt_packet_length(stream, consumed);
if (total == kMqttPacketIncomplete)
break; // wait for the rest of the stream
if (total == kMqttPacketMalformed) {
ok = false;
break;
}
if (consumed + total > stream.size())
break; // header complete, payload still arriving
packets.emplace_back(stream, consumed, total);
consumed += total;
}
if (consumed > 0)
stream.erase(0, consumed);
if (!ok)
stream.clear(); // drop the poisoned tail; the caller closes the connection
return ok;
}
void OrcaMqttConnection::feed_mqtt(Connection& conn, const std::string& bytes) {
conn.mqtt_rx.append(bytes);
std::vector<std::string> packets;
if (!drain_mqtt_packets(conn.mqtt_rx, packets)) {
// A malformed header can never resync; drop the session so the worker
// reconnects with a clean MQTT stream instead of buffering forever.
if (!stopping.load())
conn.terminal_error = boost::system::errc::make_error_code(boost::system::errc::protocol_error);
conn.io_context.stop();
return;
}
for (const std::string& packet : packets)
handle_packet(packet);
}
void OrcaMqttConnection::schedule_keepalive(const std::shared_ptr<Connection>& conn) {
const int keepalive = current_config.keepalive_seconds;
if (!conn || keepalive <= 0 || stopping.load())
return;
conn->keepalive_timer.expires_after(std::chrono::seconds(std::max(1, keepalive / 2)));
conn->keepalive_timer.async_wait([this, conn](const boost::system::error_code& error) {
if (error || stopping.load())
return;
enqueue_packet(conn, make_ping_packet());
schedule_keepalive(conn);
});
}
void OrcaMqttConnection::post_packet(const std::shared_ptr<Connection>& conn,
std::vector<uint8_t> packet) {
if (!conn || packet.empty())
return;
boost::asio::post(conn->io_context, [this, conn, packet = std::move(packet)]() mutable {
if (!stopping.load())
enqueue_packet(conn, std::move(packet));
});
}
std::size_t OrcaMqttConnection::ws_read(Connection& conn, boost::beast::flat_buffer& buffer,
boost::system::error_code& ec) {
if (conn.wss)
return conn.wss->read(buffer, ec);
if (conn.ws)
return conn.ws->read(buffer, ec);
ec = boost::asio::error::not_connected;
return 0;
}
void OrcaMqttConnection::ws_handshake(Connection& conn, const Config& config, const Endpoint& endpoint) {
const auto results = conn.resolver.resolve(endpoint.host, endpoint.port);
std::string token;
if (config.bearer_provider) {
token = config.bearer_provider();
}
auto decorator = [token](boost::beast::websocket::request_type& request) {
request.set(boost::beast::http::field::user_agent, "OrcaSlicer");
if (!token.empty())
request.set(boost::beast::http::field::authorization, "Bearer " + token);
request.set("Sec-WebSocket-Protocol", "mqtt");
};
boost::beast::http::response<boost::beast::http::string_body> response;
boost::system::error_code handshake_error;
if (config.use_tls) {
// stop() inspects the engaged optional under connection_mutex; publish it
// under the same lock, then release before the blocking connect.
{
std::lock_guard<std::mutex> lock(connection_mutex);
conn.wss.emplace(conn.io_context, conn.ssl_context);
}
auto& websocket = *conn.wss;
auto& stream = boost::beast::get_lowest_layer(websocket);
stream.expires_after(std::chrono::seconds(10));
stream.connect(results);
// Set SNI before the TLS handshake so the cloud edge selects the correct
// certificate.
auto& tls_stream = websocket.next_layer();
if (!SSL_set_tlsext_host_name(tls_stream.native_handle(), endpoint.host.c_str()))
throw std::runtime_error("failed to set Orca Cloud TLS server name");
if (!config.ca_file.empty())
conn.ssl_context.load_verify_file(config.ca_file);
else
conn.ssl_context.set_default_verify_paths();
Http::add_platform_root_certificates(conn.ssl_context.native_handle());
tls_stream.set_verify_mode(boost::asio::ssl::verify_peer);
tls_stream.set_verify_callback(boost::asio::ssl::host_name_verification(endpoint.host));
tls_stream.handshake(boost::asio::ssl::stream_base::client);
websocket.set_option(boost::beast::websocket::stream_base::decorator(decorator));
websocket.handshake(response, endpoint.host, endpoint.target, handshake_error);
} else {
{
std::lock_guard<std::mutex> lock(connection_mutex);
conn.ws.emplace(conn.io_context);
}
auto& websocket = *conn.ws;
auto& stream = boost::beast::get_lowest_layer(websocket);
stream.expires_after(std::chrono::seconds(10));
stream.connect(results);
websocket.set_option(boost::beast::websocket::stream_base::decorator(decorator));
websocket.handshake(response, endpoint.host, endpoint.target, handshake_error);
}
if (handshake_error) {
throw boost::system::system_error(handshake_error, "Orca WebSocket handshake");
}
if (response["Sec-WebSocket-Protocol"] != "mqtt") {
throw std::runtime_error("Orca WebSocket did not negotiate MQTT");
}
}
bool OrcaMqttConnection::send_request(const std::string& dev_id, const std::string& payload) {
if (dev_id.empty()) {
return false;
}
if (!connected.load()) {
return false;
}
std::shared_ptr<Connection> conn;
{
std::lock_guard<std::mutex> lock(connection_mutex);
conn = active_connection;
}
if (!conn) {
return false;
}
const std::string report = report_topic(dev_id);
{
std::lock_guard<std::mutex> lock(mutex);
if (subscriptions.count(report) != 0 && acknowledged_subscriptions.count(report) == 0) {
pending_requests.emplace_back(dev_id, payload);
return true;
}
}
post_packet(conn, make_publish_packet(request_topic(dev_id), payload));
return true;
}
void OrcaMqttConnection::connect_and_read() {
auto connection = std::make_shared<Connection>();
{
std::lock_guard<std::mutex> lock(connection_mutex);
active_connection = connection;
if (stopping.load()) {
active_connection.reset();
return;
}
}
auto clear_connection = [this, connection] {
close_connection(*connection);
std::lock_guard<std::mutex> lock(connection_mutex);
if (active_connection == connection)
active_connection.reset();
};
try {
Endpoint endpoint;
if (!parse_endpoint(current_config.url, endpoint)) {
throw std::runtime_error("invalid Orca Cloud WebSocket endpoint");
}
ws_handshake(*connection, current_config, endpoint);
// Auth precedence: a bearer_provider authenticates the WebSocket upgrade, so the
// CONNECT username/password fields are omitted entirely (the cloud form).
const bool use_bearer = static_cast<bool>(current_config.bearer_provider);
ws_write(*connection, make_connect_packet(current_config.client_id,
use_bearer ? std::string() : current_config.username,
use_bearer ? std::string() : current_config.password,
current_config.keepalive_seconds));
boost::beast::flat_buffer buffer;
expires_after(*connection, std::chrono::seconds(10));
boost::system::error_code connack_error;
ws_read(*connection, buffer, connack_error);
if (connack_error)
throw boost::system::system_error(connack_error, "read Orca MQTT CONNACK");
// Beast leaves this expiry armed after the synchronous CONNACK read above;
// disable it before starting the long-lived async WebSocket session.
expires_never(*connection);
const std::string connack = boost::beast::buffers_to_string(buffer.data());
// rc: 0 accepted, 1..5 refusal, -1 not a CONNACK. A WebSocket message may
// carry more than the CONNACK; only its first four bytes are the CONNACK.
const int rc = (connack.size() >= 4 && static_cast<uint8_t>(connack[0]) == 0x20)
? static_cast<int>(static_cast<uint8_t>(connack[3]))
: -1;
m_last_connack_rc.store(rc);
if (rc != 0) {
if (rc == 4 || rc == 5) {
// Bad credentials / not authorized — retrying cannot help. Make run()'s
// loop exit and unblock any waiting start().
stopping.store(true);
{
std::lock_guard<std::mutex> lock(mutex);
initial_completed = true;
initial_result = false;
}
initial_cv.notify_all();
}
throw std::runtime_error("Orca MQTT CONNECT refused rc=" + std::to_string(rc));
}
// Any bytes the broker sent after the 4-byte CONNACK in the same message
// start the MQTT stream; keep them for the reassembler.
if (connack.size() > 4)
connection->mqtt_rx.assign(connack, 4, std::string::npos);
// The subscription acknowledgement belongs to this MQTT session. Clear
// the previous session's state before notifying the owner, because the
// reconnect callback immediately queues the printer's initial requests.
{
std::lock_guard<std::mutex> lock(mutex);
acknowledged_subscriptions.clear();
pending_subscribe_packets.clear();
}
connection->async_session_started.store(true);
notify_state(true);
reconnect_delay_seconds.store(1); // a fresh CONNACK resets the backoff
send_current_subscriptions(connection);
feed_mqtt(*connection, {}); // drain anything that rode in with the CONNACK
start_async_read(connection);
schedule_keepalive(connection);
const std::size_t handlers_run = connection->io_context.run();
if (handlers_run == 0 && !stopping.load())
throw std::runtime_error("Orca MQTT event loop stopped unexpectedly");
if (connection->terminal_error && !stopping.load())
throw boost::system::system_error(connection->terminal_error, "read Orca MQTT message");
clear_connection();
if (!stopping.load())
notify_state(false);
} catch (...) {
clear_connection();
throw;
}
}
void OrcaMqttConnection::send_current_subscriptions(const std::shared_ptr<Connection>& conn) {
std::vector<std::string> topics;
{
std::lock_guard<std::mutex> lock(mutex);
topics.assign(subscriptions.begin(), subscriptions.end());
acknowledged_subscriptions.clear();
pending_subscribe_packets.clear();
for (const std::string& topic : topics)
pending_subscriptions.erase(topic);
}
for (const std::string& topic : topics) {
const uint16_t packet_id = next_packet_id++;
{
std::lock_guard<std::mutex> lock(mutex);
pending_subscribe_packets[packet_id] = topic;
}
enqueue_packet(conn, make_subscribe_packet(packet_id, topic, 1));
}
}
void OrcaMqttConnection::send_pending_subscriptions(const std::shared_ptr<Connection>& conn) {
std::vector<std::string> subscribe_topics;
std::vector<std::string> unsubscribe_topics;
{
std::lock_guard<std::mutex> lock(mutex);
subscribe_topics.assign(pending_subscriptions.begin(), pending_subscriptions.end());
unsubscribe_topics.assign(pending_unsubscriptions.begin(), pending_unsubscriptions.end());
pending_subscriptions.clear();
pending_unsubscriptions.clear();
}
for (const std::string& topic : subscribe_topics) {
const uint16_t packet_id = next_packet_id++;
{
std::lock_guard<std::mutex> lock(mutex);
pending_subscribe_packets[packet_id] = topic;
}
enqueue_packet(conn, make_subscribe_packet(packet_id, topic, 1));
}
for (const std::string& topic : unsubscribe_topics) {
const uint16_t packet_id = next_packet_id++;
enqueue_packet(conn, make_unsubscribe_packet(packet_id, topic));
}
}
void OrcaMqttConnection::handle_packet(const std::string& packet) {
if (packet.size() < 2) {
return;
}
const uint8_t header = static_cast<uint8_t>(packet[0]);
const uint8_t packet_type = header >> 4;
if (packet_type != 3) { // Only QoS 0 PUBLISH carries printer status.
if (packet_type == 9 && packet.size() >= 5) {
const uint16_t packet_id = (static_cast<unsigned int>(static_cast<uint8_t>(packet[2])) << 8) |
static_cast<unsigned int>(static_cast<uint8_t>(packet[3]));
std::ostringstream result_codes;
for (size_t index = 4; index < packet.size(); ++index) {
if (index != 4)
result_codes << ',';
result_codes << "0x" << std::hex << static_cast<unsigned int>(static_cast<uint8_t>(packet[index]));
}
// Each production SUBSCRIBE packet currently contains one topic.
// MQTT grants QoS 0 or 1 for a requested QoS 1 subscription; 0x80
// means the subscription was rejected.
const uint8_t result = static_cast<uint8_t>(packet[4]);
std::string topic;
std::deque<std::pair<std::string, std::string>> requests;
{
std::lock_guard<std::mutex> lock(mutex);
auto pending = pending_subscribe_packets.find(packet_id);
if (pending != pending_subscribe_packets.end()) {
topic = pending->second;
pending_subscribe_packets.erase(pending);
for (auto it = pending_requests.begin(); it != pending_requests.end();) {
if (report_topic(it->first) == topic) {
requests.push_back(std::move(*it));
it = pending_requests.erase(it);
} else {
++it;
}
}
if (result == 0 || result == 1) {
acknowledged_subscriptions.insert(topic);
}
}
}
if (topic.empty()) {
} else if (result == 0 || result == 1) {
for (const auto& request : requests) {
if (!send_request(request.first, request.second)) {
}
}
} else {
}
}
return;
}
size_t index = 1;
size_t multiplier = 1;
size_t remaining = 0;
uint8_t encoded = 0;
do {
if (index >= packet.size() || multiplier > 128 * 128 * 128) {
return;
}
encoded = static_cast<uint8_t>(packet[index++]);
remaining += (encoded & 0x7f) * multiplier;
multiplier *= 128;
} while ((encoded & 0x80) != 0);
const size_t remaining_end = index + remaining;
if (remaining_end > packet.size() || remaining < 2 || index + 2 > remaining_end) {
return;
}
const uint16_t topic_length = (static_cast<uint8_t>(packet[index]) << 8) |
static_cast<uint8_t>(packet[index + 1]);
index += 2;
if (topic_length > packet.size() - index) {
return;
}
const std::string topic(packet.data() + index, topic_length);
index += topic_length;
if (((header >> 1) & 0x03) != 0) {
if (index + 2 > remaining_end) {
return;
}
index += 2; // QoS 1/2 packet identifier; the service currently sends QoS 0.
}
const size_t payload_size = remaining_end - index;
// topic is "device/<id>/report" (or "/request"); hand the id up, drop anything else.
std::string dev_id;
if (topic.rfind("device/", 0) == 0) {
const size_t id_start = 7;
const size_t id_end = topic.rfind('/');
if (id_end != std::string::npos && id_end > id_start)
dev_id = topic.substr(id_start, id_end - id_start);
}
if (dev_id.empty()) {
} else if (on_message) {
on_message(dev_id, packet.substr(index, remaining_end - index));
} else {
}
}
void OrcaMqttConnection::notify_state(bool is_now_connected) {
StateHandler callback;
bool initial = false;
{
std::lock_guard<std::mutex> lock(mutex);
connected = is_now_connected;
initial = !initial_completed;
if (initial) {
initial_result = is_now_connected;
initial_completed = true;
}
callback = on_state;
}
if (initial)
initial_cv.notify_all();
else if (callback)
callback(is_now_connected, false);
}
void OrcaMqttConnection::run() {
while (!stopping.load()) {
const int retry_seconds = reconnect_delay_seconds.load();
const uint64_t attempt = ++m_attempt_number;
try {
connect_and_read();
} catch (const std::exception& error) {
if (!stopping.load())
notify_state(false);
}
if (stopping.load())
break;
// Grow the backoff only across attempts that never reached CONNACK; a
// successful connection resets reconnect_delay_seconds to 1 (connect_and_read).
reconnect_delay_seconds.store(std::min(retry_seconds * 2, 30));
std::unique_lock<std::mutex> lock(mutex);
state_cv.wait_for(lock, std::chrono::seconds(retry_seconds), [this] { return stopping.load(); });
}
}
} // namespace Slic3r
+172
View File
@@ -0,0 +1,172 @@
#ifndef slic3r_OrcaMqttConnection_hpp_
#define slic3r_OrcaMqttConnection_hpp_
#include <boost/asio/ssl.hpp>
#include <boost/asio/ssl/stream.hpp>
#include <boost/beast/core.hpp>
#include <boost/beast/core/tcp_stream.hpp>
#include <boost/beast/core/flat_buffer.hpp>
#include <boost/beast/ssl.hpp>
#include <boost/beast/websocket.hpp>
#include <atomic>
#include <boost/beast/websocket/stream.hpp>
#include <condition_variable>
#include <deque>
#include <functional>
#include <map>
#include <memory>
#include <mutex>
#include <set>
#include <string>
#include <thread>
#include <utility>
#include <vector>
#include <cstddef>
#include <cstdint>
namespace Slic3r {
// Minimal MQTT 3.1.1 codec + WebSocket transport (ws:// and wss://), shared by the
// LAN (OrcaSonar) and cloud (fleet) printer connections. Both PUBLISH
// commands to device/<id>/request and SUBSCRIBE device/<id>/report; Config is the
// only per-transport difference.
class OrcaMqttConnection
{
public:
using TokenProvider = std::function<std::string()>;
using MessageHandler = std::function<void(const std::string&, const std::string&)>;
using StateHandler = std::function<void(bool connected, bool initial)>;
struct Endpoint { std::string host; std::string port; std::string target; };
struct Config {
std::string url;
bool use_tls = false;
std::string ca_file;
TokenProvider bearer_provider; // set => bearer on WS upgrade, CONNECT creds omitted
std::string username;
std::string password;
std::string client_id = "OrcaSlicer";
int keepalive_seconds = 60;
};
static bool parse_endpoint(const std::string& url, Endpoint& endpoint);
// Build an MQTT 3.1.1 CONNECT packet. Clean-session is always set; the
// username/password connect flags and payload fields are added only when
// username is non-empty (the cloud form authenticates via a bearer on the
// WebSocket upgrade and omits CONNECT credentials). Public for unit tests.
static std::vector<uint8_t> make_connect_packet(const std::string& client_id,
const std::string& username,
const std::string& password,
int keepalive_seconds);
// Topic-string helpers for the per-device request/report channels and the
// MQTT 3.1.1 PUBLISH / SUBSCRIBE / UNSUBSCRIBE packet builders. All public
// for unit tests. make_publish_packet emits QoS 0 (no packet identifier).
static std::string request_topic(const std::string& dev_id); // "device/<id>/request"
static std::string report_topic(const std::string& dev_id); // "device/<id>/report"
static std::vector<uint8_t> make_publish_packet(const std::string& topic, const std::string& payload);
static std::vector<uint8_t> make_subscribe_packet(uint16_t packet_id, const std::string& topic, uint8_t qos);
static std::vector<uint8_t> make_unsubscribe_packet(uint16_t packet_id, const std::string& topic);
// Split a raw MQTT byte stream into complete packets: each complete packet is
// appended to `packets` and erased from `stream`; a partial trailing packet is
// left in `stream` for the next call. Returns false when the stream begins with
// a malformed header, which cannot resync — the caller must drop the connection.
// Public for unit tests. MQTT-over-WebSocket allows a packet to span frames and
// several packets per frame, so the receiver must reassemble the stream rather
// than assume one packet per WebSocket message.
static bool drain_mqtt_packets(std::string& stream, std::vector<std::string>& packets);
~OrcaMqttConnection();
bool start(const Config& config, MessageHandler on_message, StateHandler on_state);
void stop();
// True while the worker thread is alive (connected OR retrying). Lets callers
// avoid restarting a healthy connection.
bool is_running() const;
// True once CONNACK has been received and the socket has not since dropped.
bool is_connected() const { return connected.load(); }
bool subscribe(const std::string& dev_id);
bool unsubscribe(const std::string& dev_id);
// Last MQTT CONNACK return code: 0 ok, 1..5 refusal, -1 none seen this attempt.
int last_connack_rc() const { return m_last_connack_rc.load(); }
void clear_subscriptions();
bool send_request(const std::string& dev_id, const std::string& payload);
private:
// The endpoint may be either a TLS (wss://) or a plaintext (ws://) WebSocket;
// Connection holds whichever one is engaged and the ws_* helpers below
// dispatch on it.
using TlsWebSocket = boost::beast::websocket::stream<
boost::asio::ssl::stream<boost::beast::tcp_stream>>;
using PlainWebSocket = boost::beast::websocket::stream<boost::beast::tcp_stream>;
struct Connection;
static void append_string(std::vector<uint8_t>& packet, const std::string& value);
static void prepend_remaining_length(std::vector<uint8_t>& packet, size_t length);
static std::vector<uint8_t> make_ping_packet();
// Transport dispatch: each forwards to conn.wss (TLS) or conn.ws (plaintext).
// These synchronous operations are called only by the MQTT worker during
// connection setup. Once the MQTT session is established, all socket I/O is
// asynchronous and owned by that worker's io_context.
void ws_write(Connection& conn, const std::vector<uint8_t>& packet);
std::size_t ws_read(Connection& conn, boost::beast::flat_buffer& buffer, boost::system::error_code& ec);
void ws_handshake(Connection& conn, const Config& config, const Endpoint& endpoint);
void close_connection(Connection& conn);
void enqueue_packet(const std::shared_ptr<Connection>& conn, std::vector<uint8_t> packet);
void start_async_write(const std::shared_ptr<Connection>& conn);
void start_async_read(const std::shared_ptr<Connection>& conn);
void schedule_keepalive(const std::shared_ptr<Connection>& conn);
void post_packet(const std::shared_ptr<Connection>& conn, std::vector<uint8_t> packet);
// Ask the MQTT worker to emit subscription changes on its own io_context.
void flush_subscription_change();
void connect_and_read();
void send_current_subscriptions(const std::shared_ptr<Connection>& conn);
void send_pending_subscriptions(const std::shared_ptr<Connection>& conn);
void handle_packet(const std::string& packet);
// Append freshly received WebSocket bytes to `conn`'s MQTT stream and dispatch
// every complete packet. An MQTT packet may span several WebSocket messages and
// several packets may arrive in one, so the stream is reassembled here.
void feed_mqtt(Connection& conn, const std::string& bytes);
void notify_state(bool is_now_connected);
void run();
std::atomic_bool stopping{true};
std::atomic_int reconnect_delay_seconds{1};
// Serialises the whole of start() and stop() against each other, so the UI
// thread's stop() (disconnect / dtor) cannot race the connect thread's start()
// into a concurrent worker.join(). Recursive because start() calls stop().
std::recursive_mutex lifecycle_mutex;
std::thread worker;
std::mutex mutex;
std::mutex connection_mutex;
std::shared_ptr<Connection> active_connection;
std::condition_variable initial_cv;
std::condition_variable state_cv;
Config current_config;
MessageHandler on_message;
StateHandler on_state;
// Full report-topic strings ("device/<id>/report"), not bare device ids.
std::set<std::string> subscriptions;
std::set<std::string> pending_subscriptions;
std::set<std::string> pending_unsubscriptions;
// Requests for a subscribed device wait until the corresponding SUBACK is
// received. Otherwise an immediate pushall response can be published by
// the broker before this client is actually subscribed to the report topic.
std::set<std::string> acknowledged_subscriptions;
std::map<uint16_t, std::string> pending_subscribe_packets;
std::deque<std::pair<std::string, std::string>> pending_requests;
std::atomic<uint16_t> next_packet_id{1};
std::atomic<int> m_last_connack_rc{-1};
uint64_t m_attempt_number{0}; // worker-thread diagnostic sequence
bool initial_result{false};
bool initial_completed{false};
std::atomic_bool connected{false};
};
} // namespace Slic3r
#endif // slic3r_OrcaMqttConnection_hpp_
File diff suppressed because it is too large Load Diff
+201 -4
View File
@@ -2,22 +2,34 @@
#define __ORCA_PRINTER_AGENT_HPP__ #define __ORCA_PRINTER_AGENT_HPP__
#include "IPrinterAgent.hpp" #include "IPrinterAgent.hpp"
#include "ICloudServiceAgent.hpp"
#include "OrcaCloudServiceAgent.hpp"
#include "OrcaMqttConnection.hpp"
#include <atomic>
#include <cstdint>
#include <functional>
#include "bambu_networking.hpp" #include "bambu_networking.hpp"
#include <string> #include <string>
#include <mutex> #include <mutex>
#include <memory> #include <memory>
#include <thread>
#include <unordered_map>
#include <vector>
namespace Slic3r { class ICloudServiceAgent; } namespace Slic3r { class ICloudServiceAgent; }
namespace Slic3r { 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. * LAN and cloud commands use the same OrcaSonar protocol payloads; only the
* Actual printer connectivity requires the BBL SDK or future Orca implementation. * MQTT connection selected by route_send() differs.
*/ */
class OrcaPrinterAgent : public IPrinterAgent { class OrcaPrinterAgent : public IPrinterAgent
{
public: public:
explicit OrcaPrinterAgent(std::string log_dir); explicit OrcaPrinterAgent(std::string log_dir);
~OrcaPrinterAgent() override; ~OrcaPrinterAgent() override;
@@ -27,9 +39,18 @@ public:
// ======================================================================== // ========================================================================
void set_cloud_agent(std::shared_ptr<ICloudServiceAgent> cloud) override; 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 // Communication
int send_message(std::string dev_id, std::string json_str, int qos, int flag) override; 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 connect_printer(const PrinterConnectionParams& params) override;
int disconnect_printer() override; int disconnect_printer() override;
int send_message_to_printer(std::string dev_id, std::string json_str, int qos, int flag) 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 // Machine Selection
std::string get_user_selected_machine() override; std::string get_user_selected_machine() override;
int set_user_selected_machine(std::string dev_id) override; int set_user_selected_machine(std::string dev_id) override;
int unbind(std::string dev_id) override;
/** /**
* Get agent information. * Get agent information.
@@ -55,6 +77,8 @@ public:
// Print Job Operations // Print Job Operations
int start_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn) override; 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_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_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; int start_local_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn) override;
@@ -70,10 +94,183 @@ public:
int set_on_local_message_fn(OnMessageFn fn) override; int set_on_local_message_fn(OnMessageFn fn) override;
int set_queue_on_main_fn(QueueOnMainFn fn) override; int set_queue_on_main_fn(QueueOnMainFn fn) override;
int command_ams_refresh_rfid(std::string dev_id, int ams_id, int tray_id, int sequence_id, bool lan_mode) override;
int command_ams_calibrate(std::string dev_id, int ams_id, int sequence_id, bool lan_mode) override;
int command_ams_select_tray(std::string dev_id, std::string tray_id, int sequence_id, bool lan_mode) override;
int command_start_camera(std::string dev_id) override;
int command_xyz_abs(std::string dev_id, int sequence_id, bool lan_mode) override;
int command_auto_leveling(std::string dev_id, int sequence_id, bool lan_mode) override;
int command_go_home(std::string dev_id, bool is_printing, bool supports_mqtt_homing, int sequence_id, bool lan_mode) override;
int command_set_bed(std::string dev_id, int temp, bool supports_mqtt_bed_ctrl, int sequence_id, bool lan_mode) override;
int command_set_nozzle(std::string dev_id, int temp, int sequence_id, bool lan_mode) override;
int command_axis_control(std::string dev_id,
std::string axis,
double unit,
double input_val,
int speed,
bool is_core_xy,
bool supports_mqtt_axis_control,
int sequence_id,
bool lan_mode) override;
// Test-only: drive emit_connect_sequence directly (no socket).
void run_connect_sequence_for_test(const std::string& dev_id)
{
emit_connect_sequence(dev_id, [](const std::string&) {}, [](const std::string&) {});
}
// Test-only: advance the LAN connection epoch without a connect/disconnect cycle.
void bump_lan_generation_for_test() { ++m_lan_generation; }
// Test-only: the same for the (independent) cloud selection epoch.
void bump_cloud_generation_for_test() { ++m_cloud_generation; }
FilamentSyncMode get_filament_sync_mode() const override { return FilamentSyncMode::subscription; }
protected:
// Forward one inbound printer message to on_message_fn or on_local_message_fn (marshalled onto the UI
// thread via queue_on_main_fn when set). Body of every connection's MessageHandler.
void deliver_to_sink(const std::string& dev_id, const std::string& payload, bool local);
// Extract OrcaSonar's print.ipcam.stream_mode from LAN reports before they
// are forwarded to the GUI. The getters below then read this agent-owned state.
void parse_ipcam_info(const std::string& dev_id, const std::string& payload);
// Orca-dialect -> Bambu-dialect compatibility shim for inbound reports: the single
// place Orca Protocol JSON is rewritten into the shapes MachineObject::parse_json
// already handles, so parse_json needs no Orca-specific changes. Self-contained
// (its cache is a function-local static) and deletable together with its call site
// once parse_json reads the Orca dialect natively. See the definition for the
// per-rule detail. Returns the payload unchanged when no rule applies.
std::string merge_capabilities(const std::string& dev_id, const std::string& payload);
void forget_device_capabilities(const std::string& dev_id);
int prepare_outgoing_request(const std::string& dev_id, const std::string& payload,
std::string& command, std::string& prepared) const;
// Report the asynchronous LAN connection state using the same callback contract as
// the other printer agents. The transport result cannot be returned by
// connect_printer(), which only starts the worker.
void dispatch_local_connect(int state, const std::string& dev_id, const std::string& message);
// The LAN inbound-message handler for one connection generation: forwards to
// deliver_to_sink only while `generation` is still the live epoch.
std::function<void(const std::string&, const std::string&)> make_lan_message_handler(uint64_t generation);
// Pure LAN-address parsing + client-id. protected static so the test Probe reaches them.
static bool parse_lan_endpoint(const std::string& dev_ip, std::string& host, std::string& port);
static std::string make_lan_client_id(const std::string& dev_id);
// Pure serializer for PrintParams::ams_mapping2 -> print.gcode_file.filament_mapping.
// Entries are re-keyed by their array position; {255,255} is dropped. Empty when the
// input is empty, malformed, or has no usable entries. protected static for the test Probe.
static nlohmann::json build_filament_mapping(const std::string& ams_mapping2);
// Pure builder for the print.gcode_file command payload. A non-empty mapping is
// included as filament_mapping; an empty one is omitted so the payload is
// byte-identical to an unmapped print. protected static for the test Probe.
static nlohmann::json build_gcode_file_payload(const std::string& sequence_id,
const std::string& target,
const nlohmann::json& filament_mapping);
// Pure JSON -> entries normalization for the files.list MQTT reply
// ({"files":{...,"entries":[{name,path,is_dir,size,modified}]}}). Directory
// entries and entries without a usable name are skipped; a missing path falls
// back to the name and size/modified default to 0. protected static for the Probe.
static std::vector<PrinterFileEntry> parse_files_list_reply(const std::string& payload);
// Pure JSON -> metadata normalization for the files.metadata MQTT reply. Missing
// or malformed fields default to 0. protected static for the Probe.
static PrinterFileMetadata parse_files_metadata_reply(const std::string& payload);
// Dispatch one files.* command (list/metadata/delete) over the LAN MQTT
// connection and route its reply, matched by sequence_id, into `complete`.
// `complete` runs once on the UI thread (or the calling thread when no queue is
// set) with the raw reply payload on success, or an empty string on
// failure/timeout. Returns whether the request was dispatched.
int send_files_request(const std::string& dev_id, const std::string& command, nlohmann::json fields,
std::function<void(int result, const std::string& payload)> complete);
// Consume an inbound report that answers a pending files.* request. Returns true
// when it matched, so the caller does not forward it to the machine-state sink.
bool try_consume_files_reply(const std::string& dev_id, const std::string& payload);
// Test hook: the ws:// URL connect_printer built for the current LAN session ("" if none).
std::string lan_connection_target() const;
// Shared post-connect sequence: SUBSCRIBE, then pushing.start, pushall,
// info.get_version, info.get_capabilities. Runs identically on LAN and cloud.
void on_connected(const std::string& dev_id, OrcaMqttConnection* conn, uint64_t generation);
// The post-connect command sequence, factored behind a seam so a test can
// observe the SUBSCRIBE + 4 request payloads without a live OrcaMqttConnection.
virtual void emit_connect_sequence(const std::string& dev_id,
std::function<void(const std::string&)> subscribe,
std::function<void(const std::string&)> request);
static std::string seq(int n); // decimal string in the OrcaSlicer 20000..29999 band
static std::string build_pushing_start(const std::string& sequence_id);
static std::string build_pushing_stop(const std::string& sequence_id);
static std::string build_pushall(const std::string& sequence_id);
static std::string build_get_version(const std::string& sequence_id);
static std::string build_get_capabilities(const std::string& sequence_id);
private: private:
class OrcaSonarDiscovery;
std::string log_dir; std::string log_dir;
std::string selected_machine; std::string selected_machine;
enum CurrentConn { NONE, CLOUD, LAN };
static const char* connection_type_name(CurrentConn connection);
// The transport for the printer currently selected by the UI. LAN and
// cloud sessions have separate connection objects, so this is selection
// state rather than an inference from whichever socket happens to exist.
CurrentConn m_current_connection = NONE;
std::shared_ptr<ICloudServiceAgent> m_cloud_agent; std::shared_ptr<ICloudServiceAgent> m_cloud_agent;
std::shared_ptr<OrcaMqttConnection> lan_mqtt_connection;
// Two independent epochs: a cloud (de)selection must not fence the live LAN
// feed, and vice versa. Each transport's connect thread and inbound handler
// compare against their own counter only.
std::atomic<uint64_t> m_lan_generation{0};
std::atomic<uint64_t> m_cloud_generation{0};
// The short-lived threads that run the blocking initial connect for the current
// LAN / cloud session. Joined members (never detached) so they cannot outlive
// *this or the connection they hold a raw pointer to.
std::thread m_lan_connect_thread;
std::thread m_cloud_connect_thread;
std::unique_ptr<OrcaSonarDiscovery> m_discovery;
std::string m_lan_dev_id; // guarded by state_mutex
std::string m_lan_url; // guarded by state_mutex — the Config.url of the live LAN session
bool m_lan_use_ssl = false; // guarded by state_mutex
std::string m_lan_ca_file; // guarded by state_mutex
CameraStreamMode m_camera_stream_mode = CameraStreamMode::none; // guarded by state_mutex
std::string m_camera_url; // guarded by state_mutex
// Pending files.* MQTT replies, keyed by sequence_id. The registry is
// heap-owned so a timeout watchdog can outlive the agent without touching
// `this`; each entry carries its own one-shot guard.
struct FilesReply {
std::string device_id;
std::function<void(int, const std::string&)> complete;
std::shared_ptr<std::atomic<bool>> fired = std::make_shared<std::atomic<bool>>(false);
};
struct FilesReplyRegistry {
std::mutex mutex;
std::unordered_map<std::string, FilesReply> entries;
};
std::shared_ptr<FilesReplyRegistry> m_files_replies = std::make_shared<FilesReplyRegistry>();
OrcaCloudServiceAgent* get_orca_cloud_agent();
OrcaMqttConnection* get_appropriate_mqtt_connection(bool is_lan = true);
static bool parse_nonnegative_command_id(const std::string& value, int& result);
// Route one command payload to device/<dev_id>/request on the LAN or the shared
// cloud connection. The uniform send path for both send_message* overrides.
int route_send(bool is_lan, const std::string& dev_id, const std::string& json_str);
// Callbacks // Callbacks
OnMsgArrivedFn on_ssdp_msg_fn; OnMsgArrivedFn on_ssdp_msg_fn;
+37
View File
@@ -49,6 +49,20 @@ static std::set<int> tools_for_role(const std::string& gcode, const std::string&
return tools; return tools;
} }
// Every id named by a Tn command anywhere in `gcode`. A non-BBL single-extruder multi-filament
// setup emits T<filament_id>, and that id is what the Klipper toolchange macro consumes.
static std::set<int> tool_ids(const std::string& gcode)
{
std::set<int> tools;
GCodeReader reader;
reader.parse_buffer(gcode, [&tools](GCodeReader&, const GCodeReader::GCodeLine& line) {
const std::string cmd(line.cmd());
if (cmd.size() >= 2 && cmd[0] == 'T' && std::isdigit((unsigned char)cmd[1]))
tools.insert(std::stoi(cmd.substr(1)));
});
return tools;
}
// X where the nozzle sits while each tagged _WAIT_FOR_TEMP_ON_WIPE_TOWER M109 blocks: // X where the nozzle sits while each tagged _WAIT_FOR_TEMP_ON_WIPE_TOWER M109 blocks:
// the nearest preceding G1 carrying an X (the park travel emitted just before the wait). // the nearest preceding G1 carrying an X (the park travel emitted just before the wait).
static std::vector<double> wait_park_xs(const std::string& gcode) static std::vector<double> wait_park_xs(const std::string& gcode)
@@ -296,6 +310,29 @@ TEST_CASE("Each feature prints with its assigned filament (three filaments)", "[
CHECK(tools_for_role(gcode, "infill") == std::set<int>{ 1 }); // filament 2 CHECK(tools_for_role(gcode, "infill") == std::set<int>{ 1 }); // filament 2
} }
// The per-print wire mapping keys each entry by the filament's index in the config filament
// arrays (the ams_mapping2 position). Pin that the id the toolchange emits is that index even
// when the used filaments skip a middle one: filament 2 (index 1) is configured but unused, so
// a renumbering to the used set would emit {0, 1} instead of {0, 2} and aim the map at the
// wrong lane.
TEST_CASE("Toolchange ids keep the config index of the used filament when a middle filament is unused", "[MultiFilament]")
{
const std::string gcode = slice({ cube(20) },
multifilament_config(3, {
{ "sparse_infill_filament_id", 1 },
{ "internal_solid_filament_id", 1 },
{ "top_surface_filament_id", 1 },
{ "bottom_surface_filament_id", 1 },
{ "outer_wall_filament_id", 3 },
{ "inner_wall_filament_id", 3 },
{ "skirt_loops", 0 },
{ "brim_type", "no_brim" },
}));
CHECK(tools_for_role(gcode, "perimeter") == std::set<int>{ 2 }); // filament 3
CHECK(tools_for_role(gcode, "infill") == std::set<int>{ 0 }); // filament 1
CHECK(tool_ids(gcode) == std::set<int>{ 0, 2 });
}
// The override must survive tool ordering: object 1's walls print on their filament's // The override must survive tool ordering: object 1's walls print on their filament's
// tool, object 0 stays on the first. If dropped, every wall prints on tool 0. // tool, object 0 stays on the first. If dropped, every wall prints on tool 0.
TEST_CASE("Per-object wall filament override is honored", "[MultiFilament]") TEST_CASE("Per-object wall filament override is honored", "[MultiFilament]")
+1
View File
@@ -25,6 +25,7 @@ add_executable(${_TEST_NAME}_tests
test_toolordering_nozzle_group.cpp test_toolordering_nozzle_group.cpp
test_parallel_resolve.cpp test_parallel_resolve.cpp
test_preset_bundle_loading.cpp test_preset_bundle_loading.cpp
test_preset_bundle_ams_sync.cpp
test_preset_setting_id.cpp test_preset_setting_id.cpp
test_preset_diff.cpp test_preset_diff.cpp
test_vendor_cache.cpp test_vendor_cache.cpp
@@ -0,0 +1,197 @@
#include <catch2/catch_all.hpp>
#include <libslic3r/PrintConfig.hpp>
#include <map>
#include <string>
#include <utility>
#include <vector>
#include "catch2/catch_test_macros.hpp"
#include "libslic3r/Preset.hpp"
#include "libslic3r/PresetBundle.hpp"
using namespace Slic3r;
namespace {
// In-memory system filament preset. Mirrors the helper in test_preset_bundle_loading.cpp.
Preset &add_system_filament(PresetBundle &bundle, const std::string &name, const std::string &filament_id, const std::string &filament_type)
{
DynamicPrintConfig config(bundle.filaments.default_preset().config);
config.option<ConfigOptionString>(BBL_JSON_KEY_INHERITS, true)->value = "";
config.option<ConfigOptionStrings>("filament_type", true)->values = {filament_type};
Preset &preset = bundle.filaments.load_preset(std::string(), name, config, /*select=*/false);
preset.is_system = true;
preset.filament_id = filament_id;
return preset;
}
// One AMS tray entry as build_filament_ams_list produces it.
DynamicPrintConfig make_tray(const std::string &filament_id, const std::string &filament_type, const std::string &color, const std::string &ams_id,
const std::string &slot_id)
{
DynamicPrintConfig tray;
tray.set_key_value("filament_id", new ConfigOptionStrings{filament_id});
tray.set_key_value("filament_type", new ConfigOptionStrings{filament_type});
tray.set_key_value("filament_colour", new ConfigOptionStrings{color});
tray.set_key_value("filament_colour_type", new ConfigOptionStrings{"1"});
tray.set_key_value("filament_multi_colour", new ConfigOptionStrings{});
tray.set_key_value("ams_id", new ConfigOptionStrings{ams_id});
tray.set_key_value("slot_id", new ConfigOptionStrings{slot_id});
return tray;
}
} // namespace
// A tray the printer UI wrote carries a material type but no OrcaSlicer preset id. It must still
// sync, resolved to Generic <type> with the tray's own color.
TEST_CASE("AMS sync resolves a printer-set tray to Generic by material type", "[Preset][AMS]")
{
PresetBundle bundle;
add_system_filament(bundle, "Generic PETG @Q2", "OFYPdQJh", "PETG");
add_system_filament(bundle, "Generic PLA @Q2", "GFL99", "PLA");
bundle.filament_ams_list[0] = make_tray("OFYPdQJh", "PETG", "#FE717A", "0", "0"); // preset-backed
bundle.filament_ams_list[1] = make_tray("", "PETG", "#898F9B", "0", "1"); // printer-set, no id
bundle.filament_ams_list[2] = make_tray("", "PLA", "#FAFAFA", "0", "2"); // printer-set, no id
bundle.filament_ams_list[3] = make_tray("", "", "#000000", "0", "3"); // empty slot
std::vector<std::pair<DynamicPrintConfig *, std::string>> unknowns;
std::map<int, AMSMapInfo> maps;
MergeFilamentInfo merge;
const unsigned int count = bundle.sync_ams_list(unknowns, /*use_map=*/false, maps, /*enable_append=*/false, merge);
CHECK(count == 3);
CHECK(unknowns.empty());
REQUIRE(bundle.filament_presets.size() == 3);
CHECK(bundle.filament_presets[0] == "Generic PETG @Q2");
CHECK(bundle.filament_presets[1] == "Generic PETG @Q2");
CHECK(bundle.filament_presets[2] == "Generic PLA @Q2");
const ConfigOptionStrings *colors = bundle.project_config.option<ConfigOptionStrings>("filament_colour");
REQUIRE(colors != nullptr);
REQUIRE(colors->values.size() == 3);
CHECK(colors->values[0] == "#FE717A");
CHECK(colors->values[1] == "#898F9B");
CHECK(colors->values[2] == "#FAFAFA");
}
// Without a preset id and without a material type there is nothing to resolve; the tray stays
// skipped in a direct sync.
TEST_CASE("AMS sync skips a tray without a preset id or a material type", "[Preset][AMS]")
{
PresetBundle bundle;
add_system_filament(bundle, "Generic PETG @Q2", "OFYPdQJh", "PETG");
bundle.filament_ams_list[0] = make_tray("", "", "#000000", "0", "0");
std::vector<std::pair<DynamicPrintConfig *, std::string>> unknowns;
std::map<int, AMSMapInfo> maps;
MergeFilamentInfo merge;
CHECK(bundle.sync_ams_list(unknowns, /*use_map=*/false, maps, /*enable_append=*/false, merge) == 0);
CHECK(bundle.filament_presets.empty());
}
// Guard the pre-existing behavior: a non-empty id that matches no preset still resolves by type.
TEST_CASE("AMS sync still resolves an unmatched preset id by material type", "[Preset][AMS]")
{
PresetBundle bundle;
add_system_filament(bundle, "Generic PETG @Q2", "OFYPdQJh", "PETG");
bundle.filament_ams_list[0] = make_tray("REMOVED_ID", "PETG", "#123456", "0", "0");
std::vector<std::pair<DynamicPrintConfig *, std::string>> unknowns;
std::map<int, AMSMapInfo> maps;
MergeFilamentInfo merge;
CHECK(bundle.sync_ams_list(unknowns, /*use_map=*/false, maps, /*enable_append=*/false, merge) == 1);
CHECK(unknowns.size() == 1);
REQUIRE(bundle.filament_presets.size() == 1);
CHECK(bundle.filament_presets[0] == "Generic PETG @Q2");
}
// Mapping-mode sync used to force "Generic PLA" for an id-less tray; it must use the type instead.
TEST_CASE("AMS sync in mapping mode resolves a printer-set tray to Generic, not Generic PLA", "[Preset][AMS]")
{
PresetBundle bundle;
add_system_filament(bundle, "Generic PETG @Q2", "OFYPdQJh", "PETG");
add_system_filament(bundle, "Generic PLA @Q2", "GFL99", "PLA");
bundle.filament_presets = {"Generic PLA @Q2"};
bundle.project_config.option<ConfigOptionStrings>("filament_colour")->values = {"#000000"};
bundle.project_config.option<ConfigOptionStrings>("filament_colour_type")->values = {"1"};
bundle.filament_ams_list[0] = make_tray("", "PETG", "#898F9B", "0", "1");
std::map<int, AMSMapInfo> maps;
maps[0] = AMSMapInfo{"0", "1"};
std::vector<std::pair<DynamicPrintConfig *, std::string>> unknowns;
MergeFilamentInfo merge;
const unsigned int count = bundle.sync_ams_list(unknowns, /*use_map=*/true, maps, /*enable_append=*/false, merge);
CHECK(count == 1);
CHECK(unknowns.empty());
REQUIRE(bundle.filament_presets.size() == 1);
CHECK(bundle.filament_presets[0] == "Generic PETG @Q2");
CHECK(bundle.project_config.option<ConfigOptionStrings>("filament_colour")->values[0] == "#898F9B");
}
// The dialog builds ams_mapping2 one entry per filament_presets index, while the generated
// G-code's toolchange names the filament's index in the slice's filament config arrays. Those
// are the same index only if full_config() collects the filament arrays in filament_presets
// order; if a refactor reorders one side, every mapped print aims at the wrong lane.
TEST_CASE("Full config filament arrays follow the selected filament preset order", "[Preset][AMS]")
{
PresetBundle bundle;
add_system_filament(bundle, "Generic PETG @Q2", "OFYPdQJh", "PETG");
add_system_filament(bundle, "Generic PLA @Q2", "GFL99", "PLA");
// Deliberately not the load order: the arrays must follow this selection.
bundle.filament_presets = {"Generic PLA @Q2", "Generic PETG @Q2"};
const DynamicPrintConfig full = bundle.full_config(/*apply_extruder=*/false);
REQUIRE(full.option<ConfigOptionStrings>("filament_type") != nullptr);
CHECK(full.option<ConfigOptionStrings>("filament_settings_id")->values == std::vector<std::string>{"Generic PLA @Q2", "Generic PETG @Q2"});
CHECK(full.option<ConfigOptionStrings>("filament_type")->values == std::vector<std::string>{"PLA", "PETG"});
// The AMS sync matches trays by filament_id, so this array must follow the same order.
CHECK(full.option<ConfigOptionStrings>("filament_ids")->values == std::vector<std::string>{"GFL99", "OFYPdQJh"});
}
// get_ams_cobox_infos builds the combo arrays that SyncAmsInfoDialog pairs with AMS colors
// positionally. A printer-set tray must appear so the array stays aligned with the sync output.
TEST_CASE("AMS combo info keeps a printer-set tray and stays aligned", "[Preset][AMS]")
{
PresetBundle bundle;
add_system_filament(bundle, "Generic PETG @Q2", "OFYPdQJh", "PETG");
add_system_filament(bundle, "Generic PLA @Q2", "GFL99", "PLA");
auto tray_a = make_tray("OFYPdQJh", "PETG", "#FE717A", "0", "0");
tray_a.set_key_value("tray_name", new ConfigOptionStrings{"A"});
auto tray_b = make_tray("", "PETG", "#898F9B", "0", "1");
tray_b.set_key_value("tray_name", new ConfigOptionStrings{"B"});
auto tray_c = make_tray("GFL99", "PLA", "#FAFAFA", "0", "2");
tray_c.set_key_value("tray_name", new ConfigOptionStrings{"C"});
bundle.filament_ams_list[0] = tray_a;
bundle.filament_ams_list[1] = tray_b;
bundle.filament_ams_list[2] = tray_c;
AMSComboInfo combo_info;
bundle.get_ams_cobox_infos(combo_info);
// The id-less PETG tray resolves to a generic preset instead of being dropped, so C keeps
// its index and its color.
REQUIRE(combo_info.ams_filament_presets.size() == 3);
CHECK(combo_info.ams_filament_presets[0] == "Generic PETG @Q2");
CHECK(combo_info.ams_filament_presets[1] == "Generic PETG @Q2");
CHECK(combo_info.ams_filament_presets[2] == "Generic PLA @Q2");
REQUIRE(combo_info.ams_filament_colors.size() == 3);
CHECK(combo_info.ams_filament_colors[0] == "#FE717A");
CHECK(combo_info.ams_filament_colors[1] == "#898F9B");
CHECK(combo_info.ams_filament_colors[2] == "#FAFAFA");
REQUIRE(combo_info.ams_names.size() == 3);
CHECK(combo_info.ams_names[0] == "A");
CHECK(combo_info.ams_names[1] == "B");
CHECK(combo_info.ams_names[2] == "C");
}
@@ -1601,6 +1601,23 @@ TEST_CASE("Filaments offered for a machine follow the app's compatibility rule",
} }
} }
// An agent-reported model id (SSDP modelNumber, manual binding) must resolve to the display
// name printer profiles use as printer_model; the AMS dialogs look filaments up by it.
TEST_CASE("Printer model ids resolve to vendor display names", "[Preset][Bundle]")
{
PresetBundle bundle;
VendorProfile qidi("Qidi");
qidi.name = "Qidi";
VendorProfile::PrinterModel model;
model.model_id = "Qidi-Q1Pro";
model.name = "Qidi Q1 Pro";
qidi.models.push_back(model);
bundle.vendors.emplace(qidi.id, qidi);
CHECK(bundle.get_printer_model_display_name("Qidi-Q1Pro") == "Qidi Q1 Pro");
CHECK(bundle.get_printer_model_display_name("unknown-model").empty());
}
namespace { namespace {
+3
View File
@@ -7,6 +7,7 @@ add_executable(${_TEST_NAME}_tests
test_dev_mapping.cpp test_dev_mapping.cpp
test_filament_bitmap_utils.cpp test_filament_bitmap_utils.cpp
test_device_progress.cpp test_device_progress.cpp
test_ams_item.cpp
test_device_manager_integration.cpp test_device_manager_integration.cpp
test_web_media_controller.cpp test_web_media_controller.cpp
test_scene_raycaster.cpp test_scene_raycaster.cpp
@@ -25,6 +26,8 @@ add_executable(${_TEST_NAME}_tests
test_plugin_capabilities_in_use.cpp test_plugin_capabilities_in_use.cpp
test_plugin_status.cpp test_plugin_status.cpp
test_printer_agent.cpp test_printer_agent.cpp
test_orca_mqtt_connection.cpp
test_orca_printer_agent.cpp
test_plugin_install.cpp test_plugin_install.cpp
test_plugin_lifecycle.cpp test_plugin_lifecycle.cpp
test_plugin_printer_agent.cpp test_plugin_printer_agent.cpp
+402
View File
@@ -0,0 +1,402 @@
#pragma once
// In-process plaintext MQTT-over-WebSocket broker for the OrcaMqtt tests.
//
// It speaks just enough of MQTT 3.1.1 to drive OrcaMqttConnection /
// OrcaPrinterAgent end to end without a real network: CONNECT/CONNACK,
// SUBSCRIBE/SUBACK, UNSUBSCRIBE/UNSUBACK, client PUBLISH (QoS 0), PINGREQ and
// DISCONNECT. The outbound PUBLISH frame is built with the production
// OrcaMqttConnection::make_publish_packet() so the tests never depend on a
// second, hand-rolled MQTT encoder.
#include <boost/asio/ip/tcp.hpp>
#include <boost/asio/ip/address.hpp>
#include <boost/asio/socket_base.hpp>
#include <boost/asio/buffer.hpp>
#include <boost/asio/error.hpp>
#include <boost/beast/websocket/stream.hpp>
#include <boost/beast/core/tcp_stream.hpp>
#include <boost/beast/core/stream_traits.hpp>
#include <boost/beast/websocket/stream_base.hpp>
#include <boost/beast/websocket/rfc6455.hpp>
#include <boost/beast/core/flat_buffer.hpp>
#include <boost/beast/core/buffers_to_string.hpp>
#include <slic3r/Utils/OrcaMqttConnection.hpp>
#include <boost/asio.hpp>
#include <boost/beast/core.hpp>
#include <boost/beast/websocket.hpp>
#include <algorithm>
#include <atomic>
#include <chrono>
#include <cstddef>
#include <cstdint>
#include <mutex>
#include <optional>
#include <set>
#include <string>
#include <thread>
#include <utility>
#include <vector>
namespace orca_mqtt_test {
namespace net = boost::asio;
namespace beast = boost::beast;
namespace ws = boost::beast::websocket;
using tcp = boost::asio::ip::tcp;
// Decode an MQTT remaining-length varint starting at packet[offset].
// Returns {value, bytes_consumed}; bytes_consumed == 0 means malformed.
inline std::pair<std::size_t, std::size_t> mqtt_decode_remaining_length(const std::string& packet, std::size_t offset)
{
std::size_t value = 0;
std::size_t multiplier = 1;
std::size_t used = 0;
while (offset + used < packet.size() && used < 4) {
const std::uint8_t byte = static_cast<std::uint8_t>(packet[offset + used]);
value += static_cast<std::size_t>(byte & 0x7f) * multiplier;
multiplier *= 128;
++used;
if ((byte & 0x80) == 0)
return {value, used};
}
return {0, 0};
}
inline bool mqtt_topic_is_request(const std::string& topic)
{
static const std::string suffix = "/request";
return topic.size() >= suffix.size() &&
topic.compare(topic.size() - suffix.size(), suffix.size(), suffix) == 0;
}
class MockBroker
{
public:
// refuse_auth: answer every CONNECT with CONNACK rc 5 (not authorized) and
// close, so the reconnect/refusal paths can be exercised.
// stall_connack: accept the WebSocket upgrade and the CONNECT but never answer,
// so the client's synchronous handshake can be exercised against a peer that
// stops responding.
explicit MockBroker(bool refuse_auth = false, bool stall_connack = false)
: m_refuse_auth(refuse_auth), m_stall_connack(stall_connack), m_acceptor(m_io)
{
const tcp::endpoint endpoint(net::ip::make_address("127.0.0.1"), 0);
m_acceptor.open(endpoint.protocol());
m_acceptor.set_option(net::socket_base::reuse_address(true));
m_acceptor.bind(endpoint);
m_acceptor.listen(net::socket_base::max_listen_connections);
m_port = std::to_string(m_acceptor.local_endpoint().port());
// why: a non-blocking acceptor lets the accept loop poll a stop flag, so
// the destructor never has to interrupt a blocking accept().
m_acceptor.non_blocking(true);
m_thread = std::thread([this] { run(); });
}
~MockBroker()
{
m_stopping.store(true);
drop_client(); // unblocks the worker's blocking read
if (m_thread.joinable())
m_thread.join();
boost::system::error_code ec;
m_acceptor.close(ec); // after join: the acceptor is worker-owned
m_io.stop();
}
MockBroker(const MockBroker&) = delete;
MockBroker& operator=(const MockBroker&) = delete;
std::string ws_url() const { return "ws://127.0.0.1:" + m_port + "/mqtt"; }
std::pair<std::string, std::string> host_port() const { return {std::string("127.0.0.1"), m_port}; }
// Server -> client PUBLISH on device/<dev_id>/report, but only to a client
// that actually subscribed to that topic.
void push_report(const std::string& dev_id, const std::string& payload)
{
const std::string topic = "device/" + dev_id + "/report";
const std::vector<std::uint8_t> packet =
Slic3r::OrcaMqttConnection::make_publish_packet(topic, payload);
std::lock_guard<std::mutex> lock(m_mutex);
if (!m_stream || !m_stream_ready || m_subscriptions.find(topic) == m_subscriptions.end())
return;
boost::system::error_code ec;
m_stream->binary(true);
m_stream->write(net::buffer(packet), ec); // a vanished client is not a test failure
}
// Write arbitrary bytes as one binary WebSocket message, without the topic
// filter push_report applies. Lets a test split a single MQTT packet across
// messages or coalesce several packets into one - the two framings the real
// OrcaSonar /mqtt proxy produces with its 8 KiB reads.
void push_raw(const std::string& bytes)
{
std::lock_guard<std::mutex> lock(m_mutex);
if (!m_stream || !m_stream_ready)
return;
boost::system::error_code ec;
m_stream->binary(true);
m_stream->write(net::buffer(bytes), ec);
}
// Force-close the live client socket; the worker's read returns an error and
// the accept loop picks up the client's reconnect.
void drop_client()
{
std::lock_guard<std::mutex> lock(m_mutex);
close_client_locked();
}
// Payloads the client PUBLISHed to any device/<id>/request topic.
std::vector<std::string> received_requests() const
{
std::lock_guard<std::mutex> lock(m_mutex);
return m_received_requests;
}
// Whether a topic filter has been SUBSCRIBEd and not UNSUBSCRIBEd since.
bool is_subscribed(const std::string& topic) const
{
std::lock_guard<std::mutex> lock(m_mutex);
return m_subscriptions.find(topic) != m_subscriptions.end();
}
// MQTT CONNECTs seen; increments again after a reconnect.
int connect_count() const { return m_connect_count.load(); }
// MQTT PINGREQs seen while the client has no other traffic.
int ping_count() const { return m_ping_count.load(); }
private:
void run()
{
try {
while (!m_stopping.load()) {
tcp::socket socket(m_io);
boost::system::error_code ec;
m_acceptor.accept(socket, ec);
if (ec == net::error::would_block || ec == net::error::try_again) {
std::this_thread::sleep_for(std::chrono::milliseconds(5));
continue;
}
if (ec)
return;
try {
serve(std::move(socket));
} catch (...) {
// a client dying mid-session must not take the broker down
}
std::lock_guard<std::mutex> lock(m_mutex);
close_client_locked();
m_stream.reset();
m_subscriptions.clear(); // clean session: filters die with the connection
}
} catch (...) {
// never let an exception escape the broker thread
}
}
void serve(tcp::socket socket)
{
ws::stream<beast::tcp_stream>* stream = nullptr;
{
std::lock_guard<std::mutex> lock(m_mutex);
m_stream.emplace(std::move(socket));
m_stream_ready = false;
stream = &*m_stream;
}
// why: no io_context is ever run here, so a tcp_stream timer would never
// fire; the sync operations below carry no timeout of their own.
beast::get_lowest_layer(*stream).expires_never();
stream->set_option(ws::stream_base::decorator(
[](ws::response_type& res) { res.set("Sec-WebSocket-Protocol", "mqtt"); }));
boost::system::error_code ec;
stream->accept(ec);
if (ec)
return;
stream->binary(true);
{
std::lock_guard<std::mutex> lock(m_mutex);
m_stream_ready = true;
}
read_loop(*stream);
}
// The client sends every MQTT packet as one binary WebSocket message, so one
// read yields exactly one packet.
void read_loop(ws::stream<beast::tcp_stream>& stream)
{
beast::flat_buffer buffer;
while (!m_stopping.load()) {
boost::system::error_code ec;
buffer.clear();
stream.read(buffer, ec);
if (ec)
return;
const std::string packet = beast::buffers_to_string(buffer.data());
if (packet.empty())
continue;
if (!handle_packet(stream, packet))
return;
}
}
// Returns false when the session must be closed.
bool handle_packet(ws::stream<beast::tcp_stream>& stream, const std::string& packet)
{
switch (static_cast<std::uint8_t>(packet[0]) & 0xf0) {
case 0x10: { // CONNECT
++m_connect_count;
if (m_refuse_auth) {
write_packet(stream, {0x20, 0x02, 0x00, 0x05}); // CONNACK not authorized
return false;
}
if (m_stall_connack)
return true; // accepted, never answered: the client's read blocks
write_packet(stream, {0x20, 0x02, 0x00, 0x00}); // CONNACK accepted
return true;
}
case 0x80: { // SUBSCRIBE (0x82) - packet id follows the remaining-length varint
const auto id = packet_id(packet);
if (id)
write_packet(stream, {0x90, 0x03, id->first, id->second, 0x00}); // SUBACK, QoS 0
{
std::lock_guard<std::mutex> lock(m_mutex);
for (const std::string& topic : packet_topics(packet, /*qos_per_topic=*/true))
m_subscriptions.insert(topic);
}
return true;
}
case 0xa0: { // UNSUBSCRIBE (0xa2)
const auto id = packet_id(packet);
if (id)
write_packet(stream, {0xb0, 0x02, id->first, id->second}); // UNSUBACK
{
std::lock_guard<std::mutex> lock(m_mutex);
for (const std::string& topic : packet_topics(packet, /*qos_per_topic=*/false))
m_subscriptions.erase(topic);
}
return true;
}
case 0x30: { // PUBLISH, QoS 0 (no packet identifier)
record_publish(packet);
return true;
}
case 0xc0: // PINGREQ
++m_ping_count;
write_packet(stream, {0xd0, 0x00});
return true;
case 0xe0: // DISCONNECT
return false;
default:
return true;
}
}
// The two packet-identifier bytes sitting right after the remaining-length varint.
static std::optional<std::pair<std::uint8_t, std::uint8_t>> packet_id(const std::string& packet)
{
const auto varint = mqtt_decode_remaining_length(packet, 1);
if (varint.second == 0)
return std::nullopt;
const std::size_t pos = 1 + varint.second;
if (pos + 2 > packet.size())
return std::nullopt;
return std::make_pair(static_cast<std::uint8_t>(packet[pos]), static_cast<std::uint8_t>(packet[pos + 1]));
}
// Topic filters carried by a SUBSCRIBE/UNSUBSCRIBE payload: repeated
// [2-byte length][UTF-8 topic], each followed by a QoS byte for SUBSCRIBE.
static std::vector<std::string> packet_topics(const std::string& packet, bool qos_per_topic)
{
std::vector<std::string> topics;
const auto varint = mqtt_decode_remaining_length(packet, 1);
if (varint.second == 0)
return topics;
const std::size_t end = std::min(packet.size(), 1 + varint.second + varint.first);
std::size_t pos = 1 + varint.second + 2; // after the packet identifier
while (pos + 2 <= end) {
const std::size_t len = (static_cast<std::size_t>(static_cast<std::uint8_t>(packet[pos])) << 8) |
static_cast<std::uint8_t>(packet[pos + 1]);
pos += 2;
if (pos + len > end)
break;
topics.push_back(packet.substr(pos, len));
pos += len + (qos_per_topic ? 1 : 0);
}
return topics;
}
void record_publish(const std::string& packet)
{
const auto varint = mqtt_decode_remaining_length(packet, 1);
if (varint.second == 0)
return;
std::size_t pos = 1 + varint.second;
if (pos + 2 > packet.size())
return;
const std::size_t topic_len = (static_cast<std::size_t>(static_cast<std::uint8_t>(packet[pos])) << 8) |
static_cast<std::uint8_t>(packet[pos + 1]);
pos += 2;
if (pos + topic_len > packet.size())
return;
const std::string topic = packet.substr(pos, topic_len);
pos += topic_len;
const std::size_t end = std::min(packet.size(), 1 + varint.second + varint.first);
if (end < pos)
return;
if (!mqtt_topic_is_request(topic))
return;
std::lock_guard<std::mutex> lock(m_mutex);
m_received_requests.push_back(packet.substr(pos, end - pos));
}
// Every write - the worker's own replies and push_report() from the test
// thread - is serialised by m_mutex. The production client keeps all of its
// WebSocket operations on its MQTT worker instead.
void write_packet(ws::stream<beast::tcp_stream>& stream, const std::vector<std::uint8_t>& packet)
{
std::lock_guard<std::mutex> lock(m_mutex);
boost::system::error_code ec;
stream.binary(true);
stream.write(net::buffer(packet), ec);
}
void close_client_locked()
{
if (!m_stream)
return;
m_stream_ready = false;
boost::system::error_code ec;
auto& socket = beast::get_lowest_layer(*m_stream).socket();
if (socket.cancel(ec))
return;
// shutdown() before close() is what actually wakes a blocking read on the
// worker thread; close() alone does not on POSIX.
if (socket.shutdown(tcp::socket::shutdown_both, ec))
return;
if (socket.close(ec))
return;
}
const bool m_refuse_auth;
const bool m_stall_connack;
net::io_context m_io;
tcp::acceptor m_acceptor;
std::string m_port;
std::thread m_thread;
std::atomic_bool m_stopping{false};
std::atomic<int> m_connect_count{0};
std::atomic<int> m_ping_count{0};
mutable std::mutex m_mutex;
std::optional<ws::stream<beast::tcp_stream>> m_stream; // guarded by m_mutex
bool m_stream_ready = false; // guarded by m_mutex
std::vector<std::string> m_received_requests; // guarded by m_mutex
std::set<std::string> m_subscriptions; // guarded by m_mutex
};
} // namespace orca_mqtt_test
+136
View File
@@ -0,0 +1,136 @@
// why: match the GUI include order to avoid rpcndr.h byte/std::byte
// ambiguity in the Windows COM headers.
#include "catch2/catch_test_macros.hpp"
#ifdef WIN32
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <Windows.h>
#endif
#include <catch2/catch_all.hpp>
#include <wx/timer.h>
#include <nlohmann/json.hpp>
#include "slic3r/GUI/Widgets/AMSItem.hpp"
#include "slic3r/GUI/DeviceCore/DevFilaSystem.h"
using json = nlohmann::json;
using namespace Slic3r;
using namespace Slic3r::GUI;
// A configured slot the wire reports as having no filament (all material fields
// present and empty) is "Empty", not the unknown "?" the UI draws for a tray
// whose info is missing. The distinction must survive into the AMS panel model.
TEST_CASE("Configured empty AMS trays remain distinct from unknown trays", "[AMSItem]")
{
MachineObject machine(nullptr, nullptr, "test", "test-device", "127.0.0.1");
machine.printer_agent_id = "orca";
const json print_json = json::parse(R"({
"ams": {
"ams_exist_bits": "1",
"tray_exist_bits": "3",
"ams": [ { "id": "0", "info": "0001", "tray": [
{ "id": "0", "tag_uid": "0000000000000000", "tray_info_idx": "", "tray_type": "", "tray_color": "00000000" },
{ "id": "1" }
] } ]
}
})");
DevFilaSystemParser::ParseV1_0(print_json, &machine, machine.GetFilaSystem().get(), false);
const auto& ams_list = machine.GetFilaSystem()->GetAmsList();
const auto ams_it = ams_list.find("0");
REQUIRE(ams_it != ams_list.end());
auto* ams = ams_it->second;
REQUIRE(ams != nullptr);
REQUIRE(ams->GetTray("0") != nullptr);
CHECK(ams->GetTray("0")->is_empty);
AMSinfo info;
REQUIRE(info.parse_ams_info(&machine, ams));
REQUIRE(info.cans.size() == 2);
CHECK(info.cans[0].is_empty);
CHECK(info.cans[0].material_state == AMSCanType::AMS_CAN_TYPE_THIRDBRAND);
CHECK_FALSE(info.cans[1].is_empty);
CHECK(info.cans[1].material_state == AMSCanType::AMS_CAN_TYPE_THIRDBRAND);
}
TEST_CASE("Empty external slots remain distinct from unknown slots", "[AMSItem]")
{
MachineObject machine(nullptr, nullptr, "test", "test-device", "127.0.0.1");
machine.printer_agent_id = "orca";
DevAmsTray empty_slot = machine.parse_vt_tray(json::parse(R"({
"id": "255", "tag_uid": "0000000000000000", "tray_info_idx": "", "tray_type": "", "tray_color": "00000000"
})"));
CHECK(empty_slot.is_empty);
AMSinfo empty_info;
empty_info.parse_ext_info(&machine, empty_slot);
CHECK(empty_info.cans[0].is_empty);
CHECK(empty_info.cans[0].material_state == AMSCanType::AMS_CAN_TYPE_VIRTUAL);
DevAmsTray unknown_slot = machine.parse_vt_tray(json::parse(R"({"id": "254"})"));
CHECK_FALSE(unknown_slot.is_empty);
AMSinfo unknown_info;
unknown_info.parse_ext_info(&machine, unknown_slot);
CHECK_FALSE(unknown_info.cans[0].is_empty);
CHECK(unknown_info.cans[0].material_state == AMSCanType::AMS_CAN_TYPE_VIRTUAL);
}
// Only the OrcaSonar agent opts into the "Empty" classification. Bambu and other
// agents keep the unknown "?" for a present-but-blank tray, so they must leave the
// empty flag untouched.
TEST_CASE("Bambu-style agents keep blank AMS trays as unknown", "[AMSItem]")
{
MachineObject machine(nullptr, nullptr, "test", "test-device", "127.0.0.1");
machine.printer_agent_id = "bbl";
const json print_json = json::parse(R"({
"ams": {
"ams_exist_bits": "1",
"tray_exist_bits": "3",
"ams": [ { "id": "0", "info": "0001", "tray": [
{ "id": "0", "tag_uid": "0000000000000000", "tray_info_idx": "", "tray_type": "", "tray_color": "00000000" },
{ "id": "1" }
] } ]
}
})");
DevFilaSystemParser::ParseV1_0(print_json, &machine, machine.GetFilaSystem().get(), false);
const auto& ams_list = machine.GetFilaSystem()->GetAmsList();
const auto ams_it = ams_list.find("0");
REQUIRE(ams_it != ams_list.end());
auto* ams = ams_it->second;
REQUIRE(ams != nullptr);
REQUIRE(ams->GetTray("0") != nullptr);
CHECK_FALSE(ams->GetTray("0")->is_empty);
AMSinfo info;
REQUIRE(info.parse_ams_info(&machine, ams));
REQUIRE(info.cans.size() == 2);
CHECK_FALSE(info.cans[0].is_empty);
}
TEST_CASE("Bambu-style agents keep blank external slots as unknown", "[AMSItem]")
{
MachineObject machine(nullptr, nullptr, "test", "test-device", "127.0.0.1");
machine.printer_agent_id = "bbl";
DevAmsTray empty_slot = machine.parse_vt_tray(json::parse(R"({
"id": "255", "tag_uid": "0000000000000000", "tray_info_idx": "", "tray_type": "", "tray_color": "00000000"
})"));
CHECK_FALSE(empty_slot.is_empty);
AMSinfo info;
info.parse_ext_info(&machine, empty_slot);
CHECK_FALSE(info.cans[0].is_empty);
}
@@ -1,9 +1,16 @@
#include <catch2/catch_test_macros.hpp> #include <catch2/catch_test_macros.hpp>
#include <catch2/catch_all.hpp> #include <catch2/catch_all.hpp>
#include <libslic3r/ProjectTask.hpp>
#include <libslic3r/Preset.hpp>
#include <libslic3r/Config.hpp>
#include <slic3r/GUI/DeviceCore/DevManager.h> #include <slic3r/GUI/DeviceCore/DevManager.h>
#include <slic3r/GUI/DeviceManager.hpp> #include <slic3r/GUI/DeviceManager.hpp>
#include <slic3r/GUI/DeviceCore/DevFilaSystem.h>
#include <slic3r/GUI/FilamentMappingUtils.hpp>
#include <libslic3r/AppConfig.hpp> #include <libslic3r/AppConfig.hpp>
#include <libslic3r/PresetBundle.hpp>
#include <libslic3r/PrintConfig.hpp>
#include "slic3r/Utils/IPrinterAgent.hpp" #include "slic3r/Utils/IPrinterAgent.hpp"
#include "slic3r/Utils/CloudProvider.hpp" #include "slic3r/Utils/CloudProvider.hpp"
#include <slic3r/Utils/NetworkAgent.hpp> #include <slic3r/Utils/NetworkAgent.hpp>
@@ -18,6 +25,8 @@
#include <utility> #include <utility>
using namespace Slic3r; using namespace Slic3r;
using Slic3r::GUI::MappingSendError;
using Slic3r::GUI::prepare_filament_mapping_for_send;
using json = nlohmann::json; using json = nlohmann::json;
namespace { namespace {
@@ -49,6 +58,30 @@ public:
AgentInfo get_agent_info() override { return m_info; } AgentInfo get_agent_info() override { return m_info; }
// This test double represents a legacy agent, not OrcaPrinterAgent behavior.
bool supports_command(const std::string&, const std::string&) const override { return true; }
// It models whichever agent is registered under its id: only the Orca agent
// serializes per-print filament_mapping.
bool uses_filament_mapping() const override { return m_info.id == "orca"; }
using OrcaPrinterAgent::deliver_to_sink;
int send_message(std::string, std::string json_str, int, int) override
{
last_message = std::move(json_str);
return send_result;
}
int send_message_to_printer(std::string, std::string json_str, int, int) override
{
last_message = std::move(json_str);
return send_result;
}
std::string last_message;
int send_result = 0;
private: private:
AgentInfo m_info; AgentInfo m_info;
}; };
@@ -160,3 +193,183 @@ TEST_CASE("Device manager filters and rehomes devices by printer-agent ownership
CHECK(manager.get_my_machine_list("integration-agent-a").empty()); CHECK(manager.get_my_machine_list("integration-agent-a").empty());
CHECK(manager.get_my_machine_list("integration-agent-b").count(machine.dev_id) == 1); CHECK(manager.get_my_machine_list("integration-agent-b").count(machine.dev_id) == 1);
} }
TEST_CASE("Network agent rejects capability queries for a different device owner", "[DeviceManager][integration]")
{
NetworkAgent network(nullptr, std::make_shared<TestPrinterAgent>("bbl"));
CHECK(network.owns_agent("bbl"));
CHECK_FALSE(network.owns_agent("orca"));
CHECK(network.supports_command("bbl", "device", "print.ams_control"));
CHECK_FALSE(network.supports_command("orca", "device", "print.ams_control"));
CHECK_FALSE(network.supports_feature("orca", "device", "filament_mapping"));
// The mapping dialect is queried the same way: a legacy owner never inherits
// Orca's, and a non-owner is never queried at all.
CHECK_FALSE(network.uses_filament_mapping("bbl"));
CHECK_FALSE(network.uses_filament_mapping("orca"));
NetworkAgent orca_network(nullptr, std::make_shared<TestPrinterAgent>("orca"));
CHECK(orca_network.uses_filament_mapping("orca"));
CHECK_FALSE(orca_network.uses_filament_mapping("bbl"));
}
// Send-time mapping policy belongs to the printer agent's dialect, not to an
// agent id: the Orca dialect gets the no-AMS normalization plus both refusals.
TEST_CASE("Per-print mapping send policy follows the owning agent's dialect", "[DeviceManager][integration]")
{
auto orca_agent = std::make_shared<TestPrinterAgent>("orca");
NetworkAgent network(nullptr, orca_agent);
MachineObject obj(nullptr, &network, "test", "orca-mapping-policy", "127.0.0.1");
obj.printer_agent_id = "orca";
// No AMS: the auto-selected external spool is dropped before the capability
// gate, so it cannot refuse a print nobody mapped.
std::string external_only = R"([{"ams_id":255,"slot_id":0}])";
CHECK(prepare_filament_mapping_for_send(&obj, external_only, {}) == MappingSendError::none);
CHECK(external_only.empty());
// An AMS makes that a real target, but nothing advertised the capability yet.
obj.GetFilaSystem()->GetAmsList()["0"] = new DevAms("0", 0, DevAms::AMS);
std::string mapped = R"([{"ams_id":0,"slot_id":0}])";
CHECK(prepare_filament_mapping_for_send(&obj, mapped, {}) == MappingSendError::unsupported);
CHECK(mapped == R"([{"ams_id":0,"slot_id":0}])"); // the refusal leaves it intact
// Once the connector advertises the capability the mapping passes the gate.
orca_agent->deliver_to_sink(obj.get_dev_id(),
R"({"info":{"command":"get_capabilities","supported_features":{"filament_mapping":true}}})",
false);
CHECK(obj.printer_supports_feature("filament_mapping"));
CHECK(prepare_filament_mapping_for_send(&obj, mapped, {}) == MappingSendError::none);
// An unrecorded owner still sends through the active agent, so it takes
// that agent's dialect too rather than falling back to the legacy payload.
MachineObject unowned(nullptr, &network, "test", "orca-unowned", "127.0.0.1");
CHECK(unowned.printer_agent_id.empty());
CHECK(unowned.printer_uses_filament_mapping());
std::string unowned_external = R"([{"ams_id":255,"slot_id":0}])";
CHECK(prepare_filament_mapping_for_send(&unowned, unowned_external, {}) == MappingSendError::none);
CHECK(unowned_external.empty()); // the Orca normalization ran
// A partially mapped print is refused even while the capability holds.
FilamentInfo mapped_entry;
mapped_entry.ams_id = "0";
mapped_entry.slot_id = "0";
FilamentInfo unmapped_entry;
CHECK(prepare_filament_mapping_for_send(&obj, mapped, {mapped_entry, unmapped_entry}) ==
MappingSendError::incomplete);
}
// The legacy payload is the thing being protected: no normalization, no refusal.
TEST_CASE("A legacy printer agent keeps its mapping payload untouched", "[DeviceManager][integration]")
{
NetworkAgent network(nullptr, std::make_shared<TestPrinterAgent>("bbl"));
MachineObject obj(nullptr, &network, "test", "bbl-mapping-policy", "127.0.0.1");
obj.printer_agent_id = "bbl";
FilamentInfo mapped_entry;
mapped_entry.ams_id = "0";
mapped_entry.slot_id = "0";
FilamentInfo unmapped_entry;
std::string external_only = R"([{"ams_id":255,"slot_id":0}])";
CHECK(prepare_filament_mapping_for_send(&obj, external_only, {mapped_entry, unmapped_entry}) ==
MappingSendError::none);
CHECK(external_only == R"([{"ams_id":255,"slot_id":0}])");
}
TEST_CASE("AMS metadata retains setting_id for legacy printer agents", "[DeviceManager][integration]")
{
ScopedAppConfig app_config;
auto printer_agent = std::make_shared<TestPrinterAgent>("bbl");
NetworkAgent network(nullptr, printer_agent);
DeviceManager manager(&network, false, &app_config.config);
BBLocalMachine machine;
machine.dev_id = "bbl-setting-id";
machine.dev_name = "Bambu setting id";
machine.dev_ip = "192.0.2.32";
machine.printer_type = "C11";
MachineObject* obj = manager.insert_local_device(machine, "lan", "free", "", "access-code");
REQUIRE(obj != nullptr);
REQUIRE(obj->command_ams_filament_settings(0, 1, "GFL99", "preset-setting", "00FF00FF", "PLA", 190, 220) == 0);
const json payload = json::parse(printer_agent->last_message);
CHECK(payload["print"]["setting_id"] == "preset-setting");
}
TEST_CASE("AMS user settings do not update local state when publishing fails", "[DeviceManager][integration]")
{
ScopedAppConfig app_config;
auto printer_agent = std::make_shared<TestPrinterAgent>("bbl");
NetworkAgent network(nullptr, printer_agent);
DeviceManager manager(&network, false, &app_config.config);
BBLocalMachine machine;
machine.dev_id = "bbl-ams-setting-failure";
machine.dev_name = "Bambu AMS settings";
machine.dev_ip = "192.0.2.33";
machine.printer_type = "C11";
MachineObject* obj = manager.insert_local_device(machine, "lan", "free", "", "access-code");
REQUIRE(obj != nullptr);
auto& settings = obj->GetFilaSystem()->GetAmsSystemSetting();
settings.SetDetectOnInsertEnabled(false);
settings.SetDetectOnPowerupEnabled(false);
settings.SetDetectRemainEnabled(false);
printer_agent->send_result = -1;
CHECK(obj->command_ams_user_settings(true, true, true) != 0);
CHECK(settings.IsDetectOnInsertEnabled() == false);
CHECK(settings.IsDetectOnPowerupEnabled() == false);
CHECK(settings.IsDetectRemainEnabled() == false);
}
// The AMS dialogs resolve their filament list from the connected device's model. OrcaSonar's
// model id is optional (the agent falls back to "orcasonar"), so the resolver must stand in
// with the selected printer profile instead of yielding no model at all.
TEST_CASE("Filament printer model resolution falls back to the selected profile", "[DeviceManager][integration]")
{
PresetBundle bundle;
VendorProfile qidi("Qidi");
qidi.name = "Qidi";
VendorProfile::PrinterModel model;
model.model_id = "Qidi-Q1Pro";
model.name = "Qidi Q1 Pro";
qidi.models.push_back(model);
bundle.vendors.emplace(qidi.id, qidi);
// A vendor model id the device reported resolves through the vendor catalog.
CHECK(resolve_filament_printer_model("Qidi-Q1Pro", &bundle) == "Qidi Q1 Pro");
// The OrcaSonar fallback id has no vendor model; the selected profile stands in.
CHECK(resolve_filament_printer_model("orcasonar", &bundle).empty());
bundle.printers.get_selected_preset().config.set_key_value("printer_model", new ConfigOptionString("Generic Klipper Printer"));
CHECK(resolve_filament_printer_model("orcasonar", &bundle) == "Generic Klipper Printer");
CHECK(resolve_filament_printer_model("", &bundle) == "Generic Klipper Printer");
CHECK(resolve_filament_printer_model("orcasonar", nullptr).empty());
}
// The per-tray K/N records are Bambu firmware's flow-dynamics calibration. Agents with no
// printer-side records must not offer the AMS K/N controls (they would show a synthesized
// default and then refuse to confirm it).
TEST_CASE("Flow-dynamics K/N is offered for Bambu agents only", "[DeviceManager][integration]")
{
MachineObject bbl(nullptr, nullptr, "test", "bbl-device", "127.0.0.1");
bbl.printer_agent_id = "bbl";
CHECK(bbl.supports_extrusion_cali());
MachineObject orca(nullptr, nullptr, "test", "orca-device", "127.0.0.1");
orca.printer_agent_id = "orca";
CHECK_FALSE(orca.supports_extrusion_cali());
MachineObject moonraker(nullptr, nullptr, "test", "moonraker-device", "127.0.0.1");
moonraker.printer_agent_id = "moonraker";
CHECK_FALSE(moonraker.supports_extrusion_cali());
// No agent id predates the agent split and keeps the Bambu path.
MachineObject legacy(nullptr, nullptr, "test", "legacy-device", "127.0.0.1");
CHECK(legacy.supports_extrusion_cali());
}
@@ -0,0 +1,436 @@
#include <catch2/catch_test_macros.hpp>
#include <slic3r/Utils/OrcaMqttConnection.hpp>
#include "orca_mqtt_mock_broker.hpp"
#include <chrono>
#include <condition_variable>
#include <cstddef>
#include <cstdint>
#include <mutex>
#include <string>
#include <thread>
#include <vector>
using Slic3r::OrcaMqttConnection;
// Offset of the CONNECT variable header: 1 (fixed header) + N remaining-length varint bytes.
static size_t mqtt_varheader_offset(const std::vector<uint8_t>& p) {
size_t i = 1;
while (i < p.size() && (p[i] & 0x80)) ++i; // skip varint continuation bytes
return i + 1; // + the final varint byte
}
TEST_CASE("OrcaMqtt parse_endpoint handles ws and wss", "[OrcaMqtt]") {
OrcaMqttConnection::Endpoint ep;
REQUIRE(OrcaMqttConnection::parse_endpoint("ws://printer.local:8280/mqtt", ep));
CHECK(ep.host == "printer.local");
CHECK(ep.port == "8280");
CHECK(ep.target == "/mqtt");
REQUIRE(OrcaMqttConnection::parse_endpoint("ws://10.0.0.5/mqtt", ep));
CHECK(ep.port == "80");
REQUIRE(OrcaMqttConnection::parse_endpoint("wss://api.example.com/api/v1/printers/abc/mqtt", ep));
CHECK(ep.host == "api.example.com");
CHECK(ep.port == "443");
CHECK(ep.target == "/api/v1/printers/abc/mqtt");
CHECK_FALSE(OrcaMqttConnection::parse_endpoint("http://x/y", ep));
}
TEST_CASE("OrcaMqtt CONNECT packet - no auth (cloud form)", "[OrcaMqtt]") {
auto p = OrcaMqttConnection::make_connect_packet("OrcaSlicer", "", "", 300);
REQUIRE(p.size() >= 12);
CHECK(p[0] == 0x10); // CONNECT fixed header
const size_t v = mqtt_varheader_offset(p);
CHECK(p[v + 0] == 0x00); CHECK(p[v + 1] == 0x04); // protocol name length
CHECK(p[v + 2] == 'M'); CHECK(p[v + 3] == 'Q');
CHECK(p[v + 4] == 'T'); CHECK(p[v + 5] == 'T');
CHECK(p[v + 6] == 0x04); // protocol level 3.1.1
CHECK(p[v + 7] == 0x02); // connect flags: clean session only
CHECK(((p[v + 8] << 8) | p[v + 9]) == 300); // keepalive
}
TEST_CASE("OrcaMqtt CONNECT packet - username/password (LAN form)", "[OrcaMqtt]") {
auto p = OrcaMqttConnection::make_connect_packet("orcaslicer-lan-x", "orcasonar", "code123", 60);
CHECK(p[0] == 0x10);
const size_t v = mqtt_varheader_offset(p);
CHECK(p[v + 7] == (0x02 | 0x80 | 0x40)); // clean session + username + password flags
const std::string blob(p.begin(), p.end());
CHECK(blob.find("orcaslicer-lan-x") != std::string::npos);
CHECK(blob.find("orcasonar") != std::string::npos);
CHECK(blob.find("code123") != std::string::npos);
}
// Auth precedence (spec O3): when a bearer_provider is configured, connect_and_read
// passes empty CONNECT credentials, so the packet must carry clean-session only and
// no username/password flags or payload fields. (The precedence branch itself lives
// in connect_and_read; the cloud-style round trip test exercises it live.)
TEST_CASE("OrcaMqtt CONNECT omits creds when a bearer is configured", "[OrcaMqtt]") {
auto p = OrcaMqttConnection::make_connect_packet("cid", "", "", 60);
const size_t v = mqtt_varheader_offset(p);
CHECK(p[v + 7] == 0x02); // clean session only: no 0x80 / 0x40
const std::string blob(p.begin(), p.end());
CHECK(blob.find("orcasonar") == std::string::npos);
}
TEST_CASE("OrcaMqtt topic helpers", "[OrcaMqtt]") {
CHECK(OrcaMqttConnection::request_topic("abc") == "device/abc/request");
CHECK(OrcaMqttConnection::report_topic("abc") == "device/abc/report");
}
TEST_CASE("OrcaMqtt PUBLISH packet QoS0", "[OrcaMqtt]") {
auto p = OrcaMqttConnection::make_publish_packet("device/abc/request", "{\"ok\":1}");
CHECK((p[0] & 0xf0) == 0x30); // PUBLISH
CHECK((p[0] & 0x06) == 0x00); // QoS 0
const std::string blob(p.begin(), p.end());
CHECK(blob.find("device/abc/request") != std::string::npos);
CHECK(blob.find("{\"ok\":1}") != std::string::npos);
}
TEST_CASE("OrcaMqtt SUBSCRIBE packet", "[OrcaMqtt]") {
auto p = OrcaMqttConnection::make_subscribe_packet(7, "device/abc/report", 1);
CHECK(p[0] == 0x82); // SUBSCRIBE + reserved bit
const size_t v = mqtt_varheader_offset(p);
CHECK(((p[v] << 8) | p[v + 1]) == 7); // packet id
CHECK(p.back() == 1); // requested QoS
}
TEST_CASE("OrcaMqtt send_request refuses when not connected", "[OrcaMqtt]") {
OrcaMqttConnection conn;
CHECK_FALSE(conn.send_request("abc", "{\"pushing\":{\"command\":\"pushall\",\"sequence_id\":\"20001\"}}"));
}
TEST_CASE("OrcaMqtt start takes a Config", "[OrcaMqtt]") {
OrcaMqttConnection conn;
OrcaMqttConnection::Config cfg;
cfg.url = "ws://127.0.0.1:1/mqtt"; // nothing listening
cfg.keepalive_seconds = 42;
// start() returns false (no server) but must compile with the Config overload
const bool ok = conn.start(cfg, [](auto, auto){}, [](bool, bool){});
CHECK_FALSE(ok);
CHECK(conn.last_connack_rc() == -1);
conn.stop();
}
TEST_CASE("MockBroker starts and reports a url", "[OrcaMqtt]") {
orca_mqtt_test::MockBroker b;
CHECK(b.ws_url().rfind("ws://127.0.0.1:", 0) == 0);
CHECK(b.connect_count() == 0);
}
// --- End-to-end loopback: OrcaMqttConnection against the in-process MockBroker.
// These prove a LAN-style config (CONNECT username/password) and a cloud-style
// config (bearer on the WS upgrade, no CONNECT creds) drive the *same*
// OrcaMqttConnection code path with identical assertions.
// The client's SUBSCRIBE is written asynchronously; wait until the broker records it.
static bool wait_subscribed(orca_mqtt_test::MockBroker& broker, const std::string& topic) {
for (int i = 0; i < 200; ++i) {
if (broker.is_subscribed(topic)) return true;
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
return false;
}
static void run_round_trip(bool use_tls_flag_only) {
orca_mqtt_test::MockBroker broker;
OrcaMqttConnection conn;
OrcaMqttConnection::Config cfg;
cfg.url = broker.ws_url(); // plaintext regardless
cfg.use_tls = false; // the mock is plaintext; the flag path is unit-tested elsewhere
if (use_tls_flag_only) cfg.bearer_provider = []{ return std::string("tok"); };
else { cfg.username = "orcasonar"; cfg.password = "code"; }
// A mutex + condition_variable rather than a promise: the handler runs on the MQTT
// worker thread and a second inbound message would throw std::future_error there.
std::mutex got_mutex;
std::condition_variable got_cv;
bool got_any = false;
std::string got_id, got_payload;
REQUIRE(conn.start(cfg,
[&](const std::string& id, const std::string& payload){
{
std::lock_guard<std::mutex> l(got_mutex);
if (got_any) return; // keep the first message only
got_any = true; got_id = id; got_payload = payload;
}
got_cv.notify_all();
},
[](bool,bool){}));
REQUIRE(conn.subscribe("dev-1"));
REQUIRE(wait_subscribed(broker, "device/dev-1/report"));
REQUIRE(conn.send_request("dev-1", R"({"pushing":{"command":"pushall","sequence_id":"20001"}})"));
broker.push_report("dev-1", R"({"print":{"command":"push_status","sequence_id":"20001","result":"success"}})");
std::string id, payload;
{
std::unique_lock<std::mutex> l(got_mutex);
REQUIRE(got_cv.wait_for(l, std::chrono::seconds(3), [&]{ return got_any; }));
id = got_id; payload = got_payload;
}
CHECK(id == "dev-1");
CHECK(payload.find("push_status") != std::string::npos);
// the client's command reached the broker on the request topic. The mock records
// the PUBLISH on its own read-loop thread, so poll rather than check immediately.
std::vector<std::string> reqs;
for (int i = 0; i < 200; ++i) {
reqs = broker.received_requests();
if (!reqs.empty()) break;
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
REQUIRE(reqs.size() >= 1);
CHECK(reqs.front().find("pushall") != std::string::npos);
conn.stop();
}
TEST_CASE("OrcaMqtt round-trip — LAN-style config", "[OrcaMqtt]") { run_round_trip(false); }
TEST_CASE("OrcaMqtt round-trip — cloud-style config", "[OrcaMqtt]") { run_round_trip(true); }
TEST_CASE("OrcaMqtt keepalive runs while the connection is idle", "[OrcaMqtt]") {
orca_mqtt_test::MockBroker broker;
OrcaMqttConnection conn;
OrcaMqttConnection::Config cfg;
cfg.url = broker.ws_url();
cfg.keepalive_seconds = 2;
REQUIRE(conn.start(cfg, [](const std::string&, const std::string&) {}, [](bool, bool) {}));
for (int i = 0; i < 200 && broker.ping_count() == 0; ++i)
std::this_thread::sleep_for(std::chrono::milliseconds(10));
CHECK(broker.ping_count() > 0);
conn.stop();
}
TEST_CASE("OrcaMqtt reconnects and re-subscribes after a socket drop", "[OrcaMqtt]") {
orca_mqtt_test::MockBroker broker;
OrcaMqttConnection conn;
OrcaMqttConnection::Config cfg; cfg.url = broker.ws_url(); cfg.use_tls = false; cfg.username = "u"; cfg.password = "p";
std::mutex m; std::vector<std::string> got;
REQUIRE(conn.start(cfg,
[&](const std::string&, const std::string& p){ std::lock_guard<std::mutex> l(m); got.push_back(p); },
[](bool,bool){}));
REQUIRE(conn.subscribe("dev-1"));
REQUIRE(wait_subscribed(broker, "device/dev-1/report"));
broker.drop_client();
// the worker reconnects with ~1s backoff
for (int i = 0; i < 300 && broker.connect_count() < 2; ++i)
std::this_thread::sleep_for(std::chrono::milliseconds(20));
CHECK(broker.connect_count() >= 2);
// a report after the reconnect must still be delivered -> the SUBSCRIBE was re-sent
REQUIRE(wait_subscribed(broker, "device/dev-1/report"));
broker.push_report("dev-1", R"({"print":{"command":"push_status","sequence_id":"20002"}})");
bool delivered = false;
for (int i = 0; i < 200 && !delivered; ++i) {
{ std::lock_guard<std::mutex> l(m); delivered = !got.empty(); }
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
CHECK(delivered);
conn.stop();
}
TEST_CASE("OrcaMqtt auth rejection is terminal (no retry storm)", "[OrcaMqtt]") {
orca_mqtt_test::MockBroker broker(/*refuse_auth=*/true);
OrcaMqttConnection conn;
OrcaMqttConnection::Config cfg; cfg.url = broker.ws_url(); cfg.use_tls = false; cfg.username = "u"; cfg.password = "bad";
const bool ok = conn.start(cfg, [](const std::string&, const std::string&){}, [](bool,bool){});
CHECK_FALSE(ok);
CHECK(conn.last_connack_rc() == 5);
// worker must have stopped itself (rc 5 is terminal) — give it a moment
for (int i = 0; i < 100 && conn.is_running(); ++i)
std::this_thread::sleep_for(std::chrono::milliseconds(10));
CHECK_FALSE(conn.is_running());
// and it must NOT have hammered the broker with retries
std::this_thread::sleep_for(std::chrono::milliseconds(200));
CHECK(broker.connect_count() <= 2);
conn.stop();
}
// --- MQTT-over-WebSocket stream reassembly. The OrcaSonar /mqtt proxy writes each
// 8 KiB TCP read as its own WebSocket message, so a packet larger than one read
// (e.g. a files.list reply) spans several messages; and several small packets can
// arrive in one. The receiver must reassemble the MQTT stream, not assume one
// packet per message.
TEST_CASE("OrcaMqtt drain_mqtt_packets reassembles across message boundaries", "[OrcaMqtt]") {
const std::vector<std::uint8_t> a = OrcaMqttConnection::make_publish_packet("device/a/report", R"({"n":1})");
const std::vector<std::uint8_t> b = OrcaMqttConnection::make_publish_packet("device/a/report", R"({"n":2})");
const std::string pa(a.begin(), a.end());
const std::string pb(b.begin(), b.end());
// A partial packet yields nothing and is retained verbatim.
std::string stream = pa.substr(0, pa.size() / 2);
std::vector<std::string> packets;
OrcaMqttConnection::drain_mqtt_packets(stream, packets);
CHECK(packets.empty());
CHECK(stream == pa.substr(0, pa.size() / 2));
// Completing the packet yields it and drains the stream.
stream += pa.substr(pa.size() / 2);
OrcaMqttConnection::drain_mqtt_packets(stream, packets);
REQUIRE(packets.size() == 1);
CHECK(packets[0] == pa);
CHECK(stream.empty());
// Two packets coalesced in one buffer are both extracted, in order.
stream = pa + pb;
packets.clear();
OrcaMqttConnection::drain_mqtt_packets(stream, packets);
REQUIRE(packets.size() == 2);
CHECK(packets[0] == pa);
CHECK(packets[1] == pb);
CHECK(stream.empty());
// A trailing partial packet is kept for the next call.
stream = pa + pb.substr(0, 2);
packets.clear();
CHECK(OrcaMqttConnection::drain_mqtt_packets(stream, packets));
REQUIRE(packets.size() == 1);
CHECK(packets[0] == pa);
CHECK(stream == pb.substr(0, 2));
// A multibyte remaining length (payload > 127 bytes) is parsed correctly.
const std::string big(300, 'x');
const auto big_packet = OrcaMqttConnection::make_publish_packet("device/a/report", big);
const std::string pb_big(big_packet.begin(), big_packet.end());
stream = pb_big;
packets.clear();
REQUIRE(OrcaMqttConnection::drain_mqtt_packets(stream, packets));
REQUIRE(packets.size() == 1);
CHECK(packets[0] == pb_big);
CHECK(stream.empty());
// A malformed header is reported so the caller can drop the connection.
const std::vector<std::uint8_t> malformed_bytes{0x30, 0x80, 0x80, 0x80, 0x80};
stream.assign(malformed_bytes.begin(), malformed_bytes.end());
packets.clear();
CHECK_FALSE(OrcaMqttConnection::drain_mqtt_packets(stream, packets));
CHECK(packets.empty());
CHECK(stream.empty());
}
TEST_CASE("OrcaMqtt delivers a report split across WebSocket messages", "[OrcaMqtt]") {
orca_mqtt_test::MockBroker broker;
// Callback state is declared before `conn` so it outlives the worker: if a
// REQUIRE fails, `conn`'s destructor still runs before this state is destroyed.
std::mutex m;
std::condition_variable cv;
std::vector<std::string> got;
OrcaMqttConnection conn;
OrcaMqttConnection::Config cfg;
cfg.url = broker.ws_url();
cfg.use_tls = false;
cfg.username = "u";
cfg.password = "p";
REQUIRE(conn.start(cfg,
[&](const std::string&, const std::string& p) {
{ std::lock_guard<std::mutex> l(m); got.push_back(p); }
cv.notify_all();
},
[](bool, bool) {}));
REQUIRE(conn.subscribe("dev-1"));
REQUIRE(wait_subscribed(broker, "device/dev-1/report"));
const std::string payload = R"({"print":{"command":"push_status","sequence_id":"30001","result":"success"}})";
const auto packet = OrcaMqttConnection::make_publish_packet("device/dev-1/report", payload);
const std::string bytes(packet.begin(), packet.end());
const std::size_t third = bytes.size() / 3;
broker.push_raw(bytes.substr(0, third));
broker.push_raw(bytes.substr(third, third));
broker.push_raw(bytes.substr(2 * third));
bool delivered = false;
{
std::unique_lock<std::mutex> l(m);
delivered = cv.wait_for(l, std::chrono::seconds(3), [&] { return !got.empty(); });
}
REQUIRE(delivered);
{
std::lock_guard<std::mutex> l(m);
REQUIRE(got.size() == 1);
CHECK(got.front().find("push_status") != std::string::npos);
}
conn.stop();
}
TEST_CASE("OrcaMqtt delivers two reports coalesced into one WebSocket message", "[OrcaMqtt]") {
orca_mqtt_test::MockBroker broker;
// Callback state is declared before `conn` so it outlives the worker: if a
// REQUIRE fails, `conn`'s destructor still runs before this state is destroyed.
std::mutex m;
std::condition_variable cv;
std::vector<std::string> got;
OrcaMqttConnection conn;
OrcaMqttConnection::Config cfg;
cfg.url = broker.ws_url();
cfg.use_tls = false;
cfg.username = "u";
cfg.password = "p";
REQUIRE(conn.start(cfg,
[&](const std::string&, const std::string& p) {
{ std::lock_guard<std::mutex> l(m); got.push_back(p); }
cv.notify_all();
},
[](bool, bool) {}));
REQUIRE(conn.subscribe("dev-1"));
REQUIRE(wait_subscribed(broker, "device/dev-1/report"));
const auto p1 = OrcaMqttConnection::make_publish_packet("device/dev-1/report", R"({"print":{"command":"push_status","sequence_id":"31001"}})");
const auto p2 = OrcaMqttConnection::make_publish_packet("device/dev-1/report", R"({"print":{"command":"push_status","sequence_id":"31002"}})");
const std::string both(std::string(p1.begin(), p1.end()) + std::string(p2.begin(), p2.end()));
broker.push_raw(both);
bool two = false;
{
std::unique_lock<std::mutex> l(m);
two = cv.wait_for(l, std::chrono::seconds(3), [&] { return got.size() >= 2; });
}
REQUIRE(two);
{
std::lock_guard<std::mutex> l(m);
CHECK(got[0].find("31001") != std::string::npos);
CHECK(got[1].find("31002") != std::string::npos);
}
conn.stop();
}
TEST_CASE("OrcaMqtt stop() unblocks a stalled handshake", "[OrcaMqtt]") {
// A peer that completes the WebSocket upgrade and CONNECT but never answers:
// the worker blocks in the synchronous CONNACK read. stop() must shut the
// socket down and join it promptly rather than wait out the 10s deadline.
orca_mqtt_test::MockBroker broker(/*refuse_auth=*/false, /*stall_connack=*/true);
OrcaMqttConnection conn;
OrcaMqttConnection::Config cfg;
cfg.url = broker.ws_url();
cfg.use_tls = false;
cfg.username = "u";
cfg.password = "p";
// start() waits up to 10s for CONNACK, so run it off the test thread.
std::thread starter([&] { conn.start(cfg, [](const std::string&, const std::string&) {}, [](bool, bool) {}); });
struct FinalJoin {
std::thread& thread;
~FinalJoin() {
if (thread.joinable())
thread.join();
}
} final_join{starter};
// Wait until the broker has taken the CONNECT: the worker is now in the read.
for (int i = 0; i < 300 && broker.connect_count() == 0; ++i)
std::this_thread::sleep_for(std::chrono::milliseconds(10));
REQUIRE(broker.connect_count() > 0);
const auto started_at = std::chrono::steady_clock::now();
conn.stop();
const auto elapsed = std::chrono::steady_clock::now() - started_at;
CHECK(elapsed < std::chrono::seconds(3));
CHECK_FALSE(conn.is_running());
}
@@ -0,0 +1,567 @@
#include <catch2/catch_test_macros.hpp>
#include <catch2/matchers/catch_matchers_floating_point.hpp>
#include <libslic3r/ProjectTask.hpp>
#include <slic3r/GUI/FilamentMappingUtils.hpp>
#include <slic3r/Utils/IPrinterAgent.hpp>
#include <slic3r/Utils/OrcaCloudServiceAgent.hpp>
#include <slic3r/Utils/OrcaPrinterAgent.hpp>
#include "catch2/matchers/catch_matchers.hpp"
#include "orca_mqtt_mock_broker.hpp"
#include <chrono>
#include <functional>
#include <memory>
#include <slic3r/Utils/bambu_networking.hpp>
#include <string>
#include <thread>
#include <utility>
#include <vector>
using Slic3r::OrcaPrinterAgent;
namespace {
// Probe exposes the protected internals the tests drive.
struct Probe : OrcaPrinterAgent {
using OrcaPrinterAgent::OrcaPrinterAgent;
using OrcaPrinterAgent::deliver_to_sink;
using OrcaPrinterAgent::parse_lan_endpoint;
using OrcaPrinterAgent::make_lan_client_id;
using OrcaPrinterAgent::lan_connection_target;
using OrcaPrinterAgent::build_filament_mapping;
using OrcaPrinterAgent::build_gcode_file_payload;
using OrcaPrinterAgent::prepare_outgoing_request;
using OrcaPrinterAgent::parse_files_list_reply;
using OrcaPrinterAgent::parse_files_metadata_reply;
using OrcaPrinterAgent::try_consume_files_reply;
};
}
TEST_CASE("OrcaPrinterAgent forwards a status payload to on_message_fn", "[OrcaPrinterAgent]") {
Probe agent("/tmp");
std::string got_id, got_payload;
agent.set_on_message_fn([&](std::string id, std::string p){ got_id = std::move(id); got_payload = std::move(p); });
agent.deliver_to_sink("dev-1", R"({"print":{"command":"push_status"}})", /*local=*/false);
CHECK(got_id == "dev-1");
CHECK(got_payload.find("push_status") != std::string::npos);
}
TEST_CASE("OrcaPrinterAgent stamps the get_capabilities nozzle diameter onto push_status frames", "[OrcaPrinterAgent]") {
Probe agent("/tmp");
std::string last_payload;
agent.set_on_message_fn([&](std::string, std::string p){ last_payload = std::move(p); });
// Before any capabilities reply, a push_status frame is forwarded untouched.
agent.deliver_to_sink("dev-1", R"({"print":{"command":"push_status","mc_percent":10}})", /*local=*/false);
CHECK(last_payload.find("nozzle_diameter") == std::string::npos);
// The get_capabilities reply is forwarded verbatim; its topology nozzle diameter
// is cached for the device.
agent.deliver_to_sink(
"dev-1",
R"({"info":{"command":"get_capabilities","capabilities":{"topology":{"tools":[{"id":"T0","nozzle":{"diameter_mm":0.4}}]}}}})",
/*local=*/false);
CHECK(last_payload.find("\"command\":\"get_capabilities\"") != std::string::npos);
CHECK(last_payload.find("\"print\"") == std::string::npos);
// Later push_status frames for that device get the cached diameter plus a neutral
// nozzle_type, so MachineObject::parse_json's legacy nozzle parser can run.
agent.deliver_to_sink("dev-1", R"({"print":{"command":"push_status","mc_percent":20}})", /*local=*/false);
CHECK(last_payload.find("\"nozzle_diameter\":0.4") != std::string::npos);
CHECK(last_payload.find("\"nozzle_type\":\"N/A\"") != std::string::npos);
// A different device is unaffected.
agent.deliver_to_sink("dev-2", R"({"print":{"command":"push_status"}})", /*local=*/false);
CHECK(last_payload.find("nozzle_diameter") == std::string::npos);
// A frame that already carries real nozzle data is not overridden.
agent.deliver_to_sink("dev-1", R"({"print":{"command":"push_status","nozzle_diameter":0.6}})", /*local=*/false);
CHECK(last_payload.find("\"nozzle_diameter\":0.6") != std::string::npos);
CHECK(last_payload.find("N/A") == std::string::npos);
}
TEST_CASE("OrcaPrinterAgent owns connector capabilities and fails closed for AMS commands", "[OrcaPrinterAgent]") {
Probe agent("/tmp");
agent.deliver_to_sink("orca-caps", R"({
"info": {
"command": "get_capabilities",
"supported_features": {"fms": true, "filament_slots": true, "filament_mapping": true},
"supported_commands": ["print.ams_get_rfid"],
"capabilities": {
"protocol": {
"features": {"filament_mapping": true},
"supported_commands": ["print.ams_change_filament"]
}
}
}
})", false);
CHECK(agent.supports_feature("orca-caps", "filament_mapping"));
CHECK(agent.supports_command("orca-caps", "print.ams_filament_setting"));
CHECK(agent.supports_command("orca-caps", "print.ams_change_filament"));
CHECK(agent.supports_command("orca-caps", "print.ams_get_rfid"));
CHECK_FALSE(agent.supports_command("orca-caps", "print.ams_control"));
agent.deliver_to_sink("orca-caps", R"({
"info": {
"command": "get_capabilities",
"supported_features": {"fms": false, "filament_slots": false, "filament_mapping": false},
"supported_commands": ["print.ams_control"],
"capabilities": {"protocol": {"features": {}, "supported_commands": []}}
}
})", false);
CHECK_FALSE(agent.supports_feature("orca-caps", "filament_mapping"));
CHECK_FALSE(agent.supports_command("orca-caps", "print.ams_filament_setting"));
CHECK_FALSE(agent.supports_command("orca-caps", "print.ams_change_filament"));
CHECK_FALSE(agent.supports_command("orca-caps", "print.ams_control"));
CHECK_FALSE(agent.supports_command("orca-caps", "print.ams_calibrate"));
CHECK(agent.supports_command("orca-caps", "print.gcode_file"));
}
TEST_CASE("OrcaPrinterAgent clears cached capabilities when a device is unbound", "[OrcaPrinterAgent]") {
Probe agent("/tmp");
agent.deliver_to_sink("orca-forget", R"({
"info": {
"command": "get_capabilities",
"supported_features": {"filament_slots": true},
"supported_commands": [],
"capabilities": {"protocol": {"features": {}, "supported_commands": []}}
}
})", false);
REQUIRE(agent.supports_command("orca-forget", "print.ams_filament_setting"));
agent.unbind("orca-forget");
CHECK_FALSE(agent.supports_command("orca-forget", "print.ams_filament_setting"));
}
TEST_CASE("OrcaPrinterAgent removes setting_id from AMS metadata and gates the request", "[OrcaPrinterAgent]") {
Probe agent("/tmp");
agent.deliver_to_sink("orca-ams-write", R"({
"info": {
"command": "get_capabilities",
"supported_features": {"fms": false, "filament_slots": true},
"supported_commands": [],
"capabilities": {"protocol": {"features": {"filament_slots": true}, "supported_commands": []}}
}
})", false);
const std::string request = R"({"print":{"command":"ams_filament_setting","sequence_id":"9","tray_info_idx":"GFL99","setting_id":"preset-setting"}})";
std::string command;
std::string prepared;
CHECK(agent.prepare_outgoing_request("orca-ams-write", request, command, prepared) == BAMBU_NETWORK_SUCCESS);
CHECK(command == "print.ams_filament_setting");
const nlohmann::json parsed = nlohmann::json::parse(prepared);
CHECK_FALSE(parsed["print"].contains("setting_id"));
CHECK(parsed["print"]["tray_info_idx"] == "GFL99");
agent.deliver_to_sink("orca-ams-write", R"({
"info": {
"command": "get_capabilities",
"supported_features": {"filament_slots": false},
"capabilities": {"protocol": {"features": {"filament_slots": false}}}
}
})", false);
CHECK(agent.prepare_outgoing_request("orca-ams-write", request, command, prepared) == ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED);
}
TEST_CASE("OrcaPrinterAgent::parse_lan_endpoint", "[OrcaPrinterAgent]") {
std::string h, p;
REQUIRE(Probe::parse_lan_endpoint("192.168.1.9", h, p));
CHECK(h == "192.168.1.9"); CHECK(p == "8280");
REQUIRE(Probe::parse_lan_endpoint("http://host.local:9000/x", h, p));
CHECK(h == "host.local"); CHECK(p == "9000");
CHECK_FALSE(Probe::parse_lan_endpoint("", h, p));
}
TEST_CASE("OrcaPrinterAgent::make_lan_client_id is stable and prefixed", "[OrcaPrinterAgent]") {
const auto a = Probe::make_lan_client_id("dev-1");
const auto b = Probe::make_lan_client_id("dev-1");
CHECK(a == b); // drawn once per process
CHECK(a.rfind("orcaslicer-lan-dev-1-", 0) == 0);
}
TEST_CASE("OrcaPrinterAgent::parse_files_list_reply normalizes the MQTT entry fields", "[OrcaPrinterAgent]") {
const std::vector<Slic3r::PrinterFileEntry> files = Probe::parse_files_list_reply(R"({
"files": {
"command": "list",
"sequence_id": "70001",
"result": "success",
"entries": [
{"name": "foo.gcode", "path": "sub/foo.gcode", "is_dir": false, "size": 1234, "modified": 1700000000.75},
{"name": "bar.gcode", "path": "bar.gcode", "is_dir": false, "size": 7, "modified": 42}
]
}
})");
REQUIRE(files.size() == 2);
CHECK(files[0].path == "sub/foo.gcode");
CHECK(files[0].name == "foo.gcode");
CHECK(files[0].size == 1234);
CHECK(files[0].modified == 1700000000); // fractional seconds truncate
CHECK(files[1].path == "bar.gcode");
CHECK(files[1].name == "bar.gcode");
CHECK(files[1].size == 7);
CHECK(files[1].modified == 42);
}
TEST_CASE("OrcaPrinterAgent::parse_files_list_reply falls back to the name when path is absent", "[OrcaPrinterAgent]") {
const std::vector<Slic3r::PrinterFileEntry> files =
Probe::parse_files_list_reply(R"({"files": {"result": "success", "entries": [{"name": "solo.gcode"}]}})");
REQUIRE(files.size() == 1);
CHECK(files[0].path == "solo.gcode");
CHECK(files[0].name == "solo.gcode");
CHECK(files[0].size == 0);
CHECK(files[0].modified == 0);
}
TEST_CASE("OrcaPrinterAgent::parse_files_list_reply skips directories and unusable entries", "[OrcaPrinterAgent]") {
const std::vector<Slic3r::PrinterFileEntry> files = Probe::parse_files_list_reply(R"({
"files": {
"result": "success",
"entries": [
{"name": "sub", "path": "sub", "is_dir": true},
{"path": "no-name.gcode"},
{"name": ""},
"not-an-object",
{"name": "kept.gcode", "path": "kept.gcode"}
]
}
})");
REQUIRE(files.size() == 1);
CHECK(files[0].name == "kept.gcode");
CHECK(files[0].path == "kept.gcode");
}
TEST_CASE("OrcaPrinterAgent::parse_files_list_reply rejects a malformed or empty reply", "[OrcaPrinterAgent]") {
CHECK(Probe::parse_files_list_reply("not json").empty());
CHECK(Probe::parse_files_list_reply(R"({"result": []})").empty());
CHECK(Probe::parse_files_list_reply(R"({"files": {"entries": "nope"}})").empty());
}
TEST_CASE("OrcaPrinterAgent::parse_files_list_reply tolerates a non-boolean is_dir", "[OrcaPrinterAgent]") {
const std::vector<Slic3r::PrinterFileEntry> files =
Probe::parse_files_list_reply(R"({"files": {"result": "success", "entries": [{"name": "x.gcode", "is_dir": "yes"}]}})");
REQUIRE(files.size() == 1);
CHECK(files[0].name == "x.gcode");
}
TEST_CASE("OrcaPrinterAgent::try_consume_files_reply ignores reports it did not request", "[OrcaPrinterAgent]") {
Probe agent("/tmp");
CHECK_FALSE(agent.try_consume_files_reply("dev-1", R"({"print": {"command": "push_status"}})"));
CHECK_FALSE(agent.try_consume_files_reply("dev-1", R"({"files": {"command": "list"}})")); // no sequence_id
CHECK_FALSE(agent.try_consume_files_reply("dev-1", R"({"files": {"command": "list", "sequence_id": "999"}})")); // not pending
}
TEST_CASE("OrcaPrinterAgent::parse_files_metadata_reply parses the metadata fields", "[OrcaPrinterAgent]") {
using Catch::Matchers::WithinAbs;
const Slic3r::PrinterFileMetadata meta = Probe::parse_files_metadata_reply(R"({
"files": {
"command": "metadata",
"sequence_id": "70002",
"result": "success",
"path": "sub/foo.gcode",
"size": 1234,
"modified": 1700000000.5,
"estimated_time": 3725,
"filament_total": 10500.5,
"filament_weight_total": 31.6
}
})");
CHECK(meta.estimated_time == 3725);
CHECK_THAT(meta.filament_total, WithinAbs(10500.5, 1e-9));
CHECK_THAT(meta.filament_weight, WithinAbs(31.6, 1e-9));
}
TEST_CASE("OrcaPrinterAgent::parse_files_metadata_reply defaults missing fields to zero", "[OrcaPrinterAgent]") {
using Catch::Matchers::WithinAbs;
const Slic3r::PrinterFileMetadata meta = Probe::parse_files_metadata_reply(R"({"files": {"path": "foo.gcode"}})");
CHECK(meta.estimated_time == 0);
CHECK_THAT(meta.filament_total, WithinAbs(0.0, 1e-12));
CHECK_THAT(meta.filament_weight, WithinAbs(0.0, 1e-12));
}
TEST_CASE("OrcaPrinterAgent::parse_files_metadata_reply yields zeros for a malformed reply", "[OrcaPrinterAgent]") {
using Catch::Matchers::WithinAbs;
const Slic3r::PrinterFileMetadata malformed = Probe::parse_files_metadata_reply("not json");
CHECK(malformed.estimated_time == 0);
CHECK_THAT(malformed.filament_total, WithinAbs(0.0, 1e-12));
CHECK_THAT(malformed.filament_weight, WithinAbs(0.0, 1e-12));
const Slic3r::PrinterFileMetadata wrong_type = Probe::parse_files_metadata_reply(R"({"files": "nope"})");
CHECK(wrong_type.estimated_time == 0);
CHECK_THAT(wrong_type.filament_total, WithinAbs(0.0, 1e-12));
}
TEST_CASE("filament mapping is keyed by the ams_mapping2 array position", "[OrcaPrinterAgent]") {
const nlohmann::json mapping = Probe::build_filament_mapping(
R"([{"ams_id":1,"slot_id":5},{"ams_id":255,"slot_id":255},{"ams_id":255,"slot_id":0}])");
REQUIRE(mapping.is_array());
REQUIRE(mapping.size() == 2);
CHECK(mapping[0]["filament_index"] == 0);
CHECK(mapping[0]["ams_id"] == 1);
CHECK(mapping[0]["slot_id"] == 5);
// The unmatched middle entry is dropped; the third entry keeps index 2.
CHECK(mapping[1]["filament_index"] == 2);
CHECK(mapping[1]["ams_id"] == 255);
CHECK(mapping[1]["slot_id"] == 0);
}
// Index-correlation merge gate (plan PR 3). `ams_mapping2` is built one entry
// per logical filament, so its array position is the identifier the generated
// G-code toolchange passes to the Klipper macro (`next_filament_id`). The
// serializer must key `filament_index` by that position and must never
// re-densify after dropping unused/sentinel entries, or a used filament would
// be aimed at the wrong lane. This test covers the plan's matrix: preset order
// differing from used order, a middle filament unused, and external slots.
TEST_CASE("filament mapping index correlates with the ams_mapping2 position", "[OrcaPrinterAgent]") {
// Positions 0..4. Used filaments are 0, 2 and 4; 1 and 3 are unused.
const nlohmann::json mapping = Probe::build_filament_mapping(
R"([{"ams_id":0,"slot_id":1},{"ams_id":255,"slot_id":255},{"ams_id":2,"slot_id":3},{"ams_id":255,"slot_id":255},{"ams_id":255,"slot_id":0}])");
REQUIRE(mapping.size() == 3);
CHECK(mapping[0]["filament_index"] == 0);
CHECK(mapping[1]["filament_index"] == 2);
CHECK(mapping[2]["filament_index"] == 4); // external slot keeps its position
CHECK(mapping[2]["ams_id"] == 255);
CHECK(mapping[2]["slot_id"] == 0);
// No re-densification: a used filament after a dropped sentinel keeps its
// original logical index.
for (const auto& entry : mapping)
CHECK(entry.contains("filament_index"));
}
TEST_CASE("filament mapping is omitted when nothing remains", "[OrcaPrinterAgent]") {
CHECK(Probe::build_filament_mapping("").empty());
CHECK(Probe::build_filament_mapping(R"([{"ams_id":255,"slot_id":255}])").empty());
CHECK(Probe::build_filament_mapping("not json").empty());
CHECK(Probe::build_filament_mapping(R"({"ams_id":1,"slot_id":0})").empty()); // not an array
}
// The GUI capability gate and the agent serializer must classify the same
// entries as engaged. External slots ({255,0}/{254,0}) are normalized as-is, so
// they engage; only the {255,255} unmatched sentinel is dropped. A mismatch lets
// an entry past the GUI and refused late with a generic publish error.
TEST_CASE("the GUI mapping gate engages exactly the entries the serializer sends", "[OrcaPrinterAgent]") {
using Slic3r::GUI::has_engaged_filament_mapping;
CHECK_FALSE(has_engaged_filament_mapping(""));
CHECK_FALSE(has_engaged_filament_mapping("[]"));
CHECK_FALSE(has_engaged_filament_mapping("not json"));
CHECK_FALSE(has_engaged_filament_mapping(R"([{"ams_id":255,"slot_id":255}])"));
CHECK(has_engaged_filament_mapping(R"([{"ams_id":255,"slot_id":0}])")); // external main
CHECK(has_engaged_filament_mapping(R"([{"ams_id":254,"slot_id":0}])")); // external deputy
CHECK(has_engaged_filament_mapping(R"([{"ams_id":0,"slot_id":0}])")); // box slot
CHECK(has_engaged_filament_mapping(R"([{"ams_id":255,"slot_id":255},{"ams_id":1,"slot_id":2}])"));
for (const char* s : {"", "[]", "not json", R"([{"ams_id":255,"slot_id":255}])",
R"([{"ams_id":255,"slot_id":0}])", R"([{"ams_id":254,"slot_id":0}])",
R"([{"ams_id":0,"slot_id":0}])",
R"([{"ams_id":255,"slot_id":255},{"ams_id":1,"slot_id":2}])"}) {
CHECK(has_engaged_filament_mapping(s) == !Probe::build_filament_mapping(s).empty());
}
}
// The used-unmapped refusal reads m_ams_mapping_result: an entry with no target carries
// empty ams_id/slot_id, while an external-spool assignment (ams_id 255/254) is a real
// target. A wholly unmapped print reports no target and falls to the existing send flow.
TEST_CASE("used-filament targets split mapped from unmapped", "[OrcaPrinterAgent]") {
using Slic3r::GUI::has_any_mapped_target;
using Slic3r::GUI::has_used_filament_without_target;
Slic3r::FilamentInfo box; box.ams_id = "0"; box.slot_id = "1"; // box slot
Slic3r::FilamentInfo external; external.ams_id = "255"; external.slot_id = "0"; // external spool
Slic3r::FilamentInfo unmapped; // no target
CHECK_FALSE(has_used_filament_without_target({box, external}));
CHECK(has_used_filament_without_target({box, unmapped}));
CHECK(has_used_filament_without_target({external, unmapped}));
CHECK(has_used_filament_without_target({unmapped})); // wholly unmapped: all-invalid flow, not this refusal
CHECK(has_any_mapped_target({box, unmapped}));
CHECK(has_any_mapped_target({external, unmapped}));
CHECK_FALSE(has_any_mapped_target({unmapped}));
}
// A device with no AMS units has one source, the external spool. OrcaSlicer's
// auto-mapping force-selects it for every filament; that is not a lane choice, so it
// must not gate the print or reach print.gcode_file.
TEST_CASE("a no-AMS device drops the forced external-spool selection", "[OrcaPrinterAgent]") {
using Slic3r::GUI::drop_forced_external_selection;
using Slic3r::GUI::has_engaged_filament_mapping;
std::string external_only = R"([{"ams_id":255,"slot_id":0}])";
drop_forced_external_selection(/*device_has_ams=*/false, external_only);
CHECK(external_only.empty());
CHECK_FALSE(has_engaged_filament_mapping(external_only));
CHECK(Probe::build_filament_mapping(external_only).empty());
std::string box_mapping = R"([{"ams_id":0,"slot_id":2}])";
drop_forced_external_selection(/*device_has_ams=*/true, box_mapping);
CHECK(box_mapping == R"([{"ams_id":0,"slot_id":2}])");
CHECK(has_engaged_filament_mapping(box_mapping));
}
// An empty mapping must leave the gcode_file payload byte-identical to today:
// exactly command, sequence_id and param, with no filament_mapping key.
TEST_CASE("gcode_file payload omits filament_mapping when the map is empty", "[OrcaPrinterAgent]") {
const nlohmann::json empty = Probe::build_gcode_file_payload("7", "job.gcode", nlohmann::json::array());
REQUIRE(empty.contains("print"));
CHECK(empty["print"].size() == 3);
CHECK(empty["print"]["command"] == "gcode_file");
CHECK(empty["print"]["sequence_id"] == "7");
CHECK(empty["print"]["param"] == "job.gcode");
CHECK_FALSE(empty["print"].contains("filament_mapping"));
const nlohmann::json mapping = Probe::build_filament_mapping(R"([{"ams_id":1,"slot_id":0},{"ams_id":255,"slot_id":255}])");
const nlohmann::json with = Probe::build_gcode_file_payload("8", "job.gcode", mapping);
REQUIRE(with["print"].contains("filament_mapping"));
REQUIRE(with["print"]["filament_mapping"].size() == 1);
CHECK(with["print"]["filament_mapping"][0]["filament_index"] == 0);
}
// The defensive gate: a mapped print is refused before anything is published when the
// connector never advertised filament_mapping. The GUI send gates make this visible first;
// this covers callers that bypass them (calibration, plugin). A sentinel-only mapping does
// not engage the gate and falls through to the normal publish path.
TEST_CASE("an engaged mapping is refused when the connector never advertised filament_mapping", "[OrcaPrinterAgent]") {
OrcaPrinterAgent agent("/tmp");
Slic3r::PrintParams params;
params.dev_id = "dev-no-mapping-cap";
params.dst_file = "/tmp/job.gcode";
params.ams_mapping2 = R"([{"ams_id":0,"slot_id":2}])";
CHECK(agent.start_sdcard_print(params, {}, {}) == ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED);
params.ams_mapping2 = R"([{"ams_id":255,"slot_id":255}])";
CHECK(agent.start_sdcard_print(params, {}, {}) == BAMBU_NETWORK_ERR_PRINT_LP_PUBLISH_MSG_FAILED);
}
// The FTP "send with record" transport does not exist on OrcaSonar. It must
// report a non-success result so PrintJob falls back to start_print() rather
// than treating a print that was never sent as successful.
TEST_CASE("start_local_print_with_record never reports silent success", "[OrcaPrinterAgent]") {
Probe agent("/tmp");
Slic3r::PrintParams params;
const int rc = agent.start_local_print_with_record(params, {}, {}, {});
CHECK(rc < 0);
}
TEST_CASE("connect_printer wires up a LAN Config", "[OrcaPrinterAgent][.integration]") {
Probe agent("/tmp");
Slic3r::PrinterConnectionParams params{
"dev-1", "10.255.255.1", "orcasonar", "code", "", false, ""
};
const int rc = agent.connect_printer(params);
CHECK(rc == BAMBU_NETWORK_SUCCESS);
CHECK(agent.lan_connection_target() == "ws://10.255.255.1:8280/mqtt");
CHECK(agent.get_user_selected_machine().empty()); // LAN path must not touch the cloud selection
agent.disconnect_printer();
}
TEST_CASE("post-connect sequence is subscribe then 4 requests in order", "[OrcaPrinterAgent]") {
struct SeqProbe : OrcaPrinterAgent {
using OrcaPrinterAgent::OrcaPrinterAgent;
std::vector<std::string> calls;
void emit_connect_sequence(const std::string& dev_id,
std::function<void(const std::string&)> /*sub*/,
std::function<void(const std::string&)> /*req*/) override {
OrcaPrinterAgent::emit_connect_sequence(dev_id,
[&](const std::string& id){ calls.push_back("sub:" + id); },
[&](const std::string& body){ calls.push_back(body); });
}
} probe("/tmp");
probe.run_connect_sequence_for_test("dev-1");
REQUIRE(probe.calls.size() == 5);
CHECK(probe.calls[0] == "sub:dev-1");
CHECK(probe.calls[1].find("\"pushing\"") != std::string::npos);
CHECK(probe.calls[1].find("\"start\"") != std::string::npos);
CHECK(probe.calls[2].find("pushall") != std::string::npos);
CHECK(probe.calls[3].find("get_version") != std::string::npos);
CHECK(probe.calls[4].find("get_capabilities") != std::string::npos);
for (auto& c : probe.calls)
if (auto pos = c.find("sequence_id"); pos != std::string::npos)
CHECK(c.substr(pos).find("\"2") != std::string::npos);
}
// Hidden: spawns the connect worker and attempts a real (failing) connect.
TEST_CASE("selecting a cloud printer configures the fleet socket", "[OrcaPrinterAgent][.integration]") {
auto cloud = std::make_shared<Slic3r::OrcaCloudServiceAgent>("/tmp");
cloud->set_api_base_url("api.example.com");
OrcaPrinterAgent agent("/tmp");
agent.set_cloud_agent(cloud);
agent.set_user_selected_machine("printer-uuid-1");
// The configure runs on the connect worker; poll rather than racing it.
std::string url;
for (int i = 0; i < 300; ++i) {
url = cloud->selected_printer_mqtt_url();
if (!url.empty()) break;
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
CHECK(url == "wss://api.example.com/api/v1/printers/mqtt");
agent.set_user_selected_machine(""); // selection changes do not tear down the fleet socket
CHECK(cloud->selected_printer_mqtt_url() == "wss://api.example.com/api/v1/printers/mqtt");
}
TEST_CASE("a stale-generation inbound message is dropped", "[OrcaPrinterAgent]") {
struct GenProbe : OrcaPrinterAgent {
using OrcaPrinterAgent::OrcaPrinterAgent;
using OrcaPrinterAgent::make_lan_message_handler; // expose for the test
};
GenProbe agent("/tmp");
int hits = 0;
agent.set_on_message_fn([&](std::string, std::string){ ++hits; });
auto handler_gen1 = agent.make_lan_message_handler(/*generation=*/1);
// m_lan_generation starts at 0; two bumps -> 2, so the epoch-1 handler is stale.
agent.bump_lan_generation_for_test();
agent.bump_lan_generation_for_test();
handler_gen1("dev-1", "{}"); // late callback from gen 1
CHECK(hits == 0);
}
TEST_CASE("connect_server does not start an MQTT socket", "[OrcaCloud]") {
auto cloud = std::make_shared<Slic3r::OrcaCloudServiceAgent>("/tmp");
cloud->set_api_base_url("127.0.0.1:1"); // no session -> connect_server short-circuits before any probe
cloud->connect_server();
REQUIRE(cloud->get_mqtt_connection() != nullptr); // created in the ctor
CHECK_FALSE(cloud->get_mqtt_connection()->is_running()); // never started
CHECK(cloud->selected_printer_mqtt_url().empty());
}
TEST_CASE("send_message* reject when there is no connection", "[OrcaPrinterAgent]") {
OrcaPrinterAgent agent("/tmp"); // no cloud agent, no LAN connection
CHECK(agent.send_message("d", "{}", 0, 0) == BAMBU_NETWORK_ERR_INVALID_HANDLE);
CHECK(agent.send_message_to_printer("d", "{}", 0, 0) == BAMBU_NETWORK_ERR_INVALID_HANDLE);
CHECK(agent.send_message("", "{}", 0, 0) == BAMBU_NETWORK_ERR_INVALID_HANDLE); // empty dev_id
}
TEST_CASE("send_message_to_printer publishes on the LAN connection", "[OrcaPrinterAgent]") {
orca_mqtt_test::MockBroker broker;
OrcaPrinterAgent agent("/tmp");
const auto ep = broker.host_port();
agent.connect_printer(Slic3r::PrinterConnectionParams{"dev-1", ep.first + ":" + ep.second, "orcasonar", "code", "", false, ""});
for (int i = 0; i < 150 && broker.connect_count() == 0; ++i)
std::this_thread::sleep_for(std::chrono::milliseconds(20));
REQUIRE(broker.connect_count() >= 1);
CHECK(agent.send_message_to_printer("dev-1", R"({"print":{"command":"pause","sequence_id":"20007"}})", 0, 0)
== BAMBU_NETWORK_SUCCESS);
// on_connected also publishes 4 requests; poll until "pause" specifically shows up.
bool saw_pause = false;
for (int i = 0; i < 150 && !saw_pause; ++i) {
for (const auto& r : broker.received_requests())
if (r.find("pause") != std::string::npos) { saw_pause = true; break; }
std::this_thread::sleep_for(std::chrono::milliseconds(20));
}
CHECK(saw_pause);
agent.disconnect_printer();
}
TEST_CASE("destroying an agent mid-connect does not hang or crash", "[OrcaPrinterAgent]") {
for (int i = 0; i < 20; ++i) {
auto agent = std::make_unique<OrcaPrinterAgent>("/tmp");
agent->connect_printer(Slic3r::PrinterConnectionParams{"dev-1", "127.0.0.1:1", "orcasonar", "code", "", false, ""}); // nothing listening: instant ECONNREFUSED
agent.reset(); // ~OrcaPrinterAgent must stop the conn, join the thread, and not hang/crash
}
SUCCEED();
}