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/scripts/flatpak/com.orcaslicer.OrcaSlicer.metainfo.xml b/scripts/flatpak/com.orcaslicer.OrcaSlicer.metainfo.xml
index 9ec11fa930..a49f360403 100644
--- a/scripts/flatpak/com.orcaslicer.OrcaSlicer.metainfo.xml
+++ b/scripts/flatpak/com.orcaslicer.OrcaSlicer.metainfo.xml
@@ -45,8 +45,8 @@
#00695C
-
- https://github.com/OrcaSlicer/OrcaSlicer/releases/tag/nightly-builds
+
+ https://github.com/OrcaSlicer/OrcaSlicer/releases/tag/2.4.0-alpha
See the release page for detailed changelog.
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 c283ecbad6..970274dc6f 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-dev")
+set(SoftFever_VERSION "2.4.0-alpha-kx3")
string(REGEX MATCH "^([0-9]+)\\.([0-9]+)\\.([0-9]+)"
SoftFever_VERSION_MATCH ${SoftFever_VERSION})
set(ORCA_VERSION_MAJOR ${CMAKE_MATCH_1})