Compare commits

..
Author SHA1 Message Date
Lam Wei Lun 67eee11e25 Merge main 2026-10-12 19:55:28 +08:00
Lam Wei Lun 253f90c975 Add the MQTT path for retrieving thumbnails for cloud mode on OrcaPrinterAgent 2026-10-12 18:50:36 +08:00
HanifKoh 0cec10748e Remove the /bot fix-tidy clang-tidy Fix Bot (#16400)
Its inline suggestions rarely apply: clang-tidy adds a missing include
to the include block at the top of the file, which a PR seldom touches,
and GitHub only accepts review suggestions on lines in the PR's diff.
2026-10-12 18:47:26 +08:00
HanifKoh c55f9c2251 Add a /bot fix-tidy Command That Suggests clang-tidy's Missing Includes (#16398)
* Add a /bot fix-tidy Command That Pushes clang-tidy's Missing Includes

A maintainer comments /bot fix-tidy on a PR. A read-only job runs
scripts/run_clang_tidy.sh --fix on the PR's merge commit and returns the
patch; a second job, which never runs the PR's code, accepts it only when
it adds nothing but #include lines to C++ files the PR already changes,
then commits it on the PR head and pushes with CLANG_TIDY_BOT_TOKEN.

* Post clang-tidy's Fix as Review Suggestions Instead of Pushing It

A failed clang-tidy check now runs once more with --fix and uploads the
patch as the clang-tidy-fix artifact; the check still fails. /bot
fix-tidy downloads that patch for the PR's latest commit, checks it adds
only includes to C++ files the PR changes, and posts one review: a
one-click suggestion for each include on a line the PR's diff shows, and
the whole patch with a git apply command. Nothing is pushed and no PR
code runs in the bot, so the PR's author can run it too and no token is
needed.

* Make the clang-tidy Fix in Its Own PR-Scoped Workflow on Request

clang_tidy.yml is back to main's version. clang_tidy_fix.yml runs on
every PR push but skips its job on the first attempt; /bot fix-tidy
re-runs it, so run_clang_tidy.sh --fix runs in the PR's own context
with a read-only token and the PR's cache, never main's, and the PR's
author can ask for it. When that re-run finishes, the bot posts the
patch as review suggestions as before. When the clang-tidy step fails,
the bot also comments asking the author to run the script or the
command, edits that comment on later failures, and deletes it once the
check passes.

* Give the clang-tidy Bot Write Access to PRs It Answers

* Tell /bot fix-tidy Users How to Start a Fix Run When There Is None to Re-Run
2026-10-12 18:31:03 +08:00
SoftFever d2675f15d1 Fix Qidi start G-code ignoring the selected plate type (#16372)
# Description

With "Support multi bed types" enabled on a Qidi printer, the
first-layer bed temperature was always taken from the Smooth PEI / High
Temp Plate setting, whichever plate type was selected.

The slicer computes the right value (`first_layer_bed_temperature` in
the G-code footer follows the selected plate), but the machine start
G-code of these presets hardcoded `[hot_plate_temp_initial_layer]`:

- `fdm_qidi_x3_common` (base of X-Max 3, X-Plus 3, X-Smart 3, X-Plus 4,
Q1 Pro)
- `Qidi Q1 Pro 0.4 nozzle`
- `Qidi X-CF Pro 0.4 nozzle`

They now use `[bed_temperature_initial_layer_single]`, which follows the
selected plate type, as most other profiles do. The Qidi bundle version
is bumped to `02.04.00.17`.

With "Support multi bed types" disabled, these printers use the default
plate (Smooth PEI / High Temp Plate), so the temperature is unchanged
there.

# Screenshots/Recordings/Graphs

N/A — profile change only.

## Tests

`python3 scripts/orca_profile_tool.py check` passes.

Sliced on Qidi X-Max 3 0.4 nozzle with "Support multi bed types"
enabled, `0.20mm Standard @Qidi XMax3`, `Generic PLA @Qidi`, with a
different bed temperature set for each plate type (first layer / other
layers).

Before, with first layer set to 50–55 °C: `M190 S54` for every plate
type.

After, a two-layer print:

| Plate type | Set (first / other) | `M190` at start | `M140` at layer 2
|
| --- | --- | --- | --- |
| Cool Plate SuperTack | 51 / 61 | 51 | 61 |
| Cool Plate | 52 / 62 | 52 | 62 |
| Textured Cool Plate | 53 / 63 | 53 | 63 |
| Engineering Plate | 54 / 64 | 54 | 64 |
| Smooth PEI / High Temp Plate | 55 / 65 | 55 | 65 |
| Textured PEI Plate | 56 / 66 | 56 | 66 |

🤖 Generated with [Claude Code](https://claude.com/claude-code)4
2026-10-12 17:14:22 +08:00
HanifKoh 5371c85362 Refresh Dark Mode Colours When the System Theme Differs at Startup (#16390)
On Linux and macOS the app follows the system theme: at startup it
overwrites the saved dark_color_mode with the system appearance. The label
colours and the cached dark mode flag were already set from the saved value
by then, and the later "changed" check reads the mode only after the
overwrite, so it never fires. For that session every dialog that maps its
colours through UpdateDlgDarkUI keeps the old mode, e.g. white dialogs on a
dark main window after switching the desktop to dark, or on a fresh install
on a dark desktop.

Refresh the label colours and the flag when the system theme differs from
the saved mode.
2026-10-12 17:09:05 +08:00
SoftFever a243941b72 Add favorite filaments to AMS filament dropdown (#13946)
# Description

## Summary:

Adds favorite filament support to the AMS material selection dropdown.

Users can now click a star icon next to a selectable filament in the AMS Materials Setting dialog to mark it as a favorite. Favorited filaments appear in a new `Favorite filaments` section at the top of the dropdown, while all compatible filaments remain available in the `All filaments` section.

This PR also adds generic right-side action icon support to Orca's custom dropdown widget:

- Adds optional action icons to `DropDown::Item`.
- Adds a new `EVT_DROPDOWN_ITEM_ACTION` event for clicks on the action icon.
- Relays the action event through `ComboBox`.
- Adds `ComboBox::RefreshDropdownIfOpen()` so callers can refresh an already-open dropdown without changing normal `ForceDropdownOpen()` behavior.

The AMS favorites implementation:

- Uses existing bundled star assets:
  - `score_star_dark` for non-favorited filaments
  - `score_star_light` for favorited filaments
- Stores favorite filament IDs in app config under `ams_favorite_filament_ids`.
- Stores stable filament IDs instead of display names
- Uses existing C-style string escaping helpers for persistence.
- Keeps Bambu/RFID/read-only slot behavior unchanged

No new external dependencies.

**Note:** `AMSMaterialsSetting` now uses Orca's custom `ComboBox` on macOS as well, because the favorite-star UI depends on the custom `DropDown` item renderer. I was able to test this on Windows, but macOS/Linux smoke testing is still needed and would be appreciated.

# Screenshots/Recordings:

https://github.com/user-attachments/assets/41e881be-d4de-4a7d-8f99-4297567adef2

<img width="494" height="36" alt="Screenshot 2026-05-30 103432" src="https://github.com/user-attachments/assets/e7c34ed3-db7a-4705-a1cd-4bfe7ac1616a" />


## Tests

Tested on Windows:

- Built OrcaSlicer locally
- Opened the Device tab with Bambu Lab printer with AMS
- Opened the AMS Materials Setting dialog from an editable AMS slot
- Confirmed clicking a star toggles favorite state.
- Star clicks do not select the filament row.
- Dropdown remains open after toggling a favorite.
- Confirmed favorited filaments appear under `Favorite filaments`.
- All compatible filaments remain visible under `All filaments`.
- Confirmed Filament still selected as before
- Confirmed favorites persist after closing and reopening the dialog.
- Confirmed favorites persist after restarting OrcaSlicer.
- Confirmed the favorite IDs are saved in `OrcaSlicer.conf` under `ams_favorite_filament_ids`.
- Bambu/RFID/read-only filament slots continue using the existing read-only display behavior.

Not tested:

- macOS
- Linux/Wayland

These should be tested, `ComboBox` behavior not tested on macOS.

[How to Download Pull Requests Artifacts for Testing](https://www.orcaslicer.com/wiki/how_to_download_pr_artifacts)
2026-10-12 16:37:29 +08:00
SoftFever 6a1a5d51e9 Show an outline star for filaments that are not favorites 2026-10-12 16:03:49 +08:00
HanifKoh e625845b34 Keep Snapshot Suppression for the Whole Object List Edit (#16388)
Five Plater::TakeSnapshot guards in GUI_ObjectList were unnamed temporaries, so snapshots were allowed again as soon as the snapshot was taken. Naming them keeps the suppression until the edit is done, as everywhere else.
2026-10-12 16:02:43 +08:00
SoftFever 01b340dee1 Fix regressions in the AMS filament favorites dropdown 2026-10-12 14:54:17 +08:00
HanifKoh 4c2a63e11e Copy Plate Pick and Pattern File Paths When Loading a G-code 3MF (#16387)
The importer assigned pick_file.empty() and pattern_bbox_file.empty(), a bool, to the string fields, storing a one-character string instead of the path.
2026-10-12 14:14:28 +08:00
HanifKoh f8cf34c55b Reset the Fan Mover Instead of Releasing It (#16386)
Clearing per-export state called unique_ptr::release(), which gives up ownership without deleting. Every export uses a new GCode, so the pointer is still empty there today and nothing leaks; reset() states the intent and would free the object if a GCode were ever reused.
2026-10-12 13:42:19 +08:00
SoftFever 00d11a7ba0 Merge branch 'main' into UI-changes 2026-10-12 12:17:21 +08:00
Boris SamorodovandClaude Opus 5.5 546b9c621d Fix Qidi start G-code ignoring the selected plate type
The start G-code of fdm_qidi_x3_common, Qidi Q1 Pro 0.4 nozzle and
Qidi X-CF Pro 0.4 nozzle heated the bed to hot_plate_temp_initial_layer,
so with "Support multi bed types" enabled every plate type got the
Smooth PEI / High Temp Plate temperature. Use
bed_temperature_initial_layer_single, which follows the selected plate.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 00:49:25 +02:00
Lam Wei Lun 7b717ffa7d Thumbnail display support for JPG/PNG directly 2026-10-09 15:13:24 +08:00
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
SoftFever a13089d357 Merge branch 'main' into UI-changes 2026-06-02 14:40:05 +08:00
KrishManan fb6c93963d Add AMS filament favorites 2026-05-30 17:45:03 +08:00
KrishManan 7688f7a16c Add dropdown item action icons 2026-05-30 17:45:03 +08:00
107 changed files with 10232 additions and 1003 deletions
+7
View File
@@ -131,6 +131,13 @@
<true/>
<key>NSHumanReadableCopyright</key>
<string>${MACOSX_BUNDLE_COPYRIGHT}</string>
<key>NSLocalNetworkUsageDescription</key>
<string>OrcaSlicer uses your local network to discover and connect to printers.</string>
<key>NSBonjourServices</key>
<array>
<string>_octoprint._tcp</string>
<string>_http._tcp</string>
</array>
<key>NSAppTransportSecurity</key>
<dict>
<!-- Disable App Transport Security. Resolves https://github.com/OrcaSlicer/OrcaSlicer/issues/791 -->
+44 -10
View File
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-11 16:21+0800\n"
"POT-Creation-Date: 2026-10-12 19:00+0800\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -3999,6 +3999,15 @@ msgstr ""
msgid "Go to Plate"
msgstr ""
msgid "Device"
msgstr ""
msgid "Device (Web)"
msgstr ""
msgid "Multi-device"
msgstr ""
msgid "AMS Materials Setting"
msgstr ""
@@ -4047,6 +4056,9 @@ msgstr ""
msgid "You need to select the material type and color first."
msgstr ""
msgid "The minimum temperature cannot be greater than the maximum temperature."
msgstr ""
#, possible-c-format, possible-boost-format
msgid "Please input a valid value (K in %.1f~%.1f)"
msgstr ""
@@ -4055,6 +4067,12 @@ msgstr ""
msgid "Please input a valid value (K in %.1f~%.1f, N in %.1f~%.1f)"
msgstr ""
msgid "Favorite filaments"
msgstr ""
msgid "All filaments"
msgstr ""
msgid ""
"The nozzle flow is not set. Please set the nozzle flow rate before editing the filament.\n"
"'Device -> Print parts'"
@@ -5985,18 +6003,9 @@ msgstr ""
msgid "Preview"
msgstr ""
msgid "Device"
msgstr ""
msgid "Multi-device"
msgstr ""
msgid "Project"
msgstr ""
msgid "Device (Web)"
msgstr ""
msgid "Yes"
msgstr ""
@@ -6670,6 +6679,22 @@ msgstr ""
msgid "File '%s' was lost! Please download it again."
msgstr ""
msgid "gcodes"
msgstr ""
msgid "Failed to delete the file from printer."
msgstr ""
#, possible-c-format, possible-boost-format
msgid "Do you want to print the file '%s' from printer?"
msgstr ""
msgid "Print file"
msgstr ""
msgid "Failed to start the print job."
msgstr ""
#, possible-c-format, possible-boost-format
msgid ""
"File: %s\n"
@@ -10342,6 +10367,12 @@ msgstr ""
msgid "Clean Up"
msgstr ""
msgid "AMS filament mapping is not available for this printer. Clear the AMS mapping before printing."
msgstr ""
msgid "A filament used by this print has no AMS mapping. Assign it before printing."
msgstr ""
msgid "Preparing print job"
msgstr ""
@@ -27207,6 +27238,9 @@ msgstr ""
msgid "An error occurred loading %s"
msgstr ""
msgid "/"
msgstr ""
#, possible-boost-format
msgid "Load %1% to %2%"
msgstr ""
+45 -14
View File
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-11 16:21+0800\n"
"POT-Creation-Date: 2026-10-12 19:00+0800\n"
"PO-Revision-Date: 2025-03-15 10:55+0100\n"
"Last-Translator: \n"
"Language-Team: \n"
@@ -4375,6 +4375,16 @@ msgstr "Projectes recents"
msgid "Go to Plate"
msgstr "Anar a la placa"
msgid "Device"
msgstr "Dispositiu"
# AI Translated
msgid "Device (Web)"
msgstr "Dispositiu (Web)"
msgid "Multi-device"
msgstr "Multidispositiu"
msgid "AMS Materials Setting"
msgstr "Configuració de materials AMS"
@@ -4423,6 +4433,9 @@ msgstr "Estàs segur que vols esborrar la informació del filament?"
msgid "You need to select the material type and color first."
msgstr "Primer heu de seleccionar el tipus de material i el color."
msgid "The minimum temperature cannot be greater than the maximum temperature."
msgstr ""
#, c-format, boost-format
msgid "Please input a valid value (K in %.1f~%.1f)"
msgstr "Introduïu un valor vàlid (K a %.1f~%.1f)"
@@ -4431,6 +4444,12 @@ msgstr "Introduïu un valor vàlid (K a %.1f~%.1f)"
msgid "Please input a valid value (K in %.1f~%.1f, N in %.1f~%.1f)"
msgstr "Introduïu un valor vàlid (K a %.1f~%.1f, N a %.1f~%.1f)"
msgid "Favorite filaments"
msgstr ""
msgid "All filaments"
msgstr ""
msgid ""
"The nozzle flow is not set. Please set the nozzle flow rate before editing the filament.\n"
"'Device -> Print parts'"
@@ -6506,19 +6525,9 @@ msgstr "Preparació"
msgid "Preview"
msgstr "Previsualització"
msgid "Device"
msgstr "Dispositiu"
msgid "Multi-device"
msgstr "Multidispositiu"
msgid "Project"
msgstr "Projecte"
# AI Translated
msgid "Device (Web)"
msgstr "Dispositiu (Web)"
msgid "Yes"
msgstr "Sí"
@@ -7225,6 +7234,22 @@ msgstr "El fitxer .gcode.3mf no conté dades de G-code. Lamineu-lo amb Orca Slic
msgid "File '%s' was lost! Please download it again."
msgstr "L'arxiu '%s' s'ha perdut! Si us plau, descarregueu-lo de nou."
msgid "gcodes"
msgstr ""
msgid "Failed to delete the file from printer."
msgstr ""
#, c-format, boost-format
msgid "Do you want to print the file '%s' from printer?"
msgstr ""
msgid "Print file"
msgstr ""
msgid "Failed to start the print job."
msgstr ""
#, c-format, boost-format
msgid ""
"File: %s\n"
@@ -11411,6 +11436,12 @@ msgstr "Cancel·lar el timelapse i imprimir"
msgid "Clean Up"
msgstr "Netejar"
msgid "AMS filament mapping is not available for this printer. Clear the AMS mapping before printing."
msgstr ""
msgid "A filament used by this print has no AMS mapping. Assign it before printing."
msgstr ""
msgid "Preparing print job"
msgstr "Preparant el treball d'impressió"
@@ -31518,6 +31549,9 @@ msgstr "Activar les eines de desenvolupament"
msgid "An error occurred loading %s"
msgstr "S'ha produït un error en carregar %s"
msgid "/"
msgstr "/"
# AI Translated
#, boost-format
msgid "Load %1% to %2%"
@@ -33585,9 +33619,6 @@ msgstr ""
#~ msgid "An error occurred loading "
#~ msgstr "S'ha produït un error en carregar "
#~ msgid "/"
#~ msgstr "/"
# AI Translated
#, c-format, boost-format
#~ msgid "Load %s to "
+46 -15
View File
@@ -3,7 +3,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Orca Slicer\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-11 16:21+0800\n"
"POT-Creation-Date: 2026-10-12 19:00+0800\n"
"PO-Revision-Date: \n"
"Last-Translator: Jakub Hencl\n"
"Language-Team: \n"
@@ -4331,6 +4331,16 @@ msgstr "Nedávné projekty"
msgid "Go to Plate"
msgstr "Přejít na desku"
msgid "Device"
msgstr "Zařízení"
# AI Translated
msgid "Device (Web)"
msgstr "Zařízení (Web)"
msgid "Multi-device"
msgstr "Více zařízení"
msgid "AMS Materials Setting"
msgstr "Nastavení materiálů AMS"
@@ -4381,6 +4391,9 @@ msgstr "Opravdu chcete vymazat informace o filamentu?"
msgid "You need to select the material type and color first."
msgstr "Nejprve musíte vybrat typ materiálu a barvu."
msgid "The minimum temperature cannot be greater than the maximum temperature."
msgstr ""
#, c-format, boost-format
msgid "Please input a valid value (K in %.1f~%.1f)"
msgstr "Zadejte platnou hodnotu (K v rozsahu %.1f~%.1f)"
@@ -4389,6 +4402,12 @@ msgstr "Zadejte platnou hodnotu (K v rozsahu %.1f~%.1f)"
msgid "Please input a valid value (K in %.1f~%.1f, N in %.1f~%.1f)"
msgstr "Zadejte platnou hodnotu (K v rozsahu %.1f~%.1f, N v rozsahu %.1f~%.1f)"
msgid "Favorite filaments"
msgstr ""
msgid "All filaments"
msgstr ""
msgid ""
"The nozzle flow is not set. Please set the nozzle flow rate before editing the filament.\n"
"'Device -> Print parts'"
@@ -6482,19 +6501,9 @@ msgstr "Připravit"
msgid "Preview"
msgstr "Náhled"
msgid "Device"
msgstr "Zařízení"
msgid "Multi-device"
msgstr "Více zařízení"
msgid "Project"
msgstr "Projekt"
# AI Translated
msgid "Device (Web)"
msgstr "Zařízení (Web)"
msgid "Yes"
msgstr "Ano"
@@ -7205,6 +7214,22 @@ msgstr "Soubor .gcode.3mf neobsahuje žádná G-code data. Prosím, rozřežte m
msgid "File '%s' was lost! Please download it again."
msgstr "Soubor '%s' byl ztracen! Stáhněte jej prosím znovu."
msgid "gcodes"
msgstr ""
msgid "Failed to delete the file from printer."
msgstr ""
#, c-format, boost-format
msgid "Do you want to print the file '%s' from printer?"
msgstr ""
msgid "Print file"
msgstr ""
msgid "Failed to start the print job."
msgstr ""
#, c-format, boost-format
msgid ""
"File: %s\n"
@@ -11380,6 +11405,12 @@ msgstr "Zrušit časosběr a tisknout"
msgid "Clean Up"
msgstr "Vyčistit"
msgid "AMS filament mapping is not available for this printer. Clear the AMS mapping before printing."
msgstr ""
msgid "A filament used by this print has no AMS mapping. Assign it before printing."
msgstr ""
msgid "Preparing print job"
msgstr "Příprava tiskové úlohy"
@@ -31532,6 +31563,10 @@ msgstr "Povolit vývojářské nástroje"
msgid "An error occurred loading %s"
msgstr "Při načítání %s došlo k chybě"
# AI Translated
msgid "/"
msgstr "/"
# AI Translated
#, boost-format
msgid "Load %1% to %2%"
@@ -33601,10 +33636,6 @@ msgstr ""
#~ msgid "An error occurred loading "
#~ msgstr "Při načítání došlo k chybě: "
# AI Translated
#~ msgid "/"
#~ msgstr "/"
# AI Translated
#, c-format, boost-format
#~ msgid "Load %s to "
+45 -14
View File
@@ -3,7 +3,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Orca Slicer\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-11 16:21+0800\n"
"POT-Creation-Date: 2026-10-12 19:00+0800\n"
"PO-Revision-Date: \n"
"Last-Translator: Heiko Liebscher <hliebschergmail.com>\n"
"Language-Team: \n"
@@ -4246,6 +4246,16 @@ msgstr "Zuletzt verwendete Projekte"
msgid "Go to Plate"
msgstr "Zur Druckplatte wechseln"
msgid "Device"
msgstr "Gerät"
# AI Translated
msgid "Device (Web)"
msgstr "Gerät (Web)"
msgid "Multi-device"
msgstr "Multi-Gerät"
msgid "AMS Materials Setting"
msgstr "AMS Material Einstellung"
@@ -4294,6 +4304,9 @@ msgstr "Sind Sie sicher, dass Sie die Filamentinformationen löschen möchten?"
msgid "You need to select the material type and color first."
msgstr "Sie müssen zuerst den Materialtyp und die Farbe auswählen."
msgid "The minimum temperature cannot be greater than the maximum temperature."
msgstr ""
#, c-format, boost-format
msgid "Please input a valid value (K in %.1f~%.1f)"
msgstr "Bitte geben Sie einen gültigen Wert ein (K in %.1f~%.1f)"
@@ -4302,6 +4315,12 @@ msgstr "Bitte geben Sie einen gültigen Wert ein (K in %.1f~%.1f)"
msgid "Please input a valid value (K in %.1f~%.1f, N in %.1f~%.1f)"
msgstr "Bitte geben Sie einen gültigen Wert ein (K in %.1f~%.1f, N in %.1f~%.1f)"
msgid "Favorite filaments"
msgstr ""
msgid "All filaments"
msgstr ""
msgid ""
"The nozzle flow is not set. Please set the nozzle flow rate before editing the filament.\n"
"'Device -> Print parts'"
@@ -6364,19 +6383,9 @@ msgstr "Vorbereiten"
msgid "Preview"
msgstr "Vorschau"
msgid "Device"
msgstr "Gerät"
msgid "Multi-device"
msgstr "Multi-Gerät"
msgid "Project"
msgstr "Projekt"
# AI Translated
msgid "Device (Web)"
msgstr "Gerät (Web)"
msgid "Yes"
msgstr "Ja"
@@ -7072,6 +7081,22 @@ msgstr "Die .gcode.3mf-Datei enthält keine G-Code-Daten. Bitte slicen Sie sie m
msgid "File '%s' was lost! Please download it again."
msgstr "Die Datei '%s' ist verloren gegangen! Bitte laden Sie diese erneut herunter."
msgid "gcodes"
msgstr ""
msgid "Failed to delete the file from printer."
msgstr ""
#, c-format, boost-format
msgid "Do you want to print the file '%s' from printer?"
msgstr ""
msgid "Print file"
msgstr ""
msgid "Failed to start the print job."
msgstr ""
#, c-format, boost-format
msgid ""
"File: %s\n"
@@ -11196,6 +11221,12 @@ msgstr "Zeitraffer abbrechen & Drucken"
msgid "Clean Up"
msgstr "Bereinigen"
msgid "AMS filament mapping is not available for this printer. Clear the AMS mapping before printing."
msgstr ""
msgid "A filament used by this print has no AMS mapping. Assign it before printing."
msgstr ""
msgid "Preparing print job"
msgstr "Druckauftrag vorbereiten"
@@ -30919,6 +30950,9 @@ msgstr "Entwicklertools aktivieren"
msgid "An error occurred loading %s"
msgstr "Fehler beim Laden von %s"
msgid "/"
msgstr "/"
# AI Translated
#, boost-format
msgid "Load %1% to %2%"
@@ -32980,9 +33014,6 @@ msgstr ""
#~ msgid "An error occurred loading "
#~ msgstr "Fehler beim Laden von "
#~ msgid "/"
#~ msgstr "/"
# AI Translated
#, c-format, boost-format
#~ msgid "Load %s to "
+44 -10
View File
@@ -3,7 +3,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Orca Slicer\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-11 16:21+0800\n"
"POT-Creation-Date: 2026-10-12 19:00+0800\n"
"PO-Revision-Date: 2026-06-17 15:44-0300\n"
"Last-Translator: Alexandre Folle de Menezes\n"
"Language-Team: \n"
@@ -3995,6 +3995,15 @@ msgstr ""
msgid "Go to Plate"
msgstr ""
msgid "Device"
msgstr ""
msgid "Device (Web)"
msgstr ""
msgid "Multi-device"
msgstr ""
msgid "AMS Materials Setting"
msgstr ""
@@ -4043,6 +4052,9 @@ msgstr ""
msgid "You need to select the material type and color first."
msgstr ""
msgid "The minimum temperature cannot be greater than the maximum temperature."
msgstr ""
#, c-format, boost-format
msgid "Please input a valid value (K in %.1f~%.1f)"
msgstr ""
@@ -4051,6 +4063,12 @@ msgstr ""
msgid "Please input a valid value (K in %.1f~%.1f, N in %.1f~%.1f)"
msgstr ""
msgid "Favorite filaments"
msgstr ""
msgid "All filaments"
msgstr ""
msgid ""
"The nozzle flow is not set. Please set the nozzle flow rate before editing the filament.\n"
"'Device -> Print parts'"
@@ -5981,18 +5999,9 @@ msgstr ""
msgid "Preview"
msgstr ""
msgid "Device"
msgstr ""
msgid "Multi-device"
msgstr ""
msgid "Project"
msgstr ""
msgid "Device (Web)"
msgstr ""
msgid "Yes"
msgstr ""
@@ -6666,6 +6675,22 @@ msgstr ""
msgid "File '%s' was lost! Please download it again."
msgstr ""
msgid "gcodes"
msgstr ""
msgid "Failed to delete the file from printer."
msgstr ""
#, c-format, boost-format
msgid "Do you want to print the file '%s' from printer?"
msgstr ""
msgid "Print file"
msgstr ""
msgid "Failed to start the print job."
msgstr ""
#, c-format, boost-format
msgid ""
"File: %s\n"
@@ -10338,6 +10363,12 @@ msgstr ""
msgid "Clean Up"
msgstr ""
msgid "AMS filament mapping is not available for this printer. Clear the AMS mapping before printing."
msgstr ""
msgid "A filament used by this print has no AMS mapping. Assign it before printing."
msgstr ""
msgid "Preparing print job"
msgstr ""
@@ -27203,6 +27234,9 @@ msgstr ""
msgid "An error occurred loading %s"
msgstr ""
msgid "/"
msgstr ""
#, boost-format
msgid "Load %1% to %2%"
msgstr ""
+45 -14
View File
@@ -3,7 +3,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Orca Slicer\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-11 16:21+0800\n"
"POT-Creation-Date: 2026-10-12 19:00+0800\n"
"PO-Revision-Date: \n"
"Last-Translator: Ian A. Bassi <>\n"
"Language-Team: \n"
@@ -4123,6 +4123,16 @@ msgstr "Proyectos recientes"
msgid "Go to Plate"
msgstr "Ir a la cama"
msgid "Device"
msgstr "Dispositivo"
# AI Translated
msgid "Device (Web)"
msgstr "Dispositivo (Web)"
msgid "Multi-device"
msgstr "Multi-dispositivo"
msgid "AMS Materials Setting"
msgstr "Ajustes de materiales AMS"
@@ -4173,6 +4183,9 @@ msgstr "¿Estás seguro que quieres limpiar la información de filamento?"
msgid "You need to select the material type and color first."
msgstr "Necesitas seleccionar el tipo y el color del material primero."
msgid "The minimum temperature cannot be greater than the maximum temperature."
msgstr ""
#, c-format, boost-format
msgid "Please input a valid value (K in %.1f~%.1f)"
msgstr "Por favor, introduzca un valor válido (K in %.1f~%.1f)"
@@ -4181,6 +4194,12 @@ msgstr "Por favor, introduzca un valor válido (K in %.1f~%.1f)"
msgid "Please input a valid value (K in %.1f~%.1f, N in %.1f~%.1f)"
msgstr "Introduzca un valor válido (K en %.1f~%.1f, N en %.1f~%.1f)"
msgid "Favorite filaments"
msgstr ""
msgid "All filaments"
msgstr ""
msgid ""
"The nozzle flow is not set. Please set the nozzle flow rate before editing the filament.\n"
"'Device -> Print parts'"
@@ -6219,19 +6238,9 @@ msgstr "Preparar"
msgid "Preview"
msgstr "Previsualización"
msgid "Device"
msgstr "Dispositivo"
msgid "Multi-device"
msgstr "Multi-dispositivo"
msgid "Project"
msgstr "Proyecto"
# AI Translated
msgid "Device (Web)"
msgstr "Dispositivo (Web)"
msgid "Yes"
msgstr "Sí"
@@ -6925,6 +6934,22 @@ msgstr "El archivo .gcode .3mf no contiene datos de G-Code. Por favor, lamine co
msgid "File '%s' was lost! Please download it again."
msgstr "¡El archivo '%s' se perdió!\" Por favor, vuelva a descargárlo."
msgid "gcodes"
msgstr ""
msgid "Failed to delete the file from printer."
msgstr ""
#, c-format, boost-format
msgid "Do you want to print the file '%s' from printer?"
msgstr ""
msgid "Print file"
msgstr ""
msgid "Failed to start the print job."
msgstr ""
#, c-format, boost-format
msgid ""
"File: %s\n"
@@ -10931,6 +10956,12 @@ msgstr "Cancelar timelapse e imprimir"
msgid "Clean Up"
msgstr "Limpiar"
msgid "AMS filament mapping is not available for this printer. Clear the AMS mapping before printing."
msgstr ""
msgid "A filament used by this print has no AMS mapping. Assign it before printing."
msgstr ""
msgid "Preparing print job"
msgstr "Preparando el trabajo de impresión"
@@ -30478,6 +30509,9 @@ msgstr "Activar herramientas de desarrollo"
msgid "An error occurred loading %s"
msgstr "Se produjo un error al cargar %s"
msgid "/"
msgstr "/"
# AI Translated
#, boost-format
msgid "Load %1% to %2%"
@@ -32535,9 +32569,6 @@ msgstr ""
#~ msgid "An error occurred loading "
#~ msgstr "Se produjo un error al cargar "
#~ msgid "/"
#~ msgstr "/"
# AI Translated
#, c-format, boost-format
#~ msgid "Load %s to "
+45 -14
View File
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-11 16:21+0800\n"
"POT-Creation-Date: 2026-10-12 19:00+0800\n"
"PO-Revision-Date: 2026-07-20 13:33+0200\n"
"Last-Translator: Manu Goiogana <mgoiogana@gmail.com>\n"
"Language-Team: \n"
@@ -4165,6 +4165,16 @@ msgstr "Azken proiektuak"
msgid "Go to Plate"
msgstr "Joan plakara"
msgid "Device"
msgstr "Gailua"
# AI Translated
msgid "Device (Web)"
msgstr "Gailua (Web)"
msgid "Multi-device"
msgstr "Gailu anitz"
msgid "AMS Materials Setting"
msgstr "AMS Materialen Ezarpena"
@@ -4216,6 +4226,9 @@ msgstr "Ziur zaude filamentuaren informazioa garbitu nahi duzula?"
msgid "You need to select the material type and color first."
msgstr "Lehenik eta behin, material-mota eta kolorea hautatu behar dituzu."
msgid "The minimum temperature cannot be greater than the maximum temperature."
msgstr ""
#, c-format, boost-format
msgid "Please input a valid value (K in %.1f~%.1f)"
msgstr "Mesedez, sartu baliozko balio bat (K in %.1f~%.1f)"
@@ -4224,6 +4237,12 @@ msgstr "Mesedez, sartu baliozko balio bat (K in %.1f~%.1f)"
msgid "Please input a valid value (K in %.1f~%.1f, N in %.1f~%.1f)"
msgstr "Mesedez, sartu baliozko balio bat (K in %.1f~%.1f, N in %.1f~%.1f)"
msgid "Favorite filaments"
msgstr ""
msgid "All filaments"
msgstr ""
msgid ""
"The nozzle flow is not set. Please set the nozzle flow rate before editing the filament.\n"
"'Device -> Print parts'"
@@ -6269,19 +6288,9 @@ msgstr "Prestatu"
msgid "Preview"
msgstr "Aurrebista"
msgid "Device"
msgstr "Gailua"
msgid "Multi-device"
msgstr "Gailu anitz"
msgid "Project"
msgstr "Proiektua"
# AI Translated
msgid "Device (Web)"
msgstr "Gailua (Web)"
msgid "Yes"
msgstr "Bai"
@@ -6976,6 +6985,22 @@ msgstr ".gcode.3mf fitxategiak ez du G-code daturik. Mesedez, xerratu OrcaSlicer
msgid "File '%s' was lost! Please download it again."
msgstr "'%s' fitxategia galdu da! Deskargatu berriro."
msgid "gcodes"
msgstr ""
msgid "Failed to delete the file from printer."
msgstr ""
#, c-format, boost-format
msgid "Do you want to print the file '%s' from printer?"
msgstr ""
msgid "Print file"
msgstr ""
msgid "Failed to start the print job."
msgstr ""
#, c-format, boost-format
msgid ""
"File: %s\n"
@@ -11037,6 +11062,12 @@ msgstr "Utzi timelapsea eta inprimatu"
msgid "Clean Up"
msgstr "Garbitu"
msgid "AMS filament mapping is not available for this printer. Clear the AMS mapping before printing."
msgstr ""
msgid "A filament used by this print has no AMS mapping. Assign it before printing."
msgstr ""
msgid "Preparing print job"
msgstr "Inprimatze-lana prestatzen"
@@ -30669,6 +30700,9 @@ msgstr "Gaitu garapen-tresnak"
msgid "An error occurred loading %s"
msgstr "Errore bat gertatu da %s kargatzean"
msgid "/"
msgstr "/"
# AI Translated
#, boost-format
msgid "Load %1% to %2%"
@@ -32731,9 +32765,6 @@ msgstr ""
#~ msgid "An error occurred loading "
#~ msgstr "Errore bat gertatu da hau kargatzean: "
#~ msgid "/"
#~ msgstr "/"
# AI Translated
#, c-format, boost-format
#~ msgid "Load %s to "
+45 -14
View File
@@ -6,7 +6,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Orca Slicer\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-11 16:21+0800\n"
"POT-Creation-Date: 2026-10-12 19:00+0800\n"
"PO-Revision-Date: \n"
"Last-Translator: \n"
"Language-Team: Guislain Cyril, Thomas Lété\n"
@@ -4195,6 +4195,16 @@ msgstr "Projets récents"
msgid "Go to Plate"
msgstr "Aller à la plaque"
msgid "Device"
msgstr "Appareil"
# AI Translated
msgid "Device (Web)"
msgstr "Appareil (Web)"
msgid "Multi-device"
msgstr "Multi-appareils"
msgid "AMS Materials Setting"
msgstr "Réglage des matériaux AMS"
@@ -4245,6 +4255,9 @@ msgstr "Voulez-vous vraiment effacer les informations du filament ?"
msgid "You need to select the material type and color first."
msgstr "Vous devez d'abord sélectionner le type de matériau et sa couleur."
msgid "The minimum temperature cannot be greater than the maximum temperature."
msgstr ""
#, c-format, boost-format
msgid "Please input a valid value (K in %.1f~%.1f)"
msgstr "Veuillez saisir une valeur valide (K entre %.1f~%.1f)"
@@ -4253,6 +4266,12 @@ msgstr "Veuillez saisir une valeur valide (K entre %.1f~%.1f)"
msgid "Please input a valid value (K in %.1f~%.1f, N in %.1f~%.1f)"
msgstr "Veuillez saisir une valeur valide (K entre %.1f~%.1f, N entre %.1f~%.1f)"
msgid "Favorite filaments"
msgstr ""
msgid "All filaments"
msgstr ""
msgid ""
"The nozzle flow is not set. Please set the nozzle flow rate before editing the filament.\n"
"'Device -> Print parts'"
@@ -6308,19 +6327,9 @@ msgstr "Préparer"
msgid "Preview"
msgstr "Aperçu"
msgid "Device"
msgstr "Appareil"
msgid "Multi-device"
msgstr "Multi-appareils"
msgid "Project"
msgstr "Projet"
# AI Translated
msgid "Device (Web)"
msgstr "Appareil (Web)"
msgid "Yes"
msgstr "Oui"
@@ -7015,6 +7024,22 @@ msgstr "Le fichier G-code .3mf ne contient pas de données G-code. Veuillez le d
msgid "File '%s' was lost! Please download it again."
msgstr "Le fichier « %s » a été perdu ! Veuillez le télécharger à nouveau."
msgid "gcodes"
msgstr ""
msgid "Failed to delete the file from printer."
msgstr ""
#, c-format, boost-format
msgid "Do you want to print the file '%s' from printer?"
msgstr ""
msgid "Print file"
msgstr ""
msgid "Failed to start the print job."
msgstr ""
#, c-format, boost-format
msgid ""
"File: %s\n"
@@ -11053,6 +11078,12 @@ msgstr "Annuler le timelapse et imprimer"
msgid "Clean Up"
msgstr "Nettoyer"
msgid "AMS filament mapping is not available for this printer. Clear the AMS mapping before printing."
msgstr ""
msgid "A filament used by this print has no AMS mapping. Assign it before printing."
msgstr ""
msgid "Preparing print job"
msgstr "Préparation du travail d'impression"
@@ -30415,6 +30446,9 @@ msgstr "Activer les outils de développement"
msgid "An error occurred loading %s"
msgstr "Une erreur s’est produite lors du chargement de %s"
msgid "/"
msgstr "/"
# AI Translated
#, boost-format
msgid "Load %1% to %2%"
@@ -32466,9 +32500,6 @@ msgstr ""
#~ msgid "An error occurred loading "
#~ msgstr "Une erreur s’est produite lors du chargement de "
#~ msgid "/"
#~ msgstr "/"
# AI Translated
#, c-format, boost-format
#~ msgid "Load %s to "
+47 -16
View File
@@ -3,7 +3,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Orca Slicer\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-11 16:21+0800\n"
"POT-Creation-Date: 2026-10-12 19:00+0800\n"
"Language: hu\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
@@ -4302,6 +4302,17 @@ msgstr "Legutóbbi projektek"
msgid "Go to Plate"
msgstr "Ugrás a tálcára"
msgid "Device"
msgstr "Nyomtató"
# AI Translated
msgid "Device (Web)"
msgstr "Nyomtató (Web)"
# AI Translated
msgid "Multi-device"
msgstr "Több eszköz"
msgid "AMS Materials Setting"
msgstr "AMS anyagok beállítása"
@@ -4354,6 +4365,9 @@ msgstr "Biztosan törölni szeretnéd a filament adatait?"
msgid "You need to select the material type and color first."
msgstr "Először ki kell választanod az anyagtípust és a színt."
msgid "The minimum temperature cannot be greater than the maximum temperature."
msgstr ""
#, c-format, boost-format
msgid "Please input a valid value (K in %.1f~%.1f)"
msgstr "Adj meg érvényes értéket (K tartomány: %.1f~%.1f)"
@@ -4362,6 +4376,12 @@ msgstr "Adj meg érvényes értéket (K tartomány: %.1f~%.1f)"
msgid "Please input a valid value (K in %.1f~%.1f, N in %.1f~%.1f)"
msgstr "Adj meg érvényes értéket (K tartomány: %.1f~%.1f, N tartomány: %.1f~%.1f)"
msgid "Favorite filaments"
msgstr ""
msgid "All filaments"
msgstr ""
msgid ""
"The nozzle flow is not set. Please set the nozzle flow rate before editing the filament.\n"
"'Device -> Print parts'"
@@ -6448,20 +6468,9 @@ msgstr "Előkészítés"
msgid "Preview"
msgstr "Előnézet"
msgid "Device"
msgstr "Nyomtató"
# AI Translated
msgid "Multi-device"
msgstr "Több eszköz"
msgid "Project"
msgstr "Projekt"
# AI Translated
msgid "Device (Web)"
msgstr "Nyomtató (Web)"
msgid "Yes"
msgstr "Igen"
@@ -7167,6 +7176,22 @@ msgstr "A .gcode.3mf fájl nem tartalmaz G-code adatot. Szeleteld újra Orca Sli
msgid "File '%s' was lost! Please download it again."
msgstr "A(z) '%s' fájl elveszett! Kérlek, töltsd le újra."
msgid "gcodes"
msgstr ""
msgid "Failed to delete the file from printer."
msgstr ""
#, c-format, boost-format
msgid "Do you want to print the file '%s' from printer?"
msgstr ""
msgid "Print file"
msgstr ""
msgid "Failed to start the print job."
msgstr ""
#, c-format, boost-format
msgid ""
"File: %s\n"
@@ -11329,6 +11354,12 @@ msgstr "Timelapse megszakítása és nyomtatás"
msgid "Clean Up"
msgstr "Tisztítás"
msgid "AMS filament mapping is not available for this printer. Clear the AMS mapping before printing."
msgstr ""
msgid "A filament used by this print has no AMS mapping. Assign it before printing."
msgstr ""
msgid "Preparing print job"
msgstr "Nyomtatási feladat előkészítése"
@@ -31397,6 +31428,10 @@ msgstr "Fejlesztői eszközök engedélyezése"
msgid "An error occurred loading %s"
msgstr "Hiba történt a(z) %s betöltésekor"
# AI Translated
msgid "/"
msgstr "/"
# AI Translated
#, boost-format
msgid "Load %1% to %2%"
@@ -33459,10 +33494,6 @@ msgstr ""
#~ msgid "An error occurred loading "
#~ msgstr "Hiba történt a betöltés során: "
# AI Translated
#~ msgid "/"
#~ msgstr "/"
# AI Translated
#, c-format, boost-format
#~ msgid "Load %s to "
+45 -14
View File
@@ -3,7 +3,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Orca Slicer\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-11 16:21+0800\n"
"POT-Creation-Date: 2026-10-12 19:00+0800\n"
"PO-Revision-Date: \n"
"Last-Translator: \n"
"Language-Team: \n"
@@ -4295,6 +4295,16 @@ msgstr "Progetti recenti"
msgid "Go to Plate"
msgstr "Vai al piatto"
msgid "Device"
msgstr "Dispositivo"
# AI Translated
msgid "Device (Web)"
msgstr "Dispositivo (Web)"
msgid "Multi-device"
msgstr "Multi-dispositivo"
msgid "AMS Materials Setting"
msgstr "Impostazione materiali AMS"
@@ -4347,6 +4357,9 @@ msgstr "Sei sicuro di voler cancellare le informazioni del filamento?"
msgid "You need to select the material type and color first."
msgstr "Devi prima selezionare il tipo e il colore del materiale."
msgid "The minimum temperature cannot be greater than the maximum temperature."
msgstr ""
#, c-format, boost-format
msgid "Please input a valid value (K in %.1f~%.1f)"
msgstr "Si prega di immettere un valore valido (K in %.1f~%.1f)"
@@ -4355,6 +4368,12 @@ msgstr "Si prega di immettere un valore valido (K in %.1f~%.1f)"
msgid "Please input a valid value (K in %.1f~%.1f, N in %.1f~%.1f)"
msgstr "Si prega di immettere un valore valido (K in %.1f~%.1f, N in %.1f~%.1f)"
msgid "Favorite filaments"
msgstr ""
msgid "All filaments"
msgstr ""
msgid ""
"The nozzle flow is not set. Please set the nozzle flow rate before editing the filament.\n"
"'Device -> Print parts'"
@@ -6416,19 +6435,9 @@ msgstr "Prepara"
msgid "Preview"
msgstr "Anteprima"
msgid "Device"
msgstr "Dispositivo"
msgid "Multi-device"
msgstr "Multi-dispositivo"
msgid "Project"
msgstr "Progetto"
# AI Translated
msgid "Device (Web)"
msgstr "Dispositivo (Web)"
msgid "Yes"
msgstr "Sì"
@@ -7132,6 +7141,22 @@ msgstr "Il file .gcode.3mf non contiene dati G-code. Elaboralo con OrcaSlicer ed
msgid "File '%s' was lost! Please download it again."
msgstr "Il file '%s' è stato perso! Si prega di scaricarlo di nuovo."
msgid "gcodes"
msgstr ""
msgid "Failed to delete the file from printer."
msgstr ""
#, c-format, boost-format
msgid "Do you want to print the file '%s' from printer?"
msgstr ""
msgid "Print file"
msgstr ""
msgid "Failed to start the print job."
msgstr ""
#, c-format, boost-format
msgid ""
"File: %s\n"
@@ -11289,6 +11314,12 @@ msgstr "Annulla timelapse e stampa"
msgid "Clean Up"
msgstr "Pulisci"
msgid "AMS filament mapping is not available for this printer. Clear the AMS mapping before printing."
msgstr ""
msgid "A filament used by this print has no AMS mapping. Assign it before printing."
msgstr ""
msgid "Preparing print job"
msgstr "Preparazione della stampa"
@@ -31259,6 +31290,9 @@ msgstr "Abilita strumenti per sviluppatori"
msgid "An error occurred loading %s"
msgstr "Si è verificato un errore durante il caricamento di %s"
msgid "/"
msgstr "/"
# AI Translated
#, boost-format
msgid "Load %1% to %2%"
@@ -33322,9 +33356,6 @@ msgstr ""
#~ msgid "An error occurred loading "
#~ msgstr "Si è verificato un errore durante il caricamento di "
#~ msgid "/"
#~ msgstr "/"
# AI Translated
#, c-format, boost-format
#~ msgid "Load %s to "
+46 -15
View File
@@ -3,7 +3,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Orca Slicer\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-11 16:21+0800\n"
"POT-Creation-Date: 2026-10-12 19:00+0800\n"
"PO-Revision-Date: \n"
"Last-Translator: \n"
"Language-Team: \n"
@@ -4301,6 +4301,17 @@ msgstr "最近のプロジェクト"
msgid "Go to Plate"
msgstr "プレートへ移動"
msgid "Device"
msgstr "デバイス"
# AI Translated
msgid "Device (Web)"
msgstr "デバイス (Web)"
# AI Translated
msgid "Multi-device"
msgstr "マルチデバイス"
msgid "AMS Materials Setting"
msgstr "AMS素材設定"
@@ -4349,6 +4360,9 @@ msgstr "フィラメント情報をクリアしてもよろしいですか?"
msgid "You need to select the material type and color first."
msgstr "まず材料タイプと色を選択してください。"
msgid "The minimum temperature cannot be greater than the maximum temperature."
msgstr ""
#, c-format, boost-format
msgid "Please input a valid value (K in %.1f~%.1f)"
msgstr "有効な値を入力してください(Kは%.1f~%.1f)"
@@ -4357,6 +4371,12 @@ msgstr "有効な値を入力してください(Kは%.1f~%.1f)"
msgid "Please input a valid value (K in %.1f~%.1f, N in %.1f~%.1f)"
msgstr "有効な値を入力してください(Kは%.1f~%.1f、Nは%.1f~%.1f)"
msgid "Favorite filaments"
msgstr ""
msgid "All filaments"
msgstr ""
msgid ""
"The nozzle flow is not set. Please set the nozzle flow rate before editing the filament.\n"
"'Device -> Print parts'"
@@ -6433,20 +6453,9 @@ msgstr "準備"
msgid "Preview"
msgstr "プレビュー"
msgid "Device"
msgstr "デバイス"
# AI Translated
msgid "Multi-device"
msgstr "マルチデバイス"
msgid "Project"
msgstr "プロジェクト"
# AI Translated
msgid "Device (Web)"
msgstr "デバイス (Web)"
msgid "Yes"
msgstr "はい"
@@ -7147,6 +7156,22 @@ msgstr ".gcode.3mfファイルにG-codeデータが含まれていません。Or
msgid "File '%s' was lost! Please download it again."
msgstr "ファイル %s が見つかりませんでした、もう一度ダウンロードしてください。"
msgid "gcodes"
msgstr ""
msgid "Failed to delete the file from printer."
msgstr ""
#, c-format, boost-format
msgid "Do you want to print the file '%s' from printer?"
msgstr ""
msgid "Print file"
msgstr ""
msgid "Failed to start the print job."
msgstr ""
#, c-format, boost-format
msgid ""
"File: %s\n"
@@ -11317,6 +11342,12 @@ msgstr "タイムラプスを中止して印刷"
msgid "Clean Up"
msgstr "整理する"
msgid "AMS filament mapping is not available for this printer. Clear the AMS mapping before printing."
msgstr ""
msgid "A filament used by this print has no AMS mapping. Assign it before printing."
msgstr ""
msgid "Preparing print job"
msgstr "造形タスクを準備"
@@ -31779,6 +31810,9 @@ msgstr "開発者ツールを有効化"
msgid "An error occurred loading %s"
msgstr "%s の読み込み中にエラーが発生しました"
msgid "/"
msgstr "/"
# AI Translated
#, boost-format
msgid "Load %1% to %2%"
@@ -33882,9 +33916,6 @@ msgstr ""
#~ msgid "An error occurred loading "
#~ msgstr "読み込み中にエラーが発生しました: "
#~ msgid "/"
#~ msgstr "/"
# AI Translated
#, c-format, boost-format
#~ msgid "Load %s to "
+45 -14
View File
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Orca Slicer\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-11 16:21+0800\n"
"POT-Creation-Date: 2026-10-12 19:00+0800\n"
"PO-Revision-Date: 2025-06-02 17:12+0900\n"
"Last-Translator: crwusiz <crwusiz@gmail.com>\n"
"Language-Team: \n"
@@ -4324,6 +4324,16 @@ msgstr "최근 프로젝트"
msgid "Go to Plate"
msgstr "플레이트로 이동"
msgid "Device"
msgstr "장치"
# AI Translated
msgid "Device (Web)"
msgstr "장치 (웹)"
msgid "Multi-device"
msgstr "멀티 디바이스"
msgid "AMS Materials Setting"
msgstr "AMS 재료 설정"
@@ -4374,6 +4384,9 @@ msgstr "정말 필라멘트 정보를 삭제하시겠습니까?"
msgid "You need to select the material type and color first."
msgstr "재료 유형과 색상을 먼저 선택해야 합니다."
msgid "The minimum temperature cannot be greater than the maximum temperature."
msgstr ""
#, c-format, boost-format
msgid "Please input a valid value (K in %.1f~%.1f)"
msgstr "유효한 값을 입력하세요 (%.1f~%.1f 사이의 K)"
@@ -4382,6 +4395,12 @@ msgstr "유효한 값을 입력하세요 (%.1f~%.1f 사이의 K)"
msgid "Please input a valid value (K in %.1f~%.1f, N in %.1f~%.1f)"
msgstr "유효한 값을 입력하세요 (%.1f~%.1f 사이의 K, %.1f~%.1f 사이의 N)"
msgid "Favorite filaments"
msgstr ""
msgid "All filaments"
msgstr ""
msgid ""
"The nozzle flow is not set. Please set the nozzle flow rate before editing the filament.\n"
"'Device -> Print parts'"
@@ -6472,19 +6491,9 @@ msgstr "준비 하기"
msgid "Preview"
msgstr "미리 보기"
msgid "Device"
msgstr "장치"
msgid "Multi-device"
msgstr "멀티 디바이스"
msgid "Project"
msgstr "프로젝트"
# AI Translated
msgid "Device (Web)"
msgstr "장치 (웹)"
msgid "Yes"
msgstr "예"
@@ -7193,6 +7202,22 @@ msgstr ".gcode.3mf 파일에는 G-code 데이터가 없습니다. OrcaSlicer에
msgid "File '%s' was lost! Please download it again."
msgstr "'%s' 파일이 손실되었습니다! 다시 다운로드하세요."
msgid "gcodes"
msgstr ""
msgid "Failed to delete the file from printer."
msgstr ""
#, c-format, boost-format
msgid "Do you want to print the file '%s' from printer?"
msgstr ""
msgid "Print file"
msgstr ""
msgid "Failed to start the print job."
msgstr ""
#, c-format, boost-format
msgid ""
"File: %s\n"
@@ -11459,6 +11484,12 @@ msgstr "타임랩스 취소 후 출력"
msgid "Clean Up"
msgstr "정리"
msgid "AMS filament mapping is not available for this printer. Clear the AMS mapping before printing."
msgstr ""
msgid "A filament used by this print has no AMS mapping. Assign it before printing."
msgstr ""
msgid "Preparing print job"
msgstr "출력 작업 준비 중"
@@ -31797,6 +31828,9 @@ msgstr "개발자 도구 사용"
msgid "An error occurred loading %s"
msgstr "%s 로드 중 오류가 발생했습니다"
msgid "/"
msgstr "/"
# AI Translated
#, boost-format
msgid "Load %1% to %2%"
@@ -33872,9 +33906,6 @@ msgstr ""
#~ msgid "An error occurred loading "
#~ msgstr "로드 중 오류가 발생했습니다: "
#~ msgid "/"
#~ msgstr "/"
# AI Translated
#, c-format, boost-format
#~ msgid "Load %s to "
+1
View File
@@ -147,6 +147,7 @@ src/slic3r/GUI/HMSPanel.cpp
src/slic3r/GUI/MainFrame.cpp
src/slic3r/GUI/MediaPlayCtrl.cpp
src/slic3r/GUI/MediaFilePanel.cpp
src/slic3r/GUI/OrcaFilesPanel.cpp
src/slic3r/GUI/ImageGrid.cpp
src/slic3r/GUI/Widgets/ImageSwitchButton.cpp
src/slic3r/GUI/Printer/PrinterFileSystem.cpp
+47 -15
View File
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-11 16:21+0800\n"
"POT-Creation-Date: 2026-10-12 19:00+0800\n"
"PO-Revision-Date: 2026-07-02 14:13+0300\n"
"Last-Translator: Gintaras Kučinskas <sharanchius@gmail.com>\n"
"Language-Team: \n"
@@ -4291,6 +4291,16 @@ msgstr "Paskutinieji projektai"
msgid "Go to Plate"
msgstr "Eiti į plokštę"
msgid "Device"
msgstr "Įrenginys"
# AI Translated
msgid "Device (Web)"
msgstr "Įrenginys (Web)"
msgid "Multi-device"
msgstr "Kelių įrenginių valdymas (Multi-device)"
msgid "AMS Materials Setting"
msgstr "AMS medžiagų nuostatos"
@@ -4341,6 +4351,9 @@ msgstr "Ar jūs tikrai norite išvalyti gijos informaciją?"
msgid "You need to select the material type and color first."
msgstr "Pradžioje jums reikia pasirinkti medžiagos tipą ir spalvą."
msgid "The minimum temperature cannot be greater than the maximum temperature."
msgstr ""
#, c-format, boost-format
msgid "Please input a valid value (K in %.1f~%.1f)"
msgstr "Įveskite tinkamą reikšmę (K %.1f~%.1f)"
@@ -4349,6 +4362,12 @@ msgstr "Įveskite tinkamą reikšmę (K %.1f~%.1f)"
msgid "Please input a valid value (K in %.1f~%.1f, N in %.1f~%.1f)"
msgstr "Įveskite tinkamą reikšmę (K %.1f~%.1f, N %.1f~%.1f)"
msgid "Favorite filaments"
msgstr ""
msgid "All filaments"
msgstr ""
msgid ""
"The nozzle flow is not set. Please set the nozzle flow rate before editing the filament.\n"
"'Device -> Print parts'"
@@ -6435,19 +6454,9 @@ msgstr "Paruošimas"
msgid "Preview"
msgstr "Peržiūra"
msgid "Device"
msgstr "Įrenginys"
msgid "Multi-device"
msgstr "Kelių įrenginių valdymas (Multi-device)"
msgid "Project"
msgstr "Projektas"
# AI Translated
msgid "Device (Web)"
msgstr "Įrenginys (Web)"
msgid "Yes"
msgstr "Taip"
@@ -7153,6 +7162,22 @@ msgstr "Failas „.gcode.3mf“ neturi G-code duomenų. Supjaustykite jį su „
msgid "File '%s' was lost! Please download it again."
msgstr "Failas „%s“ nerastas! Atsisiųskite jį iš naujo."
msgid "gcodes"
msgstr ""
msgid "Failed to delete the file from printer."
msgstr ""
#, c-format, boost-format
msgid "Do you want to print the file '%s' from printer?"
msgstr ""
msgid "Print file"
msgstr ""
msgid "Failed to start the print job."
msgstr ""
#, c-format, boost-format
msgid ""
"File: %s\n"
@@ -11285,6 +11310,12 @@ msgstr "Atšaukti intervalinį fotografavimą ir spausdinti"
msgid "Clean Up"
msgstr "Išvalyti"
msgid "AMS filament mapping is not available for this printer. Clear the AMS mapping before printing."
msgstr ""
msgid "A filament used by this print has no AMS mapping. Assign it before printing."
msgstr ""
msgid "Preparing print job"
msgstr "Spausdinimo užduoties rengimas"
@@ -12954,6 +12985,7 @@ msgstr ""
msgid "Select filtered"
msgstr ""
# AI Translated
msgid "Clear all"
msgstr "Išvalyti visus"
@@ -31132,6 +31164,10 @@ msgstr "Įjungti kūrėjo įrankius"
msgid "An error occurred loading %s"
msgstr "Įvyko klaida įkeliant %s"
# AI Translated
msgid "/"
msgstr "/"
# AI Translated
#, boost-format
msgid "Load %1% to %2%"
@@ -33199,10 +33235,6 @@ msgstr ""
#~ msgid "An error occurred loading "
#~ msgstr "Įvyko klaida įkeliant "
# AI Translated
#~ msgid "/"
#~ msgstr "/"
# AI Translated
#, c-format, boost-format
#~ msgid "Load %s to "
+46 -15
View File
@@ -3,7 +3,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Orca Slicer\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-11 16:21+0800\n"
"POT-Creation-Date: 2026-10-12 19:00+0800\n"
"PO-Revision-Date: \n"
"Last-Translator: \n"
"Language-Team: \n"
@@ -4660,6 +4660,17 @@ msgstr "Recente projecten"
msgid "Go to Plate"
msgstr "Naar printbed gaan"
msgid "Device"
msgstr "Apparaat"
# AI Translated
msgid "Device (Web)"
msgstr "Apparaat (Web)"
# AI Translated
msgid "Multi-device"
msgstr "Meerdere apparaten"
msgid "AMS Materials Setting"
msgstr "AMS Materiaal instellingen"
@@ -4716,6 +4727,9 @@ msgstr "Weet u zeker dat u de filamentinformatie wilt wissen?"
msgid "You need to select the material type and color first."
msgstr "U moet eerst het materiaaltype en de kleur selecteren."
msgid "The minimum temperature cannot be greater than the maximum temperature."
msgstr ""
# AI Translated
#, c-format, boost-format
msgid "Please input a valid value (K in %.1f~%.1f)"
@@ -4726,6 +4740,12 @@ msgstr "Voer een geldige waarde in (K tussen %.1f~%.1f)"
msgid "Please input a valid value (K in %.1f~%.1f, N in %.1f~%.1f)"
msgstr "Voer een geldige waarde in (K tussen %.1f~%.1f, N tussen %.1f~%.1f)"
msgid "Favorite filaments"
msgstr ""
msgid "All filaments"
msgstr ""
# AI Translated
msgid ""
"The nozzle flow is not set. Please set the nozzle flow rate before editing the filament.\n"
@@ -6968,21 +6988,10 @@ msgstr "Voorbereiden"
msgid "Preview"
msgstr "Voorvertoning"
msgid "Device"
msgstr "Apparaat"
# AI Translated
msgid "Multi-device"
msgstr "Meerdere apparaten"
# AI Translated
msgid "Project"
msgstr "Project"
# AI Translated
msgid "Device (Web)"
msgstr "Apparaat (Web)"
msgid "Yes"
msgstr "Ja"
@@ -7731,6 +7740,22 @@ msgstr "Het .gcode.3mf-bestand bevat geen G-code-gegevens. Slice het met Orca Sl
msgid "File '%s' was lost! Please download it again."
msgstr "Bestand '%s' is verloren gegaan! Download het opnieuw."
msgid "gcodes"
msgstr ""
msgid "Failed to delete the file from printer."
msgstr ""
#, c-format, boost-format
msgid "Do you want to print the file '%s' from printer?"
msgstr ""
msgid "Print file"
msgstr ""
msgid "Failed to start the print job."
msgstr ""
#, c-format, boost-format
msgid ""
"File: %s\n"
@@ -12172,6 +12197,12 @@ msgstr "Timelapse annuleren en printen"
msgid "Clean Up"
msgstr "Opruimen"
msgid "AMS filament mapping is not available for this printer. Clear the AMS mapping before printing."
msgstr ""
msgid "A filament used by this print has no AMS mapping. Assign it before printing."
msgstr ""
msgid "Preparing print job"
msgstr "Print opdracht voorbereiden"
@@ -33323,6 +33354,9 @@ msgstr "Ontwikkelaarstools inschakelen"
msgid "An error occurred loading %s"
msgstr "Er is een fout opgetreden bij het laden van %s"
msgid "/"
msgstr "/"
# AI Translated
#, boost-format
msgid "Load %1% to %2%"
@@ -35456,9 +35490,6 @@ msgstr ""
#~ msgid "An error occurred loading "
#~ msgstr "Er is een fout opgetreden bij het laden van "
#~ msgid "/"
#~ msgstr "/"
# AI Translated
#, c-format, boost-format
#~ msgid "Load %s to "
+45 -14
View File
@@ -3,7 +3,7 @@ msgid ""
msgstr ""
"Project-Id-Version: OrcaSlicer 2.3.0-rc\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-11 16:21+0800\n"
"POT-Creation-Date: 2026-10-12 19:00+0800\n"
"PO-Revision-Date: \n"
"Last-Translator: Krzysztof Morga <<tlumaczeniebs@gmail.com>>\n"
"Language-Team: \n"
@@ -4382,6 +4382,16 @@ msgstr "Ostatnie projekty"
msgid "Go to Plate"
msgstr "Przejdź do płyty"
msgid "Device"
msgstr "Urządzenie"
# AI Translated
msgid "Device (Web)"
msgstr "Urządzenie (Web)"
msgid "Multi-device"
msgstr "Wiele urządzeń"
msgid "AMS Materials Setting"
msgstr "Ustawienia filamentów AMS"
@@ -4435,6 +4445,9 @@ msgstr "Czy na pewno usunąć informacje o filamentach?"
msgid "You need to select the material type and color first."
msgstr "Najpierw należy wybrać rodzaj i kolor filamentu."
msgid "The minimum temperature cannot be greater than the maximum temperature."
msgstr ""
#, c-format, boost-format
msgid "Please input a valid value (K in %.1f~%.1f)"
msgstr "Proszę podać prawidłową wartość (K w zakresie %.1f~%.1f)"
@@ -4443,6 +4456,12 @@ msgstr "Proszę podać prawidłową wartość (K w zakresie %.1f~%.1f)"
msgid "Please input a valid value (K in %.1f~%.1f, N in %.1f~%.1f)"
msgstr "Proszę podać prawidłową wartość (K w zakresie %.1f~%.1f, N in %.1f~%.1f)"
msgid "Favorite filaments"
msgstr ""
msgid "All filaments"
msgstr ""
msgid ""
"The nozzle flow is not set. Please set the nozzle flow rate before editing the filament.\n"
"'Device -> Print parts'"
@@ -6552,19 +6571,9 @@ msgstr "Przygotowanie"
msgid "Preview"
msgstr "Podgląd"
msgid "Device"
msgstr "Urządzenie"
msgid "Multi-device"
msgstr "Wiele urządzeń"
msgid "Project"
msgstr "Projekt"
# AI Translated
msgid "Device (Web)"
msgstr "Urządzenie (Web)"
msgid "Yes"
msgstr "Tak"
@@ -7283,6 +7292,22 @@ msgstr "Plik .gcode.3mf nie zawiera danych G-code. Proszę poddać go obróbce w
msgid "File '%s' was lost! Please download it again."
msgstr "Plik „%s” został utracony! Proszę pobrać go ponownie."
msgid "gcodes"
msgstr ""
msgid "Failed to delete the file from printer."
msgstr ""
#, c-format, boost-format
msgid "Do you want to print the file '%s' from printer?"
msgstr ""
msgid "Print file"
msgstr ""
msgid "Failed to start the print job."
msgstr ""
#, c-format, boost-format
msgid ""
"File: %s\n"
@@ -11558,6 +11583,12 @@ msgstr "Anuluj timelapse i drukuj"
msgid "Clean Up"
msgstr "Wyczyść"
msgid "AMS filament mapping is not available for this printer. Clear the AMS mapping before printing."
msgstr ""
msgid "A filament used by this print has no AMS mapping. Assign it before printing."
msgstr ""
msgid "Preparing print job"
msgstr "Przygotowywanie zadania do druku"
@@ -31778,6 +31809,9 @@ msgstr "Włącz narzędzia deweloperskie"
msgid "An error occurred loading %s"
msgstr "Wystąpił błąd podczas ładowania %s"
msgid "/"
msgstr "/"
# AI Translated
#, boost-format
msgid "Load %1% to %2%"
@@ -33820,9 +33854,6 @@ msgstr ""
#~ msgid "An error occurred loading "
#~ msgstr "Wystąpił błąd podczas ładowania"
#~ msgid "/"
#~ msgstr "/"
# AI Translated
#, c-format, boost-format
#~ msgid "Load %s to "
+45 -14
View File
@@ -3,7 +3,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Orca Slicer\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-11 16:21+0800\n"
"POT-Creation-Date: 2026-10-12 19:00+0800\n"
"PO-Revision-Date: 2026-07-26 11:14-0300\n"
"Last-Translator: Alexandre Folle de Menezes\n"
"Language-Team: Portuguese, Brazilian\n"
@@ -4132,6 +4132,15 @@ msgstr "Projetos Recentes"
msgid "Go to Plate"
msgstr "Ir para Placa"
msgid "Device"
msgstr "Dispositivo"
msgid "Device (Web)"
msgstr "Dispositivo (Web)"
msgid "Multi-device"
msgstr "Multi-dispositivo"
msgid "AMS Materials Setting"
msgstr "Configuração de Materiais AMS"
@@ -4182,6 +4191,9 @@ msgstr "Tem certeza de que deseja limpar as informações do filamento?"
msgid "You need to select the material type and color first."
msgstr "Você precisa selecionar o tipo e a cor do material primeiro."
msgid "The minimum temperature cannot be greater than the maximum temperature."
msgstr ""
#, c-format, boost-format
msgid "Please input a valid value (K in %.1f~%.1f)"
msgstr "Por favor insira um valor válido (K entre %.1f~%.1f)"
@@ -4190,6 +4202,12 @@ msgstr "Por favor insira um valor válido (K entre %.1f~%.1f)"
msgid "Please input a valid value (K in %.1f~%.1f, N in %.1f~%.1f)"
msgstr "Por favor insira um valor válido (K entre %.1f~%.1f, N entre %.1f~%.1f)"
msgid "Favorite filaments"
msgstr ""
msgid "All filaments"
msgstr ""
msgid ""
"The nozzle flow is not set. Please set the nozzle flow rate before editing the filament.\n"
"'Device -> Print parts'"
@@ -6222,18 +6240,9 @@ msgstr "Preparar"
msgid "Preview"
msgstr "Pré-visualizar"
msgid "Device"
msgstr "Dispositivo"
msgid "Multi-device"
msgstr "Multi-dispositivo"
msgid "Project"
msgstr "Projeto"
msgid "Device (Web)"
msgstr "Dispositivo (Web)"
msgid "Yes"
msgstr "Sim"
@@ -6924,6 +6933,22 @@ msgstr "O arquivo .gcode.3mf não contém dados de G-code. Por favor, fatie com
msgid "File '%s' was lost! Please download it again."
msgstr "O arquivo '%s' foi perdido! Por favor, baixe-o novamente."
msgid "gcodes"
msgstr ""
msgid "Failed to delete the file from printer."
msgstr ""
#, c-format, boost-format
msgid "Do you want to print the file '%s' from printer?"
msgstr ""
msgid "Print file"
msgstr ""
msgid "Failed to start the print job."
msgstr ""
#, c-format, boost-format
msgid ""
"File: %s\n"
@@ -10873,6 +10898,12 @@ msgstr "Cancelar Timelapse e Imprimir"
msgid "Clean Up"
msgstr "Limpar"
msgid "AMS filament mapping is not available for this printer. Clear the AMS mapping before printing."
msgstr ""
msgid "A filament used by this print has no AMS mapping. Assign it before printing."
msgstr ""
msgid "Preparing print job"
msgstr "Preparando trabalho de impressão"
@@ -30359,6 +30390,10 @@ msgstr "Ativar Ferramentas de Desenvolvedor"
msgid "An error occurred loading %s"
msgstr "Ocorreu um erro ao carregar %s"
# AI Translated
msgid "/"
msgstr "/"
# AI Translated
#, boost-format
msgid "Load %1% to %2%"
@@ -32414,10 +32449,6 @@ msgstr ""
#~ msgid "An error occurred loading "
#~ msgstr "Ocorreu um erro ao carregar "
# AI Translated
#~ msgid "/"
#~ msgstr "/"
# AI Translated
#, c-format, boost-format
#~ msgid "Load %s to "
+46 -14
View File
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: OrcaSlicer V2.5.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-11 16:21+0800\n"
"POT-Creation-Date: 2026-10-12 19:00+0800\n"
"PO-Revision-Date: 2026-02-25 13:38+0300\n"
"Last-Translator: Felix14_v2\n"
"Language-Team: Felix14_v2 (ДС/ТГ: @felix14_v2, почта: aleks111001@list.ru), Andylg <andylg@yandex.ru>\n"
@@ -4267,6 +4267,16 @@ msgstr "Недавние проекты"
msgid "Go to Plate"
msgstr "Перейти к столу"
msgid "Device"
msgstr "Принтер"
# AI Translated
msgid "Device (Web)"
msgstr "Принтер (веб)"
msgid "Multi-device"
msgstr "Принтеры"
msgid "AMS Materials Setting"
msgstr "Настройка материалов AMS"
@@ -4317,6 +4327,9 @@ msgstr "Вы действительно хотите удалить информ
msgid "You need to select the material type and color first."
msgstr "Сначала нужно выбрать тип материала и цвет."
msgid "The minimum temperature cannot be greater than the maximum temperature."
msgstr ""
#, c-format, boost-format
msgid "Please input a valid value (K in %.1f~%.1f)"
msgstr "Пожалуйста, введите допустимое значение (K в диапазоне %.1f~%.1f)"
@@ -4325,6 +4338,12 @@ msgstr "Пожалуйста, введите допустимое значени
msgid "Please input a valid value (K in %.1f~%.1f, N in %.1f~%.1f)"
msgstr "Пожалуйста, введите допустимое значение (K в диапазоне %.1f~%.1f, N в диапазоне %.1f~%.1f)"
msgid "Favorite filaments"
msgstr ""
msgid "All filaments"
msgstr ""
# Дичь какая-то бамбуковская с терминами, требуется проверка.
msgid ""
"The nozzle flow is not set. Please set the nozzle flow rate before editing the filament.\n"
@@ -6465,19 +6484,9 @@ msgstr "Подготовка"
msgid "Preview"
msgstr "Просмотр нарезки"
msgid "Device"
msgstr "Принтер"
msgid "Multi-device"
msgstr "Принтеры"
msgid "Project"
msgstr "Проект"
# AI Translated
msgid "Device (Web)"
msgstr "Принтер (веб)"
msgid "Yes"
msgstr "Да"
@@ -7190,6 +7199,22 @@ msgstr "Файл .gcode.3mf не содержит данных G-code. Испо
msgid "File '%s' was lost! Please download it again."
msgstr "Файл '%s' потерян! Пожалуйста, загрузите его снова."
msgid "gcodes"
msgstr ""
msgid "Failed to delete the file from printer."
msgstr ""
#, c-format, boost-format
msgid "Do you want to print the file '%s' from printer?"
msgstr ""
msgid "Print file"
msgstr ""
msgid "Failed to start the print job."
msgstr ""
#, c-format, boost-format
msgid ""
"File: %s\n"
@@ -11298,6 +11323,12 @@ msgstr "Отключить таймлапс и продолжить"
msgid "Clean Up"
msgstr "Очистить"
msgid "AMS filament mapping is not available for this printer. Clear the AMS mapping before printing."
msgstr ""
msgid "A filament used by this print has no AMS mapping. Assign it before printing."
msgstr ""
msgid "Preparing print job"
msgstr "Подготовка задания на печать"
@@ -12968,6 +12999,7 @@ msgstr ""
msgid "Select filtered"
msgstr ""
# AI Translated
msgid "Clear all"
msgstr "Очистить"
@@ -31626,6 +31658,9 @@ msgstr "Включить инструменты разработчика"
msgid "An error occurred loading %s"
msgstr "Ошибка при загрузке %s"
msgid "/"
msgstr "/"
# AI Translated
#, boost-format
msgid "Load %1% to %2%"
@@ -33693,9 +33728,6 @@ msgstr ""
#~ msgid "An error occurred loading "
#~ msgstr "Ошибка при загрузке "
#~ msgid "/"
#~ msgstr "/"
# AI Translated
#, c-format, boost-format
#~ msgid "Load %s to "
+46 -15
View File
@@ -3,7 +3,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Orca Slicer\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-11 16:21+0800\n"
"POT-Creation-Date: 2026-10-12 19:00+0800\n"
"Language: sv\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
@@ -4728,6 +4728,17 @@ msgstr "Senaste projekt"
msgid "Go to Plate"
msgstr "Gå till platta"
msgid "Device"
msgstr "Enhet"
# AI Translated
msgid "Device (Web)"
msgstr "Enhet (Webb)"
# AI Translated
msgid "Multi-device"
msgstr "Flera enheter"
msgid "AMS Materials Setting"
msgstr "AMS Material Inställning"
@@ -4781,6 +4792,9 @@ msgstr "Är du säker på att du vill rensa filament informationen?"
msgid "You need to select the material type and color first."
msgstr "Du måste först välja materialtyp och färg."
msgid "The minimum temperature cannot be greater than the maximum temperature."
msgstr ""
# AI Translated
#, c-format, boost-format
msgid "Please input a valid value (K in %.1f~%.1f)"
@@ -4791,6 +4805,12 @@ msgstr "Ange ett giltigt värde (K mellan %.1f~%.1f)"
msgid "Please input a valid value (K in %.1f~%.1f, N in %.1f~%.1f)"
msgstr "Ange ett giltigt värde (K mellan %.1f~%.1f, N mellan %.1f~%.1f)"
msgid "Favorite filaments"
msgstr ""
msgid "All filaments"
msgstr ""
# AI Translated
msgid ""
"The nozzle flow is not set. Please set the nozzle flow rate before editing the filament.\n"
@@ -7065,20 +7085,9 @@ msgstr "Förbered"
msgid "Preview"
msgstr "Förhandsvisning"
msgid "Device"
msgstr "Enhet"
# AI Translated
msgid "Multi-device"
msgstr "Flera enheter"
msgid "Project"
msgstr "Projekt"
# AI Translated
msgid "Device (Web)"
msgstr "Enhet (Webb)"
msgid "Yes"
msgstr "Ja"
@@ -7825,6 +7834,22 @@ msgstr "Filen .gcode.3mf innehåller inga G-code-data. Bered den med Orca Slicer
msgid "File '%s' was lost! Please download it again."
msgstr "Filen '%s' förlorades! Ladda ner den igen."
msgid "gcodes"
msgstr ""
msgid "Failed to delete the file from printer."
msgstr ""
#, c-format, boost-format
msgid "Do you want to print the file '%s' from printer?"
msgstr ""
msgid "Print file"
msgstr ""
msgid "Failed to start the print job."
msgstr ""
# AI Translated
#, c-format, boost-format
msgid ""
@@ -12320,6 +12345,12 @@ msgstr "Avbryt timelapse och skriv ut"
msgid "Clean Up"
msgstr "Rensa"
msgid "AMS filament mapping is not available for this printer. Clear the AMS mapping before printing."
msgstr ""
msgid "A filament used by this print has no AMS mapping. Assign it before printing."
msgstr ""
msgid "Preparing print job"
msgstr "Förbereder utskriftsjobb"
@@ -33657,6 +33688,9 @@ msgstr "Aktivera utvecklarverktyg"
msgid "An error occurred loading %s"
msgstr "Ett fel uppstod vid inläsning av %s"
msgid "/"
msgstr "/"
# AI Translated
#, boost-format
msgid "Load %1% to %2%"
@@ -35810,9 +35844,6 @@ msgstr ""
#~ msgid "An error occurred loading "
#~ msgstr "Ett fel uppstod vid inläsning av "
#~ msgid "/"
#~ msgstr "/"
# AI Translated
#, c-format, boost-format
#~ msgid "Load %s to "
+46 -15
View File
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Orca Slicer\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-11 16:21+0800\n"
"POT-Creation-Date: 2026-10-12 19:00+0800\n"
"PO-Revision-Date: 2026-06-19 13:40+0700\n"
"Last-Translator: Icezaza\n"
"Language-Team: Thai\n"
@@ -4279,6 +4279,16 @@ msgstr "โปรเจกต์ล่าสุด"
msgid "Go to Plate"
msgstr "ไปที่ฐานพิมพ์"
msgid "Device"
msgstr "อุปกรณ์"
# AI Translated
msgid "Device (Web)"
msgstr "อุปกรณ์ (เว็บ)"
msgid "Multi-device"
msgstr "หลายอุปกรณ์"
msgid "AMS Materials Setting"
msgstr "การตั้งค่าวัสดุ AMS"
@@ -4329,6 +4339,9 @@ msgstr "คุณแน่ใจหรือไม่ว่าต้องกา
msgid "You need to select the material type and color first."
msgstr "คุณต้องเลือกประเภทวัสดุและสีก่อน"
msgid "The minimum temperature cannot be greater than the maximum temperature."
msgstr ""
#, c-format, boost-format
msgid "Please input a valid value (K in %.1f~%.1f)"
msgstr "โปรดป้อนค่าที่ถูกต้อง (K อยู่ในช่วง %.1f~%.1f)"
@@ -4337,6 +4350,12 @@ msgstr "โปรดป้อนค่าที่ถูกต้อง (K อ
msgid "Please input a valid value (K in %.1f~%.1f, N in %.1f~%.1f)"
msgstr "โปรดป้อนค่าที่ถูกต้อง (K ใน %.1f~%.1f, N ใน %.1f~%.1f)"
msgid "Favorite filaments"
msgstr ""
msgid "All filaments"
msgstr ""
msgid ""
"The nozzle flow is not set. Please set the nozzle flow rate before editing the filament.\n"
"'Device -> Print parts'"
@@ -6399,19 +6418,9 @@ msgstr "เตรียมพิมพ์"
msgid "Preview"
msgstr "พรีวิว"
msgid "Device"
msgstr "อุปกรณ์"
msgid "Multi-device"
msgstr "หลายอุปกรณ์"
msgid "Project"
msgstr "โปรเจกต์"
# AI Translated
msgid "Device (Web)"
msgstr "อุปกรณ์ (เว็บ)"
msgid "Yes"
msgstr "ใช่"
@@ -7106,6 +7115,22 @@ msgstr "ไฟล์ .gcode.3mf ไม่มีข้อมูล G-code โป
msgid "File '%s' was lost! Please download it again."
msgstr "ไฟล์ '%s' สูญหาย! กรุณาดาวน์โหลดอีกครั้ง."
msgid "gcodes"
msgstr ""
msgid "Failed to delete the file from printer."
msgstr ""
#, c-format, boost-format
msgid "Do you want to print the file '%s' from printer?"
msgstr ""
msgid "Print file"
msgstr ""
msgid "Failed to start the print job."
msgstr ""
#, c-format, boost-format
msgid ""
"File: %s\n"
@@ -11215,6 +11240,12 @@ msgstr "ยกเลิกไทม์แลปส์และพิมพ์"
msgid "Clean Up"
msgstr "ล้าง"
msgid "AMS filament mapping is not available for this printer. Clear the AMS mapping before printing."
msgstr ""
msgid "A filament used by this print has no AMS mapping. Assign it before printing."
msgstr ""
msgid "Preparing print job"
msgstr "กำลังเตรียมงานพิมพ์"
@@ -31014,6 +31045,10 @@ msgstr "เปิดใช้งานเครื่องมือนักพ
msgid "An error occurred loading %s"
msgstr "เกิดข้อผิดพลาดในการโหลด %s"
# AI Translated
msgid "/"
msgstr "/"
# AI Translated
#, boost-format
msgid "Load %1% to %2%"
@@ -33075,10 +33110,6 @@ msgstr ""
#~ msgid "An error occurred loading "
#~ msgstr "เกิดข้อผิดพลาดในการโหลด "
# AI Translated
#~ msgid "/"
#~ msgstr "/"
# AI Translated
#, c-format, boost-format
#~ msgid "Load %s to "
+46 -15
View File
@@ -3,7 +3,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Orca Slicer\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-11 16:21+0800\n"
"POT-Creation-Date: 2026-10-12 19:00+0800\n"
"PO-Revision-Date: 2026-08-21 23:18+0300\n"
"Last-Translator: GlauTech\n"
"Language-Team: \n"
@@ -4331,6 +4331,16 @@ msgstr "Son Projeler"
msgid "Go to Plate"
msgstr "Plakaya Git"
msgid "Device"
msgstr "Cihaz"
# AI Translated
msgid "Device (Web)"
msgstr "Cihaz (Web)"
msgid "Multi-device"
msgstr "Çoklu cihaz"
msgid "AMS Materials Setting"
msgstr "AMS Malzeme Ayarı"
@@ -4381,6 +4391,9 @@ msgstr "Filament bilgisini temizlemek istediğinizden emin misiniz?"
msgid "You need to select the material type and color first."
msgstr "Önce malzeme türünü ve rengini seçmeniz gerekir."
msgid "The minimum temperature cannot be greater than the maximum temperature."
msgstr ""
#, c-format, boost-format
msgid "Please input a valid value (K in %.1f~%.1f)"
msgstr "Lütfen geçerli bir değer girin (K %.1f~%.1f içinde)"
@@ -4389,6 +4402,12 @@ msgstr "Lütfen geçerli bir değer girin (K %.1f~%.1f içinde)"
msgid "Please input a valid value (K in %.1f~%.1f, N in %.1f~%.1f)"
msgstr "Lütfen geçerli bir değer girin (K %.1f~%.1f içinde, N %.1f~%.1f içinde)"
msgid "Favorite filaments"
msgstr ""
msgid "All filaments"
msgstr ""
msgid ""
"The nozzle flow is not set. Please set the nozzle flow rate before editing the filament.\n"
"'Device -> Print parts'"
@@ -6465,19 +6484,9 @@ msgstr "Hazırlık"
msgid "Preview"
msgstr "Ön İzleme"
msgid "Device"
msgstr "Cihaz"
msgid "Multi-device"
msgstr "Çoklu cihaz"
msgid "Project"
msgstr "Proje"
# AI Translated
msgid "Device (Web)"
msgstr "Cihaz (Web)"
msgid "Yes"
msgstr "Evet"
@@ -7179,6 +7188,22 @@ msgstr ".gcode.3mf dosyası hiçbir G-code verisi içermiyor. Lütfen dosyayı B
msgid "File '%s' was lost! Please download it again."
msgstr "'%s' dosyası kayboldu! Lütfen tekrar indirin."
msgid "gcodes"
msgstr ""
msgid "Failed to delete the file from printer."
msgstr ""
#, c-format, boost-format
msgid "Do you want to print the file '%s' from printer?"
msgstr ""
msgid "Print file"
msgstr ""
msgid "Failed to start the print job."
msgstr ""
#, c-format, boost-format
msgid ""
"File: %s\n"
@@ -11347,6 +11372,12 @@ msgstr "Timelapse'i İptal Et ve Yazdır"
msgid "Clean Up"
msgstr "Temizle"
msgid "AMS filament mapping is not available for this printer. Clear the AMS mapping before printing."
msgstr ""
msgid "A filament used by this print has no AMS mapping. Assign it before printing."
msgstr ""
msgid "Preparing print job"
msgstr "Yazdırma için hazırlanıyor"
@@ -31457,6 +31488,10 @@ msgstr "Geliştirici Araçlarını Etkinleştir"
msgid "An error occurred loading %s"
msgstr "%s yüklenirken bir hata oluştu"
# AI Translated
msgid "/"
msgstr "/"
# AI Translated
#, boost-format
msgid "Load %1% to %2%"
@@ -33523,10 +33558,6 @@ msgstr ""
#~ msgid "An error occurred loading "
#~ msgstr "Yüklenirken bir hata oluştu: "
# AI Translated
#~ msgid "/"
#~ msgstr "/"
# AI Translated
#, c-format, boost-format
#~ msgid "Load %s to "
+45 -14
View File
@@ -3,7 +3,7 @@ msgid ""
msgstr ""
"Project-Id-Version: orcaslicerua\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-11 16:21+0800\n"
"POT-Creation-Date: 2026-10-12 19:00+0800\n"
"PO-Revision-Date: 2026-07-17 16:25+0300\n"
"Last-Translator: Andrij Mizyk <andm1zyk@proton.me>\n"
"Language-Team: Ukrainian\n"
@@ -4245,6 +4245,16 @@ msgstr "Останні проєкти"
msgid "Go to Plate"
msgstr "Перейти до пластини"
msgid "Device"
msgstr "Пристрій"
# AI Translated
msgid "Device (Web)"
msgstr "Пристрій (Веб)"
msgid "Multi-device"
msgstr "Багато пристроїв"
msgid "AMS Materials Setting"
msgstr "Налаштування матеріалів AMS"
@@ -4295,6 +4305,9 @@ msgstr "Ви впевнені, що хочете видалити інформа
msgid "You need to select the material type and color first."
msgstr "Спочатку потрібно вибрати тип матеріалу та колір."
msgid "The minimum temperature cannot be greater than the maximum temperature."
msgstr ""
#, c-format, boost-format
msgid "Please input a valid value (K in %.1f~%.1f)"
msgstr "Будь ласка введіть коректне значення (K в %.1f~%.1f)"
@@ -4303,6 +4316,12 @@ msgstr "Будь ласка введіть коректне значення (K
msgid "Please input a valid value (K in %.1f~%.1f, N in %.1f~%.1f)"
msgstr "Будь ласка введіть коректне значення (K в %.1f~%.1f, N в %.1f~%.1f)"
msgid "Favorite filaments"
msgstr ""
msgid "All filaments"
msgstr ""
# AI Translated
msgid ""
"The nozzle flow is not set. Please set the nozzle flow rate before editing the filament.\n"
@@ -6429,19 +6448,9 @@ msgstr "Підготовка"
msgid "Preview"
msgstr "Перегляд"
msgid "Device"
msgstr "Пристрій"
msgid "Multi-device"
msgstr "Багато пристроїв"
msgid "Project"
msgstr "Проєкт"
# AI Translated
msgid "Device (Web)"
msgstr "Пристрій (Веб)"
msgid "Yes"
msgstr "Так"
@@ -7148,6 +7157,22 @@ msgstr "Файл .gcode.3mf не містить даних G-code. Будь ла
msgid "File '%s' was lost! Please download it again."
msgstr "Файл '%s' втрачений! Завантажте його знову."
msgid "gcodes"
msgstr ""
msgid "Failed to delete the file from printer."
msgstr ""
#, c-format, boost-format
msgid "Do you want to print the file '%s' from printer?"
msgstr ""
msgid "Print file"
msgstr ""
msgid "Failed to start the print job."
msgstr ""
#, c-format, boost-format
msgid ""
"File: %s\n"
@@ -11364,6 +11389,12 @@ msgstr "Скасувати таймлапс і друкувати"
msgid "Clean Up"
msgstr "Очистити"
msgid "AMS filament mapping is not available for this printer. Clear the AMS mapping before printing."
msgstr ""
msgid "A filament used by this print has no AMS mapping. Assign it before printing."
msgstr ""
msgid "Preparing print job"
msgstr "Підготовка завдання для друку"
@@ -31624,6 +31655,9 @@ msgstr "Увімкнути інструменти розробника"
msgid "An error occurred loading %s"
msgstr "Помилка під час завантаження %s"
msgid "/"
msgstr "/"
# AI Translated
#, boost-format
msgid "Load %1% to %2%"
@@ -33718,9 +33752,6 @@ msgstr ""
#~ msgid "An error occurred loading "
#~ msgstr "Помилка під час завантаження "
#~ msgid "/"
#~ msgstr "/"
# AI Translated
#, c-format, boost-format
#~ msgid "Load %s to "
+46 -15
View File
@@ -3,7 +3,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Orca Slicer\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-11 16:21+0800\n"
"POT-Creation-Date: 2026-10-12 19:00+0800\n"
"PO-Revision-Date: 2025-10-02 17:43+0700\n"
"Last-Translator: \n"
"Language-Team: hainguyen.ts13@gmail.com\n"
@@ -4503,6 +4503,16 @@ msgstr "Dự án gần đây"
msgid "Go to Plate"
msgstr "Đi tới plate"
msgid "Device"
msgstr "Thiết bị"
# AI Translated
msgid "Device (Web)"
msgstr "Thiết bị (Web)"
msgid "Multi-device"
msgstr "Nhiều thiết bị"
msgid "AMS Materials Setting"
msgstr "Cài đặt vật liệu AMS"
@@ -4555,6 +4565,9 @@ msgstr "Bạn có chắc muốn xóa thông tin filament?"
msgid "You need to select the material type and color first."
msgstr "Bạn cần chọn loại vật liệu và màu trước."
msgid "The minimum temperature cannot be greater than the maximum temperature."
msgstr ""
#, c-format, boost-format
msgid "Please input a valid value (K in %.1f~%.1f)"
msgstr "Vui lòng nhập giá trị hợp lệ (K trong %.1f~%.1f)"
@@ -4563,6 +4576,12 @@ msgstr "Vui lòng nhập giá trị hợp lệ (K trong %.1f~%.1f)"
msgid "Please input a valid value (K in %.1f~%.1f, N in %.1f~%.1f)"
msgstr "Vui lòng nhập giá trị hợp lệ (K trong %.1f~%.1f, N trong %.1f~%.1f)"
msgid "Favorite filaments"
msgstr ""
msgid "All filaments"
msgstr ""
# AI Translated
msgid ""
"The nozzle flow is not set. Please set the nozzle flow rate before editing the filament.\n"
@@ -6789,19 +6808,9 @@ msgstr "Chuẩn bị"
msgid "Preview"
msgstr "Xem trước"
msgid "Device"
msgstr "Thiết bị"
msgid "Multi-device"
msgstr "Nhiều thiết bị"
msgid "Project"
msgstr "Dự án"
# AI Translated
msgid "Device (Web)"
msgstr "Thiết bị (Web)"
msgid "Yes"
msgstr "Có"
@@ -7513,6 +7522,22 @@ msgstr "File .gcode.3mf không chứa dữ liệu G-code. Vui lòng slice với
msgid "File '%s' was lost! Please download it again."
msgstr "File '%s' đã bị mất! Vui lòng tải lại."
msgid "gcodes"
msgstr ""
msgid "Failed to delete the file from printer."
msgstr ""
#, c-format, boost-format
msgid "Do you want to print the file '%s' from printer?"
msgstr ""
msgid "Print file"
msgstr ""
msgid "Failed to start the print job."
msgstr ""
#, c-format, boost-format
msgid ""
"File: %s\n"
@@ -11887,6 +11912,12 @@ msgstr "Hủy timelapse & In"
msgid "Clean Up"
msgstr "Dọn dẹp"
msgid "AMS filament mapping is not available for this printer. Clear the AMS mapping before printing."
msgstr ""
msgid "A filament used by this print has no AMS mapping. Assign it before printing."
msgstr ""
msgid "Preparing print job"
msgstr "Đang chuẩn bị công việc in"
@@ -32295,6 +32326,10 @@ msgstr "Bật công cụ nhà phát triển"
msgid "An error occurred loading %s"
msgstr "Đã xảy ra lỗi khi tải %s"
# AI Translated
msgid "/"
msgstr "/"
# AI Translated
#, boost-format
msgid "Load %1% to %2%"
@@ -34398,10 +34433,6 @@ msgstr ""
#~ msgid "An error occurred loading "
#~ msgstr "Đã xảy ra lỗi khi tải "
# AI Translated
#~ msgid "/"
#~ msgstr "/"
# AI Translated
#, c-format, boost-format
#~ msgid "Load %s to "
+47 -15
View File
@@ -6,7 +6,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Slic3rPE\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-11 16:21+0800\n"
"POT-Creation-Date: 2026-10-12 19:00+0800\n"
"PO-Revision-Date: 2026-06-11 12:37-0300\n"
"Last-Translator: Handle <mail@bysb.net>\n"
"Language-Team: \n"
@@ -4162,6 +4162,16 @@ msgstr "最近的项目"
msgid "Go to Plate"
msgstr "转到盘"
msgid "Device"
msgstr "设备"
# AI Translated
msgid "Device (Web)"
msgstr "设备(网页)"
msgid "Multi-device"
msgstr "多设备"
msgid "AMS Materials Setting"
msgstr "AMS 材料设置"
@@ -4210,6 +4220,9 @@ msgstr "您确定要清除耗材丝信息吗?"
msgid "You need to select the material type and color first."
msgstr "您需要先选择材料类型和颜色。"
msgid "The minimum temperature cannot be greater than the maximum temperature."
msgstr ""
#, c-format, boost-format
msgid "Please input a valid value (K in %.1f~%.1f)"
msgstr "请输入有效的数值(K的范围为%.1f~%.1f)"
@@ -4218,6 +4231,12 @@ msgstr "请输入有效的数值(K的范围为%.1f~%.1f)"
msgid "Please input a valid value (K in %.1f~%.1f, N in %.1f~%.1f)"
msgstr "请输入有效的数值(K的范围为%.1f~%.1f,N的范围为%.1f~%.1f)"
msgid "Favorite filaments"
msgstr ""
msgid "All filaments"
msgstr ""
msgid ""
"The nozzle flow is not set. Please set the nozzle flow rate before editing the filament.\n"
"'Device -> Print parts'"
@@ -6298,19 +6317,9 @@ msgstr "准备"
msgid "Preview"
msgstr "预览"
msgid "Device"
msgstr "设备"
msgid "Multi-device"
msgstr "多设备"
msgid "Project"
msgstr "项目"
# AI Translated
msgid "Device (Web)"
msgstr "设备(网页)"
msgid "Yes"
msgstr "是"
@@ -7006,6 +7015,22 @@ msgstr ".gcode.3mf文件中不包含G-code数据。请使用Orca Slicer进行切
msgid "File '%s' was lost! Please download it again."
msgstr "文件%s丢失,请重新下载。"
msgid "gcodes"
msgstr ""
msgid "Failed to delete the file from printer."
msgstr ""
#, c-format, boost-format
msgid "Do you want to print the file '%s' from printer?"
msgstr ""
msgid "Print file"
msgstr ""
msgid "Failed to start the print job."
msgstr ""
#, c-format, boost-format
msgid ""
"File: %s\n"
@@ -11115,6 +11140,12 @@ msgstr "取消延时摄影,继续打印"
msgid "Clean Up"
msgstr "清理"
msgid "AMS filament mapping is not available for this printer. Clear the AMS mapping before printing."
msgstr ""
msgid "A filament used by this print has no AMS mapping. Assign it before printing."
msgstr ""
msgid "Preparing print job"
msgstr "正在准备打印任务"
@@ -12776,6 +12807,7 @@ msgstr "未勾选"
msgid "Select filtered"
msgstr "选择筛选项"
# AI Translated
msgid "Clear all"
msgstr "清空"
@@ -31147,6 +31179,10 @@ msgstr "启用开发者工具"
msgid "An error occurred loading %s"
msgstr "加载 %s 时出错"
# AI Translated
msgid "/"
msgstr "/"
# AI Translated
#, boost-format
msgid "Load %1% to %2%"
@@ -33214,10 +33250,6 @@ msgstr ""
#~ msgid "An error occurred loading "
#~ msgstr "加载时出错: "
# AI Translated
#~ msgid "/"
#~ msgstr "/"
# AI Translated
#, c-format, boost-format
#~ msgid "Load %s to "
+47 -15
View File
@@ -9,7 +9,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Orca Slicer\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-11 16:21+0800\n"
"POT-Creation-Date: 2026-10-12 19:00+0800\n"
"PO-Revision-Date: 2025-11-28 13:48-0600\n"
"Last-Translator: tntchn <15895303+tntchn@users.noreply.github.com>\n"
"Language-Team: \n"
@@ -4237,6 +4237,16 @@ msgstr "最近的專案"
msgid "Go to Plate"
msgstr "前往列印板"
msgid "Device"
msgstr "裝置"
# AI Translated
msgid "Device (Web)"
msgstr "裝置(網頁)"
msgid "Multi-device"
msgstr "多臺裝置"
msgid "AMS Materials Setting"
msgstr "AMS 線材設定"
@@ -4288,6 +4298,9 @@ msgstr "您確認要清除線材資訊嗎?"
msgid "You need to select the material type and color first."
msgstr "您需要先選擇線材類型和顏色。"
msgid "The minimum temperature cannot be greater than the maximum temperature."
msgstr ""
#, c-format, boost-format
msgid "Please input a valid value (K in %.1f~%.1f)"
msgstr "請輸入有效的數值 (K於 %.1f~%.1f 之間)"
@@ -4296,6 +4309,12 @@ msgstr "請輸入有效的數值 (K於 %.1f~%.1f 之間)"
msgid "Please input a valid value (K in %.1f~%.1f, N in %.1f~%.1f)"
msgstr "請輸入有效的數值 (K於 %.1f~%.1f 之間,N於 %.1f~%.1f 之間)"
msgid "Favorite filaments"
msgstr ""
msgid "All filaments"
msgstr ""
msgid ""
"The nozzle flow is not set. Please set the nozzle flow rate before editing the filament.\n"
"'Device -> Print parts'"
@@ -6381,19 +6400,9 @@ msgstr "準備"
msgid "Preview"
msgstr "預覽"
msgid "Device"
msgstr "裝置"
msgid "Multi-device"
msgstr "多臺裝置"
msgid "Project"
msgstr "專案"
# AI Translated
msgid "Device (Web)"
msgstr "裝置(網頁)"
msgid "Yes"
msgstr "是"
@@ -7088,6 +7097,22 @@ msgstr ".gcode.3mf 檔案不包含 G-code 資料。請使用 OrcaSlicer 切片
msgid "File '%s' was lost! Please download it again."
msgstr "檔案 %s 遺失,請重新下載。"
msgid "gcodes"
msgstr ""
msgid "Failed to delete the file from printer."
msgstr ""
#, c-format, boost-format
msgid "Do you want to print the file '%s' from printer?"
msgstr ""
msgid "Print file"
msgstr ""
msgid "Failed to start the print job."
msgstr ""
#, c-format, boost-format
msgid ""
"File: %s\n"
@@ -11203,6 +11228,12 @@ msgstr "取消縮時攝影並列印"
msgid "Clean Up"
msgstr "清理"
msgid "AMS filament mapping is not available for this printer. Clear the AMS mapping before printing."
msgstr ""
msgid "A filament used by this print has no AMS mapping. Assign it before printing."
msgstr ""
msgid "Preparing print job"
msgstr "正在準備列印作業"
@@ -12852,6 +12883,7 @@ msgstr ""
msgid "Select filtered"
msgstr ""
# AI Translated
msgid "Clear all"
msgstr "清空"
@@ -30937,6 +30969,10 @@ msgstr "啟用開發者工具"
msgid "An error occurred loading %s"
msgstr "載入 %s 時發生錯誤"
# AI Translated
msgid "/"
msgstr "/"
# AI Translated
#, boost-format
msgid "Load %1% to %2%"
@@ -33014,10 +33050,6 @@ msgstr ""
#~ msgid "An error occurred loading "
#~ msgstr "載入時發生錯誤: "
# AI Translated
#~ msgid "/"
#~ msgstr "/"
# AI Translated
#, c-format, boost-format
#~ msgid "Load %s to "
+3
View File
@@ -0,0 +1,3 @@
<svg width="28" height="27" viewBox="0 0 28 27" fill="none" xmlns="http://www.w3.org/2000/svg">
<path transform="matrix(0.9 0 0 0.9 1.4 1.35)" d="M11.9888 2.01601C12.8984 0.494662 15.1024 0.494662 16.012 2.01601L18.8677 6.79237C19.1958 7.34113 19.7339 7.73201 20.3572 7.87447L25.7821 9.11446C27.5101 9.50943 28.1913 11.6056 27.0254 12.9408L23.3652 17.1327C22.9449 17.6143 22.7394 18.2469 22.7964 18.8836L23.2936 24.4263C23.4519 26.1918 21.6687 27.4873 20.0386 26.7911L14.9209 24.6056C14.3329 24.3545 13.6679 24.3545 13.0799 24.6056L7.96209 26.7911C6.33196 27.4873 4.54887 26.1918 4.70722 24.4263L5.20434 18.8836C5.26145 18.2469 5.05595 17.6143 4.63544 17.1327L0.97533 12.9408C-0.19048 11.6056 0.490599 9.50943 2.21859 9.11446L7.64364 7.87447C8.2669 7.73201 8.80494 7.34113 9.13302 6.79237L11.9888 2.01601Z" stroke="#949494" stroke-width="2.4" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 876 B

+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "Qidi",
"version": "02.04.00.16",
"version": "02.04.00.17",
"force_update": "0",
"description": "Qidi configurations",
"machine_model_list": [
@@ -83,7 +83,7 @@
"extruder_clearance_height_to_lid": "120",
"layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nLOG_Z\nG92 E0\n",
"machine_end_gcode": "M141 S0\nM104 S0\nM140 S0\nG1 E-3 F1800\nG0 Z{min(max_print_height, max_layer_z + 3)} F600\nG0 X0 Y0 F12000\n{if max_layer_z < max_print_height / 2}G1 Z{max_print_height / 2 + 10} F600{else}G1 Z{min(max_print_height, max_layer_z + 3)}{endif}",
"machine_start_gcode": "PRINT_START BED=[hot_plate_temp_initial_layer] HOTEND=[nozzle_temperature_initial_layer] CHAMBER=[chamber_temperature]\nM83\nM140 S[hot_plate_temp_initial_layer]\nM104 S[nozzle_temperature_initial_layer]\nG4 P3000\nG0 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85),0)} Y{max((min(print_bed_max[1] - 5, first_layer_print_min[1] + 80) - 85), 0)} Z5 F6000\nG0 Z[initial_layer_print_height] F600\nG1 E3 F1800\nG1 X{(min(print_bed_max[0], first_layer_print_min[0] + 80))} E{85 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 Y{max((min(print_bed_max[1] - 5, first_layer_print_min[1] + 80) - 85), 0) + 2} E{2 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85), 0)} E{85 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 Y{max((min(print_bed_max[1] - 5, first_layer_print_min[1] + 80) - 85), 0) + 85} E{83 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85), 0) + 2} E{2 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 Y{max((min(print_bed_max[1] - 5, first_layer_print_min[1] + 80) - 85), 0) + 3} E{82 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85), 0) + 3} Z0\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85), 0) + 6}\nG1 Z1 F600\n",
"machine_start_gcode": "PRINT_START BED=[bed_temperature_initial_layer_single] HOTEND=[nozzle_temperature_initial_layer] CHAMBER=[chamber_temperature]\nM83\nM140 S[bed_temperature_initial_layer_single]\nM104 S[nozzle_temperature_initial_layer]\nG4 P3000\nG0 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85),0)} Y{max((min(print_bed_max[1] - 5, first_layer_print_min[1] + 80) - 85), 0)} Z5 F6000\nG0 Z[initial_layer_print_height] F600\nG1 E3 F1800\nG1 X{(min(print_bed_max[0], first_layer_print_min[0] + 80))} E{85 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 Y{max((min(print_bed_max[1] - 5, first_layer_print_min[1] + 80) - 85), 0) + 2} E{2 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85), 0)} E{85 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 Y{max((min(print_bed_max[1] - 5, first_layer_print_min[1] + 80) - 85), 0) + 85} E{83 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85), 0) + 2} E{2 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 Y{max((min(print_bed_max[1] - 5, first_layer_print_min[1] + 80) - 85), 0) + 3} E{82 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85), 0) + 3} Z0\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85), 0) + 6}\nG1 Z1 F600\n",
"thumbnails_format": "PNG",
"default_filament_profile": [
"Generic PLA @Qidi Q1 Pro 0.4 nozzle"
@@ -100,7 +100,7 @@
"default_filament_profile": [
"Generic PLA @Qidi"
],
"machine_start_gcode": "G28\nM140 S[hot_plate_temp_initial_layer]\nM190 S[hot_plate_temp_initial_layer]\nM109 S[nozzle_temperature_initial_layer]\nG92 E-19\nG0 Y5 Z0.3 F3600\nG1 X5 E0 F2400\n",
"machine_start_gcode": "G28\nM140 S[bed_temperature_initial_layer_single]\nM190 S[bed_temperature_initial_layer_single]\nM109 S[nozzle_temperature_initial_layer]\nG92 E-19\nG0 Y5 Z0.3 F3600\nG1 X5 E0 F2400\n",
"machine_end_gcode": "M104 S0\nM140 S0\n;Retract the filament\nG92 E0\nG1 E-3 F300\nG28\nM84\n",
"scan_first_layer": "0"
}
@@ -24,7 +24,7 @@
"210x210/COLPIC",
"110x110/PNG"
],
"machine_start_gcode": "PRINT_START\nG28\nM141 S0\nG0 Z50 F600\nM190 S[hot_plate_temp_initial_layer]\nG28 Z\nG29; mesh bed leveling ,comment this code to close it\nG0 X0 Y0 Z50 F6000\nM141 S{overall_chamber_temperature}\nM109 S[nozzle_temperature_initial_layer]\nM106 P3 S255\nM83\nG4 P3000\nG0 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85),0)} Y{max((min(print_bed_max[1], first_layer_print_min[1] + 80) - 85),0)} Z5 F6000\nG0 Z[initial_layer_print_height] F600\nG1 E3 F1800\nG1 X{(min(print_bed_max[0], first_layer_print_min[0] + 80))} E{85 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 Y{max((min(print_bed_max[1], first_layer_print_min[1] + 80) - 85),0) + 2} E{2 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85),0)} E{85 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 Y{max((min(print_bed_max[1], first_layer_print_min[1] + 80) - 85),0) + 85} E{83 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85),0) + 2} E{2 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 Y{max((min(print_bed_max[1], first_layer_print_min[1] + 80) - 85),0) + 3} E{82 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85),0) + 12} E{-10 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 E{10 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\n",
"machine_start_gcode": "PRINT_START\nG28\nM141 S0\nG0 Z50 F600\nM190 S[bed_temperature_initial_layer_single]\nG28 Z\nG29; mesh bed leveling ,comment this code to close it\nG0 X0 Y0 Z50 F6000\nM141 S{overall_chamber_temperature}\nM109 S[nozzle_temperature_initial_layer]\nM106 P3 S255\nM83\nG4 P3000\nG0 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85),0)} Y{max((min(print_bed_max[1], first_layer_print_min[1] + 80) - 85),0)} Z5 F6000\nG0 Z[initial_layer_print_height] F600\nG1 E3 F1800\nG1 X{(min(print_bed_max[0], first_layer_print_min[0] + 80))} E{85 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 Y{max((min(print_bed_max[1], first_layer_print_min[1] + 80) - 85),0) + 2} E{2 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85),0)} E{85 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 Y{max((min(print_bed_max[1], first_layer_print_min[1] + 80) - 85),0) + 85} E{83 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85),0) + 2} E{2 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 Y{max((min(print_bed_max[1], first_layer_print_min[1] + 80) - 85),0) + 3} E{82 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85),0) + 12} E{-10 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 E{10 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\n",
"machine_end_gcode": "M141 S0\nM104 S0\nM140 S0\nG1 E-3 F1800\nG0 Z{min(max_print_height, max_layer_z + 3)} F600\nG0 X0 Y0 F12000\n{if max_layer_z < max_print_height / 2}G1 Z{max_print_height / 2 + 10} F600{else}G1 Z{min(max_print_height, max_layer_z + 3)}{endif}",
"scan_first_layer": "0"
}
+7
View File
@@ -126,6 +126,13 @@
</array>
<key>LSMinimumSystemVersion</key>
<string>10.10</string>
<key>NSLocalNetworkUsageDescription</key>
<string>OrcaSlicer uses your local network to discover and connect to printers.</string>
<key>NSBonjourServices</key>
<array>
<string>_octoprint._tcp</string>
<string>_http._tcp</string>
</array>
<key>NSPrincipalClass</key>
<string>NSApplication</string>
<key>NSHighResolutionCapable</key>
+2 -2
View File
@@ -1729,8 +1729,8 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
//plate->pattern_file = it->second->pattern_file;
plate->no_light_thumbnail_file = it->second->no_light_thumbnail_file;
plate->top_file = it->second->top_file;
plate->pick_file = it->second->pick_file.empty();
plate->pattern_bbox_file = it->second->pattern_bbox_file.empty();
plate->pick_file = it->second->pick_file;
plate->pattern_bbox_file = it->second->pattern_bbox_file;
plate->config = it->second->config;
if (!plate->thumbnail_file.empty())
+1 -1
View File
@@ -3295,7 +3295,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato
m_is_role_based_fan_on.fill(false);
m_role_based_fan_marker_layer.fill(-1);
m_fan_mover.release();
m_fan_mover.reset();
m_ordering_cache.clear();
// Belt printer: initialize belt-specific writer via virtual hook.
+111 -36
View File
@@ -4102,7 +4102,11 @@ void PresetBundle::get_ams_cobox_infos(AMSComboInfo& combox_info)
auto ams_name = ams.opt_string("tray_name", 0u);
auto filament_changed = !ams.has("filament_changed") || ams.opt_bool("filament_changed");
auto filament_multi_color = ams.opt<ConfigOptionStrings>("filament_multi_colour")->values;
if (filament_id.empty()) {
auto filament_type = ams.opt_string("filament_type", 0u);
auto is_placeholder = ams.has("filament_slot_placeholder") && ams.opt_bool("filament_slot_placeholder", 0u);
// A printer-set tray carries a material type but no preset id; resolve it to the
// matching Generic preset so it is not dropped and the combo array keeps aligning.
if (filament_id.empty() && (is_placeholder || filament_type.empty())) {
continue;
}
if (!filament_changed && this->filament_presets.size() > combox_info.ams_filament_presets.size()) {
@@ -4112,16 +4116,48 @@ void PresetBundle::get_ams_cobox_infos(AMSComboInfo& combox_info)
combox_info.ams_names.push_back(ams_name);
continue;
}
auto iter = std::find_if(filaments.begin(), filaments.end(),
[this, &filament_id](auto &f) { return f.is_compatible && filaments.get_preset_base(f) == &f && f.filament_id == filament_id; });
warn_ambiguous_filament_id_match(filaments, iter, filament_id);
auto iter = filaments.end();
if (!filament_id.empty()) {
iter = std::find_if(filaments.begin(), filaments.end(),
[this, &filament_id](auto &f) { return f.is_compatible && filaments.get_preset_base(f) == &f && f.filament_id == filament_id; });
warn_ambiguous_filament_id_match(filaments, iter, filament_id);
}
if (iter == filaments.end()) {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": filament_id %1% not found or system or compatible") % filament_id;
auto filament_type = ams.opt_string("filament_type", 0u);
if (!filament_type.empty()) {
auto original_type = filament_type;
filament_type = "Generic " + filament_type;
iter = std::find_if(filaments.begin(), filaments.end(),
[&filament_type](auto &f) { return f.is_compatible && f.is_system && boost::algorithm::starts_with(f.name, filament_type); });
if (iter == filaments.end()) {
// Similarity fallback: find a generic preset whose filament_type
// appears as a whole word in the AMS type (e.g. "ASA" in "ASA Sparkle").
auto upper_type = boost::to_upper_copy(original_type);
auto contains_word = [](const std::string& haystack, const std::string& needle) {
auto pos = haystack.find(needle);
while (pos != std::string::npos) {
bool start_ok = (pos == 0 || !std::isalnum(static_cast<unsigned char>(haystack[pos - 1])));
bool end_ok = (pos + needle.size() >= haystack.size() ||
!std::isalnum(static_cast<unsigned char>(haystack[pos + needle.size()])));
if (start_ok && end_ok)
return true;
pos = haystack.find(needle, pos + 1);
}
return false;
};
// Find the longest-matching preset type to prefer e.g. "PA-CF" over "PA".
size_t best_len = 0;
for (auto it = filaments.begin(); it != filaments.end(); ++it) {
if (!it->is_compatible || !it->is_system || !boost::algorithm::starts_with(it->name, "Generic "))
continue;
auto preset_type = boost::to_upper_copy(it->config.opt_string("filament_type", 0u));
if (preset_type.size() > best_len && contains_word(upper_type, preset_type)) {
iter = it;
best_len = preset_type.size();
filament_type = "Generic " + it->config.opt_string("filament_type", 0u);
}
}
}
}
if (iter == filaments.end()) {
// Prefer old selection
@@ -4132,7 +4168,11 @@ void PresetBundle::get_ams_cobox_infos(AMSComboInfo& combox_info)
combox_info.ams_names.push_back(ams_name);
continue;
}
iter = std::find_if(filaments.begin(), filaments.end(), [](auto &f) { return f.is_compatible && f.is_system; });
iter = std::find_if(filaments.begin(), filaments.end(), [](auto &f) {
return f.is_compatible && f.is_system && boost::algorithm::starts_with(f.name, "Generic ");
});
if (iter == filaments.end())
iter = std::find_if(filaments.begin(), filaments.end(), [](auto &f) { return f.is_compatible && f.is_system; });
if (iter == filaments.end())
continue;
}
@@ -4170,6 +4210,7 @@ unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfi
for (auto &entry : filament_ams_list) {
auto & ams = entry.second;
auto filament_id = ams.opt_string("filament_id", 0u);
const bool printer_set_tray = filament_id.empty();
auto filament_color = ams.opt_string("filament_colour", 0u);
auto filament_color_type = ams.opt_string("filament_colour_type", 0u);
auto filament_changed = !ams.has("filament_changed") || ams.opt_bool("filament_changed");
@@ -4182,42 +4223,58 @@ unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfi
ams_array_maps.push_back(temp);
index++;
if (filament_id.empty()) {
if (use_map) {
for (int j = maps.size() - 1; j >= 0; j--) {
if (maps[j].slot_id == slot_id && maps[j].ams_id == ams_id) {
maps.erase(j);
// A tray written by the printer UI carries a material type but no
// OrcaSlicer preset id. Resolve it to the matching Generic preset with
// the tray's own color instead of dropping it (direct sync) or forcing
// "Generic PLA" (mapping sync). Placeholders and typeless trays keep
// the previous behavior.
const auto tray_type = ams.opt_string("filament_type", 0u);
if (is_placeholder || tray_type.empty()) {
if (use_map) {
for (int j = maps.size() - 1; j >= 0; j--) {
if (maps[j].slot_id == slot_id && maps[j].ams_id == ams_id) {
maps.erase(j);
}
}
ams_filament_presets.push_back("Generic PLA");//for unknow matieral
auto default_unknown_color = "#CECECE";
ams_filament_colors.push_back(default_unknown_color);
ams_filament_color_types.push_back("1");
if (filament_multi_color.size() == 0) {
filament_multi_color.push_back(default_unknown_color);
}
ams_multi_color_filment.push_back(filament_multi_color);
} else if (is_placeholder) {
// Orca: push placeholders to keep index alignment with ams_infos
ams_filament_presets.push_back("");
ams_filament_colors.push_back("");
ams_filament_color_types.push_back("");
ams_multi_color_filment.push_back({});
}
ams_filament_presets.push_back("Generic PLA");//for unknow matieral
auto default_unknown_color = "#CECECE";
ams_filament_colors.push_back(default_unknown_color);
ams_filament_color_types.push_back("1");
if (filament_multi_color.size() == 0) {
filament_multi_color.push_back(default_unknown_color);
}
ams_multi_color_filment.push_back(filament_multi_color);
} else if (is_placeholder) {
// Orca: push placeholders to keep index alignment with ams_infos
ams_filament_presets.push_back("");
ams_filament_colors.push_back("");
ams_filament_color_types.push_back("");
ams_multi_color_filment.push_back({});
continue;
}
continue;
}
if (!filament_changed && this->filament_presets.size() > ams_filament_presets.size()) {
ams_filament_presets.push_back(this->filament_presets[ams_filament_presets.size()]);
ams_filament_colors.push_back(filament_color);
ams_filament_color_types.push_back(filament_color_type);
ams_multi_color_filment.push_back(filament_multi_color);
ams_infos.back().valid = true;
continue;
}
bool has_type = false;
auto filament_type = ams.opt_string("filament_type", 0u);
auto iter = std::find_if(filaments.begin(), filaments.end(), [this, &filament_id, &has_type, filament_type](auto &f) {
has_type |= f.config.opt_string("filament_type", 0u) == filament_type;
return f.is_compatible && filaments.get_preset_base(f) == &f && f.filament_id == filament_id; });
warn_ambiguous_filament_id_match(filaments, iter, filament_id);
auto iter = filaments.end();
if (!filament_id.empty()) {
iter = std::find_if(filaments.begin(), filaments.end(), [this, &filament_id, &has_type, filament_type](auto &f) {
has_type |= f.config.opt_string("filament_type", 0u) == filament_type;
return f.is_compatible && filaments.get_preset_base(f) == &f && f.filament_id == filament_id; });
warn_ambiguous_filament_id_match(filaments, iter, filament_id);
} else {
// The material type is the only identity a printer-set tray carries.
has_type = std::any_of(filaments.begin(), filaments.end(), [&filament_type](auto &f) {
return f.is_compatible && f.config.opt_string("filament_type", 0u) == filament_type; });
}
if (iter == filaments.end()) {
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": filament_id %1% not found or system or compatible") % filament_id;
if (!filament_type.empty()) {
@@ -4257,6 +4314,7 @@ unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfi
}
}
}
const bool resolved_by_material_type = printer_set_tray && iter != filaments.end();
if (iter == filaments.end()) {
// Prefer old selection
if (ams_filament_presets.size() < this->filament_presets.size()) {
@@ -4264,6 +4322,7 @@ unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfi
ams_filament_colors.push_back(filament_color);
ams_filament_color_types.push_back(filament_color_type);
ams_multi_color_filment.push_back(filament_multi_color);
ams_infos.back().valid = true;
unknowns.emplace_back(&ams, has_type ? L("The filament may not be compatible with the current machine settings. Generic filament presets will be used.") :
L("The filament model is unknown. Still using the previous filament preset."));
continue;
@@ -4279,7 +4338,8 @@ unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfi
if (iter == filaments.end())
continue;
}
unknowns.emplace_back(&ams, boost::algorithm::starts_with(iter->name, filament_type) ?
if (!resolved_by_material_type)
unknowns.emplace_back(&ams, boost::algorithm::starts_with(iter->name, filament_type) ?
(has_type ? L("The filament may not be compatible with the current machine settings. Generic filament presets will be used.") :
L("The filament model is unknown. Generic filament presets will be used.")) :
(has_type ? L("The filament may not be compatible with the current machine settings. A random filament preset will be used.") :
@@ -4290,6 +4350,7 @@ unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfi
ams_filament_colors.push_back(filament_color);
ams_filament_color_types.push_back(filament_color_type);
ams_multi_color_filment.push_back(filament_multi_color);
ams_infos.back().valid = true;
}
if (ams_filament_presets.empty())
return 0;
@@ -4729,9 +4790,21 @@ std::vector<std::vector<DynamicPrintConfig>> PresetBundle::get_extruder_filament
return filament_infos;
}
// ORCA TODO: currently, this function assumes the printer name follows the pattern of "<printer_model> <nozzle_diameter>", e.g.
// printer_type: "Bambu Lab X2D", nozzle_diameter_str: "0.4 nozzle" => printer_name: "Bambu Lab X2D 0.4 nozzle". If the printer name does
// not follow this pattern, the function may not work correctly.
std::string PresetBundle::get_printer_model_display_name(const std::string &model_id) const
{
if (model_id.empty())
return {};
for (const auto &vendor_entry : vendors) {
for (const auto &model : vendor_entry.second.models) {
if (model.model_id == model_id)
return model.name;
}
}
return {};
}
// ORCA TODO: this assumes printer names follow "<printer_model> <nozzle_diameter>", e.g.
// "Bambu Lab X2D 0.4 nozzle". Other naming schemes may not resolve correctly.
std::set<std::string> PresetBundle::get_printer_names_by_printer_type_and_nozzle(const std::string &printer_type, std::string nozzle_diameter_str, bool system_only)
{
std::set<std::string> printer_names;
@@ -4753,7 +4826,9 @@ std::set<std::string> PresetBundle::get_printer_names_by_printer_type_and_nozzle
if (printer_it->name.find(nozzle_diameter_str) != std::string::npos) printer_names.insert(printer_it->name);
}
assert(printer_names.size() == 1);
// No match is normal for a connected machine the user has not installed; only an
// ambiguous match is a bug (the caller assumes one preset per model and nozzle).
assert(printer_names.size() <= 1);
for (auto& printer_name : printer_names) {
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " " << __LINE__ << " printer name: " << printer_name;
@@ -4766,8 +4841,8 @@ std::vector<Preset *> PresetBundle::get_filament_presets_for_machine(const std::
const std::string &nozzle_diameter_str,
bool include_user_presets)
{
// Printer model plus nozzle diameter is expected to resolve to a single system printer preset;
// get_printer_names_by_printer_type_and_nozzle asserts as much in debug builds.
// Printer model plus nozzle diameter normally resolves to a single system printer preset.
// Zero matches is normal for a connected machine the user never installed.
const std::set<std::string> printer_names = get_printer_names_by_printer_type_and_nozzle(printer_type, nozzle_diameter_str);
const Preset *printer = printer_names.empty() ? nullptr : printers.find_preset(*printer_names.begin());
if (printer == nullptr)
+3
View File
@@ -413,6 +413,9 @@ public:
std::vector<std::vector<DynamicPrintConfig>> get_extruder_filament_info() const;
// Resolve a vendor model id (an agent-reported machine identity) to the display name
// printer profiles use as printer_model. Empty when no loaded vendor declares it.
std::string get_printer_model_display_name(const std::string &model_id) const;
std::set<std::string> get_printer_names_by_printer_type_and_nozzle(const std::string &printer_type, std::string nozzle_diameter_str, bool system_only = true);
// Orca: the root filament presets a connected machine can use, resolved with the rule the rest
// of the app applies (is_compatible_with_printer): an empty compatible_printers means every
-55
View File
@@ -1570,61 +1570,6 @@ std::vector<TextureIsland> compute_connected_net(const PatchUnwrap &unwrap)
return islands;
}
std::vector<uint8_t> transfer_facet_paint(const indexed_triangle_set &src_mesh,
const TriangleSelector::TriangleSplittingData &src_paint,
const indexed_triangle_set &dst_mesh)
{
if (src_mesh.indices.empty() || dst_mesh.indices.empty() || src_paint.bitstream.empty())
return {};
const TriangleMesh src(src_mesh);
TriangleSelector selector(src);
selector.deserialize(src_paint, false);
// Every sub-triangle of the source, each carrying its own state - NONE included, so the soup
// covers the whole surface and a foot point always lands on something. One soup rather than one
// tree per state: a foot point has one nearest triangle, and that triangle's state is the answer.
std::vector<indexed_triangle_set> per_state;
selector.get_facets(per_state);
indexed_triangle_set soup;
std::vector<uint8_t> label;
for (size_t st = 0; st < per_state.size(); ++st) {
const indexed_triangle_set &part = per_state[st];
if (part.indices.empty())
continue;
const int base = int(soup.vertices.size());
soup.vertices.insert(soup.vertices.end(), part.vertices.begin(), part.vertices.end());
for (const stl_triangle_vertex_indices &t : part.indices) {
soup.indices.emplace_back(t[0] + base, t[1] + base, t[2] + base);
label.push_back(uint8_t(st));
}
}
if (soup.indices.empty())
return {};
const AABBTreeIndirect::Tree3f tree =
AABBTreeIndirect::build_aabb_tree_over_indexed_triangle_set(soup.vertices, soup.indices);
std::vector<uint8_t> out(dst_mesh.indices.size(), 0);
tbb::parallel_for(tbb::blocked_range<size_t>(0, dst_mesh.indices.size()),
[&](const tbb::blocked_range<size_t> &range) {
for (size_t j = range.begin(); j < range.end(); ++j) {
const stl_triangle_vertex_indices &t = dst_mesh.indices[j];
// The centroid, not a corner: a facet takes one state, and its middle is the point that
// is inside it whatever the triangle looks like.
const Vec3f centroid = (dst_mesh.vertices[size_t(t[0])] + dst_mesh.vertices[size_t(t[1])] +
dst_mesh.vertices[size_t(t[2])]) / 3.f;
size_t hit = 0;
Vec3f foot;
const float d2 = AABBTreeIndirect::squared_distance_to_indexed_triangle_set(soup.vertices, soup.indices,
tree, centroid, hit, foot);
if (d2 >= 0.f && hit < label.size())
out[j] = label[hit];
}
});
return out;
}
void average_island_scales(std::vector<TextureIsland> &islands)
{
if (islands.empty())
-25
View File
@@ -578,31 +578,6 @@ bool sample_layer_color(const DecodedHeightTexture &texture, const TextureDispla
const Vec3f &patch_center = Vec3f::Zero(), const Vec3f &patch_axis = Vec3f::UnitZ(),
const Vec2f *lscm_uv = nullptr);
// Carries a per-facet paint annotation - the colour paint above all - from a mesh onto the mesh a
// bake produced from it: every triangle of `dst_mesh` takes the state painted at its own foot point on
// `src_mesh`, i.e. on the nearest point of the surface it was built from. Returned as one
// EnforcerBlockerType per `dst_mesh` triangle, 0 (NONE) where that foot point carried no paint.
//
// ModelVolume::restore_painting() cannot do this. Its remap pairs a source paint triangle with a
// target triangle that *overlaps* it to within 0.01 mm, which is right for a remesh or a
// simplification - they leave the surface where it was - and useless after a bake, which moves the
// surface by the relief depth. Measured on a model displaced by a tenth of its own size, that remap
// kept a quarter of the colour and left the rest unpainted; this keeps essentially all of it, because
// a foot point is exactly as far away as the relief is deep and the lookup does not care how far that
// is.
//
// The source's *sub*-triangles are what is looked up, not its triangles, so the part of a triangle a
// brush stroke only clipped a corner off keeps its own state rather than rounding to the whole
// triangle's. No distance limit is needed or wanted: the nearest point on the source surface is where
// a baked triangle came from however far the relief carried it, and an unpainted origin reports NONE
// on its own.
//
// Returns an empty vector when `src_paint` is empty or either mesh is - the caller then has nothing to
// carry, which is not the same as "carry nothing".
std::vector<uint8_t> transfer_facet_paint(const indexed_triangle_set &src_mesh,
const TriangleSelector::TriangleSplittingData &src_paint,
const indexed_triangle_set &dst_mesh);
// Area-weighted centroid and average normal of a layer's currently painted patch, in mesh-local
// coordinates - the same measurements build_texture_displacement() uses to pick its dominant
// projection axis. Used by the GUI to anchor the on-canvas "adjust texture placement" gizmo to
+14 -1
View File
@@ -372,6 +372,10 @@ set(SLIC3R_GUI_SOURCES
GUI/MarkdownTip.hpp
GUI/MediaFilePanel.cpp
GUI/MediaFilePanel.h
GUI/OrcaFilesPanel.cpp
GUI/OrcaFilesPanel.h
GUI/StoragePanel.cpp
GUI/StoragePanel.h
GUI/MediaPlayCtrl.cpp
GUI/MediaPlayCtrl.h
GUI/WebRtcMediaController.cpp
@@ -463,6 +467,11 @@ set(SLIC3R_GUI_SOURCES
GUI/PresetHints.hpp
GUI/PrinterCloudAuthDialog.cpp
GUI/PrinterCloudAuthDialog.hpp
GUI/Printer/BambuFileGridModel.cpp
GUI/Printer/BambuFileGridModel.h
GUI/Printer/IFileGridModel.h
GUI/Printer/RemoteFileGridModel.cpp
GUI/Printer/RemoteFileGridModel.h
GUI/Printer/PrinterFileSystem.cpp
GUI/Printer/PrinterFileSystem.h
GUI/PrinterWebView.cpp
@@ -776,8 +785,12 @@ set(SLIC3R_GUI_SOURCES
Utils/ICameraSignalingChannel.hpp
Utils/OrcaCloudServiceAgent.cpp
Utils/OrcaCloudServiceAgent.hpp
Utils/OrcaMqttConnection.cpp
Utils/OrcaMqttConnection.hpp
Utils/OrcaPrinterAgent.cpp
Utils/OrcaPrinterAgent.hpp
Utils/OrcaCloudSignalingChannel.cpp
Utils/OrcaCloudSignalingChannel.hpp
Utils/QidiPrinterAgent.cpp
Utils/QidiPrinterAgent.hpp
Utils/SnapmakerPrinterAgent.cpp
@@ -916,7 +929,7 @@ target_include_directories(libslic3r_gui PRIVATE Utils ${CMAKE_CURRENT_BINARY_DI
if (WIN32)
target_include_directories(libslic3r_gui SYSTEM PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../../deps/WebView2/include)
target_link_libraries(libslic3r_gui Advapi32)
target_link_libraries(libslic3r_gui Advapi32 Crypt32)
endif()
source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} FILES ${SLIC3R_GUI_SOURCES})
+10 -1
View File
@@ -7,6 +7,7 @@
#include "slic3r/GUI/DeviceCore/DevExtruderSystem.h"
#include "slic3r/GUI/DeviceCore/DevManager.h"
#include "slic3r/GUI/DeviceManager.hpp"
#include "slic3r/GUI/MsgDialog.hpp"
@@ -1633,8 +1634,16 @@ int AMSDryCtrWin::update_filament_list(DevAms* dev_ams, MachineObject* obj)
stream << std::fixed << std::setprecision(1) << obj->GetExtderSystem()->GetNozzleDiameter(extruder_id);
std::string nozzle_diameter_str = stream.str();
// The connected device's model may not resolve (OrcaSonar's is optional); the
// helper falls back to the selected profile so the list is never empty.
const std::string filament_printer_model = resolve_filament_printer_model(obj->printer_type, preset_bundle);
if (filament_printer_model.empty()) {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << " cannot resolve a printer model for the filament list";
return false;
}
for (Preset *filament_it : preset_bundle->get_filament_presets_for_machine(
DevPrinterConfigUtil::get_printer_display_name(obj->printer_type), nozzle_diameter_str, obj->is_support_user_preset)) {
filament_printer_model, nozzle_diameter_str, obj->is_support_user_preset)) {
if (!filament_id_set.insert(filament_it->filament_id).second)
continue;
const std::string filament_alias = filaments.get_preset_alias(*filament_it, true);
+269 -90
View File
@@ -4,10 +4,12 @@
#include "GUI_App.hpp"
#include "GUI.hpp"
#include "slic3r/Utils/NetworkAgent.hpp"
#include "slic3r/Utils/NetworkAgentFactory.hpp"
#include "libslic3r/Preset.hpp"
#include "I18N.hpp"
#include <algorithm>
#include <boost/log/trivial.hpp>
#include <slic3r/GUI/DeviceManager.hpp>
#include <string>
#include <sstream>
#include <ios>
@@ -16,6 +18,7 @@
#include <wx/checklst.h>
#include "slic3r/GUI/GUI_Utils.hpp"
#include "slic3r/GUI/Widgets/Button.hpp"
#include "slic3r/GUI/Widgets/DropDown.hpp"
#include "slic3r/GUI/Widgets/Label.hpp"
#include <utility>
#include "slic3r/GUI/wxExtensions.hpp"
@@ -172,7 +175,26 @@ void AMSMaterialsSetting::create()
Bind(wxEVT_PAINT, &AMSMaterialsSetting::paintEvent, this);
Bind(EVT_SELECTED_COLOR, &AMSMaterialsSetting::on_picker_color, this);
m_comboBox_filament->Connect(wxEVT_COMMAND_COMBOBOX_SELECTED, wxCommandEventHandler(AMSMaterialsSetting::on_select_filament), NULL, this);
m_comboBox_filament->Connect(wxEVT_COMMAND_COMBOBOX_SELECTED, wxCommandEventHandler(AMSMaterialsSetting::on_select_filament), NULL, this);
m_comboBox_filament->Bind(EVT_DROPDOWN_ITEM_ACTION, &AMSMaterialsSetting::on_toggle_favorite_filament, this);
// Favorites toggled in the open list are regrouped just before it opens again, so rows never move
// under the cursor. These handlers run ahead of ComboBox's own, which then measure and show the
// rebuilt rows; wxEVT_COMBOBOX_DROPDOWN would be too late, as ComboBox sends it after showing the list.
auto regroup_favorites = [this]() {
if (!m_favorites_regroup || m_comboBox_filament->is_drop_down())
return;
const FilamentInfos* info = selected_filament_info();
const std::string filament_id = info ? info->filament_id : std::string();
rebuild_filament_dropdown_items(filament_id);
};
m_comboBox_filament->Bind(wxEVT_LEFT_DOWN, [regroup_favorites](wxMouseEvent& e) { regroup_favorites(); e.Skip(); });
m_comboBox_filament->Bind(wxEVT_LEFT_DCLICK, [regroup_favorites](wxMouseEvent& e) { regroup_favorites(); e.Skip(); });
m_comboBox_filament->Bind(wxEVT_KEY_DOWN, [regroup_favorites](wxKeyEvent& e) {
if (e.GetKeyCode() == WXK_RETURN || e.GetKeyCode() == WXK_SPACE) // the keys that open the list
regroup_favorites();
e.Skip();
});
m_comboBox_cali_result->Connect(wxEVT_COMMAND_COMBOBOX_SELECTED, wxCommandEventHandler(AMSMaterialsSetting::on_select_cali_result), NULL, this);
}
@@ -191,11 +213,9 @@ void AMSMaterialsSetting::create_panel_normal(wxWindow* parent)
m_sizer_filament->Add(0, 0, 0, wxEXPAND, 0);
#ifdef __APPLE__
m_comboBox_filament = new wxComboBox(parent, wxID_ANY, wxEmptyString, wxDefaultPosition, AMS_MATERIALS_SETTING_COMBOX_WIDTH, 0, nullptr, wxCB_READONLY);
#else
m_comboBox_filament = new ::ComboBox(parent, wxID_ANY, wxEmptyString, wxDefaultPosition, AMS_MATERIALS_SETTING_COMBOX_WIDTH, 0, nullptr, wxCB_READONLY);
#endif
m_star_filled = ScalableBitmap(this, "score_star_light", 16);
m_star_empty = ScalableBitmap(this, "score_star_outline", 16);
m_sizer_filament->Add(m_comboBox_filament, 1, wxALIGN_CENTER, 0);
@@ -420,6 +440,7 @@ void AMSMaterialsSetting::paintEvent(wxPaintEvent &evt)
AMSMaterialsSetting::~AMSMaterialsSetting()
{
m_comboBox_filament->Disconnect(wxEVT_COMMAND_COMBOBOX_SELECTED, wxCommandEventHandler(AMSMaterialsSetting::on_select_filament), NULL, this);
m_comboBox_filament->Unbind(EVT_DROPDOWN_ITEM_ACTION, &AMSMaterialsSetting::on_toggle_favorite_filament, this);
m_comboBox_cali_result->Disconnect(wxEVT_COMMAND_COMBOBOX_SELECTED, wxCommandEventHandler(AMSMaterialsSetting::on_select_cali_result), NULL, this);
}
@@ -477,6 +498,13 @@ void AMSMaterialsSetting::update_filament_editing(bool is_printing)
m_button_confirm->Show(true);
}
// A third-party tray owns its temp range; BBL RFID trays keep the read-only preset values.
// Only the OrcaSonar agent accepts the temp fields, so other agents stay read-only.
const bool is_orca_agent = obj && obj->printer_agent_id == ORCA_PRINTER_AGENT_ID;
const bool can_edit = !is_printing || obj->is_support_filament_setting_inprinting;
m_input_nozzle_min->Enable(m_is_third && can_edit && is_orca_agent);
m_input_nozzle_max->Enable(m_is_third && can_edit && is_orca_agent);
if (!m_is_third) {
m_tip_readonly->SetLabelText(wxEmptyString);
m_tip_readonly->Hide();
@@ -499,6 +527,8 @@ void AMSMaterialsSetting::update_filament_editing(bool is_printing)
if (m_view_only) { // Orca: view-only (2D laser/cut) — lock every edit control and hide apply/reset
m_comboBox_filament->Enable(false);
m_comboBox_cali_result->Enable(false);
m_input_nozzle_min->Enable(false);
m_input_nozzle_max->Enable(false);
m_input_k_val->Enable(false);
m_input_n_val->Enable(false);
m_button_confirm->Hide();
@@ -531,10 +561,9 @@ void AMSMaterialsSetting::on_select_reset(wxCommandEvent& event) {
std::string selected_ams_id;
PresetBundle *preset_bundle = wxGetApp().preset_bundle;
if (preset_bundle) {
const FilamentInfos* selected_info = selected_filament_info();
for (auto it = preset_bundle->filaments.begin(); it != preset_bundle->filaments.end(); it++) {
auto filament_item = map_filament_items[m_comboBox_filament->GetValue().ToStdString()];
std::string filament_id = filament_item.filament_id;
if (it->filament_id.compare(filament_id) == 0) {
if (selected_info && it->filament_id.compare(selected_info->filament_id) == 0) {
selected_ams_id = it->filament_id;
break;
}
@@ -548,7 +577,7 @@ void AMSMaterialsSetting::on_select_reset(wxCommandEvent& event) {
}
// set k / n value
if (obj->cali_version <= -1 && obj->get_printer_series() == PrinterSeries::SERIES_P1P) {
if (obj->supports_extrusion_cali() && obj->cali_version <= -1 && obj->get_printer_series() == PrinterSeries::SERIES_P1P) {
// set extrusion cali ratio
int cali_tray_id = ams_id * 4 + slot_id;
@@ -569,7 +598,7 @@ void AMSMaterialsSetting::on_select_reset(wxCommandEvent& event) {
}
obj->command_extrusion_cali_set(cali_tray_id, "", "", k, n);
}
else {
else if (obj->supports_extrusion_cali()) {
PACalibIndexInfo select_index_info;
int tray_id = ams_id * 4 + slot_id;
if (is_virtual_tray()) {
@@ -682,12 +711,13 @@ void AMSMaterialsSetting::on_select_ok(wxCommandEvent &event)
ams_setting_id = "";
// the combobox item
auto filament_item = map_filament_items[m_comboBox_filament->GetValue().ToStdString()];
const FilamentInfos* selected_info = selected_filament_info();
const std::string filament_id = selected_info ? selected_info->filament_id : std::string();
// check filament info (rack-gated per-nozzle blacklist evaluation)
PresetBundle* preset_bundle = wxGetApp().preset_bundle;
const auto& fila_check_res = sCheckFilamentInfo(preset_bundle, obj, ams_id, slot_id,
filament_item.filament_id, ams_filament_id, ams_setting_id);
filament_id, ams_filament_id, ams_setting_id);
if (const auto& prohibit_items = fila_check_res.get_items_by_action("prohibition"); !prohibit_items.empty()) {
wxString info_msg;
@@ -728,6 +758,21 @@ void AMSMaterialsSetting::on_select_ok(wxCommandEvent &event)
return;
}
if (nozzle_temp_min_int < FILAMENT_MIN_TEMP || nozzle_temp_min_int > FILAMENT_MAX_TEMP ||
nozzle_temp_max_int < FILAMENT_MIN_TEMP || nozzle_temp_max_int > FILAMENT_MAX_TEMP) {
MessageDialog msg_dlg(nullptr,
wxString::Format(_L("The input value should be greater than %1% and less than %2%"), FILAMENT_MIN_TEMP, FILAMENT_MAX_TEMP),
wxEmptyString, wxICON_WARNING | wxOK);
msg_dlg.ShowModal();
return;
}
if (nozzle_temp_min_int > nozzle_temp_max_int) {
MessageDialog msg_dlg(nullptr, _L("The minimum temperature cannot be greater than the maximum temperature."),
wxEmptyString, wxICON_WARNING | wxOK);
msg_dlg.ShowModal();
return;
}
// Orca: log the tray payload this dialog hands the printer, so the filament_id resolved from the
// dropdown selection can be checked against the tray_info_idx the AMS actually receives. A
@@ -751,7 +796,7 @@ void AMSMaterialsSetting::on_select_ok(wxCommandEvent &event)
wxString k_text = m_input_k_val->GetTextCtrl()->GetValue();
wxString n_text = m_input_n_val->GetTextCtrl()->GetValue();
if (obj->cali_version <= -1 && (obj->get_printer_series() != PrinterSeries::SERIES_X1) && !ExtrusionCalibration::check_k_validation(k_text)) {
if (obj->supports_extrusion_cali() && obj->cali_version <= -1 && (obj->get_printer_series() != PrinterSeries::SERIES_X1) && !ExtrusionCalibration::check_k_validation(k_text)) {
wxString k_tips = wxString::Format(_L("Please input a valid value (K in %.1f~%.1f)"), MIN_PA_K_VALUE, MAX_PA_K_VALUE);
wxString kn_tips = wxString::Format(_L("Please input a valid value (K in %.1f~%.1f, N in %.1f~%.1f)"), MIN_PA_K_VALUE, MAX_PA_K_VALUE, 0.6, 2.0);
MessageDialog msg_dlg(nullptr, k_tips, wxEmptyString, wxICON_WARNING | wxOK);
@@ -781,7 +826,7 @@ void AMSMaterialsSetting::on_select_ok(wxCommandEvent &event)
vt_tray = VIRTUAL_TRAY_DEPUTY_ID;
}
if (obj->cali_version >= 0) {
if (obj->supports_extrusion_cali() && obj->cali_version >= 0) {
PACalibIndexInfo select_index_info;
select_index_info.tray_id = vt_tray;
select_index_info.ams_id = ams_id;
@@ -800,7 +845,7 @@ void AMSMaterialsSetting::on_select_ok(wxCommandEvent &event)
CalibUtils::select_PA_calib_result(select_index_info);
}
else {
else if (obj->supports_extrusion_cali()) {
obj->command_extrusion_cali_set(vt_tray, "", "", k, n);
}
}
@@ -822,7 +867,7 @@ void AMSMaterialsSetting::on_select_ok(wxCommandEvent &event)
;
}
if (obj->cali_version >= 0) {
if (obj->supports_extrusion_cali() && obj->cali_version >= 0) {
PACalibIndexInfo select_index_info;
select_index_info.tray_id = cali_tray_id;
select_index_info.ams_id = ams_id;
@@ -841,7 +886,7 @@ void AMSMaterialsSetting::on_select_ok(wxCommandEvent &event)
CalibUtils::select_PA_calib_result(select_index_info);
}
else {
else if (obj->supports_extrusion_cali()) {
obj->command_extrusion_cali_set(cali_tray_id, "", "", k, n);
}
}
@@ -937,7 +982,12 @@ bool AMSMaterialsSetting::is_virtual_tray()
void AMSMaterialsSetting::update_widgets()
{
if (obj && obj->get_printer_series() == PrinterSeries::SERIES_X1 && obj->cali_version <= -1) {
if (obj && !obj->supports_extrusion_cali()) {
// No printer-side K/N records: do not offer them.
m_panel_normal->Show();
m_panel_kn->Hide();
}
else if (obj && obj->get_printer_series() == PrinterSeries::SERIES_X1 && obj->cali_version <= -1) {
// Low version firmware does not display k value
m_panel_kn->Hide();
}
@@ -986,6 +1036,133 @@ static void _collect_filament_info(const wxString& shown_name,
query_filament_types[shown_name] = filament.config.get_filament_type();
}
static const char* AMS_FAVORITE_FILAMENT_IDS_KEY = "ams_favorite_filament_ids";
const AMSMaterialsSetting::FilamentInfos* AMSMaterialsSetting::selected_filament_info() const
{
const int selection = m_comboBox_filament->GetSelection();
if (selection < 0 || selection >= int(m_filament_list_items.size()) || !m_filament_list_items[selection].is_filament)
return nullptr;
return &m_filament_list_items[selection].filament;
}
std::set<std::string> AMSMaterialsSetting::load_favorite_filament_ids() const
{
std::set<std::string> favorite_ids;
std::vector<std::string> values;
const std::string serialized = wxGetApp().app_config->get(AMS_FAVORITE_FILAMENT_IDS_KEY);
if (!serialized.empty() && Slic3r::unescape_strings_cstyle(serialized, values)) {
favorite_ids.insert(values.begin(), values.end());
}
return favorite_ids;
}
void AMSMaterialsSetting::save_favorite_filament_ids(const std::set<std::string>& favorite_ids)
{
std::vector<std::string> values(favorite_ids.begin(), favorite_ids.end());
wxGetApp().app_config->set(AMS_FAVORITE_FILAMENT_IDS_KEY, Slic3r::escape_strings_cstyle(values));
wxGetApp().app_config->save();
}
void AMSMaterialsSetting::rebuild_filament_dropdown_items(const std::string& selected_filament_id)
{
std::vector<DropDown::Item> items;
m_filament_list_items.clear();
auto append_header = [&items, this](const wxString& label) {
DropDown::Item item;
item.text = label;
item.style = DD_ITEM_STYLE_SPLIT_ITEM | DD_ITEM_STYLE_DISABLED;
items.push_back(item);
m_filament_list_items.push_back(FilamentListItem{});
};
auto append_filament = [&items, this](const wxString& name) {
auto info_it = map_filament_items.find(name);
if (info_it == map_filament_items.end())
return;
DropDown::Item item;
item.text = name;
items.push_back(item);
m_filament_list_items.push_back(FilamentListItem{info_it->second, true});
};
// Favorites are listed again at the top under their own header. Without any, the list is the
// plain filament list.
const std::set<std::string> favorite_ids = load_favorite_filament_ids();
std::vector<wxString> favorites;
for (const wxString& name : m_sorted_filament_items) {
auto info_it = map_filament_items.find(name);
if (info_it != map_filament_items.end() && favorite_ids.count(info_it->second.filament_id) > 0)
favorites.push_back(name);
}
if (!favorites.empty()) {
append_header(_L("Favorite filaments"));
for (const wxString& name : favorites)
append_filament(name);
append_header(_L("All filaments"));
}
for (const wxString& name : m_sorted_filament_items)
append_filament(name);
int selection_idx = -1;
if (!selected_filament_id.empty()) {
for (int i = 0; i < int(m_filament_list_items.size()); ++i) {
if (m_filament_list_items[i].is_filament && m_filament_list_items[i].filament.filament_id == selected_filament_id) {
selection_idx = i;
break;
}
}
}
// Clear() first: SetItems() keeps the old selection, and SetSelection() ignores an index equal to it,
// which would leave the previous filament's name on the combo.
m_comboBox_filament->Clear();
m_comboBox_filament->SetItems(items);
update_favorite_stars();
m_comboBox_filament->SetSelection(selection_idx);
m_favorites_regroup = false;
}
void AMSMaterialsSetting::update_favorite_stars()
{
const std::set<std::string> favorite_ids = load_favorite_filament_ids();
for (size_t i = 0; i < m_filament_list_items.size(); ++i) {
const FilamentListItem& row = m_filament_list_items[i];
if (row.is_filament) {
const bool favorite = favorite_ids.count(row.filament.filament_id) > 0;
m_comboBox_filament->SetItemActionBitmap(i, favorite ? m_star_filled.bmp() : m_star_empty.bmp());
}
}
}
void AMSMaterialsSetting::toggle_favorite_filament(const std::string& filament_id)
{
if (filament_id.empty())
return;
std::set<std::string> favorite_ids = load_favorite_filament_ids();
auto iter = favorite_ids.find(filament_id);
if (iter == favorite_ids.end())
favorite_ids.insert(filament_id);
else
favorite_ids.erase(iter);
save_favorite_filament_ids(favorite_ids);
}
void AMSMaterialsSetting::on_toggle_favorite_filament(wxCommandEvent& evt)
{
const int index = evt.GetInt();
if (index < 0 || index >= int(m_filament_list_items.size()) || !m_filament_list_items[index].is_filament)
return;
toggle_favorite_filament(m_filament_list_items[index].filament.filament_id);
update_favorite_stars();
m_favorites_regroup = true;
}
void AMSMaterialsSetting::Popup(wxString filament, wxString sn, wxString temp_min, wxString temp_max, wxString k, wxString n)
{
if (!obj) return;
@@ -998,14 +1175,16 @@ void AMSMaterialsSetting::Popup(wxString filament, wxString sn, wxString temp_mi
m_input_k_val->GetTextCtrl()->SetValue(k);
m_input_n_val->GetTextCtrl()->SetValue(n);
// Tray values are re-supplied by every caller; clear so a previous popup cannot leak in.
m_input_nozzle_min->GetTextCtrl()->SetValue(wxEmptyString);
m_input_nozzle_max->GetTextCtrl()->SetValue(wxEmptyString);
wxArrayString filament_items;
wxString bambu_filament_name;
wxString hint_filament_name; // the hint type to be selected
std::unordered_map<wxString, wxString> query_filament_vendors;// some information for sort
std::unordered_map<wxString, wxString> query_filament_types; //
std::set<std::string> filament_id_set;
m_sorted_filament_items.clear();
// The alias keyed map has to start empty: it is a member, so a stale alias left by an earlier
// popup (a different printer, a different nozzle) would resolve to that printer's filament_id.
map_filament_items.clear();
@@ -1024,18 +1203,21 @@ void AMSMaterialsSetting::Popup(wxString filament, wxString sn, wxString temp_mi
}
stream << std::fixed << std::setprecision(1) << machine_diameter;
std::string nozzle_diameter_str = stream.str();
// OrcaSonar's model id is optional, so the device model alone can resolve to nothing;
// the helper falls back to the selected profile so the dropdown is never empty.
const std::string filament_printer_model = resolve_filament_printer_model(obj->printer_type, preset_bundle);
if (preset_bundle) {
if (preset_bundle && !filament_printer_model.empty()) {
BOOST_LOG_TRIVIAL(trace) << "system_preset_bundle filament number=" << preset_bundle->filaments.size();
for (Preset *filament_it : preset_bundle->get_filament_presets_for_machine(
DevPrinterConfigUtil::get_printer_display_name(obj->printer_type), nozzle_diameter_str, obj->is_support_user_preset)) {
filament_printer_model, nozzle_diameter_str, obj->is_support_user_preset)) {
if (!filament_id_set.insert(filament_it->filament_id).second)
continue;
const std::string alias = preset_bundle->filaments.get_preset_alias(*filament_it, true);
if (alias.empty())
continue;
filament_items.push_back(alias);
m_sorted_filament_items.push_back(alias);
_collect_filament_info(alias, *filament_it, query_filament_vendors, query_filament_types);
FilamentInfos filament_infos;
@@ -1044,7 +1226,6 @@ void AMSMaterialsSetting::Popup(wxString filament, wxString sn, wxString temp_mi
map_filament_items[alias] = filament_infos;
if (filament_it->filament_id == ams_filament_id) {
hint_filament_name = from_u8(alias);
bambu_filament_name = from_u8(alias);
@@ -1095,6 +1276,12 @@ void AMSMaterialsSetting::Popup(wxString filament, wxString sn, wxString temp_mi
else {
m_comboBox_filament->Show();
m_readonly_filament->Hide();
// A printer-set tray carries its own temps; the preset selection only fills
// them when the tray has none.
if (!temp_min.IsEmpty() && temp_min != "0")
m_input_nozzle_min->GetTextCtrl()->SetValue(temp_min);
if (!temp_max.IsEmpty() && temp_max != "0")
m_input_nozzle_max->GetTextCtrl()->SetValue(temp_max);
}
if (obj->cali_version >= 0) {
@@ -1215,33 +1402,22 @@ void AMSMaterialsSetting::Popup(wxString filament, wxString sn, wxString temp_mi
return name_compare != 0 ? name_compare < 0 : left < right;
};
std::sort(filament_items.begin(), filament_items.end(), _filament_sorter);
std::sort(m_sorted_filament_items.begin(), m_sorted_filament_items.end(), _filament_sorter);
}
// traverse the hint selection idx
int selection_idx = -1;
{
for(int i = 0; i < filament_items.size(); i++)
{
if (hint_filament_name == filament_items[i])
{
selection_idx = i;
break;
}
}
}
if (m_sorted_filament_items.empty())
BOOST_LOG_TRIVIAL(warning) << "ams_materials_setting: no filament presets for printer model \""
<< filament_printer_model << "\" at nozzle " << nozzle_diameter_str;
m_comboBox_filament->Set(filament_items);
m_comboBox_filament->SetSelection(selection_idx);
rebuild_filament_dropdown_items(ams_filament_id);
int selection_idx = m_comboBox_filament->GetSelection();
m_comboBox_from_printer = true;
post_select_event(selection_idx);
if (selection_idx < 0) {
m_comboBox_filament->SetValue(wxEmptyString);
}
// Set the flag whether to open the filament setting dialog from the device page
m_comboBox_filament->SetClientData(new int(1));
update();
Layout();
Fit();
@@ -1285,17 +1461,18 @@ int AMSMaterialsSetting::get_filament_variant_index(const Preset &filament, cons
void AMSMaterialsSetting::on_select_filament(wxCommandEvent &evt)
{
// Get the flag whether to open the filament setting dialog from the device page
int* from_printer = static_cast<int*>(m_comboBox_filament->GetClientData());
// True for the popup's own initial selection: the dialog pre-filled the tray's
// temps, so the matching preset must not overwrite them.
const bool initial_printer_selection = m_comboBox_from_printer;
m_filament_type = "";
const FilamentInfos* selected_info = selected_filament_info();
PresetBundle* preset_bundle = wxGetApp().preset_bundle;
std::string filament_printer_model;
if (preset_bundle) {
std::ostringstream stream;
if (obj) {
// Defensive: this dialog is opened only from StatusPanel (BBL-only) today, so the fallback fires
// only during the brief BBL startup window before firmware reports nozzle info. Without this,
// the "0.0" lookup string returns an empty set and filament lookup yields no results.
// Use the selected profile's nozzle diameter until the connected device reports one.
float machine_diameter = obj->GetExtderSystem()->GetNozzleDiameter(0);
if (machine_diameter == 0.0f) {
const ConfigOption *opt = preset_bundle->printers.get_selected_preset().config.option("nozzle_diameter");
@@ -1305,33 +1482,35 @@ void AMSMaterialsSetting::on_select_filament(wxCommandEvent &evt)
}
}
stream << std::fixed << std::setprecision(1) << machine_diameter;
filament_printer_model = resolve_filament_printer_model(obj->printer_type, preset_bundle);
}
std::string nozzle_diameter_str = stream.str();
// Resolve the selection against the same list Popup() built the dropdown from, so the two
// halves of the dialog cannot disagree about which filaments this machine can use.
const std::string selected = m_comboBox_filament->GetValue().ToStdString();
if (!selected.empty()) {
const std::string filament_id = map_filament_items[selected].filament_id;
if (selected_info && !filament_printer_model.empty()) {
const std::string& filament_id = selected_info->filament_id;
for (Preset *it : preset_bundle->get_filament_presets_for_machine(
DevPrinterConfigUtil::get_printer_display_name(obj->printer_type), nozzle_diameter_str, obj->is_support_user_preset)) {
filament_printer_model, nozzle_diameter_str, obj->is_support_user_preset)) {
if (it->filament_id != filament_id)
continue;
// ) if nozzle_temperature_range is found
const int variant_index = get_filament_variant_index(*it, nozzle_diameter_str);
ConfigOption* opt_min = it->config.option("nozzle_temperature_range_low");
if (opt_min) {
ConfigOptionInts* opt_min_ints = dynamic_cast<ConfigOptionInts*>(opt_min);
if (opt_min_ints) {
wxString text_nozzle_temp_min = wxString::Format("%d", opt_min_ints->get_at(variant_index));
m_input_nozzle_min->GetTextCtrl()->SetValue(text_nozzle_temp_min);
if (!initial_printer_selection) {
// ) if nozzle_temperature_range is found
const int variant_index = get_filament_variant_index(*it, nozzle_diameter_str);
ConfigOption* opt_min = it->config.option("nozzle_temperature_range_low");
if (opt_min) {
ConfigOptionInts* opt_min_ints = dynamic_cast<ConfigOptionInts*>(opt_min);
if (opt_min_ints) {
wxString text_nozzle_temp_min = wxString::Format("%d", opt_min_ints->get_at(variant_index));
m_input_nozzle_min->GetTextCtrl()->SetValue(text_nozzle_temp_min);
}
}
}
ConfigOption* opt_max = it->config.option("nozzle_temperature_range_high");
if (opt_max) {
ConfigOptionInts* opt_max_ints = dynamic_cast<ConfigOptionInts*>(opt_max);
if (opt_max_ints) {
wxString text_nozzle_temp_max = wxString::Format("%d", opt_max_ints->get_at(variant_index));
m_input_nozzle_max->GetTextCtrl()->SetValue(text_nozzle_temp_max);
ConfigOption* opt_max = it->config.option("nozzle_temperature_range_high");
if (opt_max) {
ConfigOptionInts* opt_max_ints = dynamic_cast<ConfigOptionInts*>(opt_max);
if (opt_max_ints) {
wxString text_nozzle_temp_max = wxString::Format("%d", opt_max_ints->get_at(variant_index));
m_input_nozzle_max->GetTextCtrl()->SetValue(text_nozzle_temp_max);
}
}
}
ConfigOption* opt_type = it->config.option("filament_type");
@@ -1364,42 +1543,36 @@ void AMSMaterialsSetting::on_select_filament(wxCommandEvent &evt)
//reset cali
int cali_select_idx = -1;
if ( !this->obj || m_filament_selection < 0) {
if ( !this->obj || m_filament_selection < 0 || !selected_info) {
m_input_k_val->Enable(false);
m_input_n_val->Enable(false);
m_button_confirm->Disable(); // ORCA No need to change style
m_comboBox_cali_result->Clear();
m_comboBox_cali_result->SetValue(wxEmptyString);
m_input_k_val->GetTextCtrl()->SetValue(wxEmptyString);
m_input_n_val->GetTextCtrl()->SetValue(wxEmptyString);
m_comboBox_filament->SetClientData(new int(0));
if (!initial_printer_selection) {
m_input_k_val->GetTextCtrl()->SetValue(wxEmptyString);
m_input_n_val->GetTextCtrl()->SetValue(wxEmptyString);
}
m_comboBox_from_printer = false;
return;
}
else {
m_button_confirm->Enable(true); // ORCA No need to change style
if (!m_view_only)
m_button_confirm->Enable(true); // ORCA No need to change style
}
//filament id
ams_filament_id = "";
ams_setting_id = "";
if (preset_bundle) {
for (auto it = preset_bundle->filaments.begin(); it != preset_bundle->filaments.end(); it++) {
auto itor = map_filament_items.find(m_comboBox_filament->GetValue().ToStdString());
if ( itor != map_filament_items.end()) {
ams_filament_id = itor->second.filament_id;
ams_setting_id = itor->second.setting_id;
break;
}
if (it->alias.compare(m_comboBox_filament->GetValue().ToStdString()) == 0) {
ams_filament_id = it->filament_id;
ams_setting_id = it->setting_id;
break;
}
}
if (selected_info) {
ams_filament_id = selected_info->filament_id;
ams_setting_id = selected_info->setting_id;
}
if (!ams_filament_id.empty())
m_clr_picker->is_empty(false);
wxArrayString items;
m_pa_profile_items.clear();
m_comboBox_cali_result->SetValue(wxEmptyString);
@@ -1455,7 +1628,7 @@ void AMSMaterialsSetting::on_select_filament(wxCommandEvent &evt)
m_comboBox_cali_result->Set(items);
if (ams_id == VIRTUAL_TRAY_MAIN_ID || ams_id == VIRTUAL_TRAY_DEPUTY_ID) {
if (from_printer && (*from_printer == 1)) {
if (initial_printer_selection) {
for (auto slot : obj->vt_slot) {
if (slot.id == std::to_string(ams_id))
cali_select_idx = CalibUtils::get_selected_calib_idx(m_pa_profile_items, slot.cali_idx);
@@ -1472,10 +1645,11 @@ void AMSMaterialsSetting::on_select_filament(wxCommandEvent &evt)
}
}
else {
if (from_printer && (*from_printer == 1)) {
if (initial_printer_selection) {
DevAmsTray* selected_tray = this->obj->GetFilaSystem()->GetAmsTray(std::to_string(ams_id), std::to_string(slot_id));
if (!selected_tray)
{
m_comboBox_from_printer = false;
return;
}
@@ -1506,7 +1680,7 @@ void AMSMaterialsSetting::on_select_filament(wxCommandEvent &evt)
else {
if (!ams_filament_id.empty()) {
//m_input_k_val->GetTextCtrl()->SetValue("0.00");
m_input_k_val->Enable(true);
m_input_k_val->Enable(!m_view_only);
}
else {
//m_input_k_val->GetTextCtrl()->SetValue("0.00");
@@ -1514,7 +1688,7 @@ void AMSMaterialsSetting::on_select_filament(wxCommandEvent &evt)
}
}
m_comboBox_filament->SetClientData(new int(0));
m_comboBox_from_printer = false;
}
void AMSMaterialsSetting::on_dpi_changed(const wxRect &suggested_rect)
@@ -1529,6 +1703,11 @@ void AMSMaterialsSetting::on_dpi_changed(const wxRect &suggested_rect)
m_button_reset->Rescale(); // ORCA
m_button_confirm->Rescale(); // ORCA
m_button_close->Rescale(); // ORCA
// An open list would keep the measurements of the old stars, so close it before replacing them.
m_comboBox_filament->CloseDropdown();
m_star_filled.msw_rescale();
m_star_empty.msw_rescale();
update_favorite_stars();
this->Refresh();
}
+25 -7
View File
@@ -31,6 +31,7 @@
#include "slic3r/GUI/Widgets/StateColor.hpp"
#include <wx/panel.h>
#include <map>
#include <set>
#define AMS_MATERIALS_SETTING_DEF_COLOUR wxColour(255, 255, 255)
#define AMS_MATERIALS_SETTING_GREY900 wxColour(38, 46, 48)
@@ -137,7 +138,7 @@ public:
std::string ams_filament_id;
std::string ams_setting_id;
bool m_is_third;
bool m_is_third = false;
// Orca: view-only mode (laser/cut). When set, the dialog is inspectable but every
// editing control is disabled and no command is sent.
bool m_view_only = false;
@@ -156,6 +157,7 @@ protected:
void create_panel_kn(wxWindow* parent);
void on_dpi_changed(const wxRect &suggested_rect) override;
void on_select_filament(wxCommandEvent& evt);
void on_toggle_favorite_filament(wxCommandEvent& evt);
void on_select_cali_result(wxCommandEvent &evt);
void on_select_ok(wxCommandEvent &event);
void on_select_reset(wxCommandEvent &event);
@@ -163,6 +165,11 @@ protected:
void on_clr_picker(wxMouseEvent &event);
bool is_virtual_tray();
void update_widgets();
void rebuild_filament_dropdown_items(const std::string& selected_filament_id);
void update_favorite_stars();
std::set<std::string> load_favorite_filament_ids() const;
void save_favorite_filament_ids(const std::set<std::string>& favorite_ids);
void toggle_favorite_filament(const std::string& filament_id);
void update_filament_editing(bool is_printing);
// Orca: the variant index of the filament's per-variant options on the nozzle this tray feeds
@@ -199,15 +206,13 @@ protected:
wxStaticText* m_n_param;
TextInput* m_input_n_val;
int m_filament_selection;
// True while the popup's initial, printer-driven selection is being handled; it
// keeps that selection from overwriting the tray's own temps with preset temps.
bool m_comboBox_from_printer = false;
int m_pa_cali_select_id = 0;
// Orca: native wxComboBox on macOS (the custom ComboBox mis-renders the filament dropdown there)
#ifdef __APPLE__
wxComboBox *m_comboBox_filament;
#else
ComboBox *m_comboBox_filament;
#endif
ComboBox * m_comboBox_cali_result;
TextInput* m_readonly_filament;
@@ -215,7 +220,20 @@ protected:
std::string filament_id;
std::string setting_id;
};
std::map<std::string, FilamentInfos> map_filament_items;
struct FilamentListItem {
FilamentInfos filament;
bool is_filament{false};
};
const FilamentInfos* selected_filament_info() const;
// Keyed by the same wxString the dropdown shows, so a row's name never has to be converted back.
std::map<wxString, FilamentInfos> map_filament_items;
std::vector<wxString> m_sorted_filament_items;
std::vector<FilamentListItem> m_filament_list_items;
ScalableBitmap m_star_filled;
ScalableBitmap m_star_empty;
// Favorites changed while the list was open; their rows are regrouped before it opens again.
bool m_favorites_regroup{false};
};
wxDECLARE_EVENT(EVT_SELECTED_COLOR, wxCommandEvent);
+7 -4
View File
@@ -530,7 +530,8 @@ void AMSSetting::on_insert_material_read(wxCommandEvent &event)
bool tray_read_opt = m_checkbox_Insert_material_auto_read->GetValue();
bool remain_opt = m_checkbox_remain->GetValue();
m_obj->command_ams_user_settings(start_read_opt, tray_read_opt, remain_opt);
if (m_obj->command_ams_user_settings(start_read_opt, tray_read_opt, remain_opt) != 0)
UpdateByObj(m_obj);
m_sizer_Insert_material_tip_inline->Layout();
Layout();
@@ -556,7 +557,8 @@ void AMSSetting::on_starting_read(wxCommandEvent &event)
bool tray_read_opt = m_checkbox_Insert_material_auto_read->GetValue();
bool remain_opt = m_checkbox_remain->GetValue();
m_obj->command_ams_user_settings(start_read_opt, tray_read_opt, remain_opt);
if (m_obj->command_ams_user_settings(start_read_opt, tray_read_opt, remain_opt) != 0)
UpdateByObj(m_obj);
m_sizer_starting_tip_inline->Layout();
Layout();
@@ -570,7 +572,8 @@ void AMSSetting::on_remain(wxCommandEvent& event)
bool start_read_opt = m_checkbox_starting_auto_read->GetValue();
bool tray_read_opt = m_checkbox_Insert_material_auto_read->GetValue();
bool remain_opt = m_checkbox_remain->GetValue();
m_obj->command_ams_user_settings(start_read_opt, tray_read_opt, remain_opt);
if (m_obj->command_ams_user_settings(start_read_opt, tray_read_opt, remain_opt) != 0)
UpdateByObj(m_obj);
event.Skip();
}
@@ -806,4 +809,4 @@ void AMSSettingArrangeAMSOrder::OnBtnRearrangeClicked(wxCommandEvent& event)
}
#endif
}} // namespace Slic3r::GUI
}} // namespace Slic3r::GUI
+8 -1
View File
@@ -10,6 +10,7 @@
#include "Widgets/DialogButtons.hpp"
#include "libslic3r/Config.hpp"
#include <boost/log/trivial.hpp>
#include <slic3r/GUI/DeviceManager.hpp>
#include <string>
#include <vector>
#include "libslic3r/calib.hpp"
@@ -745,9 +746,15 @@ wxArrayString NewCalibrationHistoryDialog::get_all_filaments(const MachineObject
std::string nozzle_diameter_str = stream.str();
if (preset_bundle) {
// OrcaSonar's model id is optional; the helper falls back to the selected profile
// so the list is never empty.
const std::string filament_printer_model = resolve_filament_printer_model(obj->printer_type, preset_bundle);
if (filament_printer_model.empty())
return filament_items;
BOOST_LOG_TRIVIAL(trace) << "system_preset_bundle filament number=" << preset_bundle->filaments.size();
for (Preset *filament_it : preset_bundle->get_filament_presets_for_machine(
DevPrinterConfigUtil::get_printer_display_name(obj->printer_type), nozzle_diameter_str, obj->is_support_user_preset)) {
filament_printer_model, nozzle_diameter_str, obj->is_support_user_preset)) {
if (!filament_id_set.insert(filament_it->filament_id).second)
continue;
const std::string alias = preset_bundle->filaments.get_preset_alias(*filament_it, true);
+16 -1
View File
@@ -1,3 +1,4 @@
#include <algorithm>
#include <array>
#include <cstddef>
#include <cassert>
@@ -8,6 +9,7 @@
#include <cstdlib>
#include <chrono>
#include <nlohmann/json.hpp>
#include <wx/app.h>
#include <wx/colour.h>
#include <string>
#include <wx/string.h>
@@ -19,6 +21,7 @@
#include <utility>
#include "DevFilaSystem.h"
#include "slic3r/Utils/NetworkAgent.hpp"
#include "slic3r/Utils/NetworkAgentFactory.hpp" // ORCA_PRINTER_AGENT_ID
#include "DevNozzleSystem.h" // DevNozzle / DevNozzleSystem for GetNozzleFlowStringByAmsId
// TODO: remove this include
@@ -61,6 +64,15 @@ void DevAmsTray::UpdateColorFromStr(const std::string& color)
}
}
void DevAmsTray::UpdateEmptyState(bool material_fields_present, bool classify_empty)
{
const auto is_zero_or_empty = [](const std::string& value) {
return value.empty() || std::all_of(value.begin(), value.end(), [](char c) { return c == '0'; });
};
is_empty = material_fields_present && classify_empty && setting_id.empty() && m_fila_type.empty() &&
is_zero_or_empty(color) && is_zero_or_empty(tag_uid);
}
void DevAmsTray::reset()
{
tag_uid = "";
@@ -82,6 +94,7 @@ void DevAmsTray::reset()
k = 0.0f;
n = 0.0f;
is_bbl = false;
is_empty = false;
hold_count = 0;
remain = 0;
}
@@ -681,7 +694,7 @@ void DevFilaSystemParser::ParseV1_0(const json& jj, MachineObject* obj, DevFilaS
//std::string type = (*tray_it)["tray_type"].get<std::string>();
std::string type = MachineObject::setting_id_to_type(curr_tray->setting_id, (*tray_it)["tray_type"].get<std::string>());
// curr_tray->setting_id is our OF id; GFS00/GFS01 are the printer's own support-filament ids.
auto* agent = GUI::wxGetApp().getAgent();
auto* agent = wxTheApp != nullptr ? GUI::wxGetApp().getAgent() : nullptr;
const std::string printer_filament_id = agent ? agent->from_orca_filament_id(curr_tray->setting_id) : curr_tray->setting_id;
if (printer_filament_id == "GFS00")
{
@@ -781,6 +794,8 @@ void DevFilaSystemParser::ParseV1_0(const json& jj, MachineObject* obj, DevFilaS
{
curr_tray->remain = -1;
}
curr_tray->UpdateEmptyState(tray_it->contains("tray_info_idx") && tray_it->contains("tray_type"),
obj->printer_agent_id == ORCA_PRINTER_AGENT_ID);
// The tray objects are reused across status updates. Reset this
// state when a previously empty slot receives a filament again.
curr_tray->is_slot_placeholder = tray_it->contains("tray_slot_placeholder");
+4 -1
View File
@@ -82,6 +82,7 @@ public:
wxColour wx_color;
bool is_bbl;
bool is_exists = false;
bool is_empty = false; // Explicitly reported as having no filament.
bool is_slot_placeholder = false; // Orca: True for empty tray slots from pull-mode agents
int hold_count = 0;
int remain = 0; // filament remain: 0 ~ 100
@@ -90,13 +91,15 @@ public:
// operators
bool operator==(DevAmsTray const& o) const
{
return id == o.id && m_fila_type == o.m_fila_type && filament_setting_id == o.filament_setting_id && color == o.color;
return id == o.id && m_fila_type == o.m_fila_type && filament_setting_id == o.filament_setting_id &&
color == o.color && is_empty == o.is_empty;
}
bool operator!=(DevAmsTray const& o) const { return !operator==(o); }
// setters
void reset();
void UpdateColorFromStr(const std::string& color);
void UpdateEmptyState(bool material_fields_present, bool classify_empty);
void set_hold_count() { hold_count = HOLD_COUNT_MAX; }
// getter
+110 -11
View File
@@ -64,6 +64,7 @@
#include <unordered_set>
#include <utility>
#include <vector>
#include <wx/app.h>
#include <wx/colour.h>
#include <tuple>
#include <wx/dir.h>
@@ -431,6 +432,32 @@ NozzleVolumeType convert_to_nozzle_type(const std::string &str)
return res;
}
std::string resolve_filament_printer_model(const std::string& printer_type, PresetBundle* preset_bundle)
{
if (!preset_bundle)
return {};
// Devices with a shipped printer config (Bambu) resolve directly.
if (const std::string display_name = DevPrinterConfigUtil::get_printer_display_name(printer_type); !display_name.empty())
return display_name;
// A vendor model id the device reported (SSDP modelNumber, manual binding).
if (!printer_type.empty()) {
if (const std::string model_name = preset_bundle->get_printer_model_display_name(printer_type); !model_name.empty())
return model_name;
}
// OrcaSonar's model id is optional; a generic device resolves against the selected profile.
if (const ConfigOptionString* model = preset_bundle->printers.get_selected_preset().config.opt<ConfigOptionString>("printer_model");
model && !model->value.empty()) {
BOOST_LOG_TRIVIAL(info) << "resolve_filament_printer_model: device type \"" << printer_type
<< "\" has no installed model; using the selected profile \"" << model->value << "\"";
return model->value;
}
return {};
}
wxString MachineObject::get_printer_type_display_str() const
{
std::string display_name = DevPrinterConfigUtil::get_printer_display_name(printer_type);
@@ -1733,6 +1760,10 @@ int MachineObject::check_resume_condition()
}
int MachineObject::command_ams_change_filament(bool load, std::string ams_id, std::string slot_id, int old_temp, int new_temp, std::optional<int> extruder_id)
{
if (!printer_supports_command("print.ams_change_filament")) {
BOOST_LOG_TRIVIAL(warning) << "command_ams_change_filament: printer agent does not support the command";
return command_with_dialog(ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED);
}
json j;
try {
auto tray_id = 0;
@@ -1776,6 +1807,10 @@ int MachineObject::command_ams_change_filament(bool load, std::string ams_id, st
int MachineObject::command_ams_user_settings(bool start_read_opt, bool tray_read_opt, bool remain_flag)
{
if (!printer_supports_command("print.ams_user_setting")) {
BOOST_LOG_TRIVIAL(warning) << "command_ams_user_settings: printer agent does not support the command";
return command_with_dialog(ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED);
}
json j;
j["print"]["command"] = "ams_user_setting";
j["print"]["sequence_id"] = std::to_string(MachineObject::m_sequence_id++);
@@ -1784,22 +1819,59 @@ int MachineObject::command_ams_user_settings(bool start_read_opt, bool tray_read
j["print"]["tray_read_option"] = tray_read_opt;
j["print"]["calibrate_remain_flag"] = remain_flag;
m_fila_system->GetAmsSystemSetting().SetDetectOnInsertEnabled(tray_read_opt);
m_fila_system->GetAmsSystemSetting().SetDetectOnPowerupEnabled(start_read_opt);
m_fila_system->GetAmsSystemSetting().SetDetectRemainEnabled(remain_flag);
ams_user_setting_start = time(nullptr);
return this->publish_json(j);
const int rc = this->publish_json(j);
if (rc == 0) {
m_fila_system->GetAmsSystemSetting().SetDetectOnInsertEnabled(tray_read_opt);
m_fila_system->GetAmsSystemSetting().SetDetectOnPowerupEnabled(start_read_opt);
m_fila_system->GetAmsSystemSetting().SetDetectRemainEnabled(remain_flag);
ams_user_setting_start = time(nullptr);
}
return rc;
}
int MachineObject::command_ams_calibrate(int ams_id)
{
if (!m_agent) return -1;
if (!m_agent->owns_agent(printer_agent_id))
return command_with_dialog(ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED);
return command_with_dialog(m_agent->command_ams_calibrate(get_dev_id(), ams_id, MachineObject::m_sequence_id++, is_lan_mode_printer()));
}
bool MachineObject::printer_supports_command(const char* command) const
{
if (!command || !m_agent)
return false;
return m_agent->supports_command(printer_agent_id, get_dev_id(), command);
}
bool MachineObject::printer_uses_filament_mapping() const
{
if (!m_agent)
return false;
// An unrecorded owner goes through owns_agent() like any other, because
// whichever agent is active is also the one formatting the payload. Only a
// recorded owner that differs from the active agent is a mismatch.
return m_agent->uses_filament_mapping(printer_agent_id);
}
bool MachineObject::printer_supports_feature(const char* feature) const
{
return feature && m_agent && m_agent->supports_feature(printer_agent_id, get_dev_id(), feature);
}
bool MachineObject::supports_extrusion_cali() const
{
// Devices with no agent id predate the agent split and keep the Bambu path.
return printer_agent_id.empty() || printer_agent_id == BBL_PRINTER_AGENT_ID;
}
int MachineObject::command_ams_filament_settings(int ams_id, int slot_id, std::string filament_id, std::string setting_id, std::string tray_color, std::string tray_type, int nozzle_temp_min, int nozzle_temp_max)
{
if (!printer_supports_command("print.ams_filament_setting")) {
BOOST_LOG_TRIVIAL(warning) << "command_ams_filament_settings: printer agent does not support slot metadata updates";
return command_with_dialog(ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED);
}
int tag_tray_id = 0;
int tag_ams_id = ams_id;
int tag_slot_id = slot_id;
@@ -1834,6 +1906,10 @@ int MachineObject::command_ams_filament_settings(int ams_id, int slot_id, std::s
int MachineObject::command_ams_refresh_rfid(int ams_id, int slot_id)
{
if (!m_agent) return -1;
if (!printer_supports_command("print.ams_get_rfid")) {
BOOST_LOG_TRIVIAL(warning) << "command_ams_refresh_rfid: printer agent does not support the command";
return command_with_dialog(ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED);
}
return command_with_dialog(m_agent->command_ams_refresh_rfid(get_dev_id(), ams_id, slot_id, MachineObject::m_sequence_id++, is_lan_mode_printer()));
}
@@ -1847,11 +1923,19 @@ int MachineObject::command_start_camera()
int MachineObject::command_ams_select_tray(std::string tray_id)
{
if (!m_agent) return -1;
if (!printer_supports_command("print.ams_change_filament")) {
BOOST_LOG_TRIVIAL(warning) << "command_ams_select_tray: printer agent does not support the command";
return command_with_dialog(ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED);
}
return command_with_dialog(m_agent->command_ams_select_tray(get_dev_id(), tray_id, MachineObject::m_sequence_id++, is_lan_mode_printer()));
}
int MachineObject::command_ams_control(std::string action)
{
if (!printer_supports_command("print.ams_control")) {
BOOST_LOG_TRIVIAL(warning) << "command_ams_control: printer agent does not support the command";
return command_with_dialog(ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED);
}
if (action == "resume" && check_resume_condition()) return 0;
//valid actions
@@ -1867,6 +1951,10 @@ int MachineObject::command_ams_control(std::string action)
int MachineObject::command_ams_drying_stop()
{
if (!printer_supports_command("print.auto_stop_ams_dry")) {
BOOST_LOG_TRIVIAL(warning) << "command_ams_drying_stop: printer agent does not support the command";
return command_with_dialog(ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED);
}
json j;
j["print"]["command"] = "auto_stop_ams_dry";
j["print"]["sequence_id"] = std::to_string(MachineObject::m_sequence_id++);
@@ -2817,7 +2905,9 @@ bool MachineObject::is_camera_busy_off()
int MachineObject::publish_json(const json& json_item, int qos, int flag)
{
int rtn = 0;
if (is_lan_mode_printer()) {
if (m_agent && !m_agent->owns_agent(printer_agent_id)) {
rtn = ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED;
} else if (is_lan_mode_printer()) {
rtn = local_publish_json(json_item.dump(), qos, flag);
} else {
rtn = cloud_publish_json(json_item.dump(), qos, flag);
@@ -2861,6 +2951,7 @@ int MachineObject::local_publish_json(std::string json_str, int qos, int flag)
std::string MachineObject::setting_id_to_type(std::string setting_id, std::string tray_type)
{
std::string type;
if (wxTheApp == nullptr) return tray_type;
PresetBundle* preset_bundle = GUI::wxGetApp().preset_bundle;
if (preset_bundle) {
for (auto it = preset_bundle->filaments.begin(); it != preset_bundle->filaments.end(); it++) {
@@ -4197,6 +4288,9 @@ int MachineObject::parse_json(std::string tunnel, std::string payload, bool key_
vt_slot[0].setting_id = jj["tray_info_idx"].get<std::string>();
//vt_tray.type = jj["tray_type"].get<std::string>();
vt_slot[0].m_fila_type = setting_id_to_type(vt_slot[0].setting_id, jj["tray_type"].get<std::string>());
// The ack carries the whole slot; re-derive empty so the panel flips off
// "Empty" without waiting out the hold.
vt_slot[0].UpdateEmptyState(true, printer_agent_id == ORCA_PRINTER_AGENT_ID);
// delay update
vt_slot[0].set_hold_count();
} else {
@@ -4222,6 +4316,9 @@ int MachineObject::parse_json(std::string tunnel, std::string payload, bool key_
tray_it->second->setting_id = jj["tray_info_idx"].get<std::string>();
tray_it->second->m_fila_type = setting_id_to_type(tray_it->second->setting_id, jj["tray_type"].get<std::string>());
// The ack carries the whole slot; re-derive empty so the panel flips off
// "Empty" without waiting out the hold.
tray_it->second->UpdateEmptyState(true, printer_agent_id == ORCA_PRINTER_AGENT_ID);
// delay update
tray_it->second->set_hold_count();
} else {
@@ -5160,7 +5257,7 @@ DevAmsTray MachineObject::parse_vt_tray(json vtray)
//std::string type = vtray["tray_type"].get<std::string>();
std::string type = setting_id_to_type(vt_tray.setting_id, vtray["tray_type"].get<std::string>());
// vt_tray.setting_id is our OF id (translated on the way in); the two support ids below are the printer's own.
auto* agent = GUI::wxGetApp().getAgent();
auto* agent = wxTheApp != nullptr ? GUI::wxGetApp().getAgent() : nullptr;
const std::string printer_filament_id = agent ? agent->from_orca_filament_id(vt_tray.setting_id) : vt_tray.setting_id;
if (printer_filament_id == "GFS00") {
vt_tray.m_fila_type = "PLA-S";
@@ -5255,6 +5352,8 @@ DevAmsTray MachineObject::parse_vt_tray(json vtray)
else {
vt_tray.remain = -1;
}
vt_tray.UpdateEmptyState(vtray.contains("tray_info_idx") && vtray.contains("tray_type"),
printer_agent_id == ORCA_PRINTER_AGENT_ID);
}
return vt_tray;
@@ -5548,11 +5647,11 @@ void MachineObject::parse_new_info2(const json& info)
if (capabilities_it == info.end() || !capabilities_it->is_object())
return;
const auto flags_it = capabilities_it->find("flags");
if (flags_it == capabilities_it->end() || !flags_it->is_object())
const bool has_flags = flags_it != capabilities_it->end() && flags_it->is_object();
if (!has_flags)
return;
const json& flags = *flags_it;
BOOST_LOG_TRIVIAL(info) << "parse_new_info2: OrcaSonar capability flags=" << flags.dump();
BOOST_LOG_TRIVIAL(info) << "parse_new_info2: capability flags=" << flags.dump();
auto parse_bool = [&flags](const char* name, bool& target) {
const auto it = flags.find(name);
+14
View File
@@ -14,6 +14,7 @@
#include <string>
#include <memory>
#include <chrono>
#include <set>
#include <unordered_set>
#include <optional>
#include <boost/thread.hpp>
@@ -72,6 +73,7 @@ class DeviceErrorDialog; // Previous definitions
class NetworkAgent;
// Orca: ManualPaCaliMethod now provided by DeviceCore/DevCalib.h (enum class)
class PresetBundle;
#define UpgradeNoError 0
#define UpgradeDownloadFailed -1
@@ -107,6 +109,11 @@ struct DevPrintTaskRatingInfo;
// given nozzle diameter (mm), bucketed per nozzle size to mirror the printer firmware.
bool is_stringing_prone_filament(const std::string& filament_id, float nozzle_diameter);
// The printer model filament presets are matched against for a connected machine. OrcaSonar's
// model id is optional, so a device with no installed vendor model falls back to the selected
// printer profile rather than resolving to no filament list at all.
std::string resolve_filament_printer_model(const std::string& printer_type, PresetBundle* preset_bundle);
class MachineObject
{
private:
@@ -795,6 +802,13 @@ public:
int command_refresh_nozzle();
int command_set_chamber(int temp);
int check_resume_condition();
bool printer_supports_command(const char* command) const;
bool printer_supports_feature(const char* feature) const;
bool printer_uses_filament_mapping() const;
// The per-tray flow-dynamics K/N records are a Bambu firmware feature. Other
// agents have no printer-side calibration to read or write, so the AMS UI must
// not offer K/N for them.
bool supports_extrusion_cali() const;
// ams controls
//int command_ams_switch(int tray_index, int old_temp = 210, int new_temp = 210);
int command_ams_change_filament(bool load, std::string ams_id, std::string slot_id, int old_temp = 210, int new_temp = 210, std::optional<int> extruder_id = std::nullopt);
@@ -970,7 +970,11 @@ int ReselectMachineDialog::CaculateSwitcherDistribution(MachineObject* obj, cons
const auto& can = ams.cans[j];
auto id = getTrayID(obj, ams.ams_id, can.can_id);
auto material = can.material_name;
if (can.material_state == AMSCanType::AMS_CAN_TYPE_THIRDBRAND ||
if (can.is_empty || can.material_state == AMSCanType::AMS_CAN_TYPE_EMPTY)
{
material = L("Empty");
}
else if (can.material_state == AMSCanType::AMS_CAN_TYPE_THIRDBRAND ||
can.material_state == AMSCanType::AMS_CAN_TYPE_BRAND ||
can.material_state == AMSCanType::AMS_CAN_TYPE_VIRTUAL)
{
@@ -979,10 +983,6 @@ int ReselectMachineDialog::CaculateSwitcherDistribution(MachineObject* obj, cons
material = L("?");
}
}
if (can.material_state == AMSCanType::AMS_CAN_TYPE_EMPTY)
{
material = L("Empty");
}
auto itOK = std::find_if(posOK.begin(), posOK.end(), [&](const trayHelper& tray){
auto amsID = std::get<0>(tray);
+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
+23 -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) {
this->handle_http_error(status, body, event.provider);
});
m_agent->start_discovery(true, false);
if (should_start_ssdp_discovery())
m_agent->start_discovery(true, false);
}
//update the plugin tips
@@ -3232,9 +3233,16 @@ bool GUI_App::on_init_inner()
#ifdef _MSW_DARK_MODE
#ifndef __WINDOWS__
// The label colours and the dark mode flag were set above from the saved mode. When the system theme
// differs from it, refresh them, or dialogs that map their colours stay in the old mode for this session.
const bool saved_dark_mode = dark_mode();
wxSystemAppearance app = wxSystemSettings::GetAppearance();
GUI::wxGetApp().app_config->set("dark_color_mode", app.IsDark() ? "1" : "0");
GUI::wxGetApp().app_config->save();
if (dark_mode() != saved_dark_mode) {
init_label_colours();
Update_dark_mode_flag();
}
#endif // __APPLE__
@@ -4165,6 +4173,18 @@ std::string GUI_App::resolve_printer_agent_id(const std::string& stored_id) cons
return (preset_bundle && preset_bundle->is_bbl_vendor()) ? BBL_PRINTER_AGENT_ID : ORCA_PRINTER_AGENT_ID;
}
bool GUI_App::should_start_ssdp_discovery()
{
// Non-Orca agents (BBL, plugins) keep their own discovery; OrcaSonar only scans in the
// background when one of its machines is already configured, otherwise the popup drives it.
const std::string agent_id = resolve_printer_agent_id(
preset_bundle ? preset_bundle->printers.get_edited_preset().config.opt_string("printer_agent")
: std::string());
if (agent_id != ORCA_PRINTER_AGENT_ID)
return true;
return m_device_manager && !m_device_manager->get_my_machine_list(ORCA_PRINTER_AGENT_ID).empty();
}
std::string GUI_App::canonical_printer_agent_id(const std::string& picked_id)
{
return picked_id == resolve_printer_agent_id("") ? std::string() : picked_id;
@@ -4236,7 +4256,8 @@ void GUI_App::switch_printer_agent()
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": printer agent switched to " << effective_agent_id;
// Start discovery so Python agents can populate the device list via SSDP callback
m_agent->start_discovery(true, false);
if (should_start_ssdp_discovery())
m_agent->start_discovery(true, false);
// Auto-switch MachineObject (new agent has empty device_info, so always re-select)
select_machine(effective_agent_id);
+3
View File
@@ -403,6 +403,9 @@ public:
// Reconcile the live printer agent with the stored preset selection.
void switch_printer_agent();
// Whether the background SSDP scan should run for the effective printer agent.
bool should_start_ssdp_discovery();
std::string resolve_printer_agent_id(const std::string& stored_id) const;
// ORCA TODO: in the future, bbl presets should specify "bbl" printer agent id
// then, all resolve and canonical would just be ORCA<->""
+11 -8
View File
@@ -1617,7 +1617,7 @@ void ObjectList::list_manipulation(const wxPoint& mouse_pos, bool evt_context_me
int obj_idx, vol_idx;
get_selected_item_indexes(obj_idx, vol_idx, item);
if (obj_idx != -1) {
Plater::TakeSnapshot(plater, _u8L("Shift objects to bed"));
Plater::TakeSnapshot snapshot(plater, _u8L("Shift objects to bed"));
(*m_objects)[obj_idx]->ensure_on_bed();
cnv->reload_scene(true, true);
update_info_items(obj_idx);
@@ -2779,12 +2779,13 @@ void ObjectList::del_info_item(const int obj_idx, InfoItemType type)
GLCanvas3D* cnv = plater->canvas3D();
switch (type) {
case InfoItemType::CustomSupports:
case InfoItemType::CustomSupports: {
cnv->get_gizmos_manager().reset_all_states();
Plater::TakeSnapshot(plater, "Remove support painting");
Plater::TakeSnapshot snapshot(plater, "Remove support painting");
for (ModelVolume* mv : (*m_objects)[obj_idx]->volumes)
mv->supported_facets.reset();
break;
}
// BBS: remove CustomSeam
@@ -2795,19 +2796,21 @@ void ObjectList::del_info_item(const int obj_idx, InfoItemType type)
}
break;
case InfoItemType::MmSegmentation:
case InfoItemType::MmSegmentation: {
cnv->get_gizmos_manager().reset_all_states();
Plater::TakeSnapshot(plater, "Remove color painting");
Plater::TakeSnapshot snapshot(plater, "Remove color painting");
for (ModelVolume* mv : (*m_objects)[obj_idx]->volumes)
mv->mmu_segmentation_facets.reset();
break;
}
case InfoItemType::FuzzySkin:
case InfoItemType::FuzzySkin: {
cnv->get_gizmos_manager().reset_all_states();
Plater::TakeSnapshot(plater, _u8L("Remove paint-on fuzzy skin"));
Plater::TakeSnapshot snapshot(plater, _u8L("Remove paint-on fuzzy skin"));
for (ModelVolume* mv : (*m_objects)[obj_idx]->volumes)
mv->fuzzy_skin_facets.reset();
break;
}
// BBS: remove Sinking
case InfoItemType::Undef : assert(false); break;
@@ -6718,7 +6721,7 @@ void ObjectList::OnEditingStarted(wxDataViewEvent &event)
else if (col == colSinking) {
Plater * plater = wxGetApp().plater();
GLCanvas3D *cnv = plater->canvas3D();
Plater::TakeSnapshot(plater, _u8L("Shift objects to bed"));
Plater::TakeSnapshot snapshot(plater, _u8L("Shift objects to bed"));
int obj_idx, vol_idx;
get_selected_item_indexes(obj_idx, vol_idx, item);
(*m_objects)[obj_idx]->ensure_on_bed();
+119 -90
View File
@@ -1,13 +1,16 @@
#include "ImageGrid.h"
#include "Printer/PrinterFileSystem.h"
#include "Printer/BambuFileGridModel.h"
#include "wxExtensions.hpp"
#include "Widgets/Label.hpp"
#include "I18N.hpp"
#include "GUI_App.hpp"
#include "GUI.hpp"
#include <slic3r/GUI/Printer/IFileGridModel.h>
#include <utility>
#include <boost/smart_ptr/shared_ptr.hpp>
#include <boost/smart_ptr/make_shared_object.hpp>
#include <wx/colour.h>
#include <cstddef>
#include <wx/dcclient.h>
@@ -76,18 +79,11 @@ ImageGrid::ImageGrid(wxWindow * parent)
void ImageGrid::SetFileSystem(boost::shared_ptr<PrinterFileSystem> file_sys)
{
if (m_file_sys) {
m_file_sys->Unbind(EVT_MODE_CHANGED, &ImageGrid::changedEvent, this);
m_file_sys->Unbind(EVT_FILE_CHANGED, &ImageGrid::changedEvent, this);
m_file_sys->Unbind(EVT_THUMBNAIL, &ImageGrid::changedEvent, this);
m_file_sys->Unbind(EVT_DOWNLOAD, &ImageGrid::changedEvent, this);
}
m_file_sys = file_sys;
if (m_file_sys) {
m_file_sys->Bind(EVT_MODE_CHANGED, &ImageGrid::changedEvent, this);
m_file_sys->Bind(EVT_FILE_CHANGED, &ImageGrid::changedEvent, this);
m_file_sys->Bind(EVT_THUMBNAIL, &ImageGrid::changedEvent, this);
m_file_sys->Bind(EVT_DOWNLOAD, &ImageGrid::changedEvent, this);
m_model.reset();
if (file_sys) {
m_model = boost::make_shared<BambuFileGridModel>(file_sys);
m_model->SetChangeHandler([this](FileGridChange c) { onModelChange(c); });
}
m_row_count = 0;
m_col_count = 1;
@@ -96,9 +92,22 @@ void ImageGrid::SetFileSystem(boost::shared_ptr<PrinterFileSystem> file_sys)
UpdateFileSystem();
}
void ImageGrid::SetModel(boost::shared_ptr<IFileGridModel> model)
{
m_file_sys.reset();
m_model = std::move(model);
if (m_model)
m_model->SetChangeHandler([this](FileGridChange c) { onModelChange(c); });
m_row_count = 0;
m_col_count = 1;
m_row_offset = 0;
m_scroll_offset = 0;
UpdateFileSystem();
}
void ImageGrid::SetStatus(ScalableBitmap const & icon, wxString const &msg)
{
int code = m_file_sys ? m_file_sys->GetLastError() : 1;
int code = m_model ? m_model->GetLastError() : 1;
m_status_icon = icon;
m_status_msg = wxString::Format(msg, code);
BOOST_LOG_TRIVIAL(info) << "ImageGrid::SetStatus: " << m_status_msg.ToUTF8().data();
@@ -107,26 +116,26 @@ void ImageGrid::SetStatus(ScalableBitmap const & icon, wxString const &msg)
void Slic3r::GUI::ImageGrid::SetFileType(int type, std::string const &storage)
{
if (!m_file_sys)
if (!m_model)
return;
m_file_sys->SetFileType((PrinterFileSystem::FileType) type, storage);
m_model->SetFileType((FileGridType) type, storage);
}
void Slic3r::GUI::ImageGrid::SetGroupMode(int mode)
{
if (!m_file_sys)
if (!m_model)
return;
if (m_file_sys->GetCount() == 0) {
m_file_sys->SetGroupMode((PrinterFileSystem::GroupMode) mode);
if (m_model->GetCount() == 0) {
m_model->SetGroupMode((FileGridGroup) mode);
return;
}
wxSize size = GetClientSize();
int index = (m_row_offset + 1 < m_row_count || m_row_count == 0)
? m_row_offset / 4 * m_col_count
: ((m_file_sys->GetCount() + m_col_count - 1) / m_col_count - (size.y + m_border_size.GetHeight() - 1) / m_cell_size.GetHeight()) * m_col_count;
auto & file = m_file_sys->GetFile(index);
m_file_sys->SetGroupMode((PrinterFileSystem::GroupMode) mode);
index = m_file_sys->GetIndexAtTime(file.time);
: ((m_model->GetCount() + m_col_count - 1) / m_col_count - (size.y + m_border_size.GetHeight() - 1) / m_cell_size.GetHeight()) * m_col_count;
time_t time = m_model->GetFile(index).time;
m_model->SetGroupMode((FileGridGroup) mode);
index = m_model->GetIndexAtTime(time);
// UpdateFileSystem(); call by changed event
m_row_offset = index / m_col_count * 4;
if (m_row_offset >= m_row_count)
@@ -137,8 +146,8 @@ void Slic3r::GUI::ImageGrid::SetGroupMode(int mode)
void Slic3r::GUI::ImageGrid::SetSelecting(bool selecting)
{
m_selecting = selecting;
if (m_file_sys)
m_file_sys->SelectAll(false);
if (m_model)
m_model->SelectAll(false);
Refresh();
}
@@ -166,14 +175,14 @@ void Slic3r::GUI::ImageGrid::Rescale()
void Slic3r::GUI::ImageGrid::Select(size_t index)
{
if (m_selecting) {
m_file_sys->ToggleSelect(index);
m_model->ToggleSelect(index);
Refresh();
return;
}
if (m_file_sys->GetGroupMode() == PrinterFileSystem::G_NONE) {
if (m_model->GetGroupMode() == FileGridGroup::All) {
return;
}
index = m_file_sys->EnterSubGroup(index);
index = m_model->EnterSubGroup(index);
// UpdateFileSystem(); call by changed event
m_row_offset = index / m_col_count * 4;
if (m_row_offset >= m_row_count)
@@ -193,9 +202,9 @@ void Slic3r::GUI::ImageGrid::DoAction(size_t index, int action)
void Slic3r::GUI::ImageGrid::UpdateFileSystem()
{
if (!m_file_sys) return;
if (m_file_sys->GetFileType() < PrinterFileSystem::F_MODEL) {
if (m_file_sys->GetGroupMode() == PrinterFileSystem::G_NONE) {
if (!m_model) return;
if (m_model->GetFileType() < FileGridType::Model) {
if (m_model->GetGroupMode() == FileGridGroup::All) {
m_cell_size.Set(396, 228);
m_border_size.Set(384, 216);
}
@@ -216,10 +225,10 @@ void Slic3r::GUI::ImageGrid::UpdateFileSystem()
void ImageGrid::UpdateLayout()
{
if (!m_file_sys) return;
if (!m_model) return;
wxSize size = GetClientSize();
wxSize title_mask_size{0, 60 * em_unit(this) / 10};
if (m_file_sys->GetGroupMode() == PrinterFileSystem::G_NONE) {
if (m_model->GetGroupMode() == FileGridGroup::All) {
title_mask_size.y = 20 * em_unit(this) / 10;
size.y -= title_mask_size.y;
}
@@ -227,7 +236,7 @@ void ImageGrid::UpdateLayout()
int cell_height = m_cell_size.GetHeight();
int ncol = (size.GetWidth() - cell_width + m_border_size.GetWidth()) / cell_width;
if (ncol <= 0) ncol = 1;
int total_height = (m_file_sys->GetCount() + ncol - 1) / ncol * cell_height + cell_height - m_border_size.GetHeight();
int total_height = (m_model->GetCount() + ncol - 1) / ncol * cell_height + cell_height - m_border_size.GetHeight();
int nrow = (total_height - size.GetHeight() + cell_height / 4 - 1) / (cell_height / 4);
m_row_offset = m_row_offset * m_col_count / ncol;
m_col_count = ncol;
@@ -236,7 +245,7 @@ void ImageGrid::UpdateLayout()
m_row_offset = m_row_count == 0 ? 0 : m_row_count - 1;
m_scroll_offset = 0;
// create mask
if (m_file_sys->GetGroupMode() == PrinterFileSystem::G_NONE) {
if (m_model->GetGroupMode() == FileGridGroup::All) {
title_mask_size.x = (m_col_count - 1) * m_cell_size.GetWidth() + m_border_size.GetWidth();
}
else {
@@ -252,7 +261,7 @@ void ImageGrid::UpdateLayout()
void Slic3r::GUI::ImageGrid::UpdateFocusRange()
{
if (!m_file_sys) return;
if (!m_model) return;
wxSize size = GetClientSize();
wxPoint off;
int index = firstItem(size, off);
@@ -261,15 +270,21 @@ void Slic3r::GUI::ImageGrid::UpdateFocusRange()
count += m_col_count;
off.y += m_cell_size.y;
}
m_file_sys->SetFocusRange(index, count);
m_model->SetFocusRange(index, count);
}
bool Slic3r::GUI::ImageGrid::hasSecondAction(const FileGridCard &card) const
{
return m_show_download || card.downloading ||
(m_model->GetFileType() == FileGridType::Model && card.printable);
}
std::pair<int, size_t> Slic3r::GUI::ImageGrid::HitTest(wxPoint const &pt)
{
if (!m_file_sys)
if (!m_model)
return {HIT_NONE, -1};
wxSize size = GetClientSize();
if (m_file_sys->GetCount() == 0) {
if (m_model->GetCount() == 0) {
if (wxRect({0, 0}, m_border_size).CenterIn(wxRect({0, 0}, size)).Contains(pt))
return {HIT_STATUS, 0};
return {HIT_NONE, -1};
@@ -287,17 +302,19 @@ std::pair<int, size_t> Slic3r::GUI::ImageGrid::HitTest(wxPoint const &pt)
index += m_col_count;
off.y -= m_cell_size.GetHeight();
}
if (index >= m_file_sys->GetCount()) { return {HIT_NONE, -1}; }
if (index >= m_model->GetCount()) { return {HIT_NONE, -1}; }
if (!m_content_rect.Contains(off)) { return {HIT_NONE, -1}; }
if (!m_selecting) {
wxRect hover_rect{0, m_content_rect.GetHeight() - m_buttons_background.GetHeight(), m_content_rect.GetWidth(), m_buttons_background.GetHeight()};
auto & file = m_file_sys->GetFile(index);
int btn = file.IsDownload() && file.DownloadProgress() >= 0 ? 3 : 2;
if (m_file_sys->GetFileType() == PrinterFileSystem::F_MODEL) {
if (m_show_download)
const FileGridCard &card = m_model->GetFile(index);
int btn = card.downloading && card.download_progress >= 0 ? 3 : 2;
if (m_model->GetFileType() == FileGridType::Model) {
if (m_show_download && card.printable)
btn = 3;
hover_rect.y -= m_content_rect.GetHeight() * 64 / 264;
}
if (!hasSecondAction(card))
btn = 1;
if (hover_rect.Contains(off.x, off.y)) {
return {HIT_ACTION, index * 4 + off.x * btn / hover_rect.GetWidth()};
} // Two buttons
@@ -315,12 +332,12 @@ void ImageGrid::mouseMoved(wxMouseEvent& event)
m_hit_item = hit.second;
if (hit.first == HIT_ITEM) {
SetToolTip({});
auto & file = m_file_sys->GetFile(hit.second);
if (auto title = file.Title(); !title.empty()) {
auto tip = wxString::Format(_L("File: %s\nTitle: %s\n"), from_u8(file.name), from_u8(title));
const FileGridCard &card = m_model->GetFile(hit.second);
if (auto title = card.title; !title.empty()) {
auto tip = wxString::Format(_L("File: %s\nTitle: %s\n"), from_u8(card.name), from_u8(title));
SetToolTip(tip);
} else {
SetToolTip(from_u8(file.name));
SetToolTip(from_u8(card.name));
}
} else
SetToolTip({});
@@ -372,9 +389,9 @@ void ImageGrid::mouseReleased(wxMouseEvent& event)
else if (m_hit_type == HIT_ACTION)
DoAction(m_hit_item / 4, m_hit_item & 3);
else if (m_hit_type == HIT_MODE)
SetGroupMode(static_cast<PrinterFileSystem::GroupMode>(2 - m_hit_item));
SetGroupMode(static_cast<int>(2 - m_hit_item));
else if (m_hit_type == HIT_STATUS)
m_file_sys->Retry();
m_model->Retry();
else
Refresh();
} else {
@@ -403,20 +420,11 @@ void ImageGrid::mouseWheelMoved(wxMouseEvent &event)
Refresh();
}
void Slic3r::GUI::ImageGrid::changedEvent(wxCommandEvent& evt)
void Slic3r::GUI::ImageGrid::onModelChange(FileGridChange change)
{
evt.Skip();
BOOST_LOG_TRIVIAL(debug) << "ImageGrid::changedEvent: " << evt.GetEventType() << " index: " << evt.GetInt()
<< " name: " << evt.GetString().ToUTF8().data() << " extra: " << evt.GetExtraLong();
if (evt.GetEventType() == EVT_FILE_CHANGED) {
if (evt.GetInt() == -1)
m_file_sys->DownloadCheckFiles(wxGetApp().app_config->get("download_path"));
BOOST_LOG_TRIVIAL(debug) << "ImageGrid::onModelChange: " << (int) change;
if (change == FileGridChange::Files || change == FileGridChange::Mode)
UpdateFileSystem();
}
else if (evt.GetEventType() == EVT_MODE_CHANGED)
UpdateFileSystem();
//else if (evt.GetEventType() == EVT_THUMBNAIL)
// RefreshRect(itemRect(evt.GetInt()), false);
else
Refresh();
}
@@ -431,7 +439,7 @@ void ImageGrid::paintEvent(wxPaintEvent& evt)
size_t Slic3r::GUI::ImageGrid::firstItem(wxSize const &size, wxPoint &off)
{
int size_y = size.y;
if (m_file_sys->GetGroupMode() == PrinterFileSystem::G_NONE)
if (m_model->GetGroupMode() == FileGridGroup::All)
size_y -= m_title_mask.GetHeight();
int offx = (size.x - (m_col_count - 1) * m_cell_size.GetWidth() - m_border_size.GetWidth()) / 2;
int offy = (m_row_offset + 1 < m_row_count || m_row_count == 0) ?
@@ -439,8 +447,8 @@ size_t Slic3r::GUI::ImageGrid::firstItem(wxSize const &size, wxPoint &off)
size_y - (size_y + m_border_size.GetHeight() - 1) / m_cell_size.GetHeight() * m_cell_size.GetHeight();
int index = (m_row_offset + 1 < m_row_count || m_row_count == 0) ?
m_row_offset / 4 * m_col_count :
((m_file_sys->GetCount() + m_col_count - 1) / m_col_count - (size_y + m_border_size.GetHeight() - 1) / m_cell_size.GetHeight()) * m_col_count;
if (m_file_sys->GetGroupMode() == PrinterFileSystem::G_NONE)
((m_model->GetCount() + m_col_count - 1) / m_col_count - (size_y + m_border_size.GetHeight() - 1) / m_cell_size.GetHeight()) * m_col_count;
if (m_model->GetGroupMode() == FileGridGroup::All)
offy += m_title_mask.GetHeight();
off = wxPoint{offx, offy};
return index;
@@ -534,7 +542,7 @@ void ImageGrid::render(wxDC& dc)
wxSize size = GetClientSize();
dc.SetPen(wxPen(GetBackgroundColour()));
dc.SetBrush(wxBrush(GetBackgroundColour()));
if (!m_file_sys || m_file_sys->GetCount() == 0) {
if (!m_model || m_model->GetCount() == 0) {
dc.DrawRectangle({ 0, 0, size.x, size.y });
if (!m_status_msg.IsEmpty()) {
auto si = m_status_icon.GetBmpSize();
@@ -562,7 +570,7 @@ void ImageGrid::render(wxDC& dc)
{
// Draw one line
wxPoint pt{off.x, off.y};
end = (index + m_col_count) < m_file_sys->GetCount() ? index + m_col_count : m_file_sys->GetCount();
end = (index + m_col_count) < m_model->GetCount() ? index + m_col_count : m_model->GetCount();
while (index < end) {
pt += m_content_rect.GetTopLeft();
// Draw content
@@ -571,7 +579,7 @@ void ImageGrid::render(wxDC& dc)
&ImageGrid::renderContent1,
&ImageGrid::renderContent2
};
(this->*contentRender[m_file_sys->GetFileType()])(dc, pt, index, hit_image == index);
(this->*contentRender[static_cast<int>(m_model->GetFileType())])(dc, pt, index, hit_image == index);
pt -= m_content_rect.GetTopLeft();
// Draw colume spacing at right
dc.DrawRectangle({pt.x + m_border_size.GetWidth(), pt.y, m_cell_size.GetWidth() - m_border_size.GetWidth(), m_border_size.GetHeight()});
@@ -588,13 +596,13 @@ void ImageGrid::render(wxDC& dc)
off.y += m_cell_size.GetHeight();
}
// Draw floating date range for non-group list
if (m_file_sys->GetGroupMode() == PrinterFileSystem::G_NONE && m_file_sys->GetCount() > 0) {
if (m_model->GetGroupMode() == FileGridGroup::All && m_model->GetCount() > 0) {
//dc.DrawBitmap(m_title_mask, {off.x, 0});
dc.DrawRectangle({off.x, 0}, m_title_mask.GetSize());
auto & file1 = m_file_sys->GetFile(start);
auto & file2 = m_file_sys->GetFile(end - 1);
auto date1 = wxDateTime((time_t) file1.time).Format(_L(TIME_FORMATS[m_file_sys->GetGroupMode()]));
auto date2 = wxDateTime((time_t) file2.time).Format(_L(TIME_FORMATS[m_file_sys->GetGroupMode()]));
const FileGridCard & file1 = m_model->GetFile(start);
const FileGridCard & file2 = m_model->GetFile(end - 1);
auto date1 = wxDateTime((time_t) file1.time).Format(_L(TIME_FORMATS[static_cast<int>(m_model->GetGroupMode())]));
auto date2 = wxDateTime((time_t) file2.time).Format(_L(TIME_FORMATS[static_cast<int>(m_model->GetGroupMode())]));
dc.SetFont(Label::Head_16);
dc.SetTextForeground(StateColor::darkModeColorFor("#262E30"));
dc.DrawText(date1 + " - " + date2, wxPoint{off.x, 2});
@@ -604,7 +612,7 @@ void ImageGrid::render(wxDC& dc)
dc.DrawRectangle({off.x, off.y, size.x - off.x * 2, size.y - off.y});
// Draw position bar
if (m_timer.IsRunning()) {
int total_height = (m_file_sys->GetCount() + m_col_count - 1) / m_col_count * m_cell_size.GetHeight() + m_cell_size.GetHeight() - m_border_size.GetHeight();
int total_height = (m_model->GetCount() + m_col_count - 1) / m_col_count * m_cell_size.GetHeight() + m_cell_size.GetHeight() - m_border_size.GetHeight();
if (total_height > size.y) {
int offset = (m_row_offset + 1 < m_row_count || m_row_count == 0) ? m_row_offset * (m_cell_size.GetHeight() / 4) : total_height - size.y;
wxRect rect = {size.x - 16, offset * size.y / total_height, 8,
@@ -618,15 +626,35 @@ void ImageGrid::render(wxDC& dc)
void Slic3r::GUI::ImageGrid::renderContent1(wxDC &dc, wxPoint const &pt, int index, bool hit)
{
bool selected = false;
auto &file = m_file_sys->GetFile(index, selected);
const FileGridCard &card = m_model->GetFile(index, selected);
// Draw thumbnail
if (file.thumbnail.IsOk()) {
float hs = (float) m_content_rect.GetWidth() / file.thumbnail.GetWidth();
float vs = (float) m_content_rect.GetHeight() / file.thumbnail.GetHeight();
dc.SetUserScale(hs, vs);
dc.DrawBitmap(file.thumbnail, {(int) (pt.x / hs), (int) (pt.y / vs)});
dc.SetUserScale(1, 1);
if (m_file_sys->GetGroupMode() != PrinterFileSystem::G_NONE) { dc.DrawBitmap(m_title_mask, pt); }
if (card.thumbnail.IsOk()) {
if (m_model->preserve_thumbnail_aspect()) {
// Contain: fit the whole thumbnail and letterbox against the tile background.
float scale = std::min((float) m_content_rect.GetWidth() / card.thumbnail.GetWidth(),
(float) m_content_rect.GetHeight() / card.thumbnail.GetHeight());
int thumb_w = (int) (card.thumbnail.GetWidth() * scale);
int thumb_h = (int) (card.thumbnail.GetHeight() * scale);
int offx = (m_content_rect.GetWidth() - thumb_w) / 2;
int offy = (m_content_rect.GetHeight() - thumb_h) / 2;
auto brush = dc.GetBrush();
auto pen = dc.GetPen();
dc.SetBrush(StateColor::darkModeColorFor(0xEEEEEE));
dc.SetPen(StateColor::darkModeColorFor(0xEEEEEE));
dc.DrawRectangle(pt, m_content_rect.GetSize());
dc.SetUserScale(scale, scale);
dc.DrawBitmap(card.thumbnail, {(int) ((pt.x + offx) / scale), (int) ((pt.y + offy) / scale)});
dc.SetUserScale(1, 1);
dc.SetBrush(brush);
dc.SetPen(pen);
} else {
float hs = (float) m_content_rect.GetWidth() / card.thumbnail.GetWidth();
float vs = (float) m_content_rect.GetHeight() / card.thumbnail.GetHeight();
dc.SetUserScale(hs, vs);
dc.DrawBitmap(card.thumbnail, {(int) (pt.x / hs), (int) (pt.y / vs)});
dc.SetUserScale(1, 1);
}
if (m_model->GetGroupMode() != FileGridGroup::All) { dc.DrawBitmap(m_title_mask, pt); }
}
bool show_download_state_always = true;
// Draw checked icon
@@ -634,14 +662,14 @@ void Slic3r::GUI::ImageGrid::renderContent1(wxDC &dc, wxPoint const &pt, int ind
dc.DrawBitmap(selected ? m_checked_icon.bmp() : m_unchecked_icon.bmp(), pt + wxPoint{10, 10});
// can't handle alpha
// dc.GradientFillLinear({pt.x, pt.y, m_border_size.GetWidth(), 60}, wxColour(0x6F, 0x6F, 0x6F, 0x99), wxColour(0x6F, 0x6F, 0x6F, 0), wxBOTTOM);
else if (m_file_sys->GetGroupMode() == PrinterFileSystem::G_NONE) {
else if (m_model->GetGroupMode() == FileGridGroup::All) {
wxString nonHoverText;
wxString secondAction = m_show_download ? _L("Download") : "";
wxString thirdAction;
int states = 0;
// Draw download progress
if (file.IsDownload()) {
int progress = file.DownloadProgress();
if (card.downloading) {
int progress = card.download_progress;
if (progress == -1) {
secondAction = _L("Cancel");
nonHoverText = _L("Download waiting...");
@@ -659,7 +687,7 @@ void Slic3r::GUI::ImageGrid::renderContent1(wxDC &dc, wxPoint const &pt, int ind
thirdAction = wxString::Format(L"%d%%...", progress);
}
}
if (m_file_sys->GetFileType() == PrinterFileSystem::F_MODEL) {
if (m_model->GetFileType() == FileGridType::Model && card.printable) {
if (secondAction != _L("Play"))
thirdAction = secondAction;
secondAction = _L_CONTEXT("Print", "Verb");
@@ -669,7 +697,8 @@ void Slic3r::GUI::ImageGrid::renderContent1(wxDC &dc, wxPoint const &pt, int ind
wxArrayString texts;
if (hit) {
texts.Add(_L("Delete"));
texts.Add(secondAction);
if (hasSecondAction(card))
texts.Add(secondAction);
if (!thirdAction.IsEmpty())
texts.Add(thirdAction);
renderButtons(dc, texts, rect, m_hit_type == HIT_ACTION ? m_hit_item & 3 : -1, states);
@@ -679,7 +708,7 @@ void Slic3r::GUI::ImageGrid::renderContent1(wxDC &dc, wxPoint const &pt, int ind
}
} else {
dc.SetTextForeground(*wxWHITE); // time text color
auto date = wxDateTime((time_t) file.time).Format(_L(TIME_FORMATS[m_file_sys->GetGroupMode()]));
auto date = wxDateTime((time_t) card.time).Format(_L(TIME_FORMATS[static_cast<int>(m_model->GetGroupMode())]));
dc.DrawText(date, pt + wxPoint{24, 16});
}
if (m_selecting && show_download_state_always)
@@ -688,7 +717,7 @@ void Slic3r::GUI::ImageGrid::renderContent1(wxDC &dc, wxPoint const &pt, int ind
void Slic3r::GUI::ImageGrid::renderContent2(wxDC &dc, wxPoint const &pt, int index, bool hit)
{
auto &file = m_file_sys->GetFile(index);
const FileGridCard &card = m_model->GetFile(index);
// Draw thumbnail & buttons
int h = m_content_rect.GetHeight() * 64 / 264;
m_content_rect.SetHeight(m_content_rect.GetHeight() - h);
@@ -711,14 +740,14 @@ void Slic3r::GUI::ImageGrid::renderContent2(wxDC &dc, wxPoint const &pt, int ind
auto em = em_unit(this);
wxRect rect{pt.x, pt.y + m_content_rect.GetHeight() - h, m_content_rect.GetWidth(), h / 2};
rect.Deflate(em, 0);
renderText2(dc, from_u8(file.name), rect);
renderText2(dc, from_u8(card.name), rect);
rect.Offset(0, h / 2);
rect.SetWidth(rect.GetWidth() / 2 - em);
dc.SetFont(Label::Body_13);
dc.SetTextForeground(StateColor::darkModeColorFor("#6B6B6B"));
renderIconText(dc, m_model_time_icon, file.Metadata("Time", "0m"), rect);
renderIconText(dc, m_model_time_icon, card.time_text.empty() ? wxString("0m") : from_u8(card.time_text), rect);
rect.Offset(m_content_rect.GetWidth() / 2, 0);
renderIconText(dc, m_model_weight_icon, file.Metadata("Weight", "0g"), rect);
renderIconText(dc, m_model_weight_icon, card.weight_text.empty() ? wxString("0g") : from_u8(card.weight_text), rect);
}
void Slic3r::GUI::ImageGrid::renderButtons(wxDC &dc, wxArrayString const &texts, wxRect const &rect2, size_t hit, int states)
+8 -1
View File
@@ -23,6 +23,7 @@
#include "Widgets/StateColor.hpp"
#include "wxExtensions.hpp"
#include "Printer/IFileGridModel.h"
class Button;
class Label;
@@ -44,6 +45,8 @@ public:
void SetFileSystem(boost::shared_ptr<PrinterFileSystem> file_sys);
void SetModel(boost::shared_ptr<IFileGridModel> model);
void SetStatus(ScalableBitmap const & icon, wxString const &msg);
boost::shared_ptr<PrinterFileSystem> GetFileSystem() { return m_file_sys; }
@@ -76,9 +79,12 @@ protected:
std::pair<int, size_t> HitTest(wxPoint const &pt);
// True when the hover bar shows a second action next to Delete.
bool hasSecondAction(const FileGridCard &card) const;
protected:
void changedEvent(wxCommandEvent& evt);
void onModelChange(FileGridChange change);
void paintEvent(wxPaintEvent& evt);
@@ -117,6 +123,7 @@ protected:
private:
boost::shared_ptr<PrinterFileSystem> m_file_sys;
boost::shared_ptr<IFileGridModel> m_model;
ScalableBitmap m_status_icon;
wxString m_status_msg;
@@ -8,7 +8,6 @@
#include "libslic3r/TextureDisplacement.hpp"
#include <exception>
#include <cstddef>
#include <cstdint>
#include "slic3r/GUI/Jobs/Worker.hpp"
#include <memory>
@@ -27,7 +26,6 @@
#include "slic3r/Utils/UndoRedo.hpp"
#include "libslic3r/ObjectID.hpp"
#include "libslic3r/TriangleMesh.hpp"
#include <array>
namespace Slic3r::GUI {
@@ -128,71 +126,16 @@ void TextureDisplacementBakeJob::finalize(bool canceled, std::exception_ptr &ept
{ mix.a_percent(), 100 - mix.a_percent() });
});
// Whether the bake replaced the triangles or only moved the vertices it was given. The classic
// path is topology-preserving by contract, so every per-triangle annotation on the volume still
// means what it did; the default pipeline refines, cuts and simplifies, so none of them do.
// Both are checked: the flag is what the bake promised, the count is what it delivered. Only
// the triangles matter - the annotations are per facet and reference no vertex index - so a
// vertex count that changed on its own (the bake compacts unreferenced vertices away) is not a
// reason to remap anything.
const bool topology_changed = m_input.options.pipeline_v2 ||
m_result.its.indices.size() != m_input.base_mesh.indices.size();
// Carry the volume's existing paint - the model's own colours above all - across that rebuild.
// Without it a bake left the mmu_segmentation_facets bitstream pointing at a triangle set that
// no longer existed, which is what scrambled the colours of an imported coloured model even
// with colour switched off on every layer and nothing in the bake that was meant to touch them.
//
// transfer_facet_paint() rather than ModelVolume::restore_painting(): the latter's remap wants
// a target triangle overlapping the source paint to within 0.01 mm, which the baked surface is
// nowhere near - it has moved by the relief depth. Nothing to do when the topology is intact:
// triangle i is still triangle i, so the paint is already exactly right and carrying it at all
// could only lose detail.
std::array<TriangleSelector::TriangleSplittingData, 4> before;
if (topology_changed) {
before[0] = volume->supported_facets.get_data();
before[1] = volume->seam_facets.get_data();
before[2] = volume->mmu_segmentation_facets.get_data();
before[3] = volume->fuzzy_skin_facets.get_data();
}
volume->set_mesh(std::move(m_result));
volume->set_new_unique_id();
volume->calculate_convex_hull();
if (topology_changed) {
const std::array<FacetsAnnotation *, 4> target{ &volume->supported_facets, &volume->seam_facets,
&volume->mmu_segmentation_facets,
&volume->fuzzy_skin_facets };
for (size_t ch = 0; ch < target.size(); ++ch) {
const std::vector<uint8_t> state =
transfer_facet_paint(m_input.base_mesh, before[ch], volume->mesh().its);
// Nothing was painted in this channel, or there is nothing to paint it onto: either
// way the annotation must not keep describing the mesh that is gone.
if (state.empty()) {
target[ch]->reset();
continue;
}
TriangleSelector selector(volume->mesh());
bool any = false;
for (size_t i = 0; i < state.size(); ++i)
if (state[i] != 0) {
selector.set_facet(int(i), EnforcerBlockerType(state[i]));
any = true;
}
if (any)
target[ch]->set(selector);
else
target[ch]->reset();
}
}
// Colour lands in mmu_segmentation_facets, merged *over* whatever is already painted there
// rather than replacing it: a triangle the texture does not colour keeps its existing filament,
// and one the user never painted at all stays at NONE, which already means "the volume's own
// filament". That is what confines the effect to the painted area without having to invent a
// colour for everything outside it. What was already painted is whatever the transfer above
// left in the annotation, so this bake's colour lands on top of it rather than instead of it.
// colour for everything outside it. Safe to index straight onto the new mesh - the bake is
// topology-preserving, so triangle i is still triangle i.
if (!m_triangle_color.empty() && m_triangle_color.size() == volume->mesh().its.indices.size()) {
TriangleSelector selector(volume->mesh());
const TriangleSelector::TriangleSplittingData &existing = volume->mmu_segmentation_facets.get_data();
@@ -216,6 +159,7 @@ void TextureDisplacementBakeJob::finalize(bool canceled, std::exception_ptr &ept
// pipeline, which rebuilds and then simplifies the mesh. A mask left behind against the old
// topology is exactly what makes the gizmo reload an empty selector over a non-empty mask and
// then erase it on the next flush - see GLGizmoTextureDisplacement::update_from_model_object().
const bool topology_changed = m_input.base_mesh.indices.size() != volume->mesh().its.indices.size();
for (int slot = 0; slot < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++slot) {
const auto it = std::find_if(m_input.layers.begin(), m_input.layers.end(),
[slot](const TextureDisplacementLayer &l) { return l.slot == slot; });
+25 -11
View File
@@ -221,26 +221,40 @@ MediaFilePanel::MediaFilePanel(wxWindow * parent)
SetSelecting(false);
});
auto onShowHide = [this](auto &e) {
e.Skip();
if (auto w = dynamic_cast<wxWindow *>(e.GetEventObject()); !w || w->IsBeingDeleted())
return;
CallAfter([this] {
auto fs = m_image_grid ? m_image_grid->GetFileSystem() : nullptr;
if (fs) IsShownOnScreen() ? fs->Start() : fs->Stop();
});
};
Bind(wxEVT_SHOW, onShowHide);
parent->GetParent()->Bind(wxEVT_SHOW, onShowHide);
Bind(wxEVT_SHOW, &MediaFilePanel::on_show_hide, this);
// A Device sub-tab switch hides the page container (StoragePanel); a main tab
// switch hides MonitorPanel inside LazyPage. Neither event reaches the other,
// so watch every ancestor below the top-level frame.
for (wxWindow* w = GetParent(); w && !w->IsTopLevel(); w = w->GetParent()) {
w->Bind(wxEVT_SHOW, &MediaFilePanel::on_show_hide, this);
m_show_hosts.push_back(w);
}
m_lan_user = "bblp";
}
MediaFilePanel::~MediaFilePanel()
{
Unbind(wxEVT_SHOW, &MediaFilePanel::on_show_hide, this);
for (wxWindow* w : m_show_hosts)
if (w && !w->IsBeingDeleted())
w->Unbind(wxEVT_SHOW, &MediaFilePanel::on_show_hide, this);
m_show_hosts.clear();
UpdateByObj(nullptr);
}
void MediaFilePanel::on_show_hide(wxShowEvent& e)
{
e.Skip();
if (auto w = dynamic_cast<wxWindow*>(e.GetEventObject()); !w || w->IsBeingDeleted())
return;
CallAfter([this] {
auto fs = m_image_grid ? m_image_grid->GetFileSystem() : nullptr;
if (fs) IsShownOnScreen() ? fs->Start() : fs->Stop();
});
}
void MediaFilePanel::UpdateByObj(MachineObject* obj)
{
bool sdcard_state_changed = false;
+5
View File
@@ -11,6 +11,7 @@
#include <boost/smart_ptr/shared_ptr.hpp>
#include <cstddef>
#include <set>
#include <vector>
#include "GUI_Utils.hpp"
#include "wxExtensions.hpp"
@@ -53,6 +54,8 @@ public:
private:
void modeChanged(wxCommandEvent & e);
void on_show_hide(wxShowEvent & e);
void fetchUrl(boost::weak_ptr<PrinterFileSystem> fs);
void doAction(size_t index, int action);
@@ -99,6 +102,8 @@ private:
int m_last_mode = 0;
int m_last_type = 0;
std::set<int> m_last_errors;
std::vector<wxWindow*> m_show_hosts;
};
+10 -9
View File
@@ -44,7 +44,7 @@
#include "Plater.hpp"
#include "MainFrame.hpp"
#include "MediaPlayCtrl.h"
#include "MediaFilePanel.h"
#include "StoragePanel.h"
#include "BindDialog.hpp"
#include "DeviceCore/DevManager.h"
@@ -201,9 +201,9 @@ void MonitorPanel::init_tabpanel()
m_tabpanel->SetBackgroundColour(wxColour("#FEFFFF"));
m_tabpanel->Bind(wxEVT_BOOKCTRL_PAGE_CHANGED, [this](wxBookCtrlEvent& e) {
auto page = m_tabpanel->GetCurrentPage();
if (page == m_media_file_panel) {
if (page == m_storage_panel) {
auto title = m_tabpanel->GetPageText(m_tabpanel->GetSelection());
m_media_file_panel->SwitchStorage(title == _L("Storage"));
m_storage_panel->SwitchStorage(title == _L("Storage"));
}
// The first page is selected while the panel is built off screen.
if (page->IsShownOnScreen())
@@ -217,8 +217,8 @@ void MonitorPanel::init_tabpanel()
add_build_steps_of(*m_status_info_panel);
m_tabpanel->AddPage(m_status_info_panel, _L("Status"), true);
add_build_step([this] {
m_media_file_panel = new MediaFilePanel(m_tabpanel);
m_tabpanel->AddPage(m_media_file_panel, _L("Storage"), false);
m_storage_panel = new StoragePanel(m_tabpanel);
m_tabpanel->AddPage(m_storage_panel, _L("Storage"), false);
});
add_build_step([this] {
m_upgrade_panel = new UpgradePanel(m_tabpanel);
@@ -267,7 +267,8 @@ void MonitorPanel::on_sys_color_changed()
{
m_status_info_panel->on_sys_color_changed();
m_upgrade_panel->on_sys_color_changed();
m_media_file_panel->Rescale();
m_storage_panel->on_sys_color_changed();
m_storage_panel->Rescale();
}
void MonitorPanel::msw_rescale()
@@ -279,7 +280,7 @@ void MonitorPanel::msw_rescale()
m_tabpanel->Rescale();
//m_status_add_machine_panel->msw_rescale();
m_status_info_panel->msw_rescale();
m_media_file_panel->Rescale();
m_storage_panel->Rescale();
m_upgrade_panel->msw_rescale();
m_hms_panel->msw_rescale();
@@ -426,8 +427,8 @@ void MonitorPanel::update_all()
}
} else if (current_page == m_upgrade_panel) {
m_upgrade_panel->update(obj);
} else if (current_page == m_media_file_panel) {
m_media_file_panel->UpdateByObj(obj);
} else if (current_page == m_storage_panel) {
m_storage_panel->UpdateByObj(obj);
}
if (current_page == m_hms_panel || (obj->GetHMS()->GetHMSItems().size() != m_hms_panel->temp_hms_list.size())) {
+2 -2
View File
@@ -57,7 +57,7 @@
namespace Slic3r {
namespace GUI {
class MediaFilePanel;
class StoragePanel;
class AddMachinePanel : public wxPanel
{
@@ -83,7 +83,7 @@ private:
wxSizer* m_main_sizer{ nullptr };
StatusPanel* m_status_info_panel{ nullptr };
MediaFilePanel* m_media_file_panel{ nullptr };
StoragePanel* m_storage_panel{ nullptr };
UpgradePanel* m_upgrade_panel{ nullptr };
HMSPanel* m_hms_panel{ nullptr };
+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
@@ -6467,9 +6467,10 @@ void Sidebar::sync_ams_list(bool is_from_big_sync_btn)
for (const auto &entry : wxGetApp().preset_bundle->filament_ams_list) {
const auto &tray = entry.second;
const bool has_filament = !tray.opt_string("filament_id", 0u).empty();
const bool has_type = !tray.opt_string("filament_type", 0u).empty();
const bool is_placeholder = tray.has("filament_slot_placeholder") &&
tray.opt_bool("filament_slot_placeholder", 0u);
if (!has_filament && !is_placeholder) {
if (!has_filament && !is_placeholder && !has_type) {
continue;
}
if (combo_index >= p->combos_filament.size()) {
@@ -0,0 +1,204 @@
#include "BambuFileGridModel.h"
#include "slic3r/GUI/GUI_App.hpp"
#include <slic3r/GUI/Printer/IFileGridModel.h>
#include <boost/smart_ptr/shared_ptr.hpp>
#include <slic3r/GUI/Printer/PrinterFileSystem.h>
#include <cstddef>
#include <ctime>
#include <functional>
#include <utility>
#include <wx/event.h>
namespace Slic3r {
namespace GUI {
static const FileGridCard& emptyCard()
{
static const FileGridCard card;
return card;
}
BambuFileGridModel::BambuFileGridModel(boost::shared_ptr<PrinterFileSystem> fs)
: m_fs(std::move(fs))
{
if (m_fs) {
m_fs->Bind(EVT_FILE_CHANGED, &BambuFileGridModel::onFileChanged, this);
m_fs->Bind(EVT_MODE_CHANGED, &BambuFileGridModel::onModeChanged, this);
m_fs->Bind(EVT_THUMBNAIL, &BambuFileGridModel::onThumbnail, this);
m_fs->Bind(EVT_DOWNLOAD, &BambuFileGridModel::onDownload, this);
}
}
BambuFileGridModel::~BambuFileGridModel()
{
if (m_fs) {
m_fs->Unbind(EVT_FILE_CHANGED, &BambuFileGridModel::onFileChanged, this);
m_fs->Unbind(EVT_MODE_CHANGED, &BambuFileGridModel::onModeChanged, this);
m_fs->Unbind(EVT_THUMBNAIL, &BambuFileGridModel::onThumbnail, this);
m_fs->Unbind(EVT_DOWNLOAD, &BambuFileGridModel::onDownload, this);
}
}
void BambuFileGridModel::EnsureCards() const
{
if (!m_dirty && (!m_fs || m_cards.size() == m_fs->GetCount()))
return;
m_dirty = false;
m_cards.clear();
if (!m_fs)
return;
size_t count = m_fs->GetCount();
m_cards.reserve(count);
for (size_t i = 0; i < count; ++i) {
bool selected = false;
PrinterFileSystem::File const &file = m_fs->GetFile(i, selected);
FileGridCard card;
card.id = file.path;
card.name = file.name;
card.title = file.Title();
card.time_text = file.Metadata("Time", "");
card.weight_text = file.Metadata("Weight", "");
card.time = file.time;
card.downloading = file.IsDownload();
card.download_progress = file.DownloadProgress();
card.selected = selected;
card.thumbnail = file.thumbnail;
m_cards.push_back(std::move(card));
}
}
size_t BambuFileGridModel::GetCount() const
{
return m_fs ? m_fs->GetCount() : 0;
}
const FileGridCard& BambuFileGridModel::GetFile(size_t index, bool& selected) const
{
EnsureCards();
if (!m_fs || index >= m_cards.size()) {
selected = false;
return emptyCard();
}
m_fs->GetFile(index, selected);
return m_cards[index];
}
const FileGridCard& BambuFileGridModel::GetFile(size_t index) const
{
EnsureCards();
if (index >= m_cards.size())
return emptyCard();
return m_cards[index];
}
FileGridType BambuFileGridModel::GetFileType() const
{
return m_fs ? static_cast<FileGridType>(m_fs->GetFileType()) : FileGridType::Timelapse;
}
void BambuFileGridModel::SetFileType(FileGridType type, const std::string& storage)
{
if (m_fs)
m_fs->SetFileType(static_cast<PrinterFileSystem::FileType>(type), storage);
}
FileGridGroup BambuFileGridModel::GetGroupMode() const
{
return m_fs ? static_cast<FileGridGroup>(m_fs->GetGroupMode()) : FileGridGroup::All;
}
void BambuFileGridModel::SetGroupMode(FileGridGroup mode)
{
if (m_fs)
m_fs->SetGroupMode(static_cast<PrinterFileSystem::GroupMode>(mode));
}
size_t BambuFileGridModel::GetIndexAtTime(time_t time) const
{
return m_fs ? m_fs->GetIndexAtTime(static_cast<boost::uint32_t>(time)) : 0;
}
size_t BambuFileGridModel::EnterSubGroup(size_t index)
{
return m_fs ? m_fs->EnterSubGroup(index) : index;
}
void BambuFileGridModel::ToggleSelect(size_t index)
{
if (m_fs)
m_fs->ToggleSelect(index);
}
void BambuFileGridModel::SelectAll(bool select)
{
if (m_fs)
m_fs->SelectAll(select);
}
void BambuFileGridModel::SetFocusRange(size_t start, size_t count)
{
if (m_fs)
m_fs->SetFocusRange(start, count);
}
void BambuFileGridModel::Retry()
{
if (m_fs)
m_fs->Retry();
}
int BambuFileGridModel::GetLastError() const
{
return m_fs ? m_fs->GetLastError() : 1;
}
void BambuFileGridModel::DownloadCheckFiles(const std::string& path)
{
if (m_fs)
m_fs->DownloadCheckFiles(path);
}
void BambuFileGridModel::SetChangeHandler(std::function<void(FileGridChange)> handler)
{
m_handler = std::move(handler);
}
void BambuFileGridModel::notify(FileGridChange change)
{
if (m_handler)
m_handler(change);
}
void BambuFileGridModel::onFileChanged(wxCommandEvent& evt)
{
evt.Skip();
if (evt.GetInt() == -1 && m_fs)
m_fs->DownloadCheckFiles(wxGetApp().app_config->get("download_path"));
m_dirty = true;
notify(FileGridChange::Files);
}
void BambuFileGridModel::onModeChanged(wxCommandEvent& evt)
{
evt.Skip();
m_dirty = true;
notify(FileGridChange::Mode);
}
void BambuFileGridModel::onThumbnail(wxCommandEvent& evt)
{
evt.Skip();
m_dirty = true;
notify(FileGridChange::Thumbnail);
}
void BambuFileGridModel::onDownload(wxCommandEvent& evt)
{
evt.Skip();
m_dirty = true;
notify(FileGridChange::Download);
}
}}
@@ -0,0 +1,61 @@
#pragma once
#include "IFileGridModel.h"
#include "PrinterFileSystem.h"
#include <boost/smart_ptr/shared_ptr.hpp>
#include <wx/event.h>
#include <cstddef>
#include <ctime>
#include <functional>
#include <string>
#include <vector>
namespace Slic3r {
namespace GUI {
// IFileGridModel over PrinterFileSystem, behavior-identical to ImageGrid's direct use.
class BambuFileGridModel : public wxEvtHandler, public IFileGridModel
{
public:
explicit BambuFileGridModel(boost::shared_ptr<PrinterFileSystem> fs);
~BambuFileGridModel() override;
size_t GetCount() const override;
const FileGridCard& GetFile(size_t index, bool& selected) const override;
const FileGridCard& GetFile(size_t index) const override;
FileGridType GetFileType() const override;
void SetFileType(FileGridType type, const std::string& storage) override;
FileGridGroup GetGroupMode() const override;
void SetGroupMode(FileGridGroup mode) override;
size_t GetIndexAtTime(time_t time) const override;
size_t EnterSubGroup(size_t index) override;
void ToggleSelect(size_t index) override;
void SelectAll(bool select) override;
void SetFocusRange(size_t start, size_t count) override;
void Retry() override;
int GetLastError() const override;
void DownloadCheckFiles(const std::string& path) override;
void SetChangeHandler(std::function<void(FileGridChange)> handler) override;
private:
void EnsureCards() const;
void notify(FileGridChange change);
void onFileChanged(wxCommandEvent& evt);
void onModeChanged(wxCommandEvent& evt);
void onThumbnail(wxCommandEvent& evt);
void onDownload(wxCommandEvent& evt);
boost::shared_ptr<PrinterFileSystem> m_fs;
std::function<void(FileGridChange)> m_handler;
mutable std::vector<FileGridCard> m_cards;
mutable bool m_dirty = true;
};
}}
+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_ANY);
if (decoded.IsOk())
bitmap = wxBitmap(decoded);
}
m_thumbnail_cache[path] = std::move(bitmap);
if (m_change_handler)
m_change_handler(FileGridChange::Thumbnail);
}
pumpThumbnailQueue();
}
void RemoteFileGridModel::requestMetadata(const std::string& path)
{
if (m_metadata_cache.find(path) != m_metadata_cache.end() || !m_metadata_requested.insert(path).second)
return;
m_metadata_queue.push_back(path);
}
void RemoteFileGridModel::pumpMetadataQueue()
{
constexpr size_t MAX_METADATA_REQUESTS = 4;
if (m_pumping_metadata_queue)
return;
m_pumping_metadata_queue = true;
NetworkAgent* agent = wxGetApp().getAgent();
if (!agent) {
while (!m_metadata_queue.empty()) {
m_metadata_cache.emplace(m_metadata_queue.front(), PrinterFileMetadata{});
m_metadata_queue.pop_front();
}
m_pumping_metadata_queue = false;
return;
}
while (!m_metadata_queue.empty() && m_metadata_requests_in_flight < MAX_METADATA_REQUESTS) {
std::string path = std::move(m_metadata_queue.front());
m_metadata_queue.pop_front();
++m_metadata_requests_in_flight;
const std::uint64_t request = m_request;
const std::string device_id = m_device_id;
const std::weak_ptr<int> lifetime = m_lifetime;
const auto completed = std::make_shared<bool>(false);
auto finish = [this, lifetime, completed, request, device_id, path](int result, PrinterFileMetadata meta) {
if (*completed)
return;
*completed = true;
if (lifetime.expired())
return;
onMetadataLoaded(request, device_id, path, result, meta);
};
const int result = agent->get_printer_file_metadata(device_id, path, finish);
if (result != 0)
finish(result, {});
}
m_pumping_metadata_queue = false;
}
void RemoteFileGridModel::onMetadataLoaded(std::uint64_t request, const std::string& device_id,
const std::string& path, int result, PrinterFileMetadata meta)
{
if (m_metadata_requests_in_flight > 0)
--m_metadata_requests_in_flight;
if (request == m_request && device_id == m_device_id) {
m_metadata_cache[path] = meta;
if (result == 0) {
for (FileGridCard& card : m_cards) {
if (card.id != path)
continue;
card.time_text = meta.estimated_time > 0 ? durationString(meta.estimated_time) : std::string();
card.weight_text = meta.filament_weight > 0
? std::to_string(int(std::round(meta.filament_weight))) + 'g'
: std::string();
break;
}
if (m_change_handler)
m_change_handler(FileGridChange::Thumbnail);
}
}
pumpMetadataQueue();
}
}}
@@ -0,0 +1,100 @@
#pragma once
#include "IFileGridModel.h"
#include "slic3r/Utils/IPrinterAgent.hpp"
#include <cstddef>
#include <cstdint>
#include <ctime>
#include <deque>
#include <functional>
#include <memory>
#include <string>
#include <unordered_map>
#include <unordered_set>
#include <vector>
namespace Slic3r {
namespace GUI {
class RemoteFileGridModel : public IFileGridModel
{
public:
enum class Status { Loading, Ready, Empty, Failed };
explicit RemoteFileGridModel(std::string device_id);
~RemoteFileGridModel() override;
void Refresh();
Status GetStatus() const { return m_status; }
void SetStatusHandler(std::function<void(Status)> handler);
void SetGroupModeHandler(std::function<void(FileGridGroup)> handler);
size_t GetCount() const override;
const FileGridCard& GetFile(size_t index, bool& selected) const override;
const FileGridCard& GetFile(size_t index) const override;
FileGridType GetFileType() const override;
void SetFileType(FileGridType type, const std::string& storage) override;
FileGridGroup GetGroupMode() const override;
void SetGroupMode(FileGridGroup mode) override;
size_t GetIndexAtTime(time_t time) const override;
size_t EnterSubGroup(size_t index) override;
void ToggleSelect(size_t index) override;
void SelectAll(bool select) override;
void SetFocusRange(size_t start, size_t count) override;
void Retry() override;
int GetLastError() const override;
void DownloadCheckFiles(const std::string& path) override;
bool preserve_thumbnail_aspect() const override { return true; }
// Delete `path` from the printer; `done` reports success on the UI thread.
void DeleteFile(const std::string& path, std::function<void(bool ok)> done);
void SetChangeHandler(std::function<void(FileGridChange)> handler) override;
private:
void setStatus(Status status);
void onFilesLoaded(std::uint64_t request, const std::string& device_id, int result,
std::vector<PrinterFileEntry> files);
void buildGroups();
size_t cardIndexForGroup(size_t index) const;
void requestThumbnail(const std::string& path);
void pumpThumbnailQueue();
void onThumbnailLoaded(std::uint64_t request, const std::string& device_id,
const std::string& path, int result, std::string image);
void requestMetadata(const std::string& path);
void pumpMetadataQueue();
void onMetadataLoaded(std::uint64_t request, const std::string& device_id,
const std::string& path, int result, PrinterFileMetadata meta);
std::string m_device_id;
mutable std::vector<FileGridCard> m_cards;
std::vector<size_t> m_month;
std::vector<size_t> m_year;
std::function<void(FileGridChange)> m_change_handler;
std::function<void(Status)> m_status_handler;
std::function<void(FileGridGroup)> m_group_mode_handler;
std::shared_ptr<int> m_lifetime = std::make_shared<int>(0);
std::unordered_map<std::string, wxBitmap> m_thumbnail_cache;
std::unordered_set<std::string> m_thumbnail_requested;
std::deque<std::string> m_thumbnail_queue;
std::unordered_map<std::string, PrinterFileMetadata> m_metadata_cache;
std::unordered_set<std::string> m_metadata_requested;
std::deque<std::string> m_metadata_queue;
std::uint64_t m_request = 0;
size_t m_thumbnail_requests_in_flight = 0;
bool m_pumping_thumbnail_queue = false;
size_t m_metadata_requests_in_flight = 0;
bool m_pumping_metadata_queue = false;
int m_last_error = 0;
FileGridType m_file_type = FileGridType::Timelapse;
FileGridGroup m_group_mode = FileGridGroup::All;
Status m_status = Status::Loading;
};
}}
+27
View File
@@ -36,6 +36,8 @@
#include "libslic3r/MultiNozzleUtils.hpp" // filament-change-gap model for the best-position popup
#include "BackgroundSlicingProcess.hpp" // complete type for background_process().get_current_gcode_result()
#include "DeviceCore/DevStorage.h"
#include "slic3r/Utils/NetworkAgentFactory.hpp"
#include "FilamentMappingUtils.hpp"
#include <wx/event.h>
#include <string>
@@ -3568,6 +3570,9 @@ void SelectMachineDialog::navigate_to_timelapse_page()
this->EndModal(wxID_CANCEL);
}
// Mapping helpers live in FilamentMappingUtils.hpp (shared with
// SendMultiMachinePage); they mirror the agent serializer exactly.
void SelectMachineDialog::on_send_print()
{
BOOST_LOG_TRIVIAL(info) << "print_job: on_ok to send";
@@ -3622,6 +3627,28 @@ void SelectMachineDialog::on_send_print()
get_ams_mapping_result(ams_mapping_array,ams_mapping_array2, ams_mapping_info);
// The policy lives in FilamentMappingUtils.hpp; this dialog only differs
// from the multi-device page in how it reports a refusal.
switch (prepare_filament_mapping_for_send(obj_, ams_mapping_array2, m_ams_mapping_result)) {
case MappingSendError::unsupported:
BOOST_LOG_TRIVIAL(warning) << "print_job: connector does not advertise filament_mapping; refusing mapped print";
m_status_bar->set_status_text(_L("AMS filament mapping is not available for this printer. Clear the AMS mapping before printing."));
Enable_Send_Button(true);
return;
case MappingSendError::incomplete:
BOOST_LOG_TRIVIAL(warning) << "print_job: a used filament has no AMS target; refusing print";
m_status_bar->set_status_text(_L("A filament used by this print has no AMS mapping. Assign it before printing."));
Enable_Send_Button(true);
return;
case MappingSendError::none:
break;
default:
// A refusal added without a handler here must not silently send.
BOOST_LOG_TRIVIAL(warning) << "print_job: unrecognized mapping refusal; refusing print";
Enable_Send_Button(true);
return;
}
if (m_print_type == PrintFromType::FROM_NORMAL) {
result = m_plater->send_gcode(m_print_plate_idx, [this](int export_stage, int current, int total, bool& cancel) {
if (this->m_is_canceled) return;
+3 -2
View File
@@ -8,6 +8,7 @@
#include "GUI.hpp"
#include "GUI_App.hpp"
#include "slic3r/Utils/NetworkAgent.hpp"
#include "GUI_Preview.hpp"
#include "MainFrame.hpp"
#include "Widgets/ProgressDialog.hpp"
@@ -847,8 +848,8 @@ void SelectMachinePopup::update_machine_list(wxCommandEvent &event)
void SelectMachinePopup::start_ssdp(bool start)
{
return;
//if (wxGetApp().getAgent()) { wxGetApp().getAgent()->start_discovery(true, start); }
if (auto* agent = wxGetApp().getAgent())
agent->start_discovery(true, start);
}
void SelectMachinePopup::OnLeftUp(wxMouseEvent &event)
+30
View File
@@ -61,6 +61,10 @@
#include "DeviceCore/DevManager.h"
#include "DeviceCore/DevStorage.h"
#include "slic3r/Utils/NetworkAgentFactory.hpp"
#include "FilamentMappingUtils.hpp"
#include "libslic3r/AppConfig.hpp"
#include "libslic3r/GCode/GCodeProcessor.hpp"
#include "libslic3r/Model.hpp"
@@ -760,6 +764,8 @@ bool SendMultiMachinePage::get_ams_mapping_result(std::string &mapping_array_str
return true;
}
// Mapping helpers live in FilamentMappingUtils.hpp, shared with SelectMachine.
void SendMultiMachinePage::on_send(wxCommandEvent& event)
{
event.Skip();
@@ -808,6 +814,30 @@ void SendMultiMachinePage::on_send(wxCommandEvent& event)
if (!wxGetApp().is_blocking_printing(obj)) {
PrintParams params = request_params(obj);
switch (prepare_filament_mapping_for_send(obj, params.ams_mapping2, m_ams_mapping_result)) {
case MappingSendError::unsupported:
{
BOOST_LOG_TRIVIAL(warning) << "SendMultiMachinePage: connector does not advertise filament_mapping; refusing mapped print for "
<< obj->get_dev_id();
MessageDialog unavailable_msg(nullptr, _L("AMS filament mapping is not available for this printer. Clear the AMS mapping before printing."), "", wxICON_WARNING | wxOK);
unavailable_msg.ShowModal();
return;
}
case MappingSendError::incomplete:
{
BOOST_LOG_TRIVIAL(warning) << "SendMultiMachinePage: a used filament has no target; refusing print for " << obj->get_dev_id();
MessageDialog incomplete_msg(nullptr, _L("A filament used by this print has no AMS mapping. Assign it before printing."), "", wxICON_WARNING | wxOK);
incomplete_msg.ShowModal();
return;
}
case MappingSendError::none:
break;
default:
// A refusal added without a handler here must not silently send.
BOOST_LOG_TRIVIAL(warning) << "SendMultiMachinePage: unrecognized mapping refusal for "
<< obj->get_dev_id();
return;
}
print_params.push_back(params);
}
}
+6 -4
View File
@@ -4695,12 +4695,14 @@ void StatusPanel::on_filament_edit(wxCommandEvent &event)
}
m_filament_setting_dlg->m_is_third = !DevFilaSystem::IsBBL_Filament(tray->tag_uid);
// The tray's own temps pre-fill the dialog for every tray type; a BBL tray
// additionally carries the SN and brand.
temp_max = tray->nozzle_temp_max;
temp_min = tray->nozzle_temp_min;
if (!m_filament_setting_dlg->m_is_third)
{
sn_number = tray->uuid;
filament = tray->sub_brands;
temp_max = tray->nozzle_temp_max;
temp_min = tray->nozzle_temp_min;
}
}
@@ -4761,11 +4763,11 @@ void StatusPanel::on_ext_spool_edit(wxCommandEvent &event)
}
m_filament_setting_dlg->m_is_third = !DevFilaSystem::IsBBL_Filament(obj->vt_slot[nozzle_index].tag_uid);
temp_max = obj->vt_slot[nozzle_index].nozzle_temp_max;
temp_min = obj->vt_slot[nozzle_index].nozzle_temp_min;
if (!m_filament_setting_dlg->m_is_third) {
sn_number = obj->vt_slot[nozzle_index].uuid;
filament = obj->vt_slot[nozzle_index].sub_brands;
temp_max = obj->vt_slot[nozzle_index].nozzle_temp_max;
temp_min = obj->vt_slot[nozzle_index].nozzle_temp_min;
}
m_filament_setting_dlg->Move(wxPoint(current_position_x,current_position_y));
+112
View File
@@ -0,0 +1,112 @@
#include "StoragePanel.h"
#include "DeviceManager.hpp"
#include "GUI_App.hpp"
#include "MediaFilePanel.h"
#include "OrcaFilesPanel.h"
#include "Widgets/StateColor.hpp"
#include <wx/colour.h>
#include <wx/panel.h>
#include <wx/event.h>
#include <wx/sizer.h>
namespace Slic3r {
namespace GUI {
StoragePanel::StoragePanel(wxWindow* parent)
: wxPanel(parent, wxID_ANY)
{
const wxColour light_background("#EEEEEE");
SetBackgroundColour(wxGetApp().dark_mode() ? StateColor::darkModeColorFor(light_background) : light_background);
SetSizer(new wxBoxSizer(wxVERTICAL));
}
MediaFilePanel* StoragePanel::ensureBambuPanel()
{
if (!m_bambu_panel) {
m_bambu_panel = new MediaFilePanel(this);
m_bambu_panel->Hide();
GetSizer()->Add(m_bambu_panel, 1, wxEXPAND);
wxGetApp().UpdateDarkUIWin(m_bambu_panel);
}
return m_bambu_panel;
}
OrcaFilesPanel* StoragePanel::ensureOrcaPanel()
{
if (!m_orca_panel) {
m_orca_panel = new OrcaFilesPanel(this);
m_orca_panel->Hide();
GetSizer()->Add(m_orca_panel, 1, wxEXPAND);
wxGetApp().UpdateDarkUIWin(m_orca_panel);
}
return m_orca_panel;
}
void StoragePanel::showChild(Child child)
{
if (m_bambu_panel)
m_bambu_panel->Show(child == Child::Bambu);
if (m_orca_panel)
m_orca_panel->Show(child == Child::Orca);
m_active_child = child;
Layout();
}
void StoragePanel::UpdateByObj(MachineObject* obj)
{
// Agent capability controls the page; Bambu remains the fallback for other printers.
Child target = m_active_child;
if (obj)
target = obj->printer_supports_feature("printer_files") ? Child::Orca : Child::Bambu;
else if (target == Child::None)
target = Child::Bambu;
if (target != m_active_child) {
if (m_active_child == Child::Bambu && m_bambu_panel)
m_bambu_panel->UpdateByObj(nullptr);
else if (m_active_child == Child::Orca && m_orca_panel)
m_orca_panel->UpdateByObj(nullptr);
if (target == Child::Bambu)
ensureBambuPanel();
else
ensureOrcaPanel();
showChild(target);
}
if (target == Child::Bambu)
ensureBambuPanel()->UpdateByObj(obj);
else if (target == Child::Orca)
ensureOrcaPanel()->UpdateByObj(obj);
}
void StoragePanel::SwitchStorage(bool external)
{
if (m_bambu_panel)
m_bambu_panel->SwitchStorage(external);
}
void StoragePanel::Rescale()
{
if (m_bambu_panel)
m_bambu_panel->Rescale();
if (m_orca_panel)
m_orca_panel->Rescale();
}
void StoragePanel::on_sys_color_changed()
{
const wxColour light_background("#EEEEEE");
SetBackgroundColour(wxGetApp().dark_mode() ? StateColor::darkModeColorFor(light_background) : light_background);
if (m_bambu_panel)
wxGetApp().UpdateDarkUIWin(m_bambu_panel);
if (m_orca_panel)
wxGetApp().UpdateDarkUIWin(m_orca_panel);
if (m_orca_panel)
m_orca_panel->on_sys_color_changed();
Refresh();
}
}}
+35
View File
@@ -0,0 +1,35 @@
#pragma once
#include <wx/panel.h>
namespace Slic3r {
class MachineObject;
namespace GUI {
class MediaFilePanel;
class OrcaFilesPanel;
class StoragePanel : public wxPanel
{
public:
explicit StoragePanel(wxWindow* parent);
void UpdateByObj(MachineObject* obj);
void SwitchStorage(bool external);
void Rescale();
void on_sys_color_changed();
private:
enum class Child { None, Bambu, Orca };
MediaFilePanel* ensureBambuPanel();
OrcaFilesPanel* ensureOrcaPanel();
void showChild(Child child);
MediaFilePanel* m_bambu_panel = nullptr;
OrcaFilesPanel* m_orca_panel = nullptr;
Child m_active_child = Child::None;
};
}}
+6
View File
@@ -1425,6 +1425,12 @@ bool SyncAmsInfoDialog::get_ams_mapping_result(std::string &mapping_array_str, s
BOOST_LOG_TRIVIAL(error) << "get_ams_mapping_result, plater is nullptr";
}
// mapping_v1_json is built one entry per filament preset, in preset order, so the
// array position is the logical filament index the generated G-code toolchange
// references (the identifier handed to the Klipper toolchange macro; see
// OrcaPrinterAgent::build_filament_mapping and the index-correlation test in
// tests/slic3rutils/test_orca_printer_agent.cpp). Never re-densify after dropping
// sentinel entries, or a used filament would be aimed at the wrong lane.
for (int i = 0; i < wxGetApp().preset_bundle->filament_presets.size(); i++) {
int tray_id = -1;
json mapping_item_v1;
+18 -12
View File
@@ -101,6 +101,7 @@ bool AMSinfo::parse_ams_info(MachineObject *obj, DevAms *ams, bool remain_flag,
Caninfo info;
// tray is exists
if (it != ams->GetTrays().end() && it->second->is_exists) {
info.is_empty = it->second->is_empty;
if (it->second->is_tray_info_ready()) {
info.can_id = it->second->id;
info.ctype = it->second->ctype;
@@ -168,6 +169,7 @@ void AMSinfo::parse_ext_info(MachineObject* obj, DevAmsTray tray) {
this->ams_type = AMSModel::EXT_AMS;
Caninfo info;
info.can_id = std::to_string(0);
info.is_empty = tray.is_empty;
this->cans.clear();
if (tray.id == std::to_string(VIRTUAL_TRAY_MAIN_ID))
@@ -1130,9 +1132,9 @@ void AMSLib::render_lite_text(wxDC& dc)
dc.SetTextForeground(temp_text_colour);
auto libsize = GetSize();
if (m_info.material_state == AMSCanType::AMS_CAN_TYPE_THIRDBRAND
if (!m_info.is_empty && (m_info.material_state == AMSCanType::AMS_CAN_TYPE_THIRDBRAND
|| m_info.material_state == AMSCanType::AMS_CAN_TYPE_BRAND
|| m_info.material_state == AMSCanType::AMS_CAN_TYPE_VIRTUAL) {
|| m_info.material_state == AMSCanType::AMS_CAN_TYPE_VIRTUAL)) {
if (m_info.material_name.empty()) {
auto tsize = dc.GetMultiLineTextExtent("?");
@@ -1180,8 +1182,8 @@ void AMSLib::render_lite_text(wxDC& dc)
}
}
if (m_info.material_state == AMSCanType::AMS_CAN_TYPE_EMPTY) {
auto tsize = dc.GetMultiLineTextExtent("/");
if (m_info.material_state == AMSCanType::AMS_CAN_TYPE_EMPTY || m_info.is_empty) {
auto tsize = dc.GetMultiLineTextExtent(_L("/"));
auto pot = wxPoint((libsize.x - tsize.x) / 2 + FromDIP(2), (libsize.y - tsize.y) / 2 + FromDIP(3));
dc.DrawText("/", pot);
}
@@ -1189,8 +1191,12 @@ void AMSLib::render_lite_text(wxDC& dc)
void AMSLib::render_generic_text(wxDC &dc)
{
bool show_k_value = true;
if (m_info.material_name.empty()) {
// K/N is Bambu firmware's flow-dynamics calibration; agents with no printer-side
// records (OrcaSonar, Moonraker) must not show a synthesized value.
const bool k_supported = !m_obj || m_obj->supports_extrusion_cali();
const bool show_kn = m_show_kn && k_supported;
bool show_k_value = k_supported;
if (!k_supported || m_info.material_name.empty()) {
show_k_value = false;
}
else if (m_info.cali_idx == -1 || (m_obj && (CalibUtils::get_selected_calib_idx(m_obj->pa_calib_tab, m_info.cali_idx) == -1))) {
@@ -1231,9 +1237,9 @@ void AMSLib::render_generic_text(wxDC &dc)
}
auto libsize = GetSize();
if (m_info.material_state == AMSCanType::AMS_CAN_TYPE_THIRDBRAND
if (!m_info.is_empty && (m_info.material_state == AMSCanType::AMS_CAN_TYPE_THIRDBRAND
|| m_info.material_state == AMSCanType::AMS_CAN_TYPE_BRAND
|| m_info.material_state == AMSCanType::AMS_CAN_TYPE_VIRTUAL) {
|| m_info.material_state == AMSCanType::AMS_CAN_TYPE_VIRTUAL)) {
if (m_info.material_name.empty() /*&& m_info.material_state != AMSCanType::AMS_CAN_TYPE_VIRTUAL*/) {
auto tsize = dc.GetMultiLineTextExtent("?");
@@ -1271,7 +1277,7 @@ void AMSLib::render_generic_text(wxDC &dc)
auto line_top_tsize = dc.GetMultiLineTextExtent(line_top);
auto line_bottom_tsize = dc.GetMultiLineTextExtent(line_bottom);
if (!m_show_kn) {
if (!show_kn) {
auto pot_top = wxPoint((libsize.x - line_top_tsize.x) / 2, (libsize.y - line_top_tsize.y) / 2 - line_top_tsize.y + FromDIP(6));
dc.DrawText(line_top, pot_top);
@@ -1302,7 +1308,7 @@ void AMSLib::render_generic_text(wxDC &dc)
//draw k&n
if (m_obj && show_k_value) {
if (m_show_kn) {
if (show_kn) {
wxString str_k = wxString::Format("K %1.3f", m_info.k);
wxString str_n = wxString::Format("N %1.3f", m_info.n);
dc.SetFont(::Label::Body_11);
@@ -1313,7 +1319,7 @@ void AMSLib::render_generic_text(wxDC &dc)
}
}
if (m_info.material_state == AMSCanType::AMS_CAN_TYPE_EMPTY) {
if (m_info.material_state == AMSCanType::AMS_CAN_TYPE_EMPTY || m_info.is_empty) {
auto tsize = dc.GetMultiLineTextExtent(_L("Empty"));
auto pot = wxPoint((libsize.x - tsize.x) / 2, (libsize.y - tsize.y) / 2 + FromDIP(3));
dc.DrawText(_L("Empty"), pot);
@@ -2810,7 +2816,7 @@ void AMSPreview::doRender(wxDC &dc)
}
else {
wxRect rect(left, (size.y - AMS_ITEM_CUBE_SIZE.y) / 2, AMS_ITEM_CUBE_SIZE.x, AMS_ITEM_CUBE_SIZE.y);
if (iter->material_state == AMSCanType::AMS_CAN_TYPE_EMPTY) {
if (iter->material_state == AMSCanType::AMS_CAN_TYPE_EMPTY || iter->is_empty) {
dc.SetPen(wxPen(wxColor(0, 0, 0)));
dc.DrawLine(rect.GetRight() - FromDIP(1), rect.GetTop() + FromDIP(1), rect.GetLeft() + FromDIP(1), rect.GetBottom() - FromDIP(1));
}
+2
View File
@@ -200,6 +200,7 @@ struct Caninfo
wxString material_name;
wxColour material_colour = {*wxWHITE};
AMSCanType material_state;
bool is_empty = false;
int ctype=0;
int material_remain = 100;
int cali_idx = -1;
@@ -215,6 +216,7 @@ public:
material_name == other.material_name &&
material_colour == other.material_colour &&
material_state == other.material_state &&
is_empty == other.is_empty &&
ctype == other.ctype &&
material_remain == other.material_remain &&
cali_idx == other.cali_idx &&
+29 -9
View File
@@ -82,6 +82,11 @@ ComboBox::ComboBox(wxWindow *parent,
e.SetId(GetId());
GetEventHandler()->ProcessEvent(e);
});
drop.Bind(EVT_DROPDOWN_ITEM_ACTION, [this](wxCommandEvent &e) {
e.SetEventObject(this);
e.SetId(GetId());
GetEventHandler()->ProcessEvent(e);
});
drop.Bind(EVT_DISMISS, [this](auto &) {
drop_down = false;
wxCommandEvent e(wxEVT_COMBOBOX_CLOSEUP);
@@ -251,7 +256,7 @@ int ComboBox::Append(const wxString &text,
int style)
{
auto valid_bit_map = bitmap.IsOk() ? bitmap : wxNullBitmap;
Item item{text, wxEmptyString, valid_bit_map, valid_bit_map, clientData, group_key, group_label};
Item item{text, wxEmptyString, valid_bit_map, valid_bit_map, wxNullBitmap, clientData, group_key, group_label};
item.style = style;
items.push_back(item);
SetClientDataType(wxClientData_Void);
@@ -297,6 +302,12 @@ void ComboBox::ForceDropdownOpen()
}
}
void ComboBox::CloseDropdown()
{
if (drop.IsShown())
drop.DismissAndNotify();
}
unsigned int ComboBox::GetCount() const { return items.size(); }
void ComboBox::set_replace_text(wxString text, wxString image_name)
@@ -350,6 +361,13 @@ void ComboBox::SetItemBitmap(unsigned int n, wxBitmap const &bitmap)
drop.Invalidate();
}
void ComboBox::SetItemActionBitmap(unsigned int n, wxBitmap const &bitmap)
{
if (n >= items.size()) return;
items[n].action_icon = bitmap.IsOk() ? bitmap : wxNullBitmap;
drop.Invalidate();
}
int ComboBox::DoInsertItems(const wxArrayStringsAdapter &items,
unsigned int pos,
void ** clientData,
@@ -357,7 +375,7 @@ int ComboBox::DoInsertItems(const wxArrayStringsAdapter &items,
{
if (pos > this->items.size()) return -1;
for (int i = 0; i < items.GetCount(); ++i) {
Item item { items[i], wxEmptyString, wxNullBitmap, wxNullBitmap, clientData ? clientData[i] : NULL };
Item item { items[i], wxEmptyString, wxNullBitmap, wxNullBitmap, wxNullBitmap, clientData ? clientData[i] : NULL };
this->items.insert(this->items.begin() + pos, item);
++pos;
}
@@ -419,16 +437,18 @@ void ComboBox::keyDown(wxKeyEvent& event)
case WXK_UP:
case WXK_DOWN:
case WXK_LEFT:
case WXK_RIGHT:
if ((event.GetKeyCode() == WXK_UP || event.GetKeyCode() == WXK_LEFT) && GetSelection() > 0) {
SetSelection(GetSelection() - 1);
} else if ((event.GetKeyCode() == WXK_DOWN || event.GetKeyCode() == WXK_RIGHT) && GetSelection() + 1 < items.size()) {
SetSelection(GetSelection() + 1);
} else {
case WXK_RIGHT: {
// Step over disabled rows: the mouse cannot select them either.
const int step = (event.GetKeyCode() == WXK_UP || event.GetKeyCode() == WXK_LEFT) ? -1 : 1;
int n = GetSelection() + step;
while (n >= 0 && n < int(items.size()) && (items[n].style & DD_ITEM_STYLE_DISABLED))
n += step;
if (n < 0 || n >= int(items.size()))
break;
}
SetSelection(n);
sendComboBoxEvent();
break;
}
case WXK_TAB:
HandleAsNavigationKey(event);
break;
+2
View File
@@ -96,10 +96,12 @@ public:
wxBitmap GetItemBitmap(unsigned int n);
void SetItemBitmap(unsigned int n, wxBitmap const &bitmap);
void SetItemActionBitmap(unsigned int n, wxBitmap const &bitmap);
bool is_drop_down(){return drop_down;}
void DeleteOneItem(unsigned int pos) { DoDeleteOneItem(pos); }
void ForceDropdownOpen();
void CloseDropdown();
protected:
virtual int DoInsertItems(const wxArrayStringsAdapter &items,
+64 -4
View File
@@ -30,6 +30,7 @@
#include "slic3r/GUI/wxExtensions.hpp"
wxDEFINE_EVENT(EVT_DISMISS, wxCommandEvent);
wxDEFINE_EVENT(EVT_DROPDOWN_ITEM_ACTION, wxCommandEvent);
BEGIN_EVENT_TABLE(DropDown, PopupWindow)
@@ -242,6 +243,19 @@ static wxSize GetBmpSize(wxBitmap & bmp)
#endif
}
// The x-range [first, second) of the column at the right of the rows that holds the items' action
// icons, empty when no item has one. render() draws the icons in it and mouseReleased() hit-tests
// clicks against it, so the drawn icon and its clickable area always match.
std::pair<int, int> DropDown::actionColumn() const
{
if (actionIconWidth <= 0)
return {0, 0};
int right = rowSize.x;
if (rowSize.y * count > GetSize().y) // position bar
right -= 6;
return {right - actionIconWidth - FromDIP(16), right};
}
static void _DrawSplitItem(const wxWindow* w, wxDC& dc, wxString split_text, wxPoint start_pt, int item_width, int item_height)
{
// save dc
@@ -371,6 +385,10 @@ void DropDown::render(wxDC &dc)
rcContent.width -= szBmp.x + 5;
}
const auto action_column = actionColumn();
if (action_column.second > action_column.first)
rcContent.width = action_column.first - rcContent.x;
std::set<wxString> groups;
// draw texts & icons
int index = 0;
@@ -434,13 +452,24 @@ void DropDown::render(wxDC &dc)
auto text = group.IsEmpty()
? (item.group_key.IsEmpty() ? item.text : item.group_label)
: item.text;
if (item.action_icon.IsOk()) {
auto action_size = GetBmpSize(item.action_icon);
wxPoint action_pt(action_column.second - action_size.x - FromDIP(8),
rcContent.y + (rcContent.height - action_size.y) / 2);
dc.DrawBitmap(item.action_icon, action_pt);
}
if (!text_off && !text.IsEmpty()) {
wxSize tSize = dc.GetMultiLineTextExtent(text);
if (pt.x + tSize.x > rcContent.GetRight()) {
if (is_hover && item.tip.IsEmpty())
SetToolTip(text);
text = wxControl::Ellipsize(text, dc, wxELLIPSIZE_END,
rcContent.GetRight() - pt.x);
int max_text_width = rcContent.GetRight() - pt.x;
if (max_text_width <= 0) {
text.clear();
} else {
if (is_hover && item.tip.IsEmpty())
SetToolTip(text);
text = wxControl::Ellipsize(text, dc, wxELLIPSIZE_END, max_text_width);
}
}
pt.y += (rcContent.height - textSize.y) / 2;
dc.SetFont(GetFont());
@@ -529,6 +558,7 @@ void DropDown::messureSize()
if (!need_sync) return;
textSize = wxSize();
iconSize = wxSize();
actionIconWidth = 0;
count = 0;
wxClientDC dc(GetParent() ? GetParent() : this);
dc.SetFont(GetFont());
@@ -565,8 +595,13 @@ void DropDown::messureSize()
size1.x += size2.x + (text_off ? 0 : 5);
}
}
if (item.action_icon.IsOk())
actionIconWidth = std::max(actionIconWidth, GetBmpSize(item.action_icon).x);
if (size1.x > textSize.x) textSize = size1;
}
// Every row leaves room for the action column, as render() and actionColumn() lay it out.
if (actionIconWidth > 0)
textSize.x += actionIconWidth + FromDIP(16);
if (!align_icon) iconSize.x = 0;
wxSize szContent = textSize;
if (szContent.x < FromDIP(120) && !use_content_width)
@@ -636,6 +671,11 @@ void DropDown::messureSize()
e.SetId(GetId());
GetEventHandler()->ProcessEvent(e);
});
subDropDown->Bind(EVT_DROPDOWN_ITEM_ACTION, [this](wxCommandEvent &e) {
e.SetEventObject(this);
e.SetId(GetId());
GetEventHandler()->ProcessEvent(e);
});
}
need_sync = false;
}
@@ -714,6 +754,15 @@ void DropDown::mouseReleased(wxMouseEvent& event)
if (hover_item < 0)
return;
if (hover_item >= 0 && (subDropDown == nullptr || subDropDown->group.empty())) { // not moved
int index = hoverIndex();
if (index >= 0 && items[index].action_icon.IsOk()) {
const auto action_column = actionColumn();
if (event.GetX() >= action_column.first && event.GetX() < action_column.second) {
sendDropDownActionEvent(index);
paintNow();
return;
}
}
sendDropDownEvent();
if (mainDropDown)
mainDropDown->hover_item = -1; // To Dismiss mainDropDown
@@ -830,6 +879,17 @@ void DropDown::sendDropDownEvent()
GetEventHandler()->ProcessEvent(event);
}
void DropDown::sendDropDownActionEvent(int index)
{
if (index < 0 || index >= int(items.size()) || (items[index].style & DD_ITEM_STYLE_DISABLED))
return;
wxCommandEvent event(EVT_DROPDOWN_ITEM_ACTION, GetId());
event.SetEventObject(this);
event.SetInt(index);
event.SetString(items[index].text);
GetEventHandler()->ProcessEvent(event);
}
void DropDown::Dismiss()
{
if (subDropDown && subDropDown->IsShown())
+6
View File
@@ -5,6 +5,7 @@
#include <wx/event.h>
#include <vector>
#include <cstddef>
#include <utility>
#include <wx/gdicmn.h>
#include "slic3r/GUI/Widgets/StateColor.hpp"
#include <boost/date_time/posix_time/ptime.hpp>
@@ -27,6 +28,7 @@ class wxWindow;
#define DD_ITEM_STYLE_DIMMED 0x0004 // gray text, but still selectable
wxDECLARE_EVENT(EVT_DISMISS, wxCommandEvent);
wxDECLARE_EVENT(EVT_DROPDOWN_ITEM_ACTION, wxCommandEvent);
class DropDown : public PopupWindow
{
@@ -37,6 +39,7 @@ public:
wxString text_static_tips;// display static tips for TextInput.eg.PrinterInfoBox
wxBitmap icon;
wxBitmap icon_textctrl;// display icon for TextInput.eg.PrinterInfoBox
wxBitmap action_icon;
void * data{nullptr};
wxString group_key{};
wxString group_label{};
@@ -65,6 +68,7 @@ private:
wxSize textSize;
wxSize iconSize;
int actionIconWidth = 0;
wxSize rowSize;
StateHandler state_handler;
@@ -135,6 +139,7 @@ private:
int hoverIndex();
int selectedItem();
std::pair<int, int> actionColumn() const;
friend class ComboBox;
void messureSize();
@@ -148,6 +153,7 @@ private:
void mouseWheelMoved(wxMouseEvent &event);
void sendDropDownEvent();
void sendDropDownActionEvent(int index);
DECLARE_EVENT_TABLE()
+60 -1
View File
@@ -31,8 +31,23 @@
#include <curl/curl.h>
#include <utility>
#ifdef OPENSSL_CERT_OVERRIDE
#include <openssl/err.h>
#include <openssl/ssl.h>
#include <openssl/x509.h>
#include <openssl/x509err.h>
#ifdef __APPLE__
# include <unistd.h>
#endif
#ifdef _WIN32
# ifndef NOMINMAX
# define NOMINMAX
# endif
# include <windows.h>
# include <wincrypt.h>
// wincrypt.h uses this token for a certificate-name property identifier.
# undef X509_NAME
#endif
namespace fs = boost::filesystem;
@@ -955,6 +970,50 @@ std::string Http::tls_system_cert_store()
return {};
}
void Http::add_platform_root_certificates(SSL_CTX* ssl_context)
{
#ifdef _WIN32
X509_STORE* openssl_store = SSL_CTX_get_cert_store(ssl_context);
if (!openssl_store)
throw std::runtime_error("unable to get OpenSSL certificate store");
const auto load_store = [&](DWORD location) {
HCERTSTORE windows_store = CertOpenStore(CERT_STORE_PROV_SYSTEM_W, 0, 0,
location | CERT_STORE_OPEN_EXISTING_FLAG | CERT_STORE_READONLY_FLAG,
L"ROOT");
if (!windows_store)
return;
PCCERT_CONTEXT windows_certificate = nullptr;
while ((windows_certificate = CertEnumCertificatesInStore(windows_store, windows_certificate)) != nullptr) {
const unsigned char* encoded = windows_certificate->pbCertEncoded;
X509* certificate = d2i_X509(nullptr, &encoded, static_cast<long>(windows_certificate->cbCertEncoded));
if (!certificate) {
ERR_clear_error();
continue;
}
ERR_clear_error();
if (X509_STORE_add_cert(openssl_store, certificate) != 1)
ERR_clear_error();
X509_free(certificate);
}
CertCloseStore(windows_store, 0);
};
load_store(CERT_SYSTEM_STORE_CURRENT_USER);
load_store(CERT_SYSTEM_STORE_LOCAL_MACHINE);
#elif defined(__APPLE__)
// macOS ships its system trust roots as a PEM bundle; the bundled OpenSSL's
// compiled-in default path is the build directory, so load this explicitly.
if (ssl_context && ::access("/etc/ssl/cert.pem", R_OK) == 0)
SSL_CTX_load_verify_locations(ssl_context, "/etc/ssl/cert.pem", nullptr);
#else
(void)ssl_context;
#endif
}
std::string Http::url_encode(const std::string &str)
{
::CURL *curl = ::curl_easy_init();
+6
View File
@@ -13,6 +13,8 @@
#include "libslic3r/Exception.hpp"
#include "libslic3r_version.h"
typedef struct ssl_ctx_st SSL_CTX;
#define MAX_SIZE_TO_FILE 3*1024
namespace Slic3r {
@@ -200,6 +202,10 @@ public:
// Return empty string on success or error message on fail.
static std::string tls_global_init();
static std::string tls_system_cert_store();
// Add platform root certificates to a standalone OpenSSL context. This
// supplements set_default_verify_paths() on platforms where OpenSSL does
// not use the native certificate store.
static void add_platform_root_certificates(SSL_CTX* ssl_context);
// converts the given string to an url_encoded_string
static std::string url_encode(const std::string &str);
+81
View File
@@ -13,6 +13,7 @@
#include <memory>
#include <vector>
#include <functional>
#include <cstdint>
#include <cmath>
#include <nlohmann/json.hpp>
#include <boost/format.hpp>
@@ -68,6 +69,34 @@ enum class CameraStreamMode {
http_snapshot // HTTP endpoint returning one image per request
};
struct PrinterFileEntry {
std::string path; // gcodes-root-relative, e.g. "sub/foo.gcode"
std::string name; // basename for display
std::uint64_t size = 0; // bytes
std::int64_t modified = 0; // unix seconds
};
// Moonraker per-file metadata: print time and filament usage. Missing fields are 0.
struct PrinterFileMetadata {
int estimated_time = 0; // seconds
double filament_total = 0; // millimetres
double filament_weight = 0; // grams
};
// Async listing of the printer's G-code files. The return value reports whether
// the request was dispatched; `callback` is invoked once with the result.
using PrinterFileListFn = std::function<void(int result, std::vector<PrinterFileEntry> files)>;
// Async fetch of one G-code file's embedded thumbnail. `image` is the raw image
// bytes (empty when the file has no thumbnail); result reports dispatch/transfer.
using PrinterFileThumbnailFn = std::function<void(int result, std::string image)>;
// Async deletion of one printer G-code file. `result` reports dispatch and outcome.
using PrinterFileDeleteFn = std::function<void(int result)>;
// Async fetch of one G-code file's Moonraker metadata. `meta` fields default to 0.
using PrinterFileMetadataFn = std::function<void(int result, PrinterFileMetadata meta)>;
/**
* IPrinterAgent - Interface for printer operations.
*
@@ -109,6 +138,16 @@ public:
*/
virtual int send_message(std::string dev_id, std::string json_str, int qos, int flag) = 0;
// Capability queries are per-device because one agent may own many printers.
// Legacy agents keep their existing behavior unless they override these.
virtual bool supports_command(const std::string& /*dev_id*/, const std::string& /*command*/) const { return true; }
virtual bool supports_feature(const std::string& /*dev_id*/, const std::string& /*feature*/) const { return false; }
// Whether this agent serializes AMS lane selection into print.gcode_file's
// per-print filament_mapping field. A dialect question, not a capability one:
// the no-AMS external-spool normalization runs before capabilities are known.
virtual bool uses_filament_mapping() const { return false; }
// why: gcode is firmware dialect, not a waist concept - commands whose body is Bambu-dialect
// gcode live on the agent that speaks it; the default is an honest refusal that MachineObject's
// publish funnel turns into a dialog.
@@ -316,6 +355,48 @@ public:
return -1;
}
/**
* List the printer's G-code files, delivered asynchronously via callback.
* Agents without a file listing report ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED.
*/
virtual int list_printer_files(const std::string& dev_id, PrinterFileListFn callback)
{
(void) dev_id;
(void) callback;
return ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED;
}
/**
* Fetch one G-code file's embedded thumbnail, delivered asynchronously via
* callback. Agents without thumbnail support report ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED.
*/
virtual int get_printer_file_thumbnail(const std::string& dev_id, const std::string& path, PrinterFileThumbnailFn callback)
{
(void) dev_id; (void) path; (void) callback;
return ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED;
}
/**
* Delete one G-code file from the printer, delivered asynchronously via
* callback. Agents without file deletion report ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED.
*/
virtual int delete_printer_file(const std::string& dev_id, const std::string& path, PrinterFileDeleteFn callback)
{
(void) dev_id; (void) path; (void) callback;
return ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED;
}
/**
* Fetch one G-code file's Moonraker metadata (print time and filament usage),
* delivered asynchronously via callback. Agents without metadata support
* report ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED.
*/
virtual int get_printer_file_metadata(const std::string& dev_id, const std::string& path, PrinterFileMetadataFn callback)
{
(void) dev_id; (void) path; (void) callback;
return ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED;
}
/**
* Register callback for fatal HTTP errors.
*/
+58
View File
@@ -806,6 +806,36 @@ int NetworkAgent::send_message(std::string dev_id, std::string json_str, int qos
return -1;
}
bool NetworkAgent::owns_agent(const std::string& expected_agent_id) const
{
if (!m_printer_agent)
return false;
return expected_agent_id.empty() || m_printer_agent->get_agent_info().id == expected_agent_id;
}
bool NetworkAgent::supports_command(const std::string& expected_agent_id, const std::string& dev_id,
const std::string& command) const
{
if (!owns_agent(expected_agent_id))
return false;
return m_printer_agent->supports_command(dev_id, command);
}
bool NetworkAgent::supports_feature(const std::string& expected_agent_id, const std::string& dev_id,
const std::string& feature) const
{
if (!owns_agent(expected_agent_id))
return false;
return m_printer_agent->supports_feature(dev_id, feature);
}
bool NetworkAgent::uses_filament_mapping(const std::string& expected_agent_id) const
{
if (!owns_agent(expected_agent_id))
return false;
return m_printer_agent->uses_filament_mapping();
}
int NetworkAgent::command_ams_refresh_rfid(std::string dev_id, int ams_id, int slot_id, int sequence_id, bool lan_mode)
{
if (m_printer_agent)
@@ -1106,4 +1136,32 @@ int NetworkAgent::get_hms_snapshot(std::string dev_id, std::string file_name, st
return -1;
}
int NetworkAgent::list_printer_files(const std::string& dev_id, PrinterFileListFn callback)
{
if (m_printer_agent)
return m_printer_agent->list_printer_files(dev_id, callback);
return ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED;
}
int NetworkAgent::get_printer_file_thumbnail(const std::string& dev_id, const std::string& path, PrinterFileThumbnailFn callback)
{
if (m_printer_agent)
return m_printer_agent->get_printer_file_thumbnail(dev_id, path, callback);
return ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED;
}
int NetworkAgent::delete_printer_file(const std::string& dev_id, const std::string& path, PrinterFileDeleteFn callback)
{
if (m_printer_agent)
return m_printer_agent->delete_printer_file(dev_id, path, callback);
return ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED;
}
int NetworkAgent::get_printer_file_metadata(const std::string& dev_id, const std::string& path, PrinterFileMetadataFn callback)
{
if (m_printer_agent)
return m_printer_agent->get_printer_file_metadata(dev_id, path, callback);
return ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED;
}
} // namespace Slic3r
+8
View File
@@ -153,6 +153,10 @@ public:
int set_on_local_message_fn(OnMessageFn fn);
int set_server_callback(OnServerErrFn fn);
int send_message(std::string dev_id, std::string json_str, int qos, int flag);
bool owns_agent(const std::string& expected_agent_id) const;
bool supports_command(const std::string& expected_agent_id, const std::string& dev_id, const std::string& command) const;
bool supports_feature(const std::string& expected_agent_id, const std::string& dev_id, const std::string& feature) const;
bool uses_filament_mapping(const std::string& expected_agent_id) const;
int command_ams_refresh_rfid(std::string dev_id, int ams_id, int slot_id, int sequence_id, bool lan_mode);
int command_ams_calibrate(std::string dev_id, int ams_id, int sequence_id, bool lan_mode);
int command_ams_select_tray(std::string dev_id, std::string tray_id, int sequence_id, bool lan_mode);
@@ -194,6 +198,10 @@ public:
std::string from_orca_filament_id(const std::string& orca_filament_id) const;
int request_bind_ticket(std::string* ticket);
int get_hms_snapshot(std::string dev_id, std::string file_name, std::function<void(std::string, int)> callback);
int list_printer_files(const std::string& dev_id, PrinterFileListFn callback);
int get_printer_file_thumbnail(const std::string& dev_id, const std::string& path, PrinterFileThumbnailFn callback);
int delete_printer_file(const std::string& dev_id, const std::string& path, PrinterFileDeleteFn callback);
int get_printer_file_metadata(const std::string& dev_id, const std::string& path, PrinterFileMetadataFn callback);
private:
struct PrinterCallbacks {
+11 -8
View File
@@ -17,7 +17,7 @@
namespace Slic3r {
static constexpr char ORCA_PRINTER_AGENT_ID[] = "orca";
static constexpr char BBL_PRINTER_AGENT_ID[] = "bbl";
static constexpr char BBL_PRINTER_AGENT_ID[] = "bbl";
// Factory function type for creating printer agents
using PrinterAgentFactory =
@@ -26,17 +26,20 @@ using PrinterAgentFactory =
// Information about a registered printer agent
struct PrinterAgentInfo
{
std::string id; // Registry/config key, e.g. "orca" or a plugin AgentInfo::id
std::string display_name; // e.g., "Orca Native", "Bambu Lab"
std::string plugin_identifier; // Empty for built-ins, otherwise <plugin_key>;<uuid>;<capability_name>
PrinterAgentFactory factory; // Function to create the agent
std::string id; // Registry/config key, e.g. "orca" or a plugin AgentInfo::id
std::string display_name; // e.g., "Orca Native", "Bambu Lab"
std::string plugin_identifier; // Empty for built-ins, otherwise <plugin_key>;<uuid>;<capability_name>
PrinterAgentFactory factory; // Function to create the agent
bool is_plugin() const { return !plugin_identifier.empty(); }
PrinterAgentInfo(const std::string& id_, const std::string& display_name_, PrinterAgentFactory factory_)
: id(id_), display_name(display_name_), factory(std::move(factory_))
{}
PrinterAgentInfo(const std::string& id_, const std::string& display_name_, const std::string& plugin_identifier, PrinterAgentFactory factory_)
PrinterAgentInfo(const std::string& id_,
const std::string& display_name_,
const std::string& plugin_identifier,
PrinterAgentFactory factory_)
: id(id_), display_name(display_name_), plugin_identifier(plugin_identifier), factory(std::move(factory_))
{}
};
@@ -118,9 +121,9 @@ public:
* @param log_dir Directory for log files
* @return Shared pointer to IPrinterAgent, or nullptr if ID not found
*/
static std::shared_ptr<IPrinterAgent> create_printer_agent_by_id(const std::string& id,
static std::shared_ptr<IPrinterAgent> create_printer_agent_by_id(const std::string& id,
std::shared_ptr<ICloudServiceAgent> cloud_agent,
const std::string& log_dir);
const std::string& log_dir);
/**
* Clear the printer agent cache.
+297 -11
View File
@@ -1,6 +1,9 @@
#include "OrcaCloudServiceAgent.hpp"
#include "CloudProvider.hpp"
#include "Http.hpp"
#include "ICameraSignalingChannel.hpp"
#include "OrcaCloudSignalingChannel.hpp"
#include "OrcaMqttConnection.hpp"
#include "bambu_networking.hpp"
#include "ICloudServiceAgent.hpp"
#include "libslic3r/Utils.hpp"
@@ -12,8 +15,11 @@
#include <boost/asio/ip/tcp.hpp>
#include <boost/asio/io_context.hpp>
#include <boost/beast/core/detail/base64.hpp>
#include <boost/beast/core.hpp>
#include <boost/beast/websocket.hpp>
#include <boost/filesystem.hpp>
#include <boost/filesystem/operations.hpp>
#include <boost/filesystem/path.hpp>
#include <boost/log/trivial.hpp>
#include <boost/uuid/name_generator_sha1.hpp>
#include <boost/uuid/uuid.hpp>
@@ -37,14 +43,18 @@
#include <openssl/hmac.h>
#include <openssl/rand.h>
#include <openssl/sha.h>
#include <openssl/ssl.h>
#include <algorithm>
#include <cctype>
#include <condition_variable>
#include <cstdint>
#include <cstdlib>
#include <fstream>
#include <iomanip>
#include <optional>
#include <random>
#include <set>
#include <sstream>
#include <string>
@@ -513,6 +523,7 @@ OrcaCloudServiceAgent::OrcaCloudServiceAgent(std::string log_dir)
, api_base_url(ORCA_DEFAULT_API_URL)
, auth_base_url(ORCA_DEFAULT_AUTH_URL)
, cloud_base_url(ORCA_DEFAULT_CLOUD_URL)
, mqtt_connection(std::make_unique<OrcaMqttConnection>())
{
auth_headers["apikey"] = ORCA_DEFAULT_PUB_KEY;
pkce_bundle.loopback_port = choose_loopback_port();
@@ -523,6 +534,8 @@ OrcaCloudServiceAgent::OrcaCloudServiceAgent(std::string log_dir)
OrcaCloudServiceAgent::~OrcaCloudServiceAgent()
{
if (mqtt_connection)
mqtt_connection->stop();
stop_health_check();
if (refresh_thread.joinable()) {
refresh_thread.join();
@@ -1000,6 +1013,8 @@ bool OrcaCloudServiceAgent::run_health_check(const std::atomic_bool* cancel)
return false;
const bool connected = res.success;
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: cloud health connected=" << connected << " status=" << res.status
<< " response_bytes=" << res.body.size();
{
std::lock_guard<std::recursive_mutex> lock(state_mutex);
is_connected = connected;
@@ -1012,11 +1027,32 @@ bool OrcaCloudServiceAgent::run_health_check(const std::atomic_bool* cancel)
int OrcaCloudServiceAgent::connect_server()
{
const bool logged_in = is_user_login();
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: OrcaCloudServiceAgent::connect_server logged_in=" << logged_in
<< " api_base_url=" << api_base_url;
if (!logged_in) {
if (mqtt_connection)
mqtt_connection->stop();
{
std::lock_guard<std::recursive_mutex> lock(state_mutex);
is_connected = false;
}
BOOST_LOG_TRIVIAL(warning) << "OrcaCloudServiceAgent: connect_server requires a logged-in user";
invoke_server_connected_callback(-1, 401);
return BAMBU_NETWORK_ERR_INVALID_HANDLE;
}
// connect_server() remains a REST health probe. The long-lived fleet MQTT socket
// is started lazily by set_user_selected_machine -> configure_selected_printer_mqtt;
// subscriptions queued before that point are replayed when it starts.
return run_health_check(nullptr) ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_CONNECTION_TO_SERVER_FAILED;
}
bool OrcaCloudServiceAgent::is_server_connected()
{
// The REST health probe is the signal; the per-printer MQTT socket does not gate
// whole-cloud connectivity (one printer reconnecting must not report the whole
// cloud as lost).
std::lock_guard<std::recursive_mutex> lock(state_mutex);
return is_connected;
}
@@ -1074,13 +1110,238 @@ int OrcaCloudServiceAgent::stop_subscribe(std::string module)
int OrcaCloudServiceAgent::add_subscribe(std::vector<std::string> dev_list)
{
(void) dev_list;
return BAMBU_NETWORK_SUCCESS;
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: OrcaCloudServiceAgent::add_subscribe count=" << dev_list.size()
<< " logged_in=" << is_user_login() << " mqtt_connection=" << (mqtt_connection ? "set" : "null");
if (!is_user_login() || !mqtt_connection) {
BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: add_subscribe rejected because cloud is not ready";
return BAMBU_NETWORK_ERR_INVALID_HANDLE;
}
bool queued = true;
for (const std::string& dev_id : dev_list)
queued = mqtt_connection->subscribe(dev_id) && queued;
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: add_subscribe queued=" << queued;
return queued ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_CONNECT_FAILED;
}
int OrcaCloudServiceAgent::del_subscribe(std::vector<std::string> dev_list)
{
(void) dev_list;
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: OrcaCloudServiceAgent::del_subscribe count=" << dev_list.size()
<< " logged_in=" << is_user_login() << " mqtt_connection=" << (mqtt_connection ? "set" : "null");
if (!is_user_login() || !mqtt_connection) {
BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: del_subscribe rejected because cloud is not ready";
return BAMBU_NETWORK_ERR_INVALID_HANDLE;
}
bool queued = true;
for (const std::string& dev_id : dev_list)
queued = mqtt_connection->unsubscribe(dev_id) && queued;
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: del_subscribe queued=" << queued;
return queued ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_CONNECT_FAILED;
}
int OrcaCloudServiceAgent::configure_selected_printer_mqtt(const std::string& dev_id,
OrcaMqttConnection::StateHandler state_handler)
{
(void) dev_id;
if (!ensure_token_fresh("configure_selected_printer_mqtt"))
{
BOOST_LOG_TRIVIAL(warning) << "ensure_token_fresh returned false";
return BAMBU_NETWORK_ERR_CONNECTION_TO_SERVER_FAILED;
}
OrcaMqttConnection::Config cfg;
cfg.url = "wss://" + api_base_url + "/api/v1/printers/mqtt";
cfg.use_tls = true;
cfg.bearer_provider = [this] { return get_access_token(); };
cfg.client_id = "OrcaSlicer";
cfg.keepalive_seconds = 300;
{
std::lock_guard<std::mutex> lock(m_selected_url_mutex);
m_selected_printer_mqtt_url = cfg.url;
}
if (mqtt_connection->is_running()) {
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: fleet MQTT connection already running";
return BAMBU_NETWORK_SUCCESS;
}
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: configuring fleet MQTT endpoint=" << cfg.url;
// NOTE: no lock is held across start() — it blocks for the whole initial connect
// attempt (up to ~10s), and the message handler below re-enters callback_mutex on
// the MQTT worker thread.
const bool ok = mqtt_connection->start(
cfg,
[this](const std::string& id, const std::string& payload) { deliver_cloud_message(id, payload); },
std::move(state_handler));
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: fleet MQTT start returned=" << ok;
return ok ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_CONNECTION_TO_SERVER_FAILED;
}
void OrcaCloudServiceAgent::teardown_selected_printer_mqtt()
{
if (mqtt_connection) {
mqtt_connection->stop();
// The connection object is reused for the next printer; drop this printer's
// report topic so its 1:1 socket does not re-subscribe the previous device.
mqtt_connection->clear_subscriptions();
}
std::lock_guard<std::mutex> lock(m_selected_url_mutex);
m_selected_printer_mqtt_url.clear();
}
std::string OrcaCloudServiceAgent::selected_printer_mqtt_url() const
{
std::lock_guard<std::mutex> lock(m_selected_url_mutex);
return m_selected_printer_mqtt_url;
}
void OrcaCloudServiceAgent::deliver_cloud_message(const std::string& dev_id, const std::string& payload)
{
OnMessageFn callback;
{
std::lock_guard<std::mutex> lock(callback_mutex);
callback = printer_status_callback;
}
if (callback)
callback(dev_id, payload);
}
int OrcaCloudServiceAgent::set_printer_status_callback(OnMessageFn fn)
{
std::lock_guard<std::mutex> lock(callback_mutex);
printer_status_callback = std::move(fn);
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: printer status callback=" << (printer_status_callback ? "set" : "clear");
return BAMBU_NETWORK_SUCCESS;
}
int OrcaCloudServiceAgent::send_printer_command(const std::string& dev_id, const std::string& body)
{
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: send_printer_command dev_id=" << dev_id
<< " body_bytes=" << body.size() << " logged_in=" << is_user_login();
if (dev_id.empty() || !is_user_login()) {
BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: send_printer_command rejected";
return BAMBU_NETWORK_ERR_INVALID_HANDLE;
}
const std::string path = std::string(ORCA_CLOUD_PRINTER) + "/" + dev_id + "/commands";
std::string response;
unsigned int http_code = 0;
int result = http_post(path, body, &response, &http_code);
BOOST_LOG_TRIVIAL(info) << "OrcaCloudServiceAgent: command dev=" << dev_id
<< " http=" << http_code << " result=" << result
<< " response_bytes=" << response.size();
return (result == BAMBU_NETWORK_SUCCESS && http_code >= 200 && http_code < 300)
? BAMBU_NETWORK_SUCCESS
: BAMBU_NETWORK_ERR_CONNECT_FAILED;
}
int OrcaCloudServiceAgent::upload_gcode_via_cloud(const std::string& dev_id,
const std::string& local_gcode_path,
std::string* job_id,
OnUpdateStatusFn update_fn,
WasCancelledFn cancel_fn)
{
if (dev_id.empty() || local_gcode_path.empty() || !is_user_login())
return BAMBU_NETWORK_ERR_INVALID_HANDLE;
if (cancel_fn && cancel_fn())
return BAMBU_NETWORK_ERR_CANCELED;
// Step 1: POST print-jobs/uploads -> a short-lived presigned R2 PUT URL. No
// metadata rides this request; filename/start are only relevant to the HTTP
// .../start finalize route, which this MQTT-driven flow does not call.
const std::string uploads_path = std::string(ORCA_CLOUD_PRINTER) + "/" + Http::url_encode(dev_id) + "/print-jobs/uploads";
std::string response;
unsigned int http_code = 0;
int result = http_post(uploads_path, "{}", &response, &http_code);
if (result != BAMBU_NETWORK_SUCCESS || http_code < 200 || http_code >= 300) {
BOOST_LOG_TRIVIAL(warning) << "OrcaCloudServiceAgent: print-jobs/uploads failed http_code=" << http_code;
return BAMBU_NETWORK_ERR_CONNECT_FAILED;
}
std::string upload_job_id;
std::string upload_url;
try {
const nlohmann::json j = nlohmann::json::parse(response);
upload_job_id = j.value("job_id", "");
upload_url = j.value("upload_url", "");
} catch (const std::exception& e) {
BOOST_LOG_TRIVIAL(error) << "OrcaCloudServiceAgent: failed to parse print-jobs/uploads response: " << e.what();
return BAMBU_NETWORK_ERR_CONNECT_FAILED;
}
if (upload_job_id.empty() || upload_url.empty()) {
BOOST_LOG_TRIVIAL(error) << "OrcaCloudServiceAgent: print-jobs/uploads response missing job_id/upload_url";
return BAMBU_NETWORK_ERR_CONNECT_FAILED;
}
if (cancel_fn && cancel_fn())
return BAMBU_NETWORK_ERR_CANCELED;
// Step 2: PUT the G-code straight to R2 with the one-time URL from step 1. This
// is a scoped, PUT-only, short-TTL capability with no bearer token of its own,
// so it bypasses http_put (which always prefixes api_base_url and attaches the
// cloud session's Authorization header - neither belongs on an R2 PUT).
bool canceled = false;
unsigned put_status = 0;
std::string put_error;
Http::put(upload_url)
.tls_verify(true)
.header("Content-Type", "text/x.gcode")
.set_put_body(boost::filesystem::path(local_gcode_path))
.timeout_connect(5)
.timeout_max(300) // large G-code over a slow link
.on_progress([&](Http::Progress progress, bool& cancel) {
if (cancel_fn && cancel_fn()) {
cancel = true;
canceled = true;
return;
}
if (update_fn && progress.ultotal > 0) {
const int percent = static_cast<int>((progress.ulnow * 100) / progress.ultotal);
update_fn(PrintingStageUpload, percent, "Uploading...");
}
})
.on_complete([&](std::string, unsigned status) { put_status = status; })
.on_error([&](std::string, std::string err, unsigned status) {
put_status = status;
put_error = std::move(err);
})
.perform_sync();
if (canceled)
return BAMBU_NETWORK_ERR_CANCELED;
if (put_status < 200 || put_status >= 300) {
BOOST_LOG_TRIVIAL(warning) << "OrcaCloudServiceAgent: R2 upload failed status=" << put_status << " error=" << put_error;
return BAMBU_NETWORK_ERR_PRINT_SG_UPLOAD_FTP_FAILED;
}
if (job_id)
*job_id = std::move(upload_job_id);
return BAMBU_NETWORK_SUCCESS;
}
int OrcaCloudServiceAgent::start_cloud_print_job(const std::string& dev_id,
const std::string& job_id,
const std::string& filename,
bool start)
{
if (dev_id.empty() || job_id.empty() || !is_user_login())
return BAMBU_NETWORK_ERR_INVALID_HANDLE;
nlohmann::json body;
if (!filename.empty())
body["filename"] = filename;
body["start"] = start;
const std::string path = std::string(ORCA_CLOUD_PRINTER) + "/" + Http::url_encode(dev_id) + "/print-jobs/" +
Http::url_encode(job_id) + "/start";
std::string response;
unsigned int http_code = 0;
const int result = http_post(path, body.dump(), &response, &http_code);
if (result != BAMBU_NETWORK_SUCCESS || http_code < 200 || http_code >= 300) {
BOOST_LOG_TRIVIAL(warning) << "OrcaCloudServiceAgent: print-jobs/" << job_id << "/start failed http_code=" << http_code;
return BAMBU_NETWORK_ERR_CONNECT_FAILED;
}
return BAMBU_NETWORK_SUCCESS;
}
@@ -2139,6 +2400,10 @@ bool OrcaCloudServiceAgent::set_user_session(const json& session_json, bool noti
void OrcaCloudServiceAgent::clear_session(bool all_backends)
{
if (mqtt_connection) {
mqtt_connection->stop();
mqtt_connection->clear_subscriptions();
}
{
std::lock_guard<std::mutex> lock(session_mutex);
session = SessionInfo{};
@@ -2254,7 +2519,11 @@ int OrcaCloudServiceAgent::http_get(const std::string& path, std::string* respon
return (res.success && !suppress) ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_CONNECT_FAILED;
}
int OrcaCloudServiceAgent::http_post(const std::string& path, const std::string& body, std::string* response_body, unsigned int* http_code)
int OrcaCloudServiceAgent::http_post(const std::string& path,
const std::string& body,
std::string* response_body,
unsigned int* http_code,
const std::string& content_type)
{
std::string url = api_base_url + path;
BOOST_LOG_TRIVIAL(trace) << "OrcaCloudServiceAgent: POST " << url;
@@ -2278,7 +2547,7 @@ int OrcaCloudServiceAgent::http_post(const std::string& path, const std::string&
http.header("Authorization", "Bearer " + token);
}
http.header("Content-Type", "application/json");
http.header("Content-Type", content_type);
http.set_post_body(body);
http.on_complete([&](std::string resp_body, unsigned resp_status) {
@@ -2745,19 +3014,28 @@ int OrcaCloudServiceAgent::get_user_print_info(unsigned int* http_code, std::str
if (http_code)
*http_code = code;
if (result != 0 || code != 200)
if (result != 0 || code != 200) {
BOOST_LOG_TRIVIAL(error) << "OrcaCloudServiceAgent: get_user_print_info failed - http_code=" << code << ", response=" << response;
return result != 0 ? result : BAMBU_NETWORK_ERR_GET_SETTING_LIST_FAILED;
}
BOOST_LOG_TRIVIAL(trace) << "OrcaCloudServiceAgent: get_user_print_info fetched - http_code=" << code << ", response=" << response;
try {
auto resp_json = nlohmann::json::parse(response);
nlohmann::json devices = nlohmann::json::array();
for (const auto& printer : resp_json.value("data", nlohmann::json::array())) {
const std::string role = printer.value("access_role", "");
nlohmann::json device;
std::string role = printer.value("access_role", "");
// A printer with the role "view" only has monitoring access for orca cloud.
// The printer is owned by a different person and was shared to the current user without
// any permission to control the printer so we discard this printer. Comment this out if
// OrcaSlicer wants to support view only printers.
if (role.empty() || role == "viewer")
continue;
nlohmann::json device;
device["dev_id"] = printer.value("id", "");
device["dev_name"] = printer.value("name", "");
if (printer.contains("model") && printer["model"].is_string())
@@ -2771,15 +3049,20 @@ int OrcaCloudServiceAgent::get_user_print_info(unsigned int* http_code, std::str
device["task_status"] = status["job"].value("state", "");
}
device["dev_online"] = online;
devices.push_back(std::move(device));
devices.push_back(device);
}
if (http_body) {
nlohmann::json out;
out["devices"] = std::move(devices);
out["devices"] = devices;
*http_body = out.dump();
}
} catch (const std::exception&) {
BOOST_LOG_TRIVIAL(debug) << "OrcaCloudServiceAgent: get_user_print_info parsed - device_count=" << devices.size()
<< ", devices=" << devices.dump();
} catch (const std::exception& e) {
BOOST_LOG_TRIVIAL(error) << "OrcaCloudServiceAgent: get_user_print_info parse exception - " << e.what();
return BAMBU_NETWORK_ERR_GET_SETTING_LIST_FAILED;
}
@@ -2864,6 +3147,9 @@ int OrcaCloudServiceAgent::get_camera_url(std::string dev_id, std::function<void
return BAMBU_NETWORK_SUCCESS;
}
std::unique_ptr<ICameraSignalingChannel> OrcaCloudServiceAgent::create_camera_signaling_channel(const std::string& dev_id)
{ return std::make_unique<OrcaCloudSignalingChannel>(this, dev_id); }
int OrcaCloudServiceAgent::get_design_staffpick(int offset, int limit, std::function<void(std::string)> callback)
{
BOOST_LOG_TRIVIAL(debug) << "OrcaCloudServiceAgent: get_design_staffpick (stub)";
+57 -2
View File
@@ -4,6 +4,11 @@
#include "CloudProvider.hpp"
#include "ICameraSignalingChannel.hpp"
#include "ICloudServiceAgent.hpp"
#include <boost/asio.hpp>
#include <boost/asio/ssl.hpp>
#include <boost/beast/core.hpp>
#include <boost/beast/ssl.hpp>
#include <boost/beast/websocket.hpp>
#include "bambu_networking.hpp"
#include <cstdlib>
#include "libslic3r/ProjectTask.hpp"
@@ -13,6 +18,9 @@
#include <atomic>
#include <chrono>
#include <functional>
#include <condition_variable>
#include <cstdint>
#include <set>
#include <memory>
#include <thread>
#include <unordered_map>
@@ -20,6 +28,8 @@
#include <vector>
#include <nlohmann/json.hpp>
#include "OrcaMqttConnection.hpp"
class wxSecretStore;
namespace Slic3r {
@@ -157,6 +167,8 @@ public:
// Configuration
void configure_urls(AppConfig* app_config);
// Hostname only; cloud REST, MQTT, and WebRTC signaling use fixed TLS
// endpoints on port 443.
void set_api_base_url(const std::string& url);
void set_auth_base_url(const std::string& url);
void set_cloud_base_url(const std::string& url);
@@ -216,6 +228,19 @@ public:
int add_subscribe(std::vector<std::string> dev_list) override;
int del_subscribe(std::vector<std::string> dev_list) override;
void enable_multi_machine(bool enable) override;
int set_printer_status_callback(OnMessageFn fn);
int send_printer_command(const std::string& dev_id, const std::string& body);
int upload_gcode_via_cloud(const std::string& dev_id,
const std::string& local_gcode_path,
std::string* job_id,
OnUpdateStatusFn update_fn,
WasCancelledFn cancel_fn);
int start_cloud_print_job(const std::string& dev_id,
const std::string& job_id,
const std::string& filename,
bool start = true);
// ========================================================================
// ICloudServiceAgent Interface Implementation - Settings Synchronization
@@ -251,7 +276,7 @@ public:
// ICloudServiceAgent Interface Implementation - Model Mall & Publishing
// ========================================================================
int get_camera_url(std::string dev_id, std::function<void(std::string)> callback) override;
// std::unique_ptr<ICameraSignalingChannel> create_camera_signaling_channel(const std::string& dev_id) override;
std::unique_ptr<ICameraSignalingChannel> create_camera_signaling_channel(const std::string& dev_id) override;
int get_design_staffpick(int offset, int limit, std::function<void(std::string)> callback) override;
int start_publish(PublishParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, std::string* out) override;
int get_model_publish_url(std::string* url) override;
@@ -357,7 +382,29 @@ public:
static std::string generate_uuid_for_setting_id(const std::string& name, const std::string& user_id = "");
OrcaMqttConnection* get_mqtt_connection() noexcept {
return mqtt_connection.get();
}
const OrcaMqttConnection* get_mqtt_connection() const noexcept {
return mqtt_connection.get();
}
// Account-scoped cloud socket: wss://<api_base_url>/api/v1/printers/mqtt.
// configure_ blocks for the duration of the initial connect attempt, so callers
// drive it off the UI thread; teardown_ is synchronous. The dev_id argument is
// retained for source compatibility with the printer-agent lifecycle; it does
// not participate in endpoint construction.
int configure_selected_printer_mqtt(const std::string& dev_id,
OrcaMqttConnection::StateHandler state_handler = {});
void teardown_selected_printer_mqtt();
// Test hook: the wss:// URL of the current fleet socket ("" when none).
std::string selected_printer_mqtt_url() const;
private:
// Fans one inbound fleet MQTT message out to printer_status_callback.
void deliver_cloud_message(const std::string& dev_id, const std::string& payload);
// Sync protocol helpers
int sync_pull(
std::function<void(const SyncPullResponse&)> on_success,
@@ -381,7 +428,11 @@ private:
// HTTP request helpers
int http_get(const std::string& path, std::string* response_body, unsigned int* http_code);
int http_post(const std::string& path, const std::string& body, std::string* response_body, unsigned int* http_code);
int http_post(const std::string& path,
const std::string& body,
std::string* response_body,
unsigned int* http_code,
const std::string& content_type = "application/json");
int http_put(const std::string& path, const std::string& body, std::string* response_body, unsigned int* http_code);
int http_delete(const std::string& path, std::string* response_body, unsigned int* http_code);
std::map<std::string, std::string> data_headers();
@@ -443,6 +494,9 @@ private:
std::chrono::system_clock::now().time_since_epoch()).count()};
// Member variables - connection state
std::unique_ptr<OrcaMqttConnection> mqtt_connection;
std::string m_selected_printer_mqtt_url; // guarded by m_selected_url_mutex
mutable std::mutex m_selected_url_mutex;
bool is_connected{false};
bool enable_track{false};
bool multi_machine_enabled{false};
@@ -456,6 +510,7 @@ private:
AppOnHttpErrorFn on_http_error_fn;
GetCountryCodeFn get_country_code_fn;
QueueOnMainFn queue_on_main_fn;
OnMessageFn printer_status_callback;
mutable std::mutex callback_mutex;
// Thread safety
@@ -0,0 +1,355 @@
#include "OrcaCloudSignalingChannel.hpp"
#include "Http.hpp"
#include "OrcaCloudServiceAgent.hpp"
#include "ICameraSignalingChannel.hpp"
#include <boost/asio/buffer.hpp>
#include <boost/asio/connect.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <boost/asio/post.hpp>
#include <boost/asio/ssl/verify_mode.hpp>
#include <boost/asio/ssl/host_name_verification.hpp>
#include <boost/asio/ssl/stream_base.hpp>
#include <boost/beast/core.hpp>
#include <boost/beast/core/stream_traits.hpp>
#include <boost/beast/core/flat_buffer.hpp>
#include <boost/beast/core/buffers_to_string.hpp>
#include <boost/log/trivial.hpp>
#include <memory>
#include <mutex>
#include <ios>
#include <exception>
#include <cstddef>
#include <nlohmann/json.hpp>
#include <openssl/ssl.h>
#include <cctype>
#include <iomanip>
#include <openssl/tls1.h>
#include <sstream>
#include <stdexcept>
#include <string>
#include <utility>
#include <vector>
namespace Slic3r {
OrcaCloudSignalingChannel::OrcaCloudSignalingChannel(OrcaCloudServiceAgent* cloud, std::string dev_id)
: m_cloud(cloud)
, m_dev_id(std::move(dev_id))
{
}
OrcaCloudSignalingChannel::~OrcaCloudSignalingChannel()
{
close();
}
void OrcaCloudSignalingChannel::open()
{
bool expected = false;
if (!m_open.compare_exchange_strong(expected, true))
return;
m_stop.store(false);
m_thread = std::thread([this] { run(); });
}
void OrcaCloudSignalingChannel::close()
{
m_stop.store(true);
std::shared_ptr<Connection> conn;
Http::Ptr request;
{
std::lock_guard<std::mutex> lock(m_mutex);
conn = m_conn;
request = m_inflight_requests;
}
// m_conn is installed only after the live-token request completes, so
// cancel the request independently before waiting for the worker.
if (request)
request->cancel();
if (conn) {
// Established session: close the socket on the io_context's own thread so
// the pending async_read completes and io_context.run() unwinds.
boost::asio::post(conn->io_context, [conn] {
boost::system::error_code ec;
boost::beast::get_lowest_layer(conn->websocket).cancel(ec);
boost::beast::get_lowest_layer(conn->websocket).close(ec);
});
// Pre-run() phase (still in the synchronous connect/handshake): best-effort
// direct interruption.
boost::system::error_code ec;
boost::beast::get_lowest_layer(conn->websocket).cancel(ec);
boost::beast::get_lowest_layer(conn->websocket).close(ec);
}
if (m_thread.joinable())
m_thread.join();
m_open.store(false);
}
void OrcaCloudSignalingChannel::send_offer(std::string sdp)
{
send_json(nlohmann::json{{"type", "webrtc.offer"}, {"sdp", std::move(sdp)}}.dump());
}
void OrcaCloudSignalingChannel::send_ice(std::string candidate, std::string mid)
{
send_json(nlohmann::json{{"type", "webrtc.ice"},
{"candidate", std::move(candidate)},
{"sdpMid", std::move(mid)}}
.dump());
}
std::string OrcaCloudSignalingChannel::encode_path_component(const std::string& value)
{
std::ostringstream encoded;
encoded << std::uppercase << std::hex;
for (unsigned char c : value) {
if (std::isalnum(c) || c == '-' || c == '_' || c == '.' || c == '~')
encoded << c;
else
encoded << '%' << std::setw(2) << std::setfill('0') << static_cast<unsigned int>(c);
}
return encoded.str();
}
void OrcaCloudSignalingChannel::unavailable(CameraUnavailableReason reason, std::string detail)
{
if (on_unavailable)
on_unavailable(reason, std::move(detail));
}
void OrcaCloudSignalingChannel::run()
{
try {
if (!m_cloud || !m_cloud->ensure_token_fresh("camera")) {
unavailable(CameraUnavailableReason::Error, "Unable to refresh OrcaCloud credentials");
m_open.store(false);
return;
}
const std::string token = m_cloud->get_access_token();
// OrcaCloud exposes a bare API hostname. WebRTC signaling uses the
// fixed HTTPS/WSS endpoints on port 443; custom schemes, ports, and
// base paths are not supported by this agent.
const std::string host = m_cloud->get_cloud_service_host();
if (token.empty() || host.empty()) {
unavailable(CameraUnavailableReason::Error, "OrcaCloud session is unavailable");
m_open.store(false);
return;
}
const std::string live_token_url =
"https://" + host + "/api/v1/printers/" + encode_path_component(m_dev_id) + "/live-token";
BOOST_LOG_TRIVIAL(info) << "signaling: POST " << live_token_url << " (dev_id=" << m_dev_id << ")";
nlohmann::json token_response;
std::string token_body;
unsigned int http_code = 0;
auto request = std::make_shared<Slic3r::Http>(Http::post(live_token_url));
request->set_post_body(std::string("{}"))
.header("Authorization", "Bearer " + token)
.header("Content-Type", "application/json")
.tls_verify(true)
.timeout_max(30)
.on_complete([&token_body, &http_code](std::string body, unsigned status) {
http_code = status;
token_body = std::move(body);
})
.on_error([&http_code](std::string, std::string, unsigned status) {
http_code = status;
});
{
std::lock_guard<std::mutex> lock(m_mutex);
m_inflight_requests = request;
}
// If close() raced with request setup, make sure this request is
// cancelled before entering the blocking call.
if (m_stop.load())
request->cancel();
try {
request->perform_sync();
} catch (...) {
std::lock_guard<std::mutex> lock(m_mutex);
if (m_inflight_requests == request)
m_inflight_requests.reset();
throw;
}
{
std::lock_guard<std::mutex> lock(m_mutex);
if (m_inflight_requests == request)
m_inflight_requests.reset();
}
try {
token_response = nlohmann::json::parse(token_body);
} catch (const std::exception&) {
}
if (http_code < 200 || http_code >= 300 || !token_response.contains("token")) {
unavailable(CameraUnavailableReason::Error, "Unable to mint camera live token");
m_open.store(false);
return;
}
std::vector<CameraIceServer> ice_servers;
if (token_response.contains("ice_servers") && token_response["ice_servers"].is_array()) {
for (const auto& entry : token_response["ice_servers"]) {
if (entry.is_string()) {
ice_servers.push_back({entry.get<std::string>(), {}, {}});
} else if (entry.is_object()) {
// RTCIceServer.urls is "string | string[]" (Cloudflare
// Realtime returns an array). Emit one CameraIceServer per
// URL, sharing the credentials.
const std::string username = entry.value("username", std::string{});
const std::string credential = entry.value("credential", std::string{});
const auto add_url = [&](const nlohmann::json& url) {
if (url.is_string() && !url.get<std::string>().empty())
ice_servers.push_back({url.get<std::string>(), username, credential});
};
const auto urls = entry.find("urls");
if (urls != entry.end()) {
if (urls->is_array()) {
for (const auto& url : *urls)
add_url(url);
} else {
add_url(*urls);
}
}
}
}
}
auto conn = std::make_shared<Connection>();
conn->ssl_context.set_default_verify_paths();
Http::add_platform_root_certificates(conn->ssl_context.native_handle());
{
std::lock_guard<std::mutex> lock(m_mutex);
m_conn = conn;
}
auto& websocket = conn->websocket;
boost::asio::ip::tcp::resolver resolver(conn->io_context);
const auto endpoints = resolver.resolve(host, "443");
boost::asio::connect(boost::beast::get_lowest_layer(websocket), endpoints);
if (!SSL_set_tlsext_host_name(websocket.next_layer().native_handle(), host.c_str()))
throw std::runtime_error("Unable to configure TLS server name");
websocket.next_layer().set_verify_mode(boost::asio::ssl::verify_peer);
websocket.next_layer().set_verify_callback(boost::asio::ssl::host_name_verification(host));
websocket.next_layer().handshake(boost::asio::ssl::stream_base::client);
const std::string ws_target = "/api/v1/printers/" + encode_path_component(m_dev_id) +
"/camera/live?token=" +
encode_path_component(token_response["token"].get<std::string>());
websocket.handshake(host, ws_target);
BOOST_LOG_TRIVIAL(info) << "signaling: websocket handshake ok (" << ice_servers.size()
<< " ice servers)";
if (on_ready)
on_ready(std::move(ice_servers));
send_json(nlohmann::json{{"type", "camera.mode"}, {"mode", "webrtc"}}.dump());
// Async read loop, driven by the connection's own io_context. run()
// returns once close() has shut the socket down, giving a bounded,
// deadlock-free teardown from any thread.
do_read(conn);
conn->io_context.run();
} catch (const std::exception& e) {
BOOST_LOG_TRIVIAL(warning) << "signaling: run() exception: " << e.what();
if (!m_stop.load())
unavailable(CameraUnavailableReason::Closed, e.what());
}
{
std::lock_guard<std::mutex> lock(m_mutex);
m_conn.reset();
}
m_open.store(false);
}
void OrcaCloudSignalingChannel::do_read(std::shared_ptr<Connection> conn)
{
auto buffer = std::make_shared<boost::beast::flat_buffer>();
conn->websocket.async_read(
*buffer, [this, conn, buffer](boost::system::error_code ec, std::size_t) {
if (ec) {
if (!m_stop.load())
unavailable(CameraUnavailableReason::Closed, ec.message());
return; // do not re-arm; io_context.run() unwinds
}
const std::string raw = boost::beast::buffers_to_string(buffer->data());
// A malformed or unexpectedly-shaped message must not tear down the
// session: parse/dispatch is guarded.
try {
dispatch_message(nlohmann::json::parse(raw), raw);
} catch (const std::exception& e) {
BOOST_LOG_TRIVIAL(warning) << "signaling: ignoring malformed message: " << e.what()
<< " raw=" << raw.substr(0, 256);
}
if (!m_stop.load())
do_read(conn);
});
}
// Returns the string at key, or "" if absent or not a string (JSON null included).
static std::string json_string(const nlohmann::json& object, const char* key)
{
const auto it = object.find(key);
return (it != object.end() && it->is_string()) ? it->get<std::string>() : std::string{};
}
void OrcaCloudSignalingChannel::dispatch_message(const nlohmann::json& message, const std::string& raw)
{
const std::string type = json_string(message, "type");
BOOST_LOG_TRIVIAL(info) << "signaling: recv type=" << type << " raw=" << raw.substr(0, 256);
if (type == "webrtc.answer") {
const std::string sdp = json_string(message, "sdp");
if (on_answer && !sdp.empty())
on_answer(sdp);
} else if (type == "webrtc.ice" && message.contains("candidate")) {
// The peer may send "candidate" as a flat string or as a nested
// RTCIceCandidateInit object { candidate, sdpMid, sdpMLineIndex }.
const nlohmann::json& candidate = message["candidate"];
std::string sdp_candidate;
std::string mid = json_string(message, "sdpMid");
if (candidate.is_string()) {
sdp_candidate = candidate.get<std::string>();
} else if (candidate.is_object()) {
sdp_candidate = json_string(candidate, "candidate");
std::string nested_mid = json_string(candidate, "sdpMid");
if (!nested_mid.empty())
mid = std::move(nested_mid);
}
if (on_ice && !sdp_candidate.empty())
on_ice(sdp_candidate, mid);
} else if (type == "webrtc.unavailable") {
const std::string reason = json_string(message, "reason");
unavailable(reason == "busy" ? CameraUnavailableReason::Busy
: reason == "disabled" ? CameraUnavailableReason::Disabled
: CameraUnavailableReason::Error,
reason.empty() ? "error" : reason);
}
}
void OrcaCloudSignalingChannel::send_json(const std::string& message)
{
std::shared_ptr<Connection> conn;
{
std::lock_guard<std::mutex> lock(m_mutex);
conn = m_conn;
}
if (!conn || m_stop.load())
return;
// Serialize the write onto the io_context thread (same thread that runs
// async_read), so reads and writes never touch the stream concurrently.
auto payload = std::make_shared<std::string>(message);
boost::asio::post(conn->io_context, [this, conn, payload] {
if (m_stop.load())
return;
boost::system::error_code ec;
conn->websocket.write(boost::asio::buffer(*payload), ec);
if (ec && !m_stop.load())
unavailable(CameraUnavailableReason::Closed, ec.message());
});
}
} // namespace Slic3r
@@ -0,0 +1,69 @@
#pragma once
#include "ICameraSignalingChannel.hpp"
#include "OrcaCloudServiceAgent.hpp"
#include "Http.hpp"
#include <boost/asio/io_context.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <boost/asio/ssl.hpp>
#include <boost/asio/ssl/context.hpp>
#include <boost/beast/ssl.hpp>
#include <boost/beast/ssl/ssl_stream.hpp>
#include <boost/beast/websocket.hpp>
#include <boost/beast/websocket/stream.hpp>
#include <nlohmann/json_fwd.hpp>
#include <atomic>
#include <memory>
#include <mutex>
#include <string>
#include <thread>
namespace Slic3r {
class OrcaCloudSignalingChannel : public ICameraSignalingChannel {
public:
OrcaCloudSignalingChannel(OrcaCloudServiceAgent* cloud, std::string dev_id);
~OrcaCloudSignalingChannel() override;
void open() override;
void close() override;
void send_offer(std::string sdp) override;
void send_ice(std::string candidate, std::string mid) override;
private:
using WebSocket = boost::beast::websocket::stream<
boost::beast::ssl_stream<boost::asio::ip::tcp::socket>>;
// The io_context and ssl_context must outlive the websocket stream that
// references them. Bundling them here with the stream declared last makes
// the destruction order correct (stream first, then contexts), and lets a
// single shared_ptr own the whole set.
struct Connection {
boost::asio::io_context io_context;
boost::asio::ssl::context ssl_context{boost::asio::ssl::context::tls_client};
WebSocket websocket{io_context, ssl_context};
};
void run();
void do_read(std::shared_ptr<Connection> conn);
void dispatch_message(const nlohmann::json& message, const std::string& raw);
void send_json(const std::string& message);
void unavailable(CameraUnavailableReason reason, std::string detail);
static std::string encode_path_component(const std::string& value);
OrcaCloudServiceAgent* m_cloud;
Http::Ptr m_inflight_requests{nullptr};
std::string m_dev_id;
std::atomic<bool> m_stop{false};
std::atomic<bool> m_open{false};
std::thread m_thread;
mutable std::mutex m_mutex;
std::shared_ptr<Connection> m_conn;
};
} // namespace Slic3r
+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
+207 -4
View File
@@ -2,22 +2,34 @@
#define __ORCA_PRINTER_AGENT_HPP__
#include "IPrinterAgent.hpp"
#include "ICloudServiceAgent.hpp"
#include "OrcaCloudServiceAgent.hpp"
#include "OrcaMqttConnection.hpp"
#include <atomic>
#include <cstdint>
#include <functional>
#include "bambu_networking.hpp"
#include <string>
#include <mutex>
#include <memory>
#include <thread>
#include <unordered_map>
#include <vector>
namespace Slic3r { class ICloudServiceAgent; }
namespace Slic3r {
class OrcaCloudServiceAgent;
/**
* OrcaPrinterAgent - Stub implementation for printer operations.
* OrcaPrinterAgent - OrcaSonar MQTT printer agent.
*
* All printer-related operations are currently stubs that return success.
* Actual printer connectivity requires the BBL SDK or future Orca implementation.
* LAN and cloud commands use the same OrcaSonar protocol payloads; only the
* MQTT connection selected by route_send() differs.
*/
class OrcaPrinterAgent : public IPrinterAgent {
class OrcaPrinterAgent : public IPrinterAgent
{
public:
explicit OrcaPrinterAgent(std::string log_dir);
~OrcaPrinterAgent() override;
@@ -27,9 +39,18 @@ public:
// ========================================================================
void set_cloud_agent(std::shared_ptr<ICloudServiceAgent> cloud) override;
CameraStreamMode get_camera_stream_mode() const override;
std::string get_camera_url() const override;
int list_printer_files(const std::string& dev_id, PrinterFileListFn callback) override;
int get_printer_file_thumbnail(const std::string& dev_id, const std::string& path, PrinterFileThumbnailFn callback) override;
int delete_printer_file(const std::string& dev_id, const std::string& path, PrinterFileDeleteFn callback) override;
int get_printer_file_metadata(const std::string& dev_id, const std::string& path, PrinterFileMetadataFn callback) override;
// Communication
int send_message(std::string dev_id, std::string json_str, int qos, int flag) override;
bool supports_command(const std::string& dev_id, const std::string& command) const override;
bool supports_feature(const std::string& dev_id, const std::string& feature) const override;
bool uses_filament_mapping() const override { return true; }
int connect_printer(const PrinterConnectionParams& params) override;
int disconnect_printer() override;
int send_message_to_printer(std::string dev_id, std::string json_str, int qos, int flag) override;
@@ -44,6 +65,7 @@ public:
// Machine Selection
std::string get_user_selected_machine() override;
int set_user_selected_machine(std::string dev_id) override;
int unbind(std::string dev_id) override;
/**
* Get agent information.
@@ -55,6 +77,8 @@ public:
// Print Job Operations
int start_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn) override;
// Unimplemented on OrcaSonar: reports a non-success result so callers fall back to
// start_print() instead of treating the missing send as success.
int start_local_print_with_record(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn) override;
int start_send_gcode_to_sdcard(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn) override;
int start_local_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn) override;
@@ -70,10 +94,189 @@ public:
int set_on_local_message_fn(OnMessageFn fn) override;
int set_queue_on_main_fn(QueueOnMainFn fn) override;
int command_ams_refresh_rfid(std::string dev_id, int ams_id, int tray_id, int sequence_id, bool lan_mode) override;
int command_ams_calibrate(std::string dev_id, int ams_id, int sequence_id, bool lan_mode) override;
int command_ams_select_tray(std::string dev_id, std::string tray_id, int sequence_id, bool lan_mode) override;
int command_start_camera(std::string dev_id) override;
int command_xyz_abs(std::string dev_id, int sequence_id, bool lan_mode) override;
int command_auto_leveling(std::string dev_id, int sequence_id, bool lan_mode) override;
int command_go_home(std::string dev_id, bool is_printing, bool supports_mqtt_homing, int sequence_id, bool lan_mode) override;
int command_set_bed(std::string dev_id, int temp, bool supports_mqtt_bed_ctrl, int sequence_id, bool lan_mode) override;
int command_set_nozzle(std::string dev_id, int temp, int sequence_id, bool lan_mode) override;
int command_axis_control(std::string dev_id,
std::string axis,
double unit,
double input_val,
int speed,
bool is_core_xy,
bool supports_mqtt_axis_control,
int sequence_id,
bool lan_mode) override;
// Test-only: drive emit_connect_sequence directly (no socket).
void run_connect_sequence_for_test(const std::string& dev_id)
{
emit_connect_sequence(dev_id, [](const std::string&) {}, [](const std::string&) {});
}
// Test-only: advance the LAN connection epoch without a connect/disconnect cycle.
void bump_lan_generation_for_test() { ++m_lan_generation; }
// Test-only: the same for the (independent) cloud selection epoch.
void bump_cloud_generation_for_test() { ++m_cloud_generation; }
FilamentSyncMode get_filament_sync_mode() const override { return FilamentSyncMode::subscription; }
protected:
// Forward one inbound printer message to on_message_fn or on_local_message_fn (marshalled onto the UI
// thread via queue_on_main_fn when set). Body of every connection's MessageHandler.
void deliver_to_sink(const std::string& dev_id, const std::string& payload, bool local);
// Extract OrcaSonar's print.ipcam.stream_mode from LAN reports before they
// are forwarded to the GUI. The getters below then read this agent-owned state.
void parse_ipcam_info(const std::string& dev_id, const std::string& payload);
// Orca-dialect -> Bambu-dialect compatibility shim for inbound reports: the single
// place Orca Protocol JSON is rewritten into the shapes MachineObject::parse_json
// already handles, so parse_json needs no Orca-specific changes. Self-contained
// (its cache is a function-local static) and deletable together with its call site
// once parse_json reads the Orca dialect natively. See the definition for the
// per-rule detail. Returns the payload unchanged when no rule applies.
std::string merge_capabilities(const std::string& dev_id, const std::string& payload);
void forget_device_capabilities(const std::string& dev_id);
int prepare_outgoing_request(const std::string& dev_id, const std::string& payload,
std::string& command, std::string& prepared) const;
// Report the asynchronous LAN connection state using the same callback contract as
// the other printer agents. The transport result cannot be returned by
// connect_printer(), which only starts the worker.
void dispatch_local_connect(int state, const std::string& dev_id, const std::string& message);
// The LAN inbound-message handler for one connection generation: forwards to
// deliver_to_sink only while `generation` is still the live epoch.
std::function<void(const std::string&, const std::string&)> make_lan_message_handler(uint64_t generation);
// Pure LAN-address parsing + client-id. protected static so the test Probe reaches them.
static bool parse_lan_endpoint(const std::string& dev_ip, std::string& host, std::string& port);
static std::string make_lan_client_id(const std::string& dev_id);
// Pure serializer for PrintParams::ams_mapping2 -> print.gcode_file.filament_mapping.
// Entries are re-keyed by their array position; {255,255} is dropped. Empty when the
// input is empty, malformed, or has no usable entries. protected static for the test Probe.
static nlohmann::json build_filament_mapping(const std::string& ams_mapping2);
// Pure builder for the print.gcode_file command payload. A non-empty mapping is
// included as filament_mapping; an empty one is omitted so the payload is
// byte-identical to an unmapped print. protected static for the test Probe.
static nlohmann::json build_gcode_file_payload(const std::string& sequence_id,
const std::string& target,
const nlohmann::json& filament_mapping);
// Pure JSON -> entries normalization for the files.list MQTT reply
// ({"files":{...,"entries":[{name,path,is_dir,size,modified}]}}). Directory
// entries and entries without a usable name are skipped; a missing path falls
// back to the name and size/modified default to 0. protected static for the Probe.
static std::vector<PrinterFileEntry> parse_files_list_reply(const std::string& payload);
// Pure JSON -> metadata normalization for the files.metadata MQTT reply. Missing
// or malformed fields default to 0. protected static for the Probe.
static PrinterFileMetadata parse_files_metadata_reply(const std::string& payload);
// Pure JSON -> image-bytes extraction for the files.thumbnails MQTT reply
// ({"files":{...,"items":[{path,data|state,...}]}}). Returns decoded image bytes
// for `path`, or empty when the path has no thumbnail, an error state, or is
// absent. protected static for the test Probe.
static std::string parse_files_thumbnail_reply(const std::string& payload, const std::string& path);
// Dispatch one files.* command (list/metadata/delete) over the LAN MQTT
// connection and route its reply, matched by sequence_id, into `complete`.
// `complete` runs once on the UI thread (or the calling thread when no queue is
// set) with the raw reply payload on success, or an empty string on
// failure/timeout. Returns whether the request was dispatched.
int send_files_request(const std::string& dev_id, const std::string& command, nlohmann::json fields,
std::function<void(int result, const std::string& payload)> complete);
// Consume an inbound report that answers a pending files.* request. Returns true
// when it matched, so the caller does not forward it to the machine-state sink.
bool try_consume_files_reply(const std::string& dev_id, const std::string& payload);
// Test hook: the ws:// URL connect_printer built for the current LAN session ("" if none).
std::string lan_connection_target() const;
// Shared post-connect sequence: SUBSCRIBE, then pushing.start, pushall,
// info.get_version, info.get_capabilities. Runs identically on LAN and cloud.
void on_connected(const std::string& dev_id, OrcaMqttConnection* conn, uint64_t generation);
// The post-connect command sequence, factored behind a seam so a test can
// observe the SUBSCRIBE + 4 request payloads without a live OrcaMqttConnection.
virtual void emit_connect_sequence(const std::string& dev_id,
std::function<void(const std::string&)> subscribe,
std::function<void(const std::string&)> request);
static std::string seq(int n); // decimal string in the OrcaSlicer 20000..29999 band
static std::string build_pushing_start(const std::string& sequence_id);
static std::string build_pushing_stop(const std::string& sequence_id);
static std::string build_pushall(const std::string& sequence_id);
static std::string build_get_version(const std::string& sequence_id);
static std::string build_get_capabilities(const std::string& sequence_id);
private:
class OrcaSonarDiscovery;
std::string log_dir;
std::string selected_machine;
enum CurrentConn { NONE, CLOUD, LAN };
static const char* connection_type_name(CurrentConn connection);
// The transport for the printer currently selected by the UI. LAN and
// cloud sessions have separate connection objects, so this is selection
// state rather than an inference from whichever socket happens to exist.
CurrentConn m_current_connection = NONE;
std::shared_ptr<ICloudServiceAgent> m_cloud_agent;
std::shared_ptr<OrcaMqttConnection> lan_mqtt_connection;
// Two independent epochs: a cloud (de)selection must not fence the live LAN
// feed, and vice versa. Each transport's connect thread and inbound handler
// compare against their own counter only.
std::atomic<uint64_t> m_lan_generation{0};
std::atomic<uint64_t> m_cloud_generation{0};
// The short-lived threads that run the blocking initial connect for the current
// LAN / cloud session. Joined members (never detached) so they cannot outlive
// *this or the connection they hold a raw pointer to.
std::thread m_lan_connect_thread;
std::thread m_cloud_connect_thread;
std::unique_ptr<OrcaSonarDiscovery> m_discovery;
std::string m_lan_dev_id; // guarded by state_mutex
std::string m_lan_url; // guarded by state_mutex — the Config.url of the live LAN session
bool m_lan_use_ssl = false; // guarded by state_mutex
std::string m_lan_ca_file; // guarded by state_mutex
CameraStreamMode m_camera_stream_mode = CameraStreamMode::none; // guarded by state_mutex
std::string m_camera_url; // guarded by state_mutex
// Pending files.* MQTT replies, keyed by sequence_id. The registry is
// heap-owned so a timeout watchdog can outlive the agent without touching
// `this`; each entry carries its own one-shot guard.
struct FilesReply {
std::string device_id;
std::function<void(int, const std::string&)> complete;
std::shared_ptr<std::atomic<bool>> fired = std::make_shared<std::atomic<bool>>(false);
};
struct FilesReplyRegistry {
std::mutex mutex;
std::unordered_map<std::string, FilesReply> entries;
};
std::shared_ptr<FilesReplyRegistry> m_files_replies = std::make_shared<FilesReplyRegistry>();
OrcaCloudServiceAgent* get_orca_cloud_agent();
OrcaMqttConnection* get_appropriate_mqtt_connection(bool is_lan = true);
static bool parse_nonnegative_command_id(const std::string& value, int& result);
// Route one command payload to device/<dev_id>/request on the LAN or the shared
// cloud connection. The uniform send path for both send_message* overrides.
int route_send(bool is_lan, const std::string& dev_id, const std::string& json_str);
// Callbacks
OnMsgArrivedFn on_ssdp_msg_fn;
+37
View File
@@ -52,6 +52,20 @@ static std::set<int> tools_for_role(const std::string& gcode, const std::string&
return tools;
}
// Every id named by a Tn command anywhere in `gcode`. A non-BBL single-extruder multi-filament
// setup emits T<filament_id>, and that id is what the Klipper toolchange macro consumes.
static std::set<int> tool_ids(const std::string& gcode)
{
std::set<int> tools;
GCodeReader reader;
reader.parse_buffer(gcode, [&tools](GCodeReader&, const GCodeReader::GCodeLine& line) {
const std::string cmd(line.cmd());
if (cmd.size() >= 2 && cmd[0] == 'T' && std::isdigit((unsigned char)cmd[1]))
tools.insert(std::stoi(cmd.substr(1)));
});
return tools;
}
// X where the nozzle sits while each tagged _WAIT_FOR_TEMP_ON_WIPE_TOWER M109 blocks:
// the nearest preceding G1 carrying an X (the park travel emitted just before the wait).
static std::vector<double> wait_park_xs(const std::string& gcode)
@@ -299,6 +313,29 @@ TEST_CASE("Each feature prints with its assigned filament (three filaments)", "[
CHECK(tools_for_role(gcode, "infill") == std::set<int>{ 1 }); // filament 2
}
// The per-print wire mapping keys each entry by the filament's index in the config filament
// arrays (the ams_mapping2 position). Pin that the id the toolchange emits is that index even
// when the used filaments skip a middle one: filament 2 (index 1) is configured but unused, so
// a renumbering to the used set would emit {0, 1} instead of {0, 2} and aim the map at the
// wrong lane.
TEST_CASE("Toolchange ids keep the config index of the used filament when a middle filament is unused", "[MultiFilament]")
{
const std::string gcode = slice({ cube(20) },
multifilament_config(3, {
{ "sparse_infill_filament_id", 1 },
{ "internal_solid_filament_id", 1 },
{ "top_surface_filament_id", 1 },
{ "bottom_surface_filament_id", 1 },
{ "outer_wall_filament_id", 3 },
{ "inner_wall_filament_id", 3 },
{ "skirt_loops", 0 },
{ "brim_type", "no_brim" },
}));
CHECK(tools_for_role(gcode, "perimeter") == std::set<int>{ 2 }); // filament 3
CHECK(tools_for_role(gcode, "infill") == std::set<int>{ 0 }); // filament 1
CHECK(tool_ids(gcode) == std::set<int>{ 0, 2 });
}
// The override must survive tool ordering: object 1's walls print on their filament's
// tool, object 0 stays on the first. If dropped, every wall prints on tool 0.
TEST_CASE("Per-object wall filament override is honored", "[MultiFilament]")
+1
View File
@@ -27,6 +27,7 @@ add_executable(${_TEST_NAME}_tests
test_toolordering_nozzle_group.cpp
test_parallel_resolve.cpp
test_preset_bundle_loading.cpp
test_preset_bundle_ams_sync.cpp
test_preset_setting_id.cpp
test_preset_diff.cpp
test_vendor_cache.cpp
+37
View File
@@ -2043,3 +2043,40 @@ TEST_CASE("3MF XML entries declaring more than an int can hold fail to load", "[
CHECK_FALSE(parser.check_3mf_from_prusa(path));
}
}
TEST_CASE("Loading a gcode.3mf from a stream keeps the plate's pick and pattern file paths", "[3mf][Regression]")
{
Model model;
REQUIRE(load_stl((std::string(TEST_DATA_DIR) + "/test_3mf/Prusa.stl").c_str(), &model));
model.add_default_instances();
ScopedTemporaryDir backup_dir("orca_pick");
model.set_backup_path(backup_dir.string());
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
auto plate = std::make_unique<PlateData>();
plate->plate_index = 0;
plate->pick_file = (backup_dir.path() / "pick.png").string();
plate->pattern_bbox_file = (backup_dir.path() / "plate.json").string();
ScopedTemporaryFile temp(".3mf");
const std::string test_file = temp.string();
StoreParams store_params;
store_params.path = test_file.c_str();
store_params.model = &model;
store_params.config = &config;
store_params.plate_data_list.push_back(plate.get());
store_params.strategy = SaveStrategy::Zip64 | SaveStrategy::Silence;
REQUIRE(store_bbs_3mf(store_params));
boost::nowide::ifstream in(test_file, std::ios::binary);
Model dst_model;
DynamicPrintConfig dst_config;
PlateDataPtrs dst_plates;
Semver file_version;
dst_model.set_backup_path(backup_dir.string());
REQUIRE(load_gcode_3mf_from_stream(in, &dst_config, &dst_model, &dst_plates, &file_version));
REQUIRE(dst_plates.size() == 1);
CHECK(dst_plates.front()->pick_file == "Metadata/pick_1.png");
CHECK(dst_plates.front()->pattern_bbox_file == "Metadata/plate_1.json");
release_PlateData_list(dst_plates);
}
@@ -0,0 +1,197 @@
#include <catch2/catch_all.hpp>
#include <libslic3r/PrintConfig.hpp>
#include <map>
#include <string>
#include <utility>
#include <vector>
#include "catch2/catch_test_macros.hpp"
#include "libslic3r/Preset.hpp"
#include "libslic3r/PresetBundle.hpp"
using namespace Slic3r;
namespace {
// In-memory system filament preset. Mirrors the helper in test_preset_bundle_loading.cpp.
Preset &add_system_filament(PresetBundle &bundle, const std::string &name, const std::string &filament_id, const std::string &filament_type)
{
DynamicPrintConfig config(bundle.filaments.default_preset().config);
config.option<ConfigOptionString>(BBL_JSON_KEY_INHERITS, true)->value = "";
config.option<ConfigOptionStrings>("filament_type", true)->values = {filament_type};
Preset &preset = bundle.filaments.load_preset(std::string(), name, config, /*select=*/false);
preset.is_system = true;
preset.filament_id = filament_id;
return preset;
}
// One AMS tray entry as build_filament_ams_list produces it.
DynamicPrintConfig make_tray(const std::string &filament_id, const std::string &filament_type, const std::string &color, const std::string &ams_id,
const std::string &slot_id)
{
DynamicPrintConfig tray;
tray.set_key_value("filament_id", new ConfigOptionStrings{filament_id});
tray.set_key_value("filament_type", new ConfigOptionStrings{filament_type});
tray.set_key_value("filament_colour", new ConfigOptionStrings{color});
tray.set_key_value("filament_colour_type", new ConfigOptionStrings{"1"});
tray.set_key_value("filament_multi_colour", new ConfigOptionStrings{});
tray.set_key_value("ams_id", new ConfigOptionStrings{ams_id});
tray.set_key_value("slot_id", new ConfigOptionStrings{slot_id});
return tray;
}
} // namespace
// A tray the printer UI wrote carries a material type but no OrcaSlicer preset id. It must still
// sync, resolved to Generic <type> with the tray's own color.
TEST_CASE("AMS sync resolves a printer-set tray to Generic by material type", "[Preset][AMS]")
{
PresetBundle bundle;
add_system_filament(bundle, "Generic PETG @Q2", "OFYPdQJh", "PETG");
add_system_filament(bundle, "Generic PLA @Q2", "GFL99", "PLA");
bundle.filament_ams_list[0] = make_tray("OFYPdQJh", "PETG", "#FE717A", "0", "0"); // preset-backed
bundle.filament_ams_list[1] = make_tray("", "PETG", "#898F9B", "0", "1"); // printer-set, no id
bundle.filament_ams_list[2] = make_tray("", "PLA", "#FAFAFA", "0", "2"); // printer-set, no id
bundle.filament_ams_list[3] = make_tray("", "", "#000000", "0", "3"); // empty slot
std::vector<std::pair<DynamicPrintConfig *, std::string>> unknowns;
std::map<int, AMSMapInfo> maps;
MergeFilamentInfo merge;
const unsigned int count = bundle.sync_ams_list(unknowns, /*use_map=*/false, maps, /*enable_append=*/false, merge);
CHECK(count == 3);
CHECK(unknowns.empty());
REQUIRE(bundle.filament_presets.size() == 3);
CHECK(bundle.filament_presets[0] == "Generic PETG @Q2");
CHECK(bundle.filament_presets[1] == "Generic PETG @Q2");
CHECK(bundle.filament_presets[2] == "Generic PLA @Q2");
const ConfigOptionStrings *colors = bundle.project_config.option<ConfigOptionStrings>("filament_colour");
REQUIRE(colors != nullptr);
REQUIRE(colors->values.size() == 3);
CHECK(colors->values[0] == "#FE717A");
CHECK(colors->values[1] == "#898F9B");
CHECK(colors->values[2] == "#FAFAFA");
}
// Without a preset id and without a material type there is nothing to resolve; the tray stays
// skipped in a direct sync.
TEST_CASE("AMS sync skips a tray without a preset id or a material type", "[Preset][AMS]")
{
PresetBundle bundle;
add_system_filament(bundle, "Generic PETG @Q2", "OFYPdQJh", "PETG");
bundle.filament_ams_list[0] = make_tray("", "", "#000000", "0", "0");
std::vector<std::pair<DynamicPrintConfig *, std::string>> unknowns;
std::map<int, AMSMapInfo> maps;
MergeFilamentInfo merge;
CHECK(bundle.sync_ams_list(unknowns, /*use_map=*/false, maps, /*enable_append=*/false, merge) == 0);
CHECK(bundle.filament_presets.empty());
}
// Guard the pre-existing behavior: a non-empty id that matches no preset still resolves by type.
TEST_CASE("AMS sync still resolves an unmatched preset id by material type", "[Preset][AMS]")
{
PresetBundle bundle;
add_system_filament(bundle, "Generic PETG @Q2", "OFYPdQJh", "PETG");
bundle.filament_ams_list[0] = make_tray("REMOVED_ID", "PETG", "#123456", "0", "0");
std::vector<std::pair<DynamicPrintConfig *, std::string>> unknowns;
std::map<int, AMSMapInfo> maps;
MergeFilamentInfo merge;
CHECK(bundle.sync_ams_list(unknowns, /*use_map=*/false, maps, /*enable_append=*/false, merge) == 1);
CHECK(unknowns.size() == 1);
REQUIRE(bundle.filament_presets.size() == 1);
CHECK(bundle.filament_presets[0] == "Generic PETG @Q2");
}
// Mapping-mode sync used to force "Generic PLA" for an id-less tray; it must use the type instead.
TEST_CASE("AMS sync in mapping mode resolves a printer-set tray to Generic, not Generic PLA", "[Preset][AMS]")
{
PresetBundle bundle;
add_system_filament(bundle, "Generic PETG @Q2", "OFYPdQJh", "PETG");
add_system_filament(bundle, "Generic PLA @Q2", "GFL99", "PLA");
bundle.filament_presets = {"Generic PLA @Q2"};
bundle.project_config.option<ConfigOptionStrings>("filament_colour")->values = {"#000000"};
bundle.project_config.option<ConfigOptionStrings>("filament_colour_type")->values = {"1"};
bundle.filament_ams_list[0] = make_tray("", "PETG", "#898F9B", "0", "1");
std::map<int, AMSMapInfo> maps;
maps[0] = AMSMapInfo{"0", "1"};
std::vector<std::pair<DynamicPrintConfig *, std::string>> unknowns;
MergeFilamentInfo merge;
const unsigned int count = bundle.sync_ams_list(unknowns, /*use_map=*/true, maps, /*enable_append=*/false, merge);
CHECK(count == 1);
CHECK(unknowns.empty());
REQUIRE(bundle.filament_presets.size() == 1);
CHECK(bundle.filament_presets[0] == "Generic PETG @Q2");
CHECK(bundle.project_config.option<ConfigOptionStrings>("filament_colour")->values[0] == "#898F9B");
}
// The dialog builds ams_mapping2 one entry per filament_presets index, while the generated
// G-code's toolchange names the filament's index in the slice's filament config arrays. Those
// are the same index only if full_config() collects the filament arrays in filament_presets
// order; if a refactor reorders one side, every mapped print aims at the wrong lane.
TEST_CASE("Full config filament arrays follow the selected filament preset order", "[Preset][AMS]")
{
PresetBundle bundle;
add_system_filament(bundle, "Generic PETG @Q2", "OFYPdQJh", "PETG");
add_system_filament(bundle, "Generic PLA @Q2", "GFL99", "PLA");
// Deliberately not the load order: the arrays must follow this selection.
bundle.filament_presets = {"Generic PLA @Q2", "Generic PETG @Q2"};
const DynamicPrintConfig full = bundle.full_config(/*apply_extruder=*/false);
REQUIRE(full.option<ConfigOptionStrings>("filament_type") != nullptr);
CHECK(full.option<ConfigOptionStrings>("filament_settings_id")->values == std::vector<std::string>{"Generic PLA @Q2", "Generic PETG @Q2"});
CHECK(full.option<ConfigOptionStrings>("filament_type")->values == std::vector<std::string>{"PLA", "PETG"});
// The AMS sync matches trays by filament_id, so this array must follow the same order.
CHECK(full.option<ConfigOptionStrings>("filament_ids")->values == std::vector<std::string>{"GFL99", "OFYPdQJh"});
}
// get_ams_cobox_infos builds the combo arrays that SyncAmsInfoDialog pairs with AMS colors
// positionally. A printer-set tray must appear so the array stays aligned with the sync output.
TEST_CASE("AMS combo info keeps a printer-set tray and stays aligned", "[Preset][AMS]")
{
PresetBundle bundle;
add_system_filament(bundle, "Generic PETG @Q2", "OFYPdQJh", "PETG");
add_system_filament(bundle, "Generic PLA @Q2", "GFL99", "PLA");
auto tray_a = make_tray("OFYPdQJh", "PETG", "#FE717A", "0", "0");
tray_a.set_key_value("tray_name", new ConfigOptionStrings{"A"});
auto tray_b = make_tray("", "PETG", "#898F9B", "0", "1");
tray_b.set_key_value("tray_name", new ConfigOptionStrings{"B"});
auto tray_c = make_tray("GFL99", "PLA", "#FAFAFA", "0", "2");
tray_c.set_key_value("tray_name", new ConfigOptionStrings{"C"});
bundle.filament_ams_list[0] = tray_a;
bundle.filament_ams_list[1] = tray_b;
bundle.filament_ams_list[2] = tray_c;
AMSComboInfo combo_info;
bundle.get_ams_cobox_infos(combo_info);
// The id-less PETG tray resolves to a generic preset instead of being dropped, so C keeps
// its index and its color.
REQUIRE(combo_info.ams_filament_presets.size() == 3);
CHECK(combo_info.ams_filament_presets[0] == "Generic PETG @Q2");
CHECK(combo_info.ams_filament_presets[1] == "Generic PETG @Q2");
CHECK(combo_info.ams_filament_presets[2] == "Generic PLA @Q2");
REQUIRE(combo_info.ams_filament_colors.size() == 3);
CHECK(combo_info.ams_filament_colors[0] == "#FE717A");
CHECK(combo_info.ams_filament_colors[1] == "#898F9B");
CHECK(combo_info.ams_filament_colors[2] == "#FAFAFA");
REQUIRE(combo_info.ams_names.size() == 3);
CHECK(combo_info.ams_names[0] == "A");
CHECK(combo_info.ams_names[1] == "B");
CHECK(combo_info.ams_names[2] == "C");
}
@@ -1604,6 +1604,23 @@ TEST_CASE("Filaments offered for a machine follow the app's compatibility rule",
}
}
// An agent-reported model id (SSDP modelNumber, manual binding) must resolve to the display
// name printer profiles use as printer_model; the AMS dialogs look filaments up by it.
TEST_CASE("Printer model ids resolve to vendor display names", "[Preset][Bundle]")
{
PresetBundle bundle;
VendorProfile qidi("Qidi");
qidi.name = "Qidi";
VendorProfile::PrinterModel model;
model.model_id = "Qidi-Q1Pro";
model.name = "Qidi Q1 Pro";
qidi.models.push_back(model);
bundle.vendors.emplace(qidi.id, qidi);
CHECK(bundle.get_printer_model_display_name("Qidi-Q1Pro") == "Qidi Q1 Pro");
CHECK(bundle.get_printer_model_display_name("unknown-model").empty());
}
namespace {

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