diff --git a/docs/filament-preset-bridge-guide.md b/docs/filament-preset-bridge-guide.md new file mode 100644 index 0000000000..c2933f7b33 --- /dev/null +++ b/docs/filament-preset-bridge-guide.md @@ -0,0 +1,356 @@ +# Eigene Filament-Presets anlegen, prüfen und mit KX-Bridge verknüpfen + +> **Gilt für:** OrcaSlicer-KX v2.4.0-alpha-kx2 oder neuer + +--- + +## Was ist die `filament_id` und warum ist sie wichtig? + +Jedes Filament-Preset in OrcaSlicer hat eine interne `filament_id`. Diese ID wird von der KX-Bridge genutzt, um beim AMS-Sync das richtige Preset zuzuordnen. + +- System-Presets (z.B. "Polymaker PolyTerra PLA") haben eine feste ID wie `GFL99` oder `OGFL04`. +- **Eigene (User-)Presets** bekommen in OrcaSlicer-KX automatisch eine eindeutige ID, die mit `P` beginnt (z.B. `P3a7f2c1`). + +Ohne eindeutige ID zeigt OrcaSlicer beim Sync immer "Generic PLA" — auch wenn das Preset existiert. + +> **Achtung — abgeleitete Presets (Issue #52):** Wenn du dein Preset von einem **Hersteller-Preset** ableitest (z.B. "Anycubic PLA Matte"), übernimmt es zunächst die feste Hersteller-ID (z.B. `GFA001`). Beim Sync wird dann fälschlicherweise das Hersteller-Preset statt deines eigenen ausgewählt. +> Ab OrcaSlicer-KX **v2.4.0-alpha-kx3** wird beim Speichern automatisch eine eigene `P...`-ID vergeben — auch für abgeleitete Presets. Hast du das Preset mit einer älteren Version angelegt, **öffne es einmal und speichere es erneut** (Save), damit die `P`-ID generiert wird. + +--- + +## 1. Eigenes Filament-Preset anlegen + +1. OrcaSlicer-KX starten +2. Rechts oben im **Filament-Dropdown** ein passendes Basis-Preset wählen (z.B. "Generic PLA" oder ein Hersteller-Preset) +3. Einstellungen nach Wunsch anpassen (Temperaturen, Kühlung, etc.) +4. Auf das **Speichern-Symbol** (Diskette) klicken → **"Save as new preset"** +5. Namen eingeben — z.B. `SUNLU PLA+ 2.0` + > Der Name muss später exakt so in der Bridge eingetragen werden. +6. Drucker auswählen: **Anycubic Kobra X 0.4 nozzle** — wichtig für die Kompatibilität! +7. **Speichern** klicken +8. OrcaSlicer **einmal neu starten** — erst dann wird die `filament_id` dauerhaft gespeichert. + +--- + +## 2. Eindeutige ID prüfen + +Nach dem Neustart prüfen, ob die ID korrekt gesetzt wurde: + +**Windows:** +``` +%APPDATA%\OrcaSlicer\user\default\filament\SUNLU PLA+ 2.0.json +``` + +**Linux:** +``` +~/.config/OrcaSlicer/user/default/filament/SUNLU PLA+ 2.0.json +``` + +Die Datei öffnen und nach `filament_id` suchen: + +```json +{ + "filament_id": "P3a7f2c1", + ... +} +``` + +✅ Korrekt: ID beginnt mit `P` gefolgt von 7 Hex-Zeichen +❌ Fehlt oder leer: OrcaSlicer-KX zu alt — Update auf v2.4.0-alpha-kx2 oder neuer + +--- + +## 3. Preset auf einen anderen PC übertragen (Import) + +### Exportieren (Quell-PC) + +Die Preset-Datei einfach kopieren: + +**Windows:** +``` +%APPDATA%\OrcaSlicer\user\default\filament\SUNLU PLA+ 2.0.json +``` + +**Linux:** +``` +~/.config/OrcaSlicer/user/default/filament/SUNLU PLA+ 2.0.json +``` + +### Importieren (Ziel-PC) + +**Methode A — Datei direkt kopieren:** +1. Die `.json`-Datei in das gleiche Verzeichnis auf dem Ziel-PC kopieren +2. OrcaSlicer neu starten → Preset erscheint im Dropdown + +**Methode B — OrcaSlicer Import-Funktion:** +1. In OrcaSlicer: **File → Import → Import Configs...** +2. Die `.json`-Datei auswählen +3. OrcaSlicer neu starten + +> **Wichtig:** Die `filament_id` in der Datei bleibt erhalten — das Preset wird auf dem Ziel-PC genauso erkannt wie auf dem Quell-PC. + +--- + +## 4. Preset in KX-Bridge verknüpfen + +1. KX-Bridge UI öffnen +2. **Filament-Verwaltung** → AMS-Slot auswählen +3. Im Feld **Filament-Name** exakt den OrcaSlicer-Preset-Namen eintragen: + ``` + SUNLU PLA+ 2.0 + ``` +4. Speichern + +Die Bridge sendet beim Sync `filament_name: "SUNLU PLA+ 2.0"` → OrcaSlicer findet das Preset anhand von Name und `filament_id` → zeigt es korrekt an. + +--- + +## Wichtige Hinweise + +| Was | Warum | +|-----|-------| +| Name in OrcaSlicer und Bridge müssen **exakt** übereinstimmen | Groß-/Kleinschreibung und Sonderzeichen werden verglichen | +| Preset muss für **Anycubic Kobra X 0.4 nozzle** kompatibel sein | Beim Speichern den richtigen Drucker auswählen | +| Nach dem ersten Speichern OrcaSlicer **neu starten** | Erst dann wird die `filament_id` persistent geschrieben | +| **OrcaSlicer-KX v2.4.0-alpha-kx2** oder neuer verwenden | Ältere Versionen generieren keine eindeutige `filament_id` für User-Presets | +| Bei von Hersteller-Presets abgeleiteten Presets: **v2.4.0-alpha-kx3** oder neuer | Erst ab dieser Version wird die geerbte Hersteller-ID beim Speichern durch eine eigene `P`-ID ersetzt (Issue #52) | + +--- +--- + +# How to Create, Verify and Import Custom Filament Presets for KX-Bridge + +> **Requires:** OrcaSlicer-KX v2.4.0-alpha-kx2 or newer + +--- + +## What is the `filament_id` and why does it matter? + +Every filament preset in OrcaSlicer has an internal `filament_id`. The KX-Bridge uses this ID to match the correct preset during AMS sync. + +- System presets (e.g. "Polymaker PolyTerra PLA") have a fixed ID like `GFL99` or `OGFL04`. +- **Custom (user) presets** automatically receive a unique ID starting with `P` (e.g. `P3a7f2c1`) in OrcaSlicer-KX. + +Without a unique ID, OrcaSlicer will always show "Generic PLA" during sync — even if the preset exists. + +> **Caution — derived presets (Issue #52):** If you derive your preset from a **vendor preset** (e.g. "Anycubic PLA Matte"), it initially inherits the fixed vendor ID (e.g. `GFA001`). During sync the vendor preset is then incorrectly selected instead of your own. +> As of OrcaSlicer-KX **v2.4.0-alpha-kx3**, a unique `P...` ID is generated automatically on save — including for derived presets. If you created the preset with an older version, **open it once and save it again** so the `P` ID gets generated. + +--- + +## 1. Create a Custom Filament Preset + +1. Launch OrcaSlicer-KX +2. Select a suitable base preset from the **filament dropdown** (e.g. "Generic PLA" or a vendor preset) +3. Adjust settings as needed (temperatures, cooling, etc.) +4. Click the **save icon** (floppy disk) → **"Save as new preset"** +5. Enter a name — e.g. `SUNLU PLA+ 2.0` + > This name must be entered in the bridge exactly as typed here. +6. Select printer: **Anycubic Kobra X 0.4 nozzle** — required for compatibility! +7. Click **Save** +8. **Restart OrcaSlicer once** — the `filament_id` is only written permanently after a restart. + +--- + +## 2. Verify the Unique ID + +After restarting, check that the ID was set correctly: + +**Windows:** +``` +%APPDATA%\OrcaSlicer\user\default\filament\SUNLU PLA+ 2.0.json +``` + +**Linux:** +``` +~/.config/OrcaSlicer/user/default/filament/SUNLU PLA+ 2.0.json +``` + +Open the file and look for `filament_id`: + +```json +{ + "filament_id": "P3a7f2c1", + ... +} +``` + +✅ Correct: ID starts with `P` followed by 7 hex characters +❌ Missing or empty: Your OrcaSlicer-KX version is too old — update to v2.4.0-alpha-kx2 or newer + +--- + +## 3. Transfer a Preset to Another PC (Import) + +### Export (source PC) + +Simply copy the preset file: + +**Windows:** +``` +%APPDATA%\OrcaSlicer\user\default\filament\SUNLU PLA+ 2.0.json +``` + +**Linux:** +``` +~/.config/OrcaSlicer/user/default/filament/SUNLU PLA+ 2.0.json +``` + +### Import (target PC) + +**Method A — Copy file directly:** +1. Copy the `.json` file to the same directory on the target PC +2. Restart OrcaSlicer → preset appears in the dropdown + +**Method B — OrcaSlicer import function:** +1. In OrcaSlicer: **File → Import → Import Configs...** +2. Select the `.json` file +3. Restart OrcaSlicer + +> **Note:** The `filament_id` inside the file is preserved — the preset will be recognized on the target PC exactly as on the source PC. + +--- + +## 4. Link the Preset in KX-Bridge + +1. Open the KX-Bridge UI +2. Go to **Filament Management** → select the AMS slot +3. In the **Filament Name** field, enter the OrcaSlicer preset name exactly: + ``` + SUNLU PLA+ 2.0 + ``` +4. Save + +The bridge sends `filament_name: "SUNLU PLA+ 2.0"` during sync → OrcaSlicer matches by name and `filament_id` → displays the preset correctly. + +--- + +## Quick Reference + +| What | Why | +|------|-----| +| Name in OrcaSlicer and Bridge must match **exactly** | Case and special characters are compared | +| Preset must be compatible with **Anycubic Kobra X 0.4 nozzle** | Select the correct printer when saving | +| **Restart OrcaSlicer** after saving for the first time | The `filament_id` is only written persistently after a restart | +| Use **OrcaSlicer-KX v2.4.0-alpha-kx2** or newer | Older versions do not generate a unique `filament_id` for user presets | +| For presets derived from vendor presets: **v2.4.0-alpha-kx3** or newer | Only from this version is the inherited vendor ID replaced with a unique `P` ID on save (Issue #52) | + +--- +--- + +# Cómo crear, verificar e importar perfiles de filamento personalizados para KX-Bridge + +> **Requiere:** OrcaSlicer-KX v2.4.0-alpha-kx2 o superior + +--- + +## ¿Qué es el `filament_id` y por qué es importante? + +Cada perfil de filamento en OrcaSlicer tiene un `filament_id` interno. KX-Bridge usa este ID para asignar el perfil correcto durante la sincronización AMS. + +- Los perfiles del sistema (p. ej. "Polymaker PolyTerra PLA") tienen un ID fijo como `GFL99` o `OGFL04`. +- Los **perfiles personalizados (usuario)** reciben automáticamente un ID único que empieza por `P` (p. ej. `P3a7f2c1`) en OrcaSlicer-KX. + +Sin un ID único, OrcaSlicer mostrará siempre "Generic PLA" durante la sincronización, aunque el perfil exista. + +> **Atención — perfiles derivados (Issue #52):** Si derivas tu perfil de un **perfil de fabricante** (p. ej. "Anycubic PLA Matte"), inicialmente hereda el ID fijo del fabricante (p. ej. `GFA001`). Durante la sincronización se selecciona entonces por error el perfil del fabricante en lugar del tuyo. +> A partir de OrcaSlicer-KX **v2.4.0-alpha-kx3**, se genera automáticamente un ID `P...` único al guardar — también para perfiles derivados. Si creaste el perfil con una versión anterior, **ábrelo una vez y vuelve a guardarlo** (Save) para que se genere el ID `P`. + +--- + +## 1. Crear un perfil de filamento personalizado + +1. Iniciar OrcaSlicer-KX +2. Seleccionar un perfil base adecuado en el **menú desplegable de filamento** (p. ej. "Generic PLA" o un perfil de fabricante) +3. Ajustar la configuración según sea necesario (temperaturas, refrigeración, etc.) +4. Hacer clic en el **icono de guardar** (disquete) → **"Save as new preset"** +5. Introducir un nombre — p. ej. `SUNLU PLA+ 2.0` + > Este nombre debe introducirse en la bridge exactamente igual. +6. Seleccionar impresora: **Anycubic Kobra X 0.4 nozzle** — ¡necesario para la compatibilidad! +7. Hacer clic en **Guardar** +8. **Reiniciar OrcaSlicer una vez** — el `filament_id` solo se escribe de forma permanente tras un reinicio. + +--- + +## 2. Verificar el ID único + +Tras reiniciar, comprobar que el ID se ha establecido correctamente: + +**Windows:** +``` +%APPDATA%\OrcaSlicer\user\default\filament\SUNLU PLA+ 2.0.json +``` + +**Linux:** +``` +~/.config/OrcaSlicer/user/default/filament/SUNLU PLA+ 2.0.json +``` + +Abrir el archivo y buscar `filament_id`: + +```json +{ + "filament_id": "P3a7f2c1", + ... +} +``` + +✅ Correcto: el ID empieza por `P` seguido de 7 caracteres hexadecimales +❌ Falta o está vacío: la versión de OrcaSlicer-KX es demasiado antigua — actualizar a v2.4.0-alpha-kx2 o superior + +--- + +## 3. Transferir un perfil a otro PC (importar) + +### Exportar (PC de origen) + +Simplemente copiar el archivo del perfil: + +**Windows:** +``` +%APPDATA%\OrcaSlicer\user\default\filament\SUNLU PLA+ 2.0.json +``` + +**Linux:** +``` +~/.config/OrcaSlicer/user/default/filament/SUNLU PLA+ 2.0.json +``` + +### Importar (PC de destino) + +**Método A — Copiar el archivo directamente:** +1. Copiar el archivo `.json` al mismo directorio en el PC de destino +2. Reiniciar OrcaSlicer → el perfil aparece en el menú desplegable + +**Método B — Función de importación de OrcaSlicer:** +1. En OrcaSlicer: **File → Import → Import Configs...** +2. Seleccionar el archivo `.json` +3. Reiniciar OrcaSlicer + +> **Nota:** El `filament_id` dentro del archivo se conserva — el perfil se reconocerá en el PC de destino exactamente igual que en el de origen. + +--- + +## 4. Vincular el perfil en KX-Bridge + +1. Abrir la interfaz de KX-Bridge +2. Ir a **Gestión de filamentos** → seleccionar la ranura AMS +3. En el campo **Nombre de filamento**, introducir el nombre exacto del perfil de OrcaSlicer: + ``` + SUNLU PLA+ 2.0 + ``` +4. Guardar + +La bridge envía `filament_name: "SUNLU PLA+ 2.0"` durante la sincronización → OrcaSlicer busca por nombre y `filament_id` → muestra el perfil correctamente. + +--- + +## Referencia rápida + +| Qué | Por qué | +|-----|---------| +| El nombre en OrcaSlicer y en Bridge debe coincidir **exactamente** | Se comparan mayúsculas, minúsculas y caracteres especiales | +| El perfil debe ser compatible con **Anycubic Kobra X 0.4 nozzle** | Seleccionar la impresora correcta al guardar | +| **Reiniciar OrcaSlicer** tras guardar por primera vez | El `filament_id` solo se escribe de forma permanente tras un reinicio | +| Usar **OrcaSlicer-KX v2.4.0-alpha-kx2** o superior | Las versiones anteriores no generan un `filament_id` único para perfiles de usuario | +| Para perfiles derivados de perfiles de fabricante: **v2.4.0-alpha-kx3** o superior | Solo a partir de esta versión se reemplaza el ID heredado del fabricante por un ID `P` único al guardar (Issue #52) | diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index 9fbfe906c3..9653af685a 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -41,6 +41,8 @@ #include #include #include +#include +#include #include "libslic3r.h" #include "Utils.hpp" @@ -636,6 +638,31 @@ void Preset::save(DynamicPrintConfig* parent_config) //BBS: add project embedded preset logic if (this->is_project_embedded) return; + + // Generate a unique filament_id for user filament presets that don't have one yet. + // Inherited presets (e.g. "My PLA" inheriting "Generic PLA @System") previously had + // no filament_id which caused AMS sync to fall back to the parent's Generic ID. + // Also generate a new ID if filament_id is inherited from the parent (== base_id). + // This happens when a user preset is saved for the first time without having its own ID. + // A user preset needs its own filament_id if: + // - it has no filament_id at all, OR + // - its filament_id does not start with "P" (user preset IDs start with "P", + // system IDs start with GFL/OGFL/GFA/etc.) + bool needs_unique_filament_id = this->is_user() && !this->name.empty() && + this->type == Preset::TYPE_FILAMENT && + (this->filament_id.empty() || this->filament_id.front() != 'P'); + if (needs_unique_filament_id) { + boost::uuids::detail::md5 hash; + boost::uuids::detail::md5::digest_type digest; + hash.process_bytes(this->name.data(), this->name.size()); + hash.get_digest(digest); + const auto char_digest = reinterpret_cast(&digest); + std::string result; + boost::algorithm::hex(char_digest, char_digest + sizeof(boost::uuids::detail::md5::digest_type), std::back_inserter(result)); + this->filament_id = "P" + result.substr(0, 7); + BOOST_LOG_TRIVIAL(info) << "Preset::save: generated filament_id='" << this->filament_id << "' for user preset '" << this->name << "'"; + } + //BBS: change to json format //this->config.save(this->file); std::string from_str; @@ -691,6 +718,8 @@ void Preset::save(DynamicPrintConfig* parent_config) opt_dst->set(opt_src); } } + if (!filament_id.empty()) + temp_config.set_key_value(BBL_JSON_KEY_FILAMENT_ID, new ConfigOptionString(filament_id)); temp_config.save_to_json(this->file, bare_name, from_str, this->version.to_string()); } else if (!filament_id.empty() && inherits().empty()) { DynamicPrintConfig temp_config = config; @@ -1643,7 +1672,10 @@ void PresetCollection::load_presets( const Preset& default_preset = this->default_preset_for(config); if (inherit_preset) { preset.config = inherit_preset->config; - preset.filament_id = inherit_preset->filament_id; + // Only inherit filament_id from parent if this preset has no own ID in JSON. + // User presets with a P-prefix ID (generated by Preset::save) must keep their own ID. + if (preset.filament_id.empty()) + preset.filament_id = inherit_preset->filament_id; extend_default_config_length(config, false, {}); preset.config.update_diff_values_to_child_config(config, extruder_id_name, extruder_variant_name, *key_set1, *key_set2); } @@ -2825,8 +2857,20 @@ void PresetCollection::save_current_preset(const std::string &new_name, bool det if (m_type == Preset::TYPE_PRINT) preset.config.option("print_settings_id", true)->value = new_name; - else if (m_type == Preset::TYPE_FILAMENT) + else if (m_type == Preset::TYPE_FILAMENT) { preset.config.option("filament_settings_id", true)->values[0] = new_name; + // Generate a unique filament_id for user presets that don't have one yet (PR #13315). + if (preset.filament_id.empty() || preset.filament_id.front() != 'P') { + boost::uuids::detail::md5 hash; + boost::uuids::detail::md5::digest_type digest; + hash.process_bytes(new_name.data(), new_name.size()); + hash.get_digest(digest); + const auto char_digest = reinterpret_cast(&digest); + std::string result; + boost::algorithm::hex(char_digest, char_digest + sizeof(boost::uuids::detail::md5::digest_type), std::back_inserter(result)); + preset.filament_id = "P" + result.substr(0, 7); + } + } else if (m_type == Preset::TYPE_PRINTER) preset.config.option("printer_settings_id", true)->value = new_name; final_inherits = preset.inherits(); diff --git a/src/libslic3r/PresetBundle.cpp b/src/libslic3r/PresetBundle.cpp index 1ace4db685..a78dac2748 100644 --- a/src/libslic3r/PresetBundle.cpp +++ b/src/libslic3r/PresetBundle.cpp @@ -3211,9 +3211,28 @@ unsigned int PresetBundle::sync_ams_list(std::vectorAdd( 0, 0, 0, wxTOP, FromDIP(33)); bool is_zh = wxGetApp().app_config->get("language") == "zh_CN"; diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index c6a5ac3aba..8b8a7e7434 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -3293,11 +3293,13 @@ std::map Sidebar::build_filament_ams_list(MachineObject tray_config.set_key_value("slot_id", new ConfigOptionStrings{slot_id}); tray_config.set_key_value("filament_type", new ConfigOptionStrings{tray.m_fila_type}); tray_config.set_key_value("tray_name", new ConfigOptionStrings{ name }); - tray_config.set_key_value("filament_colour", new ConfigOptionStrings{into_u8(wxColour("#" + tray.color).GetAsString(wxC2S_HTML_SYNTAX))}); + const std::string filament_color = into_u8(wxColour("#" + tray.color).GetAsString(wxC2S_HTML_SYNTAX)); + tray_config.set_key_value("filament_colour", new ConfigOptionStrings{filament_color}); tray_config.set_key_value("filament_multi_colour", new ConfigOptionStrings{}); tray_config.set_key_value("filament_colour_type", new ConfigOptionStrings{std::to_string(tray.ctype)}); tray_config.set_key_value("filament_exist", new ConfigOptionBools{tray.is_exists}); tray_config.set_key_value("filament_slot_placeholder", new ConfigOptionBools{tray.is_slot_placeholder}); + tray_config.set_key_value("filament_sub_brands", new ConfigOptionStrings{tray.sub_brands}); std::optional info; if (wxGetApp().preset_bundle) { info = wxGetApp().preset_bundle->get_filament_by_filament_id(tray.setting_id); @@ -3306,12 +3308,19 @@ std::map Sidebar::build_filament_ams_list(MachineObject for (int i = 0; i < tray.cols.size(); ++i) { tray_config.opt("filament_multi_colour")->values.push_back(into_u8(wxColour("#" + tray.cols[i]).GetAsString(wxC2S_HTML_SYNTAX))); } + if (tray_config.opt("filament_multi_colour")->values.empty() && !filament_color.empty()) { + tray_config.opt("filament_multi_colour")->values.push_back(filament_color); + } return tray_config; }; if (obj->ams_support_virtual_tray) { int extruder = 0x10000; // Main (first) extruder at right for (auto & vt_tray : obj->vt_slot) { + if (!vt_tray.is_exists && vt_tray.setting_id.empty() && vt_tray.m_fila_type.empty() && vt_tray.color.empty()) { + extruder = 0; + continue; + } filament_ams_list.emplace(extruder + stoi(vt_tray.id), build_tray_config(vt_tray, "Ext",vt_tray.id, "0"));//254 or 255 extruder = 0; } @@ -3468,7 +3477,9 @@ void Sidebar::sync_ams_list(bool is_from_big_sync_btn) p->plater->pop_warning_and_go_to_device_page("", Plater::PrinterWarningType::EMPTY_FILAMENT, _L("Sync printer information")); return; } - if (!wxGetApp().plater()->is_same_printer_for_connected_and_selected()) { + auto* agent = wxGetApp().getDeviceManager()->get_agent(); + const bool direct_pull_sync = agent && agent->get_filament_sync_mode() == FilamentSyncMode::pull; + if (!direct_pull_sync && !wxGetApp().plater()->is_same_printer_for_connected_and_selected()) { return; } std::string ams_filament_ids = wxGetApp().app_config->get("ams_filament_ids", p->ams_list_device); @@ -3476,31 +3487,39 @@ void Sidebar::sync_ams_list(bool is_from_big_sync_btn) if (!ams_filament_ids.empty()) { boost::algorithm::split(list2, ams_filament_ids, boost::algorithm::is_any_of(",")); } - wxGetApp().plater()->update_all_plate_thumbnails(true);//preview thumbnail for sync_dlg - SyncAmsInfoDialog::SyncInfo temp_info; - temp_info.use_dialog_pos = false; - temp_info.cancel_text_to_later = is_from_big_sync_btn; - if (m_sync_dlg == nullptr) { - m_sync_dlg = new SyncAmsInfoDialog(this, temp_info); + SyncAmsInfoDialog::SyncResult sync_result; + int dlg_res{(int) wxID_YES}; + if (direct_pull_sync) { + sync_result.direct_sync = true; + sync_result.is_same_printer = true; } else { - m_sync_dlg->set_info(temp_info); - } - int dlg_res{(int) wxID_CANCEL}; - if (m_sync_dlg->is_need_show()) { - m_sync_dlg->deal_only_exist_ext_spool(obj); - if (m_sync_dlg->is_dirty_filament()) { - wxGetApp().get_tab(Preset::TYPE_FILAMENT)->select_preset(wxGetApp().preset_bundle->filament_presets[0], false, "", false, true); - wxGetApp().preset_bundle->export_selections(*wxGetApp().app_config); - dynamic_filament_list.update(); + wxGetApp().plater()->update_all_plate_thumbnails(true);//preview thumbnail for sync_dlg + SyncAmsInfoDialog::SyncInfo temp_info; + temp_info.use_dialog_pos = false; + temp_info.cancel_text_to_later = is_from_big_sync_btn; + if (m_sync_dlg == nullptr) { + m_sync_dlg = new SyncAmsInfoDialog(this, temp_info); + } else { + m_sync_dlg->set_info(temp_info); + } + dlg_res = (int) wxID_CANCEL; + if (m_sync_dlg->is_need_show()) { + m_sync_dlg->deal_only_exist_ext_spool(obj); + if (m_sync_dlg->is_dirty_filament()) { + wxGetApp().get_tab(Preset::TYPE_FILAMENT)->select_preset(wxGetApp().preset_bundle->filament_presets[0], false, "", false, true); + wxGetApp().preset_bundle->export_selections(*wxGetApp().app_config); + dynamic_filament_list.update(); + } + m_sync_dlg->set_check_dirty_fialment(false); + dlg_res = m_sync_dlg->ShowModal(); + } else { + dlg_res =(int) wxID_YES; } - m_sync_dlg->set_check_dirty_fialment(false); - dlg_res = m_sync_dlg->ShowModal(); - } else { - dlg_res =(int) wxID_YES; } if (dlg_res == wxID_CANCEL) return; - auto sync_result = m_sync_dlg->get_result(); + if (!direct_pull_sync) + sync_result = m_sync_dlg->get_result(); if (!sync_result.is_same_printer) { BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << "check error: sync_result.is_same_printer value is false"; return; diff --git a/src/slic3r/GUI/PresetComboBoxes.cpp b/src/slic3r/GUI/PresetComboBoxes.cpp index edec9f903d..fd26eab012 100644 --- a/src/slic3r/GUI/PresetComboBoxes.cpp +++ b/src/slic3r/GUI/PresetComboBoxes.cpp @@ -267,6 +267,11 @@ int PresetComboBox::update_ams_color() auto color_pack = static_cast(cfg->option("filament_multi_colour")->clone()); // multi color (all colors in all kinds of filament) auto color_type = static_cast(cfg->option("filament_colour_type")->clone()); // color type + if (m_filament_idx >= color_head->values.size()) color_head->values.resize(m_filament_idx + 1); + if (m_filament_idx >= color_pack->values.size()) color_pack->values.resize(m_filament_idx + 1); + if (m_filament_idx >= color_type->values.size()) color_type->values.resize(m_filament_idx + 1); + if (ctype.empty()) ctype = "1"; + color_head->values[m_filament_idx] = color; color_type->values[m_filament_idx] = ctype; std::string color_str = ""; // Translate multi color info to config storage format diff --git a/src/slic3r/Utils/MoonrakerPrinterAgent.cpp b/src/slic3r/Utils/MoonrakerPrinterAgent.cpp index f1d892134b..6ff593a16e 100644 --- a/src/slic3r/Utils/MoonrakerPrinterAgent.cpp +++ b/src/slic3r/Utils/MoonrakerPrinterAgent.cpp @@ -4,6 +4,7 @@ #include "libslic3r/PresetBundle.hpp" #include "slic3r/GUI/GUI_App.hpp" #include "slic3r/GUI/DeviceCore/DevFilaSystem.h" +#include "slic3r/GUI/DeviceCore/DevExtruderSystem.h" #include "slic3r/GUI/DeviceCore/DevManager.h" #include "../GUI/DeviceCore/DevStorage.h" #include "../GUI/DeviceCore/DevFirmware.h" @@ -88,6 +89,160 @@ std::string map_moonraker_state(std::string state) return "IDLE"; } +std::string normalize_filament_name_for_match(const std::string& input) +{ + std::string normalized = input; + boost::trim(normalized); + // Ignore profile suffixes like " @0.4 nozzle" for name matching. + if (const auto suffix_pos = normalized.find(" @"); suffix_pos != std::string::npos) { + normalized = normalized.substr(0, suffix_pos); + } + // Remove non-name symbols (e.g. trademark signs) while preserving separators + // commonly used in filament names. + std::string cleaned; + cleaned.reserve(normalized.size()); + for (unsigned char c : normalized) { + if (std::isalnum(c) || c == '-' || c == '+' || c == '/' || std::isspace(c)) { + cleaned.push_back(static_cast(std::toupper(c))); + } else { + cleaned.push_back(' '); + } + } + + // Collapse repeated whitespace. + std::string collapsed; + collapsed.reserve(cleaned.size()); + bool prev_space = true; + for (unsigned char c : cleaned) { + if (std::isspace(c)) { + if (!prev_space) { + collapsed.push_back(' '); + } + prev_space = true; + } else { + collapsed.push_back(static_cast(c)); + prev_space = false; + } + } + boost::trim(collapsed); + return collapsed; +} + +bool filament_name_match_relaxed(const std::string& wanted, const std::string& candidate) +{ + if (wanted == candidate) { + return true; + } + + // Allow lane names with trailing color descriptors, e.g.: + // "ELEGOO RAPID PETG GREY" -> "ELEGOO RAPID PETG". + if (!candidate.empty() && boost::starts_with(wanted, candidate + " ")) { + return true; + } + return false; +} + +std::vector vendor_match_candidates(std::string vendor) +{ + std::vector candidates; + boost::trim(vendor); + if (vendor.empty()) { + return candidates; + } + + candidates.push_back(vendor); + + // Also try first token (e.g. "Bambu Lab" -> "Bambu") without hardcoded aliases. + const auto first_space = vendor.find_first_of(" \t"); + if (first_space != std::string::npos) { + std::string first = vendor.substr(0, first_space); + boost::trim(first); + if (!first.empty() && !boost::iequals(first, vendor)) { + candidates.push_back(first); + } + } + return candidates; +} + +std::string filament_id_by_name(const Slic3r::PresetCollection& filaments, + const std::string& filament_name, + const std::vector& vendor_filters = {}) +{ + if (filament_name.empty()) { + BOOST_LOG_TRIVIAL(debug) << "MoonrakerPrinterAgent: filament matcher received empty filament name"; + return ""; + } + + const std::string wanted = normalize_filament_name_for_match(filament_name); + std::vector normalized_vendor_filters; + normalized_vendor_filters.reserve(vendor_filters.size()); + for (const auto& vendor_filter : vendor_filters) { + const std::string normalized_vendor = normalize_filament_name_for_match(vendor_filter); + if (!normalized_vendor.empty()) { + normalized_vendor_filters.push_back(normalized_vendor); + } + } + + BOOST_LOG_TRIVIAL(debug) << "MoonrakerPrinterAgent: filament matcher lookup requested='" << filament_name + << "' normalized='" << wanted << "' vendor_filters=" << normalized_vendor_filters.size(); + + // Two-pass search: Pass 1 = compatible presets only (ideal), Pass 2 = all visible presets + // (fallback for vendors like eSUN/Eryone that have no printer-specific Kobra X profile). + for (int pass = 1; pass <= 3; ++pass) { + for (size_t i = 0; i < filaments.size(); ++i) { + const auto& preset = filaments.preset(i); + // Pass 1: compatible + visible only + // Pass 2: visible but not necessarily compatible (vendors without printer-specific profile) + // Pass 3: any preset including invisible (vendors not installed as printer) + if (pass <= 2 && !preset.is_visible) { + continue; + } + // User presets (created via "Save As") may have no filament_id yet. + // We still match them by name and use their name as the identifier. + const std::string preset_id = preset.filament_id.empty() ? preset.name : preset.filament_id; + if (pass == 1 && !preset.is_compatible) { + continue; // Pass 1: only compatible presets + } + if (!normalized_vendor_filters.empty()) { + const std::string preset_vendor = normalize_filament_name_for_match(preset.config.opt_string("filament_vendor", 0u)); + bool vendor_match = false; + for (const auto& vendor_filter : normalized_vendor_filters) { + if (preset_vendor == vendor_filter) { + vendor_match = true; + break; + } + } + if (!vendor_match) { + if (pass == 1) { + BOOST_LOG_TRIVIAL(debug) << "MoonrakerPrinterAgent: filament matcher skip preset='" << preset.name + << "' reason=vendor_filter_miss preset_vendor='" << preset_vendor << "'"; + } + continue; + } + } + const std::string candidate = normalize_filament_name_for_match(preset.name); + BOOST_LOG_TRIVIAL(debug) << "MoonrakerPrinterAgent: filament matcher compare (pass=" << pass << ") preset='" << preset.name + << "' normalized='" << candidate << "' preset_id='" << preset_id << "'"; + if (filament_name_match_relaxed(wanted, candidate)) { + BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent: filament matcher matched (pass=" << pass << ") requested='" << filament_name + << "' normalized='" << wanted << "' to preset='" << preset.name + << "' preset_id='" << preset_id << "'"; + return preset_id; + } + } + if (pass == 1) { + BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent: filament matcher pass 1 (compatible) found no match for '" + << filament_name << "', trying pass 2 (all visible)"; + } else if (pass == 2) { + BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent: filament matcher pass 2 (all visible) found no match for '" + << filament_name << "', trying pass 3 (all presets incl. invisible)"; + } + } + BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent: filament matcher found no match for requested='" << filament_name + << "' normalized='" << wanted << "'"; + return ""; +} + } // namespace namespace Slic3r { @@ -448,7 +603,7 @@ int MoonrakerPrinterAgent::set_queue_on_main_fn(QueueOnMainFn fn) return BAMBU_NETWORK_SUCCESS; } -void MoonrakerPrinterAgent::build_ams_payload(int ams_count, int max_lane_index, const std::vector& trays) +void MoonrakerPrinterAgent::build_ams_payload(int ams_count, int max_lane_index, const std::vector& trays, int active_lane_index) { // Look up MachineObject via DeviceManager @@ -499,6 +654,7 @@ void MoonrakerPrinterAgent::build_ams_payload(int ams_count, int max_lane_index, tray_json["tray_info_idx"] = tray->tray_info_idx; tray_json["tray_type"] = tray->tray_type; + tray_json["tray_sub_brands"] = tray->tray_sub_brands.empty() ? tray->tray_type : tray->tray_sub_brands; tray_json["tray_color"] = normalize_color_value(tray->tray_color); // Add temperature data if provided @@ -530,6 +686,11 @@ void MoonrakerPrinterAgent::build_ams_payload(int ams_count, int max_lane_index, ams_json["ams"] = ams_array; ams_json["ams_exist_bits"] = ams_exist_ss.str(); ams_json["tray_exist_bits"] = tray_exist_ss.str(); + if (active_lane_index >= 0) { + const std::string active_lane = std::to_string(active_lane_index); + ams_json["tray_now"] = active_lane; + ams_json["tray_tar"] = active_lane; + } // Wrap in the expected structure for ParseV1_0 nlohmann::json print_json = nlohmann::json::object(); @@ -537,6 +698,7 @@ void MoonrakerPrinterAgent::build_ams_payload(int ams_count, int max_lane_index, // Call the parser to populate DevFilaSystem DevFilaSystemParser::ParseV1_0(print_json, obj, obj->GetFilaSystem(), false); + ExtderSystemParser::ParseV1_0(print_json, obj->GetExtderSystem()); BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent::build_ams_payload: Parsed " << trays.size() << " trays"; // Set printer_type so update_sync_status() can match it against the preset's printer type. @@ -570,6 +732,7 @@ bool MoonrakerPrinterAgent::fetch_filament_info(std::string dev_id) { std::vector trays; int max_lane_index = 0; + int active_lane_index = -1; // Try Moonraker filament data (more generic, supports any filament changer // software that reports lane data to Moonraker like AFC and recent Happy @@ -578,16 +741,16 @@ bool MoonrakerPrinterAgent::fetch_filament_info(std::string dev_id) BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent::fetch_filament_info: Detected Moonraker filament system with " << (max_lane_index + 1) << " lanes"; int ams_count = (max_lane_index + 4) / 4; - build_ams_payload(ams_count, max_lane_index, trays); + build_ams_payload(ams_count, max_lane_index, trays, active_lane_index); return true; } // Attempt Happy Hare first (more widely adopted, supports more filament changers) - if (fetch_hh_filament_info(trays, max_lane_index)) { + if (fetch_hh_filament_info(trays, max_lane_index, active_lane_index)) { BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent::fetch_filament_info: Detected Happy Hare MMU with " << (max_lane_index + 1) << " gates"; int ams_count = (max_lane_index + 4) / 4; - build_ams_payload(ams_count, max_lane_index, trays); + build_ams_payload(ams_count, max_lane_index, trays, active_lane_index); return true; } @@ -801,13 +964,51 @@ bool MoonrakerPrinterAgent::fetch_moonraker_filament_data(std::vectorfilaments.filament_id_by_type(tray.tray_type) - : map_filament_type_to_generic_id(tray.tray_type); + if (bundle) { + const auto vendor_candidates = vendor_match_candidates(lane_vendor); + auto match_with_vendor_prefix = [&](const std::string& suffix) -> std::string { + if (suffix.empty()) { + return ""; + } + for (const auto& vendor_candidate : vendor_candidates) { + const std::string requested = vendor_candidate + " " + suffix; + std::string match_id = filament_id_by_name(bundle->filaments, requested, vendor_candidates); + if (!match_id.empty()) { + return match_id; + } + } + return ""; + }; + + // Prefer the most specific lane identity first, then broader vendor/material mapping. + tray.tray_info_idx = match_with_vendor_prefix(lane_name); + if (tray.tray_info_idx.empty()) { + tray.tray_info_idx = match_with_vendor_prefix(tray.tray_type); + } + if (tray.tray_info_idx.empty() && !lane_name.empty()) { + tray.tray_info_idx = filament_id_by_name(bundle->filaments, lane_name, vendor_candidates); + } + if (tray.tray_info_idx.empty()) { + tray.tray_info_idx = bundle->filaments.filament_id_by_type(tray.tray_type); + } + BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent::fetch_moonraker_filament_data: lane='" << lane_key + << "' index=" << lane_index << " material='" << tray.tray_type + << "' vendor='" << lane_vendor << "' vendor_candidates=" << vendor_candidates.size() + << "' name='" << lane_name + << "' mapped_by='preset_bundle' tray_info_idx='" << tray.tray_info_idx << "'"; + } else { + tray.tray_info_idx = map_filament_type_to_generic_id(tray.tray_type); + BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent::fetch_moonraker_filament_data: lane='" << lane_key + << "' index=" << lane_index << " material='" << tray.tray_type + << "' mapped_by='generic_fallback' tray_info_idx='" << tray.tray_info_idx << "'"; + } max_lane_index = std::max(max_lane_index, lane_index); trays.push_back(tray); @@ -822,7 +1023,7 @@ bool MoonrakerPrinterAgent::fetch_moonraker_filament_data(std::vector& trays, int& max_lane_index) +bool MoonrakerPrinterAgent::fetch_hh_filament_info(std::vector& trays, int& max_lane_index, int& active_lane_index) { // Query Happy Hare MMU status std::string url = join_url(device_info.base_url, "/printer/objects/query?mmu"); @@ -894,8 +1095,18 @@ bool MoonrakerPrinterAgent::fetch_hh_filament_info(std::vector& tra // Get arrays const auto& gate_status = mmu.contains("gate_status") ? mmu["gate_status"] : nlohmann::json::array(); const auto& gate_material = mmu.contains("gate_material") ? mmu["gate_material"] : nlohmann::json::array(); + const auto& gate_filament_name = mmu.contains("gate_filament_name") ? mmu["gate_filament_name"] : nlohmann::json::array(); const auto& gate_color = mmu.contains("gate_color") ? mmu["gate_color"] : nlohmann::json::array(); const auto& gate_temperature = mmu.contains("gate_temperature") ? mmu["gate_temperature"] : nlohmann::json::array(); + const int active_gate = safe_json_int(mmu, "gate"); + active_lane_index = active_gate; + std::string active_filament_name; + if (mmu.contains("active_filament") && mmu["active_filament"].is_object()) { + active_filament_name = safe_json_string(mmu["active_filament"], "filament_name"); + if (active_filament_name.empty()) { + active_filament_name = safe_json_string(mmu["active_filament"], "material"); + } + } if (!gate_status.is_array() || !gate_material.is_array() || !gate_color.is_array() || !gate_temperature.is_array()) { @@ -916,8 +1127,13 @@ bool MoonrakerPrinterAgent::fetch_hh_filament_info(std::vector& tra // Extract gate data std::string material = safe_array_string(gate_material, gate_idx); + std::string filament_name = safe_array_string(gate_filament_name, gate_idx); std::string color = safe_array_string(gate_color, gate_idx); int nozzle_temp = safe_array_int(gate_temperature, gate_idx); + // For the active gate, prefer active_filament from MMU state. + if (gate_idx == active_gate && !active_filament_name.empty()) { + filament_name = active_filament_name; + } // Skip if no material type (empty gate) if (material.empty()) { @@ -927,15 +1143,21 @@ bool MoonrakerPrinterAgent::fetch_hh_filament_info(std::vector& tra AmsTrayData tray; tray.slot_index = gate_idx; tray.tray_type = material; + tray.tray_sub_brands = filament_name; tray.tray_color = color; tray.nozzle_temp = nozzle_temp; tray.bed_temp = 0; // HH doesn't provide bed temp in gate arrays tray.has_filament = true; auto* bundle = GUI::wxGetApp().preset_bundle; - tray.tray_info_idx = bundle - ? bundle->filaments.filament_id_by_type(tray.tray_type) - : map_filament_type_to_generic_id(tray.tray_type); + if (bundle) { + tray.tray_info_idx = filament_id_by_name(bundle->filaments, filament_name); + if (tray.tray_info_idx.empty()) { + tray.tray_info_idx = bundle->filaments.filament_id_by_type(tray.tray_type); + } + } else { + tray.tray_info_idx = map_filament_type_to_generic_id(tray.tray_type); + } max_lane_index = std::max(max_lane_index, gate_idx); trays.push_back(tray); diff --git a/src/slic3r/Utils/MoonrakerPrinterAgent.hpp b/src/slic3r/Utils/MoonrakerPrinterAgent.hpp index a37a6c3fd6..2eeb1099df 100644 --- a/src/slic3r/Utils/MoonrakerPrinterAgent.hpp +++ b/src/slic3r/Utils/MoonrakerPrinterAgent.hpp @@ -92,14 +92,16 @@ protected: int slot_index = 0; // 0-based slot index bool has_filament = false; std::string tray_type; // Material type (e.g., "PLA", "ASA") + std::string tray_sub_brands; // Human-readable filament name std::string tray_color; // Raw color (#RRGGBB, 0xRRGGBB, or RRGGBBAA) std::string tray_info_idx; // Setting ID (optional) + std::string filament_vendor; // Vendor hint from bridge (optional, KX-Bridge sendet das) int bed_temp = 0; // Optional int nozzle_temp = 0; // Optional }; // Build ams JSON and call parser - void build_ams_payload(int ams_count, int max_lane_index, const std::vector& trays); + void build_ams_payload(int ams_count, int max_lane_index, const std::vector& trays, int active_lane_index = -1); // Methods that derived classes may need to override or access virtual bool init_device_info(std::string dev_id, std::string dev_ip, std::string username, std::string password, bool use_ssl); @@ -161,7 +163,7 @@ private: uint64_t generation); // System-specific filament fetch methods - bool fetch_hh_filament_info(std::vector& trays, int& max_lane_index); + bool fetch_hh_filament_info(std::vector& trays, int& max_lane_index, int& active_lane_index); bool fetch_moonraker_filament_data(std::vector& trays, int& max_lane_index); // JSON helper methods diff --git a/tools/verify_build.sh b/tools/verify_build.sh new file mode 100755 index 0000000000..6c653679c6 --- /dev/null +++ b/tools/verify_build.sh @@ -0,0 +1,97 @@ +#!/usr/bin/env bash +# Prüft ob ein OrcaSlicer-KX Build-Artefakt die erwarteten Strings enthält. +# Verwendung: +# ./tools/verify_build.sh linux +# ./tools/verify_build.sh windows +# +# Gibt Exit-Code 0 bei Erfolg, 1 bei Fehler zurück. + +set -euo pipefail + +PLATFORM="${1:-}" +ARTIFACT="${2:-}" + +if [[ -z "$PLATFORM" || -z "$ARTIFACT" ]]; then + echo "Verwendung: $0 " + exit 1 +fi + +if [[ ! -f "$ARTIFACT" ]]; then + echo "FEHLER: Artefakt nicht gefunden: $ARTIFACT" + exit 1 +fi + +# Versionstring aus version.inc lesen +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(dirname "$SCRIPT_DIR")" +VERSION=$(grep 'set(SoftFever_VERSION' "$REPO_ROOT/version.inc" | grep -oP '"\K[^"]+') + +if [[ -z "$VERSION" ]]; then + echo "FEHLER: Konnte VERSION nicht aus version.inc lesen" + exit 1 +fi + +echo "=== OrcaSlicer-KX Build-Verifikation ===" +echo "Plattform : $PLATFORM" +echo "Artefakt : $ARTIFACT" +echo "Erwartet : $VERSION" +echo "========================================" + +TMPDIR=$(mktemp -d) +trap "rm -rf $TMPDIR" EXIT + +CHECKS=( + "$VERSION" + "KX-Bridge" + "filament_sub_brands" +) + +BINARY="" + +if [[ "$PLATFORM" == "linux" ]]; then + echo "Extrahiere AppImage..." + chmod +x "$ARTIFACT" + cd "$TMPDIR" + "$ARTIFACT" --appimage-extract bin/orca-slicer 2>/dev/null || \ + "$ARTIFACT" --appimage-extract 2>/dev/null + BINARY=$(find "$TMPDIR/squashfs-root" -name "orca-slicer" -type f | head -1) + if [[ -z "$BINARY" ]]; then + echo "FEHLER: orca-slicer Binary nicht im AppImage gefunden" + exit 1 + fi + +elif [[ "$PLATFORM" == "windows" ]]; then + echo "Extrahiere Windows ZIP..." + unzip -q "$ARTIFACT" "OrcaSlicer.dll" -d "$TMPDIR" 2>/dev/null || true + BINARY="$TMPDIR/OrcaSlicer.dll" + if [[ ! -f "$BINARY" ]]; then + echo "FEHLER: OrcaSlicer.dll nicht im ZIP gefunden" + exit 1 + fi + +else + echo "FEHLER: Unbekannte Plattform '$PLATFORM' (erwartet: linux oder windows)" + exit 1 +fi + +echo "Binary : $BINARY ($(du -sh "$BINARY" | cut -f1))" +echo "" + +FAILED=0 +for needle in "${CHECKS[@]}"; do + if grep -qaF "$needle" "$BINARY" 2>/dev/null; then + echo " [OK] $needle" + else + echo " [FAIL] $needle — NICHT GEFUNDEN" + FAILED=1 + fi +done + +echo "" +if [[ $FAILED -eq 0 ]]; then + echo "=== VERIFIKATION ERFOLGREICH ===" + exit 0 +else + echo "=== VERIFIKATION FEHLGESCHLAGEN — Upload abgebrochen ===" + exit 1 +fi diff --git a/version.inc b/version.inc index 2aa084c894..45f7e3a962 100644 --- a/version.inc +++ b/version.inc @@ -7,7 +7,7 @@ set(SLIC3R_APP_KEY "OrcaSlicer") if(NOT DEFINED BBL_INTERNAL_TESTING) set(BBL_INTERNAL_TESTING "0") endif() -set(SoftFever_VERSION "2.4.0-beta") +set(SoftFever_VERSION "2.4.0-beta-kx3") string(REGEX MATCH "^([0-9]+)\\.([0-9]+)\\.([0-9]+)" SoftFever_VERSION_MATCH ${SoftFever_VERSION}) set(ORCA_VERSION_MAJOR ${CMAKE_MATCH_1})