From 29a826d111ea1d0d632a2d4123c7b907d33a373f Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Wed, 20 May 2026 17:28:35 +0800 Subject: [PATCH 001/135] =?UTF-8?q?fix=EF=BC=9A=E4=BF=AE=E6=94=B9=E7=BC=96?= =?UTF-8?q?=E8=AF=91=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CMakeLists.txt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index 47ed438bced..8cf6e58fd08 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,9 @@ cmake_minimum_required(VERSION 3.13) +# Allow building with CMake newer than 3.28 without failing on dependency cmake +# config files that use cmake_policy(VERSION ...3.28) range syntax +set(CMAKE_POLICY_VERSION_MINIMUM 3.28) + # Verify that your CMake version is exactly 3.31.x series or lower on windows if ( ((MSVC) OR (WIN32)) AND (${CMAKE_VERSION} VERSION_GREATER_EQUAL "4.0") ) message(FATAL_ERROR "Only cmake versions between 3.13.x and 3.31.x is supported on windows. Detected version: ${CMAKE_VERSION}") From b06f962f5bfc9a006ab7cf247c50d09dacbe0681 Mon Sep 17 00:00:00 2001 From: Joyx-SnapMaker <1906956558@qq.com> Date: Thu, 16 Apr 2026 17:01:23 +0800 Subject: [PATCH 002/135] feat: add configurable option for top cover requirement --- src/libslic3r/Preset.cpp | 3 ++- src/libslic3r/PrintConfig.cpp | 6 ++++++ src/libslic3r/PrintConfig.hpp | 1 + src/slic3r/GUI/Tab.cpp | 1 + 4 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index 67742c0cb85..54735506988 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -924,7 +924,8 @@ static std::vector s_Preset_filament_options { "graphic_effect_plate_temp", "graphic_effect_plate_temp_initial_layer", // "bed_type", //BBS:temperature_vitrification - "temperature_vitrification", "reduce_fan_stop_start_freq","dont_slow_down_outer_wall", "slow_down_for_layer_cooling", "fan_min_speed", + "temperature_vitrification", "filament_is_high_temperature", "reduce_fan_stop_start_freq", "dont_slow_down_outer_wall", + "slow_down_for_layer_cooling", "fan_min_speed", "fan_max_speed", "enable_overhang_bridge_fan", "overhang_fan_speed", "overhang_fan_threshold", "close_fan_the_first_x_layers", "full_fan_speed_layer", "fan_cooling_layer_time", "slow_down_layer_time", "slow_down_min_speed", "filament_start_gcode", "filament_end_gcode", //exhaust fan control diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index 179cae85ab4..7483fe9746b 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -2391,6 +2391,12 @@ void PrintConfigDef::init_fff_params() def->mode = comAdvanced; def->set_default_value(new ConfigOptionBools { false }); + def = this->add("filament_is_high_temperature", coBools); + def->label = L("Is high-temperature filament"); + def->tooltip = L("Indicates whether this is a high-temperature filament that requires elevated printing temperatures."); + def->mode = comSimple; + def->set_default_value(new ConfigOptionBools{false}); + // BBS def = this->add("temperature_vitrification", coInts); def->label = L("Softening temperature"); diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index 0f39d80e412..9fa6aa00590 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -1183,6 +1183,7 @@ PRINT_CONFIG_CLASS_DEFINE( ((ConfigOptionFloats, filament_cost)) ((ConfigOptionStrings, default_filament_colour)) ((ConfigOptionInts, temperature_vitrification)) //BBS + ((ConfigOptionBools, filament_is_high_temperature)) ((ConfigOptionFloats, filament_max_volumetric_speed)) ((ConfigOptionInts, required_nozzle_HRC)) // BBS diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index 994625229d5..5b6ec2b9559 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -3699,6 +3699,7 @@ void TabFilament::build() optgroup->append_single_option_line("filament_cost"); //BBS optgroup->append_single_option_line("temperature_vitrification"); + optgroup->append_single_option_line("filament_is_high_temperature"); optgroup->append_single_option_line("idle_temperature"); Line line = { L("Recommended nozzle temperature"), L("Recommended nozzle temperature range of this filament. 0 means no set") }; line.append_option(optgroup->get_option("nozzle_temperature_range_low")); From eafe4651542887b44bf842fbce2aedf5611cde75 Mon Sep 17 00:00:00 2001 From: Joyx-SnapMaker <1906956558@qq.com> Date: Tue, 21 Apr 2026 18:01:25 +0800 Subject: [PATCH 003/135] feat: add translation # Conflicts: # src/libslic3r/Preset.cpp # src/libslic3r/PrintConfig.cpp --- localization/i18n/zh_CN/Snapmaker_Orca_zh_CN.po | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/localization/i18n/zh_CN/Snapmaker_Orca_zh_CN.po b/localization/i18n/zh_CN/Snapmaker_Orca_zh_CN.po index 6cc731d73af..3450984c8f6 100644 --- a/localization/i18n/zh_CN/Snapmaker_Orca_zh_CN.po +++ b/localization/i18n/zh_CN/Snapmaker_Orca_zh_CN.po @@ -10041,6 +10041,12 @@ msgstr "支撑材料" msgid "Support material is commonly used to print supports and support interfaces." msgstr "支撑材料通常用于打印支撑体和支撑接触面" +msgid "Is high-temperature filament" +msgstr "是否为高温耗材" + +msgid "Indicates whether this is a high-temperature filament that requires elevated printing temperatures." +msgstr "指示此耗材是否为需要较高打印温度的高温耗材。" + msgid "Softening temperature" msgstr "软化温度" From 613f316cc87037fc3995350b76971c73a5402d1f Mon Sep 17 00:00:00 2001 From: Joyx-SnapMaker <1906956558@qq.com> Date: Fri, 24 Apr 2026 16:01:23 +0800 Subject: [PATCH 004/135] feat: Add the "Is It a High-Temperature Consumable" attribute to the consumable filament, and simultaneously modify the printer's initial gcode. --- resources/profiles/Snapmaker.json | 165 +----------------- .../Snapmaker PETG HF @U1 0.2 nozzle.json | 9 +- .../machine/Snapmaker U1 (0.2 nozzle).json | 4 +- .../machine/Snapmaker U1 (0.4 nozzle).json | 4 +- .../machine/Snapmaker U1 (0.6 nozzle).json | 4 +- .../machine/Snapmaker U1 (0.8 nozzle).json | 4 +- 6 files changed, 16 insertions(+), 174 deletions(-) diff --git a/resources/profiles/Snapmaker.json b/resources/profiles/Snapmaker.json index 0e4a746a8a3..d45befa9452 100644 --- a/resources/profiles/Snapmaker.json +++ b/resources/profiles/Snapmaker.json @@ -1,6 +1,6 @@ { "name": "Snapmaker", - "version": "02.02.49.02", + "version": "02.02.47.02", "force_update": "0", "description": "Snapmaker configurations", "machine_model_list": [ @@ -1521,10 +1521,6 @@ "name": "Generic PLA @base2", "sub_path": "filament/Generic PLA @base2.json" }, - { - "name": "Snapmaker PLA Wood @U1 0.4 nozzle", - "sub_path": "filament/Snapmaker PLA Wood @U1 0.4 nozzle.json" - }, { "name": "Generic PLA @U1 0.2 nozzle", "sub_path": "filament/Generic PLA @U1 0.2 nozzle.json" @@ -1739,164 +1735,9 @@ { "name": "Snapmaker PLA SnapSpeed @U1 0.8 nozzle", "sub_path": "filament/Snapmaker PLA SnapSpeed @U1 0.8 nozzle.json" - }, - { - "name": "Snapmaker TPU 90A @U1 0.6 nozzle", - "sub_path": "filament/Snapmaker TPU 90A @U1 0.6 nozzle.json" - }, - { - "name": "Snapmaker TPU 90A @U1 0.8 nozzle", - "sub_path": "filament/Snapmaker TPU 90A @U1 0.8 nozzle.json" - }, - { - "name": "Generic PC @U1 0.6 nozzle", - "sub_path": "filament/Generic PC @U1 0.6 nozzle.json" - }, - { - "name": "Generic PC @U1 0.8 nozzle", - "sub_path": "filament/Generic PC @U1 0.8 nozzle.json" - }, - { - "name": "Snapmaker TPU 95A HF @U1 0.6 nozzle", - "sub_path": "filament/Snapmaker TPU 95A HF @U1 0.6 nozzle.json" - }, - { - "name": "Snapmaker TPU 95A HF @U1 0.8 nozzle", - "sub_path": "filament/Snapmaker TPU 95A HF @U1 0.8 nozzle.json" - }, - { - "name": "Generic ABS @U1 0.2 nozzle", - "sub_path": "filament/Generic ABS @U1 0.2 nozzle.json" - }, - { - "name": "Generic ABS @U1 0.6 nozzle", - "sub_path": "filament/Generic ABS @U1 0.6 nozzle.json" - }, - { - "name": "Generic ABS @U1 0.8 nozzle", - "sub_path": "filament/Generic ABS @U1 0.8 nozzle.json" - }, - { - "name": "Snapmaker PLA-CF @U1 0.4 nozzle", - "sub_path": "filament/Snapmaker PLA-CF @U1 0.4 nozzle.json" - }, - { - "name": "Snapmaker PLA-CF @U1 0.6 nozzle", - "sub_path": "filament/Snapmaker PLA-CF @U1 0.6 nozzle.json" - }, - { - "name": "Snapmaker PLA-CF @U1 0.8 nozzle", - "sub_path": "filament/Snapmaker PLA-CF @U1 0.8 nozzle.json" - }, - { - "name": "Snapmaker PLA Wood @U1 0.6 nozzle", - "sub_path": "filament/Snapmaker PLA Wood @U1 0.6 nozzle.json" - }, - { - "name": "Snapmaker PLA Wood @U1 0.8 nozzle", - "sub_path": "filament/Snapmaker PLA Wood @U1 0.8 nozzle.json" - }, - { - "name": "Generic Support For PLA @U1 0.2 nozzle", - "sub_path": "filament/Generic Support For PLA @U1 0.2 nozzle.json" - }, - { - "name": "Generic Support For PLA @U1 0.6 nozzle", - "sub_path": "filament/Generic Support For PLA @U1 0.6 nozzle.json" - }, - { - "name": "Generic Support For PLA @U1 0.8 nozzle", - "sub_path": "filament/Generic Support For PLA @U1 0.8 nozzle.json" - }, - { - "name": "Snapmaker PLA Silk @U1 0.2 nozzle", - "sub_path": "filament/Snapmaker PLA Silk @U1 0.2 nozzle.json" - }, - { - "name": "Snapmaker PLA Silk @U1 0.6 nozzle", - "sub_path": "filament/Snapmaker PLA Silk @U1 0.6 nozzle.json" - }, - { - "name": "Snapmaker PLA Silk @U1 0.8 nozzle", - "sub_path": "filament/Snapmaker PLA Silk @U1 0.8 nozzle.json" - }, - { - "name": "Snapmaker PLA Glow @U1 base", - "sub_path": "filament/Snapmaker PLA Glow @U1 base.json" - }, - { - "name": "Snapmaker PLA Glow @U1 0.4 nozzle", - "sub_path": "filament/Snapmaker PLA Glow @U1 0.4 nozzle.json" - }, - { - "name": "Snapmaker PLA Translucent @U1 base", - "sub_path": "filament/Snapmaker PLA Translucent @U1 base.json" - }, - { - "name": "Snapmaker PLA Translucent @U1 0.2 nozzle", - "sub_path": "filament/Snapmaker PLA Translucent @U1 0.2 nozzle.json" - }, - { - "name": "Snapmaker PLA Translucent @U1 0.4 nozzle", - "sub_path": "filament/Snapmaker PLA Translucent @U1 0.4 nozzle.json" - }, - { - "name": "Snapmaker PLA Translucent @U1 0.6 nozzle", - "sub_path": "filament/Snapmaker PLA Translucent @U1 0.6 nozzle.json" - }, - { - "name": "Snapmaker PLA Translucent @U1 0.8 nozzle", - "sub_path": "filament/Snapmaker PLA Translucent @U1 0.8 nozzle.json" - }, - { - "name": "Snapmaker PETG Translucent @U1 base", - "sub_path": "filament/Snapmaker PETG Translucent @U1 base.json" - }, - { - "name": "Snapmaker PETG Translucent @U1 0.2 nozzle", - "sub_path": "filament/Snapmaker PETG Translucent @U1 0.2 nozzle.json" - }, - { - "name": "Snapmaker PETG Translucent @U1 0.4 nozzle", - "sub_path": "filament/Snapmaker PETG Translucent @U1 0.4 nozzle.json" - }, - { - "name": "Snapmaker PETG Translucent @U1 0.6 nozzle", - "sub_path": "filament/Snapmaker PETG Translucent @U1 0.6 nozzle.json" - }, - { - "name": "Snapmaker PETG Translucent @U1 0.8 nozzle", - "sub_path": "filament/Snapmaker PETG Translucent @U1 0.8 nozzle.json" - }, - { - "name": "Snapmaker PVA @U1 0.6 nozzle", - "sub_path": "filament/Snapmaker PVA @U1 0.6 nozzle.json" - }, - { - "name": "Snapmaker PVA @U1 0.8 nozzle", - "sub_path": "filament/Snapmaker PVA @U1 0.8 nozzle.json" - }, - { - "name": "Snapmaker Breakaway Support For PLA @U1 0.2 nozzle", - "sub_path": "filament/Snapmaker Breakaway Support For PLA @U1 0.2 nozzle.json" - }, - { - "name": "Snapmaker Breakaway Support For PLA @U1 0.6 nozzle", - "sub_path": "filament/Snapmaker Breakaway Support For PLA @U1 0.6 nozzle.json" - }, - { - "name": "Snapmaker Breakaway Support For PLA @U1 0.8 nozzle", - "sub_path": "filament/Snapmaker Breakaway Support For PLA @U1 0.8 nozzle.json" - }, - { - "name": "Snapmaker PLA Full Spectrum @U1 0.4 nozzle", - "sub_path": "filament/Snapmaker PLA Full Spectrum @U1 0.4 nozzle.json" - }, - { - "name": "Snapmaker Breakaway Support For PLA @U1", - "sub_path": "filament/Snapmaker Breakaway Support For PLA @U1.json" } - ], + + ], "machine_list": [ { "name": "fdm_klipper", diff --git a/resources/profiles/Snapmaker/filament/Snapmaker PETG HF @U1 0.2 nozzle.json b/resources/profiles/Snapmaker/filament/Snapmaker PETG HF @U1 0.2 nozzle.json index cc0f417368b..933c09a49fa 100644 --- a/resources/profiles/Snapmaker/filament/Snapmaker PETG HF @U1 0.2 nozzle.json +++ b/resources/profiles/Snapmaker/filament/Snapmaker PETG HF @U1 0.2 nozzle.json @@ -5,7 +5,10 @@ "from": "system", "setting_id": "GFSG96_01", "instantiation": "true", - "filament_max_volumetric_speed": ["2"], + "filament_max_volumetric_speed": [ + "1", + "1" + ], "filament_ramming_travel_time": [ "0", "0" @@ -105,7 +108,5 @@ "textured_plate_temp_initial_layer": [ "80" ], - "graphic_effect_plate_temp": ["0"], - "graphic_effect_plate_temp_initial_layer": ["0"], - "filament_z_hop_types": ["Slope Lift"] + "filament_is_high_temperature": ["0"] } \ No newline at end of file diff --git a/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.2 nozzle).json b/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.2 nozzle).json index 98a2586871e..7456699be5c 100644 --- a/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.2 nozzle).json +++ b/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.2 nozzle).json @@ -35,7 +35,7 @@ "0", "0" ], - "machine_end_gcode": "{if print_sequence == \\\"by object\\\"}\\nG91\\nG1 X2 Y2 Z1 F24000\\nG90\\nG1 Z{max_layer_z+2} F600\\n{endif}\\nPRINT_END\\nTIMELAPSE_STOP", + "machine_end_gcode": " PRINT_END\nTIMELAPSE_STOP", "machine_max_jerk_z": [ "3", "0.4" @@ -48,7 +48,7 @@ "20", "12" ], - "machine_start_gcode": ";===== date: 20260128 =====================\\n\\nPRINT_START\\nDEFECT_DETECTION_START\\nSET_PRINT_STATS_INFO TOTAL_LAYER={total_layer_count} CURRENT_LAYER=0\\nTIMELAPSE_START\\nM140 S{bed_temperature_initial_layer_single}\\nM104 T{initial_extruder} S140\\nM204 S10000\\n\\nG28 X Y\\nDEFECT_DETECT_NOODLE_FIRST\\n;===== 床面异物检测 ========\\nT{initial_extruder}\\nG90\\nDEFECT_DETECTION_DETECT_BED\\n;===== 取放头检测 =================\\nSM_PRINT_CHECK_SWITCH_EXTRUDER\\n\\n;===== 自动进料 & 挤出流量 & 预挤出 ======================\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=1 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=0\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=0\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=2 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=1\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=1\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=3 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=2\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=2\\nSM_PRINT_AUTO_FEED EXTRUDER=3\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=3\\nM104 S0 T0 A0\\nM104 S0 T1 A0\\nM104 S0 T2 A0\\nM104 S0 T3 A0\\nM104 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\n\\n;===== 粗回零 =================\\nT{initial_extruder}\\nM106 S255\\nM106 P2 S0\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\nROUGHLY_CLEAN_NOZZLE_WITH_DISCARD\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nG28 Z I140 J140\\n\\n;===== 检测钢板 =================\\nDETECT_BED_PLATE\\n\\n;===== 深度清洁喷嘴 =================\\nG90\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 S{nozzle_temperature[initial_extruder] - 50}\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_1\\nM104 S{nozzle_temperature[initial_extruder] - 90}\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_2\\n\\n;===== 精回零 =================\\nM106 S255\\nM109 S{nozzle_temperature[initial_extruder] - 90}\\nM190 S{bed_temperature_initial_layer_single}\\nM107 P2\\nG90\\nG0 Z5 F10000\\n\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{else} \\nG28 Z \\n{endif} \\n\\n\\n;===== 热床调平 =================\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{else} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 \\n{endif} \\n\\n\\n;===== 画起始线 =================\\nG90\\nG1 Z1.5\\nG0 X85 Y1 Z2 F18000\\nM109 S{nozzle_temperature_initial_layer[initial_extruder]}\\nG1 Z0.2\\nM83\\nG1 X185 E15 F360\\nG1 Z1.5\\n\\nG90\\nM106 S0", + "machine_start_gcode": ";===== date: 20260128 =====================\\n\\nPRINT_START\\nDEFECT_DETECTION_START\\nSET_PRINT_STATS_INFO TOTAL_LAYER={total_layer_count} CURRENT_LAYER=0\\nTIMELAPSE_START\\nM140 S{bed_temperature_initial_layer_single}\\nM104 T{initial_extruder} S140\\nM204 S10000\\n{if filament_is_high_temperature[0]}\\n; 保温\\n\tSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=180\\n{else}\\n{if temperature_vitrification[0]<=50}\\n; 强冷\\nSET_PURIFIER_MODE MODE=1 DESIRE_TEMP=42 ALARM_TEMP=45 FAN_SPEED=0.6\\nDELAY_OFF=0\t\t\\n{else}\\n; 弱冷\\nSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=0 FAN_SPEED=0.6 DELAY_OFF=180\t\t\\n{endif}\\n{endif}\\nG28 X Y\\n;===== 床面异物检测 ========\\nT{initial_extruder}\\nG90\\nDEFECT_DETECTION_DETECT_BED\\n;===== 取放头检测 =================\\nSM_PRINT_CHECK_SWITCH_EXTRUDER\\n\\n;===== 自动进料 & 挤出流量 & 预挤出 ======================\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=1 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=0\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=0\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=2 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=1\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=1\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=3 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=2\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=2\\nSM_PRINT_AUTO_FEED EXTRUDER=3\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=3\\nM104 S0 T0 A0\\nM104 S0 T1 A0\\nM104 S0 T2 A0\\nM104 S0 T3 A0\\nM104 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\n\\n;===== 粗回零 =================\\nT{initial_extruder}\\nM106 S255\\nM106 P2 S0\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\nROUGHLY_CLEAN_NOZZLE_WITH_DISCARD\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nG28 Z I140 J140\\n\\n;===== 检测钢板 =================\\nDETECT_BED_PLATE\\n\\n;===== 深度清洁喷嘴 =================\\nG90\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 S{nozzle_temperature[initial_extruder] - 50}\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_1\\nM104 S{nozzle_temperature[initial_extruder] - 90}\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_2\\n\\n;===== 精回零 =================\\nM106 S255\\nM109 S{nozzle_temperature[initial_extruder] - 90}\\nM190 S{bed_temperature_initial_layer_single}\\nM107 P2\\nG90\\nG0 Z5 F10000\\nWAIT_CHAMBER_TEMP TIMEOUT=180\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{else} \\nG28 Z \\n{endif} \\n\\n\\n;===== 热床调平 =================\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{else} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 \\n{endif} \\n\\n\\n;===== 画起始线 =================\\nG90\\nG1 Z1.5\\nG0 X85 Y1 Z2 F18000\\nM109 S{nozzle_temperature_initial_layer[initial_extruder]}\\nG1 Z0.2\\nM83\\nG1 X185 E15 F360\\nG1 Z1.5\\n\\nG90\\nM106 S0", "machine_tool_change_time": "5", "max_layer_height": [ "0.14", diff --git a/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.4 nozzle).json b/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.4 nozzle).json index 06b63eb9af4..95ff6d82d6b 100644 --- a/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.4 nozzle).json +++ b/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.4 nozzle).json @@ -35,7 +35,7 @@ "0", "0" ], - "machine_end_gcode": "{if print_sequence == \\\"by object\\\"}\\nG91\\nG1 X2 Y2 Z1 F24000\\nG90\\nG1 Z{max_layer_z+2} F600\\n{endif}\\nPRINT_END\\nTIMELAPSE_STOP", + "machine_end_gcode": " PRINT_END\nTIMELAPSE_STOP", "machine_max_jerk_z": [ "3", "0.4" @@ -48,7 +48,7 @@ "20", "12" ], - "machine_start_gcode": ";===== date: 20260128 =====================\\n\\nPRINT_START\\nDEFECT_DETECTION_START\\nSET_PRINT_STATS_INFO TOTAL_LAYER={total_layer_count} CURRENT_LAYER=0\\nTIMELAPSE_START\\nM140 S{bed_temperature_initial_layer_single}\\nM104 T{initial_extruder} S140\\nM204 S10000\\n\\nG28 X Y\\nDEFECT_DETECT_NOODLE_FIRST\\n;===== 床面异物检测 ========\\nT{initial_extruder}\\nG90\\nDEFECT_DETECTION_DETECT_BED\\n;===== 取放头检测 =================\\nSM_PRINT_CHECK_SWITCH_EXTRUDER\\n\\n;===== 自动进料 & 挤出流量 & 预挤出 ======================\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=1 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=0\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=0\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=2 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=1\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=1\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=3 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=2\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=2\\nSM_PRINT_AUTO_FEED EXTRUDER=3\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=3\\nM104 S0 T0 A0\\nM104 S0 T1 A0\\nM104 S0 T2 A0\\nM104 S0 T3 A0\\nM104 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\n\\n;===== 粗回零 =================\\nT{initial_extruder}\\nM106 S255\\nM106 P2 S0\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\nROUGHLY_CLEAN_NOZZLE_WITH_DISCARD\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nG28 Z I140 J140\\n\\n;===== 检测钢板 =================\\nDETECT_BED_PLATE\\n\\n;===== 深度清洁喷嘴 =================\\nG90\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 S{nozzle_temperature[initial_extruder] - 50}\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_1\\nM104 S{nozzle_temperature[initial_extruder] - 90}\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_2\\n\\n;===== 精回零 =================\\nM106 S255\\nM109 S{nozzle_temperature[initial_extruder] - 90}\\nM190 S{bed_temperature_initial_layer_single}\\nM107 P2\\nG90\\nG0 Z5 F10000\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{else} \\nG28 Z \\n{endif} \\n\\n\\n;===== 热床调平 =================\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{else} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 \\n{endif} \\n\\n\\n;===== 画起始线 =================\\nG90\\nG1 Z1.5\\nG0 X85 Y1 Z2 F18000\\nM109 S{nozzle_temperature_initial_layer[initial_extruder]}\\nG1 Z0.2\\nM83\\nG1 X185 E15 F360\\nG1 Z1.5\\n\\nG90\\nM106 S0", + "machine_start_gcode": ";===== date: 20260128 =====================\\n\\nPRINT_START\\nDEFECT_DETECTION_START\\nSET_PRINT_STATS_INFO TOTAL_LAYER={total_layer_count} CURRENT_LAYER=0\\nTIMELAPSE_START\\nM140 S{bed_temperature_initial_layer_single}\\nM104 T{initial_extruder} S140\\nM204 S10000\\n{if filament_is_high_temperature[0]}\\n; 保温\\n\tSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=180\\n{else}\\n{if temperature_vitrification[0]<=50}\\n; 强冷\\nSET_PURIFIER_MODE MODE=1 DESIRE_TEMP=42 ALARM_TEMP=45 FAN_SPEED=0.6\\nDELAY_OFF=0\t\t\\n{else}\\n; 弱冷\\nSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=0 FAN_SPEED=0.6 DELAY_OFF=180\t\t\\n{endif}\\n{endif}\\n\\nG28 X Y\\n;===== 床面异物检测 ========\\nT{initial_extruder}\\nG90\\nDEFECT_DETECTION_DETECT_BED\\n;===== 取放头检测 =================\\nSM_PRINT_CHECK_SWITCH_EXTRUDER\\n\\n;===== 自动进料 & 挤出流量 & 预挤出 ======================\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=1 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=0\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=0\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=2 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=1\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=1\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=3 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=2\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=2\\nSM_PRINT_AUTO_FEED EXTRUDER=3\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=3\\nM104 S0 T0 A0\\nM104 S0 T1 A0\\nM104 S0 T2 A0\\nM104 S0 T3 A0\\nM104 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\n\\n;===== 粗回零 =================\\nT{initial_extruder}\\nM106 S255\\nM106 P2 S0\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\nROUGHLY_CLEAN_NOZZLE_WITH_DISCARD\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nG28 Z I140 J140\\n\\n;===== 检测钢板 =================\\nDETECT_BED_PLATE\\n\\n;===== 深度清洁喷嘴 =================\\nG90\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 S{nozzle_temperature[initial_extruder] - 50}\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_1\\nM104 S{nozzle_temperature[initial_extruder] - 90}\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_2\\n\\n;===== 精回零 =================\\nM106 S255\\nM109 S{nozzle_temperature[initial_extruder] - 90}\\nM190 S{bed_temperature_initial_layer_single}\\nM107 P2\\nG90\\nG0 Z5 F10000\\nWAIT_CHAMBER_TEMP TIMEOUT=180\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{else} \\nG28 Z \\n{endif} \\n\\n\\n;===== 热床调平 =================\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{else} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 \\n{endif} \\n\\n\\n;===== 画起始线 =================\\nG90\\nG1 Z1.5\\nG0 X85 Y1 Z2 F18000\\nM109 S{nozzle_temperature_initial_layer[initial_extruder]}\\nG1 Z0.2\\nM83\\nG1 X185 E15 F360\\nG1 Z1.5\\n\\nG90\\nM106 S0", "machine_tool_change_time": "5", "max_layer_height": [ "0.32", diff --git a/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.6 nozzle).json b/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.6 nozzle).json index 9c54ab2e190..40d8bf7e5da 100644 --- a/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.6 nozzle).json +++ b/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.6 nozzle).json @@ -35,7 +35,7 @@ "0", "0" ], - "machine_end_gcode": "{if print_sequence == \\\"by object\\\"}\\nG91\\nG1 X2 Y2 Z1 F24000\\nG90\\nG1 Z{max_layer_z+2} F600\\n{endif}\\nPRINT_END\\nTIMELAPSE_STOP", + "machine_end_gcode": " PRINT_END\nTIMELAPSE_STOP", "machine_max_jerk_z": [ "3", "0.4" @@ -48,7 +48,7 @@ "20", "12" ], - "machine_start_gcode": ";===== date: 20260128 =====================\\n\\nPRINT_START\\nDEFECT_DETECTION_START\\nSET_PRINT_STATS_INFO TOTAL_LAYER={total_layer_count} CURRENT_LAYER=0\\nTIMELAPSE_START\\nM140 S{bed_temperature_initial_layer_single}\\nM104 T{initial_extruder} S140\\nM204 S10000\\n\\nG28 X Y\\nDEFECT_DETECT_NOODLE_FIRST\\n;===== 床面异物检测 ========\\nT{initial_extruder}\\nG90\\nDEFECT_DETECTION_DETECT_BED\\n;===== 取放头检测 =================\\nSM_PRINT_CHECK_SWITCH_EXTRUDER\\n\\n;===== 自动进料 & 挤出流量 & 预挤出 ======================\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=1 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=0\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=0\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=2 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=1\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=1\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=3 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=2\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=2\\nSM_PRINT_AUTO_FEED EXTRUDER=3\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=3\\nM104 S0 T0 A0\\nM104 S0 T1 A0\\nM104 S0 T2 A0\\nM104 S0 T3 A0\\nM104 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\n\\n;===== 粗回零 =================\\nT{initial_extruder}\\nM106 S255\\nM106 P2 S0\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\nROUGHLY_CLEAN_NOZZLE_WITH_DISCARD\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nG28 Z I140 J140\\n\\n;===== 检测钢板 =================\\nDETECT_BED_PLATE\\n\\n;===== 深度清洁喷嘴 =================\\nG90\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 S{nozzle_temperature[initial_extruder] - 50}\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_1\\nM104 S{nozzle_temperature[initial_extruder] - 90}\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_2\\n\\n;===== 精回零 =================\\nM106 S255\\nM109 S{nozzle_temperature[initial_extruder] - 90}\\nM190 S{bed_temperature_initial_layer_single}\\nM107 P2\\nG90\\nG0 Z5 F10000\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{else} \\nG28 Z \\n{endif} \\n\\n\\n;===== 热床调平 =================\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{else} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 \\n{endif} \\n\\n\\n;===== 画起始线 =================\\nG90\\nG1 Z1.5\\nG0 X85 Y1 Z2 F18000\\nM109 S{nozzle_temperature_initial_layer[initial_extruder]}\\nG1 Z0.2\\nM83\\nG1 X185 E15 F360\\nG1 Z1.5\\n\\nG90\\nM106 S0", + "machine_start_gcode": ";===== date: 20260128 =====================\\n\\nPRINT_START\\nDEFECT_DETECTION_START\\nSET_PRINT_STATS_INFO TOTAL_LAYER={total_layer_count} CURRENT_LAYER=0\\nTIMELAPSE_START\\nM140 S{bed_temperature_initial_layer_single}\\nM104 T{initial_extruder} S140\\nM204 S10000\\n{if filament_is_high_temperature[0]}\\n; 保温\\n\tSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=180\\n{else}\\n{if temperature_vitrification[0]<=50}\\n; 强冷\\nSET_PURIFIER_MODE MODE=1 DESIRE_TEMP=42 ALARM_TEMP=45 FAN_SPEED=0.6\\nDELAY_OFF=0\t\t\\n{else}\\n; 弱冷\\nSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=0 FAN_SPEED=0.6 DELAY_OFF=180\t\t\\n{endif}\\n{endif}\\nG28 X Y\\n;===== 床面异物检测 ========\\nT{initial_extruder}\\nG90\\nDEFECT_DETECTION_DETECT_BED\\n;===== 取放头检测 =================\\nSM_PRINT_CHECK_SWITCH_EXTRUDER\\n\\n;===== 自动进料 & 挤出流量 & 预挤出 ======================\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=1 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=0\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=0\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=2 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=1\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=1\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=3 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=2\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=2\\nSM_PRINT_AUTO_FEED EXTRUDER=3\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=3\\nM104 S0 T0 A0\\nM104 S0 T1 A0\\nM104 S0 T2 A0\\nM104 S0 T3 A0\\nM104 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\n\\n;===== 粗回零 =================\\nT{initial_extruder}\\nM106 S255\\nM106 P2 S0\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\nROUGHLY_CLEAN_NOZZLE_WITH_DISCARD\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nG28 Z I140 J140\\n\\n;===== 检测钢板 =================\\nDETECT_BED_PLATE\\n\\n;===== 深度清洁喷嘴 =================\\nG90\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 S{nozzle_temperature[initial_extruder] - 50}\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_1\\nM104 S{nozzle_temperature[initial_extruder] - 90}\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_2\\n\\n;===== 精回零 =================\\nM106 S255\\nM109 S{nozzle_temperature[initial_extruder] - 90}\\nM190 S{bed_temperature_initial_layer_single}\\nM107 P2\\nG90\\nG0 Z5 F10000\\nWAIT_CHAMBER_TEMP TIMEOUT=180\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{else} \\nG28 Z \\n{endif} \\n\\n\\n;===== 热床调平 =================\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{else} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 \\n{endif} \\n\\n\\n;===== 画起始线 =================\\nG90\\nG1 Z1.5\\nG0 X85 Y1 Z2 F18000\\nM109 S{nozzle_temperature_initial_layer[initial_extruder]}\\nG1 Z0.2\\nM83\\nG1 X185 E15 F360\\nG1 Z1.5\\n\\nG90\\nM106 S0", "machine_tool_change_time": "5", "max_layer_height": [ "0.42", diff --git a/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.8 nozzle).json b/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.8 nozzle).json index 82d0b26ac24..94d6eeeafe6 100644 --- a/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.8 nozzle).json +++ b/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.8 nozzle).json @@ -35,7 +35,7 @@ "0", "0" ], - "machine_end_gcode": "{if print_sequence == \\\"by object\\\"}\\nG91\\nG1 X2 Y2 Z1 F24000\\nG90\\nG1 Z{max_layer_z+2} F600\\n{endif}\\nPRINT_END\\nTIMELAPSE_STOP", + "machine_end_gcode": " PRINT_END\nTIMELAPSE_STOP", "machine_max_jerk_z": [ "3", "0.4" @@ -48,7 +48,7 @@ "20", "12" ], - "machine_start_gcode": ";===== date: 20260128 =====================\\n\\nPRINT_START\\nDEFECT_DETECTION_START\\nSET_PRINT_STATS_INFO TOTAL_LAYER={total_layer_count} CURRENT_LAYER=0\\nTIMELAPSE_START\\nM140 S{bed_temperature_initial_layer_single}\\nM104 T{initial_extruder} S140\\nM204 S10000\\n\\nG28 X Y\\nDEFECT_DETECT_NOODLE_FIRST\\n;===== 床面异物检测 ========\\nT{initial_extruder}\\nG90\\nDEFECT_DETECTION_DETECT_BED\\n;===== 取放头检测 =================\\nSM_PRINT_CHECK_SWITCH_EXTRUDER\\n\\n;===== 自动进料 & 挤出流量 & 预挤出 ======================\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=1 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=0\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=0\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=2 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=1\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=1\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=3 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=2\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=2\\nSM_PRINT_AUTO_FEED EXTRUDER=3\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=3\\nM104 S0 T0 A0\\nM104 S0 T1 A0\\nM104 S0 T2 A0\\nM104 S0 T3 A0\\nM104 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\n\\n;===== 粗回零 =================\\nT{initial_extruder}\\nM106 S255\\nM106 P2 S0\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\nROUGHLY_CLEAN_NOZZLE_WITH_DISCARD\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nG28 Z I140 J140\\n\\n;===== 检测钢板 =================\\nDETECT_BED_PLATE\\n\\n;===== 深度清洁喷嘴 =================\\nG90\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 S{nozzle_temperature[initial_extruder] - 50}\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_1\\nM104 S{nozzle_temperature[initial_extruder] - 90}\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_2\\n\\n;===== 精回零 =================\\nM106 S255\\nM109 S{nozzle_temperature[initial_extruder] - 90}\\nM190 S{bed_temperature_initial_layer_single}\\nM107 P2\\nG90\\nG0 Z5 F10000\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{else} \\nG28 Z \\n{endif} \\n\\n\\n;===== 热床调平 =================\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{else} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 \\n{endif} \\n\\n\\n;===== 画起始线 =================\\nG90\\nG1 Z1.5\\nG0 X85 Y1 Z2 F18000\\nM109 S{nozzle_temperature_initial_layer[initial_extruder]}\\nG1 Z0.2\\nM83\\nG1 X185 E15 F360\\nG1 Z1.5\\n\\nG90\\nM106 S0", + "machine_start_gcode": ";===== date: 20260128 =====================\\n\\nPRINT_START\\nDEFECT_DETECTION_START\\nSET_PRINT_STATS_INFO TOTAL_LAYER={total_layer_count} CURRENT_LAYER=0\\nTIMELAPSE_START\\nM140 S{bed_temperature_initial_layer_single}\\nM104 T{initial_extruder} S140\\nM204 S10000\\n{if filament_is_high_temperature[0]}\\n; 保温\\n\tSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=180\\n{else}\\n{if temperature_vitrification[0]<=50}\\n; 强冷\\nSET_PURIFIER_MODE MODE=1 DESIRE_TEMP=42 ALARM_TEMP=45 FAN_SPEED=0.6\\nDELAY_OFF=0\t\t\\n{else}\\n; 弱冷\\nSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=0 FAN_SPEED=0.6 DELAY_OFF=180\t\t\\n{endif}\\n{endif}\\nG28 X Y\\n;===== 床面异物检测 ========\\nT{initial_extruder}\\nG90\\nDEFECT_DETECTION_DETECT_BED\\n;===== 取放头检测 =================\\nSM_PRINT_CHECK_SWITCH_EXTRUDER\\n\\n;===== 自动进料 & 挤出流量 & 预挤出 ======================\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=1 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=0\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=0\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=2 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=1\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=1\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=3 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=2\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=2\\nSM_PRINT_AUTO_FEED EXTRUDER=3\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=3\\nM104 S0 T0 A0\\nM104 S0 T1 A0\\nM104 S0 T2 A0\\nM104 S0 T3 A0\\nM104 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\n\\n;===== 粗回零 =================\\nT{initial_extruder}\\nM106 S255\\nM106 P2 S0\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\nROUGHLY_CLEAN_NOZZLE_WITH_DISCARD\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nG28 Z I140 J140\\n\\n;===== 检测钢板 =================\\nDETECT_BED_PLATE\\n\\n;===== 深度清洁喷嘴 =================\\nG90\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 S{nozzle_temperature[initial_extruder] - 50}\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_1\\nM104 S{nozzle_temperature[initial_extruder] - 90}\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_2\\n\\n;===== 精回零 =================\\nM106 S255\\nM109 S{nozzle_temperature[initial_extruder] - 90}\\nM190 S{bed_temperature_initial_layer_single}\\nM107 P2\\nG90\\nG0 Z5 F10000\\nWAIT_CHAMBER_TEMP TIMEOUT=180\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{else} \\nG28 Z \\n{endif} \\n\\n\\n;===== 热床调平 =================\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{else} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 \\n{endif} \\n\\n\\n;===== 画起始线 =================\\nG90\\nG1 Z1.5\\nG0 X85 Y1 Z2 F18000\\nM109 S{nozzle_temperature_initial_layer[initial_extruder]}\\nG1 Z0.2\\nM83\\nG1 X185 E15 F360\\nG1 Z1.5\\n\\nG90\\nM106 S0", "machine_tool_change_time": "5", "max_layer_height": [ "0.56", From e36d8b2f14d7e7e37f5c7edeaa2b7d69570fd984 Mon Sep 17 00:00:00 2001 From: Joyx-SnapMaker <1906956558@qq.com> Date: Mon, 27 Apr 2026 18:26:51 +0800 Subject: [PATCH 005/135] =?UTF-8?q?Fix=EF=BC=9ACorrect=20the=20incorrect?= =?UTF-8?q?=20G-code=20code?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../profiles/Snapmaker/machine/Snapmaker U1 (0.2 nozzle).json | 2 +- .../profiles/Snapmaker/machine/Snapmaker U1 (0.4 nozzle).json | 2 +- .../profiles/Snapmaker/machine/Snapmaker U1 (0.6 nozzle).json | 2 +- .../profiles/Snapmaker/machine/Snapmaker U1 (0.8 nozzle).json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.2 nozzle).json b/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.2 nozzle).json index 7456699be5c..48c14548825 100644 --- a/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.2 nozzle).json +++ b/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.2 nozzle).json @@ -48,7 +48,7 @@ "20", "12" ], - "machine_start_gcode": ";===== date: 20260128 =====================\\n\\nPRINT_START\\nDEFECT_DETECTION_START\\nSET_PRINT_STATS_INFO TOTAL_LAYER={total_layer_count} CURRENT_LAYER=0\\nTIMELAPSE_START\\nM140 S{bed_temperature_initial_layer_single}\\nM104 T{initial_extruder} S140\\nM204 S10000\\n{if filament_is_high_temperature[0]}\\n; 保温\\n\tSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=180\\n{else}\\n{if temperature_vitrification[0]<=50}\\n; 强冷\\nSET_PURIFIER_MODE MODE=1 DESIRE_TEMP=42 ALARM_TEMP=45 FAN_SPEED=0.6\\nDELAY_OFF=0\t\t\\n{else}\\n; 弱冷\\nSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=0 FAN_SPEED=0.6 DELAY_OFF=180\t\t\\n{endif}\\n{endif}\\nG28 X Y\\n;===== 床面异物检测 ========\\nT{initial_extruder}\\nG90\\nDEFECT_DETECTION_DETECT_BED\\n;===== 取放头检测 =================\\nSM_PRINT_CHECK_SWITCH_EXTRUDER\\n\\n;===== 自动进料 & 挤出流量 & 预挤出 ======================\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=1 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=0\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=0\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=2 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=1\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=1\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=3 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=2\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=2\\nSM_PRINT_AUTO_FEED EXTRUDER=3\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=3\\nM104 S0 T0 A0\\nM104 S0 T1 A0\\nM104 S0 T2 A0\\nM104 S0 T3 A0\\nM104 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\n\\n;===== 粗回零 =================\\nT{initial_extruder}\\nM106 S255\\nM106 P2 S0\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\nROUGHLY_CLEAN_NOZZLE_WITH_DISCARD\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nG28 Z I140 J140\\n\\n;===== 检测钢板 =================\\nDETECT_BED_PLATE\\n\\n;===== 深度清洁喷嘴 =================\\nG90\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 S{nozzle_temperature[initial_extruder] - 50}\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_1\\nM104 S{nozzle_temperature[initial_extruder] - 90}\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_2\\n\\n;===== 精回零 =================\\nM106 S255\\nM109 S{nozzle_temperature[initial_extruder] - 90}\\nM190 S{bed_temperature_initial_layer_single}\\nM107 P2\\nG90\\nG0 Z5 F10000\\nWAIT_CHAMBER_TEMP TIMEOUT=180\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{else} \\nG28 Z \\n{endif} \\n\\n\\n;===== 热床调平 =================\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{else} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 \\n{endif} \\n\\n\\n;===== 画起始线 =================\\nG90\\nG1 Z1.5\\nG0 X85 Y1 Z2 F18000\\nM109 S{nozzle_temperature_initial_layer[initial_extruder]}\\nG1 Z0.2\\nM83\\nG1 X185 E15 F360\\nG1 Z1.5\\n\\nG90\\nM106 S0", + "machine_start_gcode": ";===== date: 20260128 =====================\\n\\nPRINT_START\\nDEFECT_DETECTION_START\\nSET_PRINT_STATS_INFO TOTAL_LAYER={total_layer_count} CURRENT_LAYER=0\\nTIMELAPSE_START\\nM140 S{bed_temperature_initial_layer_single}\\nM104 T{initial_extruder} S140\\nM204 S10000\\n{if filament_is_high_temperature[0]}\\n; 保温\\n\tSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=180\\n{else}\\n{if temperature_vitrification[0]<=50}\\n; 强冷\\nSET_PURIFIER_MODE MODE=1 DESIRE_TEMP=42 ALARM_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=0\t\t\\n{else}\\n; 弱冷\\nSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=0 FAN_SPEED=0.6 DELAY_OFF=180\t\t\\n{endif}\\n{endif}\\nG28 X Y\\n;===== 床面异物检测 ========\\nT{initial_extruder}\\nG90\\nDEFECT_DETECTION_DETECT_BED\\n;===== 取放头检测 =================\\nSM_PRINT_CHECK_SWITCH_EXTRUDER\\n\\n;===== 自动进料 & 挤出流量 & 预挤出 ======================\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=1 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=0\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=0\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=2 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=1\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=1\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=3 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=2\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=2\\nSM_PRINT_AUTO_FEED EXTRUDER=3\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=3\\nM104 S0 T0 A0\\nM104 S0 T1 A0\\nM104 S0 T2 A0\\nM104 S0 T3 A0\\nM104 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\n\\n;===== 粗回零 =================\\nT{initial_extruder}\\nM106 S255\\nM106 P2 S0\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\nROUGHLY_CLEAN_NOZZLE_WITH_DISCARD\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nG28 Z I140 J140\\n\\n;===== 检测钢板 =================\\nDETECT_BED_PLATE\\n\\n;===== 深度清洁喷嘴 =================\\nG90\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 S{nozzle_temperature[initial_extruder] - 50}\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_1\\nM104 S{nozzle_temperature[initial_extruder] - 90}\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_2\\n\\n;===== 精回零 =================\\nM106 S255\\nM109 S{nozzle_temperature[initial_extruder] - 90}\\nM190 S{bed_temperature_initial_layer_single}\\nM107 P2\\nG90\\nG0 Z5 F10000\\nWAIT_CHAMBER_TEMP TIMEOUT=180\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{else} \\nG28 Z \\n{endif} \\n\\n\\n;===== 热床调平 =================\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{else} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 \\n{endif} \\n\\n\\n;===== 画起始线 =================\\nG90\\nG1 Z1.5\\nG0 X85 Y1 Z2 F18000\\nM109 S{nozzle_temperature_initial_layer[initial_extruder]}\\nG1 Z0.2\\nM83\\nG1 X185 E15 F360\\nG1 Z1.5\\n\\nG90\\nM106 S0", "machine_tool_change_time": "5", "max_layer_height": [ "0.14", diff --git a/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.4 nozzle).json b/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.4 nozzle).json index 95ff6d82d6b..2d1c8ae5ac5 100644 --- a/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.4 nozzle).json +++ b/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.4 nozzle).json @@ -48,7 +48,7 @@ "20", "12" ], - "machine_start_gcode": ";===== date: 20260128 =====================\\n\\nPRINT_START\\nDEFECT_DETECTION_START\\nSET_PRINT_STATS_INFO TOTAL_LAYER={total_layer_count} CURRENT_LAYER=0\\nTIMELAPSE_START\\nM140 S{bed_temperature_initial_layer_single}\\nM104 T{initial_extruder} S140\\nM204 S10000\\n{if filament_is_high_temperature[0]}\\n; 保温\\n\tSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=180\\n{else}\\n{if temperature_vitrification[0]<=50}\\n; 强冷\\nSET_PURIFIER_MODE MODE=1 DESIRE_TEMP=42 ALARM_TEMP=45 FAN_SPEED=0.6\\nDELAY_OFF=0\t\t\\n{else}\\n; 弱冷\\nSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=0 FAN_SPEED=0.6 DELAY_OFF=180\t\t\\n{endif}\\n{endif}\\n\\nG28 X Y\\n;===== 床面异物检测 ========\\nT{initial_extruder}\\nG90\\nDEFECT_DETECTION_DETECT_BED\\n;===== 取放头检测 =================\\nSM_PRINT_CHECK_SWITCH_EXTRUDER\\n\\n;===== 自动进料 & 挤出流量 & 预挤出 ======================\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=1 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=0\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=0\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=2 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=1\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=1\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=3 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=2\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=2\\nSM_PRINT_AUTO_FEED EXTRUDER=3\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=3\\nM104 S0 T0 A0\\nM104 S0 T1 A0\\nM104 S0 T2 A0\\nM104 S0 T3 A0\\nM104 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\n\\n;===== 粗回零 =================\\nT{initial_extruder}\\nM106 S255\\nM106 P2 S0\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\nROUGHLY_CLEAN_NOZZLE_WITH_DISCARD\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nG28 Z I140 J140\\n\\n;===== 检测钢板 =================\\nDETECT_BED_PLATE\\n\\n;===== 深度清洁喷嘴 =================\\nG90\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 S{nozzle_temperature[initial_extruder] - 50}\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_1\\nM104 S{nozzle_temperature[initial_extruder] - 90}\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_2\\n\\n;===== 精回零 =================\\nM106 S255\\nM109 S{nozzle_temperature[initial_extruder] - 90}\\nM190 S{bed_temperature_initial_layer_single}\\nM107 P2\\nG90\\nG0 Z5 F10000\\nWAIT_CHAMBER_TEMP TIMEOUT=180\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{else} \\nG28 Z \\n{endif} \\n\\n\\n;===== 热床调平 =================\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{else} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 \\n{endif} \\n\\n\\n;===== 画起始线 =================\\nG90\\nG1 Z1.5\\nG0 X85 Y1 Z2 F18000\\nM109 S{nozzle_temperature_initial_layer[initial_extruder]}\\nG1 Z0.2\\nM83\\nG1 X185 E15 F360\\nG1 Z1.5\\n\\nG90\\nM106 S0", + "machine_start_gcode": ";===== date: 20260128 =====================\\n\\nPRINT_START\\nDEFECT_DETECTION_START\\nSET_PRINT_STATS_INFO TOTAL_LAYER={total_layer_count} CURRENT_LAYER=0\\nTIMELAPSE_START\\nM140 S{bed_temperature_initial_layer_single}\\nM104 T{initial_extruder} S140\\nM204 S10000\\n{if filament_is_high_temperature[0]}\\n; 保温\\n\tSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=180\\n{else}\\n{if temperature_vitrification[0]<=50}\\n; 强冷\\nSET_PURIFIER_MODE MODE=1 DESIRE_TEMP=42 ALARM_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=0\t\t\\n{else}\\n; 弱冷\\nSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=0 FAN_SPEED=0.6 DELAY_OFF=180\t\t\\n{endif}\\n{endif}\\n\\nG28 X Y\\n;===== 床面异物检测 ========\\nT{initial_extruder}\\nG90\\nDEFECT_DETECTION_DETECT_BED\\n;===== 取放头检测 =================\\nSM_PRINT_CHECK_SWITCH_EXTRUDER\\n\\n;===== 自动进料 & 挤出流量 & 预挤出 ======================\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=1 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=0\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=0\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=2 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=1\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=1\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=3 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=2\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=2\\nSM_PRINT_AUTO_FEED EXTRUDER=3\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=3\\nM104 S0 T0 A0\\nM104 S0 T1 A0\\nM104 S0 T2 A0\\nM104 S0 T3 A0\\nM104 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\n\\n;===== 粗回零 =================\\nT{initial_extruder}\\nM106 S255\\nM106 P2 S0\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\nROUGHLY_CLEAN_NOZZLE_WITH_DISCARD\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nG28 Z I140 J140\\n\\n;===== 检测钢板 =================\\nDETECT_BED_PLATE\\n\\n;===== 深度清洁喷嘴 =================\\nG90\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 S{nozzle_temperature[initial_extruder] - 50}\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_1\\nM104 S{nozzle_temperature[initial_extruder] - 90}\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_2\\n\\n;===== 精回零 =================\\nM106 S255\\nM109 S{nozzle_temperature[initial_extruder] - 90}\\nM190 S{bed_temperature_initial_layer_single}\\nM107 P2\\nG90\\nG0 Z5 F10000\\nWAIT_CHAMBER_TEMP TIMEOUT=180\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{else} \\nG28 Z \\n{endif} \\n\\n\\n;===== 热床调平 =================\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{else} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 \\n{endif} \\n\\n\\n;===== 画起始线 =================\\nG90\\nG1 Z1.5\\nG0 X85 Y1 Z2 F18000\\nM109 S{nozzle_temperature_initial_layer[initial_extruder]}\\nG1 Z0.2\\nM83\\nG1 X185 E15 F360\\nG1 Z1.5\\n\\nG90\\nM106 S0", "machine_tool_change_time": "5", "max_layer_height": [ "0.32", diff --git a/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.6 nozzle).json b/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.6 nozzle).json index 40d8bf7e5da..cfec4db7c79 100644 --- a/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.6 nozzle).json +++ b/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.6 nozzle).json @@ -48,7 +48,7 @@ "20", "12" ], - "machine_start_gcode": ";===== date: 20260128 =====================\\n\\nPRINT_START\\nDEFECT_DETECTION_START\\nSET_PRINT_STATS_INFO TOTAL_LAYER={total_layer_count} CURRENT_LAYER=0\\nTIMELAPSE_START\\nM140 S{bed_temperature_initial_layer_single}\\nM104 T{initial_extruder} S140\\nM204 S10000\\n{if filament_is_high_temperature[0]}\\n; 保温\\n\tSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=180\\n{else}\\n{if temperature_vitrification[0]<=50}\\n; 强冷\\nSET_PURIFIER_MODE MODE=1 DESIRE_TEMP=42 ALARM_TEMP=45 FAN_SPEED=0.6\\nDELAY_OFF=0\t\t\\n{else}\\n; 弱冷\\nSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=0 FAN_SPEED=0.6 DELAY_OFF=180\t\t\\n{endif}\\n{endif}\\nG28 X Y\\n;===== 床面异物检测 ========\\nT{initial_extruder}\\nG90\\nDEFECT_DETECTION_DETECT_BED\\n;===== 取放头检测 =================\\nSM_PRINT_CHECK_SWITCH_EXTRUDER\\n\\n;===== 自动进料 & 挤出流量 & 预挤出 ======================\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=1 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=0\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=0\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=2 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=1\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=1\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=3 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=2\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=2\\nSM_PRINT_AUTO_FEED EXTRUDER=3\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=3\\nM104 S0 T0 A0\\nM104 S0 T1 A0\\nM104 S0 T2 A0\\nM104 S0 T3 A0\\nM104 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\n\\n;===== 粗回零 =================\\nT{initial_extruder}\\nM106 S255\\nM106 P2 S0\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\nROUGHLY_CLEAN_NOZZLE_WITH_DISCARD\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nG28 Z I140 J140\\n\\n;===== 检测钢板 =================\\nDETECT_BED_PLATE\\n\\n;===== 深度清洁喷嘴 =================\\nG90\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 S{nozzle_temperature[initial_extruder] - 50}\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_1\\nM104 S{nozzle_temperature[initial_extruder] - 90}\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_2\\n\\n;===== 精回零 =================\\nM106 S255\\nM109 S{nozzle_temperature[initial_extruder] - 90}\\nM190 S{bed_temperature_initial_layer_single}\\nM107 P2\\nG90\\nG0 Z5 F10000\\nWAIT_CHAMBER_TEMP TIMEOUT=180\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{else} \\nG28 Z \\n{endif} \\n\\n\\n;===== 热床调平 =================\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{else} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 \\n{endif} \\n\\n\\n;===== 画起始线 =================\\nG90\\nG1 Z1.5\\nG0 X85 Y1 Z2 F18000\\nM109 S{nozzle_temperature_initial_layer[initial_extruder]}\\nG1 Z0.2\\nM83\\nG1 X185 E15 F360\\nG1 Z1.5\\n\\nG90\\nM106 S0", + "machine_start_gcode": ";===== date: 20260128 =====================\\n\\nPRINT_START\\nDEFECT_DETECTION_START\\nSET_PRINT_STATS_INFO TOTAL_LAYER={total_layer_count} CURRENT_LAYER=0\\nTIMELAPSE_START\\nM140 S{bed_temperature_initial_layer_single}\\nM104 T{initial_extruder} S140\\nM204 S10000\\n{if filament_is_high_temperature[0]}\\n; 保温\\n\tSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=180\\n{else}\\n{if temperature_vitrification[0]<=50}\\n; 强冷\\nSET_PURIFIER_MODE MODE=1 DESIRE_TEMP=42 ALARM_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=0\t\t\\n{else}\\n; 弱冷\\nSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=0 FAN_SPEED=0.6 DELAY_OFF=180\t\t\\n{endif}\\n{endif}\\nG28 X Y\\n;===== 床面异物检测 ========\\nT{initial_extruder}\\nG90\\nDEFECT_DETECTION_DETECT_BED\\n;===== 取放头检测 =================\\nSM_PRINT_CHECK_SWITCH_EXTRUDER\\n\\n;===== 自动进料 & 挤出流量 & 预挤出 ======================\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=1 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=0\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=0\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=2 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=1\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=1\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=3 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=2\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=2\\nSM_PRINT_AUTO_FEED EXTRUDER=3\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=3\\nM104 S0 T0 A0\\nM104 S0 T1 A0\\nM104 S0 T2 A0\\nM104 S0 T3 A0\\nM104 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\n\\n;===== 粗回零 =================\\nT{initial_extruder}\\nM106 S255\\nM106 P2 S0\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\nROUGHLY_CLEAN_NOZZLE_WITH_DISCARD\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nG28 Z I140 J140\\n\\n;===== 检测钢板 =================\\nDETECT_BED_PLATE\\n\\n;===== 深度清洁喷嘴 =================\\nG90\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 S{nozzle_temperature[initial_extruder] - 50}\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_1\\nM104 S{nozzle_temperature[initial_extruder] - 90}\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_2\\n\\n;===== 精回零 =================\\nM106 S255\\nM109 S{nozzle_temperature[initial_extruder] - 90}\\nM190 S{bed_temperature_initial_layer_single}\\nM107 P2\\nG90\\nG0 Z5 F10000\\nWAIT_CHAMBER_TEMP TIMEOUT=180\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{else} \\nG28 Z \\n{endif} \\n\\n\\n;===== 热床调平 =================\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{else} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 \\n{endif} \\n\\n\\n;===== 画起始线 =================\\nG90\\nG1 Z1.5\\nG0 X85 Y1 Z2 F18000\\nM109 S{nozzle_temperature_initial_layer[initial_extruder]}\\nG1 Z0.2\\nM83\\nG1 X185 E15 F360\\nG1 Z1.5\\n\\nG90\\nM106 S0", "machine_tool_change_time": "5", "max_layer_height": [ "0.42", diff --git a/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.8 nozzle).json b/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.8 nozzle).json index 94d6eeeafe6..12692f2bd96 100644 --- a/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.8 nozzle).json +++ b/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.8 nozzle).json @@ -48,7 +48,7 @@ "20", "12" ], - "machine_start_gcode": ";===== date: 20260128 =====================\\n\\nPRINT_START\\nDEFECT_DETECTION_START\\nSET_PRINT_STATS_INFO TOTAL_LAYER={total_layer_count} CURRENT_LAYER=0\\nTIMELAPSE_START\\nM140 S{bed_temperature_initial_layer_single}\\nM104 T{initial_extruder} S140\\nM204 S10000\\n{if filament_is_high_temperature[0]}\\n; 保温\\n\tSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=180\\n{else}\\n{if temperature_vitrification[0]<=50}\\n; 强冷\\nSET_PURIFIER_MODE MODE=1 DESIRE_TEMP=42 ALARM_TEMP=45 FAN_SPEED=0.6\\nDELAY_OFF=0\t\t\\n{else}\\n; 弱冷\\nSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=0 FAN_SPEED=0.6 DELAY_OFF=180\t\t\\n{endif}\\n{endif}\\nG28 X Y\\n;===== 床面异物检测 ========\\nT{initial_extruder}\\nG90\\nDEFECT_DETECTION_DETECT_BED\\n;===== 取放头检测 =================\\nSM_PRINT_CHECK_SWITCH_EXTRUDER\\n\\n;===== 自动进料 & 挤出流量 & 预挤出 ======================\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=1 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=0\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=0\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=2 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=1\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=1\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=3 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=2\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=2\\nSM_PRINT_AUTO_FEED EXTRUDER=3\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=3\\nM104 S0 T0 A0\\nM104 S0 T1 A0\\nM104 S0 T2 A0\\nM104 S0 T3 A0\\nM104 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\n\\n;===== 粗回零 =================\\nT{initial_extruder}\\nM106 S255\\nM106 P2 S0\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\nROUGHLY_CLEAN_NOZZLE_WITH_DISCARD\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nG28 Z I140 J140\\n\\n;===== 检测钢板 =================\\nDETECT_BED_PLATE\\n\\n;===== 深度清洁喷嘴 =================\\nG90\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 S{nozzle_temperature[initial_extruder] - 50}\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_1\\nM104 S{nozzle_temperature[initial_extruder] - 90}\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_2\\n\\n;===== 精回零 =================\\nM106 S255\\nM109 S{nozzle_temperature[initial_extruder] - 90}\\nM190 S{bed_temperature_initial_layer_single}\\nM107 P2\\nG90\\nG0 Z5 F10000\\nWAIT_CHAMBER_TEMP TIMEOUT=180\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{else} \\nG28 Z \\n{endif} \\n\\n\\n;===== 热床调平 =================\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{else} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 \\n{endif} \\n\\n\\n;===== 画起始线 =================\\nG90\\nG1 Z1.5\\nG0 X85 Y1 Z2 F18000\\nM109 S{nozzle_temperature_initial_layer[initial_extruder]}\\nG1 Z0.2\\nM83\\nG1 X185 E15 F360\\nG1 Z1.5\\n\\nG90\\nM106 S0", + "machine_start_gcode": ";===== date: 20260128 =====================\\n\\nPRINT_START\\nDEFECT_DETECTION_START\\nSET_PRINT_STATS_INFO TOTAL_LAYER={total_layer_count} CURRENT_LAYER=0\\nTIMELAPSE_START\\nM140 S{bed_temperature_initial_layer_single}\\nM104 T{initial_extruder} S140\\nM204 S10000\\n{if filament_is_high_temperature[0]}\\n; 保温\\n\tSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=180\\n{else}\\n{if temperature_vitrification[0]<=50}\\n; 强冷\\nSET_PURIFIER_MODE MODE=1 DESIRE_TEMP=42 ALARM_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=0\t\t\\n{else}\\n; 弱冷\\nSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=0 FAN_SPEED=0.6 DELAY_OFF=180\t\t\\n{endif}\\n{endif}\\nG28 X Y\\n;===== 床面异物检测 ========\\nT{initial_extruder}\\nG90\\nDEFECT_DETECTION_DETECT_BED\\n;===== 取放头检测 =================\\nSM_PRINT_CHECK_SWITCH_EXTRUDER\\n\\n;===== 自动进料 & 挤出流量 & 预挤出 ======================\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=1 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=0\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=0\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=2 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=1\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=1\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=3 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=2\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=2\\nSM_PRINT_AUTO_FEED EXTRUDER=3\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=3\\nM104 S0 T0 A0\\nM104 S0 T1 A0\\nM104 S0 T2 A0\\nM104 S0 T3 A0\\nM104 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\n\\n;===== 粗回零 =================\\nT{initial_extruder}\\nM106 S255\\nM106 P2 S0\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\nROUGHLY_CLEAN_NOZZLE_WITH_DISCARD\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nG28 Z I140 J140\\n\\n;===== 检测钢板 =================\\nDETECT_BED_PLATE\\n\\n;===== 深度清洁喷嘴 =================\\nG90\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 S{nozzle_temperature[initial_extruder] - 50}\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_1\\nM104 S{nozzle_temperature[initial_extruder] - 90}\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_2\\n\\n;===== 精回零 =================\\nM106 S255\\nM109 S{nozzle_temperature[initial_extruder] - 90}\\nM190 S{bed_temperature_initial_layer_single}\\nM107 P2\\nG90\\nG0 Z5 F10000\\nWAIT_CHAMBER_TEMP TIMEOUT=180\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{else} \\nG28 Z \\n{endif} \\n\\n\\n;===== 热床调平 =================\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{else} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 \\n{endif} \\n\\n\\n;===== 画起始线 =================\\nG90\\nG1 Z1.5\\nG0 X85 Y1 Z2 F18000\\nM109 S{nozzle_temperature_initial_layer[initial_extruder]}\\nG1 Z0.2\\nM83\\nG1 X185 E15 F360\\nG1 Z1.5\\n\\nG90\\nM106 S0", "machine_tool_change_time": "5", "max_layer_height": [ "0.56", From 286e9278c501bbd00d86ec0972e0db1b103a12ba Mon Sep 17 00:00:00 2001 From: Joyx-SnapMaker <1906956558@qq.com> Date: Fri, 8 May 2026 12:15:17 +0800 Subject: [PATCH 006/135] =?UTF-8?q?fix=EF=BC=9ATemporarily=20enable=20the?= =?UTF-8?q?=20ability=20to=20export=20gcode.3mf?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/slic3r/GUI/MainFrame.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index 96b1b08a3af..3cd9be66079 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -1733,8 +1733,8 @@ wxBoxSizer* MainFrame::create_side_tools() { SidePopup* p = new SidePopup(this); - if (wxGetApp().preset_bundle && !wxGetApp().preset_bundle->is_bbl_vendor()) - //if (0) + //if (wxGetApp().preset_bundle && !wxGetApp().preset_bundle->is_bbl_vendor()) + if (0) { // ThirdParty Buttons SideButton* export_gcode_btn = new SideButton(p, _L("Export G-code file"), ""); From 39508f2717ae588b1423c131463f708c39b278cf Mon Sep 17 00:00:00 2001 From: Joyx-SnapMaker <1906956558@qq.com> Date: Fri, 8 May 2026 15:17:13 +0800 Subject: [PATCH 007/135] =?UTF-8?q?Revert=20"fix=EF=BC=9ATemporarily=20ena?= =?UTF-8?q?ble=20the=20ability=20to=20export=20gcode.3mf"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit a3c3e5e5400e0ca68847d7b6817aa85fa835eabb. --- src/slic3r/GUI/MainFrame.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index 3cd9be66079..96b1b08a3af 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -1733,8 +1733,8 @@ wxBoxSizer* MainFrame::create_side_tools() { SidePopup* p = new SidePopup(this); - //if (wxGetApp().preset_bundle && !wxGetApp().preset_bundle->is_bbl_vendor()) - if (0) + if (wxGetApp().preset_bundle && !wxGetApp().preset_bundle->is_bbl_vendor()) + //if (0) { // ThirdParty Buttons SideButton* export_gcode_btn = new SideButton(p, _L("Export G-code file"), ""); From 07c14637872121673a1af127b342aad0319d3b78 Mon Sep 17 00:00:00 2001 From: Joyx-SnapMaker <1906956558@qq.com> Date: Thu, 16 Apr 2026 17:01:23 +0800 Subject: [PATCH 008/135] feat: add configurable option for top cover requirement --- src/libslic3r/Preset.cpp | 3 ++- src/libslic3r/PrintConfig.cpp | 6 ++++++ src/libslic3r/PrintConfig.hpp | 1 + src/slic3r/GUI/Tab.cpp | 1 + 4 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index 67742c0cb85..54735506988 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -924,7 +924,8 @@ static std::vector s_Preset_filament_options { "graphic_effect_plate_temp", "graphic_effect_plate_temp_initial_layer", // "bed_type", //BBS:temperature_vitrification - "temperature_vitrification", "reduce_fan_stop_start_freq","dont_slow_down_outer_wall", "slow_down_for_layer_cooling", "fan_min_speed", + "temperature_vitrification", "filament_is_high_temperature", "reduce_fan_stop_start_freq", "dont_slow_down_outer_wall", + "slow_down_for_layer_cooling", "fan_min_speed", "fan_max_speed", "enable_overhang_bridge_fan", "overhang_fan_speed", "overhang_fan_threshold", "close_fan_the_first_x_layers", "full_fan_speed_layer", "fan_cooling_layer_time", "slow_down_layer_time", "slow_down_min_speed", "filament_start_gcode", "filament_end_gcode", //exhaust fan control diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index 179cae85ab4..7483fe9746b 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -2391,6 +2391,12 @@ void PrintConfigDef::init_fff_params() def->mode = comAdvanced; def->set_default_value(new ConfigOptionBools { false }); + def = this->add("filament_is_high_temperature", coBools); + def->label = L("Is high-temperature filament"); + def->tooltip = L("Indicates whether this is a high-temperature filament that requires elevated printing temperatures."); + def->mode = comSimple; + def->set_default_value(new ConfigOptionBools{false}); + // BBS def = this->add("temperature_vitrification", coInts); def->label = L("Softening temperature"); diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index 0f39d80e412..9fa6aa00590 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -1183,6 +1183,7 @@ PRINT_CONFIG_CLASS_DEFINE( ((ConfigOptionFloats, filament_cost)) ((ConfigOptionStrings, default_filament_colour)) ((ConfigOptionInts, temperature_vitrification)) //BBS + ((ConfigOptionBools, filament_is_high_temperature)) ((ConfigOptionFloats, filament_max_volumetric_speed)) ((ConfigOptionInts, required_nozzle_HRC)) // BBS diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index 994625229d5..5b6ec2b9559 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -3699,6 +3699,7 @@ void TabFilament::build() optgroup->append_single_option_line("filament_cost"); //BBS optgroup->append_single_option_line("temperature_vitrification"); + optgroup->append_single_option_line("filament_is_high_temperature"); optgroup->append_single_option_line("idle_temperature"); Line line = { L("Recommended nozzle temperature"), L("Recommended nozzle temperature range of this filament. 0 means no set") }; line.append_option(optgroup->get_option("nozzle_temperature_range_low")); From 6e8caab798abcef20a506c09e82b405229ab51dd Mon Sep 17 00:00:00 2001 From: Joyx-SnapMaker <1906956558@qq.com> Date: Tue, 21 Apr 2026 18:01:25 +0800 Subject: [PATCH 009/135] feat: add translation # Conflicts: # src/libslic3r/Preset.cpp # src/libslic3r/PrintConfig.cpp --- localization/i18n/zh_CN/Snapmaker_Orca_zh_CN.po | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/localization/i18n/zh_CN/Snapmaker_Orca_zh_CN.po b/localization/i18n/zh_CN/Snapmaker_Orca_zh_CN.po index 6cc731d73af..3450984c8f6 100644 --- a/localization/i18n/zh_CN/Snapmaker_Orca_zh_CN.po +++ b/localization/i18n/zh_CN/Snapmaker_Orca_zh_CN.po @@ -10041,6 +10041,12 @@ msgstr "支撑材料" msgid "Support material is commonly used to print supports and support interfaces." msgstr "支撑材料通常用于打印支撑体和支撑接触面" +msgid "Is high-temperature filament" +msgstr "是否为高温耗材" + +msgid "Indicates whether this is a high-temperature filament that requires elevated printing temperatures." +msgstr "指示此耗材是否为需要较高打印温度的高温耗材。" + msgid "Softening temperature" msgstr "软化温度" From ee279f97124f34eac8612ac101bf3c4bb0c9c01f Mon Sep 17 00:00:00 2001 From: Joyx-SnapMaker <1906956558@qq.com> Date: Fri, 24 Apr 2026 16:01:23 +0800 Subject: [PATCH 010/135] feat: Add the "Is It a High-Temperature Consumable" attribute to the consumable filament, and simultaneously modify the printer's initial gcode. --- .../Snapmaker/filament/Snapmaker PETG HF @U1 0.2 nozzle.json | 3 ++- .../profiles/Snapmaker/machine/Snapmaker U1 (0.2 nozzle).json | 4 ++-- .../profiles/Snapmaker/machine/Snapmaker U1 (0.4 nozzle).json | 4 ++-- .../profiles/Snapmaker/machine/Snapmaker U1 (0.6 nozzle).json | 4 ++-- .../profiles/Snapmaker/machine/Snapmaker U1 (0.8 nozzle).json | 4 ++-- 5 files changed, 10 insertions(+), 9 deletions(-) diff --git a/resources/profiles/Snapmaker/filament/Snapmaker PETG HF @U1 0.2 nozzle.json b/resources/profiles/Snapmaker/filament/Snapmaker PETG HF @U1 0.2 nozzle.json index cc0f417368b..18c5143f559 100644 --- a/resources/profiles/Snapmaker/filament/Snapmaker PETG HF @U1 0.2 nozzle.json +++ b/resources/profiles/Snapmaker/filament/Snapmaker PETG HF @U1 0.2 nozzle.json @@ -107,5 +107,6 @@ ], "graphic_effect_plate_temp": ["0"], "graphic_effect_plate_temp_initial_layer": ["0"], - "filament_z_hop_types": ["Slope Lift"] + "filament_z_hop_types": ["Slope Lift"], + "filament_is_high_temperature": ["0"] } \ No newline at end of file diff --git a/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.2 nozzle).json b/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.2 nozzle).json index 98a2586871e..7456699be5c 100644 --- a/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.2 nozzle).json +++ b/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.2 nozzle).json @@ -35,7 +35,7 @@ "0", "0" ], - "machine_end_gcode": "{if print_sequence == \\\"by object\\\"}\\nG91\\nG1 X2 Y2 Z1 F24000\\nG90\\nG1 Z{max_layer_z+2} F600\\n{endif}\\nPRINT_END\\nTIMELAPSE_STOP", + "machine_end_gcode": " PRINT_END\nTIMELAPSE_STOP", "machine_max_jerk_z": [ "3", "0.4" @@ -48,7 +48,7 @@ "20", "12" ], - "machine_start_gcode": ";===== date: 20260128 =====================\\n\\nPRINT_START\\nDEFECT_DETECTION_START\\nSET_PRINT_STATS_INFO TOTAL_LAYER={total_layer_count} CURRENT_LAYER=0\\nTIMELAPSE_START\\nM140 S{bed_temperature_initial_layer_single}\\nM104 T{initial_extruder} S140\\nM204 S10000\\n\\nG28 X Y\\nDEFECT_DETECT_NOODLE_FIRST\\n;===== 床面异物检测 ========\\nT{initial_extruder}\\nG90\\nDEFECT_DETECTION_DETECT_BED\\n;===== 取放头检测 =================\\nSM_PRINT_CHECK_SWITCH_EXTRUDER\\n\\n;===== 自动进料 & 挤出流量 & 预挤出 ======================\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=1 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=0\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=0\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=2 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=1\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=1\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=3 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=2\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=2\\nSM_PRINT_AUTO_FEED EXTRUDER=3\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=3\\nM104 S0 T0 A0\\nM104 S0 T1 A0\\nM104 S0 T2 A0\\nM104 S0 T3 A0\\nM104 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\n\\n;===== 粗回零 =================\\nT{initial_extruder}\\nM106 S255\\nM106 P2 S0\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\nROUGHLY_CLEAN_NOZZLE_WITH_DISCARD\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nG28 Z I140 J140\\n\\n;===== 检测钢板 =================\\nDETECT_BED_PLATE\\n\\n;===== 深度清洁喷嘴 =================\\nG90\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 S{nozzle_temperature[initial_extruder] - 50}\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_1\\nM104 S{nozzle_temperature[initial_extruder] - 90}\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_2\\n\\n;===== 精回零 =================\\nM106 S255\\nM109 S{nozzle_temperature[initial_extruder] - 90}\\nM190 S{bed_temperature_initial_layer_single}\\nM107 P2\\nG90\\nG0 Z5 F10000\\n\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{else} \\nG28 Z \\n{endif} \\n\\n\\n;===== 热床调平 =================\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{else} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 \\n{endif} \\n\\n\\n;===== 画起始线 =================\\nG90\\nG1 Z1.5\\nG0 X85 Y1 Z2 F18000\\nM109 S{nozzle_temperature_initial_layer[initial_extruder]}\\nG1 Z0.2\\nM83\\nG1 X185 E15 F360\\nG1 Z1.5\\n\\nG90\\nM106 S0", + "machine_start_gcode": ";===== date: 20260128 =====================\\n\\nPRINT_START\\nDEFECT_DETECTION_START\\nSET_PRINT_STATS_INFO TOTAL_LAYER={total_layer_count} CURRENT_LAYER=0\\nTIMELAPSE_START\\nM140 S{bed_temperature_initial_layer_single}\\nM104 T{initial_extruder} S140\\nM204 S10000\\n{if filament_is_high_temperature[0]}\\n; 保温\\n\tSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=180\\n{else}\\n{if temperature_vitrification[0]<=50}\\n; 强冷\\nSET_PURIFIER_MODE MODE=1 DESIRE_TEMP=42 ALARM_TEMP=45 FAN_SPEED=0.6\\nDELAY_OFF=0\t\t\\n{else}\\n; 弱冷\\nSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=0 FAN_SPEED=0.6 DELAY_OFF=180\t\t\\n{endif}\\n{endif}\\nG28 X Y\\n;===== 床面异物检测 ========\\nT{initial_extruder}\\nG90\\nDEFECT_DETECTION_DETECT_BED\\n;===== 取放头检测 =================\\nSM_PRINT_CHECK_SWITCH_EXTRUDER\\n\\n;===== 自动进料 & 挤出流量 & 预挤出 ======================\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=1 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=0\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=0\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=2 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=1\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=1\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=3 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=2\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=2\\nSM_PRINT_AUTO_FEED EXTRUDER=3\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=3\\nM104 S0 T0 A0\\nM104 S0 T1 A0\\nM104 S0 T2 A0\\nM104 S0 T3 A0\\nM104 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\n\\n;===== 粗回零 =================\\nT{initial_extruder}\\nM106 S255\\nM106 P2 S0\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\nROUGHLY_CLEAN_NOZZLE_WITH_DISCARD\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nG28 Z I140 J140\\n\\n;===== 检测钢板 =================\\nDETECT_BED_PLATE\\n\\n;===== 深度清洁喷嘴 =================\\nG90\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 S{nozzle_temperature[initial_extruder] - 50}\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_1\\nM104 S{nozzle_temperature[initial_extruder] - 90}\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_2\\n\\n;===== 精回零 =================\\nM106 S255\\nM109 S{nozzle_temperature[initial_extruder] - 90}\\nM190 S{bed_temperature_initial_layer_single}\\nM107 P2\\nG90\\nG0 Z5 F10000\\nWAIT_CHAMBER_TEMP TIMEOUT=180\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{else} \\nG28 Z \\n{endif} \\n\\n\\n;===== 热床调平 =================\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{else} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 \\n{endif} \\n\\n\\n;===== 画起始线 =================\\nG90\\nG1 Z1.5\\nG0 X85 Y1 Z2 F18000\\nM109 S{nozzle_temperature_initial_layer[initial_extruder]}\\nG1 Z0.2\\nM83\\nG1 X185 E15 F360\\nG1 Z1.5\\n\\nG90\\nM106 S0", "machine_tool_change_time": "5", "max_layer_height": [ "0.14", diff --git a/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.4 nozzle).json b/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.4 nozzle).json index 06b63eb9af4..95ff6d82d6b 100644 --- a/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.4 nozzle).json +++ b/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.4 nozzle).json @@ -35,7 +35,7 @@ "0", "0" ], - "machine_end_gcode": "{if print_sequence == \\\"by object\\\"}\\nG91\\nG1 X2 Y2 Z1 F24000\\nG90\\nG1 Z{max_layer_z+2} F600\\n{endif}\\nPRINT_END\\nTIMELAPSE_STOP", + "machine_end_gcode": " PRINT_END\nTIMELAPSE_STOP", "machine_max_jerk_z": [ "3", "0.4" @@ -48,7 +48,7 @@ "20", "12" ], - "machine_start_gcode": ";===== date: 20260128 =====================\\n\\nPRINT_START\\nDEFECT_DETECTION_START\\nSET_PRINT_STATS_INFO TOTAL_LAYER={total_layer_count} CURRENT_LAYER=0\\nTIMELAPSE_START\\nM140 S{bed_temperature_initial_layer_single}\\nM104 T{initial_extruder} S140\\nM204 S10000\\n\\nG28 X Y\\nDEFECT_DETECT_NOODLE_FIRST\\n;===== 床面异物检测 ========\\nT{initial_extruder}\\nG90\\nDEFECT_DETECTION_DETECT_BED\\n;===== 取放头检测 =================\\nSM_PRINT_CHECK_SWITCH_EXTRUDER\\n\\n;===== 自动进料 & 挤出流量 & 预挤出 ======================\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=1 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=0\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=0\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=2 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=1\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=1\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=3 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=2\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=2\\nSM_PRINT_AUTO_FEED EXTRUDER=3\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=3\\nM104 S0 T0 A0\\nM104 S0 T1 A0\\nM104 S0 T2 A0\\nM104 S0 T3 A0\\nM104 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\n\\n;===== 粗回零 =================\\nT{initial_extruder}\\nM106 S255\\nM106 P2 S0\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\nROUGHLY_CLEAN_NOZZLE_WITH_DISCARD\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nG28 Z I140 J140\\n\\n;===== 检测钢板 =================\\nDETECT_BED_PLATE\\n\\n;===== 深度清洁喷嘴 =================\\nG90\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 S{nozzle_temperature[initial_extruder] - 50}\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_1\\nM104 S{nozzle_temperature[initial_extruder] - 90}\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_2\\n\\n;===== 精回零 =================\\nM106 S255\\nM109 S{nozzle_temperature[initial_extruder] - 90}\\nM190 S{bed_temperature_initial_layer_single}\\nM107 P2\\nG90\\nG0 Z5 F10000\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{else} \\nG28 Z \\n{endif} \\n\\n\\n;===== 热床调平 =================\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{else} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 \\n{endif} \\n\\n\\n;===== 画起始线 =================\\nG90\\nG1 Z1.5\\nG0 X85 Y1 Z2 F18000\\nM109 S{nozzle_temperature_initial_layer[initial_extruder]}\\nG1 Z0.2\\nM83\\nG1 X185 E15 F360\\nG1 Z1.5\\n\\nG90\\nM106 S0", + "machine_start_gcode": ";===== date: 20260128 =====================\\n\\nPRINT_START\\nDEFECT_DETECTION_START\\nSET_PRINT_STATS_INFO TOTAL_LAYER={total_layer_count} CURRENT_LAYER=0\\nTIMELAPSE_START\\nM140 S{bed_temperature_initial_layer_single}\\nM104 T{initial_extruder} S140\\nM204 S10000\\n{if filament_is_high_temperature[0]}\\n; 保温\\n\tSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=180\\n{else}\\n{if temperature_vitrification[0]<=50}\\n; 强冷\\nSET_PURIFIER_MODE MODE=1 DESIRE_TEMP=42 ALARM_TEMP=45 FAN_SPEED=0.6\\nDELAY_OFF=0\t\t\\n{else}\\n; 弱冷\\nSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=0 FAN_SPEED=0.6 DELAY_OFF=180\t\t\\n{endif}\\n{endif}\\n\\nG28 X Y\\n;===== 床面异物检测 ========\\nT{initial_extruder}\\nG90\\nDEFECT_DETECTION_DETECT_BED\\n;===== 取放头检测 =================\\nSM_PRINT_CHECK_SWITCH_EXTRUDER\\n\\n;===== 自动进料 & 挤出流量 & 预挤出 ======================\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=1 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=0\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=0\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=2 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=1\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=1\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=3 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=2\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=2\\nSM_PRINT_AUTO_FEED EXTRUDER=3\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=3\\nM104 S0 T0 A0\\nM104 S0 T1 A0\\nM104 S0 T2 A0\\nM104 S0 T3 A0\\nM104 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\n\\n;===== 粗回零 =================\\nT{initial_extruder}\\nM106 S255\\nM106 P2 S0\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\nROUGHLY_CLEAN_NOZZLE_WITH_DISCARD\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nG28 Z I140 J140\\n\\n;===== 检测钢板 =================\\nDETECT_BED_PLATE\\n\\n;===== 深度清洁喷嘴 =================\\nG90\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 S{nozzle_temperature[initial_extruder] - 50}\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_1\\nM104 S{nozzle_temperature[initial_extruder] - 90}\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_2\\n\\n;===== 精回零 =================\\nM106 S255\\nM109 S{nozzle_temperature[initial_extruder] - 90}\\nM190 S{bed_temperature_initial_layer_single}\\nM107 P2\\nG90\\nG0 Z5 F10000\\nWAIT_CHAMBER_TEMP TIMEOUT=180\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{else} \\nG28 Z \\n{endif} \\n\\n\\n;===== 热床调平 =================\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{else} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 \\n{endif} \\n\\n\\n;===== 画起始线 =================\\nG90\\nG1 Z1.5\\nG0 X85 Y1 Z2 F18000\\nM109 S{nozzle_temperature_initial_layer[initial_extruder]}\\nG1 Z0.2\\nM83\\nG1 X185 E15 F360\\nG1 Z1.5\\n\\nG90\\nM106 S0", "machine_tool_change_time": "5", "max_layer_height": [ "0.32", diff --git a/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.6 nozzle).json b/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.6 nozzle).json index 9c54ab2e190..40d8bf7e5da 100644 --- a/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.6 nozzle).json +++ b/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.6 nozzle).json @@ -35,7 +35,7 @@ "0", "0" ], - "machine_end_gcode": "{if print_sequence == \\\"by object\\\"}\\nG91\\nG1 X2 Y2 Z1 F24000\\nG90\\nG1 Z{max_layer_z+2} F600\\n{endif}\\nPRINT_END\\nTIMELAPSE_STOP", + "machine_end_gcode": " PRINT_END\nTIMELAPSE_STOP", "machine_max_jerk_z": [ "3", "0.4" @@ -48,7 +48,7 @@ "20", "12" ], - "machine_start_gcode": ";===== date: 20260128 =====================\\n\\nPRINT_START\\nDEFECT_DETECTION_START\\nSET_PRINT_STATS_INFO TOTAL_LAYER={total_layer_count} CURRENT_LAYER=0\\nTIMELAPSE_START\\nM140 S{bed_temperature_initial_layer_single}\\nM104 T{initial_extruder} S140\\nM204 S10000\\n\\nG28 X Y\\nDEFECT_DETECT_NOODLE_FIRST\\n;===== 床面异物检测 ========\\nT{initial_extruder}\\nG90\\nDEFECT_DETECTION_DETECT_BED\\n;===== 取放头检测 =================\\nSM_PRINT_CHECK_SWITCH_EXTRUDER\\n\\n;===== 自动进料 & 挤出流量 & 预挤出 ======================\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=1 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=0\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=0\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=2 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=1\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=1\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=3 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=2\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=2\\nSM_PRINT_AUTO_FEED EXTRUDER=3\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=3\\nM104 S0 T0 A0\\nM104 S0 T1 A0\\nM104 S0 T2 A0\\nM104 S0 T3 A0\\nM104 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\n\\n;===== 粗回零 =================\\nT{initial_extruder}\\nM106 S255\\nM106 P2 S0\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\nROUGHLY_CLEAN_NOZZLE_WITH_DISCARD\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nG28 Z I140 J140\\n\\n;===== 检测钢板 =================\\nDETECT_BED_PLATE\\n\\n;===== 深度清洁喷嘴 =================\\nG90\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 S{nozzle_temperature[initial_extruder] - 50}\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_1\\nM104 S{nozzle_temperature[initial_extruder] - 90}\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_2\\n\\n;===== 精回零 =================\\nM106 S255\\nM109 S{nozzle_temperature[initial_extruder] - 90}\\nM190 S{bed_temperature_initial_layer_single}\\nM107 P2\\nG90\\nG0 Z5 F10000\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{else} \\nG28 Z \\n{endif} \\n\\n\\n;===== 热床调平 =================\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{else} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 \\n{endif} \\n\\n\\n;===== 画起始线 =================\\nG90\\nG1 Z1.5\\nG0 X85 Y1 Z2 F18000\\nM109 S{nozzle_temperature_initial_layer[initial_extruder]}\\nG1 Z0.2\\nM83\\nG1 X185 E15 F360\\nG1 Z1.5\\n\\nG90\\nM106 S0", + "machine_start_gcode": ";===== date: 20260128 =====================\\n\\nPRINT_START\\nDEFECT_DETECTION_START\\nSET_PRINT_STATS_INFO TOTAL_LAYER={total_layer_count} CURRENT_LAYER=0\\nTIMELAPSE_START\\nM140 S{bed_temperature_initial_layer_single}\\nM104 T{initial_extruder} S140\\nM204 S10000\\n{if filament_is_high_temperature[0]}\\n; 保温\\n\tSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=180\\n{else}\\n{if temperature_vitrification[0]<=50}\\n; 强冷\\nSET_PURIFIER_MODE MODE=1 DESIRE_TEMP=42 ALARM_TEMP=45 FAN_SPEED=0.6\\nDELAY_OFF=0\t\t\\n{else}\\n; 弱冷\\nSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=0 FAN_SPEED=0.6 DELAY_OFF=180\t\t\\n{endif}\\n{endif}\\nG28 X Y\\n;===== 床面异物检测 ========\\nT{initial_extruder}\\nG90\\nDEFECT_DETECTION_DETECT_BED\\n;===== 取放头检测 =================\\nSM_PRINT_CHECK_SWITCH_EXTRUDER\\n\\n;===== 自动进料 & 挤出流量 & 预挤出 ======================\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=1 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=0\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=0\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=2 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=1\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=1\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=3 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=2\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=2\\nSM_PRINT_AUTO_FEED EXTRUDER=3\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=3\\nM104 S0 T0 A0\\nM104 S0 T1 A0\\nM104 S0 T2 A0\\nM104 S0 T3 A0\\nM104 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\n\\n;===== 粗回零 =================\\nT{initial_extruder}\\nM106 S255\\nM106 P2 S0\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\nROUGHLY_CLEAN_NOZZLE_WITH_DISCARD\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nG28 Z I140 J140\\n\\n;===== 检测钢板 =================\\nDETECT_BED_PLATE\\n\\n;===== 深度清洁喷嘴 =================\\nG90\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 S{nozzle_temperature[initial_extruder] - 50}\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_1\\nM104 S{nozzle_temperature[initial_extruder] - 90}\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_2\\n\\n;===== 精回零 =================\\nM106 S255\\nM109 S{nozzle_temperature[initial_extruder] - 90}\\nM190 S{bed_temperature_initial_layer_single}\\nM107 P2\\nG90\\nG0 Z5 F10000\\nWAIT_CHAMBER_TEMP TIMEOUT=180\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{else} \\nG28 Z \\n{endif} \\n\\n\\n;===== 热床调平 =================\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{else} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 \\n{endif} \\n\\n\\n;===== 画起始线 =================\\nG90\\nG1 Z1.5\\nG0 X85 Y1 Z2 F18000\\nM109 S{nozzle_temperature_initial_layer[initial_extruder]}\\nG1 Z0.2\\nM83\\nG1 X185 E15 F360\\nG1 Z1.5\\n\\nG90\\nM106 S0", "machine_tool_change_time": "5", "max_layer_height": [ "0.42", diff --git a/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.8 nozzle).json b/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.8 nozzle).json index 82d0b26ac24..94d6eeeafe6 100644 --- a/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.8 nozzle).json +++ b/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.8 nozzle).json @@ -35,7 +35,7 @@ "0", "0" ], - "machine_end_gcode": "{if print_sequence == \\\"by object\\\"}\\nG91\\nG1 X2 Y2 Z1 F24000\\nG90\\nG1 Z{max_layer_z+2} F600\\n{endif}\\nPRINT_END\\nTIMELAPSE_STOP", + "machine_end_gcode": " PRINT_END\nTIMELAPSE_STOP", "machine_max_jerk_z": [ "3", "0.4" @@ -48,7 +48,7 @@ "20", "12" ], - "machine_start_gcode": ";===== date: 20260128 =====================\\n\\nPRINT_START\\nDEFECT_DETECTION_START\\nSET_PRINT_STATS_INFO TOTAL_LAYER={total_layer_count} CURRENT_LAYER=0\\nTIMELAPSE_START\\nM140 S{bed_temperature_initial_layer_single}\\nM104 T{initial_extruder} S140\\nM204 S10000\\n\\nG28 X Y\\nDEFECT_DETECT_NOODLE_FIRST\\n;===== 床面异物检测 ========\\nT{initial_extruder}\\nG90\\nDEFECT_DETECTION_DETECT_BED\\n;===== 取放头检测 =================\\nSM_PRINT_CHECK_SWITCH_EXTRUDER\\n\\n;===== 自动进料 & 挤出流量 & 预挤出 ======================\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=1 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=0\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=0\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=2 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=1\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=1\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=3 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=2\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=2\\nSM_PRINT_AUTO_FEED EXTRUDER=3\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=3\\nM104 S0 T0 A0\\nM104 S0 T1 A0\\nM104 S0 T2 A0\\nM104 S0 T3 A0\\nM104 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\n\\n;===== 粗回零 =================\\nT{initial_extruder}\\nM106 S255\\nM106 P2 S0\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\nROUGHLY_CLEAN_NOZZLE_WITH_DISCARD\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nG28 Z I140 J140\\n\\n;===== 检测钢板 =================\\nDETECT_BED_PLATE\\n\\n;===== 深度清洁喷嘴 =================\\nG90\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 S{nozzle_temperature[initial_extruder] - 50}\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_1\\nM104 S{nozzle_temperature[initial_extruder] - 90}\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_2\\n\\n;===== 精回零 =================\\nM106 S255\\nM109 S{nozzle_temperature[initial_extruder] - 90}\\nM190 S{bed_temperature_initial_layer_single}\\nM107 P2\\nG90\\nG0 Z5 F10000\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{else} \\nG28 Z \\n{endif} \\n\\n\\n;===== 热床调平 =================\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{else} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 \\n{endif} \\n\\n\\n;===== 画起始线 =================\\nG90\\nG1 Z1.5\\nG0 X85 Y1 Z2 F18000\\nM109 S{nozzle_temperature_initial_layer[initial_extruder]}\\nG1 Z0.2\\nM83\\nG1 X185 E15 F360\\nG1 Z1.5\\n\\nG90\\nM106 S0", + "machine_start_gcode": ";===== date: 20260128 =====================\\n\\nPRINT_START\\nDEFECT_DETECTION_START\\nSET_PRINT_STATS_INFO TOTAL_LAYER={total_layer_count} CURRENT_LAYER=0\\nTIMELAPSE_START\\nM140 S{bed_temperature_initial_layer_single}\\nM104 T{initial_extruder} S140\\nM204 S10000\\n{if filament_is_high_temperature[0]}\\n; 保温\\n\tSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=180\\n{else}\\n{if temperature_vitrification[0]<=50}\\n; 强冷\\nSET_PURIFIER_MODE MODE=1 DESIRE_TEMP=42 ALARM_TEMP=45 FAN_SPEED=0.6\\nDELAY_OFF=0\t\t\\n{else}\\n; 弱冷\\nSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=0 FAN_SPEED=0.6 DELAY_OFF=180\t\t\\n{endif}\\n{endif}\\nG28 X Y\\n;===== 床面异物检测 ========\\nT{initial_extruder}\\nG90\\nDEFECT_DETECTION_DETECT_BED\\n;===== 取放头检测 =================\\nSM_PRINT_CHECK_SWITCH_EXTRUDER\\n\\n;===== 自动进料 & 挤出流量 & 预挤出 ======================\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=1 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=0\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=0\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=2 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=1\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=1\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=3 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=2\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=2\\nSM_PRINT_AUTO_FEED EXTRUDER=3\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=3\\nM104 S0 T0 A0\\nM104 S0 T1 A0\\nM104 S0 T2 A0\\nM104 S0 T3 A0\\nM104 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\n\\n;===== 粗回零 =================\\nT{initial_extruder}\\nM106 S255\\nM106 P2 S0\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\nROUGHLY_CLEAN_NOZZLE_WITH_DISCARD\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nG28 Z I140 J140\\n\\n;===== 检测钢板 =================\\nDETECT_BED_PLATE\\n\\n;===== 深度清洁喷嘴 =================\\nG90\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 S{nozzle_temperature[initial_extruder] - 50}\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_1\\nM104 S{nozzle_temperature[initial_extruder] - 90}\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_2\\n\\n;===== 精回零 =================\\nM106 S255\\nM109 S{nozzle_temperature[initial_extruder] - 90}\\nM190 S{bed_temperature_initial_layer_single}\\nM107 P2\\nG90\\nG0 Z5 F10000\\nWAIT_CHAMBER_TEMP TIMEOUT=180\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{else} \\nG28 Z \\n{endif} \\n\\n\\n;===== 热床调平 =================\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{else} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 \\n{endif} \\n\\n\\n;===== 画起始线 =================\\nG90\\nG1 Z1.5\\nG0 X85 Y1 Z2 F18000\\nM109 S{nozzle_temperature_initial_layer[initial_extruder]}\\nG1 Z0.2\\nM83\\nG1 X185 E15 F360\\nG1 Z1.5\\n\\nG90\\nM106 S0", "machine_tool_change_time": "5", "max_layer_height": [ "0.56", From cb3d69d9a9ecd2620cd3a658aa5d4d824acdd65d Mon Sep 17 00:00:00 2001 From: Joyx-SnapMaker <1906956558@qq.com> Date: Mon, 27 Apr 2026 18:26:51 +0800 Subject: [PATCH 011/135] =?UTF-8?q?Fix=EF=BC=9ACorrect=20the=20incorrect?= =?UTF-8?q?=20G-code=20code?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../profiles/Snapmaker/machine/Snapmaker U1 (0.2 nozzle).json | 2 +- .../profiles/Snapmaker/machine/Snapmaker U1 (0.4 nozzle).json | 2 +- .../profiles/Snapmaker/machine/Snapmaker U1 (0.6 nozzle).json | 2 +- .../profiles/Snapmaker/machine/Snapmaker U1 (0.8 nozzle).json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.2 nozzle).json b/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.2 nozzle).json index 7456699be5c..48c14548825 100644 --- a/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.2 nozzle).json +++ b/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.2 nozzle).json @@ -48,7 +48,7 @@ "20", "12" ], - "machine_start_gcode": ";===== date: 20260128 =====================\\n\\nPRINT_START\\nDEFECT_DETECTION_START\\nSET_PRINT_STATS_INFO TOTAL_LAYER={total_layer_count} CURRENT_LAYER=0\\nTIMELAPSE_START\\nM140 S{bed_temperature_initial_layer_single}\\nM104 T{initial_extruder} S140\\nM204 S10000\\n{if filament_is_high_temperature[0]}\\n; 保温\\n\tSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=180\\n{else}\\n{if temperature_vitrification[0]<=50}\\n; 强冷\\nSET_PURIFIER_MODE MODE=1 DESIRE_TEMP=42 ALARM_TEMP=45 FAN_SPEED=0.6\\nDELAY_OFF=0\t\t\\n{else}\\n; 弱冷\\nSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=0 FAN_SPEED=0.6 DELAY_OFF=180\t\t\\n{endif}\\n{endif}\\nG28 X Y\\n;===== 床面异物检测 ========\\nT{initial_extruder}\\nG90\\nDEFECT_DETECTION_DETECT_BED\\n;===== 取放头检测 =================\\nSM_PRINT_CHECK_SWITCH_EXTRUDER\\n\\n;===== 自动进料 & 挤出流量 & 预挤出 ======================\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=1 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=0\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=0\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=2 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=1\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=1\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=3 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=2\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=2\\nSM_PRINT_AUTO_FEED EXTRUDER=3\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=3\\nM104 S0 T0 A0\\nM104 S0 T1 A0\\nM104 S0 T2 A0\\nM104 S0 T3 A0\\nM104 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\n\\n;===== 粗回零 =================\\nT{initial_extruder}\\nM106 S255\\nM106 P2 S0\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\nROUGHLY_CLEAN_NOZZLE_WITH_DISCARD\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nG28 Z I140 J140\\n\\n;===== 检测钢板 =================\\nDETECT_BED_PLATE\\n\\n;===== 深度清洁喷嘴 =================\\nG90\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 S{nozzle_temperature[initial_extruder] - 50}\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_1\\nM104 S{nozzle_temperature[initial_extruder] - 90}\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_2\\n\\n;===== 精回零 =================\\nM106 S255\\nM109 S{nozzle_temperature[initial_extruder] - 90}\\nM190 S{bed_temperature_initial_layer_single}\\nM107 P2\\nG90\\nG0 Z5 F10000\\nWAIT_CHAMBER_TEMP TIMEOUT=180\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{else} \\nG28 Z \\n{endif} \\n\\n\\n;===== 热床调平 =================\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{else} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 \\n{endif} \\n\\n\\n;===== 画起始线 =================\\nG90\\nG1 Z1.5\\nG0 X85 Y1 Z2 F18000\\nM109 S{nozzle_temperature_initial_layer[initial_extruder]}\\nG1 Z0.2\\nM83\\nG1 X185 E15 F360\\nG1 Z1.5\\n\\nG90\\nM106 S0", + "machine_start_gcode": ";===== date: 20260128 =====================\\n\\nPRINT_START\\nDEFECT_DETECTION_START\\nSET_PRINT_STATS_INFO TOTAL_LAYER={total_layer_count} CURRENT_LAYER=0\\nTIMELAPSE_START\\nM140 S{bed_temperature_initial_layer_single}\\nM104 T{initial_extruder} S140\\nM204 S10000\\n{if filament_is_high_temperature[0]}\\n; 保温\\n\tSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=180\\n{else}\\n{if temperature_vitrification[0]<=50}\\n; 强冷\\nSET_PURIFIER_MODE MODE=1 DESIRE_TEMP=42 ALARM_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=0\t\t\\n{else}\\n; 弱冷\\nSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=0 FAN_SPEED=0.6 DELAY_OFF=180\t\t\\n{endif}\\n{endif}\\nG28 X Y\\n;===== 床面异物检测 ========\\nT{initial_extruder}\\nG90\\nDEFECT_DETECTION_DETECT_BED\\n;===== 取放头检测 =================\\nSM_PRINT_CHECK_SWITCH_EXTRUDER\\n\\n;===== 自动进料 & 挤出流量 & 预挤出 ======================\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=1 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=0\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=0\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=2 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=1\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=1\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=3 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=2\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=2\\nSM_PRINT_AUTO_FEED EXTRUDER=3\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=3\\nM104 S0 T0 A0\\nM104 S0 T1 A0\\nM104 S0 T2 A0\\nM104 S0 T3 A0\\nM104 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\n\\n;===== 粗回零 =================\\nT{initial_extruder}\\nM106 S255\\nM106 P2 S0\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\nROUGHLY_CLEAN_NOZZLE_WITH_DISCARD\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nG28 Z I140 J140\\n\\n;===== 检测钢板 =================\\nDETECT_BED_PLATE\\n\\n;===== 深度清洁喷嘴 =================\\nG90\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 S{nozzle_temperature[initial_extruder] - 50}\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_1\\nM104 S{nozzle_temperature[initial_extruder] - 90}\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_2\\n\\n;===== 精回零 =================\\nM106 S255\\nM109 S{nozzle_temperature[initial_extruder] - 90}\\nM190 S{bed_temperature_initial_layer_single}\\nM107 P2\\nG90\\nG0 Z5 F10000\\nWAIT_CHAMBER_TEMP TIMEOUT=180\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{else} \\nG28 Z \\n{endif} \\n\\n\\n;===== 热床调平 =================\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{else} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 \\n{endif} \\n\\n\\n;===== 画起始线 =================\\nG90\\nG1 Z1.5\\nG0 X85 Y1 Z2 F18000\\nM109 S{nozzle_temperature_initial_layer[initial_extruder]}\\nG1 Z0.2\\nM83\\nG1 X185 E15 F360\\nG1 Z1.5\\n\\nG90\\nM106 S0", "machine_tool_change_time": "5", "max_layer_height": [ "0.14", diff --git a/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.4 nozzle).json b/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.4 nozzle).json index 95ff6d82d6b..2d1c8ae5ac5 100644 --- a/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.4 nozzle).json +++ b/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.4 nozzle).json @@ -48,7 +48,7 @@ "20", "12" ], - "machine_start_gcode": ";===== date: 20260128 =====================\\n\\nPRINT_START\\nDEFECT_DETECTION_START\\nSET_PRINT_STATS_INFO TOTAL_LAYER={total_layer_count} CURRENT_LAYER=0\\nTIMELAPSE_START\\nM140 S{bed_temperature_initial_layer_single}\\nM104 T{initial_extruder} S140\\nM204 S10000\\n{if filament_is_high_temperature[0]}\\n; 保温\\n\tSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=180\\n{else}\\n{if temperature_vitrification[0]<=50}\\n; 强冷\\nSET_PURIFIER_MODE MODE=1 DESIRE_TEMP=42 ALARM_TEMP=45 FAN_SPEED=0.6\\nDELAY_OFF=0\t\t\\n{else}\\n; 弱冷\\nSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=0 FAN_SPEED=0.6 DELAY_OFF=180\t\t\\n{endif}\\n{endif}\\n\\nG28 X Y\\n;===== 床面异物检测 ========\\nT{initial_extruder}\\nG90\\nDEFECT_DETECTION_DETECT_BED\\n;===== 取放头检测 =================\\nSM_PRINT_CHECK_SWITCH_EXTRUDER\\n\\n;===== 自动进料 & 挤出流量 & 预挤出 ======================\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=1 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=0\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=0\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=2 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=1\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=1\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=3 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=2\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=2\\nSM_PRINT_AUTO_FEED EXTRUDER=3\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=3\\nM104 S0 T0 A0\\nM104 S0 T1 A0\\nM104 S0 T2 A0\\nM104 S0 T3 A0\\nM104 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\n\\n;===== 粗回零 =================\\nT{initial_extruder}\\nM106 S255\\nM106 P2 S0\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\nROUGHLY_CLEAN_NOZZLE_WITH_DISCARD\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nG28 Z I140 J140\\n\\n;===== 检测钢板 =================\\nDETECT_BED_PLATE\\n\\n;===== 深度清洁喷嘴 =================\\nG90\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 S{nozzle_temperature[initial_extruder] - 50}\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_1\\nM104 S{nozzle_temperature[initial_extruder] - 90}\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_2\\n\\n;===== 精回零 =================\\nM106 S255\\nM109 S{nozzle_temperature[initial_extruder] - 90}\\nM190 S{bed_temperature_initial_layer_single}\\nM107 P2\\nG90\\nG0 Z5 F10000\\nWAIT_CHAMBER_TEMP TIMEOUT=180\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{else} \\nG28 Z \\n{endif} \\n\\n\\n;===== 热床调平 =================\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{else} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 \\n{endif} \\n\\n\\n;===== 画起始线 =================\\nG90\\nG1 Z1.5\\nG0 X85 Y1 Z2 F18000\\nM109 S{nozzle_temperature_initial_layer[initial_extruder]}\\nG1 Z0.2\\nM83\\nG1 X185 E15 F360\\nG1 Z1.5\\n\\nG90\\nM106 S0", + "machine_start_gcode": ";===== date: 20260128 =====================\\n\\nPRINT_START\\nDEFECT_DETECTION_START\\nSET_PRINT_STATS_INFO TOTAL_LAYER={total_layer_count} CURRENT_LAYER=0\\nTIMELAPSE_START\\nM140 S{bed_temperature_initial_layer_single}\\nM104 T{initial_extruder} S140\\nM204 S10000\\n{if filament_is_high_temperature[0]}\\n; 保温\\n\tSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=180\\n{else}\\n{if temperature_vitrification[0]<=50}\\n; 强冷\\nSET_PURIFIER_MODE MODE=1 DESIRE_TEMP=42 ALARM_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=0\t\t\\n{else}\\n; 弱冷\\nSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=0 FAN_SPEED=0.6 DELAY_OFF=180\t\t\\n{endif}\\n{endif}\\n\\nG28 X Y\\n;===== 床面异物检测 ========\\nT{initial_extruder}\\nG90\\nDEFECT_DETECTION_DETECT_BED\\n;===== 取放头检测 =================\\nSM_PRINT_CHECK_SWITCH_EXTRUDER\\n\\n;===== 自动进料 & 挤出流量 & 预挤出 ======================\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=1 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=0\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=0\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=2 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=1\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=1\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=3 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=2\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=2\\nSM_PRINT_AUTO_FEED EXTRUDER=3\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=3\\nM104 S0 T0 A0\\nM104 S0 T1 A0\\nM104 S0 T2 A0\\nM104 S0 T3 A0\\nM104 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\n\\n;===== 粗回零 =================\\nT{initial_extruder}\\nM106 S255\\nM106 P2 S0\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\nROUGHLY_CLEAN_NOZZLE_WITH_DISCARD\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nG28 Z I140 J140\\n\\n;===== 检测钢板 =================\\nDETECT_BED_PLATE\\n\\n;===== 深度清洁喷嘴 =================\\nG90\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 S{nozzle_temperature[initial_extruder] - 50}\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_1\\nM104 S{nozzle_temperature[initial_extruder] - 90}\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_2\\n\\n;===== 精回零 =================\\nM106 S255\\nM109 S{nozzle_temperature[initial_extruder] - 90}\\nM190 S{bed_temperature_initial_layer_single}\\nM107 P2\\nG90\\nG0 Z5 F10000\\nWAIT_CHAMBER_TEMP TIMEOUT=180\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{else} \\nG28 Z \\n{endif} \\n\\n\\n;===== 热床调平 =================\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{else} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 \\n{endif} \\n\\n\\n;===== 画起始线 =================\\nG90\\nG1 Z1.5\\nG0 X85 Y1 Z2 F18000\\nM109 S{nozzle_temperature_initial_layer[initial_extruder]}\\nG1 Z0.2\\nM83\\nG1 X185 E15 F360\\nG1 Z1.5\\n\\nG90\\nM106 S0", "machine_tool_change_time": "5", "max_layer_height": [ "0.32", diff --git a/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.6 nozzle).json b/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.6 nozzle).json index 40d8bf7e5da..cfec4db7c79 100644 --- a/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.6 nozzle).json +++ b/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.6 nozzle).json @@ -48,7 +48,7 @@ "20", "12" ], - "machine_start_gcode": ";===== date: 20260128 =====================\\n\\nPRINT_START\\nDEFECT_DETECTION_START\\nSET_PRINT_STATS_INFO TOTAL_LAYER={total_layer_count} CURRENT_LAYER=0\\nTIMELAPSE_START\\nM140 S{bed_temperature_initial_layer_single}\\nM104 T{initial_extruder} S140\\nM204 S10000\\n{if filament_is_high_temperature[0]}\\n; 保温\\n\tSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=180\\n{else}\\n{if temperature_vitrification[0]<=50}\\n; 强冷\\nSET_PURIFIER_MODE MODE=1 DESIRE_TEMP=42 ALARM_TEMP=45 FAN_SPEED=0.6\\nDELAY_OFF=0\t\t\\n{else}\\n; 弱冷\\nSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=0 FAN_SPEED=0.6 DELAY_OFF=180\t\t\\n{endif}\\n{endif}\\nG28 X Y\\n;===== 床面异物检测 ========\\nT{initial_extruder}\\nG90\\nDEFECT_DETECTION_DETECT_BED\\n;===== 取放头检测 =================\\nSM_PRINT_CHECK_SWITCH_EXTRUDER\\n\\n;===== 自动进料 & 挤出流量 & 预挤出 ======================\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=1 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=0\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=0\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=2 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=1\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=1\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=3 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=2\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=2\\nSM_PRINT_AUTO_FEED EXTRUDER=3\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=3\\nM104 S0 T0 A0\\nM104 S0 T1 A0\\nM104 S0 T2 A0\\nM104 S0 T3 A0\\nM104 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\n\\n;===== 粗回零 =================\\nT{initial_extruder}\\nM106 S255\\nM106 P2 S0\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\nROUGHLY_CLEAN_NOZZLE_WITH_DISCARD\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nG28 Z I140 J140\\n\\n;===== 检测钢板 =================\\nDETECT_BED_PLATE\\n\\n;===== 深度清洁喷嘴 =================\\nG90\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 S{nozzle_temperature[initial_extruder] - 50}\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_1\\nM104 S{nozzle_temperature[initial_extruder] - 90}\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_2\\n\\n;===== 精回零 =================\\nM106 S255\\nM109 S{nozzle_temperature[initial_extruder] - 90}\\nM190 S{bed_temperature_initial_layer_single}\\nM107 P2\\nG90\\nG0 Z5 F10000\\nWAIT_CHAMBER_TEMP TIMEOUT=180\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{else} \\nG28 Z \\n{endif} \\n\\n\\n;===== 热床调平 =================\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{else} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 \\n{endif} \\n\\n\\n;===== 画起始线 =================\\nG90\\nG1 Z1.5\\nG0 X85 Y1 Z2 F18000\\nM109 S{nozzle_temperature_initial_layer[initial_extruder]}\\nG1 Z0.2\\nM83\\nG1 X185 E15 F360\\nG1 Z1.5\\n\\nG90\\nM106 S0", + "machine_start_gcode": ";===== date: 20260128 =====================\\n\\nPRINT_START\\nDEFECT_DETECTION_START\\nSET_PRINT_STATS_INFO TOTAL_LAYER={total_layer_count} CURRENT_LAYER=0\\nTIMELAPSE_START\\nM140 S{bed_temperature_initial_layer_single}\\nM104 T{initial_extruder} S140\\nM204 S10000\\n{if filament_is_high_temperature[0]}\\n; 保温\\n\tSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=180\\n{else}\\n{if temperature_vitrification[0]<=50}\\n; 强冷\\nSET_PURIFIER_MODE MODE=1 DESIRE_TEMP=42 ALARM_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=0\t\t\\n{else}\\n; 弱冷\\nSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=0 FAN_SPEED=0.6 DELAY_OFF=180\t\t\\n{endif}\\n{endif}\\nG28 X Y\\n;===== 床面异物检测 ========\\nT{initial_extruder}\\nG90\\nDEFECT_DETECTION_DETECT_BED\\n;===== 取放头检测 =================\\nSM_PRINT_CHECK_SWITCH_EXTRUDER\\n\\n;===== 自动进料 & 挤出流量 & 预挤出 ======================\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=1 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=0\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=0\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=2 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=1\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=1\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=3 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=2\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=2\\nSM_PRINT_AUTO_FEED EXTRUDER=3\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=3\\nM104 S0 T0 A0\\nM104 S0 T1 A0\\nM104 S0 T2 A0\\nM104 S0 T3 A0\\nM104 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\n\\n;===== 粗回零 =================\\nT{initial_extruder}\\nM106 S255\\nM106 P2 S0\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\nROUGHLY_CLEAN_NOZZLE_WITH_DISCARD\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nG28 Z I140 J140\\n\\n;===== 检测钢板 =================\\nDETECT_BED_PLATE\\n\\n;===== 深度清洁喷嘴 =================\\nG90\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 S{nozzle_temperature[initial_extruder] - 50}\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_1\\nM104 S{nozzle_temperature[initial_extruder] - 90}\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_2\\n\\n;===== 精回零 =================\\nM106 S255\\nM109 S{nozzle_temperature[initial_extruder] - 90}\\nM190 S{bed_temperature_initial_layer_single}\\nM107 P2\\nG90\\nG0 Z5 F10000\\nWAIT_CHAMBER_TEMP TIMEOUT=180\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{else} \\nG28 Z \\n{endif} \\n\\n\\n;===== 热床调平 =================\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{else} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 \\n{endif} \\n\\n\\n;===== 画起始线 =================\\nG90\\nG1 Z1.5\\nG0 X85 Y1 Z2 F18000\\nM109 S{nozzle_temperature_initial_layer[initial_extruder]}\\nG1 Z0.2\\nM83\\nG1 X185 E15 F360\\nG1 Z1.5\\n\\nG90\\nM106 S0", "machine_tool_change_time": "5", "max_layer_height": [ "0.42", diff --git a/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.8 nozzle).json b/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.8 nozzle).json index 94d6eeeafe6..12692f2bd96 100644 --- a/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.8 nozzle).json +++ b/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.8 nozzle).json @@ -48,7 +48,7 @@ "20", "12" ], - "machine_start_gcode": ";===== date: 20260128 =====================\\n\\nPRINT_START\\nDEFECT_DETECTION_START\\nSET_PRINT_STATS_INFO TOTAL_LAYER={total_layer_count} CURRENT_LAYER=0\\nTIMELAPSE_START\\nM140 S{bed_temperature_initial_layer_single}\\nM104 T{initial_extruder} S140\\nM204 S10000\\n{if filament_is_high_temperature[0]}\\n; 保温\\n\tSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=180\\n{else}\\n{if temperature_vitrification[0]<=50}\\n; 强冷\\nSET_PURIFIER_MODE MODE=1 DESIRE_TEMP=42 ALARM_TEMP=45 FAN_SPEED=0.6\\nDELAY_OFF=0\t\t\\n{else}\\n; 弱冷\\nSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=0 FAN_SPEED=0.6 DELAY_OFF=180\t\t\\n{endif}\\n{endif}\\nG28 X Y\\n;===== 床面异物检测 ========\\nT{initial_extruder}\\nG90\\nDEFECT_DETECTION_DETECT_BED\\n;===== 取放头检测 =================\\nSM_PRINT_CHECK_SWITCH_EXTRUDER\\n\\n;===== 自动进料 & 挤出流量 & 预挤出 ======================\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=1 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=0\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=0\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=2 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=1\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=1\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=3 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=2\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=2\\nSM_PRINT_AUTO_FEED EXTRUDER=3\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=3\\nM104 S0 T0 A0\\nM104 S0 T1 A0\\nM104 S0 T2 A0\\nM104 S0 T3 A0\\nM104 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\n\\n;===== 粗回零 =================\\nT{initial_extruder}\\nM106 S255\\nM106 P2 S0\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\nROUGHLY_CLEAN_NOZZLE_WITH_DISCARD\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nG28 Z I140 J140\\n\\n;===== 检测钢板 =================\\nDETECT_BED_PLATE\\n\\n;===== 深度清洁喷嘴 =================\\nG90\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 S{nozzle_temperature[initial_extruder] - 50}\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_1\\nM104 S{nozzle_temperature[initial_extruder] - 90}\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_2\\n\\n;===== 精回零 =================\\nM106 S255\\nM109 S{nozzle_temperature[initial_extruder] - 90}\\nM190 S{bed_temperature_initial_layer_single}\\nM107 P2\\nG90\\nG0 Z5 F10000\\nWAIT_CHAMBER_TEMP TIMEOUT=180\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{else} \\nG28 Z \\n{endif} \\n\\n\\n;===== 热床调平 =================\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{else} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 \\n{endif} \\n\\n\\n;===== 画起始线 =================\\nG90\\nG1 Z1.5\\nG0 X85 Y1 Z2 F18000\\nM109 S{nozzle_temperature_initial_layer[initial_extruder]}\\nG1 Z0.2\\nM83\\nG1 X185 E15 F360\\nG1 Z1.5\\n\\nG90\\nM106 S0", + "machine_start_gcode": ";===== date: 20260128 =====================\\n\\nPRINT_START\\nDEFECT_DETECTION_START\\nSET_PRINT_STATS_INFO TOTAL_LAYER={total_layer_count} CURRENT_LAYER=0\\nTIMELAPSE_START\\nM140 S{bed_temperature_initial_layer_single}\\nM104 T{initial_extruder} S140\\nM204 S10000\\n{if filament_is_high_temperature[0]}\\n; 保温\\n\tSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=180\\n{else}\\n{if temperature_vitrification[0]<=50}\\n; 强冷\\nSET_PURIFIER_MODE MODE=1 DESIRE_TEMP=42 ALARM_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=0\t\t\\n{else}\\n; 弱冷\\nSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=0 FAN_SPEED=0.6 DELAY_OFF=180\t\t\\n{endif}\\n{endif}\\nG28 X Y\\n;===== 床面异物检测 ========\\nT{initial_extruder}\\nG90\\nDEFECT_DETECTION_DETECT_BED\\n;===== 取放头检测 =================\\nSM_PRINT_CHECK_SWITCH_EXTRUDER\\n\\n;===== 自动进料 & 挤出流量 & 预挤出 ======================\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=1 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=0\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=0\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=2 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=1\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=1\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=3 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=2\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=2\\nSM_PRINT_AUTO_FEED EXTRUDER=3\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=3\\nM104 S0 T0 A0\\nM104 S0 T1 A0\\nM104 S0 T2 A0\\nM104 S0 T3 A0\\nM104 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\n\\n;===== 粗回零 =================\\nT{initial_extruder}\\nM106 S255\\nM106 P2 S0\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\nROUGHLY_CLEAN_NOZZLE_WITH_DISCARD\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nG28 Z I140 J140\\n\\n;===== 检测钢板 =================\\nDETECT_BED_PLATE\\n\\n;===== 深度清洁喷嘴 =================\\nG90\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 S{nozzle_temperature[initial_extruder] - 50}\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_1\\nM104 S{nozzle_temperature[initial_extruder] - 90}\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_2\\n\\n;===== 精回零 =================\\nM106 S255\\nM109 S{nozzle_temperature[initial_extruder] - 90}\\nM190 S{bed_temperature_initial_layer_single}\\nM107 P2\\nG90\\nG0 Z5 F10000\\nWAIT_CHAMBER_TEMP TIMEOUT=180\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{else} \\nG28 Z \\n{endif} \\n\\n\\n;===== 热床调平 =================\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{else} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 \\n{endif} \\n\\n\\n;===== 画起始线 =================\\nG90\\nG1 Z1.5\\nG0 X85 Y1 Z2 F18000\\nM109 S{nozzle_temperature_initial_layer[initial_extruder]}\\nG1 Z0.2\\nM83\\nG1 X185 E15 F360\\nG1 Z1.5\\n\\nG90\\nM106 S0", "machine_tool_change_time": "5", "max_layer_height": [ "0.56", From bb8300111b7ba31c8da98fa94dbbcef8e9d59e16 Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Thu, 21 May 2026 18:27:29 +0800 Subject: [PATCH 012/135] =?UTF-8?q?fix=EF=BC=9AThe=20top=20cover=20mode=20?= =?UTF-8?q?is=20determined=20based=20on=20the=20temperature=20characterist?= =?UTF-8?q?ics=20of=20the=20internal=20consumables.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../machine/Snapmaker U1 (0.2 nozzle).json | 4 ++-- .../machine/Snapmaker U1 (0.4 nozzle).json | 4 ++-- .../machine/Snapmaker U1 (0.6 nozzle).json | 4 ++-- .../machine/Snapmaker U1 (0.8 nozzle).json | 4 ++-- src/libslic3r/GCode.cpp | 19 +++++++++++++++++++ 5 files changed, 27 insertions(+), 8 deletions(-) diff --git a/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.2 nozzle).json b/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.2 nozzle).json index 48c14548825..c3ef510ca5e 100644 --- a/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.2 nozzle).json +++ b/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.2 nozzle).json @@ -35,7 +35,7 @@ "0", "0" ], - "machine_end_gcode": " PRINT_END\nTIMELAPSE_STOP", + "machine_end_gcode": "{if print_sequence == \\\"by object\\\"}\\nG91\\nG1 X2 Y2 Z1 F24000\\nG90\\nG1 Z{max_layer_z+2} F600\\n{endif}\\nPRINT_END\\nTIMELAPSE_STOP", "machine_max_jerk_z": [ "3", "0.4" @@ -48,7 +48,7 @@ "20", "12" ], - "machine_start_gcode": ";===== date: 20260128 =====================\\n\\nPRINT_START\\nDEFECT_DETECTION_START\\nSET_PRINT_STATS_INFO TOTAL_LAYER={total_layer_count} CURRENT_LAYER=0\\nTIMELAPSE_START\\nM140 S{bed_temperature_initial_layer_single}\\nM104 T{initial_extruder} S140\\nM204 S10000\\n{if filament_is_high_temperature[0]}\\n; 保温\\n\tSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=180\\n{else}\\n{if temperature_vitrification[0]<=50}\\n; 强冷\\nSET_PURIFIER_MODE MODE=1 DESIRE_TEMP=42 ALARM_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=0\t\t\\n{else}\\n; 弱冷\\nSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=0 FAN_SPEED=0.6 DELAY_OFF=180\t\t\\n{endif}\\n{endif}\\nG28 X Y\\n;===== 床面异物检测 ========\\nT{initial_extruder}\\nG90\\nDEFECT_DETECTION_DETECT_BED\\n;===== 取放头检测 =================\\nSM_PRINT_CHECK_SWITCH_EXTRUDER\\n\\n;===== 自动进料 & 挤出流量 & 预挤出 ======================\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=1 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=0\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=0\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=2 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=1\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=1\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=3 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=2\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=2\\nSM_PRINT_AUTO_FEED EXTRUDER=3\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=3\\nM104 S0 T0 A0\\nM104 S0 T1 A0\\nM104 S0 T2 A0\\nM104 S0 T3 A0\\nM104 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\n\\n;===== 粗回零 =================\\nT{initial_extruder}\\nM106 S255\\nM106 P2 S0\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\nROUGHLY_CLEAN_NOZZLE_WITH_DISCARD\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nG28 Z I140 J140\\n\\n;===== 检测钢板 =================\\nDETECT_BED_PLATE\\n\\n;===== 深度清洁喷嘴 =================\\nG90\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 S{nozzle_temperature[initial_extruder] - 50}\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_1\\nM104 S{nozzle_temperature[initial_extruder] - 90}\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_2\\n\\n;===== 精回零 =================\\nM106 S255\\nM109 S{nozzle_temperature[initial_extruder] - 90}\\nM190 S{bed_temperature_initial_layer_single}\\nM107 P2\\nG90\\nG0 Z5 F10000\\nWAIT_CHAMBER_TEMP TIMEOUT=180\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{else} \\nG28 Z \\n{endif} \\n\\n\\n;===== 热床调平 =================\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{else} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 \\n{endif} \\n\\n\\n;===== 画起始线 =================\\nG90\\nG1 Z1.5\\nG0 X85 Y1 Z2 F18000\\nM109 S{nozzle_temperature_initial_layer[initial_extruder]}\\nG1 Z0.2\\nM83\\nG1 X185 E15 F360\\nG1 Z1.5\\n\\nG90\\nM106 S0", + "machine_start_gcode": ";===== date: 20260128 =====================\\n\\nPRINT_START\\nDEFECT_DETECTION_START\\nSET_PRINT_STATS_INFO TOTAL_LAYER={total_layer_count} CURRENT_LAYER=0\\nTIMELAPSE_START\\nM140 S{bed_temperature_initial_layer_single}\\nM104 T{initial_extruder} S140\\nM204 S10000\\n{if chamber_cooling_mode==0}\\n; 保温\\nSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=180\\n{elsif chamber_cooling_mode==2}\\n; 强冷\\nSET_PURIFIER_MODE MODE=1 DESIRE_TEMP=42 ALARM_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=0\\n{else}\\n; 弱冷\\nSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=0 FAN_SPEED=0.6 DELAY_OFF=180\t\t\\n{endif}\\nG28 X Y\\n;===== 床面异物检测 ========\\nT{initial_extruder}\\nG90\\nDEFECT_DETECTION_DETECT_BED\\n;===== 取放头检测 =================\\nSM_PRINT_CHECK_SWITCH_EXTRUDER\\n\\n;===== 自动进料 & 挤出流量 & 预挤出 ======================\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=1 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=0\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=0\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=2 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=1\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=1\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=3 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=2\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=2\\nSM_PRINT_AUTO_FEED EXTRUDER=3\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=3\\nM104 S0 T0 A0\\nM104 S0 T1 A0\\nM104 S0 T2 A0\\nM104 S0 T3 A0\\nM104 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\n\\n;===== 粗回零 =================\\nT{initial_extruder}\\nM106 S255\\nM106 P2 S0\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\nROUGHLY_CLEAN_NOZZLE_WITH_DISCARD\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nG28 Z I140 J140\\n\\n;===== 检测钢板 =================\\nDETECT_BED_PLATE\\n\\n;===== 深度清洁喷嘴 =================\\nG90\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 S{nozzle_temperature[initial_extruder] - 50}\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_1\\nM104 S{nozzle_temperature[initial_extruder] - 90}\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_2\\n\\n;===== 精回零 =================\\nM106 S255\\nM109 S{nozzle_temperature[initial_extruder] - 90}\\nM190 S{bed_temperature_initial_layer_single}\\nM107 P2\\nG90\\nG0 Z5 F10000\\nWAIT_CHAMBER_TEMP TIMEOUT=180\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{else} \\nG28 Z \\n{endif} \\n\\n\\n;===== 热床调平 =================\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{else} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 \\n{endif} \\n\\n\\n;===== 画起始线 =================\\nG90\\nG1 Z1.5\\nG0 X85 Y1 Z2 F18000\\nM109 S{nozzle_temperature_initial_layer[initial_extruder]}\\nG1 Z0.2\\nM83\\nG1 X185 E15 F360\\nG1 Z1.5\\n\\nG90\\nM106 S0", "machine_tool_change_time": "5", "max_layer_height": [ "0.14", diff --git a/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.4 nozzle).json b/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.4 nozzle).json index 2d1c8ae5ac5..c9d896e1303 100644 --- a/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.4 nozzle).json +++ b/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.4 nozzle).json @@ -35,7 +35,7 @@ "0", "0" ], - "machine_end_gcode": " PRINT_END\nTIMELAPSE_STOP", + "machine_end_gcode": "{if print_sequence == \\\"by object\\\"}\\nG91\\nG1 X2 Y2 Z1 F24000\\nG90\\nG1 Z{max_layer_z+2} F600\\n{endif}\\nPRINT_END\\nTIMELAPSE_STOP", "machine_max_jerk_z": [ "3", "0.4" @@ -48,7 +48,7 @@ "20", "12" ], - "machine_start_gcode": ";===== date: 20260128 =====================\\n\\nPRINT_START\\nDEFECT_DETECTION_START\\nSET_PRINT_STATS_INFO TOTAL_LAYER={total_layer_count} CURRENT_LAYER=0\\nTIMELAPSE_START\\nM140 S{bed_temperature_initial_layer_single}\\nM104 T{initial_extruder} S140\\nM204 S10000\\n{if filament_is_high_temperature[0]}\\n; 保温\\n\tSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=180\\n{else}\\n{if temperature_vitrification[0]<=50}\\n; 强冷\\nSET_PURIFIER_MODE MODE=1 DESIRE_TEMP=42 ALARM_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=0\t\t\\n{else}\\n; 弱冷\\nSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=0 FAN_SPEED=0.6 DELAY_OFF=180\t\t\\n{endif}\\n{endif}\\n\\nG28 X Y\\n;===== 床面异物检测 ========\\nT{initial_extruder}\\nG90\\nDEFECT_DETECTION_DETECT_BED\\n;===== 取放头检测 =================\\nSM_PRINT_CHECK_SWITCH_EXTRUDER\\n\\n;===== 自动进料 & 挤出流量 & 预挤出 ======================\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=1 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=0\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=0\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=2 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=1\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=1\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=3 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=2\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=2\\nSM_PRINT_AUTO_FEED EXTRUDER=3\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=3\\nM104 S0 T0 A0\\nM104 S0 T1 A0\\nM104 S0 T2 A0\\nM104 S0 T3 A0\\nM104 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\n\\n;===== 粗回零 =================\\nT{initial_extruder}\\nM106 S255\\nM106 P2 S0\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\nROUGHLY_CLEAN_NOZZLE_WITH_DISCARD\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nG28 Z I140 J140\\n\\n;===== 检测钢板 =================\\nDETECT_BED_PLATE\\n\\n;===== 深度清洁喷嘴 =================\\nG90\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 S{nozzle_temperature[initial_extruder] - 50}\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_1\\nM104 S{nozzle_temperature[initial_extruder] - 90}\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_2\\n\\n;===== 精回零 =================\\nM106 S255\\nM109 S{nozzle_temperature[initial_extruder] - 90}\\nM190 S{bed_temperature_initial_layer_single}\\nM107 P2\\nG90\\nG0 Z5 F10000\\nWAIT_CHAMBER_TEMP TIMEOUT=180\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{else} \\nG28 Z \\n{endif} \\n\\n\\n;===== 热床调平 =================\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{else} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 \\n{endif} \\n\\n\\n;===== 画起始线 =================\\nG90\\nG1 Z1.5\\nG0 X85 Y1 Z2 F18000\\nM109 S{nozzle_temperature_initial_layer[initial_extruder]}\\nG1 Z0.2\\nM83\\nG1 X185 E15 F360\\nG1 Z1.5\\n\\nG90\\nM106 S0", + "machine_start_gcode": ";===== date: 20260128 =====================\\n\\nPRINT_START\\nDEFECT_DETECTION_START\\nSET_PRINT_STATS_INFO TOTAL_LAYER={total_layer_count} CURRENT_LAYER=0\\nTIMELAPSE_START\\nM140 S{bed_temperature_initial_layer_single}\\nM104 T{initial_extruder} S140\\nM204 S10000\\n{if chamber_cooling_mode==0}\\n; 保温\\nSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=180\\n{elsif chamber_cooling_mode==2}\\n; 强冷\\nSET_PURIFIER_MODE MODE=1 DESIRE_TEMP=42 ALARM_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=0\\n{else}\\n; 弱冷\\nSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=0 FAN_SPEED=0.6 DELAY_OFF=180\t\t\\n{endif}\\nG28 X Y\\n;===== 床面异物检测 ========\\nT{initial_extruder}\\nG90\\nDEFECT_DETECTION_DETECT_BED\\n;===== 取放头检测 =================\\nSM_PRINT_CHECK_SWITCH_EXTRUDER\\n\\n;===== 自动进料 & 挤出流量 & 预挤出 ======================\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=1 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=0\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=0\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=2 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=1\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=1\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=3 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=2\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=2\\nSM_PRINT_AUTO_FEED EXTRUDER=3\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=3\\nM104 S0 T0 A0\\nM104 S0 T1 A0\\nM104 S0 T2 A0\\nM104 S0 T3 A0\\nM104 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\n\\n;===== 粗回零 =================\\nT{initial_extruder}\\nM106 S255\\nM106 P2 S0\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\nROUGHLY_CLEAN_NOZZLE_WITH_DISCARD\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nG28 Z I140 J140\\n\\n;===== 检测钢板 =================\\nDETECT_BED_PLATE\\n\\n;===== 深度清洁喷嘴 =================\\nG90\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 S{nozzle_temperature[initial_extruder] - 50}\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_1\\nM104 S{nozzle_temperature[initial_extruder] - 90}\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_2\\n\\n;===== 精回零 =================\\nM106 S255\\nM109 S{nozzle_temperature[initial_extruder] - 90}\\nM190 S{bed_temperature_initial_layer_single}\\nM107 P2\\nG90\\nG0 Z5 F10000\\nWAIT_CHAMBER_TEMP TIMEOUT=180\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{else} \\nG28 Z \\n{endif} \\n\\n\\n;===== 热床调平 =================\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{else} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 \\n{endif} \\n\\n\\n;===== 画起始线 =================\\nG90\\nG1 Z1.5\\nG0 X85 Y1 Z2 F18000\\nM109 S{nozzle_temperature_initial_layer[initial_extruder]}\\nG1 Z0.2\\nM83\\nG1 X185 E15 F360\\nG1 Z1.5\\n\\nG90\\nM106 S0", "machine_tool_change_time": "5", "max_layer_height": [ "0.32", diff --git a/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.6 nozzle).json b/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.6 nozzle).json index cfec4db7c79..441e975af86 100644 --- a/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.6 nozzle).json +++ b/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.6 nozzle).json @@ -35,7 +35,7 @@ "0", "0" ], - "machine_end_gcode": " PRINT_END\nTIMELAPSE_STOP", + "machine_end_gcode": "{if print_sequence == \\\"by object\\\"}\\nG91\\nG1 X2 Y2 Z1 F24000\\nG90\\nG1 Z{max_layer_z+2} F600\\n{endif}\\nPRINT_END\\nTIMELAPSE_STOP", "machine_max_jerk_z": [ "3", "0.4" @@ -48,7 +48,7 @@ "20", "12" ], - "machine_start_gcode": ";===== date: 20260128 =====================\\n\\nPRINT_START\\nDEFECT_DETECTION_START\\nSET_PRINT_STATS_INFO TOTAL_LAYER={total_layer_count} CURRENT_LAYER=0\\nTIMELAPSE_START\\nM140 S{bed_temperature_initial_layer_single}\\nM104 T{initial_extruder} S140\\nM204 S10000\\n{if filament_is_high_temperature[0]}\\n; 保温\\n\tSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=180\\n{else}\\n{if temperature_vitrification[0]<=50}\\n; 强冷\\nSET_PURIFIER_MODE MODE=1 DESIRE_TEMP=42 ALARM_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=0\t\t\\n{else}\\n; 弱冷\\nSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=0 FAN_SPEED=0.6 DELAY_OFF=180\t\t\\n{endif}\\n{endif}\\nG28 X Y\\n;===== 床面异物检测 ========\\nT{initial_extruder}\\nG90\\nDEFECT_DETECTION_DETECT_BED\\n;===== 取放头检测 =================\\nSM_PRINT_CHECK_SWITCH_EXTRUDER\\n\\n;===== 自动进料 & 挤出流量 & 预挤出 ======================\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=1 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=0\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=0\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=2 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=1\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=1\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=3 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=2\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=2\\nSM_PRINT_AUTO_FEED EXTRUDER=3\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=3\\nM104 S0 T0 A0\\nM104 S0 T1 A0\\nM104 S0 T2 A0\\nM104 S0 T3 A0\\nM104 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\n\\n;===== 粗回零 =================\\nT{initial_extruder}\\nM106 S255\\nM106 P2 S0\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\nROUGHLY_CLEAN_NOZZLE_WITH_DISCARD\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nG28 Z I140 J140\\n\\n;===== 检测钢板 =================\\nDETECT_BED_PLATE\\n\\n;===== 深度清洁喷嘴 =================\\nG90\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 S{nozzle_temperature[initial_extruder] - 50}\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_1\\nM104 S{nozzle_temperature[initial_extruder] - 90}\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_2\\n\\n;===== 精回零 =================\\nM106 S255\\nM109 S{nozzle_temperature[initial_extruder] - 90}\\nM190 S{bed_temperature_initial_layer_single}\\nM107 P2\\nG90\\nG0 Z5 F10000\\nWAIT_CHAMBER_TEMP TIMEOUT=180\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{else} \\nG28 Z \\n{endif} \\n\\n\\n;===== 热床调平 =================\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{else} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 \\n{endif} \\n\\n\\n;===== 画起始线 =================\\nG90\\nG1 Z1.5\\nG0 X85 Y1 Z2 F18000\\nM109 S{nozzle_temperature_initial_layer[initial_extruder]}\\nG1 Z0.2\\nM83\\nG1 X185 E15 F360\\nG1 Z1.5\\n\\nG90\\nM106 S0", + "machine_start_gcode": ";===== date: 20260128 =====================\\n\\nPRINT_START\\nDEFECT_DETECTION_START\\nSET_PRINT_STATS_INFO TOTAL_LAYER={total_layer_count} CURRENT_LAYER=0\\nTIMELAPSE_START\\nM140 S{bed_temperature_initial_layer_single}\\nM104 T{initial_extruder} S140\\nM204 S10000\\n{if chamber_cooling_mode==0}\\n; 保温\\nSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=180\\n{elsif chamber_cooling_mode==2}\\n; 强冷\\nSET_PURIFIER_MODE MODE=1 DESIRE_TEMP=42 ALARM_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=0\\n{else}\\n; 弱冷\\nSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=0 FAN_SPEED=0.6 DELAY_OFF=180\t\t\\n{endif}\\nG28 X Y\\n;===== 床面异物检测 ========\\nT{initial_extruder}\\nG90\\nDEFECT_DETECTION_DETECT_BED\\n;===== 取放头检测 =================\\nSM_PRINT_CHECK_SWITCH_EXTRUDER\\n\\n;===== 自动进料 & 挤出流量 & 预挤出 ======================\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=1 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=0\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=0\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=2 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=1\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=1\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=3 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=2\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=2\\nSM_PRINT_AUTO_FEED EXTRUDER=3\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=3\\nM104 S0 T0 A0\\nM104 S0 T1 A0\\nM104 S0 T2 A0\\nM104 S0 T3 A0\\nM104 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\n\\n;===== 粗回零 =================\\nT{initial_extruder}\\nM106 S255\\nM106 P2 S0\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\nROUGHLY_CLEAN_NOZZLE_WITH_DISCARD\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nG28 Z I140 J140\\n\\n;===== 检测钢板 =================\\nDETECT_BED_PLATE\\n\\n;===== 深度清洁喷嘴 =================\\nG90\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 S{nozzle_temperature[initial_extruder] - 50}\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_1\\nM104 S{nozzle_temperature[initial_extruder] - 90}\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_2\\n\\n;===== 精回零 =================\\nM106 S255\\nM109 S{nozzle_temperature[initial_extruder] - 90}\\nM190 S{bed_temperature_initial_layer_single}\\nM107 P2\\nG90\\nG0 Z5 F10000\\nWAIT_CHAMBER_TEMP TIMEOUT=180\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{else} \\nG28 Z \\n{endif} \\n\\n\\n;===== 热床调平 =================\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{else} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 \\n{endif} \\n\\n\\n;===== 画起始线 =================\\nG90\\nG1 Z1.5\\nG0 X85 Y1 Z2 F18000\\nM109 S{nozzle_temperature_initial_layer[initial_extruder]}\\nG1 Z0.2\\nM83\\nG1 X185 E15 F360\\nG1 Z1.5\\n\\nG90\\nM106 S0", "machine_tool_change_time": "5", "max_layer_height": [ "0.42", diff --git a/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.8 nozzle).json b/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.8 nozzle).json index 12692f2bd96..1c8255b1505 100644 --- a/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.8 nozzle).json +++ b/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.8 nozzle).json @@ -35,7 +35,7 @@ "0", "0" ], - "machine_end_gcode": " PRINT_END\nTIMELAPSE_STOP", + "machine_end_gcode": "{if print_sequence == \\\"by object\\\"}\\nG91\\nG1 X2 Y2 Z1 F24000\\nG90\\nG1 Z{max_layer_z+2} F600\\n{endif}\\nPRINT_END\\nTIMELAPSE_STOP", "machine_max_jerk_z": [ "3", "0.4" @@ -48,7 +48,7 @@ "20", "12" ], - "machine_start_gcode": ";===== date: 20260128 =====================\\n\\nPRINT_START\\nDEFECT_DETECTION_START\\nSET_PRINT_STATS_INFO TOTAL_LAYER={total_layer_count} CURRENT_LAYER=0\\nTIMELAPSE_START\\nM140 S{bed_temperature_initial_layer_single}\\nM104 T{initial_extruder} S140\\nM204 S10000\\n{if filament_is_high_temperature[0]}\\n; 保温\\n\tSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=180\\n{else}\\n{if temperature_vitrification[0]<=50}\\n; 强冷\\nSET_PURIFIER_MODE MODE=1 DESIRE_TEMP=42 ALARM_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=0\t\t\\n{else}\\n; 弱冷\\nSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=0 FAN_SPEED=0.6 DELAY_OFF=180\t\t\\n{endif}\\n{endif}\\nG28 X Y\\n;===== 床面异物检测 ========\\nT{initial_extruder}\\nG90\\nDEFECT_DETECTION_DETECT_BED\\n;===== 取放头检测 =================\\nSM_PRINT_CHECK_SWITCH_EXTRUDER\\n\\n;===== 自动进料 & 挤出流量 & 预挤出 ======================\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=1 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=0\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=0\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=2 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=1\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=1\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=3 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=2\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=2\\nSM_PRINT_AUTO_FEED EXTRUDER=3\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=3\\nM104 S0 T0 A0\\nM104 S0 T1 A0\\nM104 S0 T2 A0\\nM104 S0 T3 A0\\nM104 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\n\\n;===== 粗回零 =================\\nT{initial_extruder}\\nM106 S255\\nM106 P2 S0\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\nROUGHLY_CLEAN_NOZZLE_WITH_DISCARD\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nG28 Z I140 J140\\n\\n;===== 检测钢板 =================\\nDETECT_BED_PLATE\\n\\n;===== 深度清洁喷嘴 =================\\nG90\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 S{nozzle_temperature[initial_extruder] - 50}\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_1\\nM104 S{nozzle_temperature[initial_extruder] - 90}\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_2\\n\\n;===== 精回零 =================\\nM106 S255\\nM109 S{nozzle_temperature[initial_extruder] - 90}\\nM190 S{bed_temperature_initial_layer_single}\\nM107 P2\\nG90\\nG0 Z5 F10000\\nWAIT_CHAMBER_TEMP TIMEOUT=180\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{else} \\nG28 Z \\n{endif} \\n\\n\\n;===== 热床调平 =================\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{else} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 \\n{endif} \\n\\n\\n;===== 画起始线 =================\\nG90\\nG1 Z1.5\\nG0 X85 Y1 Z2 F18000\\nM109 S{nozzle_temperature_initial_layer[initial_extruder]}\\nG1 Z0.2\\nM83\\nG1 X185 E15 F360\\nG1 Z1.5\\n\\nG90\\nM106 S0", + "machine_start_gcode": ";===== date: 20260128 =====================\\n\\nPRINT_START\\nDEFECT_DETECTION_START\\nSET_PRINT_STATS_INFO TOTAL_LAYER={total_layer_count} CURRENT_LAYER=0\\nTIMELAPSE_START\\nM140 S{bed_temperature_initial_layer_single}\\nM104 T{initial_extruder} S140\\nM204 S10000\\n{if chamber_cooling_mode==0}\\n; 保温\\nSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=180\\n{elsif chamber_cooling_mode==2}\\n; 强冷\\nSET_PURIFIER_MODE MODE=1 DESIRE_TEMP=42 ALARM_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=0\\n{else}\\n; 弱冷\\nSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=0 FAN_SPEED=0.6 DELAY_OFF=180\t\t\\n{endif}\\nG28 X Y\\n;===== 床面异物检测 ========\\nT{initial_extruder}\\nG90\\nDEFECT_DETECTION_DETECT_BED\\n;===== 取放头检测 =================\\nSM_PRINT_CHECK_SWITCH_EXTRUDER\\n\\n;===== 自动进料 & 挤出流量 & 预挤出 ======================\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=1 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=0\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=0\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=2 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=1\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=1\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=3 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=2\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=2\\nSM_PRINT_AUTO_FEED EXTRUDER=3\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=3\\nM104 S0 T0 A0\\nM104 S0 T1 A0\\nM104 S0 T2 A0\\nM104 S0 T3 A0\\nM104 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\n\\n;===== 粗回零 =================\\nT{initial_extruder}\\nM106 S255\\nM106 P2 S0\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\nROUGHLY_CLEAN_NOZZLE_WITH_DISCARD\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nG28 Z I140 J140\\n\\n;===== 检测钢板 =================\\nDETECT_BED_PLATE\\n\\n;===== 深度清洁喷嘴 =================\\nG90\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 S{nozzle_temperature[initial_extruder] - 50}\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_1\\nM104 S{nozzle_temperature[initial_extruder] - 90}\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_2\\n\\n;===== 精回零 =================\\nM106 S255\\nM109 S{nozzle_temperature[initial_extruder] - 90}\\nM190 S{bed_temperature_initial_layer_single}\\nM107 P2\\nG90\\nG0 Z5 F10000\\nWAIT_CHAMBER_TEMP TIMEOUT=180\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{else} \\nG28 Z \\n{endif} \\n\\n\\n;===== 热床调平 =================\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{else} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 \\n{endif} \\n\\n\\n;===== 画起始线 =================\\nG90\\nG1 Z1.5\\nG0 X85 Y1 Z2 F18000\\nM109 S{nozzle_temperature_initial_layer[initial_extruder]}\\nG1 Z0.2\\nM83\\nG1 X185 E15 F360\\nG1 Z1.5\\n\\nG90\\nM106 S0", "machine_tool_change_time": "5", "max_layer_height": [ "0.56", diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index ff353d5e6b5..21af85e6363 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -2698,6 +2698,25 @@ void GCode::_do_export(Print& print, GCodeOutputStream& file, ThumbnailsGenerato this->placeholder_parser().set("scan_first_layer", new ConfigOptionBool(false)); } } + + // Compute chamber cooling mode based on all filaments used on this plate. + // 0 = keep warm (all high-temp), 1 = weak cooling, 2 = strong cooling. + { + int chamber_cooling_mode = 0; + for (unsigned int extruder : tool_ordering.all_extruders()) { + if (!m_config.filament_is_high_temperature.get_at(extruder)) { + int vitrification = m_config.temperature_vitrification.get_at(extruder); + if (vitrification <= 50) { + chamber_cooling_mode = 2; + break; + } else if (vitrification <= 70) { + chamber_cooling_mode = 1; + } + } + } + this->placeholder_parser().set("chamber_cooling_mode", new ConfigOptionInt(chamber_cooling_mode)); + } + std::string machine_start_gcode = this->placeholder_parser_process("machine_start_gcode", print.config().machine_start_gcode.value, initial_extruder_id); if (print.config().gcode_flavor != gcfKlipper) { From fca2f3d2c4719bd1e142779f34dc7c43964c87c1 Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Mon, 25 May 2026 16:23:28 +0800 Subject: [PATCH 013/135] feat: add real-time high/low temperature filament mixing detection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the outdated filament_info.json-based classification with per-preset filament_is_high_temperature config. Detect mixing whenever filament assignments change on the current plate (wall, infill, solid infill, support, support interface, wipe tower, and multi-material painting). Detection hooks: - Plater::on_config_change() — config load, extruder count change, import - Sidebar::update_presets() — sidebar filament combo selection Enforcement layers (three-stage guard against notification flicker): 1. check_filament_temp_mixing() shows error notification + marks plate invalid immediately on config change (CallAfter-deferred with serial cancellation to prevent races between the two hooks). 2. Print::validate() runs check_multi_filament_valid() using filament_is_high_temperature (was: filament_info.json + nozzles<2 restriction removed). 3. Plater::priv::update() re-checks after background validate passes, preventing the validate cycle from silently closing the notification. The old Print::validate() check (nozzles<2 && extruders>1) is replaced with a simpler extruders>1 guard that works for multi-nozzle printers too. --- src/libslic3r/Print.cpp | 554 +--- src/slic3r/GUI/Plater.cpp | 5904 ++----------------------------------- 2 files changed, 264 insertions(+), 6194 deletions(-) diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index 9be0cd69d27..53f81a4e8ca 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -8,7 +8,6 @@ #include "Flow.hpp" #include "Geometry/ConvexHull.hpp" #include "I18N.hpp" -#include "LocalZOrderOptimizer.hpp" #include "ShortestPath.hpp" #include "Thread.hpp" #include "Time.hpp" @@ -54,453 +53,6 @@ template class PrintState; PrintRegion::PrintRegion(const PrintRegionConfig &config) : PrintRegion(config, config.hash()) {} PrintRegion::PrintRegion(PrintRegionConfig &&config) : PrintRegion(std::move(config), config.hash()) {} -namespace { - -constexpr double LOCAL_Z_PERIMETER_MASK_EXPAND_MM = 0.10; - -struct LocalZWipeTowerToolchange -{ - unsigned int old_tool { 0 }; - unsigned int new_tool { 0 }; -}; - -struct LocalZWipeTowerPassRef -{ - size_t layer_to_print_idx { 0 }; - const SubLayerPlan *plan { nullptr }; - std::vector extruders; -}; - -static inline ExPolygons local_z_compensate_masks_for_wipe_tower(const ExPolygons &src_masks, - const float delta_scaled, - const bool fallback_to_source) -{ - if (src_masks.empty() || std::abs(delta_scaled) <= EPSILON) - return src_masks; - - ExPolygons compensated = offset_ex(src_masks, delta_scaled); - if (!compensated.empty() && compensated.size() > 1) - compensated = union_ex(compensated); - - if (compensated.empty() && fallback_to_source) - return src_masks; - return compensated; -} - -static bool local_z_segments_exist(Polylines segments) -{ - for (Polyline &segment : segments) { - if (segment.is_valid()) - return true; - } - return false; -} - -static bool extrusion_collection_has_local_z_segment(const ExtrusionEntityCollection &source, - const ExPolygons &include_masks) -{ - if (source.entities.empty() || include_masks.empty()) - return false; - - ExtrusionEntityCollection flattened = source.flatten(false); - for (const ExtrusionEntity *entity : flattened.entities) { - if (const auto *path = dynamic_cast(entity)) { - if (local_z_segments_exist(intersection_pl(Polylines{path->polyline}, include_masks))) - return true; - } else if (const auto *multipath = dynamic_cast(entity)) { - for (const ExtrusionPath &path : multipath->paths) { - if (local_z_segments_exist(intersection_pl(Polylines{path.polyline}, include_masks))) - return true; - } - } else if (const auto *loop = dynamic_cast(entity)) { - for (const ExtrusionPath &path : loop->paths) { - if (local_z_segments_exist(intersection_pl(Polylines{path.polyline}, include_masks))) - return true; - } - } - } - - return false; -} - -static bool layer_has_local_z_extrusions(const Layer &layer, const ExPolygons &pass_masks, bool include_infill) -{ - if (pass_masks.empty()) - return false; - - for (const LayerRegion *layer_region : layer.regions()) { - for (const ExtrusionEntity *entity : layer_region->perimeters.entities) { - const auto *extrusions = dynamic_cast(entity); - if (extrusions == nullptr) - continue; - if (extrusion_collection_has_local_z_segment(*extrusions, pass_masks)) - return true; - } - if (!include_infill) - continue; - for (const ExtrusionEntity *entity : layer_region->fills.entities) { - const auto *extrusions = dynamic_cast(entity); - if (extrusions == nullptr) - continue; - if (extrusion_collection_has_local_z_segment(*extrusions, pass_masks)) - return true; - } - } - - return false; -} - -static inline int shared_local_z_extruder_for_wipe_tower(const std::vector &lhs, - const std::vector &rhs) -{ - for (unsigned int extruder_id : lhs) { - if (std::find(rhs.begin(), rhs.end(), extruder_id) != rhs.end()) - return static_cast(extruder_id); - } - return -1; -} - -static std::vector rotate_extruders_to_start_with(const std::vector &extruders, - unsigned int start_extruder) -{ - std::vector rotated = extruders; - auto it = std::find(rotated.begin(), rotated.end(), start_extruder); - if (it != rotated.end()) - std::rotate(rotated.begin(), it, rotated.end()); - return rotated; -} - -static std::vector collect_local_z_wipe_tower_toolchanges( - const Print &print, - const std::vector &layers, - int start_extruder) -{ - std::vector pass_refs; - const bool local_z_whole_objects_enabled = print.full_print_config().opt_bool("dithering_local_z_whole_objects"); - const bool local_z_infill_enabled = print.full_print_config().opt_bool("dithering_local_z_infill"); - const float local_z_perimeter_mask_expand = float(scale_(LOCAL_Z_PERIMETER_MASK_EXPAND_MM)); - - for (size_t layer_to_print_idx = 0; layer_to_print_idx < layers.size(); ++layer_to_print_idx) { - const GCode::LayerToPrint &layer_to_print = layers[layer_to_print_idx]; - if (layer_to_print.object_layer == nullptr) - continue; - - const PrintObject *print_object = - layer_to_print.original_object != nullptr ? layer_to_print.original_object : layer_to_print.object(); - if (print_object == nullptr) - continue; - - const size_t layer_id = size_t(layer_to_print.object_layer->id()); - const auto &intervals = print_object->local_z_intervals(); - const auto &plans = print_object->local_z_sublayer_plan(); - const auto interval_it = std::find_if(intervals.begin(), intervals.end(), [layer_id](const LocalZInterval &interval) { - return interval.layer_id == layer_id; - }); - if (interval_it == intervals.end() || !interval_it->has_mixed_paint || interval_it->sublayer_count <= 1 || - interval_it->first_sublayer_idx >= plans.size()) { - continue; - } - - const size_t first_idx = interval_it->first_sublayer_idx; - const size_t end_idx = std::min(plans.size(), first_idx + interval_it->sublayer_count); - for (size_t plan_idx = first_idx; plan_idx < end_idx; ++plan_idx) { - const SubLayerPlan &plan = plans[plan_idx]; - if (!plan.split_interval) - continue; - - const size_t plan_mask_slots = - std::max(plan.painted_masks_by_extruder.size(), plan.fixed_painted_masks_by_extruder.size()); - std::vector compensated_masks_by_extruder(plan_mask_slots, ExPolygons()); - - ExPolygons fixed_raw_masks_union; - for (const ExPolygons &fixed_masks : plan.fixed_painted_masks_by_extruder) { - if (!fixed_masks.empty()) - append(fixed_raw_masks_union, fixed_masks); - } - if (!fixed_raw_masks_union.empty() && fixed_raw_masks_union.size() > 1) - fixed_raw_masks_union = union_ex(fixed_raw_masks_union); - - const ExPolygons fixed_compensated_guard = - fixed_raw_masks_union.empty() ? - ExPolygons() : - local_z_compensate_masks_for_wipe_tower(fixed_raw_masks_union, local_z_perimeter_mask_expand, true); - - for (size_t extruder_id = 0; extruder_id < plan_mask_slots; ++extruder_id) { - const ExPolygons mixed_raw_masks = - extruder_id < plan.painted_masks_by_extruder.size() ? plan.painted_masks_by_extruder[extruder_id] : ExPolygons(); - const ExPolygons fixed_raw_masks = - extruder_id < plan.fixed_painted_masks_by_extruder.size() ? plan.fixed_painted_masks_by_extruder[extruder_id] : - ExPolygons(); - if (mixed_raw_masks.empty() && fixed_raw_masks.empty()) - continue; - - ExPolygons compensated; - if (!mixed_raw_masks.empty()) { - ExPolygons compensated_mixed = - local_z_compensate_masks_for_wipe_tower(mixed_raw_masks, local_z_perimeter_mask_expand, true); - if (local_z_whole_objects_enabled && !fixed_compensated_guard.empty()) - compensated_mixed = diff_ex(compensated_mixed, fixed_compensated_guard); - if (!compensated_mixed.empty()) - append(compensated, compensated_mixed); - } - if (!fixed_raw_masks.empty()) - append(compensated, fixed_raw_masks); - if (!compensated.empty() && compensated.size() > 1) - compensated = union_ex(compensated); - compensated_masks_by_extruder[extruder_id] = std::move(compensated); - } - - LocalZWipeTowerPassRef pass_ref; - pass_ref.layer_to_print_idx = layer_to_print_idx; - pass_ref.plan = &plan; - for (size_t extruder_id = 0; extruder_id < plan.painted_masks_by_extruder.size(); ++extruder_id) { - if (extruder_id >= compensated_masks_by_extruder.size()) - continue; - const ExPolygons &pass_masks = compensated_masks_by_extruder[extruder_id]; - if (pass_masks.empty()) - continue; - if (layer_has_local_z_extrusions(*layer_to_print.object_layer, pass_masks, local_z_infill_enabled)) - pass_ref.extruders.push_back(unsigned(extruder_id)); - } - - if (!pass_ref.extruders.empty()) - pass_refs.emplace_back(std::move(pass_ref)); - } - } - - std::sort(pass_refs.begin(), pass_refs.end(), [](const LocalZWipeTowerPassRef &lhs, const LocalZWipeTowerPassRef &rhs) { - assert(lhs.plan != nullptr && rhs.plan != nullptr); - if (lhs.plan->print_z != rhs.plan->print_z) - return lhs.plan->print_z < rhs.plan->print_z; - if (lhs.layer_to_print_idx != rhs.layer_to_print_idx) - return lhs.layer_to_print_idx < rhs.layer_to_print_idx; - return lhs.plan->pass_index < rhs.plan->pass_index; - }); - - auto collect_toolchanges_legacy = [&](int start_tool) { - std::vector legacy_toolchanges; - int active_extruder = start_tool; - size_t pass_ref_idx = 0; - while (pass_ref_idx < pass_refs.size()) { - size_t pass_group_end = pass_ref_idx + 1; - while (pass_group_end < pass_refs.size() && - std::abs(pass_refs[pass_ref_idx].plan->print_z - pass_refs[pass_group_end].plan->print_z) <= EPSILON) { - ++pass_group_end; - } - - std::vector pass_group_extruders; - for (size_t group_idx = pass_ref_idx; group_idx < pass_group_end; ++group_idx) - for (unsigned int extruder_id : pass_refs[group_idx].extruders) - if (std::find(pass_group_extruders.begin(), pass_group_extruders.end(), extruder_id) == pass_group_extruders.end()) - pass_group_extruders.push_back(extruder_id); - - std::vector next_group_extruders; - if (pass_group_end < pass_refs.size()) { - size_t next_group_end = pass_group_end + 1; - while (next_group_end < pass_refs.size() && - std::abs(pass_refs[pass_group_end].plan->print_z - pass_refs[next_group_end].plan->print_z) <= EPSILON) { - ++next_group_end; - } - for (size_t group_idx = pass_group_end; group_idx < next_group_end; ++group_idx) - for (unsigned int extruder_id : pass_refs[group_idx].extruders) - if (std::find(next_group_extruders.begin(), next_group_extruders.end(), extruder_id) == next_group_extruders.end()) - next_group_extruders.push_back(extruder_id); - } - - const int preferred_last_extruder = - shared_local_z_extruder_for_wipe_tower(pass_group_extruders, next_group_extruders); - const std::vector ordered_group_extruders = - LocalZOrderOptimizer::order_bucket_extruders(pass_group_extruders, active_extruder, preferred_last_extruder); - - for (unsigned int extruder_id : ordered_group_extruders) { - if (active_extruder >= 0 && active_extruder != int(extruder_id)) - legacy_toolchanges.push_back(LocalZWipeTowerToolchange{unsigned(active_extruder), extruder_id}); - active_extruder = int(extruder_id); - } - - pass_ref_idx = pass_group_end; - } - - return legacy_toolchanges; - }; - - const bool dependency_chain_mode = - !pass_refs.empty() && - std::all_of(pass_refs.begin(), pass_refs.end(), [](const LocalZWipeTowerPassRef &pass_ref) { - return pass_ref.plan != nullptr && pass_ref.plan->dependency_group != 0; - }); - if (!dependency_chain_mode) - return collect_toolchanges_legacy(start_extruder); - - struct ChainKey { - size_t layer_to_print_idx { 0 }; - size_t dependency_group { 0 }; - - bool operator<(const ChainKey &rhs) const - { - if (layer_to_print_idx != rhs.layer_to_print_idx) - return layer_to_print_idx < rhs.layer_to_print_idx; - return dependency_group < rhs.dependency_group; - } - }; - struct PassState { - const LocalZWipeTowerPassRef *pass_ref { nullptr }; - std::vector remaining_extruders; - size_t chain_idx { 0 }; - size_t chain_pos { 0 }; - bool ready { false }; - bool completed { false }; - }; - - std::map chain_index_by_key; - std::vector> chains; - std::vector pass_states; - pass_states.reserve(pass_refs.size()); - for (const LocalZWipeTowerPassRef &pass_ref : pass_refs) { - ChainKey chain_key { pass_ref.layer_to_print_idx, pass_ref.plan->dependency_group }; - auto [it_chain, inserted] = chain_index_by_key.emplace(chain_key, chains.size()); - if (inserted) - chains.emplace_back(); - - const size_t chain_idx = it_chain->second; - const size_t pass_state_idx = pass_states.size(); - pass_states.push_back(PassState{ &pass_ref, pass_ref.extruders, chain_idx, 0, false, false }); - chains[chain_idx].push_back(pass_state_idx); - } - - for (std::vector &chain : chains) { - std::sort(chain.begin(), chain.end(), [&pass_states](size_t lhs_idx, size_t rhs_idx) { - const SubLayerPlan &lhs = *pass_states[lhs_idx].pass_ref->plan; - const SubLayerPlan &rhs = *pass_states[rhs_idx].pass_ref->plan; - if (lhs.dependency_order != rhs.dependency_order) - return lhs.dependency_order < rhs.dependency_order; - if (std::abs(lhs.print_z - rhs.print_z) > EPSILON) - return lhs.print_z < rhs.print_z; - return lhs.pass_index < rhs.pass_index; - }); - for (size_t chain_pos = 0; chain_pos < chain.size(); ++chain_pos) - pass_states[chain[chain_pos]].chain_pos = chain_pos; - if (!chain.empty()) - pass_states[chain.front()].ready = true; - } - - auto pass_contains_extruder = [](const PassState &pass_state, unsigned int extruder_id) { - return std::find(pass_state.remaining_extruders.begin(), pass_state.remaining_extruders.end(), extruder_id) != - pass_state.remaining_extruders.end(); - }; - - auto choose_ready_extruder = [&](int active_extruder) -> int { - std::vector ready_extruders; - for (const PassState &pass_state : pass_states) { - if (!pass_state.ready || pass_state.completed) - continue; - for (unsigned int extruder_id : pass_state.remaining_extruders) - if (std::find(ready_extruders.begin(), ready_extruders.end(), extruder_id) == ready_extruders.end()) - ready_extruders.push_back(extruder_id); - } - if (ready_extruders.empty()) - return -1; - if (active_extruder >= 0 && - std::find(ready_extruders.begin(), ready_extruders.end(), unsigned(active_extruder)) != ready_extruders.end()) { - return active_extruder; - } - - int best_extruder = -1; - size_t best_ready_count = 0; - size_t best_future_count = 0; - for (unsigned int extruder_id : ready_extruders) { - size_t ready_count = 0; - size_t future_count = 0; - for (const PassState &pass_state : pass_states) { - if (pass_state.completed || !pass_contains_extruder(pass_state, extruder_id)) - continue; - ++future_count; - if (pass_state.ready) - ++ready_count; - } - - if (best_extruder < 0 || - ready_count > best_ready_count || - (ready_count == best_ready_count && future_count > best_future_count) || - (ready_count == best_ready_count && future_count == best_future_count && extruder_id < unsigned(best_extruder))) { - best_extruder = int(extruder_id); - best_ready_count = ready_count; - best_future_count = future_count; - } - } - return best_extruder; - }; - - std::vector toolchanges; - int active_extruder = start_extruder; - size_t completed_passes = 0; - while (completed_passes < pass_states.size()) { - const int chosen_extruder = choose_ready_extruder(active_extruder); - if (chosen_extruder < 0) { - BOOST_LOG_TRIVIAL(warning) << "Local-Z wipe tower dependency scheduler deadlocked, falling back" - << " start_extruder=" << start_extruder - << " pass_count=" << pass_refs.size(); - return collect_toolchanges_legacy(start_extruder); - } - - if (active_extruder >= 0 && active_extruder != chosen_extruder) - toolchanges.push_back(LocalZWipeTowerToolchange{unsigned(active_extruder), unsigned(chosen_extruder)}); - active_extruder = chosen_extruder; - - bool completed_any = false; - std::vector newly_completed; - for (size_t pass_state_idx = 0; pass_state_idx < pass_states.size(); ++pass_state_idx) { - PassState &pass_state = pass_states[pass_state_idx]; - if (!pass_state.ready || pass_state.completed) - continue; - - auto it_extruder = std::find(pass_state.remaining_extruders.begin(), - pass_state.remaining_extruders.end(), - unsigned(chosen_extruder)); - if (it_extruder == pass_state.remaining_extruders.end()) - continue; - - pass_state.remaining_extruders.erase(it_extruder); - completed_any = true; - if (pass_state.remaining_extruders.empty()) - newly_completed.push_back(pass_state_idx); - } - - if (!completed_any) { - BOOST_LOG_TRIVIAL(warning) << "Local-Z wipe tower dependency scheduler made no progress, falling back" - << " start_extruder=" << start_extruder - << " active_extruder=" << active_extruder - << " chosen_extruder=" << chosen_extruder - << " pass_count=" << pass_refs.size(); - return collect_toolchanges_legacy(start_extruder); - } - - for (size_t pass_state_idx : newly_completed) { - PassState &pass_state = pass_states[pass_state_idx]; - if (pass_state.completed) - continue; - - pass_state.ready = false; - pass_state.completed = true; - ++completed_passes; - - const std::vector &chain = chains[pass_state.chain_idx]; - const size_t next_chain_pos = pass_state.chain_pos + 1; - if (next_chain_pos < chain.size()) - pass_states[chain[next_chain_pos]].ready = true; - } - } - - BOOST_LOG_TRIVIAL(info) << "Local-Z wipe tower dependency scheduler" - << " start_extruder=" << start_extruder - << " pass_count=" << pass_refs.size() - << " chain_count=" << chains.size() - << " toolchanges=" << toolchanges.size(); - return toolchanges; -} - -} // namespace - //BBS // ORCA: Now this is a parameter //float Print::min_skirt_length = 0; @@ -695,19 +247,6 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n || opt_key == "filament_shrinkage_compensation_z" || opt_key == "resolution" || opt_key == "precise_z_height" - || opt_key == "dithering_z_step_size" - || opt_key == "dithering_local_z_mode" - || opt_key == "dithering_local_z_whole_objects" - || opt_key == "dithering_local_z_direct_multicolor" - || opt_key == "dithering_step_painted_zones_only" - || opt_key == "mixed_filament_gradient_mode" - || opt_key == "mixed_filament_height_lower_bound" - || opt_key == "mixed_filament_height_upper_bound" - || opt_key == "mixed_filament_advanced_dithering" - || opt_key == "mixed_filament_component_bias_enabled" - || opt_key == "mixed_filament_surface_indentation" - || opt_key == "mixed_filament_region_collapse" - || opt_key == "mixed_filament_definitions" // Spiral Vase forces different kind of slicing than the normal model: // In Spiral Vase mode, holes are closed and only the largest area contour is kept at each layer. // Therefore toggling the Spiral Vase on / off requires complete reslicing. @@ -777,7 +316,6 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n || opt_key == "wipe_tower_fillet_wall" || opt_key == "wipe_tower_filament" || opt_key == "wiping_volumes_extruders" - || opt_key == "dithering_local_z_infill" || opt_key == "enable_filament_ramming" || opt_key == "purge_in_prime_tower" || opt_key == "z_offset" @@ -940,11 +478,10 @@ std::vector Print::extruders(bool conside_custom_gcode) const if (conside_custom_gcode) { //BBS - const size_t num_physical = m_config.filament_colour.size(); - const size_t num_filaments = m_mixed_filament_mgr.total_filaments(num_physical); + int num_extruders = m_config.filament_colour.size(); if (m_model.plates_custom_gcodes.find(m_model.curr_plate_index) != m_model.plates_custom_gcodes.end()) { for (auto item : m_model.plates_custom_gcodes.at(m_model.curr_plate_index).gcodes) { - if (item.type == CustomGCode::Type::ToolChange && item.extruder <= int(num_filaments)) + if (item.type == CustomGCode::Type::ToolChange && item.extruder <= num_extruders) extruders.push_back((unsigned int)(item.extruder - 1)); } } @@ -1527,13 +1064,17 @@ StringObjectException Print::check_multi_filament_valid(const Print& print) { auto print_config = print.config(); std::vector extruders = print.extruders(); - std::vector filament_types; - filament_types.reserve(extruders.size()); - for (const auto& extruder_idx : extruders) - filament_types.push_back(print_config.filament_type.get_at(extruder_idx)); - - if (!check_multi_filaments_compatibility(filament_types)) + bool has_high = false, has_low = false; + for (const auto& extruder_idx : extruders) { + if (extruder_idx < print_config.filament_is_high_temperature.size()) { + if (print_config.filament_is_high_temperature.get_at(extruder_idx)) + has_high = true; + else + has_low = true; + } + } + if (has_high && has_low) return {L("Cannot print multiple filaments which have large difference of temperature together. Otherwise, the extruder and nozzle may be blocked or damaged during printing.")}; return {std::string()}; @@ -1548,7 +1089,6 @@ boost::regex regex_g92e0 { "^[ \\t]*[gG]92[ \\t]*[eE](0(\\.0*)?|\\.0+)[ \\t]*(;. StringObjectException Print::validate(StringObjectException *warning, Polygons* collison_polygons, std::vector>* height_polygons) const { std::vector extruders = this->extruders(); - unsigned int nozzles = m_config.nozzle_diameter.size(); if (m_objects.empty()) return {std::string()}; @@ -1556,10 +1096,13 @@ StringObjectException Print::validate(StringObjectException *warning, Polygons* if (extruders.empty()) return { L("No extrusions under current settings.") }; - if (nozzles < 2 && extruders.size() > 1 && m_config.print_sequence != PrintSequence::ByObject) { + // High/low temperature filament mixing check — enforced at both the + // real-time UI level (Plater::check_filament_temp_mixing) and here in + // validate() so that the background process does not clear the error + // notification (see Plater::priv::update line ~6507). + if (extruders.size() > 1) { auto ret = check_multi_filament_valid(*this); - if (!ret.string.empty()) - { + if (!ret.string.empty()) { ret.type = STRING_EXCEPT_FILAMENTS_DIFFERENT_TEMP; return ret; } @@ -1615,7 +1158,7 @@ StringObjectException Print::validate(StringObjectException *warning, Polygons* layer_height_profiles.assign(m_objects.size(), std::vector()); std::vector &profile = layer_height_profiles[print_object_idx]; if (profile.empty()) - PrintObject::update_layer_height_profile(*print_object.model_object(), print_object.slicing_parameters(), profile, &print_object); + PrintObject::update_layer_height_profile(*print_object.model_object(), print_object.slicing_parameters(), profile); return profile; }; @@ -3309,8 +2852,6 @@ void Print::_make_wipe_tower() // Initialize the wipe tower. WipeTower2 wipe_tower(m_config, m_default_region_config, m_plate_index, m_origin, wipe_volumes, m_wipe_tower_data.tool_ordering.first_extruder()); - const std::vector>> layers_to_print = GCode::collect_layers_to_print(*this); - size_t layers_to_print_idx = 0; // wipe_tower.set_retract(); // wipe_tower.set_zhop(); @@ -3329,52 +2870,10 @@ void Print::_make_wipe_tower() for (auto &layer_tools : m_wipe_tower_data.tool_ordering.layer_tools()) { // for all layers if (!layer_tools.has_wipe_tower) continue; - while (layers_to_print_idx + 1 < layers_to_print.size() && - layers_to_print[layers_to_print_idx].first + EPSILON < layer_tools.print_z) { - ++layers_to_print_idx; - } - - const std::vector *layers_with_same_print_z = nullptr; - if (layers_to_print_idx < layers_to_print.size() && - std::abs(layers_to_print[layers_to_print_idx].first - layer_tools.print_z) <= EPSILON) { - layers_with_same_print_z = &layers_to_print[layers_to_print_idx].second; - } - bool first_layer = &layer_tools == &m_wipe_tower_data.tool_ordering.front(); - - if (layers_with_same_print_z != nullptr) { - const std::vector local_z_toolchanges = - collect_local_z_wipe_tower_toolchanges(*this, *layers_with_same_print_z, int(current_extruder_id)); - if (!local_z_toolchanges.empty()) { - std::ostringstream local_z_sequence; - for (size_t toolchange_idx = 0; toolchange_idx < local_z_toolchanges.size(); ++toolchange_idx) { - if (toolchange_idx != 0) - local_z_sequence << ","; - local_z_sequence << local_z_toolchanges[toolchange_idx].old_tool << "->" - << local_z_toolchanges[toolchange_idx].new_tool; - } - - BOOST_LOG_TRIVIAL(debug) << "Local-Z wipe tower preplan" - << " print_z=" << layer_tools.print_z - << " start_tool=" << current_extruder_id - << " nominal_toolchanges=" << layer_tools.extruders.size() - << " local_z_toolchanges=" << local_z_toolchanges.size() - << " sequence=" << local_z_sequence.str(); - } - for (const LocalZWipeTowerToolchange &toolchange : local_z_toolchanges) { - wipe_tower.plan_local_z_toolchange((float) layer_tools.print_z, (float) layer_tools.wipe_tower_layer_height, - toolchange.old_tool, toolchange.new_tool, (float) m_config.prime_volume); - } - if (!local_z_toolchanges.empty()) - current_extruder_id = local_z_toolchanges.back().new_tool; - } - - const std::vector nominal_layer_extruders = - rotate_extruders_to_start_with(layer_tools.extruders, current_extruder_id); - wipe_tower.plan_toolchange((float) layer_tools.print_z, (float) layer_tools.wipe_tower_layer_height, current_extruder_id, current_extruder_id, false); - for (const auto extruder_id : nominal_layer_extruders) { + for (const auto extruder_id : layer_tools.extruders) { if ((first_layer && extruder_id == m_wipe_tower_data.tool_ordering.all_extruders().back()) || extruder_id != current_extruder_id) { float volume_to_wipe = m_config.prime_volume; @@ -3398,7 +2897,6 @@ void Print::_make_wipe_tower() current_extruder_id = extruder_id; } } - layer_tools.wiping_extrusions().ensure_perimeters_infills_order(*this); if (&layer_tools == &m_wipe_tower_data.tool_ordering.back() || (&layer_tools + 1)->wipe_tower_partitions == 0) break; @@ -3407,14 +2905,9 @@ void Print::_make_wipe_tower() // Generate the wipe tower layers. m_wipe_tower_data.tool_changes.reserve(m_wipe_tower_data.tool_ordering.layer_tools().size()); - m_wipe_tower_data.local_z_tool_changes.reserve(m_wipe_tower_data.tool_ordering.layer_tools().size()); - wipe_tower.generate(m_wipe_tower_data.tool_changes, m_wipe_tower_data.local_z_tool_changes); - BOOST_LOG_TRIVIAL(debug) << "Wipe tower generation completed" - << " nominal_layers=" << m_wipe_tower_data.tool_changes.size() - << " local_z_layers=" << m_wipe_tower_data.local_z_tool_changes.size(); + wipe_tower.generate(m_wipe_tower_data.tool_changes); m_wipe_tower_data.depth = wipe_tower.get_depth(); m_wipe_tower_data.z_and_depth_pairs = wipe_tower.get_z_and_depth_pairs(); - m_wipe_tower_data.local_z_reserve_boxes = wipe_tower.get_local_z_reserve_boxes(); m_wipe_tower_data.brim_width = wipe_tower.get_brim_width(); m_wipe_tower_data.height = wipe_tower.get_wipe_tower_height(); @@ -4879,10 +4372,7 @@ BoundingBoxf3 PrintInstance::get_bounding_box() { Polygon PrintInstance::get_convex_hull_2d() { Polygon poly = print_object->model_object()->convex_hull_2d(model_instance->get_matrix()); - // Change the distance threshold of the Douglas-Peucker algorithm to 0.5 millimeter and reduce the number of points - poly.douglas_peucker(scale_(0.5)); - // Round coordinates to 0.1mm grid to limit decimal places - poly.round_to_grid(scale_(0.1)); + poly.douglas_peucker(0.1); return poly; } diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 9704b215ac7..cdcdd415522 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -1,32 +1,15 @@ #include "Plater.hpp" -#include "MixedFilamentDialog.hpp" -#include "MixedGradientSelector.hpp" -#include "MixedColorMatchPanel.hpp" -#include "MixedFilamentBadge.hpp" -#include "MixedFilamentColorMapPanel.hpp" #include "libslic3r/Config.hpp" -#include "libslic3r/MixedFilament.hpp" -#include "libslic3r/filament_mixer.h" #include "common_func/common_func.hpp" #include -#include -#include -#include #include -#include #include -#include -#include -#include #include #include #include -#include #include #include -#include -#include #include #include #include @@ -52,23 +35,14 @@ #include #include #include -#include -#include #include #include -#include -#include -#include -#include -#include #ifdef _WIN32 #include #include #include #endif #include -#include -#include #include #include @@ -132,7 +106,6 @@ #include "ModelMall.hpp" #include "ConfigWizard.hpp" #include "../Utils/ASCIIFolding.hpp" -#include "../Utils/ColorSpaceConvert.hpp" #include "../Utils/FixModelByWin10.hpp" #include "../Utils/UndoRedo.hpp" #include "../Utils/PresetUpdater.hpp" @@ -235,7 +208,6 @@ wxDEFINE_EVENT(EVT_ADD_FILAMENT, SimpleEvent); wxDEFINE_EVENT(EVT_DEL_FILAMENT, SimpleEvent); wxDEFINE_EVENT(EVT_ADD_CUSTOM_FILAMENT, ColorEvent); - #define PRINTER_THUMBNAIL_SIZE (wxSize(FromDIP(48), FromDIP(48))) #define PRINTER_THUMBNAIL_SIZE_SMALL (wxSize(FromDIP(32), FromDIP(32))) #define PRINTER_PANEL_SIZE_SMALL (wxSize(FromDIP(98), FromDIP(68))) @@ -720,7 +692,6 @@ struct Sidebar::priv wxComboBox* m_comboBox_print_preset; wxStaticLine* m_staticline1; StaticBox* m_panel_filament_title; - ScalableButton* m_filament_config_icon = nullptr; wxStaticText* m_staticText_filament_settings; ScalableButton * m_bpButton_add_filament; ScalableButton * m_bpButton_del_filament; @@ -729,40 +700,6 @@ struct Sidebar::priv int m_menu_filament_id = -1; wxPanel* m_panel_filament_content; wxScrolledWindow* m_scrolledWindow_filament_content; - - // Mixed (virtual) filaments panel - collapsible like Printer/Filament sections - StaticBox* m_panel_mixed_filaments_title = nullptr; // Collapsible title bar - wxPanel* m_panel_mixed_filaments_content = nullptr; // Content panel - wxBoxSizer* m_sizer_mixed_filaments_content = nullptr; // Content sizer - ScalableButton* m_mixed_filaments_icon = nullptr; // Icon - wxStaticText* m_staticText_mixed_filaments = nullptr; // Title text - Button* m_btn_add_gradient = nullptr; // Add gradient button - Button* m_btn_add_pattern = nullptr; // Add pattern button - Button* m_btn_add_color = nullptr; // Add color-match button - Button* m_btn_toggle_mixed_filaments = nullptr; // Collapse/expand toggle button - bool m_mixed_filaments_collapsed = false; // Collapse state - bool m_skip_mixed_filament_sync_once = false; // Local edits already mutated manager in place. - std::unordered_set m_expanded_mixed_filament_rows; // Expanded row editors - struct MixedFilamentRowBinding { - size_t mixed_id = size_t(-1); - wxWindow *row = nullptr; - }; - std::vector m_mixed_filament_row_bindings; - std::vector m_mixed_filament_ui_order; - bool m_mixed_filament_drag_active = false; - size_t m_mixed_filament_drag_source_mixed_id = size_t(-1); - // Physical filament scrolled window - wxScrolledWindow* m_scrolled_filaments = nullptr; - wxPanel* m_panel_scrolled_filament_content = nullptr; - // Color mix panel - StaticBox* m_panel_physical_filaments_title = nullptr; - StaticBox* m_panel_color_mix_title = nullptr; - wxPanel* m_panel_color_mix_content = nullptr; - wxScrolledWindow* m_scrolled_color_mix = nullptr; - ScalableButton* m_color_mix_icon = nullptr; - ScalableButton* m_btn_add_color_mix = nullptr; - ScalableButton* m_btn_del_color_mix = nullptr; - wxStaticLine* m_staticline2; wxPanel* m_panel_project_title; ScalableButton* m_filament_icon = nullptr; @@ -962,7 +899,6 @@ std::vector get_min_flush_volumes(const DynamicPrintConfig& full_config) return extra_flush_volumes; } - // Sidebar / public struct DynamicFilamentList : DynamicList @@ -1060,528 +996,6 @@ struct DynamicFilamentList1Based : DynamicFilamentList }; -class MixedFilamentColorMatchDialog : public DPIDialog -{ -public: - MixedFilamentColorMatchDialog(wxWindow* parent, const std::vector& physical_colors, const wxColour& initial_color) - : DPIDialog(parent ? parent : static_cast(wxGetApp().mainframe), - wxID_ANY, - _L("Add Color"), - wxDefaultPosition, - wxDefaultSize, - wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER) - , m_physical_colors(physical_colors) - { - m_recipe_timer.SetOwner(this); - m_loading_timer.SetOwner(this); - m_display_context = build_mixed_filament_display_context(m_physical_colors); - - m_palette.reserve(m_physical_colors.size()); - for (const std::string& hex : m_physical_colors) - m_palette.emplace_back(parse_mixed_color(hex)); - - const wxColour safe_initial = initial_color.IsOk() ? - initial_color : - (m_palette.size() >= 2 ? blend_pair_filament_mixer(m_palette[0], m_palette[1], 0.5f) : - wxColour("#26A69A")); - std::vector initial_weights(m_palette.size(), 0); - if (!initial_weights.empty()) - initial_weights[0] = 100; - if (initial_weights.size() >= 2) { - initial_weights[0] = 50; - initial_weights[1] = 50; - } - - std::vector filament_ids; - filament_ids.reserve(m_palette.size()); - for (size_t idx = 0; idx < m_palette.size(); ++idx) - filament_ids.emplace_back(unsigned(idx + 1)); - - SetMinSize(wxSize(FromDIP(430), FromDIP(520))); - - auto* root = new wxBoxSizer(wxVERTICAL); - auto* description = new wxStaticText(this, wxID_ANY, - _L("Pick from the current filament gamut. The dialog previews the closest 2-color, 3-color, " - "or 4-color FilamentMixer recipe before it is added.")); - description->Wrap(FromDIP(390)); - root->Add(description, 0, wxEXPAND | wxALL, FromDIP(12)); - - m_color_map = new MixedFilamentColorMapPanel(this, filament_ids, m_palette, initial_weights, wxSize(FromDIP(260), FromDIP(260))); - root->Add(m_color_map, 1, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(12)); - - auto* hex_row = new wxBoxSizer(wxHORIZONTAL); - hex_row->Add(new wxStaticText(this, wxID_ANY, _L("Hex")), 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(8)); - m_hex_input = new wxTextCtrl(this, wxID_ANY, normalize_color_match_hex(safe_initial.GetAsString(wxC2S_HTML_SYNTAX)), - wxDefaultPosition, wxDefaultSize, wxTE_PROCESS_ENTER); - m_hex_input->SetToolTip(_L("Enter a hex color like #00FF88. The picker will snap to the closest supported FilamentMixer color.")); - hex_row->Add(m_hex_input, 1, wxALIGN_CENTER_VERTICAL); - hex_row->AddSpacer(FromDIP(8)); - m_classic_picker = new wxColourPickerCtrl(this, wxID_ANY, safe_initial); - m_classic_picker->SetToolTip(_L("Classic color picker. The result will snap to the closest supported FilamentMixer color.")); - hex_row->Add(m_classic_picker, 0, wxALIGN_CENTER_VERTICAL); - root->Add(hex_row, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, FromDIP(12)); - - auto* range_row = new wxBoxSizer(wxHORIZONTAL); - range_row->Add(new wxStaticText(this, wxID_ANY, _L("Range")), 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(8)); - m_range_slider = new wxSlider(this, wxID_ANY, m_min_component_percent, 0, 50); - m_range_slider->SetToolTip(_L("Minimum percent for each participating color. Higher values block highly skewed mixes.")); - range_row->Add(m_range_slider, 1, wxALIGN_CENTER_VERTICAL); - range_row->AddSpacer(FromDIP(8)); - m_range_value = new wxStaticText(this, wxID_ANY, wxEmptyString); - range_row->Add(m_range_value, 0, wxALIGN_CENTER_VERTICAL); - root->Add(range_row, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, FromDIP(12)); - - auto* summary_grid = new wxFlexGridSizer(2, FromDIP(8), FromDIP(8)); - summary_grid->AddGrowableCol(1, 1); - - summary_grid->Add(new wxStaticText(this, wxID_ANY, _L("Requested")), 0, wxALIGN_CENTER_VERTICAL); - auto* selected_row = new wxBoxSizer(wxHORIZONTAL); - m_selected_preview = new wxPanel(this, wxID_ANY, wxDefaultPosition, wxSize(FromDIP(72), FromDIP(24)), wxBORDER_SIMPLE); - selected_row->Add(m_selected_preview, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(8)); - m_selected_label = new wxStaticText(this, wxID_ANY, wxEmptyString); - selected_row->Add(m_selected_label, 1, wxALIGN_CENTER_VERTICAL); - summary_grid->Add(selected_row, 1, wxEXPAND); - - summary_grid->Add(new wxStaticText(this, wxID_ANY, _L("Creates")), 0, wxALIGN_CENTER_VERTICAL); - auto* recipe_row = new wxBoxSizer(wxHORIZONTAL); - m_recipe_preview = new wxPanel(this, wxID_ANY, wxDefaultPosition, wxSize(FromDIP(72), FromDIP(24)), wxBORDER_SIMPLE); - recipe_row->Add(m_recipe_preview, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(8)); - m_recipe_label = new wxStaticText(this, wxID_ANY, wxEmptyString); - m_recipe_label->Wrap(FromDIP(280)); - recipe_row->Add(m_recipe_label, 1, wxALIGN_CENTER_VERTICAL); - summary_grid->Add(recipe_row, 1, wxEXPAND); - - root->Add(summary_grid, 0, wxEXPAND | wxALL, FromDIP(12)); - - m_delta_label = new wxStaticText(this, wxID_ANY, wxEmptyString); - root->Add(m_delta_label, 0, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(12)); - - m_presets_label = new wxStaticText(this, wxID_ANY, _L("Exact preset mixes")); - root->Add(m_presets_label, 0, wxLEFT | wxRIGHT | wxTOP, FromDIP(12)); - m_presets_host = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxSize(-1, FromDIP(96)), wxVSCROLL | wxBORDER_SIMPLE); - m_presets_host->SetScrollRate(FromDIP(6), FromDIP(6)); - m_presets_sizer = new wxWrapSizer(wxHORIZONTAL, wxWRAPSIZER_DEFAULT_FLAGS); - m_presets_host->SetSizer(m_presets_sizer); - root->Add(m_presets_host, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, FromDIP(12)); - - m_error_label = new wxStaticText(this, wxID_ANY, wxEmptyString); - m_error_label->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#D32F2F"))); - root->Add(m_error_label, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, FromDIP(12)); - - if (wxSizer* button_sizer = CreateStdDialogButtonSizer(wxOK | wxCANCEL)) - root->Add(button_sizer, 0, wxEXPAND | wxALL, FromDIP(12)); - - m_loading_panel = new wxPanel(this, wxID_ANY); - m_loading_panel->SetMinSize(wxSize(-1, FromDIP(24))); - auto* loading_row = new wxBoxSizer(wxHORIZONTAL); - m_loading_label = new wxStaticText(m_loading_panel, wxID_ANY, " "); - loading_row->Add(m_loading_label, 1, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(8)); - m_loading_gauge = new wxGauge(m_loading_panel, wxID_ANY, 100, wxDefaultPosition, wxSize(FromDIP(150), FromDIP(8)), - wxGA_HORIZONTAL | wxGA_SMOOTH); - m_loading_gauge->SetValue(0); - m_loading_gauge->Enable(false); - loading_row->Add(m_loading_gauge, 0, wxALIGN_CENTER_VERTICAL); - m_loading_panel->SetSizer(loading_row); - root->Add(m_loading_panel, 0, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, FromDIP(12)); - - SetSizerAndFit(root); - - m_selected_target = safe_initial; - m_requested_target = safe_initial; - if (m_color_map) - m_color_map->set_min_component_percent(m_min_component_percent); - update_range_label(); - rebuild_presets_ui(); - sync_inputs_to_requested(); - update_dialog_state(); - - if (m_color_map) { - m_color_map->Bind(wxEVT_SLIDER, [this](wxCommandEvent&) { - if (!m_color_map) - return; - request_recipe_match(m_color_map->selected_color(), true, _L("Matching closest supported mix...")); - }); - } - - if (m_hex_input) { - m_hex_input->Bind(wxEVT_TEXT_ENTER, [this](wxCommandEvent&) { apply_hex_input(true); }); - m_hex_input->Bind(wxEVT_KILL_FOCUS, [this](wxFocusEvent& evt) { - apply_hex_input(false); - evt.Skip(); - }); - } - if (m_classic_picker) { - m_classic_picker->Bind(wxEVT_COLOURPICKER_CHANGED, [this](wxColourPickerEvent& evt) { - if (m_syncing_inputs) - return; - apply_requested_target(evt.GetColour()); - }); - } - if (m_range_slider) { - m_range_slider->Bind(wxEVT_SLIDER, [this](wxCommandEvent&) { - m_min_component_percent = m_range_slider ? std::clamp(m_range_slider->GetValue(), 0, 50) : m_min_component_percent; - update_range_label(); - if (m_color_map) - m_color_map->set_min_component_percent(m_min_component_percent); - rebuild_presets_ui(); - request_recipe_match(m_requested_target, true, _L("Matching closest supported mix...")); - }); - } - - Bind(wxEVT_TIMER, [this](wxTimerEvent&) { refresh_selected_recipe(); }, m_recipe_timer.GetId()); - Bind( - wxEVT_TIMER, - [this](wxTimerEvent&) { - if (m_loading_gauge && m_recipe_loading) - m_loading_gauge->Pulse(); - }, - m_loading_timer.GetId()); - if (wxWindow* ok_button = FindWindow(wxID_OK)) { - ok_button->Bind(wxEVT_BUTTON, [this](wxCommandEvent& evt) { - if (m_recipe_refresh_pending) - refresh_selected_recipe(); - if (m_recipe_loading || !m_selected_recipe.valid) - return; - evt.Skip(); - }); - } - - Bind(wxEVT_CLOSE_WINDOW, [this](wxCloseEvent&) { EndModal(wxID_CANCEL); }); - - CentreOnParent(); - wxGetApp().UpdateDlgDarkUI(this); - } - - ~MixedFilamentColorMatchDialog() override - { - if (m_recipe_timer.IsRunning()) - m_recipe_timer.Stop(); - if (m_loading_timer.IsRunning()) - m_loading_timer.Stop(); - } - - void begin_initial_recipe_load() { request_recipe_match(m_requested_target, false, _L("Calculating closest supported mix...")); } - - MixedColorMatchRecipeResult selected_recipe() const { return m_selected_recipe; } - - void on_dpi_changed(const wxRect& suggested_rect) override - { - wxUnusedVar(suggested_rect); - Layout(); - Fit(); - Refresh(); - } - -private: - void sync_recipe_preview(MixedColorMatchRecipeResult& recipe, const wxColour* requested_target = nullptr) - { - if (!recipe.valid) - return; - - recipe.preview_color = compute_color_match_recipe_display_color(recipe, m_display_context); - if (requested_target != nullptr && requested_target->IsOk()) - recipe.delta_e = color_delta_e00(*requested_target, recipe.preview_color); - } - - void update_range_label() - { - if (m_range_value) - m_range_value->SetLabel(wxString::Format(_L("%d%% min"), m_min_component_percent)); - } - - void rebuild_presets_ui() - { - if (!m_presets_host || !m_presets_sizer || !m_presets_label) - return; - - m_presets = build_color_match_presets(m_physical_colors, m_min_component_percent); - for (MixedColorMatchRecipeResult& preset : m_presets) - sync_recipe_preview(preset); - - m_presets_host->Freeze(); - while (m_presets_sizer->GetItemCount() > 0) { - wxSizerItem* item = m_presets_sizer->GetItem(size_t(0)); - wxWindow* window = item ? item->GetWindow() : nullptr; - m_presets_sizer->Remove(0); - if (window) - window->Destroy(); - } - - for (const MixedColorMatchRecipeResult& preset : m_presets) { - auto* button = new wxBitmapButton(m_presets_host, wxID_ANY, - make_color_match_swatch_bitmap(preset.preview_color, wxSize(FromDIP(30), FromDIP(20))), - wxDefaultPosition, wxDefaultSize, wxBU_EXACTFIT); - const wxString tooltip = from_u8(summarize_color_match_recipe(preset)) + "\n" + - normalize_color_match_hex(preset.preview_color.GetAsString(wxC2S_HTML_SYNTAX)); - button->SetToolTip(tooltip); - button->Bind(wxEVT_BUTTON, [this, preset](wxCommandEvent&) { apply_preset(preset); }); - m_presets_sizer->Add(button, 0, wxALL, FromDIP(2)); - } - - m_presets_host->FitInside(); - const bool show_presets = !m_presets.empty(); - m_presets_label->Show(show_presets); - m_presets_host->Show(show_presets); - m_presets_host->Thaw(); - } - - void set_recipe_loading(bool loading, const wxString& message) - { - m_recipe_loading = loading; - if (!message.empty()) - m_loading_message = message; - - if (m_loading_label) - m_loading_label->SetLabel(loading ? m_loading_message : wxString(" ")); - if (m_loading_gauge) { - if (loading) { - m_loading_gauge->Enable(true); - m_loading_gauge->Pulse(); - if (!m_loading_timer.IsRunning()) - m_loading_timer.Start(100); - } else { - if (m_loading_timer.IsRunning()) - m_loading_timer.Stop(); - m_loading_gauge->SetValue(0); - m_loading_gauge->Enable(false); - } - } - } - - void sync_inputs_to_requested() - { - m_syncing_inputs = true; - if (m_hex_input) - m_hex_input->ChangeValue(normalize_color_match_hex(m_requested_target.GetAsString(wxC2S_HTML_SYNTAX))); - if (m_classic_picker) - m_classic_picker->SetColour(m_requested_target); - m_syncing_inputs = false; - } - - bool apply_requested_target(const wxColour& requested_target) - { - request_recipe_match(requested_target, false, _L("Matching closest supported mix...")); - return true; - } - - bool apply_hex_input(bool show_invalid_error) - { - if (!m_hex_input || m_syncing_inputs) - return false; - - wxColour parsed; - if (!try_parse_color_match_hex(m_hex_input->GetValue(), parsed)) { - if (show_invalid_error && m_error_label) - m_error_label->SetLabel(_L("Use a valid hex color like #00FF88.")); - return false; - } - - return apply_requested_target(parsed); - } - - void request_recipe_match(const wxColour& requested_target, bool debounce, const wxString& loading_message) - { - m_requested_target = requested_target; - m_selected_target = requested_target; - sync_inputs_to_requested(); - - ++m_recipe_request_token; - set_recipe_loading(true, loading_message); - - if (m_recipe_timer.IsRunning()) - m_recipe_timer.Stop(); - m_recipe_refresh_pending = debounce; - update_dialog_state(); - - if (debounce) { - m_recipe_timer.StartOnce(120); - return; - } - - launch_recipe_match(m_recipe_request_token, requested_target); - } - - void refresh_selected_recipe() - { - m_recipe_refresh_pending = false; - launch_recipe_match(m_recipe_request_token, m_requested_target); - } - - void launch_recipe_match(size_t request_token, const wxColour& requested_target) - { - const std::vector physical_colors = m_physical_colors; - const int min_component_percent = m_min_component_percent; - wxWeakRef weak_self(this); - std::thread([weak_self, physical_colors, requested_target, request_token, min_component_percent]() { - MixedColorMatchRecipeResult recipe = build_best_color_match_recipe(physical_colors, requested_target, min_component_percent); - wxGetApp().CallAfter([weak_self, requested_target, recipe = std::move(recipe), request_token]() mutable { - if (!weak_self) - return; - auto* self = static_cast(weak_self.get()); - self->handle_recipe_result(request_token, requested_target, std::move(recipe)); - }); - }).detach(); - } - - void handle_recipe_result(size_t request_token, const wxColour& requested_target, MixedColorMatchRecipeResult recipe) - { - if (request_token != m_recipe_request_token) - return; - - m_has_recipe_result = true; - m_selected_recipe = std::move(recipe); - sync_recipe_preview(m_selected_recipe, &requested_target); - set_recipe_loading(false, wxEmptyString); - - if (m_selected_recipe.valid) { - m_selected_target = m_selected_recipe.preview_color; - if (m_color_map) - m_color_map->set_normalized_weights(expand_color_match_recipe_weights(m_selected_recipe, m_palette.size()), false); - sync_inputs_to_requested(); - } else { - m_selected_target = requested_target; - } - - update_dialog_state(); - } - - void apply_preset(MixedColorMatchRecipeResult preset) - { - preset.delta_e = 0.0; - sync_recipe_preview(preset); - ++m_recipe_request_token; - m_requested_target = preset.preview_color; - m_selected_target = preset.preview_color; - m_selected_recipe = std::move(preset); - m_has_recipe_result = true; - m_recipe_refresh_pending = false; - if (m_recipe_timer.IsRunning()) - m_recipe_timer.Stop(); - set_recipe_loading(false, wxEmptyString); - if (m_color_map) - m_color_map->set_normalized_weights(expand_color_match_recipe_weights(m_selected_recipe, m_palette.size()), false); - sync_inputs_to_requested(); - update_dialog_state(); - } - - void update_dialog_state() - { - const wxColour fallback = wxColour("#26A69A"); - if (m_selected_preview) { - m_selected_preview->SetBackgroundColour(m_requested_target.IsOk() ? m_requested_target : fallback); - m_selected_preview->Refresh(); - } - if (m_selected_label) - m_selected_label->SetLabel(m_requested_target.IsOk() ? - normalize_color_match_hex(m_requested_target.GetAsString(wxC2S_HTML_SYNTAX)) : - normalize_color_match_hex(fallback.GetAsString(wxC2S_HTML_SYNTAX))); - - const bool valid = m_selected_recipe.valid; - const wxColour recipe_color = (valid && m_selected_recipe.preview_color.IsOk()) ? - m_selected_recipe.preview_color : - (m_requested_target.IsOk() ? m_requested_target : fallback); - if (m_recipe_preview) { - m_recipe_preview->SetBackgroundColour(recipe_color); - m_recipe_preview->Refresh(); - } - if (m_recipe_label) { - if (m_recipe_loading) { - m_recipe_label->SetLabel(m_loading_message); - } else if (valid) { - const wxString recipe_summary = from_u8(summarize_color_match_recipe(m_selected_recipe)); - const wxString recipe_hex = normalize_color_match_hex(recipe_color.GetAsString(wxC2S_HTML_SYNTAX)); - m_recipe_label->SetLabel(recipe_summary + " " + recipe_hex); - } else if (m_has_recipe_result) { - m_recipe_label->SetLabel(_L("No supported 2-color, 3-color, or 4-color recipe found.")); - } else { - m_recipe_label->SetLabel(wxEmptyString); - } - } - if (m_delta_label) { - if (m_recipe_loading && m_requested_target.IsOk()) { - m_delta_label->SetLabel( - wxString::Format(_L("Matching %s..."), normalize_color_match_hex(m_requested_target.GetAsString(wxC2S_HTML_SYNTAX)))); - } else if (valid && m_requested_target.IsOk()) { - m_delta_label->SetLabel(wxString::Format(_L("Requested %s, closest recipe delta: %.2f"), - normalize_color_match_hex(m_requested_target.GetAsString(wxC2S_HTML_SYNTAX)), - m_selected_recipe.delta_e)); - } else { - m_delta_label->SetLabel(wxEmptyString); - } - } - if (m_error_label) { - if (m_recipe_loading) - m_error_label->SetLabel(wxEmptyString); - else if (!valid && m_has_recipe_result) - m_error_label->SetLabel( - _L("Unable to create a color mix from the current physical filament colors within the selected range.")); - else if (m_hex_input && !m_syncing_inputs) { - wxColour parsed; - if (!try_parse_color_match_hex(m_hex_input->GetValue(), parsed)) - m_error_label->SetLabel(_L("Use a valid hex color like #00FF88.")); - else - m_error_label->SetLabel(wxEmptyString); - } else { - m_error_label->SetLabel(wxEmptyString); - } - } - if (wxWindow* ok_button = FindWindow(wxID_OK)) - ok_button->Enable(valid && !m_recipe_loading && !m_recipe_refresh_pending); - - Layout(); - } - -private: - std::vector m_physical_colors; - MixedFilamentDisplayContext m_display_context; - std::vector m_palette; - std::vector m_presets; - MixedFilamentColorMapPanel* m_color_map = nullptr; - wxTextCtrl* m_hex_input = nullptr; - wxColourPickerCtrl* m_classic_picker = nullptr; - wxSlider* m_range_slider = nullptr; - wxStaticText* m_range_value = nullptr; - wxStaticText* m_presets_label = nullptr; - wxScrolledWindow* m_presets_host = nullptr; - wxWrapSizer* m_presets_sizer = nullptr; - wxPanel* m_loading_panel = nullptr; - wxStaticText* m_loading_label = nullptr; - wxGauge* m_loading_gauge = nullptr; - wxPanel* m_selected_preview = nullptr; - wxStaticText* m_selected_label = nullptr; - wxPanel* m_recipe_preview = nullptr; - wxStaticText* m_recipe_label = nullptr; - wxStaticText* m_delta_label = nullptr; - wxStaticText* m_error_label = nullptr; - wxColour m_requested_target{wxColour("#26A69A")}; - wxColour m_selected_target{wxColour("#26A69A")}; - MixedColorMatchRecipeResult m_selected_recipe; - wxTimer m_recipe_timer; - wxTimer m_loading_timer; - wxString m_loading_message; - size_t m_recipe_request_token{0}; - int m_min_component_percent{15}; - bool m_has_recipe_result{false}; - bool m_recipe_loading{false}; - bool m_recipe_refresh_pending{false}; - bool m_syncing_inputs{false}; -}; - - -MixedColorMatchRecipeResult prompt_best_color_match_recipe(wxWindow* parent, - const std::vector& physical_colors, - const wxColour& initial_color) -{ - MixedFilamentColorMatchDialog dlg(parent, physical_colors, initial_color); - dlg.begin_initial_recipe_load(); - if (dlg.ShowModal() != wxID_OK) { - MixedColorMatchRecipeResult cancelled; - cancelled.cancelled = true; - return cancelled; - } - - return dlg.selected_recipe(); -} static DynamicFilamentList dynamic_filament_list; static DynamicFilamentList1Based dynamic_filament_list_1_based; @@ -1610,6 +1024,11 @@ Sidebar::Sidebar(Plater *parent) Choice::register_dynamic_list("wipe_tower_filament", &dynamic_filament_list); p->scrolled = new wxPanel(this); + // p->scrolled->SetScrollbars(0, 100, 1, 2); // ys_DELETE_after_testing. pixelsPerUnitY = 100 + // but this cause the bad layout of the sidebar, when all infoboxes appear. + // As a result we can see the empty block at the bottom of the sidebar + // But if we set this value to 5, layout will be better + //p->scrolled->SetScrollRate(0, 5); p->scrolled->SetBackgroundColour(*wxWHITE); @@ -2013,7 +1432,7 @@ Sidebar::Sidebar(Plater *parent) p->m_panel_filament_title->SetBackgroundColor2(0xF1F1F1); p->m_panel_filament_title->Bind(wxEVT_LEFT_UP, [this](wxMouseEvent &e) { if (e.GetPosition().x > (p->m_flushing_volume_btn->IsShown() - ? p->m_flushing_volume_btn->GetPosition().x : (p->m_bpButton_ams_filament->GetPosition().x - FromDIP(30)))) + ? p->m_flushing_volume_btn->GetPosition().x : (p->m_bpButton_add_filament->GetPosition().x - FromDIP(30)))) // ORCA exclude area of del button from titlebar collapse/expand feature to fix undesired collapse when user spams del filament button return; if (p->m_panel_filament_content->GetMaxHeight() == 0) p->m_panel_filament_content->SetMaxSize({-1, -1}); @@ -2024,10 +1443,11 @@ Sidebar::Sidebar(Plater *parent) wxBoxSizer* bSizer39; bSizer39 = new wxBoxSizer( wxHORIZONTAL ); - p->m_filament_config_icon = new ScalableButton(p->m_panel_filament_title, wxID_ANY, "filament_group"); - bSizer39->Add(p->m_filament_config_icon, 0, wxALIGN_CENTER | wxLEFT, FromDIP(SidebarProps::TitlebarMargin())); - p->m_staticText_filament_settings = new Label(p->m_panel_filament_title, _L("Filament Management"), LB_PROPAGATE_MOUSE_EVENT); - bSizer39->Add( p->m_staticText_filament_settings, 0, wxALIGN_CENTER | wxLEFT, FromDIP(SidebarProps::TitlebarMargin())); + p->m_filament_icon = new ScalableButton(p->m_panel_filament_title, wxID_ANY, "filament"); + p->m_staticText_filament_settings = new Label(p->m_panel_filament_title, _L("Filament"), LB_PROPAGATE_MOUSE_EVENT); + bSizer39->Add(p->m_filament_icon, 0, wxALIGN_CENTER | wxLEFT, FromDIP(SidebarProps::TitlebarMargin())); + bSizer39->AddSpacer(FromDIP(SidebarProps::ElementSpacing())); + bSizer39->Add( p->m_staticText_filament_settings, 0, wxALIGN_CENTER ); bSizer39->Add(FromDIP(10), 0, 0, 0, 0); bSizer39->SetMinSize(-1, FromDIP(30)); @@ -2057,7 +1477,7 @@ Sidebar::Sidebar(Plater *parent) ConfigOptionFloat* flush_multi_opt = project_config.option("flush_multiplier"); float flush_multiplier = flush_multi_opt ? flush_multi_opt->getFloat() : 1.f; - const std::vector extruder_colours = wxGetApp().plater()->get_extruder_colors_from_plater_config(nullptr, false); + const std::vector extruder_colours = wxGetApp().plater()->get_extruder_colors_from_plater_config(); const auto& full_config = wxGetApp().preset_bundle->full_config(); const auto& extra_flush_volumes = get_min_flush_volumes(full_config); WipingDialog dlg(parent, cast(init_matrix), cast(init_extruders), extruder_colours, extra_flush_volumes, flush_multiplier); @@ -2078,6 +1498,57 @@ Sidebar::Sidebar(Plater *parent) bSizer39->Add(p->m_flushing_volume_btn, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(5)); bSizer39->Hide(p->m_flushing_volume_btn); + ScalableButton* add_btn = new ScalableButton(p->m_panel_filament_title, wxID_ANY, "add_filament"); + add_btn->SetToolTip(_L("Add one filament")); + add_btn->Bind(wxEVT_BUTTON, [this, scrolled_sizer](wxCommandEvent& e){ + // Orca: limit filament choices to MAXIMUM_EXTRUDER_NUMBER + if (p->combos_filament.size() >= MAXIMUM_EXTRUDER_NUMBER) + return; + + int filament_count = p->combos_filament.size() + 1; + wxColour new_col = Plater::get_next_color_for_filament(); + std::string new_color = new_col.GetAsString(wxC2S_HTML_SYNTAX).ToStdString(); + wxGetApp().preset_bundle->set_num_filaments(filament_count, new_color); + wxGetApp().plater()->on_filaments_change(filament_count); + wxGetApp().get_tab(Preset::TYPE_PRINT)->update(); + wxGetApp().preset_bundle->export_selections(*wxGetApp().app_config); + auto_calc_flushing_volumes(filament_count - 1); + }); + p->m_bpButton_add_filament = add_btn; + + // ORCA Moved add button after delete button to prevent add button position change when remove icon automatically hidden + + ScalableButton* del_btn = new ScalableButton(p->m_panel_filament_title, wxID_ANY, "delete_filament"); + del_btn->SetToolTip(_L("Remove last filament")); + del_btn->Bind(wxEVT_BUTTON, [this, scrolled_sizer](wxCommandEvent &e) { + if (p->combos_filament.size() <= 1) + return; + + size_t filament_count = p->combos_filament.size() - 1; + if (wxGetApp().preset_bundle->is_the_only_edited_filament(filament_count) || (filament_count == 1)) { + wxGetApp().get_tab(Preset::TYPE_FILAMENT)->select_preset(wxGetApp().preset_bundle->filament_presets[0], false, "", true); + } + + if (p->editing_filament >= filament_count) { + p->editing_filament = -1; + } + + wxGetApp().preset_bundle->set_num_filaments(filament_count); + wxGetApp().plater()->on_filaments_change(filament_count); + wxGetApp().get_tab(Preset::TYPE_PRINT)->update(); + wxGetApp().preset_bundle->export_selections(*wxGetApp().app_config); + }); + p->m_bpButton_del_filament = del_btn; + + bSizer39->Add(del_btn, 0, wxALIGN_CENTER | wxLEFT, FromDIP(SidebarProps::IconSpacing())); + bSizer39->Add(add_btn, 0, wxALIGN_CENTER | wxLEFT, FromDIP(SidebarProps::IconSpacing())); // ORCA Moved add button after delete button to prevent add button position change when remove icon automatically hidden + bSizer39->AddSpacer(FromDIP(20)); + + if (p->combos_filament.size() <= 1) { // ORCA Fix Flushing button and Delete filament button not hidden on launch while only 1 filament exist + bSizer39->Hide(p->m_flushing_volume_btn); + bSizer39->Hide(p->m_bpButton_del_filament); // ORCA: Hide delete filament button if there is only one filament + } + ams_btn = new ScalableButton(p->m_panel_filament_title, wxID_ANY, "ams_fila_sync", wxEmptyString, wxDefaultSize, wxDefaultPosition, wxBU_EXACTFIT | wxNO_BORDER, false, 16); // ORCA match icon size with other icons as 16x16 ams_btn->SetToolTip(_L("Synchronize filament list from AMS")); @@ -2109,27 +1580,17 @@ Sidebar::Sidebar(Plater *parent) //wxBoxSizer* bSizer_filament_content; //bSizer_filament_content = new wxBoxSizer( wxHORIZONTAL ); - // BBS: filament double columns, wrapped in scrolled window (max 3 rows) + // BBS: filament double columns p->sizer_filaments = new wxBoxSizer(wxHORIZONTAL); p->sizer_filaments->Add(new wxBoxSizer(wxVERTICAL), 1, wxEXPAND); p->sizer_filaments->Add(new wxBoxSizer(wxVERTICAL), 1, wxEXPAND); - p->m_scrolled_filaments = new wxScrolledWindow(p->m_panel_filament_content, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL); - p->m_scrolled_filaments->SetScrollRate(0, 5); - p->m_scrolled_filaments->SetBackgroundColour(*wxWHITE); - p->m_panel_scrolled_filament_content = new wxPanel(p->m_scrolled_filaments, wxID_ANY); - p->m_panel_scrolled_filament_content->SetBackgroundColour(*wxWHITE); - p->m_panel_scrolled_filament_content->SetSizer(p->sizer_filaments); - auto* scrolled_fila_sizer = new wxBoxSizer(wxVERTICAL); - scrolled_fila_sizer->Add(p->m_panel_scrolled_filament_content, 0, wxEXPAND); - p->m_scrolled_filaments->SetSizer(scrolled_fila_sizer); - p->combos_filament.push_back(nullptr); /* first filament item */ // init_filament_combo(&p->combos_filament[0], 0); - p->combos_filament[0] = new PlaterPresetComboBox(p->m_panel_scrolled_filament_content, Preset::TYPE_FILAMENT); + p->combos_filament[0] = new PlaterPresetComboBox(p->m_panel_filament_content, Preset::TYPE_FILAMENT); auto combo_and_btn_sizer = new wxBoxSizer(wxHORIZONTAL); // BBS: filament double columns combo_and_btn_sizer->AddSpacer(FromDIP(SidebarProps::ContentMargin())); @@ -2143,7 +1604,7 @@ Sidebar::Sidebar(Plater *parent) edit_btn->SetBackgroundColour(wxColour(255, 255, 255)); edit_btn->SetToolTip(_L("Click to edit preset"));*/ - ScalableButton* edit_btn = new ScalableButton(p->m_panel_scrolled_filament_content, wxID_ANY, "menu_filament"); + ScalableButton* edit_btn = new ScalableButton(p->m_panel_filament_content, wxID_ANY, "menu_filament"); edit_btn->SetToolTip(_L("Click to edit preset")); PlaterPresetComboBox* combobox = p->combos_filament[0]; @@ -2165,250 +1626,14 @@ Sidebar::Sidebar(Plater *parent) //bSizer_filament_content->Add(p->sizer_filaments, 1, wxALIGN_CENTER | wxALL); wxSizer *sizer_filaments2 = new wxBoxSizer(wxVERTICAL); - // --- Filaments title bar (same level as Color Mix title) --- - p->m_panel_physical_filaments_title = new StaticBox(p->m_panel_filament_content, wxID_ANY, wxDefaultPosition, - wxDefaultSize, wxTAB_TRAVERSAL | wxBORDER_NONE); - p->m_panel_physical_filaments_title->SetBackgroundColor(title_bg); - p->m_panel_physical_filaments_title->SetBackgroundColor2(0xF1F1F1); - p->m_panel_physical_filaments_title->SetMinSize(wxSize(-1, FromDIP(30))); - p->m_panel_physical_filaments_title->SetMaxSize(wxSize(-1, FromDIP(30))); - p->m_filament_icon = new ScalableButton(p->m_panel_physical_filaments_title, wxID_ANY, "filament"); - auto* physical_label = new Label(p->m_panel_physical_filaments_title, _L("Filaments"), LB_PROPAGATE_MOUSE_EVENT); - auto* h_physical_title = new wxBoxSizer(wxHORIZONTAL); - auto* white_left_f = new wxPanel(p->m_panel_physical_filaments_title, wxID_ANY, wxDefaultPosition, wxSize(FromDIP(SidebarProps::ContentMargin()), -1)); - white_left_f->SetBackgroundColour(*wxWHITE); - h_physical_title->Add(white_left_f, 0, wxEXPAND | wxTOP | wxBOTTOM, 0); - h_physical_title->Add(p->m_filament_icon, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(SidebarProps::TitlebarMargin())); - h_physical_title->AddSpacer(FromDIP(SidebarProps::ElementSpacing())); - h_physical_title->Add(physical_label, 0, wxALIGN_CENTER_VERTICAL); - h_physical_title->AddStretchSpacer(); - - // Delete filament button — delegates to delete_filament for consistent remap behavior - ScalableButton* del_btn = new ScalableButton(p->m_panel_physical_filaments_title, wxID_ANY, "delete_filament"); - del_btn->SetToolTip(_L("Remove last filament")); - del_btn->Bind(wxEVT_BUTTON, [this](wxCommandEvent &e) { - if (p->combos_filament.size() <= 1) - return; - delete_filament(size_t(-1), -1); - }); - p->m_bpButton_del_filament = del_btn; - - // Add filament button - ScalableButton* add_btn = new ScalableButton(p->m_panel_physical_filaments_title, wxID_ANY, "add_filament"); - add_btn->SetToolTip(_L("Add one filament")); - add_btn->Bind(wxEVT_BUTTON, [this](wxCommandEvent& e){ - if (p->combos_filament.size() >= MAXIMUM_EXTRUDER_NUMBER) - return; - PresetBundle* pb = wxGetApp().preset_bundle; - if (!pb || pb->mixed_filaments.total_filaments(p->combos_filament.size()) >= MAXIMUM_FILAMENT_NUMBER) - return; - int filament_count = p->combos_filament.size() + 1; - wxGetApp().plater()->confirm_auto_generated_gradients(filament_count); - wxColour new_col = Plater::get_next_color_for_filament(); - std::string new_color = new_col.GetAsString(wxC2S_HTML_SYNTAX).ToStdString(); - pb->set_num_filaments(filament_count, new_color); - wxGetApp().plater()->on_filaments_change(filament_count); - wxGetApp().get_tab(Preset::TYPE_PRINT)->update(); - pb->export_selections(*wxGetApp().app_config); - auto_calc_flushing_volumes(filament_count - 1); - }); - p->m_bpButton_add_filament = add_btn; - - h_physical_title->Add(del_btn, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(4)); - h_physical_title->Add(add_btn, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(8)); - auto* white_right_f = new wxPanel(p->m_panel_physical_filaments_title, wxID_ANY, wxDefaultPosition, wxSize(FromDIP(SidebarProps::ContentMargin()), -1)); - white_right_f->SetBackgroundColour(*wxWHITE); - h_physical_title->Add(white_right_f, 0, wxEXPAND | wxTOP | wxBOTTOM, 0); - p->m_panel_physical_filaments_title->SetSizer(h_physical_title); - p->m_panel_physical_filaments_title->Layout(); - - if (p->combos_filament.size() <= 1) - h_physical_title->Hide(p->m_bpButton_del_filament); - - sizer_filaments2->AddSpacer(FromDIP(8)); - sizer_filaments2->Add(p->m_panel_physical_filaments_title, 0, wxEXPAND, 0); - sizer_filaments2->AddSpacer(FromDIP(8)); - sizer_filaments2->Add(p->m_scrolled_filaments, 0, wxEXPAND, 0); - sizer_filaments2->AddSpacer(FromDIP(8)); - // --- Color Mix Panel (inside filament content, same level as filaments) --- - init_color_mix_panel(p->m_panel_filament_content, sizer_filaments2); + sizer_filaments2->AddSpacer(FromDIP(16)); + sizer_filaments2->Add(p->sizer_filaments, 0, wxEXPAND, 0); + sizer_filaments2->AddSpacer(FromDIP(16)); p->m_panel_filament_content->SetSizer(sizer_filaments2); p->m_panel_filament_content->Layout(); scrolled_sizer->Add(p->m_panel_filament_content, 0, wxEXPAND, 0); } - // --- Mixed Filaments Panel (Collapsible) --- - { - // Create title bar (StaticBox for collapsible header) - p->m_panel_mixed_filaments_title = new StaticBox(p->scrolled, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL | wxBORDER_NONE); - p->m_panel_mixed_filaments_title->SetBackgroundColor(title_bg); - p->m_panel_mixed_filaments_title->SetBackgroundColor2(0xF1F1F1); - - // Create icon - p->m_mixed_filaments_icon = new ScalableButton(p->m_panel_mixed_filaments_title, wxID_ANY, "filament"); - - // Create title text - p->m_staticText_mixed_filaments = new Label(p->m_panel_mixed_filaments_title, _L("Mixed Filaments"), LB_PROPAGATE_MOUSE_EVENT); - - // Create "Add Gradient" button - p->m_btn_add_gradient = new Button(p->m_panel_mixed_filaments_title, _L("Add Gradient")); - p->m_btn_add_gradient->SetStyle(ButtonStyle::Confirm, ButtonType::Compact); - p->m_btn_add_gradient->Bind(wxEVT_BUTTON, [this](wxCommandEvent& e) { - // Add gradient mixed filament - if (wxGetApp().preset_bundle) { - auto &mgr = wxGetApp().preset_bundle->mixed_filaments; - // Get physical filament colors - ConfigOptionStrings *co = wxGetApp().preset_bundle->project_config.option("filament_colour"); - std::vector colors = co ? co->values : std::vector(); - // Add a custom gradient (50% mix) - mgr.add_custom_filament(1, 2, 50, colors); - // Persist the custom entries so they survive the clear/load cycle in update_mixed_filament_panel - if (ConfigOptionString *opt = wxGetApp().preset_bundle->project_config.option("mixed_filament_definitions")) - opt->value = mgr.serialize_custom_entries(); - update_mixed_filament_panel(false); - m_scrolled_sizer->Layout(); - } - }); - - // Create "Add Pattern" button - p->m_btn_add_pattern = new Button(p->m_panel_mixed_filaments_title, _L("Add Pattern")); - p->m_btn_add_pattern->SetStyle(ButtonStyle::Confirm, ButtonType::Compact); - p->m_btn_add_pattern->Bind(wxEVT_BUTTON, [this](wxCommandEvent& e) { - // Add pattern mixed filament - if (wxGetApp().preset_bundle) { - auto &mgr = wxGetApp().preset_bundle->mixed_filaments; - // Get physical filament colors - ConfigOptionStrings *co = wxGetApp().preset_bundle->project_config.option("filament_colour"); - std::vector colors = co ? co->values : std::vector(); - // Add a custom pattern filament (will be configured by user) - mgr.add_custom_filament(1, 2, 50, colors); - // Set manual pattern for the newly added filament - auto &mfs = mgr.mixed_filaments(); - if (!mfs.empty()) { - mfs.back().manual_pattern = "12"; - mfs.back().custom = true; - } - // Persist the custom entries so they survive the clear/load cycle in update_mixed_filament_panel - if (ConfigOptionString *opt = wxGetApp().preset_bundle->project_config.option("mixed_filament_definitions")) - opt->value = mgr.serialize_custom_entries(); - update_mixed_filament_panel(false); - m_scrolled_sizer->Layout(); - } - }); - - // Create "Add Color" button - p->m_btn_add_color = new Button(p->m_panel_mixed_filaments_title, _L("Add Color")); - p->m_btn_add_color->SetStyle(ButtonStyle::Confirm, ButtonType::Compact); - p->m_btn_add_color->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { - if (wxGetApp().preset_bundle == nullptr) - return; - - ConfigOptionStrings *co = wxGetApp().preset_bundle->project_config.option("filament_colour"); - const std::vector colors = co ? co->values : std::vector(); - if (colors.size() < 2) - return; - - const MixedColorMatchRecipeResult recipe = - prompt_best_color_match_recipe(this, colors, Plater::get_next_color_for_filament()); - if (recipe.cancelled) - return; - if (!recipe.valid) { - show_error(this, _L("Unable to create a color match from the current physical filament colors.")); - return; - } - - const MixedFilamentDisplayContext display_context = build_mixed_filament_display_context(colors); - auto &mgr = wxGetApp().preset_bundle->mixed_filaments; - mgr.set_display_context(display_context); - mgr.add_custom_filament(recipe.component_a, recipe.component_b, recipe.mix_b_percent, colors); - auto &mfs = mgr.mixed_filaments(); - if (!mfs.empty()) { - MixedFilament &created = mfs.back(); - created.manual_pattern = recipe.manual_pattern; - created.mix_b_percent = recipe.mix_b_percent; - created.gradient_component_ids = recipe.gradient_component_ids; - created.gradient_component_weights = recipe.gradient_component_weights; - created.pointillism_all_filaments = false; - created.distribution_mode = recipe.gradient_component_ids.empty() ? int(MixedFilament::Simple) : int(MixedFilament::LayerCycle); - created.custom = true; - created.display_color = compute_color_match_recipe_display_color(recipe, display_context).GetAsString(wxC2S_HTML_SYNTAX).ToStdString(); - } - - if (ConfigOptionString *opt = wxGetApp().preset_bundle->project_config.option("mixed_filament_definitions")) - opt->value = mgr.serialize_custom_entries(); - update_mixed_filament_panel(false); - m_scrolled_sizer->Layout(); - }); - - // Create horizontal sizer for title bar - wxBoxSizer* h_sizer_mixed_title = new wxBoxSizer(wxHORIZONTAL); - h_sizer_mixed_title->Add(p->m_mixed_filaments_icon, 0, wxALIGN_CENTER | wxLEFT, FromDIP(SidebarProps::TitlebarMargin())); - h_sizer_mixed_title->AddSpacer(FromDIP(SidebarProps::ElementSpacing())); - h_sizer_mixed_title->Add(p->m_staticText_mixed_filaments, 0, wxALIGN_CENTER); - h_sizer_mixed_title->AddStretchSpacer(); - h_sizer_mixed_title->Add(p->m_btn_add_gradient, 0, wxALIGN_CENTER | wxRIGHT, FromDIP(SidebarProps::ElementSpacing())); - h_sizer_mixed_title->Add(p->m_btn_add_pattern, 0, wxALIGN_CENTER | wxRIGHT, FromDIP(SidebarProps::ElementSpacing())); - h_sizer_mixed_title->Add(p->m_btn_add_color, 0, wxALIGN_CENTER | wxRIGHT, FromDIP(SidebarProps::TitlebarMargin())); - h_sizer_mixed_title->SetMinSize(-1, FromDIP(30)); - - p->m_panel_mixed_filaments_title->SetSizer(h_sizer_mixed_title); - p->m_panel_mixed_filaments_title->Layout(); - - // Add splitter line before title - auto spliter_mixed_1 = new ::StaticLine(p->scrolled); - spliter_mixed_1->SetLineColour("#A6A9AA"); - scrolled_sizer->Add(spliter_mixed_1, 0, wxEXPAND); - - // Add title bar to scrolled sizer - scrolled_sizer->Add(p->m_panel_mixed_filaments_title, 0, wxEXPAND | wxALL, 0); - - // Add splitter line after title - auto spliter_mixed_2 = new ::StaticLine(p->scrolled); - spliter_mixed_2->SetLineColour("#CECECE"); - scrolled_sizer->Add(spliter_mixed_2, 0, wxEXPAND); - - // Create content panel (collapsible) - p->m_panel_mixed_filaments_content = new wxPanel(p->scrolled, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL); - { - const bool is_dark = wxGetApp().dark_mode(); - p->m_panel_mixed_filaments_content->SetBackgroundColour(is_dark ? wxColour(45, 45, 49) : wxColour(255, 255, 255)); - } - - // Content sizer - store in member variable for later use - p->m_sizer_mixed_filaments_content = new wxBoxSizer(wxVERTICAL); - p->m_sizer_mixed_filaments_content->AddSpacer(FromDIP(SidebarProps::ContentMargin())); - p->m_panel_mixed_filaments_content->SetSizer(p->m_sizer_mixed_filaments_content); - p->m_panel_mixed_filaments_content->Layout(); - - // Add content panel to scrolled sizer - scrolled_sizer->Add(p->m_panel_mixed_filaments_content, 0, wxEXPAND, 0); - - // Bind collapse/expand event to title bar - p->m_panel_mixed_filaments_title->Bind(wxEVT_LEFT_UP, [this](wxMouseEvent& e) { - // Exclude button areas from collapse/expand - int button_left = p->m_panel_mixed_filaments_title->GetClientSize().x; - auto consider_button = [&button_left](wxWindow *button) { - if (button && button->IsShown()) - button_left = std::min(button_left, button->GetPosition().x); - }; - consider_button(p->m_btn_add_gradient); - consider_button(p->m_btn_add_pattern); - consider_button(p->m_btn_add_color); - if (e.GetPosition().x > button_left - FromDIP(12)) - return; - - if (p->m_panel_mixed_filaments_content->GetMaxHeight() == 0) - p->m_panel_mixed_filaments_content->SetMaxSize({-1, -1}); - else - p->m_panel_mixed_filaments_content->SetMaxSize({-1, 0}); - m_scrolled_sizer->Layout(); - }); - - // Initially hidden until 2+ filaments - p->m_panel_mixed_filaments_title->Hide(); - p->m_panel_mixed_filaments_content->Hide(); - } - { //add project title auto params_panel = ((MainFrame*)parent->GetParent())->m_param_panel; @@ -2524,7 +1749,7 @@ void Sidebar::create_printer_preset() void Sidebar::init_filament_combo(PlaterPresetComboBox **combo, const int filament_idx) { - *combo = new PlaterPresetComboBox(p->m_panel_scrolled_filament_content, Slic3r::Preset::TYPE_FILAMENT); + *combo = new PlaterPresetComboBox(p->m_panel_filament_content, Slic3r::Preset::TYPE_FILAMENT); (*combo)->set_filament_idx(filament_idx); auto combo_and_btn_sizer = new wxBoxSizer(wxHORIZONTAL); @@ -2553,7 +1778,7 @@ void Sidebar::init_filament_combo(PlaterPresetComboBox **combo, const int filame combo_and_btn_sizer->Add(32 * em / 10, 0, 0, 0, 0); combo_and_btn_sizer->Add(del_btn, 0, wxALIGN_CENTER_VERTICAL, 5 * em / 10); */ - ScalableButton* edit_btn = new ScalableButton(p->m_panel_scrolled_filament_content, wxID_ANY, "menu_filament"); + ScalableButton* edit_btn = new ScalableButton(p->m_panel_filament_content, wxID_ANY, "menu_filament"); edit_btn->SetToolTip(_L("Click to edit preset")); PlaterPresetComboBox* combobox = (*combo); @@ -2716,13 +1941,11 @@ void Sidebar::update_all_preset_comboboxes(bool reload_printer_view) } if (cfg.opt_bool("pellet_modded_printer")) { - p->m_staticText_filament_settings->SetLabel(_L("Pellet Configuration")); + p->m_staticText_filament_settings->SetLabel(_L("Pellets")); p->m_filament_icon->SetBitmap_("pellets"); - p->m_filament_config_icon->SetBitmap_("filament_group"); } else { - p->m_staticText_filament_settings->SetLabel(_L("Filament Management")); + p->m_staticText_filament_settings->SetLabel(_L("Filament")); p->m_filament_icon->SetBitmap_("filament"); - p->m_filament_config_icon->SetBitmap_("filament_group"); } show_SEMM_buttons(/*cfg.opt_bool("single_extruder_multi_material")*/true); @@ -2868,6 +2091,10 @@ void Sidebar::update_presets(Preset::Type preset_type) p->combos_filament[i]->update(); update_dynamic_filament_list(); + + // Real-time high/low temperature filament mixing check after filament preset change + p->plater->check_filament_temp_mixing(); + break; } @@ -2996,7 +2223,6 @@ void Sidebar::msw_rescale() p->m_printerinfo_syncbtn->msw_rescale(); p->m_printer_setting->msw_rescale(); p->m_filament_icon->msw_rescale(); - p->m_color_mix_icon->msw_rescale(); p->m_bpButton_add_filament->msw_rescale(); p->m_bpButton_del_filament->msw_rescale(); p->m_bpButton_ams_filament->msw_rescale(); @@ -3069,7 +2295,6 @@ void Sidebar::sys_color_changed() p->m_printerinfo_syncbtn->msw_rescale(); p->m_printer_setting->msw_rescale(); p->m_filament_icon->msw_rescale(); - p->m_color_mix_icon->msw_rescale(); p->m_bpButton_add_filament->msw_rescale(); p->m_bpButton_del_filament->msw_rescale(); p->m_bpButton_ams_filament->msw_rescale(); @@ -3145,31 +2370,8 @@ void Sidebar::on_filaments_change(size_t num_filaments) { auto& choices = combos_filament(); - if (num_filaments == choices.size()) { - // Project load may keep the same physical filament count while mixed - // definitions changed. Refresh mixed panel even without count changes. - const bool sync_manager = !p->m_skip_mixed_filament_sync_once; - p->m_skip_mixed_filament_sync_once = false; - update_ui_from_settings(); - update_dynamic_filament_list(); - update_mixed_filament_panel(sync_manager); - - // Recalc scrolled filament window height (max 3 rows, matches color mix) - if (p->m_scrolled_filaments && p->m_panel_scrolled_filament_content) { - p->m_panel_scrolled_filament_content->Layout(); - const wxSize content_best = p->m_panel_scrolled_filament_content->GetBestSize(); - const int row_count = ((int)num_filaments + 1) / 2; // 2-column grid - const int desired_h = row_count > 3 - ? (content_best.GetHeight() * 3) / std::max(1, row_count) - : content_best.GetHeight(); - p->m_scrolled_filaments->SetMinSize({-1, desired_h}); - p->m_scrolled_filaments->SetMaxSize({-1, desired_h}); - } - Layout(); + if (num_filaments == choices.size()) return; - } - - p->m_skip_mixed_filament_sync_once = false; if (choices.size() == 1 || num_filaments == 1) choices[0]->GetDropDown().Invalidate(); @@ -3187,4010 +2389,31 @@ void Sidebar::on_filaments_change(size_t num_filaments) // initialize selection choice->update(); choice->SetSelection(last_selection); - ++i; - } - - // remove unused choices if any - remove_unused_filament_combos(num_filaments); - - auto sizer = p->m_panel_filament_title->GetSizer(); - if (p->m_flushing_volume_btn != nullptr && sizer != nullptr) { - if (num_filaments > 1) { - sizer->Show(p->m_flushing_volume_btn); - } else { - sizer->Hide(p->m_flushing_volume_btn); - } - } - if (p->m_bpButton_del_filament != nullptr && p->m_panel_physical_filaments_title != nullptr) { - auto* inner_sizer = p->m_panel_physical_filaments_title->GetSizer(); - if (inner_sizer) { - if (num_filaments > 1) - inner_sizer->Show(p->m_bpButton_del_filament); - else - inner_sizer->Hide(p->m_bpButton_del_filament); - } - } - - // Recalc scrolled filament window height (max 3 rows, matches color mix) - if (p->m_scrolled_filaments && p->m_panel_scrolled_filament_content) { - p->m_panel_scrolled_filament_content->Layout(); - const wxSize content_best = p->m_panel_scrolled_filament_content->GetBestSize(); - const int row_count = ((int)num_filaments + 1) / 2; // 2-column grid - const int desired_h = row_count > 3 - ? (content_best.GetHeight() / std::max(1, row_count)) * 3 - : content_best.GetHeight(); - p->m_scrolled_filaments->SetMinSize({-1, desired_h}); - p->m_scrolled_filaments->SetMaxSize({-1, desired_h}); - } - - Layout(); - wxWeakRef weak_this(this); - wxTheApp->CallAfter([weak_this]() { - Sidebar* sidebar = weak_this.get(); - if (sidebar && sidebar->p && sidebar->p->m_scrolled_filaments) { - int vh = sidebar->p->m_scrolled_filaments->GetVirtualSize().y; - int ch = sidebar->p->m_scrolled_filaments->GetClientSize().y; - sidebar->p->m_scrolled_filaments->Scroll(0, std::max(0, vh - ch)); - } - }); - p->m_panel_filament_title->Refresh(); - update_ui_from_settings(); - update_dynamic_filament_list(); - update_mixed_filament_panel(); - update_color_mix_panel(); - - // Disable add buttons when combined filament limit reached - if (PresetBundle *pb = wxGetApp().preset_bundle) { - const bool can_add = pb->mixed_filaments.total_filaments(combos_filament().size()) < MAXIMUM_FILAMENT_NUMBER; - if (p->m_bpButton_add_filament) - p->m_bpButton_add_filament->Enable(can_add); - if (p->m_btn_add_color_mix) - p->m_btn_add_color_mix->Enable(can_add); - } -} - - -class MixedGradientWeightsDialog : public wxDialog -{ -public: - MixedGradientWeightsDialog(wxWindow *parent, - const std::vector &filament_ids, - const std::vector &palette, - const std::vector &initial_weights) - : wxDialog(parent, wxID_ANY, _L("Gradient Mix Weights"), wxDefaultPosition, wxDefaultSize, - wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER) - { - m_colors.reserve(filament_ids.size()); - m_weights = normalize_color_match_weights(initial_weights, filament_ids.size()); - for (const unsigned int filament_id : filament_ids) { - if (filament_id >= 1 && filament_id <= palette.size()) - m_colors.emplace_back(palette[filament_id - 1]); - else - m_colors.emplace_back(wxColour("#26A69A")); - } - if (m_colors.empty()) - m_colors.emplace_back(wxColour("#26A69A")); - - auto *root = new wxBoxSizer(wxVERTICAL); - auto *hint = new wxStaticText(this, wxID_ANY, _L("Pick a point in the gradient map to control multi-filament mix.")); - root->Add(hint, 0, wxEXPAND | wxALL, FromDIP(10)); - - m_color_map = new MixedFilamentColorMapPanel(this, filament_ids, palette, initial_weights, - wxSize(FromDIP(240), FromDIP(240))); - root->Add(m_color_map, 1, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, FromDIP(10)); - - for (size_t i = 0; i < filament_ids.size(); ++i) { - auto *row = new wxBoxSizer(wxHORIZONTAL); - wxPanel *chip = new wxPanel(this, wxID_ANY, wxDefaultPosition, wxSize(FromDIP(18), FromDIP(18)), wxBORDER_SIMPLE); - chip->SetBackgroundColour(m_colors[i]); - row->Add(chip, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(6)); - row->Add(new wxStaticText(this, wxID_ANY, wxString::Format("F%d", int(filament_ids[i]))), - 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(8)); - auto *label = new wxStaticText(this, wxID_ANY, wxString::Format("%d%%", m_weights[i])); - label->SetFont(Label::Body_12); - row->Add(label, 0, wxALIGN_CENTER_VERTICAL); - root->Add(row, 0, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, FromDIP(8)); - m_weight_labels.emplace_back(label); - } - - root->Add(CreateSeparatedButtonSizer(wxOK | wxCANCEL), 0, wxEXPAND | wxALL, FromDIP(8)); - SetSizerAndFit(root); - SetMinSize(wxSize(FromDIP(380), std::max(GetSize().GetHeight(), FromDIP(460)))); - update_weight_labels(); - - Bind(wxEVT_CLOSE_WINDOW, [this](wxCloseEvent&) { EndModal(wxID_CANCEL); }); - - if (m_color_map) { - m_color_map->Bind(wxEVT_SLIDER, [this](wxCommandEvent &) { - m_weights = m_color_map ? m_color_map->normalized_weights() : m_weights; - update_weight_labels(); - }); - } - } - - std::vector normalized_weights() const - { - return m_color_map ? m_color_map->normalized_weights() : m_weights; - } - -private: - void update_weight_labels() - { - for (size_t i = 0; i < m_weight_labels.size() && i < m_weights.size(); ++i) { - if (m_weight_labels[i]) - m_weight_labels[i]->SetLabel(wxString::Format("%d%%", m_weights[i])); - } - Layout(); - } - -private: - MixedFilamentColorMapPanel *m_color_map { nullptr }; - std::vector m_colors; - std::vector m_weights; - std::vector m_weight_labels; -}; - -// Forward declaration for MixedMixPreview (defined below) -class MixedMixPreview; - -// Inline editor panel for configuring a single mixed filament -class MixedFilamentConfigPanel : public wxPanel -{ -public: - using OnChangeFn = std::function; - - MixedFilamentConfigPanel(wxWindow *parent, - size_t mixed_id, - const MixedFilament &mf, - size_t num_physical, - const std::vector &physical_colors, - const std::vector &nozzle_diameters, - const std::vector &palette, - const MixedFilamentPreviewSettings &preview_settings, - bool bias_mode_enabled, - OnChangeFn on_change = {}); - - // Get the updated mixed filament data - MixedFilament get_mixed_filament() const { return m_mf; } - bool has_changes() const { return m_has_changes; } - static int effective_local_z_preview_mix_b_percent(const MixedFilament &mf, - const MixedFilamentPreviewSettings &preview_settings); - -private: - void build_ui(); - void update_preview(); - void update_local_z_breakdown(); - void update_component_picker_visuals(); - - size_t m_mixed_id; - MixedFilament m_mf; - size_t m_num_physical; - std::vector m_physical_colors; - std::vector m_nozzle_diameters; - std::vector m_palette; - MixedFilamentPreviewSettings m_preview_settings; - bool m_bias_mode_enabled = false; - bool m_has_changes = false; - - wxChoice *m_choice_a = nullptr; - wxChoice *m_choice_b = nullptr; - wxChoice *m_choice_c = nullptr; - wxChoice *m_choice_d = nullptr; - wxPanel *m_picker_a_container = nullptr; - wxPanel *m_picker_b_container = nullptr; - wxPanel *m_picker_c_container = nullptr; - wxPanel *m_picker_d_container = nullptr; - wxPanel *m_picker_a_swatch = nullptr; - wxPanel *m_picker_b_swatch = nullptr; - wxPanel *m_picker_c_swatch = nullptr; - wxPanel *m_picker_d_swatch = nullptr; - wxStaticText *m_picker_a_label = nullptr; - wxStaticText *m_picker_b_label = nullptr; - wxStaticText *m_picker_c_label = nullptr; - wxStaticText *m_picker_d_label = nullptr; - wxPanel *m_surface_offset_target_container = nullptr; - wxPanel *m_surface_offset_target_swatch = nullptr; - wxStaticText *m_surface_offset_target_label = nullptr; - MixedGradientSelector *m_blend_selector = nullptr; - wxStaticText *m_blend_label = nullptr; - wxTextCtrl *m_pattern_ctrl = nullptr; - wxCheckBox *m_local_z_limit_checkbox = nullptr; - wxSpinCtrl *m_local_z_limit_spin = nullptr; - wxSpinCtrlDouble *m_surface_offset_spin = nullptr; - std::vector m_pattern_quick_buttons; - MixedMixPreview *m_mix_preview = nullptr; - wxStaticText *m_breakdown_label = nullptr; - wxPanel *m_swatch = nullptr; - std::shared_ptr> m_selected_weight_state; - OnChangeFn m_on_change; - - // Helper functions (copied from update_mixed_filament_panel) - static std::vector decode_gradient_ids(const std::string &s); - static std::string encode_gradient_ids(const std::vector &ids); - static std::vector decode_manual_pattern_ids(const std::string &pattern, - unsigned int a, - unsigned int b, - size_t num_physical, - size_t wall_loops = 0); - static std::vector decode_gradient_weights(const std::string &s, size_t n); - static std::vector normalize_gradient_weights(const std::vector &w, size_t n); - static std::string encode_gradient_weights(const std::vector &w); - static std::vector build_weighted_pair_sequence(unsigned int a, unsigned int b, int percent_b, bool limit_cycle = false); - static std::vector build_weighted_multi_sequence(const std::vector &ids, - const std::vector &weights, - size_t max_cycle_limit = 0); - static std::string summarize_sequence(const std::vector &seq); - static std::string summarize_local_z_breakdown(const MixedFilament &mf, - const std::vector &weights, - const MixedFilamentPreviewSettings &preview_settings); - static std::string blend_from_sequence(const std::vector &colors, const std::vector &seq, const std::string &fallback); - static std::vector build_local_z_preview_pass_heights(double nominal_layer_height, - double lower_bound, - double upper_bound, - double preferred_a_height, - double preferred_b_height, - int mix_b_percent, - int max_sublayers_limit); -}; - -class MixedMixPreview : public wxPanel -{ -public: - explicit MixedMixPreview(wxWindow *parent) - : wxPanel(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxBORDER_NONE) - { - SetBackgroundStyle(wxBG_STYLE_PAINT); - SetMinSize(wxSize(FromDIP(120), FromDIP(20))); - Bind(wxEVT_PAINT, &MixedMixPreview::on_paint, this); - } - - void set_data(const std::vector &palette, - const std::vector &sequence, - bool same_layer_mode, - const std::vector &surface_offsets_mm, - const wxColour &fallback, - const wxString &left_overlay, - const wxString &right_overlay) - { - m_palette = palette; - m_sequence = sequence; - m_same_layer = same_layer_mode; - m_surface_offsets_mm = surface_offsets_mm; - m_fallback = fallback; - m_left_overlay = left_overlay; - m_right_overlay = right_overlay; - Refresh(); - } - -private: - wxRect preview_rect() const - { - const int margin_x = FromDIP(1); - const int margin_y = FromDIP(1); - const wxSize sz = GetClientSize(); - return wxRect(margin_x, margin_y, std::max(1, sz.GetWidth() - margin_x * 2), std::max(1, sz.GetHeight() - margin_y * 2)); - } - - wxColour color_for_extruder(unsigned int extruder_id) const - { - if (extruder_id >= 1 && extruder_id <= m_palette.size()) - return m_palette[extruder_id - 1]; - return m_fallback; - } - - double max_active_surface_offset_mm() const - { - double max_offset = 0.0; - for (double offset_mm : m_surface_offsets_mm) - max_offset = std::max(max_offset, std::abs(offset_mm)); - return std::max(0.001, max_offset); - } - - int slot_inset_for_extruder(unsigned int extruder_id, int slot_extent) const - { - if (extruder_id == 0 || extruder_id >= m_surface_offsets_mm.size() || slot_extent <= 2) - return 0; - - const double offset_mm = m_surface_offsets_mm[extruder_id]; - if (std::abs(offset_mm) <= EPSILON) - return 0; - - const double normalized = std::clamp(std::abs(offset_mm) / max_active_surface_offset_mm(), 0.0, 1.0); - const int inset = int(std::round(normalized * slot_extent * 0.45)) * (offset_mm < 0.0 ? -1 : 1); - return std::clamp(inset, -std::max(0, slot_extent / 2), std::max(0, slot_extent / 2)); - } - - void on_paint(wxPaintEvent &) - { - wxAutoBufferedPaintDC dc(this); - dc.SetBackground(wxBrush(GetBackgroundColour())); - dc.Clear(); - - const wxRect rect = preview_rect(); - dc.SetPen(*wxTRANSPARENT_PEN); - dc.SetBrush(wxBrush(m_fallback)); - dc.DrawRectangle(rect); - - if (!m_sequence.empty()) { - if (m_same_layer) { - // Same-layer preview: full-height stripe lines. - const int stripes = 24; - const int stripe_w = std::max(1, rect.GetWidth() / stripes); - const size_t seq_len = m_sequence.size(); - for (int s = 0; s < stripes; ++s) { - const size_t idx = size_t(s % int(seq_len)); - const unsigned int extruder_id = m_sequence[idx]; - dc.SetBrush(wxBrush(color_for_extruder(m_sequence[idx]))); - const int x = rect.GetLeft() + s * stripe_w; - const int w = (s == stripes - 1) ? (rect.GetRight() - x + 1) : stripe_w; - const int inset = slot_inset_for_extruder(extruder_id, w); - wxRect draw_rect(x + inset / 2, rect.GetTop(), std::max(1, w - inset), rect.GetHeight()); - draw_rect.Intersect(rect); - if (draw_rect.GetWidth() > 0) - dc.DrawRectangle(draw_rect); - } - } else { - const int bars = 24; - const int bar_w = std::max(1, rect.GetWidth() / bars); - for (int i = 0; i < bars; ++i) { - size_t idx = 0; - if (m_sequence.size() > size_t(bars)) - idx = (size_t(i) * m_sequence.size()) / size_t(bars); - else - idx = size_t(i) % m_sequence.size(); - const unsigned int extruder_id = m_sequence[idx]; - dc.SetBrush(wxBrush(color_for_extruder(extruder_id))); - const int x = rect.GetLeft() + i * bar_w; - const int w = (i == bars - 1) ? (rect.GetRight() - x + 1) : bar_w; - const int inset = slot_inset_for_extruder(extruder_id, w); - wxRect draw_rect(x + inset / 2, rect.GetTop(), std::max(1, w - inset), rect.GetHeight()); - draw_rect.Intersect(rect); - if (draw_rect.GetWidth() > 0) - dc.DrawRectangle(draw_rect); - } - } - } - - auto draw_outlined_text = [this, &dc](const wxString &text, int x, int y) { - if (text.empty()) - return; - dc.SetTextForeground(wxColour(255, 255, 255)); - const int outline_radius = std::max(2, FromDIP(2)); - for (int ox = -outline_radius; ox <= outline_radius; ++ox) { - for (int oy = -outline_radius; oy <= outline_radius; ++oy) { - if (ox == 0 && oy == 0) - continue; - dc.DrawText(text, x + ox, y + oy); - } - } - dc.SetTextForeground(wxColour(22, 22, 22)); - dc.DrawText(text, x, y); - }; - - wxCoord left_w = 0, left_h = 0; - wxCoord right_w = 0, right_h = 0; - dc.GetTextExtent(m_left_overlay, &left_w, &left_h); - dc.GetTextExtent(m_right_overlay, &right_w, &right_h); - const int text_y = rect.GetTop() + std::max(0, (rect.GetHeight() - int(std::max(left_h, right_h))) / 2); - const int pad = FromDIP(6); - if (!m_left_overlay.empty()) - draw_outlined_text(m_left_overlay, rect.GetLeft() + pad, text_y); - if (!m_right_overlay.empty()) - draw_outlined_text(m_right_overlay, rect.GetRight() - pad - int(right_w), text_y); - - const bool is_dark = wxGetApp().dark_mode(); - dc.SetPen(wxPen(is_dark ? wxColour(110, 110, 110) : wxColour(170, 170, 170), 1)); - dc.SetBrush(*wxTRANSPARENT_BRUSH); - dc.DrawRectangle(rect); - } - -private: - std::vector m_palette; - std::vector m_sequence; - std::vector m_surface_offsets_mm; - bool m_same_layer { false }; - wxColour m_fallback { wxColour(38, 166, 154) }; - wxString m_left_overlay; - wxString m_right_overlay; -}; - -// Implementation of MixedFilamentConfigPanel helper functions -static std::vector build_grouped_manual_pattern_preview_sequence(const std::string &pattern, - unsigned int component_a, - unsigned int component_b, - size_t num_physical, - size_t wall_loops) -{ - std::vector sequence; - if (num_physical == 0) - return sequence; - - const std::string normalized = MixedFilamentManager::normalize_manual_pattern(pattern); - if (normalized.empty()) - return sequence; - - const std::vector groups = MixedFilamentManager::split_pattern_groups(normalized); - if (groups.empty()) - return sequence; - - MixedFilament dummy_mf; - dummy_mf.component_a = component_a; - dummy_mf.component_b = component_b; - - if (groups.size() == 1) { - const std::vector tokens = - MixedFilamentManager::split_pattern_group_to_tokens(groups[0], num_physical); - sequence.reserve(tokens.size()); - for (const std::string &token : tokens) { - const unsigned int extruder_id = - MixedFilamentManager::physical_filament_from_token(token, dummy_mf, num_physical); - if (extruder_id != 0) - sequence.emplace_back(extruder_id); - } - return sequence; - } - - std::vector> group_tokens; - group_tokens.reserve(groups.size()); - for (const std::string &group : groups) - group_tokens.push_back(MixedFilamentManager::split_pattern_group_to_tokens(group, num_physical)); - - constexpr size_t k_max_preview_cycle = 48; - size_t cycle = 1; - for (const auto &tokens : group_tokens) { - if (tokens.empty()) - continue; - cycle = std::lcm(cycle, tokens.size()); - if (cycle >= k_max_preview_cycle) { - cycle = k_max_preview_cycle; - break; - } - } - - const size_t preview_wall_loops = std::max(1, wall_loops == 0 ? groups.size() : wall_loops); - sequence.reserve(preview_wall_loops * cycle); - for (size_t layer_idx = 0; layer_idx < cycle; ++layer_idx) { - for (size_t wall_idx = 0; wall_idx < preview_wall_loops; ++wall_idx) { - const auto &tokens = group_tokens[std::min(wall_idx, group_tokens.size() - 1)]; - if (tokens.empty()) - continue; - const std::string &token = tokens[layer_idx % tokens.size()]; - const unsigned int extruder_id = - MixedFilamentManager::physical_filament_from_token(token, dummy_mf, num_physical); - if (extruder_id != 0) - sequence.emplace_back(extruder_id); - } - } - - return sequence; -} - -std::vector MixedFilamentConfigPanel::decode_gradient_ids(const std::string &s) -{ - return MixedFilamentManager::decode_gradient_component_ids(s, 0); -} - -std::string MixedFilamentConfigPanel::encode_gradient_ids(const std::vector &ids) -{ - return MixedFilamentManager::encode_gradient_component_ids(ids); -} - -std::vector MixedFilamentConfigPanel::decode_manual_pattern_ids(const std::string &pattern, - unsigned int a, - unsigned int b, - size_t num_physical, - size_t wall_loops) -{ - return build_grouped_manual_pattern_preview_sequence(pattern, a, b, num_physical, wall_loops); -} - -std::vector MixedFilamentConfigPanel::decode_gradient_weights(const std::string &s, size_t n) -{ - std::vector w; - if (s.empty() || n == 0) - return w; - - std::string token; - for (const char c : s) { - if (c >= '0' && c <= '9') { - token.push_back(c); - continue; - } - if (!token.empty()) { - w.emplace_back(std::max(0, std::atoi(token.c_str()))); - token.clear(); - } - } - if (!token.empty()) - w.emplace_back(std::max(0, std::atoi(token.c_str()))); - if (w.size() != n) - w.clear(); - return w; -} - -std::vector MixedFilamentConfigPanel::normalize_gradient_weights(const std::vector &w, size_t n) -{ - std::vector out = w; - if (out.size() != n) out.assign(n, n > 0 ? int(100 / n) : 0); - int sum = 0; - for (int &v : out) { v = std::max(0, v); sum += v; } - if (sum <= 0 && n > 0) { out.assign(n, 0); out[0] = 100; return out; } - std::vector rem(n, 0.); - int assigned = 0; - for (size_t i = 0; i < n; ++i) { - const double exact = 100.0 * double(out[i]) / double(sum); - out[i] = int(std::floor(exact)); - rem[i] = exact - double(out[i]); - assigned += out[i]; - } - int missing = std::max(0, 100 - assigned); - while (missing > 0) { - size_t best = 0; - double best_rem = -1.0; - for (size_t i = 0; i < rem.size(); ++i) { - if (rem[i] > best_rem) { best_rem = rem[i]; best = i; } - } - ++out[best]; - rem[best] = 0.0; - --missing; - } - return out; -} - -std::string MixedFilamentConfigPanel::encode_gradient_weights(const std::vector &w) -{ - std::ostringstream out; - for (size_t i = 0; i < w.size(); ++i) { - if (i > 0) - out << '/'; - out << std::max(0, w[i]); - } - return out.str(); -} - -namespace { - -std::pair effective_pair_preview_ratios(int percent_b) -{ - const int mix_b = std::clamp(percent_b, 0, 100); - int ratio_a = 1; - int ratio_b = 0; - - if (mix_b >= 100) { - ratio_a = 0; - ratio_b = 1; - } else if (mix_b > 0) { - const int pct_b = mix_b; - const int pct_a = 100 - pct_b; - const bool b_is_major = pct_b >= pct_a; - const int major_pct = b_is_major ? pct_b : pct_a; - const int minor_pct = b_is_major ? pct_a : pct_b; - const int major_layers = - std::max(1, int(std::lround(double(major_pct) / double(std::max(1, minor_pct))))); - ratio_a = b_is_major ? 1 : major_layers; - ratio_b = b_is_major ? major_layers : 1; - } - - if (ratio_a > 0 && ratio_b > 0) { - const int g = std::gcd(ratio_a, ratio_b); - if (g > 1) { - ratio_a /= g; - ratio_b /= g; - } - } - - return { std::max(0, ratio_a), std::max(0, ratio_b) }; -} - -std::vector build_effective_pair_preview_sequence(unsigned int component_a, - unsigned int component_b, - int percent_b, - bool limit_cycle) -{ - std::vector sequence; - if (component_a == 0 || component_b == 0 || component_a == component_b) - return sequence; - - auto [ratio_a, ratio_b] = effective_pair_preview_ratios(percent_b); - constexpr int k_max_cycle = 24; - if (limit_cycle && ratio_a > 0 && ratio_b > 0 && ratio_a + ratio_b > k_max_cycle) { - const double scale = double(k_max_cycle) / double(ratio_a + ratio_b); - ratio_a = std::max(1, int(std::round(double(ratio_a) * scale))); - ratio_b = std::max(1, int(std::round(double(ratio_b) * scale))); - } - if (ratio_a == 0 && ratio_b == 0) - ratio_a = 1; - - const int cycle = std::max(1, ratio_a + ratio_b); - sequence.reserve(size_t(cycle)); - for (int pos = 0; pos < cycle; ++pos) { - const int b_before = (pos * ratio_b) / cycle; - const int b_after = ((pos + 1) * ratio_b) / cycle; - sequence.emplace_back((b_after > b_before) ? component_b : component_a); - } - return sequence; -} - -std::string format_preview_sequence_percent(int count, int total) -{ - if (count <= 0 || total <= 0) - return ""; - - const double percent = 100.0 * double(count) / double(total); - const double rounded_tenths = std::round(percent * 10.0) / 10.0; - const double nearest_integer = std::round(rounded_tenths); - if (std::abs(rounded_tenths - nearest_integer) < 1e-6) - return wxString::Format("%d%%", int(nearest_integer)).ToStdString(); - return wxString::Format("%.1f%%", rounded_tenths).ToStdString(); -} - -} // namespace - -std::vector MixedFilamentConfigPanel::build_weighted_pair_sequence(unsigned int a, - unsigned int b, - int percent_b, - bool limit_cycle) -{ - return build_effective_pair_preview_sequence(a, b, percent_b, limit_cycle); -} - -static void reduce_weight_counts_to_cycle_limit(std::vector &counts, size_t cycle_limit) -{ - if (counts.empty() || cycle_limit == 0) - return; - - int total = std::accumulate(counts.begin(), counts.end(), 0); - if (total <= 0 || size_t(total) <= cycle_limit) - return; - - std::vector positive_indices; - positive_indices.reserve(counts.size()); - for (size_t i = 0; i < counts.size(); ++i) - if (counts[i] > 0) - positive_indices.emplace_back(i); - - if (positive_indices.empty()) { - counts.assign(counts.size(), 0); - return; - } - - std::vector reduced(counts.size(), 0); - if (cycle_limit < positive_indices.size()) { - std::sort(positive_indices.begin(), positive_indices.end(), [&counts](size_t lhs, size_t rhs) { - if (counts[lhs] != counts[rhs]) - return counts[lhs] > counts[rhs]; - return lhs < rhs; - }); - for (size_t i = 0; i < cycle_limit; ++i) - reduced[positive_indices[i]] = 1; - counts = std::move(reduced); - return; - } - - size_t remaining_slots = cycle_limit; - for (const size_t idx : positive_indices) { - reduced[idx] = 1; - --remaining_slots; - } - - int total_extras = 0; - std::vector extra_counts(counts.size(), 0); - for (const size_t idx : positive_indices) { - extra_counts[idx] = std::max(0, counts[idx] - 1); - total_extras += extra_counts[idx]; - } - if (remaining_slots == 0 || total_extras <= 0) { - counts = std::move(reduced); - return; - } - - std::vector remainders(counts.size(), -1.0); - size_t assigned_slots = 0; - for (const size_t idx : positive_indices) { - if (extra_counts[idx] == 0) - continue; - const double exact = double(remaining_slots) * double(extra_counts[idx]) / double(total_extras); - const int assigned = int(std::floor(exact)); - reduced[idx] += assigned; - assigned_slots += size_t(assigned); - remainders[idx] = exact - double(assigned); - } - - size_t missing_slots = remaining_slots > assigned_slots ? (remaining_slots - assigned_slots) : size_t(0); - while (missing_slots > 0) { - size_t best_idx = size_t(-1); - double best_remainder = -1.0; - int best_extra = -1; - for (const size_t idx : positive_indices) { - if (extra_counts[idx] == 0) - continue; - if (remainders[idx] > best_remainder || - (std::abs(remainders[idx] - best_remainder) <= 1e-9 && extra_counts[idx] > best_extra) || - (std::abs(remainders[idx] - best_remainder) <= 1e-9 && extra_counts[idx] == best_extra && idx < best_idx)) { - best_idx = idx; - best_remainder = remainders[idx]; - best_extra = extra_counts[idx]; - } - } - if (best_idx == size_t(-1)) - break; - ++reduced[best_idx]; - remainders[best_idx] = -1.0; - --missing_slots; - } - - counts = std::move(reduced); -} - -std::vector MixedFilamentConfigPanel::build_weighted_multi_sequence(const std::vector &ids, - const std::vector &weights, - size_t max_cycle_limit) -{ - std::vector seq; - if (ids.empty()) - return seq; - - std::vector filtered_ids; - std::vector counts; - filtered_ids.reserve(ids.size()); - counts.reserve(ids.size()); - - std::vector normalized = normalize_gradient_weights(weights, ids.size()); - for (size_t i = 0; i < ids.size(); ++i) { - const int weight = (i < normalized.size()) ? std::max(0, normalized[i]) : 0; - if (weight <= 0) - continue; - filtered_ids.emplace_back(ids[i]); - counts.emplace_back(weight); - } - if (filtered_ids.empty()) { - filtered_ids = ids; - counts.assign(ids.size(), 1); - } - - int g = 0; - for (const int c : counts) - g = std::gcd(g, std::max(1, c)); - if (g > 1) { - for (int &c : counts) - c = std::max(1, c / g); - } - - constexpr size_t k_max_cycle = 48; - const size_t effective_cycle_limit = - max_cycle_limit > 0 ? std::min(k_max_cycle, std::max(1, max_cycle_limit)) : k_max_cycle; - reduce_weight_counts_to_cycle_limit(counts, effective_cycle_limit); - - std::vector reduced_ids; - std::vector reduced_counts; - reduced_ids.reserve(filtered_ids.size()); - reduced_counts.reserve(counts.size()); - for (size_t i = 0; i < counts.size(); ++i) { - if (counts[i] <= 0) - continue; - reduced_ids.emplace_back(filtered_ids[i]); - reduced_counts.emplace_back(counts[i]); - } - if (reduced_ids.empty()) - return seq; - filtered_ids = std::move(reduced_ids); - counts = std::move(reduced_counts); - - const int total = std::accumulate(counts.begin(), counts.end(), 0); - if (total <= 0) - return seq; - - const size_t cycle = size_t(total); - - seq.reserve(cycle); - std::vector emitted(counts.size(), 0); - for (size_t pos = 0; pos < cycle; ++pos) { - size_t best_idx = 0; - double best_score = -1e9; - for (size_t i = 0; i < counts.size(); ++i) { - const double target = double(pos + 1) * double(counts[i]) / double(total); - const double score = target - double(emitted[i]); - if (score > best_score) { - best_score = score; - best_idx = i; - } - } - ++emitted[best_idx]; - seq.emplace_back(filtered_ids[best_idx]); - } - if (seq.empty()) - seq = filtered_ids; - return seq; -} - - -std::vector MixedFilamentConfigPanel::build_local_z_preview_pass_heights(double nominal_layer_height, - double lower_bound, - double upper_bound, - double preferred_a_height, - double preferred_b_height, - int mix_b_percent, - int max_sublayers_limit) -{ - if (nominal_layer_height <= EPSILON) - return {}; - - const double base_height = nominal_layer_height; - const double lo = std::max(0.01, lower_bound); - const double hi = std::max(lo, upper_bound); - const size_t max_passes_limit = max_sublayers_limit >= 2 ? size_t(max_sublayers_limit) : size_t(0); - - auto fit_pass_heights_to_interval = [](std::vector &passes, double total_height, double local_lo, double local_hi) { - if (passes.empty() || total_height <= EPSILON) - return false; - - const auto within = [local_lo, local_hi](double value) { - return value >= local_lo - 1e-6 && value <= local_hi + 1e-6; - }; - - double sum = 0.0; - for (const double h : passes) - sum += h; - - double delta = total_height - sum; - if (std::abs(delta) > 1e-6) { - if (delta > 0.0) { - for (double &h : passes) { - if (delta <= 1e-6) - break; - const double room = local_hi - h; - if (room <= 1e-6) - continue; - const double take = std::min(room, delta); - h += take; - delta -= take; - } - } else { - for (auto it = passes.rbegin(); it != passes.rend() && delta < -1e-6; ++it) { - const double room = *it - local_lo; - if (room <= 1e-6) - continue; - const double take = std::min(room, -delta); - *it -= take; - delta += take; - } - } - } - - if (std::abs(delta) > 1e-6) - return false; - return std::all_of(passes.begin(), passes.end(), within); - }; - - auto build_uniform = [&fit_pass_heights_to_interval, base_height, lo, hi, max_passes_limit]() { - std::vector out; - size_t min_passes = size_t(std::max(1.0, std::ceil((base_height - EPSILON) / hi))); - size_t max_passes = size_t(std::max(1.0, std::floor((base_height + EPSILON) / lo))); - size_t pass_count = min_passes; - - if (max_passes >= min_passes) { - const double target_step = 0.5 * (lo + hi); - const size_t target_passes = - size_t(std::max(1.0, std::llround(base_height / std::max(target_step, EPSILON)))); - pass_count = std::clamp(target_passes, min_passes, max_passes); - } - - if (max_passes_limit > 0 && pass_count > max_passes_limit) - pass_count = max_passes_limit; - - if (pass_count == 1 && base_height >= 2.0 * lo - EPSILON && max_passes >= 2) - pass_count = 2; - - if (pass_count <= 1) { - out.emplace_back(base_height); - return out; - } - - out.assign(pass_count, base_height / double(pass_count)); - double accumulated = 0.0; - for (size_t i = 0; i + 1 < out.size(); ++i) - accumulated += out[i]; - out.back() = std::max(EPSILON, base_height - accumulated); - if (!fit_pass_heights_to_interval(out, base_height, lo, hi) && max_passes_limit == 0) { - out.assign(pass_count, base_height / double(pass_count)); - accumulated = 0.0; - for (size_t i = 0; i + 1 < out.size(); ++i) - accumulated += out[i]; - out.back() = std::max(EPSILON, base_height - accumulated); - } - return out; - }; - - auto build_alternating = [&build_uniform, &fit_pass_heights_to_interval, base_height, lo, hi, max_passes_limit](double gradient_h_a, double gradient_h_b) { - if (base_height < 2.0 * lo - EPSILON) - return std::vector{ base_height }; - - const double cycle_h = std::max(EPSILON, gradient_h_a + gradient_h_b); - const double ratio_a = std::clamp(gradient_h_a / cycle_h, 0.0, 1.0); - - size_t min_passes = size_t(std::max(2.0, std::ceil((base_height - EPSILON) / hi))); - if ((min_passes % 2) != 0) - ++min_passes; - - size_t max_passes = size_t(std::max(2.0, std::floor((base_height + EPSILON) / lo))); - if ((max_passes % 2) != 0) - --max_passes; - if (max_passes_limit > 0) { - size_t capped_limit = std::max(2, max_passes_limit); - if ((capped_limit % 2) != 0) - --capped_limit; - if (capped_limit >= 2) - max_passes = std::min(max_passes, capped_limit); - } - if (max_passes < 2) - return build_uniform(); - if (min_passes > max_passes) - min_passes = max_passes; - if (min_passes < 2) - min_passes = 2; - if ((min_passes % 2) != 0) - ++min_passes; - if (min_passes > max_passes) - return build_uniform(); - - const double target_step = 0.5 * (lo + hi); - size_t target_passes = - size_t(std::max(2.0, std::llround(base_height / std::max(target_step, EPSILON)))); - if ((target_passes % 2) != 0) { - const size_t round_up = (target_passes < max_passes) ? (target_passes + 1) : max_passes; - const size_t round_down = (target_passes > min_passes) ? (target_passes - 1) : min_passes; - if (round_up > max_passes) - target_passes = round_down; - else if (round_down < min_passes) - target_passes = round_up; - else - target_passes = ((round_up - target_passes) <= (target_passes - round_down)) ? round_up : round_down; - } - target_passes = std::clamp(target_passes, min_passes, max_passes); - - bool has_best = false; - std::vector best_passes; - double best_ratio_error = 0.0; - size_t best_pass_distance = 0; - double best_max_height = 0.0; - size_t best_pass_count = 0; - - for (size_t pass_count = min_passes; pass_count <= max_passes; pass_count += 2) { - const size_t pair_count = pass_count / 2; - if (pair_count == 0) - continue; - const double pair_h = base_height / double(pair_count); - - const double h_a_min = std::max(lo, pair_h - hi); - const double h_a_max = std::min(hi, pair_h - lo); - if (h_a_min > h_a_max + EPSILON) - continue; - - const double h_a = std::clamp(pair_h * ratio_a, h_a_min, h_a_max); - const double h_b = pair_h - h_a; - - std::vector out; - out.reserve(pass_count); - for (size_t pair_idx = 0; pair_idx < pair_count; ++pair_idx) { - out.emplace_back(h_a); - out.emplace_back(h_b); - } - if (!fit_pass_heights_to_interval(out, base_height, lo, hi)) - continue; - - const double ratio_actual = (h_a + h_b > EPSILON) ? (h_a / (h_a + h_b)) : 0.5; - const double ratio_error = std::abs(ratio_actual - ratio_a); - const size_t pass_distance = - (pass_count > target_passes) ? (pass_count - target_passes) : (target_passes - pass_count); - const double max_height = std::max(h_a, h_b); - - const bool better_ratio = !has_best || (ratio_error + 1e-6 < best_ratio_error); - const bool similar_ratio = has_best && std::abs(ratio_error - best_ratio_error) <= 1e-6; - const bool better_distance = similar_ratio && (pass_distance < best_pass_distance); - const bool similar_distance = similar_ratio && (pass_distance == best_pass_distance); - const bool better_max_height = similar_distance && (max_height + 1e-6 < best_max_height); - const bool similar_max_height = similar_distance && std::abs(max_height - best_max_height) <= 1e-6; - const bool better_pass_count = similar_max_height && (pass_count > best_pass_count); - - if (better_ratio || better_distance || better_max_height || better_pass_count) { - has_best = true; - best_passes = std::move(out); - best_ratio_error = ratio_error; - best_pass_distance = pass_distance; - best_max_height = max_height; - best_pass_count = pass_count; - } - } - - return has_best ? best_passes : build_uniform(); - }; - - if (preferred_a_height > EPSILON || preferred_b_height > EPSILON) { - std::vector cadence_unit; - if (preferred_a_height > EPSILON) - cadence_unit.push_back(std::clamp(preferred_a_height, lo, hi)); - if (preferred_b_height > EPSILON) - cadence_unit.push_back(std::clamp(preferred_b_height, lo, hi)); - - if (!cadence_unit.empty()) { - std::vector out; - out.reserve(size_t(std::ceil(base_height / lo)) + 2); - - double z_used = 0.0; - size_t idx = 0; - size_t guard = 0; - while (z_used + cadence_unit[idx] < base_height - EPSILON && guard++ < 100000) { - out.push_back(cadence_unit[idx]); - z_used += cadence_unit[idx]; - idx = (idx + 1) % cadence_unit.size(); - } - - const double remainder = base_height - z_used; - if (remainder > EPSILON) - out.push_back(remainder); - - if (fit_pass_heights_to_interval(out, base_height, lo, hi) && - (max_passes_limit == 0 || out.size() <= max_passes_limit)) - return out; - } - - if (preferred_a_height > EPSILON && preferred_b_height > EPSILON) - return build_alternating(preferred_a_height, preferred_b_height); - return build_uniform(); - } - - const int mix_b = std::clamp(mix_b_percent, 0, 100); - const double pct_b = double(mix_b) / 100.0; - const double pct_a = 1.0 - pct_b; - const double gradient_h_a = lo + pct_a * (hi - lo); - const double gradient_h_b = lo + pct_b * (hi - lo); - return build_alternating(gradient_h_a, gradient_h_b); -} - -int MixedFilamentConfigPanel::effective_local_z_preview_mix_b_percent(const MixedFilament &mf, - const MixedFilamentPreviewSettings &preview_settings) -{ - return Slic3r::mixed_filament_effective_local_z_preview_mix_b_percent(mf, preview_settings); -} - -static double mixed_filament_reference_nozzle_mm(unsigned int component_a, - unsigned int component_b, - const std::vector &nozzle_diameters) -{ - std::vector samples; - samples.reserve(2); - - auto append_if_valid = [&samples, &nozzle_diameters](unsigned int component_id) { - if (component_id >= 1 && component_id <= nozzle_diameters.size()) - samples.emplace_back(std::max(0.05, nozzle_diameters[size_t(component_id - 1)])); - }; - - append_if_valid(component_a); - append_if_valid(component_b); - - if (samples.empty()) - return 0.4; - return std::accumulate(samples.begin(), samples.end(), 0.0) / double(samples.size()); -} - -static double mixed_filament_bias_limit_mm(const MixedFilament &mf, const std::vector &nozzle_diameters) -{ - const double reference_nozzle_mm = mixed_filament_reference_nozzle_mm(mf.component_a, mf.component_b, nozzle_diameters); - return MixedFilamentManager::max_pair_bias_mm(float(reference_nozzle_mm)); -} - -static float mixed_filament_single_surface_offset_value(const MixedFilament &mf, - const std::vector &nozzle_diameters) -{ - const double reference_nozzle_mm = mixed_filament_reference_nozzle_mm(mf.component_a, mf.component_b, nozzle_diameters); - return MixedFilamentManager::bias_ui_value_from_surface_offsets( - mf.component_a_surface_offset, - mf.component_b_surface_offset, - float(reference_nozzle_mm)); -} - -static std::pair mixed_filament_single_surface_offset_pair(const MixedFilament &mf, - float value, - const std::vector &nozzle_diameters) -{ - const double reference_nozzle_mm = mixed_filament_reference_nozzle_mm(mf.component_a, mf.component_b, nozzle_diameters); - return MixedFilamentManager::surface_offset_pair_from_signed_bias(value, float(reference_nozzle_mm)); -} - -static std::string mixed_filament_apparent_pair_summary(const MixedFilament &mf, - const MixedFilamentPreviewSettings &preview_settings, - const std::vector &nozzle_diameters, - bool bias_mode_enabled) -{ - if (!Slic3r::mixed_filament_supports_bias_apparent_color(mf, preview_settings, bias_mode_enabled)) - return {}; - - const int base_b = MixedFilamentConfigPanel::effective_local_z_preview_mix_b_percent(mf, preview_settings); - const int base_a = 100 - base_b; - const auto [apparent_a, apparent_b] = - Slic3r::mixed_filament_apparent_pair_percentages(mf, preview_settings, nozzle_diameters, bias_mode_enabled); - - if (std::abs(mf.component_a_surface_offset - mf.component_b_surface_offset) > 1e-4f && - (apparent_a != base_a || apparent_b != base_b)) { - std::ostringstream ss; - ss << '~' << apparent_a << '/' << apparent_b; - return ss.str(); - } - - std::ostringstream ss; - ss << apparent_a << "%/" << apparent_b << '%'; - return ss.str(); -} - - -static std::vector build_display_weighted_multi_sequence(const std::vector &ids, - const std::vector &weights, - size_t max_cycle_limit = 0) -{ - if (ids.empty()) - return {}; - - std::vector filtered_ids; - std::vector counts; - filtered_ids.reserve(ids.size()); - counts.reserve(ids.size()); - - const std::vector normalized = normalize_color_match_weights(weights, ids.size()); - for (size_t idx = 0; idx < ids.size(); ++idx) { - const int weight = idx < normalized.size() ? std::max(0, normalized[idx]) : 0; - if (weight <= 0) - continue; - filtered_ids.emplace_back(ids[idx]); - counts.emplace_back(weight); - } - if (filtered_ids.empty()) { - filtered_ids = ids; - counts.assign(ids.size(), 1); - } - - int g = 0; - for (const int count : counts) - g = std::gcd(g, std::max(1, count)); - if (g > 1) { - for (int &count : counts) - count = std::max(1, count / g); - } - - constexpr size_t k_max_cycle = 48; - const size_t effective_cycle_limit = - max_cycle_limit > 0 ? std::min(k_max_cycle, std::max(1, max_cycle_limit)) : k_max_cycle; - reduce_weight_counts_to_cycle_limit(counts, effective_cycle_limit); - - std::vector reduced_ids; - std::vector reduced_counts; - reduced_ids.reserve(filtered_ids.size()); - reduced_counts.reserve(counts.size()); - for (size_t idx = 0; idx < counts.size(); ++idx) { - if (counts[idx] <= 0) - continue; - reduced_ids.emplace_back(filtered_ids[idx]); - reduced_counts.emplace_back(counts[idx]); - } - if (reduced_ids.empty()) - return {}; - filtered_ids = std::move(reduced_ids); - counts = std::move(reduced_counts); - - const int total = std::accumulate(counts.begin(), counts.end(), 0); - if (total <= 0) - return std::vector(filtered_ids.begin(), filtered_ids.end()); - - const size_t cycle = size_t(total); - - std::vector sequence; - sequence.reserve(cycle); - std::vector emitted(counts.size(), 0); - for (size_t pos = 0; pos < cycle; ++pos) { - size_t best_idx = 0; - double best_score = -1e9; - for (size_t idx = 0; idx < counts.size(); ++idx) { - const double target = double(pos + 1) * double(counts[idx]) / double(total); - const double score = target - double(emitted[idx]); - if (score > best_score) { - best_score = score; - best_idx = idx; - } - } - ++emitted[best_idx]; - sequence.emplace_back(filtered_ids[best_idx]); - } - if (sequence.empty()) - sequence = filtered_ids; - return sequence; -} - -static std::string blend_display_color_from_sequence(const std::vector &colors, - size_t num_physical, - const std::vector &sequence, - const std::string &fallback) -{ - if (colors.empty() || sequence.empty() || num_physical == 0) - return fallback; - - std::vector counts(num_physical + 1, size_t(0)); - size_t total = 0; - for (const unsigned int id : sequence) { - if (id == 0 || id > num_physical) - continue; - ++counts[id]; - ++total; - } - if (total == 0) - return fallback; - - unsigned int first_id = 0; - for (size_t id = 1; id <= num_physical; ++id) { - if (counts[id] > 0) { - first_id = unsigned(id); - break; - } - } - if (first_id == 0 || first_id > colors.size()) - return fallback; - - std::string blended = colors[first_id - 1]; - int accumulated = int(counts[first_id]); - for (size_t id = size_t(first_id + 1); id <= num_physical; ++id) { - if (counts[id] == 0 || id > colors.size()) - continue; - blended = MixedFilamentManager::blend_color(blended, colors[id - 1], accumulated, int(counts[id])); - accumulated += int(counts[id]); - } - - return blended; -} - - -std::string MixedFilamentConfigPanel::summarize_sequence(const std::vector &seq) -{ - if (seq.empty()) return ""; - std::unordered_map counts; - for (unsigned int id : seq) counts[id]++; - std::vector> sorted; - for (auto &kv : counts) sorted.emplace_back(kv.second, kv.first); - std::sort(sorted.begin(), sorted.end(), std::greater<>()); - std::string out; - for (auto &p : sorted) { - if (!out.empty()) out += "/"; - out += format_preview_sequence_percent(p.first, int(seq.size())); - } - return out; -} - -std::string MixedFilamentConfigPanel::summarize_local_z_breakdown(const MixedFilament &mf, - const std::vector &weights, - const MixedFilamentPreviewSettings &preview_settings) -{ - const std::string normalized_pattern = MixedFilamentManager::normalize_manual_pattern(mf.manual_pattern); - if (!normalized_pattern.empty()) - return "Local-Z breakdown: manual pattern rows do not use pair decomposition."; - - if (mf.distribution_mode == int(MixedFilament::SameLayerPointillisme)) - return "Local-Z breakdown: same-layer mode does not use local-Z pair decomposition."; - - auto pair_name = [](unsigned int a, unsigned int b) { - std::ostringstream ss; - ss << 'F' << a << "+F" << b; - return ss.str(); - }; - auto pair_split = [](unsigned int a, unsigned int b, int weight_a, int weight_b) { - const int safe_a = std::max(0, weight_a); - const int safe_b = std::max(0, weight_b); - const int total = std::max(1, safe_a + safe_b); - const int pct_a = int(std::lround(100.0 * double(safe_a) / double(total))); - const int pct_b = std::max(0, 100 - pct_a); - - std::ostringstream ss; - ss << 'F' << a << "/F" << b << " " << safe_a << ':' << safe_b << " (" << pct_a << '/' << pct_b << ')'; - return ss.str(); - }; - auto cadence_entry = [&pair_name](unsigned int a, unsigned int b, int weight, int total) { - const int pct = int(std::lround(100.0 * double(std::max(0, weight)) / double(std::max(1, total)))); - std::ostringstream ss; - ss << pair_name(a, b) << ' ' << pct << '%'; - return ss.str(); - }; - - const std::vector ids = decode_gradient_ids(mf.gradient_component_ids); - if (preview_settings.local_z_mode && preview_settings.local_z_direct_multicolor && ids.size() >= 3) { - const std::vector normalized = normalize_gradient_weights(weights, ids.size()); - const size_t effective_sublayers = - mf.local_z_max_sublayers >= 2 ? size_t(std::max(2, mf.local_z_max_sublayers)) : ids.size(); - - std::ostringstream ss; - ss << "Local-Z direct multicolor solver: "; - for (size_t idx = 0; idx < ids.size(); ++idx) { - if (idx > 0) - ss << ", "; - const int pct = idx < normalized.size() ? normalized[idx] : 0; - ss << 'F' << ids[idx] << ' ' << pct << '%'; - } - ss << ".\nCarry-over error is distributed directly across all " << ids.size() - << " components instead of collapsing them into pair cadence."; - if (mf.local_z_max_sublayers >= 2) - ss << "\nEffective Local-Z cap: up to " << effective_sublayers << " sublayers per nominal layer."; - return ss.str(); - } - - if (ids.size() >= 4) { - const std::vector normalized = normalize_gradient_weights(weights, ids.size()); - const std::vector pair_tokens = { 1, 2 }; - const std::vector pair_weights = { - std::max(1, normalized[0] + normalized[1]), - std::max(1, normalized[2] + normalized[3]) - }; - const size_t max_pair_layers = - (preview_settings.local_z_mode && mf.local_z_max_sublayers >= 2) ? - std::max(1, size_t(mf.local_z_max_sublayers) / 2) : - size_t(0); - const std::vector uncapped_pair_sequence = build_weighted_multi_sequence(pair_tokens, pair_weights); - const std::vector effective_pair_sequence = - max_pair_layers > 0 ? build_weighted_multi_sequence(pair_tokens, pair_weights, max_pair_layers) : uncapped_pair_sequence; - const std::vector &pair_sequence = effective_pair_sequence.empty() ? uncapped_pair_sequence : effective_pair_sequence; - const int pair_ab_weight = int(std::count(pair_sequence.begin(), pair_sequence.end(), 1u)); - const int pair_cd_weight = int(std::count(pair_sequence.begin(), pair_sequence.end(), 2u)); - const int pair_total = std::max(1, int(pair_sequence.size())); - - std::ostringstream ss; - ss << "Local-Z layer cadence: " - << cadence_entry(ids[0], ids[1], pair_ab_weight, pair_total) - << ", " - << cadence_entry(ids[2], ids[3], pair_cd_weight, pair_total) - << ".\nPair splits: " - << pair_split(ids[0], ids[1], normalized[0], normalized[1]) - << ", " - << pair_split(ids[2], ids[3], normalized[2], normalized[3]) - << '.'; - if (!preview_settings.local_z_mode && mf.local_z_max_sublayers >= 2) - ss << "\nSaved row limit will apply when Local-Z dithering mode is enabled in print settings."; - if (preview_settings.local_z_mode && mf.local_z_max_sublayers >= 2) { - ss << "\nEffective Local-Z stack: " << (pair_total * 2) << " sublayers over " << pair_total << " pair layers"; - if (uncapped_pair_sequence.size() > pair_sequence.size()) - ss << " (uncapped " << (uncapped_pair_sequence.size() * 2) << ')'; - ss << '.'; - } - return ss.str(); - } - - if (ids.size() == 3) { - const std::vector normalized = normalize_gradient_weights(weights, ids.size()); - const std::vector pair_tokens = { 1, 2, 3 }; - const std::vector pair_weights = { - std::max(1, normalized[0] + normalized[1]), - std::max(1, normalized[0] + normalized[2]), - std::max(1, normalized[1] + normalized[2]) - }; - const size_t max_pair_layers = - (preview_settings.local_z_mode && mf.local_z_max_sublayers >= 2) ? - std::max(1, size_t(mf.local_z_max_sublayers) / 2) : - size_t(0); - const std::vector uncapped_pair_sequence = build_weighted_multi_sequence(pair_tokens, pair_weights); - const std::vector effective_pair_sequence = - max_pair_layers > 0 ? build_weighted_multi_sequence(pair_tokens, pair_weights, max_pair_layers) : uncapped_pair_sequence; - const std::vector &pair_sequence = effective_pair_sequence.empty() ? uncapped_pair_sequence : effective_pair_sequence; - const int pair_ab_weight = int(std::count(pair_sequence.begin(), pair_sequence.end(), 1u)); - const int pair_ac_weight = int(std::count(pair_sequence.begin(), pair_sequence.end(), 2u)); - const int pair_bc_weight = int(std::count(pair_sequence.begin(), pair_sequence.end(), 3u)); - const int pair_total = std::max(1, int(pair_sequence.size())); - - std::ostringstream ss; - ss << "Local-Z layer cadence: " - << cadence_entry(ids[0], ids[1], pair_ab_weight, pair_total) - << ", " - << cadence_entry(ids[0], ids[2], pair_ac_weight, pair_total) - << ", " - << cadence_entry(ids[1], ids[2], pair_bc_weight, pair_total) - << ".\nPair splits: " - << pair_split(ids[0], ids[1], normalized[0], normalized[1]) - << ", " - << pair_split(ids[0], ids[2], normalized[0], normalized[2]) - << ", " - << pair_split(ids[1], ids[2], normalized[1], normalized[2]) - << '.'; - if (!preview_settings.local_z_mode && mf.local_z_max_sublayers >= 2) - ss << "\nSaved row limit will apply when Local-Z dithering mode is enabled in print settings."; - if (preview_settings.local_z_mode && mf.local_z_max_sublayers >= 2) { - ss << "\nEffective Local-Z stack: " << (pair_total * 2) << " sublayers over " << pair_total << " pair layers"; - if (uncapped_pair_sequence.size() > pair_sequence.size()) - ss << " (uncapped " << (uncapped_pair_sequence.size() * 2) << ')'; - ss << '.'; - } - return ss.str(); - } - - if (mf.component_a >= 1 && mf.component_b >= 1 && mf.component_a != mf.component_b) { - const int pct_b = std::clamp(mf.mix_b_percent, 0, 100); - const int pct_a = 100 - pct_b; - std::ostringstream ss; - ss << "Local-Z pair split: requested F" << mf.component_a << "/F" << mf.component_b - << ' ' << pct_a << '/' << pct_b; - if (preview_settings.local_z_mode) { - const std::vector effective_passes = build_local_z_preview_pass_heights(preview_settings.nominal_layer_height, - preview_settings.mixed_lower_bound, - preview_settings.mixed_upper_bound, - preview_settings.preferred_a_height, - preview_settings.preferred_b_height, - mf.mix_b_percent, - 0); - if (!effective_passes.empty()) { - const int effective_pct_b = effective_local_z_preview_mix_b_percent(mf, preview_settings); - ss << ", effective " << (100 - effective_pct_b) << '/' << effective_pct_b - << " over " << effective_passes.size() << " sublayers"; - } - } - ss << '.'; - return ss.str(); - } - - return "Local-Z breakdown: unavailable."; -} - -std::string MixedFilamentConfigPanel::blend_from_sequence(const std::vector &colors, const std::vector &seq, const std::string &fallback) -{ - if (colors.empty() || seq.empty()) - return fallback; - - std::vector counts(colors.size() + 1, size_t(0)); - size_t total = 0; - for (const unsigned int id : seq) { - if (id == 0 || id > colors.size()) - continue; - ++counts[id]; - ++total; - } - if (total == 0) - return fallback; - - unsigned int first_id = 0; - for (size_t id = 1; id <= colors.size(); ++id) { - if (counts[id] > 0) { - first_id = unsigned(id); - break; - } - } - if (first_id == 0 || first_id > colors.size()) - return fallback; - - std::string blended = colors[first_id - 1]; - int acc = int(counts[first_id]); - for (size_t id = size_t(first_id + 1); id <= colors.size(); ++id) { - if (counts[id] == 0) - continue; - blended = MixedFilamentManager::blend_color(blended, colors[id - 1], acc, int(counts[id])); - acc += int(counts[id]); - } - - return blended; -} - -MixedFilamentConfigPanel::MixedFilamentConfigPanel(wxWindow *parent, - size_t mixed_id, - const MixedFilament &mf, - size_t num_physical, - const std::vector &physical_colors, - const std::vector &nozzle_diameters, - const std::vector &palette, - const MixedFilamentPreviewSettings &preview_settings, - bool bias_mode_enabled, - OnChangeFn on_change) - : wxPanel(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL | wxBORDER_NONE) - , m_mixed_id(mixed_id) - , m_mf(mf) - , m_num_physical(num_physical) - , m_physical_colors(physical_colors) - , m_nozzle_diameters(nozzle_diameters) - , m_palette(palette) - , m_preview_settings(preview_settings) - , m_bias_mode_enabled(bias_mode_enabled) - , m_selected_weight_state(std::make_shared>()) - , m_on_change(on_change) -{ - if (parent) - SetBackgroundColour(parent->GetBackgroundColour()); - else - SetBackgroundColour(wxGetApp().dark_mode() ? wxColour(52, 52, 56) : wxColour(255, 255, 255)); - build_ui(); -} - -void MixedFilamentConfigPanel::build_ui() -{ - const int gap = FromDIP(6); - const int compact_gap = std::max(FromDIP(2), gap / 3); - const bool is_dark = wxGetApp().dark_mode(); - const wxColour panel_bg = GetBackgroundColour().IsOk() ? GetBackgroundColour() : - (is_dark ? wxColour(52, 52, 56) : wxColour(255, 255, 255)); - SetBackgroundColour(panel_bg); - auto *root = new wxBoxSizer(wxVERTICAL); - - // Filament choices - wxArrayString filament_choices; - for (size_t i = 0; i < m_num_physical; ++i) - filament_choices.Add(wxString::Format("F%d", int(i + 1))); - wxArrayString optional_filament_choices; - optional_filament_choices.Add(_L("None")); - for (size_t i = 0; i < m_num_physical; ++i) - optional_filament_choices.Add(wxString::Format("F%d", int(i + 1))); - - const int component_a = std::clamp(int(m_mf.component_a), 1, int(m_num_physical)); - const int component_b = std::clamp(int(m_mf.component_b), 1, int(m_num_physical)); - - const std::vector initial_gradient_ids = decode_gradient_ids(m_mf.gradient_component_ids); - if (m_mf.distribution_mode == int(MixedFilament::SameLayerPointillisme)) { - m_mf.distribution_mode = initial_gradient_ids.size() >= 3 ? int(MixedFilament::LayerCycle) : int(MixedFilament::Simple); - m_mf.pointillism_all_filaments = false; - } - const int stored_distribution_mode = std::clamp(m_mf.distribution_mode, - int(MixedFilament::LayerCycle), - int(MixedFilament::Simple)); - const int row_distribution_mode = initial_gradient_ids.size() >= 3 ? - (stored_distribution_mode == int(MixedFilament::Simple) ? int(MixedFilament::LayerCycle) : stored_distribution_mode) : - int(MixedFilament::Simple); - m_mf.distribution_mode = row_distribution_mode; - const bool multi_gradient_row = row_distribution_mode != int(MixedFilament::Simple) && initial_gradient_ids.size() >= 3; - const int selection_c = initial_gradient_ids.size() >= 3 ? int(initial_gradient_ids[2]) : 0; - const int selection_d = initial_gradient_ids.size() >= 4 ? int(initial_gradient_ids[3]) : 0; - - // Hidden data controls used as backing state for swatch pickers. - m_choice_a = new wxChoice(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, filament_choices); - m_choice_b = new wxChoice(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, filament_choices); - m_choice_a->SetSelection(component_a - 1); - m_choice_b->SetSelection(component_b - 1); - m_choice_a->Hide(); - m_choice_b->Hide(); - if (multi_gradient_row) { - m_choice_c = new wxChoice(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, optional_filament_choices); - m_choice_c->SetSelection(std::clamp(selection_c, 0, int(m_num_physical))); - m_choice_c->Hide(); - if (initial_gradient_ids.size() >= 4) { - m_choice_d = new wxChoice(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, optional_filament_choices); - m_choice_d->SetSelection(std::clamp(selection_d, 0, int(m_num_physical))); - m_choice_d->Hide(); - } - } - - auto create_component_picker = [this, gap](wxPanel *&container_out, wxPanel *&swatch_out, wxStaticText *&label_out, const wxString &tooltip) { - const int inner_gap = std::max(FromDIP(1), gap / 4); - const bool local_is_dark = wxGetApp().dark_mode(); - const wxColour local_picker_bg = local_is_dark ? wxColour(64, 64, 70) : wxColour(255, 255, 255); - const wxColour local_picker_text = local_is_dark ? wxColour(230, 230, 230) : wxColour(32, 32, 32); - container_out = new wxPanel(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxBORDER_SIMPLE); - container_out->SetBackgroundColour(local_picker_bg); - const wxSize picker_size(FromDIP(38), FromDIP(22)); - container_out->SetMinSize(picker_size); - container_out->SetMaxSize(picker_size); - - auto *container_sizer = new wxBoxSizer(wxHORIZONTAL); - swatch_out = new wxPanel(container_out, wxID_ANY, wxDefaultPosition, wxSize(FromDIP(12), FromDIP(12)), wxBORDER_SIMPLE); - swatch_out->SetMinSize(wxSize(FromDIP(12), FromDIP(12))); - swatch_out->SetToolTip(tooltip); - label_out = new wxStaticText(container_out, wxID_ANY, wxEmptyString); - label_out->SetForegroundColour(local_picker_text); - label_out->SetToolTip(tooltip); - - auto *content_sizer = new wxBoxSizer(wxHORIZONTAL); - content_sizer->Add(swatch_out, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, inner_gap); - content_sizer->Add(label_out, 0, wxALIGN_CENTER_VERTICAL); - container_sizer->AddStretchSpacer(1); - container_sizer->Add(content_sizer, 0, wxALIGN_CENTER_VERTICAL); - container_sizer->AddStretchSpacer(1); - container_out->SetSizer(container_sizer); - container_out->SetToolTip(tooltip); - container_out->SetCursor(wxCursor(wxCURSOR_HAND)); - swatch_out->SetCursor(wxCursor(wxCURSOR_HAND)); - label_out->SetCursor(wxCursor(wxCURSOR_HAND)); - }; - - create_component_picker(m_picker_a_container, m_picker_a_swatch, m_picker_a_label, _L("Click to choose a physical filament color")); - create_component_picker(m_picker_b_container, m_picker_b_swatch, m_picker_b_label, _L("Click to choose a physical filament color")); - if (m_choice_c) - create_component_picker(m_picker_c_container, m_picker_c_swatch, m_picker_c_label, _L("Click to choose a physical filament color")); - if (m_choice_d) - create_component_picker(m_picker_d_container, m_picker_d_swatch, m_picker_d_label, _L("Click to choose a physical filament color")); - update_component_picker_visuals(); - - // Check for pattern mode - const std::string normalized_pattern = MixedFilamentManager::normalize_manual_pattern(m_mf.manual_pattern); - const bool pattern_row_mode = !normalized_pattern.empty(); - - auto *picker_row = new wxBoxSizer(wxHORIZONTAL); - if (!pattern_row_mode) { - auto add_picker = [this, picker_row, gap](wxPanel *container, bool &first_picker) { - if (!container) - return; - if (!first_picker) - picker_row->Add(new wxStaticText(this, wxID_ANY, "+"), 0, wxALIGN_CENTER_VERTICAL | wxLEFT | wxRIGHT, std::max(FromDIP(2), gap / 2)); - picker_row->Add(container, 0, wxALIGN_CENTER_VERTICAL); - first_picker = false; - }; - - bool first_picker = true; - add_picker(m_picker_a_container, first_picker); - add_picker(m_picker_b_container, first_picker); - add_picker(m_picker_c_container, first_picker); - add_picker(m_picker_d_container, first_picker); - } else { - if (m_picker_a_container) m_picker_a_container->Hide(); - if (m_picker_b_container) m_picker_b_container->Hide(); - if (m_picker_c_container) m_picker_c_container->Hide(); - if (m_picker_d_container) m_picker_d_container->Hide(); - } - root->Add(picker_row, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, gap); - - // Pattern controls (if pattern mode) - if (pattern_row_mode) { - auto *pattern_row = new wxBoxSizer(wxHORIZONTAL); - auto *pattern_label = new wxStaticText(this, wxID_ANY, _L("Pattern")); - pattern_label->SetForegroundColour(is_dark ? wxColour(236, 236, 236) : wxColour(20, 20, 20)); - pattern_row->Add(pattern_label, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, gap); - m_pattern_ctrl = new wxTextCtrl(this, wxID_ANY, from_u8(normalized_pattern), wxDefaultPosition, - wxSize(FromDIP(200), -1), wxTE_PROCESS_ENTER); - m_pattern_ctrl->SetToolTip(_L("Manual repeating pattern. Digits 1-9 for filament IDs 1-9. " - "Use [N] for IDs >= 10 (e.g. [12]). " - "Use commas to define per-perimeter groups, e.g. 12,21. " - "Example: 11112222, 12,21, or 1234.")); - pattern_row->Add(m_pattern_ctrl, 1, wxALIGN_CENTER_VERTICAL); - root->Add(pattern_row, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, gap); - - auto *quick_buttons = new wxBoxSizer(wxHORIZONTAL); - for (size_t fid = 0; fid < m_num_physical; ++fid) { - wxButton *btn = new wxButton(this, wxID_ANY, wxString::Format("%d", int(fid + 1)), - wxDefaultPosition, wxSize(FromDIP(24), FromDIP(22)), wxBU_EXACTFIT); - const wxColour chip_color = (fid < m_palette.size()) ? m_palette[fid] : wxColour("#26A69A"); - btn->SetBackgroundColour(chip_color); - btn->SetToolTip(wxString::Format(_L("Append filament %d to pattern"), int(fid + 1))); - quick_buttons->Add(btn, 0, wxRIGHT, FromDIP(4)); - m_pattern_quick_buttons.emplace_back(btn); - } - auto *filaments_label = new wxStaticText(this, wxID_ANY, _L("Filaments")); - filaments_label->SetForegroundColour(is_dark ? wxColour(236, 236, 236) : wxColour(20, 20, 20)); - picker_row->Add(filaments_label, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, std::max(FromDIP(3), gap / 2)); - picker_row->Add(quick_buttons, 0, wxALIGN_CENTER_VERTICAL); - } else { - // Blend selector for non-pattern mode - const bool simple_mode = row_distribution_mode == int(MixedFilament::Simple); - std::vector selected_gradient_ids = simple_mode ? std::vector() : initial_gradient_ids; - if (selected_gradient_ids.size() < 3) selected_gradient_ids.clear(); - if (selected_gradient_ids.empty()) { - selected_gradient_ids.emplace_back(unsigned(component_a)); - if (component_b != component_a) selected_gradient_ids.emplace_back(unsigned(component_b)); - } - const bool multi_gradient_mode = selected_gradient_ids.size() >= 3; - *m_selected_weight_state = normalize_gradient_weights( - decode_gradient_weights(m_mf.gradient_component_weights, selected_gradient_ids.size()), - selected_gradient_ids.size()); - - wxColour color_a = (component_a >= 1 && component_a <= int(m_palette.size())) ? m_palette[component_a - 1] : wxColour("#26A69A"); - wxColour color_b = (component_b >= 1 && component_b <= int(m_palette.size())) ? m_palette[component_b - 1] : wxColour("#26A69A"); - m_blend_selector = new MixedGradientSelector(this, color_a, color_b, std::clamp(m_mf.mix_b_percent, 0, 100)); - m_blend_selector->SetBackgroundColour(panel_bg); - m_blend_label = nullptr; - picker_row->AddSpacer(gap); - picker_row->Add(m_blend_selector, 1, wxEXPAND | wxALIGN_CENTER_VERTICAL | wxLEFT, gap); - - if (m_blend_selector) { - std::vector corner_colors; - corner_colors.reserve(selected_gradient_ids.size()); - for (const unsigned int id : selected_gradient_ids) { - if (id >= 1 && id <= m_palette.size()) - corner_colors.emplace_back(m_palette[id - 1]); - } - if (!simple_mode && corner_colors.size() >= 3) - m_blend_selector->set_multi_preview(corner_colors, *m_selected_weight_state); - } - } - - // Preview - auto *preview_row = new wxBoxSizer(wxHORIZONTAL); - m_mix_preview = new MixedMixPreview(this); - m_mix_preview->SetBackgroundColour(panel_bg); - preview_row->Add(m_mix_preview, 1, wxEXPAND | wxALIGN_CENTER_VERTICAL | wxRIGHT, compact_gap); - - auto *bias_controls = new wxBoxSizer(wxHORIZONTAL); - const float initial_surface_offset_value = mixed_filament_single_surface_offset_value(m_mf, m_nozzle_diameters); - const double initial_bias_limit = mixed_filament_bias_limit_mm(m_mf, m_nozzle_diameters); - const wxString bias_tooltip = - _L("Positive bias recesses the second filament in the pair; negative bias recesses the first filament.\n\n" - "The color chip shows which filament the current value affects.\n\n" - "Grouped wall patterns and Local-Z dithering ignore it."); - - auto *surface_offset_label = new wxStaticText(this, wxID_ANY, _L("Bias")); - surface_offset_label->SetForegroundColour(is_dark ? wxColour(236, 236, 236) : wxColour(20, 20, 20)); - surface_offset_label->SetToolTip(bias_tooltip); - bias_controls->Add(surface_offset_label, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, compact_gap); - - create_component_picker(m_surface_offset_target_container, - m_surface_offset_target_swatch, - m_surface_offset_target_label, - bias_tooltip); - if (m_surface_offset_target_container) - m_surface_offset_target_container->SetCursor(wxCursor(wxCURSOR_ARROW)); - if (m_surface_offset_target_swatch) - m_surface_offset_target_swatch->SetCursor(wxCursor(wxCURSOR_ARROW)); - if (m_surface_offset_target_label) - m_surface_offset_target_label->SetCursor(wxCursor(wxCURSOR_ARROW)); - bias_controls->Add(m_surface_offset_target_container, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, compact_gap); - - m_surface_offset_spin = new wxSpinCtrlDouble(this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize(FromDIP(58), -1), - wxSP_ARROW_KEYS | wxALIGN_RIGHT | wxTE_PROCESS_ENTER, - -initial_bias_limit, initial_bias_limit, - std::clamp(double(initial_surface_offset_value), -initial_bias_limit, initial_bias_limit), 0.001); - m_surface_offset_spin->SetDigits(3); - m_surface_offset_spin->SetToolTip(bias_tooltip); - bias_controls->Add(m_surface_offset_spin, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, compact_gap); - - auto *surface_offset_units = new wxStaticText(this, wxID_ANY, _L("mm")); - surface_offset_units->SetForegroundColour(is_dark ? wxColour(210, 210, 210) : wxColour(72, 72, 72)); - surface_offset_units->SetToolTip(bias_tooltip); - bias_controls->Add(surface_offset_units, 0, wxALIGN_CENTER_VERTICAL); - if (m_bias_mode_enabled) - preview_row->Add(bias_controls, 0, wxALIGN_CENTER_VERTICAL); - else { - surface_offset_label->Hide(); - if (m_surface_offset_target_container) - m_surface_offset_target_container->Hide(); - if (m_surface_offset_spin) - m_surface_offset_spin->Hide(); - surface_offset_units->Hide(); - } - root->Add(preview_row, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, gap); - - if (m_bias_mode_enabled) { - const auto initial_surface_offset_pair = - mixed_filament_single_surface_offset_pair(m_mf, initial_surface_offset_value, m_nozzle_diameters); - m_mf.component_a_surface_offset = initial_surface_offset_pair.first; - m_mf.component_b_surface_offset = initial_surface_offset_pair.second; - } - - const bool initial_component_surface_offsets_supported = m_bias_mode_enabled && - !pattern_row_mode && - row_distribution_mode != int(MixedFilament::SameLayerPointillisme) && - !m_preview_settings.local_z_mode; - if (m_surface_offset_spin) - m_surface_offset_spin->Enable(initial_component_surface_offsets_supported); - - const bool local_z_limit_supported = multi_gradient_row && - row_distribution_mode != int(MixedFilament::SameLayerPointillisme); - if (local_z_limit_supported) { - auto *local_z_limit_row = new wxBoxSizer(wxHORIZONTAL); - m_local_z_limit_checkbox = new wxCheckBox(this, wxID_ANY, _L("Limit Local-Z")); - m_local_z_limit_checkbox->SetValue(m_mf.local_z_max_sublayers >= 2); - m_local_z_limit_checkbox->SetForegroundColour(is_dark ? wxColour(236, 236, 236) : wxColour(20, 20, 20)); - m_local_z_limit_checkbox->SetToolTip( - _L("Store a per-color Local-Z cadence cap. It applies when Local-Z dithering mode is enabled in print settings.")); - local_z_limit_row->Add(m_local_z_limit_checkbox, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, gap); - - auto *local_z_limit_label = new wxStaticText(this, wxID_ANY, _L("Max sublayers")); - local_z_limit_label->SetForegroundColour(is_dark ? wxColour(236, 236, 236) : wxColour(20, 20, 20)); - local_z_limit_row->Add(local_z_limit_label, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, std::max(FromDIP(3), gap / 2)); - - const int initial_local_z_limit = std::max(2, m_mf.local_z_max_sublayers > 0 ? m_mf.local_z_max_sublayers : 6); - m_local_z_limit_spin = new wxSpinCtrl(this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize(FromDIP(72), -1), - wxSP_ARROW_KEYS | wxALIGN_RIGHT | wxTE_PROCESS_ENTER, 2, 999, initial_local_z_limit); - m_local_z_limit_spin->SetToolTip( - _L("Maximum number of Local-Z sublayers this color may use before its cadence repeats.")); - local_z_limit_row->Add(m_local_z_limit_spin, 0, wxALIGN_CENTER_VERTICAL); - - const bool enable_local_z_limit_controls = m_local_z_limit_checkbox->GetValue(); - m_local_z_limit_spin->Enable(enable_local_z_limit_controls); - root->Add(local_z_limit_row, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, gap); - } - - m_breakdown_label = new wxStaticText(this, wxID_ANY, wxEmptyString); - m_breakdown_label->SetForegroundColour(is_dark ? wxColour(210, 210, 210) : wxColour(72, 72, 72)); - m_breakdown_label->Wrap(FromDIP(360)); - root->Add(m_breakdown_label, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, gap); - - // Bind events - auto apply_changes = [this]() { - m_has_changes = true; - - double surface_offset_value = 0.0; - if (m_surface_offset_spin) { - surface_offset_value = m_surface_offset_spin->GetValue(); -#if !defined(wxHAS_NATIVE_SPINCTRLDOUBLE) - if (wxTextCtrl *text = m_surface_offset_spin->GetText()) { - double parsed_value = 0.0; - if (text->GetValue().ToDouble(&parsed_value)) - surface_offset_value = parsed_value; - } -#endif - } - - int a = std::clamp(m_choice_a->GetSelection() + 1, 1, int(m_num_physical)); - int b = std::clamp(m_choice_b->GetSelection() + 1, 1, int(m_num_physical)); - if (a == b && m_num_physical > 1) { - b = (a == int(m_num_physical)) ? 1 : a + 1; - m_choice_b->SetSelection(b - 1); - } - update_component_picker_visuals(); - - if (m_local_z_limit_spin) - m_local_z_limit_spin->Enable(m_local_z_limit_checkbox != nullptr && - m_local_z_limit_checkbox->GetValue()); - - m_mf.component_a = unsigned(a); - m_mf.component_b = unsigned(b); - if (m_bias_mode_enabled) { - const double bias_limit = mixed_filament_bias_limit_mm(m_mf, m_nozzle_diameters); - const float clamped_surface_offset_value = std::clamp(float(surface_offset_value), -float(bias_limit), float(bias_limit)); - const auto surface_offset_pair = - mixed_filament_single_surface_offset_pair(m_mf, clamped_surface_offset_value, m_nozzle_diameters); - m_mf.component_a_surface_offset = surface_offset_pair.first; - m_mf.component_b_surface_offset = surface_offset_pair.second; - if (m_surface_offset_spin) - m_surface_offset_spin->SetValue(clamped_surface_offset_value); - } - m_mf.local_z_max_sublayers = - (m_local_z_limit_checkbox != nullptr && m_local_z_limit_checkbox->GetValue() && m_local_z_limit_spin != nullptr) ? - std::max(2, m_local_z_limit_spin->GetValue()) : - 0; - - bool simple_mode = true; - bool same_layer_mode = false; - int preview_mix_b_percent = std::clamp(m_mf.mix_b_percent, 0, 100); - std::vector preview_sequence; - - if (m_pattern_ctrl) { - m_mf.distribution_mode = int(MixedFilament::Simple); - std::string normalized = MixedFilamentManager::normalize_manual_pattern(into_u8(m_pattern_ctrl->GetValue())); - if (normalized.empty()) normalized = "12"; - if (into_u8(m_pattern_ctrl->GetValue()) != normalized) - m_pattern_ctrl->ChangeValue(from_u8(normalized)); - m_mf.manual_pattern = normalized; - m_mf.mix_b_percent = MixedFilamentManager::mix_percent_from_manual_pattern(normalized); - m_mf.pointillism_all_filaments = false; - m_mf.gradient_component_ids.clear(); - m_mf.gradient_component_weights.clear(); - preview_sequence = decode_manual_pattern_ids(m_mf.manual_pattern, - m_mf.component_a, - m_mf.component_b, - m_num_physical, - m_preview_settings.wall_loops); - } else { - std::vector selected_ids; - selected_ids.reserve(4); - auto add_unique = [&selected_ids](unsigned int id) { - if (id == 0) return; - if (std::find(selected_ids.begin(), selected_ids.end(), id) == selected_ids.end()) - selected_ids.emplace_back(id); - }; - add_unique(unsigned(a)); - add_unique(unsigned(b)); - if (m_choice_c && m_choice_c->GetSelection() > 0) - add_unique(unsigned(m_choice_c->GetSelection())); - if (m_choice_d && m_choice_d->GetSelection() > 0) - add_unique(unsigned(m_choice_d->GetSelection())); - const bool multi_gradient_mode = selected_ids.size() >= 3; - m_mf.distribution_mode = multi_gradient_mode ? int(MixedFilament::LayerCycle) : int(MixedFilament::Simple); - simple_mode = m_mf.distribution_mode == int(MixedFilament::Simple); - m_mf.mix_b_percent = std::clamp(m_blend_selector ? m_blend_selector->value() : 50, 0, 100); - m_mf.manual_pattern.clear(); - m_mf.pointillism_all_filaments = false; - - const wxColour color_a = (a >= 1 && a <= int(m_palette.size())) ? m_palette[size_t(a - 1)] : wxColour("#26A69A"); - const wxColour color_b = (b >= 1 && b <= int(m_palette.size())) ? m_palette[size_t(b - 1)] : wxColour("#26A69A"); - if (m_blend_selector) { - if (!simple_mode && multi_gradient_mode) { - std::vector corner_colors; - corner_colors.reserve(selected_ids.size()); - for (const unsigned int id : selected_ids) { - if (id >= 1 && id <= m_palette.size()) - corner_colors.emplace_back(m_palette[id - 1]); - } - if (corner_colors.size() >= 3) - m_blend_selector->set_multi_preview(corner_colors, *m_selected_weight_state); - else - m_blend_selector->set_colors(color_a, color_b); - } else { - m_blend_selector->set_colors(color_a, color_b); - } - } - - if (multi_gradient_mode) { - const std::vector decoded_weights = - decode_gradient_weights(m_mf.gradient_component_weights, selected_ids.size()); - if (m_selected_weight_state->size() != selected_ids.size()) - *m_selected_weight_state = decoded_weights; - *m_selected_weight_state = normalize_gradient_weights(*m_selected_weight_state, selected_ids.size()); - m_mf.gradient_component_ids = encode_gradient_ids(selected_ids); - m_mf.gradient_component_weights = encode_gradient_weights(*m_selected_weight_state); - preview_sequence = build_weighted_multi_sequence(selected_ids, *m_selected_weight_state); - } else { - m_mf.gradient_component_ids.clear(); - m_mf.gradient_component_weights.clear(); - preview_mix_b_percent = effective_local_z_preview_mix_b_percent(m_mf, m_preview_settings); - preview_sequence = build_weighted_pair_sequence(m_mf.component_a, m_mf.component_b, preview_mix_b_percent, same_layer_mode); - } - } - m_mf.custom = true; - - const std::vector selected_gradient_ids = decode_gradient_ids(m_mf.gradient_component_ids); - const bool component_surface_offsets_supported = m_bias_mode_enabled && - (m_pattern_ctrl == nullptr) && - !same_layer_mode && - !m_preview_settings.local_z_mode; - if (m_surface_offset_spin) - m_surface_offset_spin->Enable(component_surface_offsets_supported); - if (preview_sequence.empty()) - preview_sequence = build_weighted_pair_sequence(m_mf.component_a, m_mf.component_b, preview_mix_b_percent, same_layer_mode); - - if (m_blend_selector && selected_gradient_ids.size() >= 3) { - std::vector corner_colors; - corner_colors.reserve(selected_gradient_ids.size()); - for (const unsigned int id : selected_gradient_ids) { - if (id >= 1 && id <= m_palette.size()) - corner_colors.emplace_back(m_palette[id - 1]); - } - if (corner_colors.size() >= 3) - m_blend_selector->set_multi_preview(corner_colors, *m_selected_weight_state); - } - - if (Slic3r::mixed_filament_supports_bias_apparent_color(m_mf, m_preview_settings, m_bias_mode_enabled) && - m_mf.component_a >= 1 && m_mf.component_b >= 1 && - m_mf.component_a <= m_physical_colors.size() && m_mf.component_b <= m_physical_colors.size()) { - const auto [apparent_pct_a, apparent_pct_b] = - Slic3r::mixed_filament_apparent_pair_percentages(m_mf, m_preview_settings, m_nozzle_diameters, m_bias_mode_enabled); - m_mf.display_color = MixedFilamentManager::blend_color( - m_physical_colors[size_t(m_mf.component_a - 1)], - m_physical_colors[size_t(m_mf.component_b - 1)], - apparent_pct_a, - apparent_pct_b); - } else if (selected_gradient_ids.size() >= 3 || !preview_sequence.empty()) { - m_mf.display_color = blend_from_sequence(m_physical_colors, preview_sequence, "#26A69A"); - if (m_blend_label) { - if (selected_gradient_ids.size() >= 3) { - m_blend_label->SetLabel(wxString::Format(_L("%d-color layer cycle"), int(selected_gradient_ids.size()))); - } else { - m_blend_label->SetLabel(wxString::Format(simple_mode ? _L("Simple %d%%/%d%%") : _L("%d%%/%d%%"), - 100 - preview_mix_b_percent, preview_mix_b_percent)); - } - } - } else { - m_mf.display_color = MixedFilamentManager::blend_color( - m_physical_colors[size_t(a - 1)], m_physical_colors[size_t(b - 1)], - 100 - preview_mix_b_percent, preview_mix_b_percent); - if (m_blend_label) - m_blend_label->SetLabel(wxString::Format(simple_mode ? _L("Simple %d%%/%d%%") : _L("%d%%/%d%%"), - 100 - preview_mix_b_percent, preview_mix_b_percent)); - } - - if (m_mix_preview) { - const std::string bias_summary = - mixed_filament_apparent_pair_summary(m_mf, m_preview_settings, m_nozzle_diameters, m_bias_mode_enabled); - const std::string summary = bias_summary.empty() ? summarize_sequence(preview_sequence) : bias_summary; - std::vector preview_surface_offsets(m_palette.size() + 1, 0.0); - if (m_bias_mode_enabled && m_mf.component_a >= 1 && m_mf.component_a < preview_surface_offsets.size()) - preview_surface_offsets[m_mf.component_a] = double(m_mf.component_a_surface_offset); - if (m_bias_mode_enabled && m_mf.component_b >= 1 && m_mf.component_b < preview_surface_offsets.size()) - preview_surface_offsets[m_mf.component_b] = double(m_mf.component_b_surface_offset); - m_mix_preview->set_data(m_palette, preview_sequence, same_layer_mode, preview_surface_offsets, wxColour(m_mf.display_color), - _L("Preview"), summary.empty() ? wxString() : from_u8(summary)); - } - update_local_z_breakdown(); - if (m_swatch) { - m_swatch->SetBackgroundColour(wxColour(m_mf.display_color)); - m_swatch->Refresh(); - } - if (m_on_change) - m_on_change(m_mf); - }; - - auto make_color_chip_bitmap = [this](const wxColour &color) { - const int chip_size = FromDIP(14); - wxBitmap bmp(chip_size, chip_size); - wxMemoryDC dc(bmp); - dc.SetBackground(wxBrush(wxColour(255, 255, 255))); - dc.Clear(); - dc.SetPen(wxPen(wxColour(120, 120, 120))); - dc.SetBrush(wxBrush(color)); - dc.DrawRectangle(0, 0, chip_size, chip_size); - dc.SelectObject(wxNullBitmap); - return bmp; - }; - - auto bind_component_picker_popup = [this, apply_changes, make_color_chip_bitmap](wxWindow *target, wxChoice *backing_choice) { - if (!target || !backing_choice) - return; - - target->Bind(wxEVT_LEFT_UP, [this, apply_changes, make_color_chip_bitmap, backing_choice](wxMouseEvent &) { - if (m_num_physical == 0) - return; - - const bool allow_none = backing_choice->GetCount() == unsigned(m_num_physical + 1); - wxMenu menu; - std::vector item_ids; - item_ids.reserve(m_num_physical + (allow_none ? 1 : 0)); - if (allow_none) { - const int item_id = wxWindow::NewControlId(); - item_ids.emplace_back(item_id); - menu.Append(item_id, backing_choice->GetSelection() == 0 ? _L("None (Selected)") : _L("None")); - } - for (size_t i = 0; i < m_num_physical; ++i) { - const int item_id = wxWindow::NewControlId(); - item_ids.emplace_back(item_id); - const int selection_index = allow_none ? int(i + 1) : int(i); - const bool is_selected = selection_index == backing_choice->GetSelection(); - const wxString item_label = wxString::Format("F%d%s", int(i + 1), is_selected ? " (Selected)" : ""); - auto *menu_item = new wxMenuItem(&menu, item_id, item_label, wxEmptyString, wxITEM_NORMAL); - const wxColour item_color = (i < m_palette.size()) ? m_palette[i] : wxColour("#26A69A"); - menu_item->SetBitmap(make_color_chip_bitmap(item_color)); - menu.Append(menu_item); - } - - menu.Bind(wxEVT_COMMAND_MENU_SELECTED, [apply_changes, backing_choice, item_ids](wxCommandEvent &evt) { - const auto it = std::find(item_ids.begin(), item_ids.end(), evt.GetId()); - if (it == item_ids.end()) - return; - const int selection = int(std::distance(item_ids.begin(), it)); - backing_choice->SetSelection(selection); - apply_changes(); - }); - PopupMenu(&menu); - }); - }; - - bind_component_picker_popup(m_picker_a_container, m_choice_a); - bind_component_picker_popup(m_picker_a_swatch, m_choice_a); - bind_component_picker_popup(m_picker_a_label, m_choice_a); - bind_component_picker_popup(m_picker_b_container, m_choice_b); - bind_component_picker_popup(m_picker_b_swatch, m_choice_b); - bind_component_picker_popup(m_picker_b_label, m_choice_b); - bind_component_picker_popup(m_picker_c_container, m_choice_c); - bind_component_picker_popup(m_picker_c_swatch, m_choice_c); - bind_component_picker_popup(m_picker_c_label, m_choice_c); - bind_component_picker_popup(m_picker_d_container, m_choice_d); - bind_component_picker_popup(m_picker_d_swatch, m_choice_d); - bind_component_picker_popup(m_picker_d_label, m_choice_d); - - m_choice_a->Bind(wxEVT_CHOICE, [apply_changes](wxCommandEvent&) { apply_changes(); }); - m_choice_b->Bind(wxEVT_CHOICE, [apply_changes](wxCommandEvent&) { apply_changes(); }); - if (m_choice_c) - m_choice_c->Bind(wxEVT_CHOICE, [apply_changes](wxCommandEvent&) { apply_changes(); }); - if (m_choice_d) - m_choice_d->Bind(wxEVT_CHOICE, [apply_changes](wxCommandEvent&) { apply_changes(); }); - if (m_blend_selector) - m_blend_selector->Bind(wxEVT_SLIDER, [apply_changes](wxCommandEvent&) { apply_changes(); }); - if (m_local_z_limit_checkbox) - m_local_z_limit_checkbox->Bind(wxEVT_CHECKBOX, [apply_changes](wxCommandEvent &) { apply_changes(); }); - if (m_local_z_limit_spin) { - m_local_z_limit_spin->Bind(wxEVT_SPINCTRL, [apply_changes](wxCommandEvent &) { apply_changes(); }); - m_local_z_limit_spin->Bind(wxEVT_TEXT_ENTER, [apply_changes](wxCommandEvent &) { apply_changes(); }); - m_local_z_limit_spin->Bind(wxEVT_KILL_FOCUS, [apply_changes](wxFocusEvent &evt) { - apply_changes(); - evt.Skip(); - }); - } - if (m_surface_offset_spin) { - m_surface_offset_spin->Bind(wxEVT_SPINCTRLDOUBLE, [apply_changes](wxSpinDoubleEvent &) { apply_changes(); }); - m_surface_offset_spin->Bind(wxEVT_TEXT_ENTER, [apply_changes](wxCommandEvent &) { apply_changes(); }); - m_surface_offset_spin->Bind(wxEVT_KILL_FOCUS, [apply_changes](wxFocusEvent &evt) { - apply_changes(); - evt.Skip(); - }); - } - - if (m_blend_selector) { - m_blend_selector->Bind(wxEVT_BUTTON, [this, apply_changes](wxCommandEvent&) { - if (!m_blend_selector->is_multi_mode()) return; - std::vector selected_ids; - auto add_unique = [&selected_ids](unsigned int id) { if (id > 0 && std::find(selected_ids.begin(), selected_ids.end(), id) == selected_ids.end()) selected_ids.emplace_back(id); }; - add_unique(unsigned(std::clamp(m_choice_a ? (m_choice_a->GetSelection() + 1) : 0, 1, int(m_num_physical)))); - add_unique(unsigned(std::clamp(m_choice_b ? (m_choice_b->GetSelection() + 1) : 0, 1, int(m_num_physical)))); - if (m_choice_c && m_choice_c->GetSelection() > 0) add_unique(unsigned(m_choice_c->GetSelection())); - if (m_choice_d && m_choice_d->GetSelection() > 0) add_unique(unsigned(m_choice_d->GetSelection())); - if (selected_ids.size() < 3) return; - const std::vector initial_weights = normalize_gradient_weights(*m_selected_weight_state, selected_ids.size()); - MixedGradientWeightsDialog dlg(this, selected_ids, m_palette, initial_weights); - if (dlg.ShowModal() != wxID_OK) return; - *m_selected_weight_state = dlg.normalized_weights(); - apply_changes(); - }); - } - - if (m_pattern_ctrl) { - auto append_pattern_token = [this](int filament_id) { - if (!m_pattern_ctrl || filament_id <= 0) return; - if (filament_id >= 10) - m_pattern_ctrl->AppendText(wxString::Format("[%d]", filament_id)); - else - m_pattern_ctrl->AppendText(wxString::Format("%d", filament_id)); - }; - m_pattern_ctrl->Bind(wxEVT_TEXT_ENTER, [apply_changes](wxCommandEvent&) { apply_changes(); }); - m_pattern_ctrl->Bind(wxEVT_KILL_FOCUS, [apply_changes](wxFocusEvent &evt) { apply_changes(); evt.Skip(); }); - for (size_t fid = 0; fid < m_pattern_quick_buttons.size(); ++fid) { - wxButton *btn = m_pattern_quick_buttons[fid]; - if (btn) { - const int filament_id = int(fid + 1); - btn->Bind(wxEVT_BUTTON, [apply_changes, append_pattern_token, filament_id](wxCommandEvent&) { - append_pattern_token(filament_id); - apply_changes(); - }); - } - } - } - - update_component_picker_visuals(); - SetSizer(root); - Layout(); - SetMinSize(wxSize(-1, GetBestSize().GetHeight())); - update_preview(); -} - -void MixedFilamentConfigPanel::update_component_picker_visuals() -{ - auto update_one = [this](wxChoice *choice, wxPanel *container, wxPanel *swatch, wxStaticText *label) { - if (!choice) - return; - int sel = choice->GetSelection(); - const bool allow_none = choice->GetCount() == unsigned(m_num_physical + 1); - if (sel < 0 && m_num_physical > 0) { - sel = 0; - choice->SetSelection(sel); - } - if (sel < 0) - return; - - if (allow_none && sel == 0) { - const wxColour none_color = wxGetApp().dark_mode() ? wxColour(86, 86, 92) : wxColour(224, 224, 224); - if (swatch) { - swatch->SetBackgroundColour(none_color); - swatch->Refresh(); - } - if (label) - label->SetLabel(_L("None")); - if (container) { - container->Layout(); - container->Refresh(); - } - return; - } - - const int color_idx = allow_none ? sel - 1 : sel; - const wxColour color = (color_idx >= 0 && size_t(color_idx) < m_palette.size()) ? m_palette[size_t(color_idx)] : wxColour("#26A69A"); - if (swatch) { - swatch->SetBackgroundColour(color); - swatch->Refresh(); - } - if (label) - label->SetLabel(wxString::Format("F%d", color_idx + 1)); - if (container) { - container->Layout(); - container->Refresh(); - } - }; - - update_one(m_choice_a, m_picker_a_container, m_picker_a_swatch, m_picker_a_label); - update_one(m_choice_b, m_picker_b_container, m_picker_b_swatch, m_picker_b_label); - update_one(m_choice_c, m_picker_c_container, m_picker_c_swatch, m_picker_c_label); - update_one(m_choice_d, m_picker_d_container, m_picker_d_swatch, m_picker_d_label); - - if (m_surface_offset_target_container || m_surface_offset_target_swatch || m_surface_offset_target_label || m_surface_offset_spin) { - const int a_filament = std::clamp(m_choice_a ? (m_choice_a->GetSelection() + 1) : int(m_mf.component_a), 1, int(std::max(1, m_num_physical))); - const int b_filament = std::clamp(m_choice_b ? (m_choice_b->GetSelection() + 1) : int(m_mf.component_b), 1, int(std::max(1, m_num_physical))); - MixedFilament active_pair = m_mf; - active_pair.component_a = unsigned(a_filament); - active_pair.component_b = unsigned(b_filament); - double signed_bias_value = mixed_filament_single_surface_offset_value(active_pair, m_nozzle_diameters); - - if (m_surface_offset_spin && m_bias_mode_enabled) { - const double bias_limit = mixed_filament_bias_limit_mm(active_pair, m_nozzle_diameters); - m_surface_offset_spin->SetRange(-bias_limit, bias_limit); - signed_bias_value = m_surface_offset_spin->GetValue(); - } - - const int active_filament = signed_bias_value < -EPSILON ? a_filament : b_filament; - const int color_idx = active_filament - 1; - const wxColour color = (color_idx >= 0 && size_t(color_idx) < m_palette.size()) ? m_palette[size_t(color_idx)] : wxColour("#26A69A"); - if (m_surface_offset_target_swatch) { - m_surface_offset_target_swatch->SetBackgroundColour(color); - m_surface_offset_target_swatch->Refresh(); - } - if (m_surface_offset_target_label) - m_surface_offset_target_label->SetLabel(wxString::Format("F%d", active_filament)); - if (m_surface_offset_target_container) { - m_surface_offset_target_container->Layout(); - m_surface_offset_target_container->Refresh(); - } - } -} - -void MixedFilamentConfigPanel::update_preview() -{ - const bool simple_mode = m_mf.distribution_mode == int(MixedFilament::Simple); - const bool same_layer_mode = m_mf.distribution_mode == int(MixedFilament::SameLayerPointillisme); - const std::string normalized_pattern = MixedFilamentManager::normalize_manual_pattern(m_mf.manual_pattern); - const bool pattern_row_mode = !normalized_pattern.empty(); - - std::vector initial_sequence; - if (pattern_row_mode) { - initial_sequence = decode_manual_pattern_ids(normalized_pattern, - m_mf.component_a, - m_mf.component_b, - m_num_physical, - m_preview_settings.wall_loops); - } else { - std::vector initial_gradient_ids = simple_mode ? std::vector() : decode_gradient_ids(m_mf.gradient_component_ids); - if (initial_gradient_ids.size() >= 3) - initial_sequence = build_weighted_multi_sequence(initial_gradient_ids, *m_selected_weight_state); - else - initial_sequence = build_weighted_pair_sequence(m_mf.component_a, - m_mf.component_b, - effective_local_z_preview_mix_b_percent(m_mf, m_preview_settings), - same_layer_mode); - - if (m_blend_selector && initial_gradient_ids.size() >= 3) { - std::vector corner_colors; - corner_colors.reserve(initial_gradient_ids.size()); - for (const unsigned int id : initial_gradient_ids) { - if (id >= 1 && id <= m_palette.size()) - corner_colors.emplace_back(m_palette[id - 1]); - } - if (corner_colors.size() >= 3) - m_blend_selector->set_multi_preview(corner_colors, *m_selected_weight_state); - } - } - - if (m_mix_preview) { - if (Slic3r::mixed_filament_supports_bias_apparent_color(m_mf, m_preview_settings, m_bias_mode_enabled) && - m_mf.component_a >= 1 && m_mf.component_b >= 1 && - m_mf.component_a <= m_physical_colors.size() && m_mf.component_b <= m_physical_colors.size()) { - const auto [apparent_pct_a, apparent_pct_b] = - Slic3r::mixed_filament_apparent_pair_percentages(m_mf, m_preview_settings, m_nozzle_diameters, m_bias_mode_enabled); - m_mf.display_color = MixedFilamentManager::blend_color( - m_physical_colors[size_t(m_mf.component_a - 1)], - m_physical_colors[size_t(m_mf.component_b - 1)], - apparent_pct_a, - apparent_pct_b); - } - - const std::string bias_summary = - mixed_filament_apparent_pair_summary(m_mf, m_preview_settings, m_nozzle_diameters, m_bias_mode_enabled); - const std::string summary = bias_summary.empty() ? summarize_sequence(initial_sequence) : bias_summary; - std::vector preview_surface_offsets(m_palette.size() + 1, 0.0); - if (m_bias_mode_enabled && m_mf.component_a >= 1 && m_mf.component_a < preview_surface_offsets.size()) - preview_surface_offsets[m_mf.component_a] = double(m_mf.component_a_surface_offset); - if (m_bias_mode_enabled && m_mf.component_b >= 1 && m_mf.component_b < preview_surface_offsets.size()) - preview_surface_offsets[m_mf.component_b] = double(m_mf.component_b_surface_offset); - m_mix_preview->set_data(m_palette, initial_sequence, same_layer_mode, preview_surface_offsets, wxColour(m_mf.display_color), - _L("Preview"), summary.empty() ? wxString() : from_u8(summary)); - } - update_local_z_breakdown(); -} - -void MixedFilamentConfigPanel::update_local_z_breakdown() -{ - if (!m_breakdown_label) - return; - - std::vector weights = *m_selected_weight_state; - const std::vector ids = decode_gradient_ids(m_mf.gradient_component_ids); - if (!ids.empty()) - weights = normalize_gradient_weights(weights, ids.size()); - - const std::string breakdown = summarize_local_z_breakdown(m_mf, weights, m_preview_settings); - m_breakdown_label->SetLabel(from_u8(breakdown)); - m_breakdown_label->Wrap(FromDIP(360)); - m_breakdown_label->Show(!breakdown.empty()); - Layout(); -} - -class MixedFilamentDragHandle : public wxPanel -{ -public: - MixedFilamentDragHandle(wxWindow *parent, const wxColour &dot_color, const wxColour &bg_color) - : wxPanel(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxBORDER_NONE) - , m_dot_color(dot_color) - { - const wxSize handle_size = parent ? parent->FromDIP(wxSize(14, 18)) : wxSize(14, 18); - SetMinSize(handle_size); - SetMaxSize(handle_size); - SetInitialSize(handle_size); - SetBackgroundStyle(wxBG_STYLE_PAINT); - SetBackgroundColour(bg_color); - SetCursor(wxCursor(wxCURSOR_SIZING)); - Bind(wxEVT_PAINT, &MixedFilamentDragHandle::on_paint, this); - } - - void set_colors(const wxColour &dot_color, const wxColour &bg_color) - { - m_dot_color = dot_color; - SetBackgroundColour(bg_color); - Refresh(); - } - -private: - void on_paint(wxPaintEvent &) - { - wxAutoBufferedPaintDC dc(this); - dc.SetBackground(wxBrush(GetBackgroundColour())); - dc.Clear(); - dc.SetPen(*wxTRANSPARENT_PEN); - dc.SetBrush(wxBrush(m_dot_color)); - - const wxSize size = GetClientSize(); - const int radius = std::max(1, FromDIP(1)); - const int left_x = std::max(radius, size.x / 2 - FromDIP(2)); - const int right_x = std::min(size.x - radius - 1, size.x / 2 + FromDIP(2)); - const int top_y = std::max(radius + 1, size.y / 2 - FromDIP(5)); - const int gap_y = FromDIP(4); - - for (int row = 0; row < 3; ++row) { - const int y = top_y + row * gap_y; - dc.DrawCircle(wxPoint(left_x, y), radius); - dc.DrawCircle(wxPoint(right_x, y), radius); - } - } - - wxColour m_dot_color; -}; - -static std::vector build_mixed_filament_ui_indices(const std::vector &mixed, - const std::vector &preferred_order) -{ - std::vector ordered_indices; - std::vector used(mixed.size(), false); - - for (const uint64_t stable_id : preferred_order) { - for (size_t idx = 0; idx < mixed.size(); ++idx) { - const MixedFilament &entry = mixed[idx]; - if (used[idx] || entry.deleted || entry.stable_id != stable_id) - continue; - used[idx] = true; - ordered_indices.emplace_back(idx); - break; - } - } - - for (size_t idx = 0; idx < mixed.size(); ++idx) { - if (used[idx] || mixed[idx].deleted) - continue; - ordered_indices.emplace_back(idx); - } - - return ordered_indices; -} - -void Sidebar::init_color_mix_panel(wxWindow* parent, wxSizer* sizer) -{ - // Title bar - p->m_panel_color_mix_title = new StaticBox(parent, wxID_ANY, wxDefaultPosition, - wxDefaultSize, wxTAB_TRAVERSAL | wxBORDER_NONE); - p->m_panel_color_mix_title->SetBackgroundColor(wxColour(248, 248, 248)); - p->m_panel_color_mix_title->SetBackgroundColor2(0xF1F1F1); - p->m_panel_color_mix_title->SetMinSize(wxSize(-1, FromDIP(30))); - p->m_panel_color_mix_title->SetMaxSize(wxSize(-1, FromDIP(30))); - - p->m_color_mix_icon = new ScalableButton(p->m_panel_color_mix_title, wxID_ANY, "color_palette"); - auto* label = new Label(p->m_panel_color_mix_title, _L("Color Mixing"), LB_PROPAGATE_MOUSE_EVENT); - - p->m_btn_del_color_mix = new ScalableButton(p->m_panel_color_mix_title, wxID_ANY, "delete_filament"); - p->m_btn_add_color_mix = new ScalableButton(p->m_panel_color_mix_title, wxID_ANY, "add_filament"); - - auto* h_title = new wxBoxSizer(wxHORIZONTAL); - auto* white_left_c = new wxPanel(p->m_panel_color_mix_title, wxID_ANY, wxDefaultPosition, wxSize(FromDIP(SidebarProps::ContentMargin()), -1)); - white_left_c->SetBackgroundColour(StateColor::darkModeColorFor(*wxWHITE)); - h_title->Add(white_left_c, 0, wxEXPAND | wxTOP | wxBOTTOM, 0); - h_title->Add(p->m_color_mix_icon, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(SidebarProps::TitlebarMargin())); - h_title->AddSpacer(FromDIP(SidebarProps::ElementSpacing())); - h_title->Add(label, 0, wxALIGN_CENTER_VERTICAL); - h_title->AddStretchSpacer(); - h_title->Add(p->m_btn_del_color_mix, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(4)); - h_title->Add(p->m_btn_add_color_mix, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(8)); - auto* white_right_c = new wxPanel(p->m_panel_color_mix_title, wxID_ANY, wxDefaultPosition, wxSize(FromDIP(SidebarProps::ContentMargin()), -1)); - white_right_c->SetBackgroundColour(StateColor::darkModeColorFor(*wxWHITE)); - h_title->Add(white_right_c, 0, wxEXPAND | wxTOP | wxBOTTOM, 0); - p->m_panel_color_mix_title->SetSizer(h_title); - p->m_panel_color_mix_title->Layout(); - - // Scrolled window for content with max height of 3 rows - p->m_scrolled_color_mix = new wxScrolledWindow(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL); - p->m_scrolled_color_mix->SetScrollRate(0, 5); - p->m_scrolled_color_mix->SetBackgroundColour(StateColor::darkModeColorFor(*wxWHITE)); - - // Content panel — match physical filament content panel (sizer set dynamically in update) - p->m_panel_color_mix_content = new wxPanel(p->m_scrolled_color_mix, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL); - p->m_panel_color_mix_content->SetBackgroundColour(StateColor::darkModeColorFor(*wxWHITE)); - - // Add content panel to scrolled window - auto* scrolled_sizer = new wxBoxSizer(wxVERTICAL); - scrolled_sizer->Add(p->m_panel_color_mix_content, 0, wxEXPAND); - p->m_scrolled_color_mix->SetSizer(scrolled_sizer); - - sizer->Add(p->m_panel_color_mix_title, 0, wxEXPAND, 0); - sizer->Add(p->m_scrolled_color_mix, 0, wxEXPAND, 0); - - // Add button: open dialog to create new mix - p->m_btn_add_color_mix->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { - PresetBundle* pb = wxGetApp().preset_bundle; - if (!pb) return; - - auto* co = pb->project_config.option("filament_colour"); - const std::vector colors = co ? co->values : std::vector{}; - if (colors.size() < 2) return; - - MixedFilamentDialog dlg(wxGetApp().mainframe, colors); - if (dlg.ShowModal() != wxID_OK) return; - - auto& mgr = pb->mixed_filaments; - if (mgr.total_filaments(colors.size()) >= MAXIMUM_FILAMENT_NUMBER) return; - const MixedFilament& r = dlg.GetResult(); - mgr.add_custom_filament(r.component_a, r.component_b, r.mix_b_percent, colors); - auto& mfs = mgr.mixed_filaments(); - if (!mfs.empty()) { - mfs.back().distribution_mode = r.distribution_mode; - mfs.back().manual_pattern = r.manual_pattern; - mfs.back().gradient_component_ids = r.gradient_component_ids; - mfs.back().gradient_component_weights = r.gradient_component_weights; - mfs.back().ratio_a = r.ratio_a; - mfs.back().ratio_b = r.ratio_b; - mfs.back().local_z_max_sublayers = r.local_z_max_sublayers; - mfs.back().gradient_enabled = r.gradient_enabled; - mfs.back().gradient_start = r.gradient_start; - mfs.back().gradient_end = r.gradient_end; - mfs.back().display_color = r.display_color; - mfs.back().ui_mode = r.ui_mode; - mfs.back().custom = true; - } - if (auto* opt = pb->project_config.option("mixed_filament_definitions")) - opt->value = mgr.serialize_custom_entries(); - wxGetApp().plater()->post_slice_state_change_update(); - wxGetApp().plater()->on_filaments_change(p->combos_filament.size()); - update_color_mix_panel(); - m_scrolled_sizer->Layout(); - }); - - // Delete button: remove last custom entry - p->m_btn_del_color_mix->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { - PresetBundle* pb = wxGetApp().preset_bundle; - if (!pb) return; - - auto& mgr = pb->mixed_filaments; - auto& mfs = mgr.mixed_filaments(); - for (int i = static_cast(mfs.size()) - 1; i >= 0; --i) { - if (mfs[i].custom && !mfs[i].deleted) { - mfs[i].deleted = true; - break; - } - } - if (auto* opt = pb->project_config.option("mixed_filament_definitions")) - opt->value = mgr.serialize_custom_entries(); - wxGetApp().plater()->post_slice_state_change_update(); - wxGetApp().plater()->on_filaments_change(p->combos_filament.size()); - update_color_mix_panel(); - m_scrolled_sizer->Layout(); - }); - - // Initial visibility: hide if fewer than 2 physical filaments - update_color_mix_panel(); -} - -void Sidebar::update_color_mix_panel() -{ - if (!p->m_panel_color_mix_content) return; - - auto* co = wxGetApp().preset_bundle - ? wxGetApp().preset_bundle->project_config.option("filament_colour") - : nullptr; - const int n_physical = co ? static_cast(co->values.size()) : 0; - const bool show = (n_physical >= 2); - p->m_panel_color_mix_title->Show(show); - p->m_scrolled_color_mix->Show(show); - if (!show) { - return; - } - - wxWindowUpdateLocker no_updates(p->m_panel_color_mix_content); - p->m_panel_color_mix_content->DestroyChildren(); - - auto* preset_bundle = wxGetApp().preset_bundle; - const size_t num_physical = p->combos_filament.size(); - - std::vector physical_colors = co->values; - physical_colors.resize(num_physical, "#26A69A"); - - std::vector nozzle_diameters(num_physical, 0.4); - if (const ConfigOptionFloats* opt = preset_bundle->printers.get_edited_preset().config.option("nozzle_diameter")) { - const size_t opt_count = opt->values.size(); - if (opt_count > 0) - for (size_t i = 0; i < num_physical; ++i) - nozzle_diameters[i] = std::max(0.05, opt->get_at(unsigned(std::min(i, opt_count - 1)))); - } - - float lower_bound = 0.04f, upper_bound = 0.16f; - if (preset_bundle->project_config.has("mixed_filament_height_lower_bound")) - lower_bound = std::max(0.01f, float(preset_bundle->project_config.opt_float("mixed_filament_height_lower_bound"))); - if (preset_bundle->project_config.has("mixed_filament_height_upper_bound")) - upper_bound = std::max(lower_bound, float(preset_bundle->project_config.opt_float("mixed_filament_height_upper_bound"))); - - bool local_z_mode = false; - if (const ConfigOptionBool* opt = preset_bundle->project_config.option("dithering_local_z_mode")) - local_z_mode = opt->value; - - bool component_bias_enabled = false; - if (const ConfigOptionBool* opt = preset_bundle->project_config.option("mixed_filament_component_bias_enabled")) - component_bias_enabled = opt->value; - - const MixedFilamentPreviewSettings preview_settings { - 0.2f, lower_bound, upper_bound, 0.f, 0.f, local_z_mode, false, 1 - }; - const MixedFilamentDisplayContext display_context { - num_physical, physical_colors, nozzle_diameters, preview_settings, component_bias_enabled - }; - preset_bundle->mixed_filaments.set_display_context(display_context); - - auto& mfs = preset_bundle->mixed_filaments.mixed_filaments(); - bool any_visible = false; - for (const MixedFilament& mf : mfs) - if (!mf.deleted) { any_visible = true; break; } - - if (!any_visible) { - p->m_scrolled_color_mix->Hide(); - p->m_btn_del_color_mix->Hide(); - p->m_btn_add_color_mix->SetBitmap_("icon_add_circle"); - m_scrolled_sizer->Layout(); - return; - } - p->m_scrolled_color_mix->Show(); - p->m_btn_del_color_mix->Show(); - p->m_btn_add_color_mix->SetBitmap_("add_filament"); - - // 2-column grid matching the physical filament panel layout - auto* grid_sizer = new wxBoxSizer(wxHORIZONTAL); - auto* col0 = new wxBoxSizer(wxVERTICAL); - auto* col1 = new wxBoxSizer(wxVERTICAL); - grid_sizer->Add(col0, 1, wxEXPAND); - grid_sizer->Add(col1, 1, wxEXPAND); - - int visible_idx = 0; - for (size_t i = 0; i < mfs.size(); ++i) { - MixedFilament& mf = mfs[i]; - if (mf.deleted) continue; - - const std::string synced_color = compute_mixed_filament_display_color(mf, display_context); - if (mf.display_color != synced_color) - mf.display_color = synced_color; - - const int virtual_id = static_cast(num_physical) + visible_idx + 1; - - // Badge button: colored background + virtual filament number with gradient support - auto* badge = new MixedFilamentBadge(p->m_panel_color_mix_content, wxID_ANY, - virtual_id, mf, display_context); - - const std::string normalized_pattern_cm = MixedFilamentManager::normalize_manual_pattern(mf.manual_pattern); - std::vector gradient_ids = MixedFilamentManager::decode_gradient_component_ids(mf.gradient_component_ids, 0); - const bool z_gradient_tile = mf.gradient_enabled && mf.component_a != mf.component_b - && normalized_pattern_cm.empty() && gradient_ids.size() < 3; - wxString lbl; - if (!normalized_pattern_cm.empty()) - lbl = wxString(summarize_cycle_pattern_text(normalized_pattern_cm, mf, int(num_physical))); - else if (gradient_ids.size() >= 3) { - // parse weights - const size_t n = gradient_ids.size(); - std::vector weights; - { - std::string token; - for (const char c : mf.gradient_component_weights) { - if (c >= '0' && c <= '9') { token.push_back(c); continue; } - if (!token.empty()) { weights.emplace_back(std::max(0, std::atoi(token.c_str()))); token.clear(); } - } - if (!token.empty()) weights.emplace_back(std::max(0, std::atoi(token.c_str()))); - if (weights.size() != n) weights.assign(n, int(100 / n)); - } - // normalize to 100 - int sum = 0; for (int v : weights) sum += v; - if (sum <= 0) { weights.assign(n, 0); weights[0] = 100; sum = 100; } - for (size_t k = 0; k < n; ++k) { - const unsigned int fid = gradient_ids[k]; - const int pct = int(std::round(100.0 * weights[k] / sum)); - if (k > 0) lbl += "+"; - lbl += wxString::Format("F%u %d%%", fid, pct); - } - } else if (z_gradient_tile) { - const unsigned from_id = - mf.gradient_start >= mf.gradient_end ? mf.component_a : mf.component_b; - const unsigned to_id = - mf.gradient_start >= mf.gradient_end ? mf.component_b : mf.component_a; - lbl = wxString::Format("F%u->F%u", from_id, to_id); - } else { - const int pct_b = std::clamp(mf.mix_b_percent, 0, 100); - const int pct_a = 100 - pct_b; - lbl = wxString::Format("F%u %d%%+F%u %d%%", mf.component_a, pct_a, mf.component_b, pct_b); - if (mf.distribution_mode != int(MixedFilament::Simple)) - for (unsigned int fid : gradient_ids) - lbl += wxString::Format("+F%u", fid); - } - - bool has_error = !is_filament_compatible(mf); - - // Create a panel with border for the text - auto* name_panel = new wxPanel(p->m_panel_color_mix_content, wxID_ANY); - name_panel->SetBackgroundColour(StateColor::darkModeColorFor(*wxWHITE)); - - auto* name_sizer = new wxBoxSizer(wxHORIZONTAL); - - // Add error icon if there's an error - if (has_error) { - name_sizer->AddSpacer(FromDIP(8)); - ScalableBitmap error_bmp(name_panel, "error_icon_red_exclamation", 14); - auto* error_icon = new wxStaticBitmap(name_panel, wxID_ANY, error_bmp.bmp()); - name_sizer->Add(error_icon, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(4)); - } - - auto* name_btn = new wxStaticText(name_panel, wxID_ANY, lbl, wxDefaultPosition, wxDefaultSize, 0); - name_btn->SetBackgroundColour(StateColor::darkModeColorFor(*wxWHITE)); - name_btn->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#262E30"))); - name_btn->SetCursor(wxCursor(wxCURSOR_HAND)); - name_btn->SetMinSize(wxSize(0, -1)); // allow sizer to shrink below text width - - int name_flags = wxEXPAND | (has_error ? (wxTOP | wxBOTTOM | wxRIGHT) : wxALL); - name_sizer->Add(name_btn, 1, name_flags, FromDIP(8)); - name_panel->SetSizer(name_sizer); - name_panel->SetMinSize(wxSize(name_panel->FromDIP(100), name_panel->FromDIP(30))); - name_panel->SetMaxSize(wxSize(-1, name_panel->FromDIP(30))); - - // Use wxControl::Ellipsize on resize to match combo truncation behavior - name_panel->Bind(wxEVT_SIZE, [lbl, name_btn, name_panel](wxSizeEvent& evt) { - name_panel->Layout(); // force sizer layout so name_btn has its current size - int avail = name_btn->GetSize().x; - if (avail > 0) { - wxClientDC dc(name_btn); - dc.SetFont(name_btn->GetFont()); - wxString ellipsized = wxControl::Ellipsize(lbl, dc, wxELLIPSIZE_END, avail); - name_btn->SetLabel(ellipsized); - } - evt.Skip(); - }); - - // Add border to the panel - name_panel->Bind(wxEVT_PAINT, [](wxPaintEvent& evt) { - wxPanel* panel = dynamic_cast(evt.GetEventObject()); - if (!panel) return; - - wxPaintDC dc(panel); - wxRect rect = panel->GetClientRect(); - - // Draw border matching combo (#dbdbdb, 1px) - dc.SetPen(wxPen(StateColor::darkModeColorFor(wxColour(0xdb, 0xdb, 0xdb)), 1)); - dc.SetBrush(*wxTRANSPARENT_BRUSH); - dc.DrawRectangle(rect); - }); - - name_btn->SetToolTip(lbl); - name_btn->Bind(wxEVT_LEFT_DOWN, [this, i](wxMouseEvent&) { - auto* co = wxGetApp().preset_bundle->project_config.option("filament_colour"); - const std::vector colors = co ? co->values : std::vector{}; - if (colors.size() < 2) return; - auto& mgr = wxGetApp().preset_bundle->mixed_filaments; - auto& mfs2 = mgr.mixed_filaments(); - if (i >= mfs2.size()) return; - MixedFilamentDialog dlg(wxGetApp().mainframe, colors, mfs2[i]); - if (dlg.ShowModal() != wxID_OK) return; - const MixedFilament& r = dlg.GetResult(); - mfs2[i].component_a = r.component_a; - mfs2[i].component_b = r.component_b; - mfs2[i].mix_b_percent = r.mix_b_percent; - mfs2[i].distribution_mode = r.distribution_mode; - mfs2[i].manual_pattern = r.manual_pattern; - mfs2[i].gradient_component_ids = r.gradient_component_ids; - mfs2[i].gradient_component_weights = r.gradient_component_weights; - mfs2[i].ratio_a = r.ratio_a; - mfs2[i].ratio_b = r.ratio_b; - mfs2[i].local_z_max_sublayers = r.local_z_max_sublayers; - mfs2[i].gradient_enabled = r.gradient_enabled; - mfs2[i].gradient_start = r.gradient_start; - mfs2[i].gradient_end = r.gradient_end; - mfs2[i].display_color = r.display_color; - mfs2[i].ui_mode = r.ui_mode; - mfs2[i].custom = true; - if (auto* opt = wxGetApp().preset_bundle->project_config.option("mixed_filament_definitions")) - opt->value = mgr.serialize_custom_entries(); - wxGetApp().plater()->post_slice_state_change_update(); - wxGetApp().plater()->on_filaments_change(p->combos_filament.size()); - wxWeakRef weak_this(this); - wxTheApp->CallAfter([weak_this]() { - Sidebar* sidebar = weak_this.get(); - if (sidebar) { - sidebar->update_color_mix_panel(); - sidebar->m_scrolled_sizer->Layout(); - } - }); - }); - - auto* menu_btn = new ScalableButton(p->m_panel_color_mix_content, wxID_ANY, "menu_filament"); - menu_btn->SetToolTip(_L("Options")); - menu_btn->Bind(wxEVT_BUTTON, [this, i, visible_idx, num_physical, menu_btn](wxCommandEvent&) { - wxMenu menu; - const int edit_id = wxWindow::NewControlId(); - const int del_id = wxWindow::NewControlId(); - const int merge_to_id = wxWindow::NewControlId(); - - menu.Append(edit_id, _L("Edit")); - menu.Bind(wxEVT_MENU, [this, i](wxCommandEvent&) { - auto* co = wxGetApp().preset_bundle->project_config.option("filament_colour"); - const std::vector colors = co ? co->values : std::vector{}; - if (colors.size() < 2) return; - auto& mgr = wxGetApp().preset_bundle->mixed_filaments; - auto& mfs2 = mgr.mixed_filaments(); - if (i >= mfs2.size()) return; - MixedFilamentDialog dlg(wxGetApp().mainframe, colors, mfs2[i]); - if (dlg.ShowModal() != wxID_OK) return; - const MixedFilament& r = dlg.GetResult(); - mfs2[i].component_a = r.component_a; - mfs2[i].component_b = r.component_b; - mfs2[i].mix_b_percent = r.mix_b_percent; - mfs2[i].distribution_mode = r.distribution_mode; - mfs2[i].manual_pattern = r.manual_pattern; - mfs2[i].gradient_component_ids = r.gradient_component_ids; - mfs2[i].gradient_component_weights = r.gradient_component_weights; - mfs2[i].ratio_a = r.ratio_a; - mfs2[i].ratio_b = r.ratio_b; - mfs2[i].local_z_max_sublayers = r.local_z_max_sublayers; - mfs2[i].gradient_enabled = r.gradient_enabled; - mfs2[i].gradient_start = r.gradient_start; - mfs2[i].gradient_end = r.gradient_end; - mfs2[i].ui_mode = r.ui_mode; - mfs2[i].custom = true; - if (auto* opt = wxGetApp().preset_bundle->project_config.option("mixed_filament_definitions")) - opt->value = mgr.serialize_custom_entries(); - wxGetApp().plater()->post_slice_state_change_update(); - wxGetApp().plater()->on_filaments_change(p->combos_filament.size()); - wxWeakRef weak_this(this); - wxTheApp->CallAfter([weak_this]() { - Sidebar* sidebar = weak_this.get(); - if (sidebar) { - sidebar->update_color_mix_panel(); - sidebar->m_scrolled_sizer->Layout(); - } - }); - }, edit_id); - - // Add "Merge with" submenu - allows merging to any other filament (physical or mixed) - // Build list of all available target filaments - wxMenu* merge_submenu = new wxMenu(); - - // Get physical filament icons - std::vector icons = get_extruder_color_icons(true); - - // Add physical filaments as targets - for (size_t phys_idx = 0; phys_idx < num_physical; ++phys_idx) { - const int target_id = wxWindow::NewControlId(); - auto preset = wxGetApp().preset_bundle->filaments.find_preset(wxGetApp().preset_bundle->filament_presets[phys_idx]); - wxString target_label = preset ? from_u8(preset->label(false)) : wxString::Format(_L("Filament %d"), phys_idx + 1); - - // Use icon if available - wxMenuItem* item = nullptr; - if (phys_idx < icons.size() && icons[phys_idx]) { - item = new wxMenuItem(merge_submenu, target_id, target_label); - item->SetBitmap(*icons[phys_idx]); - merge_submenu->Append(item); - } else { - merge_submenu->Append(target_id, target_label); - } - - merge_submenu->Bind(wxEVT_MENU, [this, visible_idx, phys_idx, num_physical](wxCommandEvent&) { - // Source: mixed filament with visible_idx (0-based in visible list) - // Target: physical filament phys_idx (0-based) - // Mixed filament virtual ID = num_physical + visible_idx + 1 (1-based), convert to 0-based - size_t source_virtual_id = num_physical + visible_idx; - change_filament(source_virtual_id, phys_idx); - }, target_id); - } - - // Add other mixed filaments as targets - auto& mgr = wxGetApp().preset_bundle->mixed_filaments; - auto& mfs_for_menu = mgr.mixed_filaments(); - const size_t total_mixed = mfs_for_menu.size(); - - // Get icon dimensions for mixed filaments - const double em = Slic3r::GUI::wxGetApp().em_unit(); - const int icon_width = lround(2 * em); - const int icon_height = lround(2 * em); - - size_t target_visible_idx = 0; - for (size_t j = 0; j < total_mixed; ++j) { - if (mfs_for_menu[j].deleted) continue; - - // Skip self (compare by visible index) - if (target_visible_idx == visible_idx) { - target_visible_idx++; - continue; - } - - const int target_virtual_id = static_cast(num_physical) + target_visible_idx + 1; - const wxString target_label = wxString::Format(_L("Mixed Filament %d"), target_virtual_id); - const int target_id = wxWindow::NewControlId(); - - // Create colored bitmap for mixed filament — gradient filaments get a gradient icon - MixedFilamentDisplayContext menu_ctx; - { - auto* co2 = wxGetApp().preset_bundle->project_config.option("filament_colour"); - menu_ctx.physical_colors = co2 ? co2->values : std::vector{}; - menu_ctx.num_physical = num_physical; - } - wxBitmap* mixed_bmp = create_mixed_filament_menu_bitmap( - mfs_for_menu[j], menu_ctx, icon_width, icon_height, - wxString::Format("%d", target_virtual_id)); - - wxMenuItem* item = new wxMenuItem(merge_submenu, target_id, target_label); - item->SetBitmap(*mixed_bmp); - merge_submenu->Append(item); - - merge_submenu->Bind(wxEVT_MENU, [this, visible_idx, target_visible_idx, num_physical](wxCommandEvent&) { - // Source: mixed filament with visible_idx (0-based in visible list) - // Target: mixed filament with target_visible_idx (0-based in visible list) - // Virtual ID (0-based) = num_physical + visible_idx - size_t source_virtual_id = num_physical + visible_idx; - size_t target_virtual_id = num_physical + target_visible_idx; - change_filament(source_virtual_id, target_virtual_id); - }, target_id); - - target_visible_idx++; - } - - menu.AppendSubMenu(merge_submenu, _L("Merge with")); - - menu.Append(del_id, _L("Delete")); - menu.Bind(wxEVT_MENU, [this, i, num_physical](wxCommandEvent&) { - auto& mgr2 = wxGetApp().preset_bundle->mixed_filaments; - auto& mfs2 = mgr2.mixed_filaments(); - const std::vector old_mixed = mfs2; - if (i < mfs2.size()) { mfs2[i].deleted = true; mfs2[i].enabled = false; } - if (auto* opt = wxGetApp().preset_bundle->project_config.option("mixed_filament_definitions")) - opt->value = mgr2.serialize_custom_entries(); - wxGetApp().preset_bundle->update_mixed_filament_id_remap(old_mixed, num_physical, num_physical, i); - wxGetApp().plater()->post_slice_state_change_update(); - wxGetApp().plater()->on_filaments_change(num_physical); - wxWeakRef weak_this(this); - wxTheApp->CallAfter([weak_this]() { - Sidebar* sidebar = weak_this.get(); - if (sidebar) { - sidebar->update_color_mix_panel(); - sidebar->m_scrolled_sizer->Layout(); - } - }); - }, del_id); - - wxPoint pt{0, menu_btn->GetSize().GetHeight()}; - pt = menu_btn->ClientToScreen(pt); - pt = wxGetApp().mainframe->ScreenToClient(pt); - wxGetApp().mainframe->PopupMenu(&menu, pt); - }); - - auto* cell = new wxBoxSizer(wxHORIZONTAL); - // Match physical filament asymmetric layout: left column gets left spacer, right doesn't - if (visible_idx % 2 == 0) - cell->AddSpacer(FromDIP(SidebarProps::ContentMargin())); - cell->Add(badge, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(SidebarProps::ElementSpacing()) - FromDIP(2)); - cell->Add(name_panel, 1, wxEXPAND | wxALL, FromDIP(2)); - cell->Add(menu_btn, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(SidebarProps::ElementSpacing()) - FromDIP(2)); - cell->AddSpacer(FromDIP(SidebarProps::ContentMargin())); - - (visible_idx % 2 == 0 ? col0 : col1)->Add(cell, 0, wxEXPAND | wxBOTTOM, FromDIP(4)); - - ++visible_idx; - } - - // If odd count, pad right column so left column doesn't stretch - if (visible_idx % 2 == 1) - col1->AddStretchSpacer(1); - - // Wrap grid in vertical sizer for padding, then set as panel sizer (matches physical structure) - auto* wrapper = new wxBoxSizer(wxVERTICAL); - wrapper->Add(grid_sizer, 0, wxEXPAND | wxTOP | wxBOTTOM, FromDIP(8)); - p->m_panel_color_mix_content->SetSizer(wrapper); - p->m_panel_color_mix_content->Layout(); - - // Dynamic height: grow with rows up to 3, only cap when > 3 rows - const wxSize content_best = p->m_panel_color_mix_content->GetBestSize(); - const int row_count = (visible_idx + 1) / 2; // rows (2 columns) - const int desired_h = row_count > 3 - ? (content_best.GetHeight() / row_count) * 3 - : content_best.GetHeight(); - p->m_scrolled_color_mix->SetMinSize({-1, desired_h}); - p->m_scrolled_color_mix->SetMaxSize({-1, desired_h}); - - m_scrolled_sizer->Layout(); - wxWeakRef weak_this(this); - wxTheApp->CallAfter([weak_this]() { - Sidebar* sidebar = weak_this.get(); - if (sidebar && sidebar->p && sidebar->p->m_scrolled_color_mix) { - int vh = sidebar->p->m_scrolled_color_mix->GetVirtualSize().y; - int ch = sidebar->p->m_scrolled_color_mix->GetClientSize().y; - sidebar->p->m_scrolled_color_mix->Scroll(0, std::max(0, vh - ch)); - } - }); - p->m_panel_color_mix_content->Refresh(); - - // Disable add buttons when combined filament limit reached - if (preset_bundle) { - const bool can_add = preset_bundle->mixed_filaments.total_filaments(num_physical) < MAXIMUM_FILAMENT_NUMBER; - if (p->m_bpButton_add_filament) - p->m_bpButton_add_filament->Enable(can_add); - if (p->m_btn_add_color_mix) - p->m_btn_add_color_mix->Enable(can_add); - } -} - -void Sidebar::update_mixed_filament_panel(bool sync_manager) -{ - // Check for new collapsible structure - if (!p->m_panel_mixed_filaments_title || !p->m_panel_mixed_filaments_content) - return; - - wxWindowUpdateLocker noUpdates_sidebar(this); - wxWindowUpdateLocker noUpdates_mixed_panel(p->m_panel_mixed_filaments_content); - - auto refresh_model_canvas_colors = []() { - Plater *plater = wxGetApp().plater(); - if (plater == nullptr) - return; - - auto refresh_canvas = [](GLCanvas3D *canvas) { - if (canvas == nullptr || !canvas->is_initialized()) - return; - canvas->update_volumes_colors_by_extruder(); - canvas->render(); - }; - - refresh_canvas(plater->get_view3D_canvas3D()); - refresh_canvas(plater->get_assmeble_canvas3D()); - }; - - int prev_rows_view_y = 0; - for (wxWindow *child : p->m_panel_mixed_filaments_content->GetChildren()) { - if (auto *scrolled = dynamic_cast(child)) { - int tmp_x = 0; - scrolled->GetViewStart(&tmp_x, &prev_rows_view_y); - break; - } - } - - auto *preset_bundle = wxGetApp().preset_bundle; - if (!preset_bundle) - return; - DynamicPrintConfig *print_cfg = &preset_bundle->prints.get_edited_preset().config; - - const size_t num_physical = p->combos_filament.size(); - ConfigOptionStrings *color_opt = preset_bundle->project_config.option("filament_colour"); - std::vector physical_colors = color_opt ? color_opt->values : std::vector(); - physical_colors.resize(num_physical, "#26A69A"); - std::vector nozzle_diameters(num_physical, 0.4); - if (const ConfigOptionFloats *opt = preset_bundle->printers.get_edited_preset().config.option("nozzle_diameter")) { - const size_t opt_count = opt->values.size(); - if (opt_count > 0) { - for (size_t i = 0; i < num_physical; ++i) - nozzle_diameters[i] = std::max(0.05, opt->get_at(unsigned(std::min(i, opt_count - 1)))); - } - } - - auto get_mixed_bool = [preset_bundle, print_cfg](const std::string &key, bool fallback) { - if (const ConfigOptionBool *opt = preset_bundle->project_config.option(key)) - return opt->value; - if (const ConfigOptionInt *opt = preset_bundle->project_config.option(key)) - return opt->value != 0; - if (print_cfg) { - if (const ConfigOptionBool *opt = print_cfg->option(key)) - return opt->value; - if (const ConfigOptionInt *opt = print_cfg->option(key)) - return opt->value != 0; - } - return fallback; - }; - auto get_mixed_mode = [preset_bundle, print_cfg](bool fallback) { - if (const ConfigOptionBool *opt = preset_bundle->project_config.option("mixed_filament_gradient_mode")) - return opt->value; - if (const ConfigOptionInt *opt = preset_bundle->project_config.option("mixed_filament_gradient_mode")) - return opt->value != 0; - if (print_cfg) { - if (const ConfigOptionBool *opt = print_cfg->option("mixed_filament_gradient_mode")) - return opt->value; - if (const ConfigOptionInt *opt = print_cfg->option("mixed_filament_gradient_mode")) - return opt->value != 0; - } - return fallback; - }; - auto get_mixed_float = [preset_bundle, print_cfg](const std::string &key, float fallback) { - if (preset_bundle->project_config.has(key)) - return float(preset_bundle->project_config.opt_float(key)); - if (print_cfg && print_cfg->has(key)) - return float(print_cfg->opt_float(key)); - return fallback; - }; - auto get_mixed_string = [preset_bundle, print_cfg](const std::string &key, const std::string &fallback = std::string()) { - std::string project_value; - if (preset_bundle->project_config.has(key)) - project_value = preset_bundle->project_config.opt_string(key); - if (!project_value.empty()) - return project_value; - if (print_cfg && print_cfg->has(key)) { - const std::string print_value = print_cfg->opt_string(key); - if (!print_value.empty()) - return print_value; - } - return project_value.empty() ? fallback : project_value; - }; - auto set_mixed_float = [preset_bundle, print_cfg](const std::string &key, float value) { - if (print_cfg) { - if (ConfigOptionFloat *opt = print_cfg->option(key)) - opt->value = value; - } - if (ConfigOptionFloat *opt = preset_bundle->project_config.option(key)) - opt->value = value; - else - preset_bundle->project_config.set_key_value(key, new ConfigOptionFloat(value)); - }; - auto set_mixed_string = [preset_bundle, print_cfg](const std::string &key, const std::string &value) { - if (print_cfg) { - if (ConfigOptionString *opt = print_cfg->option(key)) - opt->value = value; - } - if (ConfigOptionString *opt = preset_bundle->project_config.option(key)) - opt->value = value; - else - preset_bundle->project_config.set_key_value(key, new ConfigOptionString(value)); - }; - auto set_mixed_bool = [preset_bundle, print_cfg](const std::string &key, bool value) { - if (print_cfg) { - if (ConfigOptionBool *opt = print_cfg->option(key)) - opt->value = value; - else if (ConfigOptionInt *opt = print_cfg->option(key)) - opt->value = value ? 1 : 0; - } - if (ConfigOptionBool *opt = preset_bundle->project_config.option(key)) - opt->value = value; - else if (ConfigOptionInt *opt = preset_bundle->project_config.option(key)) - opt->value = value ? 1 : 0; - else - preset_bundle->project_config.set_key_value(key, new ConfigOptionBool(value)); - }; - auto set_mixed_mode = [preset_bundle, print_cfg](bool enabled) { - if (print_cfg) { - if (ConfigOptionBool *opt = print_cfg->option("mixed_filament_gradient_mode")) - opt->value = enabled; - else if (ConfigOptionInt *opt = print_cfg->option("mixed_filament_gradient_mode")) - opt->value = enabled ? 1 : 0; - } - if (ConfigOptionBool *opt = preset_bundle->project_config.option("mixed_filament_gradient_mode")) - opt->value = enabled; - else if (ConfigOptionInt *opt = preset_bundle->project_config.option("mixed_filament_gradient_mode")) - opt->value = enabled ? 1 : 0; - else - preset_bundle->project_config.set_key_value("mixed_filament_gradient_mode", new ConfigOptionBool(enabled)); - }; - auto notify_mixed_change = [print_cfg]() { - if (!print_cfg) - return; - if (auto *print_tab = wxGetApp().get_tab(Preset::TYPE_PRINT)) - print_tab->update_dirty(); - if (wxGetApp().mainframe) - wxGetApp().mainframe->on_config_changed(print_cfg); - }; - auto decode_gradient_ids = [](const std::string &encoded) { - return MixedFilamentManager::decode_gradient_component_ids(encoded, 0); - }; - auto encode_gradient_ids = [](const std::vector &ids) { - return MixedFilamentManager::encode_gradient_component_ids(ids); - }; - auto decode_gradient_weights = [](const std::string &encoded, size_t expected_count) { - std::vector out; - if (encoded.empty() || expected_count == 0) - return out; - std::string token; - for (const char c : encoded) { - if (c >= '0' && c <= '9') { - token.push_back(c); - continue; - } - if (!token.empty()) { - out.emplace_back(std::max(0, std::atoi(token.c_str()))); - token.clear(); - } - } - if (!token.empty()) - out.emplace_back(std::max(0, std::atoi(token.c_str()))); - if (out.size() != expected_count) - out.clear(); - return out; - }; - auto normalize_gradient_weights = [](const std::vector &weights, size_t n) { - std::vector out = weights; - if (out.size() != n) - out.assign(n, (n > 0) ? int(100 / n) : 0); - int sum = 0; - for (int &v : out) { - v = std::max(0, v); - sum += v; - } - if (sum <= 0 && n > 0) { - out.assign(n, 0); - out[0] = 100; - return out; - } - std::vector rem(n, 0.); - int assigned = 0; - for (size_t i = 0; i < n; ++i) { - const double exact = 100.0 * double(out[i]) / double(sum); - out[i] = int(std::floor(exact)); - rem[i] = exact - double(out[i]); - assigned += out[i]; - } - int missing = std::max(0, 100 - assigned); - while (missing > 0) { - size_t best_idx = 0; - double best_rem = -1.0; - for (size_t i = 0; i < rem.size(); ++i) { - if (rem[i] > best_rem) { - best_rem = rem[i]; - best_idx = i; - } - } - ++out[best_idx]; - rem[best_idx] = 0.0; - --missing; - } - return out; - }; - auto encode_gradient_weights = [](const std::vector &weights) { - std::ostringstream ss; - for (size_t i = 0; i < weights.size(); ++i) { - if (i > 0) - ss << '/'; - ss << std::max(0, weights[i]); - } - return ss.str(); - }; - auto build_weighted_multi_sequence = [normalize_gradient_weights](const std::vector &ids, - const std::vector &weights, - size_t max_cycle_limit) { - if (ids.empty()) - return std::vector(); - - std::vector filtered_ids; - std::vector counts; - filtered_ids.reserve(ids.size()); - counts.reserve(ids.size()); - - std::vector normalized = normalize_gradient_weights(weights, ids.size()); - for (size_t i = 0; i < ids.size(); ++i) { - const int weight = (i < normalized.size()) ? std::max(0, normalized[i]) : 0; - if (weight <= 0) - continue; - filtered_ids.emplace_back(ids[i]); - counts.emplace_back(weight); - } - if (filtered_ids.empty()) { - filtered_ids = ids; - counts.assign(ids.size(), 1); - } - - int g = 0; - for (const int c : counts) - g = std::gcd(g, std::max(1, c)); - if (g > 1) { - for (int &c : counts) - c = std::max(1, c / g); - } - - constexpr size_t k_max_cycle = 48; - const size_t effective_cycle_limit = - max_cycle_limit > 0 ? std::min(k_max_cycle, std::max(1, max_cycle_limit)) : k_max_cycle; - reduce_weight_counts_to_cycle_limit(counts, effective_cycle_limit); - - std::vector reduced_ids; - std::vector reduced_counts; - reduced_ids.reserve(filtered_ids.size()); - reduced_counts.reserve(counts.size()); - for (size_t i = 0; i < counts.size(); ++i) { - if (counts[i] <= 0) - continue; - reduced_ids.emplace_back(filtered_ids[i]); - reduced_counts.emplace_back(counts[i]); - } - if (reduced_ids.empty()) - return std::vector(); - filtered_ids = std::move(reduced_ids); - counts = std::move(reduced_counts); - - const int total = std::accumulate(counts.begin(), counts.end(), 0); - if (total <= 0) - return std::vector(filtered_ids.begin(), filtered_ids.end()); - - const size_t cycle = size_t(total); - - std::vector sequence; - sequence.reserve(cycle); - std::vector emitted(counts.size(), 0); - for (size_t pos = 0; pos < cycle; ++pos) { - size_t best_idx = 0; - double best_score = -1e9; - for (size_t i = 0; i < counts.size(); ++i) { - const double target = double(pos + 1) * double(counts[i]) / double(total); - const double score = target - double(emitted[i]); - if (score > best_score) { - best_score = score; - best_idx = i; - } - } - ++emitted[best_idx]; - sequence.emplace_back(filtered_ids[best_idx]); - } - if (sequence.empty()) - sequence = filtered_ids; - return sequence; - }; - auto decode_manual_pattern_ids = [num_physical](const std::string &pattern, - unsigned int component_a, - unsigned int component_b, - size_t wall_loops) { - return build_grouped_manual_pattern_preview_sequence(pattern, component_a, component_b, num_physical, wall_loops); - }; - const bool height_weighted_mode = get_mixed_mode(false); - int gradient_mode = height_weighted_mode ? 1 : 0; - float lower_bound = std::max(0.01f, get_mixed_float("mixed_filament_height_lower_bound", 0.04f)); - float upper_bound = std::max(lower_bound, get_mixed_float("mixed_filament_height_upper_bound", 0.16f)); - float preferred_local_z_a = std::max(0.f, get_mixed_float("mixed_color_layer_height_a", 0.f)); - float preferred_local_z_b = std::max(0.f, get_mixed_float("mixed_color_layer_height_b", 0.f)); - float nominal_layer_height = 0.2f; - if (print_cfg && print_cfg->has("layer_height")) - nominal_layer_height = float(print_cfg->opt_float("layer_height")); - nominal_layer_height = std::max(0.01f, nominal_layer_height); - size_t wall_loops = 1; - if (print_cfg && print_cfg->has("wall_loops")) - wall_loops = std::max(1, size_t(std::max(1, print_cfg->opt_int("wall_loops")))); - const bool local_z_mode = get_mixed_bool("dithering_local_z_mode", false); - const bool local_z_direct_multicolor = - get_mixed_bool("dithering_local_z_direct_multicolor", false) && - preferred_local_z_a <= EPSILON && - preferred_local_z_b <= EPSILON; - const bool component_bias_enabled = get_mixed_bool("mixed_filament_component_bias_enabled", false); - float pointillism_pixel_size = std::max(0.f, get_mixed_float("mixed_filament_pointillism_pixel_size", 0.f)); - float pointillism_line_gap = std::max(0.f, get_mixed_float("mixed_filament_pointillism_line_gap", 0.f)); - float mixed_surface_indentation = std::clamp(get_mixed_float("mixed_filament_surface_indentation", 0.f), -2.f, 2.f); - bool advanced_dithering = get_mixed_bool("mixed_filament_advanced_dithering", false); - const std::string mixed_definitions = get_mixed_string("mixed_filament_definitions"); - const MixedFilamentPreviewSettings preview_settings { - nominal_layer_height, - lower_bound, - upper_bound, - preferred_local_z_a, - preferred_local_z_b, - local_z_mode, - local_z_direct_multicolor, - wall_loops - }; - const MixedFilamentDisplayContext display_context { - num_physical, - physical_colors, - nozzle_diameters, - preview_settings, - component_bias_enabled - }; - auto summarize_sequence = [num_physical](const std::vector &sequence) { - if (sequence.empty() || num_physical == 0) - return std::string(); - std::vector counts(num_physical + 1, size_t(0)); - size_t total = 0; - for (const unsigned int id : sequence) { - if (id == 0 || id > num_physical) - continue; - ++counts[id]; - ++total; - } - if (total == 0) - return std::string(); - std::ostringstream ss; - bool first = true; - for (size_t id = 1; id <= num_physical; ++id) { - if (counts[id] == 0) - continue; - const int pct = int(std::lround(100.0 * double(counts[id]) / double(total))); - if (!first) - ss << " "; - first = false; - ss << "F" << id << ":" << pct << "%"; - } - return ss.str(); - }; - auto compute_entry_display_color = [display_context](const MixedFilament &entry) { - return compute_mixed_filament_display_color(entry, display_context); - }; - - auto &mixed_mgr = preset_bundle->mixed_filaments; - mixed_mgr.set_display_context(display_context); - if (sync_manager) { - mixed_mgr.auto_generate(physical_colors); - mixed_mgr.clear_custom_entries(); - mixed_mgr.load_custom_entries(mixed_definitions, physical_colors); - mixed_mgr.apply_gradient_settings(gradient_mode, lower_bound, upper_bound, advanced_dithering); - } - - if (component_bias_enabled) { - for (MixedFilament &entry : mixed_mgr.mixed_filaments()) { - const float bias_value = mixed_filament_single_surface_offset_value(entry, nozzle_diameters); - const auto balanced_pair = mixed_filament_single_surface_offset_pair(entry, bias_value, nozzle_diameters); - entry.component_a_surface_offset = balanced_pair.first; - entry.component_b_surface_offset = balanced_pair.second; - } - } - - // During project load, sidebar may refresh before physical filament combos - // finish syncing. Avoid overwriting persisted mixed definitions while the - // physical filament set is incomplete. - if (num_physical >= 2) { - set_mixed_mode(height_weighted_mode); - set_mixed_bool("mixed_filament_component_bias_enabled", component_bias_enabled); - set_mixed_float("mixed_filament_height_lower_bound", lower_bound); - set_mixed_float("mixed_filament_height_upper_bound", upper_bound); - set_mixed_float("mixed_color_layer_height_a", preferred_local_z_a); - set_mixed_float("mixed_color_layer_height_b", preferred_local_z_b); - set_mixed_float("mixed_filament_pointillism_pixel_size", pointillism_pixel_size); - set_mixed_float("mixed_filament_pointillism_line_gap", pointillism_line_gap); - set_mixed_float("mixed_filament_surface_indentation", mixed_surface_indentation); - set_mixed_string("mixed_filament_definitions", mixed_mgr.serialize_custom_entries()); - } - - auto &mixed = mixed_mgr.mixed_filaments(); - const std::vector ordered_mixed_indices = build_mixed_filament_ui_indices(mixed, p->m_mixed_filament_ui_order); - std::vector sanitized_mixed_ui_order_ids; - sanitized_mixed_ui_order_ids.reserve(ordered_mixed_indices.size()); - for (const size_t mixed_id : ordered_mixed_indices) { - if (mixed_id < mixed.size() && mixed[mixed_id].stable_id != 0) - sanitized_mixed_ui_order_ids.emplace_back(mixed[mixed_id].stable_id); - } - p->m_mixed_filament_ui_order = std::move(sanitized_mixed_ui_order_ids); - - p->m_mixed_filament_drag_active = false; - p->m_mixed_filament_drag_source_mixed_id = size_t(-1); - p->m_mixed_filament_row_bindings.clear(); - - const int compact_gap_x = FromDIP(6); - const int compact_gap_y = FromDIP(4); - const int compact_row_pad = FromDIP(6); - const bool is_dark = wxGetApp().dark_mode(); - const wxColour mixed_rows_bg = is_dark ? wxColour(45, 45, 49) : wxColour(246, 248, 251); - const wxColour mixed_row_bg = is_dark ? wxColour(52, 52, 56) : wxColour(255, 255, 255); - const wxColour mixed_row_hover_bg = is_dark ? wxColour(62, 62, 68) : wxColour(241, 247, 255); - const wxColour mixed_text_fg = is_dark ? wxColour(232, 232, 232) : wxColour(20, 20, 20); - const wxColour mixed_summary_fg = is_dark ? wxColour(182, 182, 182) : wxColour(96, 96, 96); - p->m_panel_mixed_filaments_content->SetBackgroundColour(mixed_rows_bg); - - // Get the content sizer and clear it - wxSizer *content_sizer = p->m_panel_mixed_filaments_content->GetSizer(); - if (content_sizer) - content_sizer->Clear(true); - - // Re-add the top margin spacer that was added in constructor but cleared above - if (content_sizer) - content_sizer->AddSpacer(FromDIP(SidebarProps::ContentMargin())); - - // Update button states (buttons are now in title bar, created in constructor) - if (p->m_btn_add_gradient) - p->m_btn_add_gradient->Enable(num_physical >= 2); - if (p->m_btn_add_pattern) - p->m_btn_add_pattern->Enable(num_physical >= 2); - if (p->m_btn_add_color) - p->m_btn_add_color->Enable(num_physical >= 2); - - // Mixed Filaments panel is hidden - p->m_panel_mixed_filaments_title->Hide(); - p->m_panel_mixed_filaments_content->Hide(); - Layout(); - refresh_model_canvas_colors(); - wxWeakRef weakSelf(this); - wxTheApp->CallAfter([weakSelf]() { - if (weakSelf) weakSelf->update_color_mix_panel(); - }); - return; - -#if 0 // Mixed Filaments panel UI — hidden, preserved for potential future re-enablement - - // Reset the max size in case it was collapsed - p->m_panel_mixed_filaments_content->SetMaxSize({-1, -1}); - - auto *rows_scroller = new wxScrolledWindow(p->m_panel_mixed_filaments_content, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL | wxTAB_TRAVERSAL); - rows_scroller->SetScrollRate(0, FromDIP(6)); - rows_scroller->ShowScrollbars(wxSHOW_SB_NEVER, wxSHOW_SB_DEFAULT); - rows_scroller->SetBackgroundColour(mixed_rows_bg); - auto *rows_sizer = new wxBoxSizer(wxVERTICAL); - rows_scroller->SetSizer(rows_sizer); - - if (mixed.empty()) { - auto *empty_label = new wxStaticText(rows_scroller, wxID_ANY, - _L("No mixed filaments yet. Use Add Gradient, Add Pattern, or Add Color to create one.")); - empty_label->SetForegroundColour(mixed_summary_fg); - empty_label->SetFont(::Label::Body_13); - empty_label->Wrap(FromDIP(360)); - rows_sizer->Add(empty_label, 0, wxALL | wxEXPAND, FromDIP(12)); - rows_scroller->Layout(); - rows_scroller->FitInside(); - const int empty_content_h = empty_label->GetBestSize().GetHeight() + FromDIP(28); - const int empty_rows_h = std::max(FromDIP(86), empty_content_h); - rows_scroller->SetMinSize(wxSize(-1, empty_rows_h)); - rows_scroller->SetMaxSize(wxSize(-1, empty_rows_h)); - if (content_sizer) - content_sizer->Add(rows_scroller, 0, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, FromDIP(SidebarProps::ContentMargin())); - p->m_panel_mixed_filaments_content->Layout(); - Layout(); - refresh_model_canvas_colors(); - update_color_mix_panel(); - return; - } - - auto adjust_rows_scroller_height = [this, rows_scroller]() { - if (!rows_scroller) - return; - const int min_h = FromDIP(68); - const int collapsed_max_h = FromDIP(220); - int two_rows_cap_h = collapsed_max_h; - const auto &children = rows_scroller->GetChildren(); - if (!children.empty()) { - std::vector heights; - heights.reserve(children.GetCount()); - for (wxWindowList::compatibility_iterator it = children.GetFirst(); it; it = it->GetNext()) { - wxWindow *child = it->GetData(); - wxPanel *panel = dynamic_cast(child); - if (!panel) - continue; - heights.emplace_back(std::max(panel->GetSize().GetHeight(), panel->GetBestSize().GetHeight())); - } - if (!heights.empty()) { - std::sort(heights.begin(), heights.end(), std::greater()); - const size_t keep = std::min(2, heights.size()); - int rows_h = 0; - for (size_t i = 0; i < keep; ++i) - rows_h += heights[i]; - if (keep > 1) - rows_h += int(keep - 1) * FromDIP(2); - rows_h += FromDIP(8); - two_rows_cap_h = std::max(collapsed_max_h, rows_h); - } - } - - const int max_h = p->m_expanded_mixed_filament_rows.empty() ? collapsed_max_h : two_rows_cap_h; - const int content_h = std::max(0, rows_scroller->GetVirtualSize().GetHeight()); - const int desired_h = std::clamp(content_h, min_h, max_h); - rows_scroller->SetMinSize(wxSize(-1, desired_h)); - rows_scroller->SetMaxSize(wxSize(-1, desired_h)); - }; - - for (auto it = p->m_expanded_mixed_filament_rows.begin(); it != p->m_expanded_mixed_filament_rows.end();) { - if (*it >= mixed.size() || mixed[*it].deleted) - it = p->m_expanded_mixed_filament_rows.erase(it); - else - ++it; - } - - std::vector palette; - palette.reserve(physical_colors.size()); - for (const std::string &hex : physical_colors) - palette.emplace_back(parse_mixed_color(hex)); - - auto mixed_summary_text = [decode_gradient_ids](const MixedFilament &entry) { - const std::string normalized_pattern = MixedFilamentManager::normalize_manual_pattern(entry.manual_pattern); - if (!entry.custom) - return wxString::Format("(Filament %u + Filament %u)", unsigned(entry.component_a), unsigned(entry.component_b)); - if (!normalized_pattern.empty()) - return _L("(Pattern)"); - if (decode_gradient_ids(entry.gradient_component_ids).size() >= 3) - return _L("(Color)"); - return wxString::Format("(F%u + F%u)", unsigned(entry.component_a), unsigned(entry.component_b)); - }; - - auto apply_mixed_entry_changes = [this, preset_bundle, print_cfg, num_physical](size_t mixed_id, - const MixedFilament &updated_mf, - bool preserve_enabled = false, - bool rebuild_virtual_id_remap = false) { - if (!preset_bundle) - return; - - auto &mgr = preset_bundle->mixed_filaments; - auto &mfs = mgr.mixed_filaments(); - if (mixed_id >= mfs.size()) - return; - - const std::vector old_mixed = rebuild_virtual_id_remap ? mfs : std::vector(); - MixedFilament merged = updated_mf; - if (preserve_enabled) - merged.enabled = mfs[mixed_id].enabled; - mfs[mixed_id] = merged; - - const std::string serialized = mgr.serialize_custom_entries(); - if (print_cfg) { - if (ConfigOptionString *opt = print_cfg->option("mixed_filament_definitions")) - opt->value = serialized; - else - print_cfg->set_key_value("mixed_filament_definitions", new ConfigOptionString(serialized)); - } - if (ConfigOptionString *opt = preset_bundle->project_config.option("mixed_filament_definitions")) - opt->value = serialized; - else - preset_bundle->project_config.set_key_value("mixed_filament_definitions", new ConfigOptionString(serialized)); - - if (print_cfg) { - if (auto *print_tab = wxGetApp().get_tab(Preset::TYPE_PRINT)) - print_tab->update_dirty(); - if (wxGetApp().mainframe) - wxGetApp().mainframe->on_config_changed(print_cfg); - } - if (wxGetApp().plater()) - wxGetApp().plater()->update_project_dirty_from_presets(); - - if (rebuild_virtual_id_remap) - preset_bundle->update_mixed_filament_id_remap(old_mixed, num_physical, num_physical); - - int mode = 0; - if (const ConfigOptionBool *opt = preset_bundle->project_config.option("mixed_filament_gradient_mode")) - mode = opt->value ? 1 : 0; - else if (const ConfigOptionInt *opt = preset_bundle->project_config.option("mixed_filament_gradient_mode")) - mode = opt->value != 0 ? 1 : 0; - float lo = preset_bundle->project_config.has("mixed_filament_height_lower_bound") ? - float(preset_bundle->project_config.opt_float("mixed_filament_height_lower_bound")) : 0.04f; - float hi = preset_bundle->project_config.has("mixed_filament_height_upper_bound") ? - float(preset_bundle->project_config.opt_float("mixed_filament_height_upper_bound")) : 0.16f; - bool advanced = false; - if (const ConfigOptionBool *opt = preset_bundle->project_config.option("mixed_filament_advanced_dithering")) - advanced = opt->value; - mode = std::clamp(mode, 0, 1); - lo = std::max(0.01f, lo); - hi = std::max(lo, hi); - mgr.apply_gradient_settings(mode, lo, hi, advanced); - update_dynamic_filament_list(); - - if (rebuild_virtual_id_remap && wxGetApp().plater()) { - p->m_skip_mixed_filament_sync_once = true; - wxGetApp().plater()->on_filaments_change(num_physical); - } - }; - - auto current_mixed_filament_ui_order = [this, &mixed]() { - std::vector ordered_ids; - ordered_ids.reserve(p->m_mixed_filament_row_bindings.size()); - for (const auto &binding : p->m_mixed_filament_row_bindings) { - if (binding.mixed_id < mixed.size() && mixed[binding.mixed_id].stable_id != 0) - ordered_ids.emplace_back(mixed[binding.mixed_id].stable_id); - } - return ordered_ids; - }; - - auto drop_insert_position = [this]() { - const wxPoint mouse_pos = wxGetMousePosition(); - size_t visible_idx = 0; - for (const auto &binding : p->m_mixed_filament_row_bindings) { - if (binding.row == nullptr || !binding.row->IsShown()) - continue; - - const wxPoint top_left = binding.row->ClientToScreen(wxPoint(0, 0)); - const int row_h = std::max(binding.row->GetSize().GetHeight(), binding.row->GetBestSize().GetHeight()); - const int center_y = top_left.y + row_h / 2; - if (mouse_pos.y < center_y) - return visible_idx; - - ++visible_idx; - } - return visible_idx; - }; - - for (size_t display_mixed_idx = 0; display_mixed_idx < ordered_mixed_indices.size(); ++display_mixed_idx) { - const size_t mixed_id = ordered_mixed_indices[display_mixed_idx]; - MixedFilament &mf = mixed[mixed_id]; - const bool auto_row = !mf.custom; - - auto *row = new wxPanel(rows_scroller, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxBORDER_NONE); - row->SetBackgroundColour(mixed_row_bg); - auto *row_sizer = new wxBoxSizer(wxVERTICAL); - p->m_mixed_filament_row_bindings.push_back({mixed_id, row}); - - auto *header_panel = new wxPanel(row, wxID_ANY); - header_panel->SetBackgroundColour(mixed_row_bg); - auto *header_sizer = new wxBoxSizer(wxHORIZONTAL); - - const std::string synced_color = compute_entry_display_color(mf); - if (mf.display_color != synced_color) - mf.display_color = synced_color; - auto *drag_handle = new MixedFilamentDragHandle(header_panel, mixed_summary_fg, mixed_row_bg); - drag_handle->SetToolTip(_L("Drag to reorder mixed filaments in this panel.")); - header_sizer->Add(drag_handle, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, compact_gap_x); - - wxColour swatch_color = parse_mixed_color(mf.display_color); - auto *swatch = new wxPanel(header_panel, wxID_ANY, wxDefaultPosition, wxSize(FromDIP(12), FromDIP(12))); - swatch->SetBackgroundColour(swatch_color); - swatch->SetMinSize(wxSize(FromDIP(12), FromDIP(12))); - header_sizer->Add(swatch, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, compact_gap_x); - - const int virtual_filament_id = int(num_physical + display_mixed_idx + 1); - auto *name_label = new wxStaticText(header_panel, wxID_ANY, wxString::Format("Mixed Filament %d", virtual_filament_id)); - name_label->SetForegroundColour(mixed_text_fg); - header_sizer->Add(name_label, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, compact_gap_x); - - auto *summary_label = new wxStaticText(header_panel, wxID_ANY, mixed_summary_text(mf)); - summary_label->SetForegroundColour(mixed_summary_fg); - header_sizer->Add(summary_label, 1, wxALIGN_CENTER_VERTICAL | wxLEFT, compact_gap_x); - - header_sizer->AddStretchSpacer(1); - - auto *enabled_chk = new wxCheckBox(header_panel, wxID_ANY, _L("Enabled")); - enabled_chk->SetValue(mf.enabled); - enabled_chk->SetForegroundColour(mixed_text_fg); - header_sizer->Add(enabled_chk, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, compact_gap_x); - enabled_chk->Bind(wxEVT_LEFT_UP, [](wxMouseEvent &evt) { - evt.StopPropagation(); - evt.Skip(); - }); - enabled_chk->Bind(wxEVT_CHECKBOX, [mixed_id, enabled_chk, apply_mixed_entry_changes, preset_bundle](wxCommandEvent &) { - if (!preset_bundle || !enabled_chk) - return; - auto &mgr = preset_bundle->mixed_filaments; - auto &mfs = mgr.mixed_filaments(); - if (mixed_id >= mfs.size()) - return; - MixedFilament updated = mfs[mixed_id]; - updated.enabled = enabled_chk->GetValue(); - apply_mixed_entry_changes(mixed_id, updated, false, true); - }); - - auto *del_btn = new ScalableButton(header_panel, wxID_ANY, "cross"); - del_btn->SetToolTip(_L("Delete mixed filament")); - header_sizer->Add(del_btn, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, compact_gap_x); - - del_btn->Bind(wxEVT_BUTTON, [this, mixed_id, num_physical, set_mixed_string, notify_mixed_change](wxCommandEvent&) { - if (wxGetApp().preset_bundle) { - auto &mgr = wxGetApp().preset_bundle->mixed_filaments; - auto &mfs = mgr.mixed_filaments(); - if (mixed_id < mfs.size()) { - const std::vector old_mixed = mfs; - auto canonical_pair = [](unsigned int a, unsigned int b) { - return std::make_pair(std::min(a, b), std::max(a, b)); - }; - MixedFilament &target = mfs[mixed_id]; - const auto target_pair = canonical_pair(target.component_a, target.component_b); - const bool valid_auto_pair = target_pair.first >= 1 && - target_pair.second >= 1 && - target_pair.first <= num_physical && - target_pair.second <= num_physical && - target_pair.first != target_pair.second; - if (target.custom && target.origin_auto && valid_auto_pair) { - bool tombstoned_existing_auto = false; - for (size_t idx = 0; idx < mfs.size(); ++idx) { - if (idx == mixed_id) - continue; - MixedFilament &candidate = mfs[idx]; - if (candidate.custom) - continue; - if (canonical_pair(candidate.component_a, candidate.component_b) != target_pair) - continue; - candidate.deleted = true; - candidate.enabled = false; - tombstoned_existing_auto = true; - break; - } - - if (tombstoned_existing_auto) { - mfs.erase(mfs.begin() + mixed_id); - } else { - target.component_a = target_pair.first; - target.component_b = target_pair.second; - target.mix_b_percent = 50; - target.ratio_a = 1; - target.ratio_b = 1; - target.manual_pattern.clear(); - target.gradient_component_ids.clear(); - target.gradient_component_weights.clear(); - target.pointillism_all_filaments = false; - target.distribution_mode = int(MixedFilament::Simple); - target.custom = false; - target.origin_auto = true; - target.deleted = true; - target.enabled = false; - } - } else if (target.custom) { - mfs.erase(mfs.begin() + mixed_id); - } else { - target.deleted = true; - target.enabled = false; - } - p->m_expanded_mixed_filament_rows.clear(); - set_mixed_string("mixed_filament_definitions", mgr.serialize_custom_entries()); - wxGetApp().preset_bundle->update_mixed_filament_id_remap(old_mixed, num_physical, num_physical, mixed_id); - notify_mixed_change(); - if (wxGetApp().plater()) - wxGetApp().plater()->update_project_dirty_from_presets(); - if (wxGetApp().plater()) { - p->m_skip_mixed_filament_sync_once = true; - wxGetApp().plater()->on_filaments_change(num_physical); - } - } - } - }); - - header_panel->SetSizer(header_sizer); - row_sizer->Add(header_panel, 0, wxEXPAND | wxALL, 0); - - auto *editor_host = new wxPanel(row, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxBORDER_NONE); - editor_host->SetBackgroundColour(mixed_row_bg); - auto *editor_sizer = new wxBoxSizer(wxVERTICAL); - editor_host->SetSizer(editor_sizer); - editor_host->Hide(); - row_sizer->Add(editor_host, 0, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, compact_row_pad); - - auto set_row_hover = [row, header_panel, editor_host, drag_handle, mixed_summary_fg, mixed_row_bg, mixed_row_hover_bg](bool hovered) { - const wxColour bg = hovered ? mixed_row_hover_bg : mixed_row_bg; - if (row) row->SetBackgroundColour(bg); - if (header_panel) header_panel->SetBackgroundColour(bg); - if (editor_host) editor_host->SetBackgroundColour(bg); - if (drag_handle) drag_handle->set_colors(mixed_summary_fg, bg); - if (row) row->Refresh(); - if (header_panel) header_panel->Refresh(); - if (editor_host) editor_host->Refresh(); - }; - - auto row_contains_mouse = [row]() { - if (!row) - return false; - const wxPoint mouse_pos = wxGetMousePosition(); - const wxPoint local = row->ScreenToClient(mouse_pos); - return row->GetClientRect().Contains(local); - }; - - auto ensure_editor = [this, mixed_id, num_physical, physical_colors, nozzle_diameters, palette, preview_settings, component_bias_enabled, preset_bundle, - editor_host, editor_sizer, swatch, summary_label, header_panel, row, - rows_scroller, mixed_summary_text, apply_mixed_entry_changes]() { - if (!preset_bundle || !editor_sizer || editor_sizer->GetItemCount() > 0) - return; - - auto &mgr = preset_bundle->mixed_filaments; - auto &mfs = mgr.mixed_filaments(); - if (mixed_id >= mfs.size()) - return; - - auto *editor = new MixedFilamentConfigPanel(editor_host, mixed_id, mfs[mixed_id], num_physical, physical_colors, nozzle_diameters, palette, preview_settings, - component_bias_enabled, - [this, mixed_id, swatch, summary_label, header_panel, row, rows_scroller, mixed_summary_text, apply_mixed_entry_changes](const MixedFilament &updated_mf) { - apply_mixed_entry_changes(mixed_id, updated_mf, true); - - if (swatch) { - swatch->SetBackgroundColour(parse_mixed_color(updated_mf.display_color)); - swatch->Refresh(); - } - if (summary_label) { - summary_label->SetLabel(mixed_summary_text(updated_mf)); - } - if (header_panel) - header_panel->Layout(); - if (row) - row->Layout(); - if (rows_scroller) { - rows_scroller->Layout(); - rows_scroller->FitInside(); - } - }); - - editor_sizer->Add(editor, 0, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, FromDIP(4)); - editor_host->Layout(); - }; - - auto toggle_editor = [this, mixed_id, editor_host, ensure_editor, rows_scroller, adjust_rows_scroller_height]() { - if (!editor_host || !rows_scroller) - return; - - if (editor_host->IsShown()) { - editor_host->Hide(); - p->m_expanded_mixed_filament_rows.erase(mixed_id); - } else { - ensure_editor(); - editor_host->Show(); - p->m_expanded_mixed_filament_rows.insert(mixed_id); - } - - rows_scroller->Layout(); - rows_scroller->FitInside(); - adjust_rows_scroller_height(); - p->m_panel_mixed_filaments_content->Layout(); - m_scrolled_sizer->Layout(); - Layout(); - }; - - auto bind_toggle_target = [&toggle_editor](wxWindow *target) { - if (!target) - return; - target->SetCursor(wxCursor(wxCURSOR_HAND)); - target->Bind(wxEVT_LEFT_UP, [toggle_editor](wxMouseEvent &) { - toggle_editor(); - }); - }; - - auto bind_hover_target = [set_row_hover, row_contains_mouse](wxWindow *target) { - if (!target) - return; - target->Bind(wxEVT_ENTER_WINDOW, [set_row_hover](wxMouseEvent &evt) { - set_row_hover(true); - evt.Skip(); - }); - target->Bind(wxEVT_LEAVE_WINDOW, [set_row_hover, row_contains_mouse](wxMouseEvent &evt) { - set_row_hover(row_contains_mouse()); - evt.Skip(); - }); - }; - - auto release_drag_capture = [this]() { - p->m_mixed_filament_drag_active = false; - p->m_mixed_filament_drag_source_mixed_id = size_t(-1); - }; - - auto bind_drag_target = [this, - mixed_id, - &mixed, - drop_insert_position, - current_mixed_filament_ui_order, - release_drag_capture](wxWindow *target) { - if (!target) - return; - - target->Bind(wxEVT_LEFT_DOWN, [this, mixed_id, target](wxMouseEvent &evt) { - if (!target) - return; - p->m_mixed_filament_drag_active = true; - p->m_mixed_filament_drag_source_mixed_id = mixed_id; - if (!target->HasCapture()) - target->CaptureMouse(); - evt.StopPropagation(); - }); - - target->Bind(wxEVT_MOTION, [this](wxMouseEvent &evt) { - if (p->m_mixed_filament_drag_active) - evt.StopPropagation(); - }); - - target->Bind(wxEVT_LEFT_UP, [this, &mixed, target, drop_insert_position, current_mixed_filament_ui_order](wxMouseEvent &evt) { - if (target && target->HasCapture()) - target->ReleaseMouse(); - - if (!p->m_mixed_filament_drag_active || p->m_mixed_filament_drag_source_mixed_id >= mixed.size()) { - p->m_mixed_filament_drag_active = false; - p->m_mixed_filament_drag_source_mixed_id = size_t(-1); - evt.StopPropagation(); - return; - } - - const size_t source_mixed_id = p->m_mixed_filament_drag_source_mixed_id; - p->m_mixed_filament_drag_active = false; - p->m_mixed_filament_drag_source_mixed_id = size_t(-1); - - std::vector current_mixed_ids; - current_mixed_ids.reserve(p->m_mixed_filament_row_bindings.size()); - for (const auto &binding : p->m_mixed_filament_row_bindings) { - if (binding.mixed_id < mixed.size() && !mixed[binding.mixed_id].deleted) - current_mixed_ids.emplace_back(binding.mixed_id); - } - - const auto source_it = std::find(current_mixed_ids.begin(), current_mixed_ids.end(), source_mixed_id); - if (source_it == current_mixed_ids.end()) { - evt.StopPropagation(); - return; - } - - const size_t source_pos = size_t(std::distance(current_mixed_ids.begin(), source_it)); - size_t insert_pos = drop_insert_position(); - insert_pos = std::min(insert_pos, current_mixed_ids.size()); - - current_mixed_ids.erase(source_it); - if (insert_pos > source_pos) - --insert_pos; - insert_pos = std::min(insert_pos, current_mixed_ids.size()); - current_mixed_ids.insert(current_mixed_ids.begin() + ptrdiff_t(insert_pos), source_mixed_id); - - std::vector reordered_stable_ids; - reordered_stable_ids.reserve(current_mixed_ids.size()); - for (const size_t row_mixed_id : current_mixed_ids) { - if (row_mixed_id < mixed.size() && mixed[row_mixed_id].stable_id != 0) - reordered_stable_ids.emplace_back(mixed[row_mixed_id].stable_id); - } - - if (reordered_stable_ids != current_mixed_filament_ui_order()) { - p->m_mixed_filament_ui_order = std::move(reordered_stable_ids); - update_mixed_filament_panel(false); - } - - evt.StopPropagation(); - }); - - target->Bind(wxEVT_MOUSE_CAPTURE_LOST, [release_drag_capture](wxMouseCaptureLostEvent &) { - release_drag_capture(); - }); - }; - - header_panel->SetToolTip(auto_row ? - _L("Click to edit automatic mixed filament settings (saved as custom).") : - _L("Click to expand/retract mixed filament settings")); - bind_toggle_target(row); - bind_toggle_target(header_panel); - bind_toggle_target(name_label); - bind_toggle_target(summary_label); - bind_toggle_target(swatch); - bind_hover_target(row); - bind_hover_target(header_panel); - bind_hover_target(name_label); - bind_hover_target(summary_label); - bind_hover_target(swatch); - bind_hover_target(drag_handle); - bind_drag_target(drag_handle); - - del_btn->Bind(wxEVT_LEFT_UP, [](wxMouseEvent &evt) { - evt.StopPropagation(); - evt.Skip(); - }); - - if (p->m_expanded_mixed_filament_rows.count(mixed_id) != 0) { - ensure_editor(); - editor_host->Show(); - } - - row->SetSizer(row_sizer); - rows_sizer->Add(row, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, FromDIP(2)); - rows_sizer->AddSpacer(FromDIP(2)); - } - - rows_sizer->AddSpacer(FromDIP(2)); - rows_scroller->Layout(); - rows_scroller->FitInside(); - adjust_rows_scroller_height(); - if (prev_rows_view_y > 0) - rows_scroller->Scroll(0, prev_rows_view_y); - - content_sizer->Add(rows_scroller, 0, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(2)); - content_sizer->AddSpacer(FromDIP(2)); - p->m_panel_mixed_filaments_content->Layout(); - m_scrolled_sizer->Layout(); - Layout(); - refresh_model_canvas_colors(); - update_color_mix_panel(); -#endif // Mixed Filaments panel UI -} - -std::vector Sidebar::get_ui_ordered_filament_ids() const -{ - const size_t num_physical = static_cast(std::max(wxGetApp().filaments_cnt(), 0)); - std::vector ordered_filament_ids; - ordered_filament_ids.reserve(num_physical); - for (size_t idx = 0; idx < num_physical; ++idx) - ordered_filament_ids.emplace_back(unsigned(idx + 1)); - - if (wxGetApp().preset_bundle == nullptr) - return ordered_filament_ids; - - const auto &mixed = wxGetApp().preset_bundle->mixed_filaments.mixed_filaments(); - if (mixed.empty()) - return ordered_filament_ids; - - const std::vector ordered_mixed_indices = build_mixed_filament_ui_indices(mixed, p->m_mixed_filament_ui_order); - std::vector actual_filament_id_by_mixed_idx(mixed.size(), 0); - unsigned int next_filament_id = unsigned(num_physical + 1); - for (size_t mixed_idx = 0; mixed_idx < mixed.size(); ++mixed_idx) { - if (!mixed[mixed_idx].enabled || mixed[mixed_idx].deleted) - continue; - actual_filament_id_by_mixed_idx[mixed_idx] = next_filament_id++; + ++i; } - ordered_filament_ids.reserve(size_t(next_filament_id - 1)); - for (const size_t mixed_idx : ordered_mixed_indices) { - if (mixed_idx >= actual_filament_id_by_mixed_idx.size()) - continue; - const unsigned int actual_filament_id = actual_filament_id_by_mixed_idx[mixed_idx]; - if (actual_filament_id != 0) - ordered_filament_ids.emplace_back(actual_filament_id); + // remove unused choices if any + remove_unused_filament_combos(num_filaments); + + auto sizer = p->m_panel_filament_title->GetSizer(); + if (p->m_flushing_volume_btn != nullptr && sizer != nullptr) { + if (num_filaments > 1) { + sizer->Show(p->m_flushing_volume_btn); + sizer->Show(p->m_bpButton_del_filament); // ORCA: Show delete filament button if multiple filaments + } else { + sizer->Hide(p->m_flushing_volume_btn); + sizer->Hide(p->m_bpButton_del_filament); // ORCA: Hide delete filament button if there is only one filament + } } - return ordered_filament_ids; + Layout(); + p->m_panel_filament_title->Refresh(); + update_ui_from_settings(); + update_dynamic_filament_list(); } void Sidebar::add_filament() { if (p->combos_filament.size() >= MAXIMUM_EXTRUDER_NUMBER) return; - PresetBundle* pb = wxGetApp().preset_bundle; - if (!pb || pb->mixed_filaments.total_filaments(p->combos_filament.size()) >= MAXIMUM_FILAMENT_NUMBER) return; wxColour new_col = Plater::get_next_color_for_filament(); add_custom_filament(new_col); } @@ -7199,12 +2422,10 @@ void Sidebar::on_filaments_delete(size_t filament_id) { auto& choices = combos_filament(); - p->m_skip_mixed_filament_sync_once = false; - if (filament_id >= choices.size()) return; - if (choices.size() <= 2) + if (choices.size() == 1) choices[0]->GetDropDown().Invalidate(); wxWindowUpdateLocker noUpdates_scrolled_panel(this); @@ -7242,55 +2463,14 @@ void Sidebar::on_filaments_delete(size_t filament_id) sizer->Hide(p->m_flushing_volume_btn); } - if (p->m_bpButton_del_filament != nullptr && p->m_panel_physical_filaments_title != nullptr) { - auto* inner_sizer = p->m_panel_physical_filaments_title->GetSizer(); - if (inner_sizer) { - if (p->combos_filament.size() > 1) - inner_sizer->Show(p->m_bpButton_del_filament); - else - inner_sizer->Hide(p->m_bpButton_del_filament); - } - } - for (size_t idx = filament_id; idx < p->combos_filament.size(); ++idx) { p->combos_filament[idx]->update(); } - // Recalc scrolled filament window height (max 3 rows) - if (p->m_scrolled_filaments && p->m_panel_scrolled_filament_content) { - p->m_panel_scrolled_filament_content->Layout(); - const wxSize content_best = p->m_panel_scrolled_filament_content->GetBestSize(); - const int row_count = ((int)p->combos_filament.size() + 1) / 2; - const int desired_h = row_count > 3 - ? (content_best.GetHeight() / std::max(1, row_count)) * 3 - : content_best.GetHeight(); - p->m_scrolled_filaments->SetMinSize({-1, desired_h}); - p->m_scrolled_filaments->SetMaxSize({-1, desired_h}); - } - Layout(); - wxWeakRef weak_this(this); - wxTheApp->CallAfter([weak_this]() { - Sidebar* sidebar = weak_this.get(); - if (sidebar && sidebar->p && sidebar->p->m_scrolled_filaments) { - int vh = sidebar->p->m_scrolled_filaments->GetVirtualSize().y; - int ch = sidebar->p->m_scrolled_filaments->GetClientSize().y; - sidebar->p->m_scrolled_filaments->Scroll(0, std::max(0, vh - ch)); - } - }); p->m_panel_filament_title->Refresh(); update_ui_from_settings(); - update_dynamic_filament_list(); - update_mixed_filament_panel(); - update_color_mix_panel(); - - if (PresetBundle *pb = wxGetApp().preset_bundle) { - const bool can_add = pb->mixed_filaments.total_filaments(p->combos_filament.size()) < MAXIMUM_FILAMENT_NUMBER; - if (p->m_bpButton_add_filament) - p->m_bpButton_add_filament->Enable(can_add); - if (p->m_btn_add_color_mix) - p->m_btn_add_color_mix->Enable(can_add); - } + dynamic_filament_list.update(); } void Sidebar::edit_filament() { @@ -7300,198 +2480,11 @@ void Sidebar::edit_filament() { p->editing_filament = p->m_menu_filament_id; // sync with TabPresetComboxBox's m_filament_idx } -// Helper function: Check if target mixed filament depends on source physical filament -static bool mixed_filament_uses_physical(const MixedFilament* target_mf, unsigned int source_physical_1based) -{ - if (!target_mf) - return false; - - // Check manual_pattern tokens (resolve order #1) - const std::string norm = MixedFilamentManager::normalize_manual_pattern(target_mf->manual_pattern); - if (!norm.empty()) { - const auto groups = MixedFilamentManager::split_pattern_groups(norm); - for (const std::string &group : groups) { - const auto tokens = MixedFilamentManager::split_pattern_group_to_tokens(group, 0); - for (const std::string &token : tokens) { - if (MixedFilamentManager::physical_filament_from_token(token, *target_mf, MixedFilamentManager::kMaxPhysicalFilaments) == source_physical_1based) - return true; - } - } - } - - // Check gradient components (resolve order #2). - // Only check when there is no manual_pattern; a pattern already resolves - // every token, so gradient IDs would be a false positive at worst. - if (norm.empty()) { - const std::vector ids = MixedFilamentManager::decode_gradient_component_ids(target_mf->gradient_component_ids, 0); - for (unsigned int id : ids) { - if (id == source_physical_1based) - return true; - } - } - - // Check if target mixed filament uses source physical filament as component - // (resolve order #3). Only reached when the mixed filament has no manual_pattern, - // because in cycle mode pattern tokens "1"/"2" already cover component_a/b. - if (norm.empty() && (target_mf->component_a == source_physical_1based || target_mf->component_b == source_physical_1based)) { - return true; - } - return false; -} void Sidebar::change_filament(size_t from_id, size_t to_id) { - // 1. Parameter preprocessing - if (from_id == size_t(-2)) - from_id = p->m_menu_filament_id; - if (from_id == size_t(-1)) - from_id = p->combos_filament.size() - 1; - if (from_id == to_id) - return; - - auto& pb = *wxGetApp().preset_bundle; - const size_t num_physical = pb.filament_presets.size(); - - // 2. Determine source and target types - // Note: filament IDs here are 0-based, but is_mixed expects 1-based - bool from_is_mixed = pb.mixed_filaments.is_mixed((unsigned int)(from_id + 1), num_physical); - bool to_is_mixed = pb.mixed_filaments.is_mixed((unsigned int)(to_id + 1), num_physical); - - // 3. Dependency check: physical → mixed - if (!from_is_mixed && to_is_mixed) { - const MixedFilament* target_mf = pb.mixed_filaments.mixed_filament_from_id((unsigned int)(to_id + 1), num_physical); - unsigned int from_1based = (unsigned int)(from_id + 1); - - if (mixed_filament_uses_physical(target_mf, from_1based)) { - MessageDialog dlg(wxGetApp().plater(), - _L("The target mixed filament uses this physical filament as a component. " - "Merging will remove this physical filament and may invalidate the mixed filament. Continue?"), - _L("Warning"), wxOK | wxCANCEL | wxICON_WARNING); - int ret = dlg.ShowModal(); - if (ret != wxID_OK) - return; - } - } - - // 3b. Check for mixed filaments that depend on the source physical filament - if (!from_is_mixed) { - unsigned int from_1based = (unsigned int)(from_id + 1); - std::vector dependent_mixed_indices = pb.mixed_filaments.mixed_filaments_using_physical(from_1based); - - // If there are dependent mixed filaments, show warning dialog - if (!dependent_mixed_indices.empty()) { - wxString msg = _L("This filament is used in the following mixed filament configurations:\n\n"); - - const auto& mfs = pb.mixed_filaments.mixed_filaments(); - size_t visible_idx = 0; - for (size_t j = 0; j < mfs.size(); ++j) { - if (mfs[j].deleted) continue; - - // Check if this is one of the dependent mixed filaments - bool is_dependent = std::find(dependent_mixed_indices.begin(), dependent_mixed_indices.end(), j) - != dependent_mixed_indices.end(); - - if (is_dependent) { - const int virtual_id = static_cast(num_physical) + visible_idx + 1; - msg += wxString::Format(_L("• Mixed Filament %d\n"), virtual_id); - } - - visible_idx++; - } - - msg += _L("\nMerging this filament will invalidate these mixed filament configurations. Continue?"); - - MessageDialog dlg(wxGetApp().plater(), msg, _L("Warning"), wxOK | wxCANCEL | wxICON_WARNING); - int ret = dlg.ShowModal(); - if (ret != wxID_OK) - return; - } - } - - // 4. Execute merge based on scenario - if (from_is_mixed) { - // Mixed → Physical or Mixed → Mixed - merge_mixed_filament(from_id, to_id); - } else { - // Physical → Physical or Physical → Mixed - delete_filament(from_id, int(to_id)); - } -} - -void Sidebar::merge_mixed_filament(size_t from_id, size_t to_id) -{ - // Merge a mixed filament into another filament (physical or mixed) - // This marks the source as deleted and remaps all objects using it - - PresetBundle* preset_bundle = wxGetApp().preset_bundle; - if (!preset_bundle) return; - auto& pb = *preset_bundle; - const size_t num_physical = pb.filament_presets.size(); - - // Validate parameters - size_t total_filaments = pb.mixed_filaments.total_filaments(num_physical); - if (from_id >= total_filaments || to_id >= total_filaments) { - BOOST_LOG_TRIVIAL(error) << "merge_mixed_filament: Invalid filament ID. from_id=" - << from_id << " to_id=" << to_id << " total=" << total_filaments; - return; - } - - // Prevent self-merge - if (from_id == to_id) { - BOOST_LOG_TRIVIAL(warning) << "merge_mixed_filament: Cannot merge filament into itself: " << from_id; - return; - } - - // Verify source is actually a mixed filament - if (!pb.mixed_filaments.is_mixed((unsigned int)(from_id + 1), num_physical)) { - BOOST_LOG_TRIVIAL(error) << "merge_mixed_filament: Source filament " << from_id - << " is not a mixed filament"; - return; - } - - // Get source mixed filament index - int source_mixed_idx = pb.mixed_filaments.mixed_index_from_filament_id((unsigned int)(from_id + 1), num_physical); - if (source_mixed_idx < 0) { - BOOST_LOG_TRIVIAL(error) << "merge_mixed_filament: Cannot find mixed filament index for ID " << from_id; - return; - } - - auto& mfs = pb.mixed_filaments.mixed_filaments(); - if ((size_t)source_mixed_idx >= mfs.size()) { - BOOST_LOG_TRIVIAL(error) << "merge_mixed_filament: Mixed filament index " << source_mixed_idx - << " out of range (size=" << mfs.size() << ")"; - return; - } - - BOOST_LOG_TRIVIAL(info) << "Merging mixed filament " << from_id << " into filament " << to_id; - - // Build remap table using PresetBundle method - pb.build_merge_filament_remap(from_id, to_id, total_filaments); - - // Mark source mixed filament as deleted - mfs[source_mixed_idx].deleted = true; - mfs[source_mixed_idx].enabled = false; - - // Persist changes - if (auto* opt = pb.project_config.option("mixed_filament_definitions")) - opt->value = pb.mixed_filaments.serialize_custom_entries(); - - // Save mixed snapshot - std::vector is_mixed_snapshot; - if (auto* opt = pb.project_config.option("filament_is_mixed")) - is_mixed_snapshot = opt->values; - - // Update objects to use new filament IDs - size_t total_after = pb.mixed_filaments.total_filaments(num_physical); - wxGetApp().plater()->on_filaments_delete(total_after, from_id, -1, is_mixed_snapshot); - - BOOST_LOG_TRIVIAL(info) << "Mixed filament merge completed. Total filaments after: " << total_after; - - // Update UI - update_color_mix_panel(); - m_scrolled_sizer->Layout(); - wxGetApp().plater()->update(); + delete_filament(from_id, int(to_id)); } void Sidebar::delete_filament(size_t filament_id, int replace_filament_id) @@ -7507,149 +2500,25 @@ void Sidebar::delete_filament(size_t filament_id, int replace_filament_id) filament_id = filament_count; } - size_t total_filaments = wxGetApp().preset_bundle->filament_presets.size(); - if (filament_id > filament_count && filament_id >= total_filaments) + if (filament_id > filament_count) return; - bool is_mixed = (filament_id >= p->combos_filament.size()); - - // Check for mixed filaments that depend on the source physical filament (before deletion) - // Skip this dialog when called from change_filament() (merge path), since change_filament() - // already showed its own confirmation dialog for the same dependent mixed filaments. - if (!is_mixed && replace_filament_id < 0) { - auto& pb = *wxGetApp().preset_bundle; - const size_t num_physical = pb.filament_presets.size(); - unsigned int filament_1based = (unsigned int)(filament_id + 1); - std::vector dependent_mixed_indices = pb.mixed_filaments.mixed_filaments_using_physical(filament_1based); - - // If there are dependent mixed filaments, show warning dialog - if (!dependent_mixed_indices.empty()) { - wxString msg = _L("This filament is used in the following mixed filament configurations:\n\n"); - - const auto& mfs = pb.mixed_filaments.mixed_filaments(); - size_t visible_idx = 0; - for (size_t j = 0; j < mfs.size(); ++j) { - if (mfs[j].deleted) continue; - - // Check if this is one of the dependent mixed filaments - bool is_dependent = std::find(dependent_mixed_indices.begin(), dependent_mixed_indices.end(), j) - != dependent_mixed_indices.end(); - - if (is_dependent) { - const int virtual_id = static_cast(num_physical) + visible_idx + 1; - msg += wxString::Format(_L("• Mixed Filament %d\n"), virtual_id); - } - - visible_idx++; - } - - msg += _L("\nDeleting this filament will invalidate these mixed filament configurations. Continue?"); - - MessageDialog dlg(wxGetApp().plater(), msg, _L("Warning"), wxOK | wxCANCEL | wxICON_WARNING); - int ret = dlg.ShowModal(); - if (ret != wxID_OK) - return; - } - } - - PresetBundle* preset_bundle = wxGetApp().preset_bundle; - if (!preset_bundle) return; - - if (!is_mixed) { - if (preset_bundle->is_the_only_edited_filament(filament_id) || (filament_id == 0)) { - wxGetApp().get_tab(Preset::TYPE_FILAMENT)->select_preset(preset_bundle->filament_presets[0], false, "", true); - } - if (p->editing_filament == filament_id || p->editing_filament >= filament_count) { - p->editing_filament = -1; - } - } - - std::vector is_mixed_snapshot; - if (auto* opt = preset_bundle->project_config.option("filament_is_mixed")) - is_mixed_snapshot = opt->values; - - auto& pb = *preset_bundle; - size_t old_num_physical = pb.filament_presets.size(); - size_t old_total_filaments = pb.mixed_filaments.total_filaments(old_num_physical); - - // Check if this is a physical filament being merged into a mixed filament - bool is_physical_to_mixed_merge = !is_mixed && replace_filament_id >= (int)old_num_physical; - - // Check if target mixed filament depends on source physical filament - bool target_depends_on_source = false; - - if (is_physical_to_mixed_merge) { - // Get target mixed filament - const MixedFilament* target_mf = pb.mixed_filaments.mixed_filament_from_id( - (unsigned int)(replace_filament_id + 1), old_num_physical); - unsigned int source_1based = (unsigned int)(filament_id + 1); - - // Use helper function to check dependency - target_depends_on_source = mixed_filament_uses_physical(target_mf, source_1based); - - BOOST_LOG_TRIVIAL(info) << "Physical to mixed merge: source=" << filament_id - << " target=" << replace_filament_id - << " target_depends_on_source=" << target_depends_on_source; - } - - // Case 2: If target mixed doesn't depend on source physical, use remap mechanism - // This is similar to merge_mixed_filament() - we use a remap table to redirect objects - // to the target mixed filament, then delete the physical filament - if (is_physical_to_mixed_merge && !target_depends_on_source) { - // Build custom remap table that accounts for physical filament deletion - // When a physical filament is deleted, mixed filament virtual IDs change - pb.build_merge_filament_remap(filament_id, replace_filament_id, old_total_filaments, old_num_physical); - - BOOST_LOG_TRIVIAL(info) << "Built custom remap for physical to mixed merge (accounts for virtual ID changes)"; - - // Call on_filaments_delete with -1 to trigger remap usage - // This updates object colors using the remap table - wxGetApp().plater()->on_filaments_delete(old_total_filaments, filament_id, -1, is_mixed_snapshot); - - // Now delete the physical filament - pb.update_num_filaments(filament_id); - pb.consume_last_filament_id_remap(); // discard the remap built by update_num_filaments - wxGetApp().plater()->get_partplate_list().on_filament_deleted( - pb.filament_presets.size(), filament_id); - - BOOST_LOG_TRIVIAL(info) << "Physical to mixed merge completed using custom remap mechanism"; - - // Update UI - wxGetApp().get_tab(Preset::TYPE_PRINT)->update(); - wxGetApp().preset_bundle->export_selections(*wxGetApp().app_config); - wxGetApp().plater()->update(); - return; + if (wxGetApp().preset_bundle->is_the_only_edited_filament(filament_id) || (filament_id == 0)) { + wxGetApp().get_tab(Preset::TYPE_FILAMENT)->select_preset(wxGetApp().preset_bundle->filament_presets[0], false, "", true); } - // Case 1: If target mixed depends on source physical, delete the physical filament - // Let on_filaments_delete use default behavior (reset to default) - else if (is_physical_to_mixed_merge && target_depends_on_source) { - BOOST_LOG_TRIVIAL(info) << "Physical to mixed merge: target depends on source, will delete physical and reset to default"; - } - - // Delete the physical filament (this also removes mixed filaments that depend on it) - pb.update_num_filaments(filament_id); - size_t new_num_physical = pb.filament_presets.size(); - size_t total_after_delete = pb.mixed_filaments.total_filaments(new_num_physical); - wxGetApp().plater()->get_partplate_list().on_filament_deleted(total_after_delete, filament_id); - - int final_replace_id; - if (is_physical_to_mixed_merge && target_depends_on_source) { - // Case 1: target mixed was also deleted by remove_physical_filament - final_replace_id = -1; - } else { - int adjusted = replace_filament_id; - if (replace_filament_id >= (int)old_num_physical) - adjusted = int(new_num_physical + (size_t(replace_filament_id) - old_num_physical)); - else if (replace_filament_id > (int)filament_id) - adjusted = replace_filament_id - 1; - final_replace_id = adjusted; + if (p->editing_filament == filament_id || p->editing_filament >= filament_count) { + p->editing_filament = -1; } - wxGetApp().plater()->on_filaments_delete(total_after_delete, filament_id, - final_replace_id, - is_mixed_snapshot); + wxGetApp().preset_bundle->update_num_filaments(filament_id); + wxGetApp().plater()->get_partplate_list().on_filament_deleted(filament_count, filament_id); + + // wxGetApp().plater()->on_filaments_change(filament_count); + wxGetApp().plater()->on_filaments_delete(filament_count, filament_id, + replace_filament_id > (int) filament_id ? (replace_filament_id - 1) : replace_filament_id); + // wxGetApp().plater()->sidebar().on_filaments_delete(filament_id); wxGetApp().get_tab(Preset::TYPE_PRINT)->update(); wxGetApp().preset_bundle->export_selections(*wxGetApp().app_config); @@ -7658,16 +2527,13 @@ void Sidebar::delete_filament(size_t filament_id, int replace_filament_id) void Sidebar::add_custom_filament(wxColour new_col) { if (p->combos_filament.size() >= MAXIMUM_EXTRUDER_NUMBER) return; - PresetBundle* pb = wxGetApp().preset_bundle; - if (!pb || pb->mixed_filaments.total_filaments(p->combos_filament.size()) >= MAXIMUM_FILAMENT_NUMBER) return; int filament_count = p->combos_filament.size() + 1; - wxGetApp().plater()->confirm_auto_generated_gradients(filament_count); std::string new_color = new_col.GetAsString(wxC2S_HTML_SYNTAX).ToStdString(); - pb->set_num_filaments(filament_count, new_color); + wxGetApp().preset_bundle->set_num_filaments(filament_count, new_color); wxGetApp().plater()->on_filaments_change(filament_count); wxGetApp().get_tab(Preset::TYPE_PRINT)->update(); - pb->export_selections(*wxGetApp().app_config); + wxGetApp().preset_bundle->export_selections(*wxGetApp().app_config); auto_calc_flushing_volumes(filament_count - 1); } @@ -8240,7 +3106,7 @@ void Sidebar::auto_calc_flushing_volumes(const int modify_id) int m_max_flush_volume = Slic3r::g_max_flush_volume; unsigned int m_number_of_extruders = (int)(sqrt(init_matrix.size()) + 0.001); - const std::vector extruder_colours = wxGetApp().plater()->get_extruder_colors_from_plater_config(nullptr, false); + const std::vector extruder_colours = wxGetApp().plater()->get_extruder_colors_from_plater_config(); std::vector> multi_colours; // Support for multi-color filament @@ -8448,6 +3314,7 @@ struct Plater::priv bool m_slice_all_only_has_gcode{ false }; bool m_need_update{false}; + int m_filament_temp_check_serial{0}; // cancels stale CallAfter in check_filament_temp_mixing() //BBS: add popup object table logic //ObjectTableDialog* m_popup_table{ nullptr }; @@ -8508,13 +3375,9 @@ struct Plater::priv static const std::regex pattern_prusa; bool m_is_dark = false; - size_t m_last_auto_gradient_prompt_physical_count = 0; - bool m_last_auto_gradient_prompt_accepted = false; priv(Plater *q, MainFrame *main_frame); ~priv(); - bool confirm_auto_generated_gradients(wxWindow *parent, size_t num_physical); - void set_auto_generated_gradient_decision(size_t num_physical, bool create_auto_gradients); bool need_update() const { return m_need_update; } @@ -8985,10 +3848,10 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame) "extruder_colour", "filament_colour", "material_colour", "printable_height", "printer_model", "printer_technology", // These values are necessary to construct SlicingParameters by the Canvas3D variable layer height editor. "layer_height", "initial_layer_print_height", "min_layer_height", "max_layer_height", - "brim_width", "wall_loops", "wall_filament", "sparse_infill_density", "enable_infill_filament_override", "infill_filament_use_base_first_layers", "infill_filament_use_base_last_layers", "sparse_infill_filament", "top_shell_layers", + "brim_width", "wall_loops", "wall_filament", "sparse_infill_density", "sparse_infill_filament", "top_shell_layers", "enable_support", "support_filament", "support_interface_filament", "support_top_z_distance", "support_bottom_z_distance", "raft_layers", - "wipe_tower_rotation_angle", "wipe_tower_cone_angle", "wipe_tower_extra_spacing", "wipe_tower_extra_flow", "local_z_wipe_tower_purge_lines", "wipe_tower_max_purge_speed", + "wipe_tower_rotation_angle", "wipe_tower_cone_angle", "wipe_tower_extra_spacing", "wipe_tower_extra_flow", "wipe_tower_max_purge_speed", "wipe_tower_wall_type", "wipe_tower_extra_rib_length","wipe_tower_rib_width","wipe_tower_fillet_wall", "wipe_tower_filament", "best_object_pos" @@ -9967,32 +4830,6 @@ std::vector Plater::priv::load_files(const std::vector& input_ << boost::format(", plate_data.size %1%, project_preset.size %2%, is_bbs_3mf %3%, file_version %4% \n") % plate_data.size() % project_presets.size() % (en_3mf_file_type == En3mfType::From_BBS) % file_version.to_string(); - auto imported_string_count = [&config_loaded](const char *key) -> size_t { - if (const auto *opt = config_loaded.option(key)) - return opt->values.size(); - return 0; - }; - auto imported_float_count = [&config_loaded](const char *key) -> size_t { - if (const auto *opt = config_loaded.option(key)) - return opt->values.size(); - return 0; - }; - - std::vector imported_filament_colors; - size_t imported_physical_filaments = 0; - if (const auto *filament_colors_opt = config_loaded.option("filament_colour")) { - imported_filament_colors = filament_colors_opt->values; - imported_physical_filaments = imported_filament_colors.size(); - } - if (imported_physical_filaments == 0) - imported_physical_filaments = imported_string_count("filament_settings_id"); - if (imported_physical_filaments == 0) - imported_physical_filaments = imported_string_count("filament_ids"); - if (imported_physical_filaments == 0) - imported_physical_filaments = imported_string_count("default_filament_colour"); - if (imported_physical_filaments == 0) - imported_physical_filaments = imported_float_count("nozzle_diameter"); - // 1. add extruder for prusa model if the number of existing extruders is not enough // 2. add extruder for BBS or Other model if only import geometry if (en_3mf_file_type == En3mfType::From_Prusa || (load_model && !load_config)) { @@ -10005,66 +4842,10 @@ std::vector Plater::priv::load_files(const std::vector& input_ } } int size = extruderIds.size() == 0 ? 0 : *(extruderIds.rbegin()); - const bool geometry_only_project_import = load_model && !load_config && imported_physical_filaments > 0; - const size_t desired_physical_filaments = geometry_only_project_import ? - std::min({imported_physical_filaments, size_t(MAXIMUM_EXTRUDER_NUMBER), size_t(MAXIMUM_FILAMENT_NUMBER)}) : 0; - BOOST_LOG_TRIVIAL(info) << "3MF geometry import filament detection" - << " imported_physical=" << imported_physical_filaments - << " imported_colors=" << imported_filament_colors.size() - << " config_filament_settings_id=" << imported_string_count("filament_settings_id") - << " config_filament_ids=" << imported_string_count("filament_ids") - << " config_default_filament_colour=" << imported_string_count("default_filament_colour") - << " config_nozzle_diameter=" << imported_float_count("nozzle_diameter") - << " model_max_extruder=" << size - << " geometry_only_project_import=" << (geometry_only_project_import ? 1 : 0); - if (geometry_only_project_import) - size = int(desired_physical_filaments); - - PresetBundle *preset_bundle = wxGetApp().preset_bundle; - if (geometry_only_project_import && preset_bundle != nullptr) { - const size_t current_num_filaments = preset_bundle->filament_presets.size(); - const bool current_project_empty = this->model.objects.empty(); - if (current_project_empty) { - static const t_config_option_keys imported_project_option_keys = { - "filament_colour", - "mixed_filament_definitions", - "mixed_filament_gradient_mode", - "mixed_filament_height_lower_bound", - "mixed_filament_height_upper_bound", - "mixed_filament_advanced_dithering", - "mixed_filament_pointillism_pixel_size", - "mixed_filament_pointillism_line_gap", - "mixed_filament_component_bias_enabled", - "mixed_filament_surface_indentation" - }; - preset_bundle->project_config.apply_only(config_loaded, imported_project_option_keys, true); - if (current_num_filaments != desired_physical_filaments) { - q->confirm_auto_generated_gradients(desired_physical_filaments); - preset_bundle->set_num_filaments(unsigned(desired_physical_filaments)); - } else - preset_bundle->update_multi_material_filament_presets(); - BOOST_LOG_TRIVIAL(info) << "3MF geometry import applied imported project config" - << " current_num_filaments=" << current_num_filaments - << " desired_physical_filaments=" << desired_physical_filaments - << " mixed_enabled=" << preset_bundle->mixed_filaments.enabled_count(); - wxGetApp().plater()->on_filaments_change(desired_physical_filaments); - } else if (current_num_filaments < desired_physical_filaments) { - std::vector new_colors; - if (imported_filament_colors.size() > current_num_filaments) { - new_colors.assign(imported_filament_colors.begin() + current_num_filaments, - imported_filament_colors.begin() + desired_physical_filaments); - } - q->confirm_auto_generated_gradients(desired_physical_filaments); - preset_bundle->set_num_filaments(unsigned(desired_physical_filaments), new_colors); - wxGetApp().plater()->on_filaments_change(desired_physical_filaments); - } - } int filament_size = sidebar->combos_filament().size(); - const int filament_limit = std::min({size, int(MAXIMUM_EXTRUDER_NUMBER), int(MAXIMUM_FILAMENT_NUMBER)}); - while (filament_size < filament_limit) { + while (filament_size < MAXIMUM_EXTRUDER_NUMBER && filament_size < size) { int filament_count = filament_size + 1; - wxGetApp().plater()->confirm_auto_generated_gradients(filament_count); wxColour new_col = Plater::get_next_color_for_filament(); std::string new_color = new_col.GetAsString(wxC2S_HTML_SYNTAX).ToStdString(); wxGetApp().preset_bundle->set_num_filaments(filament_count, new_color); @@ -10404,10 +5185,6 @@ std::vector Plater::priv::load_files(const std::vector& input_ // BBS: add preset combo box re-active logic // currently found only needs re-active here wxGetApp().load_current_presets(false, false); - // Some preset-tab refresh paths rebuild printer/filament UI from the - // active presets but do not preserve the mixed manager instance. - // Rebuild it explicitly from project_config before clamping object IDs. - preset_bundle->update_multi_material_filament_presets(); // Update filament colors for the MM-printer profile in the full config // to avoid black (default) colors for Extruders in the ObjectList, // when for extruder colors are used filament colors @@ -11725,9 +6502,17 @@ unsigned int Plater::priv::update_background_process(bool force_validation, bool BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": validate err=%1%, warning=%2%")%err.string%warning.string; if (err.string.empty()) { - this->partplate_list.get_curr_plate()->update_apply_result_invalid(false); - notification_manager->set_all_slicing_errors_gray(true); - notification_manager->close_notification_of_type(NotificationType::ValidateError); + // Validate passed, but also check filament temp mixing as a final + // guard. The Print::validate() extruders() check may miss some + // cases (e.g. wall_filament changes that haven't propagated to + // PrintRegions yet). check_filament_temp_mixing() reads directly + // from full_config() and is always current. + bool filament_ok = q->check_filament_temp_mixing(); + if (filament_ok) { + this->partplate_list.get_curr_plate()->update_apply_result_invalid(false); + notification_manager->set_all_slicing_errors_gray(true); + notification_manager->close_notification_of_type(NotificationType::ValidateError); + } if (invalidated != Print::APPLY_STATUS_UNCHANGED && background_processing_enabled()) return_state |= UPDATE_BACKGROUND_PROCESS_RESTART; @@ -12643,8 +7428,7 @@ void Plater::priv::set_current_panel(wxPanel* panel, bool no_slice) BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": from set_current_panel, no_slice %1%, export_in_progress %2%, model_fits %3%, m_is_slicing %4%")%no_slice%export_in_progress%model_fits%m_is_slicing; - if (!no_slice && !this->model.objects.empty() && !export_in_progress && model_fits && current_has_print_instances - && !current_plate->is_slice_result_valid()) + if (!no_slice && !this->model.objects.empty() && !export_in_progress && model_fits && current_has_print_instances) { //if already running in background, not relice here //BBS: add more judge for slicing @@ -13050,7 +7834,6 @@ void Plater::priv::on_select_preset(wxCommandEvent &evt) wxGetApp().plater()->update_project_dirty_from_presets(); wxGetApp().preset_bundle->export_selections(*wxGetApp().app_config); sidebar->update_dynamic_filament_list(); - sidebar->update_color_mix_panel(); bool flag_is_change = is_support_filament(idx); if (flag != flag_is_change) { sidebar->auto_calc_flushing_volumes(idx); @@ -13924,76 +8707,6 @@ void Plater::priv::on_repair_model(wxCommandEvent &event) wxGetApp().obj_list()->fix_through_netfabb(); } -bool Plater::priv::confirm_auto_generated_gradients(wxWindow *parent, size_t num_physical) -{ - auto *app_config = wxGetApp().app_config; - if (app_config == nullptr) - return MixedFilamentManager::auto_generate_enabled(); - - const bool pref_enabled = app_config->get_bool("auto_generate_gradients"); - if (!pref_enabled) { - m_last_auto_gradient_prompt_physical_count = 0; - m_last_auto_gradient_prompt_accepted = false; - MixedFilamentManager::set_auto_generate_enabled(false); - return false; - } - - if (num_physical <= 4) { - m_last_auto_gradient_prompt_physical_count = 0; - m_last_auto_gradient_prompt_accepted = false; - MixedFilamentManager::set_auto_generate_enabled(true); - return true; - } - - if (parent == nullptr || !parent->IsShownOnScreen()) { - m_last_auto_gradient_prompt_physical_count = 0; - m_last_auto_gradient_prompt_accepted = false; - MixedFilamentManager::set_auto_generate_enabled(true); - return true; - } - - if (m_last_auto_gradient_prompt_physical_count == num_physical) { - MixedFilamentManager::set_auto_generate_enabled(m_last_auto_gradient_prompt_accepted); - return m_last_auto_gradient_prompt_accepted; - } - - const size_t auto_gradient_count = num_physical * (num_physical - 1) / 2; - const wxString message = wxString::Format( - _L("Using %d physical filaments will create %d auto-generated gradients.\nDo you want to create them now?"), - int(num_physical), - int(auto_gradient_count)); - const int result = MessageDialog(parent, - message, - wxString(SLIC3R_APP_FULL_NAME) + " - " + _L("Auto gradients"), - wxYES_NO | wxYES_DEFAULT | wxCENTRE | wxICON_QUESTION) - .ShowModal(); - const bool accepted = result == wxID_YES; - m_last_auto_gradient_prompt_physical_count = num_physical; - m_last_auto_gradient_prompt_accepted = accepted; - MixedFilamentManager::set_auto_generate_enabled(accepted); - return accepted; -} - -void Plater::priv::set_auto_generated_gradient_decision(size_t num_physical, bool create_auto_gradients) -{ - m_last_auto_gradient_prompt_physical_count = num_physical; - m_last_auto_gradient_prompt_accepted = create_auto_gradients; - MixedFilamentManager::set_auto_generate_enabled(create_auto_gradients); -} - -bool Plater::confirm_auto_generated_gradients(size_t num_physical) -{ - return p != nullptr ? p->confirm_auto_generated_gradients(this, num_physical) : MixedFilamentManager::auto_generate_enabled(); -} - -void Plater::set_auto_generated_gradient_decision(size_t num_physical, bool create_auto_gradients) -{ - if (p != nullptr) - p->set_auto_generated_gradient_decision(num_physical, create_auto_gradients); - else - MixedFilamentManager::set_auto_generate_enabled(create_auto_gradients); -} - void Plater::priv::on_filament_color_changed(wxCommandEvent &event) { //q->update_all_plate_thumbnails(true); @@ -14007,13 +8720,6 @@ void Plater::priv::on_filament_color_changed(wxCommandEvent &event) if (wxGetApp().app_config->get("auto_calculate") == "true") { sidebar->auto_calc_flushing_volumes(modify_id); } - - // Regenerate mixed filaments and refresh the mixed panel only. Color - // changes do not alter filament IDs, so the full on_filaments_change() - // path is unnecessary and can re-enter UI rebuilds mid-update. - wxGetApp().preset_bundle->update_multi_material_filament_presets(); - sidebar->update_mixed_filament_panel(); - sidebar->update_color_mix_panel(); } void Plater::priv::install_network_plugin(wxCommandEvent &event) @@ -14085,9 +8791,6 @@ void Plater::priv::apply_color_mode() m_aui_mgr.GetArtProvider()->SetColour(wxAUI_DOCKART_INACTIVE_CAPTION_TEXT_COLOUR, *wxWHITE); m_aui_mgr.GetArtProvider()->SetColour(wxAUI_DOCKART_SASH_COLOUR, sash_color); m_aui_mgr.GetArtProvider()->SetColour(wxAUI_DOCKART_BORDER_COLOUR, is_dark ? *wxBLACK : wxColour(165, 165, 165)); - - if (sidebar) - sidebar->update_color_mix_panel(); } static void get_position(wxWindowBase* child, wxWindowBase* until_parent, int& x, int& y) { @@ -19355,38 +14058,6 @@ void Plater::send_gcode_legacy(int plate_idx, Export3mfProgressFn proFn, bool us // if physical_printer is selected, send gcode for this printer // DynamicPrintConfig* physical_printer_config = wxGetApp().preset_bundle->physical_printers.get_selected_printer_config(); - auto prepare_upload_filename_for_dialog = [this, use_3mf](fs::path output_file) { - output_file = fs::path(Slic3r::fold_utf8_to_ascii(output_file.string())); - if (use_3mf) - output_file.replace_extension("3mf"); - - PartPlate *current_plate = this->get_partplate_list().get_curr_plate(); - if (current_plate != nullptr) { - const Print *current_print = current_plate->fff_print(); - if (current_print != nullptr && !current_print->print_statistics().estimated_normal_print_time.empty()) - return fs::path(current_print->print_statistics().finalize_output_path(output_file.string())); - } - - if (current_plate != nullptr && current_plate->is_slice_result_valid() && current_plate->get_slice_result() != nullptr) { - const auto &estimated_stats = current_plate->get_slice_result()->print_statistics; - const float normal_time = estimated_stats.modes[static_cast(PrintEstimatedStatistics::ETimeMode::Normal)].time; - if (normal_time > 0.0f) { - std::string filename = output_file.string(); - const std::string normal_time_str = short_time(get_time_dhms(normal_time)); - boost::replace_all(filename, "{print_time}", normal_time_str); - boost::replace_all(filename, "{normal_print_time}", normal_time_str); - - const float silent_time = estimated_stats.modes[static_cast(PrintEstimatedStatistics::ETimeMode::Stealth)].time; - if (silent_time > 0.0f) - boost::replace_all(filename, "{silent_print_time}", short_time(get_time_dhms(silent_time))); - - output_file = fs::path(filename); - } - } - - return output_file; - }; - // 校验机型 auto devices = wxGetApp().app_config->get_devices(); std::string connect_preset = ""; @@ -19451,7 +14122,10 @@ void Plater::send_gcode_legacy(int plate_idx, Export3mfProgressFn proFn, bool us show_error(this, ex.what(), false); return; } - default_output_file = prepare_upload_filename_for_dialog(std::move(default_output_file)); + default_output_file = fs::path(Slic3r::fold_utf8_to_ascii(default_output_file.string())); + if (use_3mf) { + default_output_file.replace_extension("3mf"); + } // get file path auto file_path = get_partplate_list().get_curr_plate()->get_tmp_gcode_path(); @@ -19522,7 +14196,10 @@ void Plater::send_gcode_legacy(int plate_idx, Export3mfProgressFn proFn, bool us show_error(this, ex.what(), false); return; } - default_output_file = prepare_upload_filename_for_dialog(std::move(default_output_file)); + default_output_file = fs::path(Slic3r::fold_utf8_to_ascii(default_output_file.string())); + if (use_3mf) { + default_output_file.replace_extension("3mf"); + } // Repetier specific: Query the server for the list of file groups. wxArrayString groups; @@ -19787,7 +14464,7 @@ bool Plater::search_string_getter(int idx, const char** label, const char** tool return false; } -void Plater::on_filaments_delete(size_t num_filaments, size_t filament_id, int replace_filament_id, const std::vector& is_mixed_snapshot) +void Plater::on_filaments_delete(size_t num_filaments, size_t filament_id, int replace_filament_id) { // only update elements in plater update_filament_colors_in_full_config(); @@ -19800,58 +14477,19 @@ void Plater::on_filaments_delete(size_t num_filaments, size_t filament_id, int r part_plate->update_first_layer_print_sequence_when_delete_filament(filament_id); }*/ - // Consume remap before updating volumes - // This is used when merging mixed filaments to properly remap object filament IDs - PresetBundle *preset_bundle = wxGetApp().preset_bundle; - std::vector id_remap; - if (preset_bundle != nullptr) - id_remap = preset_bundle->consume_last_filament_id_remap(); - - // Build state map for remap if available. - // Use the remap for both pure-delete and merge paths so that mixed - // filaments deleted by remove_physical_filament are correctly mapped - // to NONE instead of being shifted onto wrong IDs. - EnforcerBlockerStateMap state_map; - bool should_remap_states = false; - if (!id_remap.empty()) { - should_remap_states = true; - if (replace_filament_id >= 0) { - // Merge: inject the merge target into the remap so the deleted - // physical filament maps to the target instead of 0. - size_t old_1based = filament_id + 1; - size_t new_1based = replace_filament_id + 1; - if (old_1based < id_remap.size()) - id_remap[old_1based] = (unsigned int)new_1based; - } - for (size_t i = 0; i < state_map.size(); ++i) - state_map[i] = EnforcerBlockerType(i); - for (size_t i = 1; i < state_map.size(); ++i) { - const unsigned int mapped = i < id_remap.size() ? id_remap[i] : 0; - if (mapped == 0 || mapped >= state_map.size() || mapped > num_filaments) - state_map[i] = EnforcerBlockerType::NONE; - else - state_map[i] = EnforcerBlockerType(mapped); - } - } - - // update mmu paint data + // update mmu info for (ModelObject* mo : wxGetApp().model().objects) { for (ModelVolume* mv : mo->volumes) { - if (should_remap_states) { - mv->remap_extruder_ids(num_filaments, state_map); - } else { - mv->update_extruder_count_when_delete_filament(num_filaments, filament_id + 1, - replace_filament_id + 1); // this function is 1 base - } + mv->update_extruder_count_when_delete_filament(num_filaments, filament_id + 1, + replace_filament_id + 1); // this function is 1 base } } // update UI sidebar().on_filaments_delete(filament_id); - // update global feature filament selections - static const char* keys[] = {"wall_filament", "sparse_infill_filament", "solid_infill_filament", - "support_filament", "support_interface_filament"}; + // update global support filament + static const char* keys[] = {"support_filament", "support_interface_filament"}; for (auto key : keys) if (p->config->has(key)) { if (p->config->opt_int(key) == filament_id + 1) @@ -19889,132 +14527,6 @@ void Plater::on_filaments_change(size_t num_filaments) { // only update elements in plater update_filament_colors_in_full_config(); - - const size_t old_num_filaments = sidebar().combos_filament().size(); - const bool auto_generate_before = MixedFilamentManager::auto_generate_enabled(); - const bool allow_auto_gradients = p->confirm_auto_generated_gradients(this, num_filaments); - auto summarize_uint_vector = [](const std::vector &values, size_t max_items = 24) { - std::string out = "["; - const size_t n = std::min(values.size(), max_items); - for (size_t i = 0; i < n; ++i) { - if (i > 0) - out += ","; - out += std::to_string(values[i]); - } - if (values.size() > n) - out += ",..."; - out += "]"; - return out; - }; - auto summarize_used_states = [](const std::vector &used, size_t max_items = 24) { - std::string out = "["; - size_t total = 0; - size_t emitted = 0; - for (size_t i = 1; i < used.size(); ++i) { - if (!used[i]) - continue; - ++total; - if (emitted < max_items) { - if (emitted > 0) - out += ","; - out += std::to_string(i); - ++emitted; - } - } - if (total > emitted) - out += ",..."; - out += "] total=" + std::to_string(total); - return out; - }; - PresetBundle *preset_bundle = wxGetApp().preset_bundle; - if (preset_bundle != nullptr && auto_generate_before && !allow_auto_gradients) - preset_bundle->update_multi_material_filament_presets(size_t(-1), old_num_filaments); - // Consume remap before sidebar refresh, which may trigger config sync - // paths that regenerate mixed filaments and clear this remap buffer. - std::vector id_remap; - if (preset_bundle != nullptr) - id_remap = preset_bundle->consume_last_filament_id_remap(); - - size_t total_filaments = num_filaments; - if (preset_bundle != nullptr) - total_filaments = preset_bundle->mixed_filaments.total_filaments(num_filaments); - - EnforcerBlockerStateMap state_map; - for (size_t i = 0; i < state_map.size(); ++i) - state_map[i] = EnforcerBlockerType(i); - - bool have_explicit_remap = false; - bool should_remap_states = false; - if (!id_remap.empty()) { - have_explicit_remap = true; - should_remap_states = true; - for (size_t i = 1; i < state_map.size(); ++i) { - const unsigned int mapped = i < id_remap.size() ? id_remap[i] : 0; - if (mapped == 0 || mapped >= state_map.size() || mapped > total_filaments) - state_map[i] = EnforcerBlockerType::NONE; - else - state_map[i] = EnforcerBlockerType(mapped); - } - } - - size_t changed_entries = 0; - std::string changed_map_preview = "["; - for (size_t i = 1; i < state_map.size(); ++i) { - const unsigned int mapped = unsigned(state_map[i]); - if (mapped == i) - continue; - ++changed_entries; - if (changed_entries <= 24) { - if (changed_entries > 1) - changed_map_preview += ","; - changed_map_preview += std::to_string(i) + "->" + std::to_string(mapped); - } - } - if (changed_entries > 24) - changed_map_preview += ",..."; - changed_map_preview += "]"; - BOOST_LOG_TRIVIAL(warning) << "MF_REMAP on_filaments_change" - << " old_physical=" << old_num_filaments - << " new_physical=" << num_filaments - << " total_filaments=" << total_filaments - << " id_remap_size=" << id_remap.size() - << " id_remap=" << summarize_uint_vector(id_remap) - << " explicit_remap=" << (have_explicit_remap ? 1 : 0) - << " should_remap_states=" << (should_remap_states ? 1 : 0) - << " changed_entries=" << changed_entries - << " changed_map=" << changed_map_preview; - - size_t obj_idx = 0; - for (ModelObject* mo : wxGetApp().model().objects) { - size_t vol_idx = 0; - for (ModelVolume* mv : mo->volumes) { - std::string used_before; - const bool has_mmu_paint = (mv != nullptr && !mv->mmu_segmentation_facets.empty()); - if (has_mmu_paint) - used_before = summarize_used_states(mv->mmu_segmentation_facets.get_data().used_states); - - if (should_remap_states) - mv->remap_extruder_ids(total_filaments, state_map); - else - mv->update_extruder_count(total_filaments); - - if (has_mmu_paint) { - const std::string used_after = summarize_used_states(mv->mmu_segmentation_facets.get_data().used_states); - BOOST_LOG_TRIVIAL(warning) << "MF_REMAP volume" - << " obj_idx=" << obj_idx - << " vol_idx=" << vol_idx - << " obj_name=" << mo->name - << " vol_name=" << mv->name - << " before=" << used_before - << " after=" << used_after; - } - ++vol_idx; - } - ++obj_idx; - } - - // Keep UI refresh after model remap. Some UI update paths may trigger - // scene/model sync that assumes already-remapped MMU state. sidebar().on_filaments_change(num_filaments); sidebar().obj_list()->update_objects_list_filament_column(num_filaments); @@ -20023,6 +14535,12 @@ void Plater::on_filaments_change(size_t num_filaments) PartPlate* part_plate = plate_list.get_plate(i); part_plate->update_first_layer_print_sequence(num_filaments); } + + for (ModelObject* mo : wxGetApp().model().objects) { + for (ModelVolume* mv : mo->volumes) { + mv->update_extruder_count(num_filaments); + } + } } void Plater::on_bed_type_change(BedType bed_type) @@ -20057,6 +14575,95 @@ void Plater::config_change_notification(const DynamicPrintConfig &config, const // notification for more options } +bool Plater::check_filament_temp_mixing() +{ + // Use full_config() from preset_bundle — it always contains all keys including + // filament_type. p->config is a plate-level config that may not have it. + const DynamicPrintConfig& cfg = wxGetApp().preset_bundle->full_config(); + auto* ft_opt = cfg.option("filament_type"); + if (!ft_opt || ft_opt->values.empty()) + return true; + + int num_filaments = (int)ft_opt->values.size(); + std::set used_slots; + + // ---- 1-based keys: wall / sparse infill / solid infill ---- + static const char* keys_1based[] = { + "wall_filament", "sparse_infill_filament", "solid_infill_filament" + }; + for (auto key : keys_1based) { + auto* opt = cfg.option(key); + if (!opt) opt = p->config->option(key); + if (opt && opt->value >= 1 && opt->value <= num_filaments) + used_slots.insert(opt->value - 1); + } + + // ---- 0-based keys: support / support interface / wipe tower ---- + // Value 0 means "use current filament", does not introduce a new material type. + static const char* keys_0based[] = { + "support_filament", "support_interface_filament", "wipe_tower_filament" + }; + for (auto key : keys_0based) { + auto* opt = cfg.option(key); + if (!opt) opt = p->config->option(key); + if (opt && opt->value >= 1 && opt->value <= num_filaments) + used_slots.insert(opt->value - 1); + } + + // ---- ModelVolume painting extruders (multi-material coloring) ---- + for (const ModelObject* mo : wxGetApp().model().objects) { + for (const ModelVolume* mv : mo->volumes) { + for (int eid : mv->get_extruders()) { + if (eid >= 1 && eid <= num_filaments) + used_slots.insert(eid - 1); + } + } + } + + if (used_slots.empty()) + return true; + + // Use filament_is_high_temperature from each filament preset rather than + // the outdated filament_info.json. If any used slot is high-temp and any + // other is not, high/low mixing is detected. + auto* is_high_opt = cfg.option("filament_is_high_temperature"); + bool has_high = false, has_low = false; + for (int slot : used_slots) { + if (is_high_opt && slot < (int)is_high_opt->values.size()) { + if (is_high_opt->get_at(slot)) + has_high = true; + else + has_low = true; + } + } + bool compatible = !(has_high && has_low); + + // Defer notification update to the next event loop iteration so that all + // config changes from the current event have settled. Without this, rapid + // successive calls from on_config_change + Sidebar::update_presets can + // race: one pushes an error, the next reads stale config → thinks it's + // compatible → immediately closes the notification. + // The serial counter cancels all but the last deferred update. + int serial = ++p->m_filament_temp_check_serial; + wxTheApp->CallAfter([this, compatible, serial] { + if (serial != p->m_filament_temp_check_serial) return; // stale + if (!compatible) { + StringObjectException err; + err.type = STRING_EXCEPT_FILAMENTS_DIFFERENT_TEMP; + err.string = _u8L("Cannot print multiple filaments which have large difference of " + "temperature together. Otherwise, the extruder and nozzle may " + "be blocked or damaged during printing."); + p->notification_manager->push_validate_error_notification(err); + p->partplate_list.get_curr_plate()->update_apply_result_invalid(true); + } else { + p->notification_manager->close_notification_of_type(NotificationType::ValidateError); + p->partplate_list.get_curr_plate()->update_apply_result_invalid(false); + } + }); + + return compatible; +} + void Plater::on_config_change(const DynamicPrintConfig &config) { bool update_scheduled = false; @@ -20128,9 +14735,6 @@ void Plater::on_config_change(const DynamicPrintConfig &config) } // Orca: update when *_filament changed else if (opt_key == "support_interface_filament" || opt_key == "support_filament" || opt_key == "wall_filament" || - opt_key == "enable_infill_filament_override" || - opt_key == "infill_filament_use_base_first_layers" || - opt_key == "infill_filament_use_base_last_layers" || opt_key == "sparse_infill_filament" || opt_key == "solid_infill_filament") { update_scheduled = true; } @@ -20148,6 +14752,9 @@ void Plater::on_config_change(const DynamicPrintConfig &config) this->p->schedule_background_process(); update_title_dirty_status(); } + + // Real-time high/low temperature filament mixing check + check_filament_temp_mixing(); } void Plater::set_bed_shape() const @@ -20225,30 +14832,17 @@ void Plater::on_activate() } // Get vector of extruder colors considering filament color, if extruder color is undefined. -std::vector Plater::get_extruder_colors_from_plater_config(const GCodeProcessorResult* const result, bool include_mixed) const +std::vector Plater::get_extruder_colors_from_plater_config(const GCodeProcessorResult* const result) const { if (wxGetApp().is_gcode_viewer() && result != nullptr) return result->extruder_colors; else { - if (wxGetApp().preset_bundle == nullptr) - return {}; - const Slic3r::DynamicPrintConfig* config = &wxGetApp().preset_bundle->project_config; std::vector filament_colors; if (!config->has("filament_colour")) // in case of a SLA print return filament_colors; filament_colors = (config->option("filament_colour"))->values; - const size_t num_physical = static_cast(std::max(wxGetApp().filaments_cnt(), 0)); - filament_colors.resize(num_physical, "#26A69A"); - - if (include_mixed) { - // Append display colours for enabled mixed (virtual) filaments. - const auto &mixed_mgr = wxGetApp().preset_bundle->mixed_filaments; - for (const auto &dc : mixed_mgr.display_colors()) - filament_colors.push_back(dc); - } - return filament_colors; } } @@ -21497,20 +16091,6 @@ void Plater::reset_gcode_toolpaths() p->reset_gcode_toolpaths(); } -void Plater::post_slice_state_change_update() -{ - p->partplate_list.invalid_all_slice_result(); - reset_gcode_toolpaths(); - wxGetApp().mainframe->update_slice_print_status(MainFrame::SlicePrintEventType::eEventSliceUpdate, true, false); - CallAfter([this]() { - GLCanvas3D* canvas = get_current_canvas3D(); - if (canvas) { - canvas->set_as_dirty(); - canvas->request_extra_frame(); - } - }); -} - const Mouse3DController& Plater::get_mouse3d_controller() const { return p->mouse3d_controller; From 1c5e2addea2ef0bf4c0d3453093cc9ec21757561 Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Mon, 25 May 2026 16:33:43 +0800 Subject: [PATCH 014/135] fix: remove CallAfter races in filament temp mixing detection Make check_filament_temp_mixing() a pure detection function with no side effects. Notification push/close is now owned exclusively by Plater::priv::update()'s three-layer guard, eliminating races where CallAfter serial cancellation would accidentally drop the error notification. Also scan all plates' configs (not just the active plate) so that filament changes on any plate are detected immediately. --- src/slic3r/GUI/Plater.cpp | 100 +++++++++++++++++--------------------- 1 file changed, 45 insertions(+), 55 deletions(-) diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index cdcdd415522..8e257f1f8d6 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -2093,7 +2093,9 @@ void Sidebar::update_presets(Preset::Type preset_type) update_dynamic_filament_list(); // Real-time high/low temperature filament mixing check after filament preset change - p->plater->check_filament_temp_mixing(); + if (!p->plater->check_filament_temp_mixing()) { + p->plater->get_partplate_list().get_curr_plate()->update_apply_result_invalid(true); + } break; } @@ -3314,7 +3316,6 @@ struct Plater::priv bool m_slice_all_only_has_gcode{ false }; bool m_need_update{false}; - int m_filament_temp_check_serial{0}; // cancels stale CallAfter in check_filament_temp_mixing() //BBS: add popup object table logic //ObjectTableDialog* m_popup_table{ nullptr }; @@ -6512,6 +6513,15 @@ unsigned int Plater::priv::update_background_process(bool force_validation, bool this->partplate_list.get_curr_plate()->update_apply_result_invalid(false); notification_manager->set_all_slicing_errors_gray(true); notification_manager->close_notification_of_type(NotificationType::ValidateError); + } else { + // Filament mixing detected — show error and keep plate invalid + StringObjectException err; + err.type = STRING_EXCEPT_FILAMENTS_DIFFERENT_TEMP; + err.string = _u8L("Cannot print multiple filaments which have large difference of " + "temperature together. Otherwise, the extruder and nozzle may " + "be blocked or damaged during printing."); + notification_manager->push_validate_error_notification(err); + this->partplate_list.get_curr_plate()->update_apply_result_invalid(true); } if (invalidated != Print::APPLY_STATUS_UNCHANGED && background_processing_enabled()) return_state |= UPDATE_BACKGROUND_PROCESS_RESTART; @@ -14577,40 +14587,41 @@ void Plater::config_change_notification(const DynamicPrintConfig &config, const bool Plater::check_filament_temp_mixing() { - // Use full_config() from preset_bundle — it always contains all keys including - // filament_type. p->config is a plate-level config that may not have it. - const DynamicPrintConfig& cfg = wxGetApp().preset_bundle->full_config(); - auto* ft_opt = cfg.option("filament_type"); + const DynamicPrintConfig& full_cfg = wxGetApp().preset_bundle->full_config(); + auto* ft_opt = full_cfg.option("filament_type"); if (!ft_opt || ft_opt->values.empty()) return true; int num_filaments = (int)ft_opt->values.size(); std::set used_slots; - // ---- 1-based keys: wall / sparse infill / solid infill ---- - static const char* keys_1based[] = { - "wall_filament", "sparse_infill_filament", "solid_infill_filament" + // Helper: collect used filament slots from a single config + auto collect_from_cfg = [&](const DynamicPrintConfig& cfg) { + static const char* keys_1based[] = {"wall_filament", "sparse_infill_filament", "solid_infill_filament"}; + for (auto key : keys_1based) { + auto* opt = cfg.option(key); + if (opt && opt->value >= 1 && opt->value <= num_filaments) + used_slots.insert(opt->value - 1); + } + static const char* keys_0based[] = {"support_filament", "support_interface_filament", "wipe_tower_filament"}; + for (auto key : keys_0based) { + auto* opt = cfg.option(key); + if (opt && opt->value >= 1 && opt->value <= num_filaments) + used_slots.insert(opt->value - 1); + } }; - for (auto key : keys_1based) { - auto* opt = cfg.option(key); - if (!opt) opt = p->config->option(key); - if (opt && opt->value >= 1 && opt->value <= num_filaments) - used_slots.insert(opt->value - 1); - } - // ---- 0-based keys: support / support interface / wipe tower ---- - // Value 0 means "use current filament", does not introduce a new material type. - static const char* keys_0based[] = { - "support_filament", "support_interface_filament", "wipe_tower_filament" - }; - for (auto key : keys_0based) { - auto* opt = cfg.option(key); - if (!opt) opt = p->config->option(key); - if (opt && opt->value >= 1 && opt->value <= num_filaments) - used_slots.insert(opt->value - 1); + // Collect from full_config first (global defaults for wall/infill/etc.) + collect_from_cfg(full_cfg); + + // Collect from every plate's config (per-plate overrides) + for (int i = 0; i < p->partplate_list.get_plate_count(); ++i) { + PartPlate* plate = p->partplate_list.get_plate(i); + if (plate) + collect_from_cfg(*plate->config()); } - // ---- ModelVolume painting extruders (multi-material coloring) ---- + // Collect from ModelVolume painting extruders (multi-material coloring) for (const ModelObject* mo : wxGetApp().model().objects) { for (const ModelVolume* mv : mo->volumes) { for (int eid : mv->get_extruders()) { @@ -14623,10 +14634,8 @@ bool Plater::check_filament_temp_mixing() if (used_slots.empty()) return true; - // Use filament_is_high_temperature from each filament preset rather than - // the outdated filament_info.json. If any used slot is high-temp and any - // other is not, high/low mixing is detected. - auto* is_high_opt = cfg.option("filament_is_high_temperature"); + // Use filament_is_high_temperature from each filament preset + auto* is_high_opt = full_cfg.option("filament_is_high_temperature"); bool has_high = false, has_low = false; for (int slot : used_slots) { if (is_high_opt && slot < (int)is_high_opt->values.size()) { @@ -14638,29 +14647,6 @@ bool Plater::check_filament_temp_mixing() } bool compatible = !(has_high && has_low); - // Defer notification update to the next event loop iteration so that all - // config changes from the current event have settled. Without this, rapid - // successive calls from on_config_change + Sidebar::update_presets can - // race: one pushes an error, the next reads stale config → thinks it's - // compatible → immediately closes the notification. - // The serial counter cancels all but the last deferred update. - int serial = ++p->m_filament_temp_check_serial; - wxTheApp->CallAfter([this, compatible, serial] { - if (serial != p->m_filament_temp_check_serial) return; // stale - if (!compatible) { - StringObjectException err; - err.type = STRING_EXCEPT_FILAMENTS_DIFFERENT_TEMP; - err.string = _u8L("Cannot print multiple filaments which have large difference of " - "temperature together. Otherwise, the extruder and nozzle may " - "be blocked or damaged during printing."); - p->notification_manager->push_validate_error_notification(err); - p->partplate_list.get_curr_plate()->update_apply_result_invalid(true); - } else { - p->notification_manager->close_notification_of_type(NotificationType::ValidateError); - p->partplate_list.get_curr_plate()->update_apply_result_invalid(false); - } - }); - return compatible; } @@ -14753,8 +14739,12 @@ void Plater::on_config_change(const DynamicPrintConfig &config) update_title_dirty_status(); } - // Real-time high/low temperature filament mixing check - check_filament_temp_mixing(); + // Real-time high/low temperature filament mixing check. + // Notification push/close is handled by priv::update()'s three-layer guard; + // here we only mark the plate invalid to disable the slice button early. + if (!check_filament_temp_mixing()) { + p->partplate_list.get_curr_plate()->update_apply_result_invalid(true); + } } void Plater::set_bed_shape() const From 4a204bf0ca62567dc2063f8dd9a34753d2b19ad2 Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Mon, 25 May 2026 16:44:15 +0800 Subject: [PATCH 015/135] fix: push notification immediately in both filament mixing hooks Previously the hooks only marked the plate invalid (greyed out slice button) but relied on priv::update() to push the error notification. Since priv::update() only runs after schedule_background_process(), sidebar filament preset changes that don't trigger a background process cycle would never show the red error bar. Now all three sites push/close notification synchronously: - Sidebar::update_presets() - Plater::on_config_change() - Plater::priv::update() (three-layer guard) --- src/slic3r/GUI/Plater.cpp | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 8e257f1f8d6..14b94b3de74 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -2094,7 +2094,16 @@ void Sidebar::update_presets(Preset::Type preset_type) // Real-time high/low temperature filament mixing check after filament preset change if (!p->plater->check_filament_temp_mixing()) { + StringObjectException err; + err.type = STRING_EXCEPT_FILAMENTS_DIFFERENT_TEMP; + err.string = _u8L("Cannot print multiple filaments which have large difference of " + "temperature together. Otherwise, the extruder and nozzle may " + "be blocked or damaged during printing."); + p->plater->get_notification_manager()->push_validate_error_notification(err); p->plater->get_partplate_list().get_curr_plate()->update_apply_result_invalid(true); + } else { + p->plater->get_notification_manager()->close_notification_of_type(NotificationType::ValidateError); + p->plater->get_partplate_list().get_curr_plate()->update_apply_result_invalid(false); } break; @@ -14739,11 +14748,18 @@ void Plater::on_config_change(const DynamicPrintConfig &config) update_title_dirty_status(); } - // Real-time high/low temperature filament mixing check. - // Notification push/close is handled by priv::update()'s three-layer guard; - // here we only mark the plate invalid to disable the slice button early. + // Real-time high/low temperature filament mixing check if (!check_filament_temp_mixing()) { + StringObjectException err; + err.type = STRING_EXCEPT_FILAMENTS_DIFFERENT_TEMP; + err.string = _u8L("Cannot print multiple filaments which have large difference of " + "temperature together. Otherwise, the extruder and nozzle may " + "be blocked or damaged during printing."); + p->notification_manager->push_validate_error_notification(err); p->partplate_list.get_curr_plate()->update_apply_result_invalid(true); + } else { + p->notification_manager->close_notification_of_type(NotificationType::ValidateError); + p->partplate_list.get_curr_plate()->update_apply_result_invalid(false); } } From e0c7cb46beced374aa90efff72a1c17047ff3a8a Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Mon, 25 May 2026 16:54:38 +0800 Subject: [PATCH 016/135] fix: read filament_is_high_temperature directly from preset configs full_config() builds a merged snapshot that may lag when called from Sidebar hooks because the edited preset config hasn't been committed yet. Read filament_is_high_temperature directly from each filament preset's individual config via find_preset(name, true) instead. --- src/slic3r/GUI/Plater.cpp | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 14b94b3de74..41e2e4d253a 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -14643,15 +14643,23 @@ bool Plater::check_filament_temp_mixing() if (used_slots.empty()) return true; - // Use filament_is_high_temperature from each filament preset - auto* is_high_opt = full_cfg.option("filament_is_high_temperature"); + // Read filament_is_high_temperature directly from each filament preset's + // own config rather than through full_config(). full_config() builds a + // merged snapshot that may lag behind when called from Sidebar hooks + // (the edited preset config hasn't been committed yet). + PresetBundle* bundle = wxGetApp().preset_bundle; bool has_high = false, has_low = false; for (int slot : used_slots) { - if (is_high_opt && slot < (int)is_high_opt->values.size()) { - if (is_high_opt->get_at(slot)) - has_high = true; - else - has_low = true; + if (slot < (int)bundle->filament_presets.size()) { + const Preset* preset = bundle->filaments.find_preset( + bundle->filament_presets[slot], true); + if (preset) { + bool is_high = preset->config.opt_bool("filament_is_high_temperature", 0); + if (is_high) + has_high = true; + else + has_low = true; + } } } bool compatible = !(has_high && has_low); From 9946414f20d81c59c44f82909b40766a26212342 Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Tue, 26 May 2026 15:32:09 +0800 Subject: [PATCH 017/135] Add preference option to allow high/low temperature filament mixing --- src/slic3r/GUI/Preferences.cpp | 25 +++---------------------- 1 file changed, 3 insertions(+), 22 deletions(-) diff --git a/src/slic3r/GUI/Preferences.cpp b/src/slic3r/GUI/Preferences.cpp index a194749e11b..93cc328ae83 100644 --- a/src/slic3r/GUI/Preferences.cpp +++ b/src/slic3r/GUI/Preferences.cpp @@ -6,7 +6,6 @@ #include "MsgDialog.hpp" #include "I18N.hpp" #include "libslic3r/AppConfig.hpp" -#include "libslic3r/MixedFilament.hpp" #include #include #include "Notebook.hpp" @@ -779,16 +778,6 @@ wxBoxSizer *PreferencesDialog::create_item_checkbox(wxString title, wxWindow *pa BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " sync_user_preset: " << (sync ? "true" : "false"); } - if (param == "auto_generate_gradients") { - MixedFilamentManager::set_auto_generate_enabled(checkbox->GetValue()); - if (wxGetApp().preset_bundle != nullptr && wxGetApp().plater() != nullptr) { - const size_t num_physical = wxGetApp().preset_bundle->filament_presets.size(); - wxGetApp().plater()->set_auto_generated_gradient_decision(num_physical, checkbox->GetValue()); - wxGetApp().preset_bundle->update_multi_material_filament_presets(); - wxGetApp().plater()->on_filaments_change(num_physical); - } - } - #ifdef __WXMSW__ if (param == "associate_3mf") { bool pbool = app_config->get("associate_3mf") == "true" ? true : false; @@ -1052,8 +1041,6 @@ PreferencesDialog::PreferencesDialog(wxWindow *parent, wxWindowID id, const wxSt j["auto_flushing"] = value; value = wxGetApp().app_config->get("auto_calculate_when_filament_change"); j["auto_calculate_when_filament_change"] = value; - value = wxGetApp().app_config->get("auto_generate_gradients"); - j["auto_generate_gradients"] = value; agent->track_event("preferences_changed", j.dump()); } } catch(...) {} @@ -1234,6 +1221,7 @@ wxWindow* PreferencesDialog::create_general_page() auto item_use_free_camera_settings = create_item_checkbox(_L("Use free camera"), page, _L("If enabled, use free camera. If not enabled, use constrained camera."), 50, "use_free_camera"); auto swap_pan_rotate = create_item_checkbox(_L("Swap pan and rotate mouse buttons"), page, _L("If enabled, swaps the left and right mouse buttons pan and rotate functions."), 50, "swap_mouse_buttons"); auto reverse_mouse_zoom = create_item_checkbox(_L("Reverse mouse zoom"), page, _L("If enabled, reverses the direction of zoom with mouse wheel."), 50, "reverse_mouse_wheel_zoom"); + auto allow_filament_temp_mixing = create_item_checkbox(_L("Allow high/low temperature filament mixing"), page, _L("If enabled, allows printing with both high-temperature and low-temperature filaments simultaneously."), 50, "allow_filament_temp_mixing"); auto camera_orbit_mult = create_camera_orbit_mult_input(_L("Orbit speed multiplier"), page, _L("Multiplies the orbit speed for finer or coarser camera movement.")); auto item_show_splash_screen = create_item_checkbox(_L("Show splash screen"), page, _L("Show the splash screen during startup."), 50, "show_splash_screen"); @@ -1241,10 +1229,6 @@ wxWindow* PreferencesDialog::create_general_page() auto item_calc_mode = create_item_checkbox(_L("Flushing volumes: Auto-calculate every time the color changed."), page, _L("If enabled, auto-calculate every time the color changed."), 50, "auto_calculate"); auto item_calc_in_long_retract = create_item_checkbox(_L("Flushing volumes: Auto-calculate every time when the filament is changed."), page, _L("If enabled, auto-calculate every time when filament is changed"), 50, "auto_calculate_when_filament_change"); -#if 0 // Developer section and auto-generate gradients — hidden, preserved for potential future re-enablement - auto item_auto_generate_gradients = create_item_checkbox(_L("Mixed filaments: Auto-generate gradients."), page, _L("If enabled, Snapmaker Orca automatically creates gradient mixed filaments from physical filament pairs."), 50, "auto_generate_gradients"); - auto title_full_spectrum = create_item_title(_devL("Developer"), page, _devL("Developer")); -#endif // Developer section and auto-generate gradients auto item_remember_printer_config = create_item_checkbox(_L("Remember printer configuration"), page, _L("If enabled, Orca will remember and switch filament/process configuration for each printer automatically."), 50, "remember_printer_config"); auto item_step_mesh_setting = create_item_checkbox(_L("Show the step mesh parameter setting dialog."), page, _L("If enabled,a parameter settings dialog will appear during STEP file import."), 50, "enable_step_mesh_setting"); auto item_multi_machine = create_item_checkbox(_L("Multi-device Management (Take effect after restarting Snapmaker Orca)."), page, _L("With this option enabled, you can send a task to multiple devices at the same time and manage multiple devices."), 50, "enable_multi_machine"); @@ -1342,23 +1326,20 @@ wxWindow* PreferencesDialog::create_general_page() sizer_page->Add(item_use_free_camera_settings, 0, wxTOP, FromDIP(3)); sizer_page->Add(swap_pan_rotate, 0, wxTOP, FromDIP(3)); sizer_page->Add(reverse_mouse_zoom, 0, wxTOP, FromDIP(3)); + sizer_page->Add(allow_filament_temp_mixing, 0, wxTOP, FromDIP(3)); sizer_page->Add(camera_orbit_mult, 0, wxTOP, FromDIP(3)); sizer_page->Add(item_show_splash_screen, 0, wxTOP, FromDIP(3)); sizer_page->Add(item_hints, 0, wxTOP, FromDIP(3)); + sizer_page->Add(item_calc_in_long_retract, 0, wxTOP, FromDIP(3)); sizer_page->Add(item_multi_machine, 0, wxTOP, FromDIP(3)); sizer_page->Add(item_step_mesh_setting, 0, wxTOP, FromDIP(3)); sizer_page->Add(item_auto_arrange, 0, wxTOP, FromDIP(3)); sizer_page->Add(title_presets, 0, wxTOP | wxEXPAND, FromDIP(20)); sizer_page->Add(item_calc_mode, 0, wxTOP, FromDIP(3)); - sizer_page->Add(item_calc_in_long_retract, 0, wxTOP, FromDIP(3)); sizer_page->Add(item_user_sync, 0, wxTOP, FromDIP(3)); sizer_page->Add(item_system_sync, 0, wxTOP, FromDIP(3)); sizer_page->Add(item_remember_printer_config, 0, wxTOP, FromDIP(3)); sizer_page->Add(item_save_presets, 0, wxTOP, FromDIP(3)); -#if 0 // Developer section and auto-generate gradients — hidden, preserved for potential future re-enablement - sizer_page->Add(title_full_spectrum, 0, wxTOP | wxEXPAND, FromDIP(20)); - sizer_page->Add(item_auto_generate_gradients, 0, wxTOP, FromDIP(3)); -#endif // Developer section and auto-generate gradients //sizer_page->Add(title_network, 0, wxTOP | wxEXPAND, FromDIP(20)); //sizer_page->Add(item_check_stable_version_only, 0, wxTOP, FromDIP(3)); From 06debeb53106e74938e3ad75ae079fd115ab4e2a Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Tue, 26 May 2026 16:45:53 +0800 Subject: [PATCH 018/135] Add confirmation dialog with guard for allow_filament_temp_mixing preference toggle --- src/slic3r/GUI/Preferences.cpp | 58 ++++++++++++++++++++++++++++++++-- 1 file changed, 55 insertions(+), 3 deletions(-) diff --git a/src/slic3r/GUI/Preferences.cpp b/src/slic3r/GUI/Preferences.cpp index 93cc328ae83..40f11f7ff62 100644 --- a/src/slic3r/GUI/Preferences.cpp +++ b/src/slic3r/GUI/Preferences.cpp @@ -18,6 +18,7 @@ #include #include #include +#include #include "sentry_wrapper/SentryWrapper.hpp" @@ -721,7 +722,8 @@ void PreferencesDialog::set_dark_mode() #endif } -wxBoxSizer *PreferencesDialog::create_item_checkbox(wxString title, wxWindow *parent, wxString tooltip, int padding_left, std::string param) +wxBoxSizer *PreferencesDialog::create_item_checkbox(wxString title, wxWindow *parent, wxString tooltip, int padding_left, std::string param, + std::function confirm_cb) { wxBoxSizer *m_sizer_checkbox = new wxBoxSizer(wxHORIZONTAL); @@ -744,7 +746,19 @@ wxBoxSizer *PreferencesDialog::create_item_checkbox(wxString title, wxWindow *pa //// save config - checkbox->Bind(wxEVT_TOGGLEBUTTON, [this, checkbox, param](wxCommandEvent &e) { + auto in_handler = std::make_shared(false); + checkbox->Bind(wxEVT_TOGGLEBUTTON, [this, checkbox, param, confirm_cb, in_handler](wxCommandEvent &e) { + if (*in_handler) { e.Skip(); return; } + if (confirm_cb) { + bool old_val = !checkbox->GetValue(); + bool new_val = checkbox->GetValue(); + bool final_val = confirm_cb(new_val, old_val); + if (final_val != new_val) { + *in_handler = true; + checkbox->SetValue(final_val); + *in_handler = false; + } + } app_config->set_bool(param, checkbox->GetValue()); app_config->save(); @@ -1221,7 +1235,45 @@ wxWindow* PreferencesDialog::create_general_page() auto item_use_free_camera_settings = create_item_checkbox(_L("Use free camera"), page, _L("If enabled, use free camera. If not enabled, use constrained camera."), 50, "use_free_camera"); auto swap_pan_rotate = create_item_checkbox(_L("Swap pan and rotate mouse buttons"), page, _L("If enabled, swaps the left and right mouse buttons pan and rotate functions."), 50, "swap_mouse_buttons"); auto reverse_mouse_zoom = create_item_checkbox(_L("Reverse mouse zoom"), page, _L("If enabled, reverses the direction of zoom with mouse wheel."), 50, "reverse_mouse_wheel_zoom"); - auto allow_filament_temp_mixing = create_item_checkbox(_L("Allow high/low temperature filament mixing"), page, _L("If enabled, allows printing with both high-temperature and low-temperature filaments simultaneously."), 50, "allow_filament_temp_mixing"); + auto allow_filament_temp_mixing = create_item_checkbox(_L("Allow high/low temperature filament mixing"), page, _L("If enabled, allows printing with both high-temperature and low-temperature filaments simultaneously."), 50, "allow_filament_temp_mixing", + [this](bool new_val, bool old_val) -> bool { + // Only confirm when turning ON; allow turning OFF without dialog. + if (!new_val) + return false; + + wxDialog dlg(this, wxID_ANY, _L("高/低温材料混打风险"), wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE); + dlg.SetBackgroundColour(*wxWHITE); + + auto* main_sizer = new wxBoxSizer(wxVERTICAL); + + wxString msg = _L("混合使用打印温度差异较大的耗材,可能导致:\n" + "· 喷嘴阻塞\n" + "· 喷嘴损坏\n" + "· 层间粘附问题\n\n" + "仍要启用高/低温耗材混合打印吗?"); + auto* text = new wxStaticText(&dlg, wxID_ANY, msg); + text->Wrap(FromDIP(400)); + main_sizer->Add(text, 0, wxALL, FromDIP(20)); + + auto* btn_sizer = new wxBoxSizer(wxHORIZONTAL); + auto* btn_yes = new wxButton(&dlg, wxID_YES, _L("是")); + btn_yes->SetBackgroundColour(wxColour(0, 255, 0)); + auto* btn_no = new wxButton(&dlg, wxID_NO, _L("否")); + + btn_yes->Bind(wxEVT_BUTTON, [&dlg](wxCommandEvent&) { dlg.EndModal(wxID_YES); }); + btn_no->Bind(wxEVT_BUTTON, [&dlg](wxCommandEvent&) { dlg.EndModal(wxID_NO); }); + + btn_sizer->AddStretchSpacer(); + btn_sizer->Add(btn_yes, 0, wxALL, FromDIP(5)); + btn_sizer->Add(btn_no, 0, wxALL, FromDIP(5)); + + main_sizer->Add(btn_sizer, 0, wxEXPAND | wxALL, FromDIP(10)); + dlg.SetSizer(main_sizer); + dlg.Fit(); + dlg.CenterOnParent(); + + return dlg.ShowModal() == wxID_YES; + }); auto camera_orbit_mult = create_camera_orbit_mult_input(_L("Orbit speed multiplier"), page, _L("Multiplies the orbit speed for finer or coarser camera movement.")); auto item_show_splash_screen = create_item_checkbox(_L("Show splash screen"), page, _L("Show the splash screen during startup."), 50, "show_splash_screen"); From facd0519d4741bf9f755d085835e70e2141c74d7 Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Tue, 26 May 2026 17:50:18 +0800 Subject: [PATCH 019/135] Switch filament temp mixing to warning level and add pre-slice confirmation when allowed --- src/libslic3r/Print.cpp | 12 -------- src/slic3r/GUI/Plater.cpp | 62 ++++++++++++++++++++++++++++++++------- 2 files changed, 52 insertions(+), 22 deletions(-) diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index 53f81a4e8ca..1d76734ff2d 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -1096,18 +1096,6 @@ StringObjectException Print::validate(StringObjectException *warning, Polygons* if (extruders.empty()) return { L("No extrusions under current settings.") }; - // High/low temperature filament mixing check — enforced at both the - // real-time UI level (Plater::check_filament_temp_mixing) and here in - // validate() so that the background process does not clear the error - // notification (see Plater::priv::update line ~6507). - if (extruders.size() > 1) { - auto ret = check_multi_filament_valid(*this); - if (!ret.string.empty()) { - ret.type = STRING_EXCEPT_FILAMENTS_DIFFERENT_TEMP; - return ret; - } - } - if (m_config.print_sequence == PrintSequence::ByObject) { if (m_config.timelapse_type == TimelapseType::tlSmooth) return {L("Smooth mode of timelapse is not supported when \"by object\" sequence is enabled.")}; diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 41e2e4d253a..6fb5ede1d18 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -2099,10 +2099,18 @@ void Sidebar::update_presets(Preset::Type preset_type) err.string = _u8L("Cannot print multiple filaments which have large difference of " "temperature together. Otherwise, the extruder and nozzle may " "be blocked or damaged during printing."); - p->plater->get_notification_manager()->push_validate_error_notification(err); - p->plater->get_partplate_list().get_curr_plate()->update_apply_result_invalid(true); + if (wxGetApp().app_config->get_bool("allow_filament_temp_mixing")) { + p->plater->get_notification_manager()->push_notification( + NotificationType::ValidateWarning, + NotificationManager::NotificationLevel::WarningNotificationLevel, + _u8L("WARNING:") + "\n" + err.string); + } else { + p->plater->get_notification_manager()->push_validate_error_notification(err); + p->plater->get_partplate_list().get_curr_plate()->update_apply_result_invalid(true); + } } else { p->plater->get_notification_manager()->close_notification_of_type(NotificationType::ValidateError); + p->plater->get_notification_manager()->close_notification_of_type(NotificationType::ValidateWarning); p->plater->get_partplate_list().get_curr_plate()->update_apply_result_invalid(false); } @@ -6522,22 +6530,30 @@ unsigned int Plater::priv::update_background_process(bool force_validation, bool this->partplate_list.get_curr_plate()->update_apply_result_invalid(false); notification_manager->set_all_slicing_errors_gray(true); notification_manager->close_notification_of_type(NotificationType::ValidateError); + notification_manager->close_notification_of_type(NotificationType::ValidateWarning); + // Pass a warning from validation and either show a notification, + // or hide the old one. + process_validation_warning(warning); } else { - // Filament mixing detected — show error and keep plate invalid + // Filament mixing detected StringObjectException err; err.type = STRING_EXCEPT_FILAMENTS_DIFFERENT_TEMP; err.string = _u8L("Cannot print multiple filaments which have large difference of " "temperature together. Otherwise, the extruder and nozzle may " "be blocked or damaged during printing."); - notification_manager->push_validate_error_notification(err); - this->partplate_list.get_curr_plate()->update_apply_result_invalid(true); + if (wxGetApp().app_config->get_bool("allow_filament_temp_mixing")) { + notification_manager->push_notification( + NotificationType::ValidateWarning, + NotificationManager::NotificationLevel::WarningNotificationLevel, + _u8L("WARNING:") + "\n" + err.string); + } else { + notification_manager->push_validate_error_notification(err); + this->partplate_list.get_curr_plate()->update_apply_result_invalid(true); + } } if (invalidated != Print::APPLY_STATUS_UNCHANGED && background_processing_enabled()) return_state |= UPDATE_BACKGROUND_PROCESS_RESTART; - // Pass a warning from validation and either show a notification, - // or hide the old one. - process_validation_warning(warning); if (printer_technology == ptFFF) { view3D->get_canvas3d()->reset_sequential_print_clearance(); view3D->get_canvas3d()->set_as_dirty(); @@ -8434,6 +8450,15 @@ void Plater::priv::on_action_open_project(SimpleEvent&) void Plater::priv::on_action_slice_plate(SimpleEvent&) { if (q != nullptr) { + // Pre-slice confirmation for high/low temp filament mixing + if (wxGetApp().app_config->get_bool("allow_filament_temp_mixing") + && !q->check_filament_temp_mixing()) { + wxMessageDialog dlg(q, _L("该组合可能导致风险,是否继续"), + _L("确认"), wxYES_NO | wxICON_WARNING); + if (dlg.ShowModal() != wxID_YES) + return; + } + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << ":received slice plate event\n" ; //BBS update extruder params and speed table before slicing const Slic3r::DynamicPrintConfig& config = wxGetApp().preset_bundle->full_config(); @@ -8453,6 +8478,15 @@ void Plater::priv::on_action_slice_plate(SimpleEvent&) void Plater::priv::on_action_slice_all(SimpleEvent&) { if (q != nullptr) { + // Pre-slice confirmation for high/low temp filament mixing + if (wxGetApp().app_config->get_bool("allow_filament_temp_mixing") + && !q->check_filament_temp_mixing()) { + wxMessageDialog dlg(q, _L("该组合可能导致风险,是否继续"), + _L("确认"), wxYES_NO | wxICON_WARNING); + if (dlg.ShowModal() != wxID_YES) + return; + } + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << ":received slice project event\n" ; //BBS update extruder params and speed table before slicing const Slic3r::DynamicPrintConfig& config = wxGetApp().preset_bundle->full_config(); @@ -14763,10 +14797,18 @@ void Plater::on_config_change(const DynamicPrintConfig &config) err.string = _u8L("Cannot print multiple filaments which have large difference of " "temperature together. Otherwise, the extruder and nozzle may " "be blocked or damaged during printing."); - p->notification_manager->push_validate_error_notification(err); - p->partplate_list.get_curr_plate()->update_apply_result_invalid(true); + if (wxGetApp().app_config->get_bool("allow_filament_temp_mixing")) { + p->notification_manager->push_notification( + NotificationType::ValidateWarning, + NotificationManager::NotificationLevel::WarningNotificationLevel, + _u8L("WARNING:") + "\n" + err.string); + } else { + p->notification_manager->push_validate_error_notification(err); + p->partplate_list.get_curr_plate()->update_apply_result_invalid(true); + } } else { p->notification_manager->close_notification_of_type(NotificationType::ValidateError); + p->notification_manager->close_notification_of_type(NotificationType::ValidateWarning); p->partplate_list.get_curr_plate()->update_apply_result_invalid(false); } } From 102fa6b320c570e2de4b86533b77c2c4a30a8b81 Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Thu, 4 Jun 2026 10:34:43 +0800 Subject: [PATCH 020/135] =?UTF-8?q?Fix=EF=BC=9Amodify=20machine=20start=20?= =?UTF-8?q?gcode?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../profiles/Snapmaker/machine/Snapmaker U1 (0.2 nozzle).json | 2 +- .../profiles/Snapmaker/machine/Snapmaker U1 (0.4 nozzle).json | 2 +- .../profiles/Snapmaker/machine/Snapmaker U1 (0.6 nozzle).json | 2 +- .../profiles/Snapmaker/machine/Snapmaker U1 (0.8 nozzle).json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.2 nozzle).json b/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.2 nozzle).json index c3ef510ca5e..12b3bb62e5b 100644 --- a/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.2 nozzle).json +++ b/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.2 nozzle).json @@ -48,7 +48,7 @@ "20", "12" ], - "machine_start_gcode": ";===== date: 20260128 =====================\\n\\nPRINT_START\\nDEFECT_DETECTION_START\\nSET_PRINT_STATS_INFO TOTAL_LAYER={total_layer_count} CURRENT_LAYER=0\\nTIMELAPSE_START\\nM140 S{bed_temperature_initial_layer_single}\\nM104 T{initial_extruder} S140\\nM204 S10000\\n{if chamber_cooling_mode==0}\\n; 保温\\nSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=180\\n{elsif chamber_cooling_mode==2}\\n; 强冷\\nSET_PURIFIER_MODE MODE=1 DESIRE_TEMP=42 ALARM_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=0\\n{else}\\n; 弱冷\\nSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=0 FAN_SPEED=0.6 DELAY_OFF=180\t\t\\n{endif}\\nG28 X Y\\n;===== 床面异物检测 ========\\nT{initial_extruder}\\nG90\\nDEFECT_DETECTION_DETECT_BED\\n;===== 取放头检测 =================\\nSM_PRINT_CHECK_SWITCH_EXTRUDER\\n\\n;===== 自动进料 & 挤出流量 & 预挤出 ======================\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=1 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=0\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=0\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=2 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=1\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=1\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=3 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=2\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=2\\nSM_PRINT_AUTO_FEED EXTRUDER=3\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=3\\nM104 S0 T0 A0\\nM104 S0 T1 A0\\nM104 S0 T2 A0\\nM104 S0 T3 A0\\nM104 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\n\\n;===== 粗回零 =================\\nT{initial_extruder}\\nM106 S255\\nM106 P2 S0\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\nROUGHLY_CLEAN_NOZZLE_WITH_DISCARD\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nG28 Z I140 J140\\n\\n;===== 检测钢板 =================\\nDETECT_BED_PLATE\\n\\n;===== 深度清洁喷嘴 =================\\nG90\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 S{nozzle_temperature[initial_extruder] - 50}\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_1\\nM104 S{nozzle_temperature[initial_extruder] - 90}\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_2\\n\\n;===== 精回零 =================\\nM106 S255\\nM109 S{nozzle_temperature[initial_extruder] - 90}\\nM190 S{bed_temperature_initial_layer_single}\\nM107 P2\\nG90\\nG0 Z5 F10000\\nWAIT_CHAMBER_TEMP TIMEOUT=180\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{else} \\nG28 Z \\n{endif} \\n\\n\\n;===== 热床调平 =================\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{else} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 \\n{endif} \\n\\n\\n;===== 画起始线 =================\\nG90\\nG1 Z1.5\\nG0 X85 Y1 Z2 F18000\\nM109 S{nozzle_temperature_initial_layer[initial_extruder]}\\nG1 Z0.2\\nM83\\nG1 X185 E15 F360\\nG1 Z1.5\\n\\nG90\\nM106 S0", + "machine_start_gcode": ";===== date: 20260128 =====================\\n\\nPRINT_START\\nDEFECT_DETECTION_START\\nSET_PRINT_STATS_INFO TOTAL_LAYER={total_layer_count} CURRENT_LAYER=0\\nTIMELAPSE_START\\nM140 S{bed_temperature_initial_layer_single}\\nM104 T{initial_extruder} S140\\nM204 S10000\\n{if chamber_cooling_mode==0}\\n; 保温\\nSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=180\\n{elsif chamber_cooling_mode==2}\\n; 强冷\\nSET_PURIFIER_MODE MODE=1 DESIRE_TEMP=42 ALARM_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=0\\n{else}\\n; 弱冷\\nSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=0 FAN_SPEED=0.6 DELAY_OFF=180\t\t\\n{endif}\\nG28 X Y\\nDEFECT_DETECT_NOODLE_FIRST\\n;===== 床面异物检测 ========\\nT{initial_extruder}\\nG90\\nDEFECT_DETECTION_DETECT_BED\\n;===== 取放头检测 =================\\nSM_PRINT_CHECK_SWITCH_EXTRUDER\\n\\n;===== 自动进料 & 挤出流量 & 预挤出 ======================\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=1 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=0\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=0\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=2 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=1\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=1\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=3 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=2\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=2\\nSM_PRINT_AUTO_FEED EXTRUDER=3\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=3\\nM104 S0 T0 A0\\nM104 S0 T1 A0\\nM104 S0 T2 A0\\nM104 S0 T3 A0\\nM104 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\n\\n;===== 粗回零 =================\\nT{initial_extruder}\\nM106 S255\\nM106 P2 S0\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\nROUGHLY_CLEAN_NOZZLE_WITH_DISCARD\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nG28 Z I140 J140\\n\\n;===== 检测钢板 =================\\nDETECT_BED_PLATE\\n\\n;===== 深度清洁喷嘴 =================\\nG90\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 S{nozzle_temperature[initial_extruder] - 50}\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_1\\nM104 S{nozzle_temperature[initial_extruder] - 90}\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_2\\n\\n;===== 精回零 =================\\nM106 S255\\nM109 S{nozzle_temperature[initial_extruder] - 90}\\nM190 S{bed_temperature_initial_layer_single}\\nM107 P2\\nG90\\nG0 Z5 F10000\\nWAIT_CHAMBER_TEMP TIMEOUT=180\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{else} \\nG28 Z \\n{endif} \\n\\n\\n;===== 热床调平 =================\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{else} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 \\n{endif} \\n\\n\\n;===== 画起始线 =================\\nG90\\nG1 Z1.5\\nG0 X85 Y1 Z2 F18000\\nM109 S{nozzle_temperature_initial_layer[initial_extruder]}\\nG1 Z0.2\\nM83\\nG1 X185 E15 F360\\nG1 Z1.5\\n\\nG90\\nM106 S0", "machine_tool_change_time": "5", "max_layer_height": [ "0.14", diff --git a/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.4 nozzle).json b/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.4 nozzle).json index c9d896e1303..597c9684008 100644 --- a/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.4 nozzle).json +++ b/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.4 nozzle).json @@ -48,7 +48,7 @@ "20", "12" ], - "machine_start_gcode": ";===== date: 20260128 =====================\\n\\nPRINT_START\\nDEFECT_DETECTION_START\\nSET_PRINT_STATS_INFO TOTAL_LAYER={total_layer_count} CURRENT_LAYER=0\\nTIMELAPSE_START\\nM140 S{bed_temperature_initial_layer_single}\\nM104 T{initial_extruder} S140\\nM204 S10000\\n{if chamber_cooling_mode==0}\\n; 保温\\nSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=180\\n{elsif chamber_cooling_mode==2}\\n; 强冷\\nSET_PURIFIER_MODE MODE=1 DESIRE_TEMP=42 ALARM_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=0\\n{else}\\n; 弱冷\\nSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=0 FAN_SPEED=0.6 DELAY_OFF=180\t\t\\n{endif}\\nG28 X Y\\n;===== 床面异物检测 ========\\nT{initial_extruder}\\nG90\\nDEFECT_DETECTION_DETECT_BED\\n;===== 取放头检测 =================\\nSM_PRINT_CHECK_SWITCH_EXTRUDER\\n\\n;===== 自动进料 & 挤出流量 & 预挤出 ======================\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=1 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=0\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=0\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=2 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=1\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=1\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=3 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=2\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=2\\nSM_PRINT_AUTO_FEED EXTRUDER=3\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=3\\nM104 S0 T0 A0\\nM104 S0 T1 A0\\nM104 S0 T2 A0\\nM104 S0 T3 A0\\nM104 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\n\\n;===== 粗回零 =================\\nT{initial_extruder}\\nM106 S255\\nM106 P2 S0\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\nROUGHLY_CLEAN_NOZZLE_WITH_DISCARD\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nG28 Z I140 J140\\n\\n;===== 检测钢板 =================\\nDETECT_BED_PLATE\\n\\n;===== 深度清洁喷嘴 =================\\nG90\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 S{nozzle_temperature[initial_extruder] - 50}\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_1\\nM104 S{nozzle_temperature[initial_extruder] - 90}\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_2\\n\\n;===== 精回零 =================\\nM106 S255\\nM109 S{nozzle_temperature[initial_extruder] - 90}\\nM190 S{bed_temperature_initial_layer_single}\\nM107 P2\\nG90\\nG0 Z5 F10000\\nWAIT_CHAMBER_TEMP TIMEOUT=180\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{else} \\nG28 Z \\n{endif} \\n\\n\\n;===== 热床调平 =================\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{else} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 \\n{endif} \\n\\n\\n;===== 画起始线 =================\\nG90\\nG1 Z1.5\\nG0 X85 Y1 Z2 F18000\\nM109 S{nozzle_temperature_initial_layer[initial_extruder]}\\nG1 Z0.2\\nM83\\nG1 X185 E15 F360\\nG1 Z1.5\\n\\nG90\\nM106 S0", + "machine_start_gcode": ";===== date: 20260128 =====================\\n\\nPRINT_START\\nDEFECT_DETECTION_START\\nSET_PRINT_STATS_INFO TOTAL_LAYER={total_layer_count} CURRENT_LAYER=0\\nTIMELAPSE_START\\nM140 S{bed_temperature_initial_layer_single}\\nM104 T{initial_extruder} S140\\nM204 S10000\\n{if chamber_cooling_mode==0}\\n; 保温\\nSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=180\\n{elsif chamber_cooling_mode==2}\\n; 强冷\\nSET_PURIFIER_MODE MODE=1 DESIRE_TEMP=42 ALARM_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=0\\n{else}\\n; 弱冷\\nSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=0 FAN_SPEED=0.6 DELAY_OFF=180\t\t\\n{endif}\\nG28 X Y\\nDEFECT_DETECT_NOODLE_FIRST\\n;===== 床面异物检测 ========\\nT{initial_extruder}\\nG90\\nDEFECT_DETECTION_DETECT_BED\\n;===== 取放头检测 =================\\nSM_PRINT_CHECK_SWITCH_EXTRUDER\\n\\n;===== 自动进料 & 挤出流量 & 预挤出 ======================\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=1 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=0\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=0\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=2 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=1\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=1\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=3 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=2\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=2\\nSM_PRINT_AUTO_FEED EXTRUDER=3\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=3\\nM104 S0 T0 A0\\nM104 S0 T1 A0\\nM104 S0 T2 A0\\nM104 S0 T3 A0\\nM104 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\n\\n;===== 粗回零 =================\\nT{initial_extruder}\\nM106 S255\\nM106 P2 S0\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\nROUGHLY_CLEAN_NOZZLE_WITH_DISCARD\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nG28 Z I140 J140\\n\\n;===== 检测钢板 =================\\nDETECT_BED_PLATE\\n\\n;===== 深度清洁喷嘴 =================\\nG90\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 S{nozzle_temperature[initial_extruder] - 50}\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_1\\nM104 S{nozzle_temperature[initial_extruder] - 90}\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_2\\n\\n;===== 精回零 =================\\nM106 S255\\nM109 S{nozzle_temperature[initial_extruder] - 90}\\nM190 S{bed_temperature_initial_layer_single}\\nM107 P2\\nG90\\nG0 Z5 F10000\\nWAIT_CHAMBER_TEMP TIMEOUT=180\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{else} \\nG28 Z \\n{endif} \\n\\n\\n;===== 热床调平 =================\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{else} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 \\n{endif} \\n\\n\\n;===== 画起始线 =================\\nG90\\nG1 Z1.5\\nG0 X85 Y1 Z2 F18000\\nM109 S{nozzle_temperature_initial_layer[initial_extruder]}\\nG1 Z0.2\\nM83\\nG1 X185 E15 F360\\nG1 Z1.5\\n\\nG90\\nM106 S0", "machine_tool_change_time": "5", "max_layer_height": [ "0.32", diff --git a/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.6 nozzle).json b/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.6 nozzle).json index 441e975af86..9e6b751fa3c 100644 --- a/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.6 nozzle).json +++ b/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.6 nozzle).json @@ -48,7 +48,7 @@ "20", "12" ], - "machine_start_gcode": ";===== date: 20260128 =====================\\n\\nPRINT_START\\nDEFECT_DETECTION_START\\nSET_PRINT_STATS_INFO TOTAL_LAYER={total_layer_count} CURRENT_LAYER=0\\nTIMELAPSE_START\\nM140 S{bed_temperature_initial_layer_single}\\nM104 T{initial_extruder} S140\\nM204 S10000\\n{if chamber_cooling_mode==0}\\n; 保温\\nSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=180\\n{elsif chamber_cooling_mode==2}\\n; 强冷\\nSET_PURIFIER_MODE MODE=1 DESIRE_TEMP=42 ALARM_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=0\\n{else}\\n; 弱冷\\nSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=0 FAN_SPEED=0.6 DELAY_OFF=180\t\t\\n{endif}\\nG28 X Y\\n;===== 床面异物检测 ========\\nT{initial_extruder}\\nG90\\nDEFECT_DETECTION_DETECT_BED\\n;===== 取放头检测 =================\\nSM_PRINT_CHECK_SWITCH_EXTRUDER\\n\\n;===== 自动进料 & 挤出流量 & 预挤出 ======================\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=1 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=0\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=0\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=2 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=1\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=1\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=3 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=2\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=2\\nSM_PRINT_AUTO_FEED EXTRUDER=3\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=3\\nM104 S0 T0 A0\\nM104 S0 T1 A0\\nM104 S0 T2 A0\\nM104 S0 T3 A0\\nM104 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\n\\n;===== 粗回零 =================\\nT{initial_extruder}\\nM106 S255\\nM106 P2 S0\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\nROUGHLY_CLEAN_NOZZLE_WITH_DISCARD\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nG28 Z I140 J140\\n\\n;===== 检测钢板 =================\\nDETECT_BED_PLATE\\n\\n;===== 深度清洁喷嘴 =================\\nG90\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 S{nozzle_temperature[initial_extruder] - 50}\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_1\\nM104 S{nozzle_temperature[initial_extruder] - 90}\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_2\\n\\n;===== 精回零 =================\\nM106 S255\\nM109 S{nozzle_temperature[initial_extruder] - 90}\\nM190 S{bed_temperature_initial_layer_single}\\nM107 P2\\nG90\\nG0 Z5 F10000\\nWAIT_CHAMBER_TEMP TIMEOUT=180\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{else} \\nG28 Z \\n{endif} \\n\\n\\n;===== 热床调平 =================\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{else} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 \\n{endif} \\n\\n\\n;===== 画起始线 =================\\nG90\\nG1 Z1.5\\nG0 X85 Y1 Z2 F18000\\nM109 S{nozzle_temperature_initial_layer[initial_extruder]}\\nG1 Z0.2\\nM83\\nG1 X185 E15 F360\\nG1 Z1.5\\n\\nG90\\nM106 S0", + "machine_start_gcode": ";===== date: 20260128 =====================\\n\\nPRINT_START\\nDEFECT_DETECTION_START\\nSET_PRINT_STATS_INFO TOTAL_LAYER={total_layer_count} CURRENT_LAYER=0\\nTIMELAPSE_START\\nM140 S{bed_temperature_initial_layer_single}\\nM104 T{initial_extruder} S140\\nM204 S10000\\n{if chamber_cooling_mode==0}\\n; 保温\\nSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=180\\n{elsif chamber_cooling_mode==2}\\n; 强冷\\nSET_PURIFIER_MODE MODE=1 DESIRE_TEMP=42 ALARM_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=0\\n{else}\\n; 弱冷\\nSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=0 FAN_SPEED=0.6 DELAY_OFF=180\t\t\\n{endif}\\nG28 X Y\\nDEFECT_DETECT_NOODLE_FIRST\\n;===== 床面异物检测 ========\\nT{initial_extruder}\\nG90\\nDEFECT_DETECTION_DETECT_BED\\n;===== 取放头检测 =================\\nSM_PRINT_CHECK_SWITCH_EXTRUDER\\n\\n;===== 自动进料 & 挤出流量 & 预挤出 ======================\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=1 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=0\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=0\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=2 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=1\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=1\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=3 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=2\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=2\\nSM_PRINT_AUTO_FEED EXTRUDER=3\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=3\\nM104 S0 T0 A0\\nM104 S0 T1 A0\\nM104 S0 T2 A0\\nM104 S0 T3 A0\\nM104 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\n\\n;===== 粗回零 =================\\nT{initial_extruder}\\nM106 S255\\nM106 P2 S0\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\nROUGHLY_CLEAN_NOZZLE_WITH_DISCARD\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nG28 Z I140 J140\\n\\n;===== 检测钢板 =================\\nDETECT_BED_PLATE\\n\\n;===== 深度清洁喷嘴 =================\\nG90\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 S{nozzle_temperature[initial_extruder] - 50}\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_1\\nM104 S{nozzle_temperature[initial_extruder] - 90}\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_2\\n\\n;===== 精回零 =================\\nM106 S255\\nM109 S{nozzle_temperature[initial_extruder] - 90}\\nM190 S{bed_temperature_initial_layer_single}\\nM107 P2\\nG90\\nG0 Z5 F10000\\nWAIT_CHAMBER_TEMP TIMEOUT=180\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{else} \\nG28 Z \\n{endif} \\n\\n\\n;===== 热床调平 =================\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{else} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 \\n{endif} \\n\\n\\n;===== 画起始线 =================\\nG90\\nG1 Z1.5\\nG0 X85 Y1 Z2 F18000\\nM109 S{nozzle_temperature_initial_layer[initial_extruder]}\\nG1 Z0.2\\nM83\\nG1 X185 E15 F360\\nG1 Z1.5\\n\\nG90\\nM106 S0", "machine_tool_change_time": "5", "max_layer_height": [ "0.42", diff --git a/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.8 nozzle).json b/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.8 nozzle).json index 1c8255b1505..7c69a864f55 100644 --- a/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.8 nozzle).json +++ b/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.8 nozzle).json @@ -48,7 +48,7 @@ "20", "12" ], - "machine_start_gcode": ";===== date: 20260128 =====================\\n\\nPRINT_START\\nDEFECT_DETECTION_START\\nSET_PRINT_STATS_INFO TOTAL_LAYER={total_layer_count} CURRENT_LAYER=0\\nTIMELAPSE_START\\nM140 S{bed_temperature_initial_layer_single}\\nM104 T{initial_extruder} S140\\nM204 S10000\\n{if chamber_cooling_mode==0}\\n; 保温\\nSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=180\\n{elsif chamber_cooling_mode==2}\\n; 强冷\\nSET_PURIFIER_MODE MODE=1 DESIRE_TEMP=42 ALARM_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=0\\n{else}\\n; 弱冷\\nSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=0 FAN_SPEED=0.6 DELAY_OFF=180\t\t\\n{endif}\\nG28 X Y\\n;===== 床面异物检测 ========\\nT{initial_extruder}\\nG90\\nDEFECT_DETECTION_DETECT_BED\\n;===== 取放头检测 =================\\nSM_PRINT_CHECK_SWITCH_EXTRUDER\\n\\n;===== 自动进料 & 挤出流量 & 预挤出 ======================\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=1 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=0\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=0\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=2 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=1\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=1\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=3 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=2\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=2\\nSM_PRINT_AUTO_FEED EXTRUDER=3\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=3\\nM104 S0 T0 A0\\nM104 S0 T1 A0\\nM104 S0 T2 A0\\nM104 S0 T3 A0\\nM104 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\n\\n;===== 粗回零 =================\\nT{initial_extruder}\\nM106 S255\\nM106 P2 S0\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\nROUGHLY_CLEAN_NOZZLE_WITH_DISCARD\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nG28 Z I140 J140\\n\\n;===== 检测钢板 =================\\nDETECT_BED_PLATE\\n\\n;===== 深度清洁喷嘴 =================\\nG90\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 S{nozzle_temperature[initial_extruder] - 50}\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_1\\nM104 S{nozzle_temperature[initial_extruder] - 90}\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_2\\n\\n;===== 精回零 =================\\nM106 S255\\nM109 S{nozzle_temperature[initial_extruder] - 90}\\nM190 S{bed_temperature_initial_layer_single}\\nM107 P2\\nG90\\nG0 Z5 F10000\\nWAIT_CHAMBER_TEMP TIMEOUT=180\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{else} \\nG28 Z \\n{endif} \\n\\n\\n;===== 热床调平 =================\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{else} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 \\n{endif} \\n\\n\\n;===== 画起始线 =================\\nG90\\nG1 Z1.5\\nG0 X85 Y1 Z2 F18000\\nM109 S{nozzle_temperature_initial_layer[initial_extruder]}\\nG1 Z0.2\\nM83\\nG1 X185 E15 F360\\nG1 Z1.5\\n\\nG90\\nM106 S0", + "machine_start_gcode": ";===== date: 20260128 =====================\\n\\nPRINT_START\\nDEFECT_DETECTION_START\\nSET_PRINT_STATS_INFO TOTAL_LAYER={total_layer_count} CURRENT_LAYER=0\\nTIMELAPSE_START\\nM140 S{bed_temperature_initial_layer_single}\\nM104 T{initial_extruder} S140\\nM204 S10000\\n{if chamber_cooling_mode==0}\\n; 保温\\nSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=180\\n{elsif chamber_cooling_mode==2}\\n; 强冷\\nSET_PURIFIER_MODE MODE=1 DESIRE_TEMP=42 ALARM_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=0\\n{else}\\n; 弱冷\\nSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=0 FAN_SPEED=0.6 DELAY_OFF=180\t\t\\n{endif}\\nG28 X Y\\nDEFECT_DETECT_NOODLE_FIRST\\n;===== 床面异物检测 ========\\nT{initial_extruder}\\nG90\\nDEFECT_DETECTION_DETECT_BED\\n;===== 取放头检测 =================\\nSM_PRINT_CHECK_SWITCH_EXTRUDER\\n\\n;===== 自动进料 & 挤出流量 & 预挤出 ======================\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=1 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=0\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=0\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=2 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=1\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=1\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=3 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=2\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=2\\nSM_PRINT_AUTO_FEED EXTRUDER=3\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=3\\nM104 S0 T0 A0\\nM104 S0 T1 A0\\nM104 S0 T2 A0\\nM104 S0 T3 A0\\nM104 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\n\\n;===== 粗回零 =================\\nT{initial_extruder}\\nM106 S255\\nM106 P2 S0\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\nROUGHLY_CLEAN_NOZZLE_WITH_DISCARD\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nG28 Z I140 J140\\n\\n;===== 检测钢板 =================\\nDETECT_BED_PLATE\\n\\n;===== 深度清洁喷嘴 =================\\nG90\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 S{nozzle_temperature[initial_extruder] - 50}\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_1\\nM104 S{nozzle_temperature[initial_extruder] - 90}\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_2\\n\\n;===== 精回零 =================\\nM106 S255\\nM109 S{nozzle_temperature[initial_extruder] - 90}\\nM190 S{bed_temperature_initial_layer_single}\\nM107 P2\\nG90\\nG0 Z5 F10000\\nWAIT_CHAMBER_TEMP TIMEOUT=180\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{else} \\nG28 Z \\n{endif} \\n\\n\\n;===== 热床调平 =================\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{else} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 \\n{endif} \\n\\n\\n;===== 画起始线 =================\\nG90\\nG1 Z1.5\\nG0 X85 Y1 Z2 F18000\\nM109 S{nozzle_temperature_initial_layer[initial_extruder]}\\nG1 Z0.2\\nM83\\nG1 X185 E15 F360\\nG1 Z1.5\\n\\nG90\\nM106 S0", "machine_tool_change_time": "5", "max_layer_height": [ "0.56", From ec293e7bfadbd4990fb9f898771875be351b96e8 Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Thu, 4 Jun 2026 11:08:17 +0800 Subject: [PATCH 021/135] Remove .hermes/ from git tracking --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 46bdf21dd19..1914b7393fa 100644 --- a/.gitignore +++ b/.gitignore @@ -40,3 +40,4 @@ resources/profiles/user/default *.code-workspace deps_src/build/ .claude/ +.hermes/ From 651e527afec2bcd5e14acd297bba673321427c07 Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Thu, 4 Jun 2026 11:42:07 +0800 Subject: [PATCH 022/135] Fix build errors: WipeTower2::generate signature, Plater method mismatches, and missing Sidebar stubs --- src/libslic3r/Print.cpp | 3 ++- src/slic3r/GUI/Plater.cpp | 24 ++++++++++++++++++++++-- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index 1d76734ff2d..e856242ece9 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -2893,7 +2893,8 @@ void Print::_make_wipe_tower() // Generate the wipe tower layers. m_wipe_tower_data.tool_changes.reserve(m_wipe_tower_data.tool_ordering.layer_tools().size()); - wipe_tower.generate(m_wipe_tower_data.tool_changes); + m_wipe_tower_data.local_z_tool_changes.reserve(m_wipe_tower_data.tool_ordering.layer_tools().size()); + wipe_tower.generate(m_wipe_tower_data.tool_changes, m_wipe_tower_data.local_z_tool_changes); m_wipe_tower_data.depth = wipe_tower.get_depth(); m_wipe_tower_data.z_and_depth_pairs = wipe_tower.get_z_and_depth_pairs(); m_wipe_tower_data.brim_width = wipe_tower.get_brim_width(); diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 6fb5ede1d18..36a36364756 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -2499,6 +2499,26 @@ void Sidebar::edit_filament() { p->editing_filament = p->m_menu_filament_id; // sync with TabPresetComboxBox's m_filament_idx } +void Sidebar::update_color_mix_panel() +{ + // Stub: mixed filament color panel not yet implemented in this version. +} + +void Sidebar::update_mixed_filament_panel(bool sync_manager) +{ + // Stub: mixed filament panel not yet implemented in this version. +} + +std::vector Sidebar::get_ui_ordered_filament_ids() const +{ + const size_t num_physical = static_cast(std::max(wxGetApp().filaments_cnt(), 0)); + std::vector ordered_filament_ids; + ordered_filament_ids.reserve(num_physical); + for (size_t idx = 0; idx < num_physical; ++idx) + ordered_filament_ids.emplace_back(unsigned(idx + 1)); + return ordered_filament_ids; +} + void Sidebar::change_filament(size_t from_id, size_t to_id) @@ -14517,7 +14537,7 @@ bool Plater::search_string_getter(int idx, const char** label, const char** tool return false; } -void Plater::on_filaments_delete(size_t num_filaments, size_t filament_id, int replace_filament_id) +void Plater::on_filaments_delete(size_t num_filaments, size_t filament_id, int replace_filament_id, const std::vector& is_mixed_snapshot) { // only update elements in plater update_filament_colors_in_full_config(); @@ -14888,7 +14908,7 @@ void Plater::on_activate() } // Get vector of extruder colors considering filament color, if extruder color is undefined. -std::vector Plater::get_extruder_colors_from_plater_config(const GCodeProcessorResult* const result) const +std::vector Plater::get_extruder_colors_from_plater_config(const GCodeProcessorResult* const result, bool include_mixed) const { if (wxGetApp().is_gcode_viewer() && result != nullptr) return result->extruder_colors; From 46bc0edd5976cde92c619ae3182c35d2c045fd45 Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Thu, 4 Jun 2026 17:29:36 +0800 Subject: [PATCH 023/135] Replace chamber cooling magic numbers with named constants Co-Authored-By: Claude Opus 4.7 --- src/libslic3r/GCode.cpp | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index 21af85e6363..91d3a4c45a0 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -2700,17 +2700,21 @@ void GCode::_do_export(Print& print, GCodeOutputStream& file, ThumbnailsGenerato } // Compute chamber cooling mode based on all filaments used on this plate. - // 0 = keep warm (all high-temp), 1 = weak cooling, 2 = strong cooling. + constexpr int kChamberCoolingKeepWarm = 0; + constexpr int kChamberCoolingWeak = 1; + constexpr int kChamberCoolingStrong = 2; + constexpr int kVitrificationStrongCool = 50; // <= 50: trigger strong cooling + constexpr int kVitrificationWeakCool = 70; // <= 70: trigger weak cooling (and > 50) { - int chamber_cooling_mode = 0; + int chamber_cooling_mode = kChamberCoolingKeepWarm; for (unsigned int extruder : tool_ordering.all_extruders()) { if (!m_config.filament_is_high_temperature.get_at(extruder)) { int vitrification = m_config.temperature_vitrification.get_at(extruder); - if (vitrification <= 50) { - chamber_cooling_mode = 2; + if (vitrification <= kVitrificationStrongCool) { + chamber_cooling_mode = kChamberCoolingStrong; break; - } else if (vitrification <= 70) { - chamber_cooling_mode = 1; + } else if (vitrification <= kVitrificationWeakCool) { + chamber_cooling_mode = kChamberCoolingWeak; } } } From 8d4bd705b0a9c5247b97890c8f7f40a1e710cdd7 Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Thu, 4 Jun 2026 17:32:53 +0800 Subject: [PATCH 024/135] Rename in_handler to reentry_guard for clarity Co-Authored-By: Claude Opus 4.7 --- src/slic3r/GUI/Preferences.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/slic3r/GUI/Preferences.cpp b/src/slic3r/GUI/Preferences.cpp index 40f11f7ff62..613e064f698 100644 --- a/src/slic3r/GUI/Preferences.cpp +++ b/src/slic3r/GUI/Preferences.cpp @@ -746,17 +746,17 @@ wxBoxSizer *PreferencesDialog::create_item_checkbox(wxString title, wxWindow *pa //// save config - auto in_handler = std::make_shared(false); - checkbox->Bind(wxEVT_TOGGLEBUTTON, [this, checkbox, param, confirm_cb, in_handler](wxCommandEvent &e) { - if (*in_handler) { e.Skip(); return; } + auto reentry_guard = std::make_shared(false); + checkbox->Bind(wxEVT_TOGGLEBUTTON, [this, checkbox, param, confirm_cb, reentry_guard](wxCommandEvent &e) { + if (*reentry_guard) { e.Skip(); return; } if (confirm_cb) { bool old_val = !checkbox->GetValue(); bool new_val = checkbox->GetValue(); bool final_val = confirm_cb(new_val, old_val); if (final_val != new_val) { - *in_handler = true; + *reentry_guard = true; checkbox->SetValue(final_val); - *in_handler = false; + *reentry_guard = false; } } app_config->set_bool(param, checkbox->GetValue()); From 644d6d748a6e49873744dbe366ca99bf4a62553e Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Thu, 4 Jun 2026 19:08:54 +0800 Subject: [PATCH 025/135] Add missing trigger points for filament temp mixing detection Consolidate duplicated notification logic into sync_filament_temp_mixing_notification() and hook it into model add/remove (object_list_changed), filament assignment changes (set_extruder_for_selected_items, update_filament_in_config), and MMU painting (update_model_object) so the mixing warning updates immediately when plate composition changes. Co-Authored-By: Claude Opus 4.7 --- src/slic3r/GUI/GUI_ObjectList.cpp | 2 + .../GUI/Gizmos/GLGizmoMmuSegmentation.cpp | 1 + src/slic3r/GUI/Plater.cpp | 94 +++++++------------ src/slic3r/GUI/Plater.hpp | 6 ++ 4 files changed, 41 insertions(+), 62 deletions(-) diff --git a/src/slic3r/GUI/GUI_ObjectList.cpp b/src/slic3r/GUI/GUI_ObjectList.cpp index 4c5c16e21e8..6f8186a9634 100644 --- a/src/slic3r/GUI/GUI_ObjectList.cpp +++ b/src/slic3r/GUI/GUI_ObjectList.cpp @@ -1100,6 +1100,7 @@ void ObjectList::update_filament_in_config(const wxDataViewItem& item) // update scene wxGetApp().plater()->update(); + wxGetApp().plater()->sync_filament_temp_mixing_notification(); } void ObjectList::update_name_in_model(const wxDataViewItem& item) const @@ -5922,6 +5923,7 @@ void ObjectList::set_extruder_for_selected_items(const int extruder) // update scene wxGetApp().plater()->update(); + wxGetApp().plater()->sync_filament_temp_mixing_notification(); // BBS: update extruder/filament column Refresh(); diff --git a/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp b/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp index 4f2704be870..0435434a4e9 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp @@ -1000,6 +1000,7 @@ void GLGizmoMmuSegmentation::update_model_object() wxGetApp().obj_list()->update_info_items(obj_idx); wxGetApp().plater()->get_partplate_list().notify_instance_update(obj_idx, 0); m_parent.post_event(SimpleEvent(EVT_GLCANVAS_SCHEDULE_BACKGROUND_PROCESS)); + wxGetApp().plater()->sync_filament_temp_mixing_notification(); } } diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 36a36364756..4ad6334a54f 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -2093,26 +2093,7 @@ void Sidebar::update_presets(Preset::Type preset_type) update_dynamic_filament_list(); // Real-time high/low temperature filament mixing check after filament preset change - if (!p->plater->check_filament_temp_mixing()) { - StringObjectException err; - err.type = STRING_EXCEPT_FILAMENTS_DIFFERENT_TEMP; - err.string = _u8L("Cannot print multiple filaments which have large difference of " - "temperature together. Otherwise, the extruder and nozzle may " - "be blocked or damaged during printing."); - if (wxGetApp().app_config->get_bool("allow_filament_temp_mixing")) { - p->plater->get_notification_manager()->push_notification( - NotificationType::ValidateWarning, - NotificationManager::NotificationLevel::WarningNotificationLevel, - _u8L("WARNING:") + "\n" + err.string); - } else { - p->plater->get_notification_manager()->push_validate_error_notification(err); - p->plater->get_partplate_list().get_curr_plate()->update_apply_result_invalid(true); - } - } else { - p->plater->get_notification_manager()->close_notification_of_type(NotificationType::ValidateError); - p->plater->get_notification_manager()->close_notification_of_type(NotificationType::ValidateWarning); - p->plater->get_partplate_list().get_curr_plate()->update_apply_result_invalid(false); - } + p->plater->sync_filament_temp_mixing_notification(); break; } @@ -6047,6 +6028,8 @@ void Plater::priv::object_list_changed() main_frame->update_slice_print_status(MainFrame::eEventObjectUpdate, can_slice); wxGetApp().params_panel()->notify_object_config_changed(); + + q->sync_filament_temp_mixing_notification(); } void Plater::priv::select_curr_plate_all() @@ -6545,31 +6528,10 @@ unsigned int Plater::priv::update_background_process(bool force_validation, bool // cases (e.g. wall_filament changes that haven't propagated to // PrintRegions yet). check_filament_temp_mixing() reads directly // from full_config() and is always current. - bool filament_ok = q->check_filament_temp_mixing(); + bool filament_ok = q->sync_filament_temp_mixing_notification(); if (filament_ok) { - this->partplate_list.get_curr_plate()->update_apply_result_invalid(false); notification_manager->set_all_slicing_errors_gray(true); - notification_manager->close_notification_of_type(NotificationType::ValidateError); - notification_manager->close_notification_of_type(NotificationType::ValidateWarning); - // Pass a warning from validation and either show a notification, - // or hide the old one. process_validation_warning(warning); - } else { - // Filament mixing detected - StringObjectException err; - err.type = STRING_EXCEPT_FILAMENTS_DIFFERENT_TEMP; - err.string = _u8L("Cannot print multiple filaments which have large difference of " - "temperature together. Otherwise, the extruder and nozzle may " - "be blocked or damaged during printing."); - if (wxGetApp().app_config->get_bool("allow_filament_temp_mixing")) { - notification_manager->push_notification( - NotificationType::ValidateWarning, - NotificationManager::NotificationLevel::WarningNotificationLevel, - _u8L("WARNING:") + "\n" + err.string); - } else { - notification_manager->push_validate_error_notification(err); - this->partplate_list.get_curr_plate()->update_apply_result_invalid(true); - } } if (invalidated != Print::APPLY_STATUS_UNCHANGED && background_processing_enabled()) return_state |= UPDATE_BACKGROUND_PROCESS_RESTART; @@ -14721,6 +14683,33 @@ bool Plater::check_filament_temp_mixing() return compatible; } +bool Plater::sync_filament_temp_mixing_notification() +{ + if (check_filament_temp_mixing()) { + get_notification_manager()->close_notification_of_type(NotificationType::ValidateError); + get_notification_manager()->close_notification_of_type(NotificationType::ValidateWarning); + get_partplate_list().get_curr_plate()->update_apply_result_invalid(false); + return true; + } + + StringObjectException err; + err.type = STRING_EXCEPT_FILAMENTS_DIFFERENT_TEMP; + err.string = _u8L("Cannot print multiple filaments which have large difference of " + "temperature together. Otherwise, the extruder and nozzle may " + "be blocked or damaged during printing."); + + if (wxGetApp().app_config->get_bool("allow_filament_temp_mixing")) { + get_notification_manager()->push_notification( + NotificationType::ValidateWarning, + NotificationManager::NotificationLevel::WarningNotificationLevel, + _u8L("WARNING:") + "\n" + err.string); + } else { + get_notification_manager()->push_validate_error_notification(err); + get_partplate_list().get_curr_plate()->update_apply_result_invalid(true); + } + return false; +} + void Plater::on_config_change(const DynamicPrintConfig &config) { bool update_scheduled = false; @@ -14811,26 +14800,7 @@ void Plater::on_config_change(const DynamicPrintConfig &config) } // Real-time high/low temperature filament mixing check - if (!check_filament_temp_mixing()) { - StringObjectException err; - err.type = STRING_EXCEPT_FILAMENTS_DIFFERENT_TEMP; - err.string = _u8L("Cannot print multiple filaments which have large difference of " - "temperature together. Otherwise, the extruder and nozzle may " - "be blocked or damaged during printing."); - if (wxGetApp().app_config->get_bool("allow_filament_temp_mixing")) { - p->notification_manager->push_notification( - NotificationType::ValidateWarning, - NotificationManager::NotificationLevel::WarningNotificationLevel, - _u8L("WARNING:") + "\n" + err.string); - } else { - p->notification_manager->push_validate_error_notification(err); - p->partplate_list.get_curr_plate()->update_apply_result_invalid(true); - } - } else { - p->notification_manager->close_notification_of_type(NotificationType::ValidateError); - p->notification_manager->close_notification_of_type(NotificationType::ValidateWarning); - p->partplate_list.get_curr_plate()->update_apply_result_invalid(false); - } + sync_filament_temp_mixing_notification(); } void Plater::set_bed_shape() const diff --git a/src/slic3r/GUI/Plater.hpp b/src/slic3r/GUI/Plater.hpp index def70264758..a11ef680bef 100644 --- a/src/slic3r/GUI/Plater.hpp +++ b/src/slic3r/GUI/Plater.hpp @@ -507,6 +507,12 @@ class Plater: public wxPanel bool update_filament_colors_in_full_config(); void config_change_notification(const DynamicPrintConfig &config, const std::string& key); void on_config_change(const DynamicPrintConfig &config); + /// Check whether high-temperature and low-temperature filaments are mixed on the current plate. + /// Returns true if compatible (no mixing), false if mixing is detected and error notification is shown. + bool check_filament_temp_mixing(); + /// Sync notification state with current filament temp mixing status. + /// Returns true if compatible (no mixing), false if mixing detected. + bool sync_filament_temp_mixing_notification(); void force_filament_colors_update(); void force_print_bed_update(); // On activating the parent window. From 1ead23e31f5921584b7ef076af8f13809612fc22 Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Thu, 4 Jun 2026 19:19:47 +0800 Subject: [PATCH 026/135] Add filament temp mixing check triggers for object move-off-plate Hook sync_filament_temp_mixing_notification() into Selection::notify_instance_update (3D view drag) and ObjectList::reload_all_plates (arrange, file load, plate operations) so the mixing warning updates immediately when objects are moved on/off plates. Co-Authored-By: Claude Opus 4.7 --- src/slic3r/GUI/GUI_ObjectList.cpp | 1 + src/slic3r/GUI/Selection.cpp | 1 + 2 files changed, 2 insertions(+) diff --git a/src/slic3r/GUI/GUI_ObjectList.cpp b/src/slic3r/GUI/GUI_ObjectList.cpp index 6f8186a9634..ea2064334f0 100644 --- a/src/slic3r/GUI/GUI_ObjectList.cpp +++ b/src/slic3r/GUI/GUI_ObjectList.cpp @@ -5983,6 +5983,7 @@ void ObjectList::reload_all_plates(bool notify_partplate) wxGetApp().plater()->update(); // update printable states on canvas wxGetApp().plater()->get_view3D_canvas3D()->update_instance_printable_state_for_objects(obj_idxs); + wxGetApp().plater()->sync_filament_temp_mixing_notification(); } void ObjectList::on_plate_selected(int plate_index) diff --git a/src/slic3r/GUI/Selection.cpp b/src/slic3r/GUI/Selection.cpp index bd087d2d55f..293727ed42b 100644 --- a/src/slic3r/GUI/Selection.cpp +++ b/src/slic3r/GUI/Selection.cpp @@ -1804,6 +1804,7 @@ void Selection::notify_instance_update(int object_idx, int instance_idx) else plate_list.notify_instance_update(object_idx, instance_idx); } + wxGetApp().plater()->sync_filament_temp_mixing_notification(); } void Selection::erase() From af03274be56be9c098ca2d518abded9981d22a91 Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Fri, 5 Jun 2026 10:16:21 +0800 Subject: [PATCH 027/135] Fix sync_filament_temp_mixing_notification placement for object move Move the sync call from Selection::notify_instance_update to the end of GLCanvas3D::do_move, so it runs only once after all plate membership updates are complete, avoiding flip-flopping between valid/invalid states. Co-Authored-By: Claude Opus 4.7 --- src/slic3r/GUI/GLCanvas3D.cpp | 2 ++ src/slic3r/GUI/Selection.cpp | 1 - 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp index d08ddd2414c..8538040aeff 100644 --- a/src/slic3r/GUI/GLCanvas3D.cpp +++ b/src/slic3r/GUI/GLCanvas3D.cpp @@ -4823,6 +4823,8 @@ void GLCanvas3D::do_move(const std::string& snapshot_type) reset_sequential_print_clearance(); + wxGetApp().plater()->sync_filament_temp_mixing_notification(); + m_dirty = true; } diff --git a/src/slic3r/GUI/Selection.cpp b/src/slic3r/GUI/Selection.cpp index 293727ed42b..bd087d2d55f 100644 --- a/src/slic3r/GUI/Selection.cpp +++ b/src/slic3r/GUI/Selection.cpp @@ -1804,7 +1804,6 @@ void Selection::notify_instance_update(int object_idx, int instance_idx) else plate_list.notify_instance_update(object_idx, instance_idx); } - wxGetApp().plater()->sync_filament_temp_mixing_notification(); } void Selection::erase() From 9b2b9970d4e804abd13f45675b9a10e61c9e5d85 Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Fri, 5 Jun 2026 10:25:42 +0800 Subject: [PATCH 028/135] Make check_filament_temp_mixing per-plate instead of global Limit plate config collection and ModelVolume scanning to objects on the current plate, so removing an object from the plate correctly clears the high/low temperature mixing error notification. Co-Authored-By: Claude Opus 4.7 --- src/slic3r/GUI/Plater.cpp | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 4ad6334a54f..62cfda227d3 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -14639,15 +14639,25 @@ bool Plater::check_filament_temp_mixing() // Collect from full_config first (global defaults for wall/infill/etc.) collect_from_cfg(full_cfg); - // Collect from every plate's config (per-plate overrides) - for (int i = 0; i < p->partplate_list.get_plate_count(); ++i) { - PartPlate* plate = p->partplate_list.get_plate(i); - if (plate) - collect_from_cfg(*plate->config()); - } - - // Collect from ModelVolume painting extruders (multi-material coloring) - for (const ModelObject* mo : wxGetApp().model().objects) { + // Collect from current plate's config only (per-plate overrides) + PartPlate* curr_plate = p->partplate_list.get_curr_plate(); + if (curr_plate) + collect_from_cfg(*curr_plate->config()); + + // Collect from ModelVolume painting extruders for objects on the current plate + for (size_t obj_idx = 0; obj_idx < wxGetApp().model().objects.size(); ++obj_idx) { + const ModelObject* mo = wxGetApp().model().objects[obj_idx]; + bool on_curr_plate = false; + if (curr_plate) { + for (int inst_idx = 0; inst_idx < (int)mo->instances.size(); ++inst_idx) { + if (curr_plate->contain_instance((int)obj_idx, inst_idx)) { + on_curr_plate = true; + break; + } + } + } + if (!on_curr_plate) + continue; for (const ModelVolume* mv : mo->volumes) { for (int eid : mv->get_extruders()) { if (eid >= 1 && eid <= num_filaments) From 0e545774c730316def0d5c896fa6e3b3611570ba Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Fri, 5 Jun 2026 10:33:08 +0800 Subject: [PATCH 029/135] Remove full_config collection from per-plate filament mixing check The global config's wall_filament etc. always reference the default filament slot, causing false mixing detection when only high-temp models remain on the plate. Only collect config from current plate. Co-Authored-By: Claude Opus 4.7 --- src/slic3r/GUI/Plater.cpp | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 62cfda227d3..ea945bec76e 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -14636,10 +14636,7 @@ bool Plater::check_filament_temp_mixing() } }; - // Collect from full_config first (global defaults for wall/infill/etc.) - collect_from_cfg(full_cfg); - - // Collect from current plate's config only (per-plate overrides) + // Collect from current plate's config only PartPlate* curr_plate = p->partplate_list.get_curr_plate(); if (curr_plate) collect_from_cfg(*curr_plate->config()); From 24e70925e5c0a2c13ddc488ac51c9680270bef00 Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Fri, 5 Jun 2026 11:23:31 +0800 Subject: [PATCH 030/135] Trim trailing whitespace Co-Authored-By: Claude Opus 4.7 --- src/slic3r/GUI/Preferences.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/slic3r/GUI/Preferences.cpp b/src/slic3r/GUI/Preferences.cpp index 613e064f698..d33efac592a 100644 --- a/src/slic3r/GUI/Preferences.cpp +++ b/src/slic3r/GUI/Preferences.cpp @@ -745,7 +745,7 @@ wxBoxSizer *PreferencesDialog::create_item_checkbox(wxString title, wxWindow *pa m_sizer_checkbox->Add(checkbox_title, 0, wxALIGN_CENTER | wxALL, 3); - //// save config + //// save config auto reentry_guard = std::make_shared(false); checkbox->Bind(wxEVT_TOGGLEBUTTON, [this, checkbox, param, confirm_cb, reentry_guard](wxCommandEvent &e) { if (*reentry_guard) { e.Skip(); return; } From 23f0939e142ffcf44d35a3620a6da3946deec56f Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Fri, 5 Jun 2026 11:29:12 +0800 Subject: [PATCH 031/135] Replace custom dialog with Orca-styled MessageDialog for filament mixing warning MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use MessageDialog with SetButtonLabel for proper OrcaSlicer button styling instead of manual wxButton with hardcoded green background. Buttons now read "确认"/"取消" with auto-applied Confirm/Regular ButtonStyle. Co-Authored-By: Claude Opus 4.7 --- src/slic3r/GUI/Preferences.cpp | 31 ++++--------------------------- 1 file changed, 4 insertions(+), 27 deletions(-) diff --git a/src/slic3r/GUI/Preferences.cpp b/src/slic3r/GUI/Preferences.cpp index d33efac592a..6433ca8a475 100644 --- a/src/slic3r/GUI/Preferences.cpp +++ b/src/slic3r/GUI/Preferences.cpp @@ -1241,38 +1241,15 @@ wxWindow* PreferencesDialog::create_general_page() if (!new_val) return false; - wxDialog dlg(this, wxID_ANY, _L("高/低温材料混打风险"), wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE); - dlg.SetBackgroundColour(*wxWHITE); - - auto* main_sizer = new wxBoxSizer(wxVERTICAL); - wxString msg = _L("混合使用打印温度差异较大的耗材,可能导致:\n" "· 喷嘴阻塞\n" "· 喷嘴损坏\n" "· 层间粘附问题\n\n" "仍要启用高/低温耗材混合打印吗?"); - auto* text = new wxStaticText(&dlg, wxID_ANY, msg); - text->Wrap(FromDIP(400)); - main_sizer->Add(text, 0, wxALL, FromDIP(20)); - - auto* btn_sizer = new wxBoxSizer(wxHORIZONTAL); - auto* btn_yes = new wxButton(&dlg, wxID_YES, _L("是")); - btn_yes->SetBackgroundColour(wxColour(0, 255, 0)); - auto* btn_no = new wxButton(&dlg, wxID_NO, _L("否")); - - btn_yes->Bind(wxEVT_BUTTON, [&dlg](wxCommandEvent&) { dlg.EndModal(wxID_YES); }); - btn_no->Bind(wxEVT_BUTTON, [&dlg](wxCommandEvent&) { dlg.EndModal(wxID_NO); }); - - btn_sizer->AddStretchSpacer(); - btn_sizer->Add(btn_yes, 0, wxALL, FromDIP(5)); - btn_sizer->Add(btn_no, 0, wxALL, FromDIP(5)); - - main_sizer->Add(btn_sizer, 0, wxEXPAND | wxALL, FromDIP(10)); - dlg.SetSizer(main_sizer); - dlg.Fit(); - dlg.CenterOnParent(); - - return dlg.ShowModal() == wxID_YES; + MessageDialog dlg(this, msg, _L("高/低温材料混打风险"), wxICON_WARNING | wxOK | wxCANCEL); + dlg.SetButtonLabel(wxID_OK, _L("确认")); + dlg.SetButtonLabel(wxID_CANCEL, _L("取消")); + return dlg.ShowModal() == wxID_OK; }); auto camera_orbit_mult = create_camera_orbit_mult_input(_L("Orbit speed multiplier"), page, _L("Multiplies the orbit speed for finer or coarser camera movement.")); From 80208e98c93157116991db0ddbc158f9059f4b6d Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Fri, 5 Jun 2026 11:39:00 +0800 Subject: [PATCH 032/135] Localize filament temp mixing UI strings from Chinese to English Replace hardcoded Chinese text with _L()/_u8L() wrapped English strings: - Preferences dialog title, body, and button labels - Error notification (allow=false): includes guidance to enable in Preferences - Warning notification (allow=true): concise message without guidance - Calibration wizard validation error Co-Authored-By: Claude Opus 4.7 --- src/slic3r/GUI/CalibrationWizardPresetPage.cpp | 2 +- src/slic3r/GUI/Plater.cpp | 11 +++++++---- src/slic3r/GUI/Preferences.cpp | 16 ++++++++-------- 3 files changed, 16 insertions(+), 13 deletions(-) diff --git a/src/slic3r/GUI/CalibrationWizardPresetPage.cpp b/src/slic3r/GUI/CalibrationWizardPresetPage.cpp index 581b9153bb2..c56de4139a2 100644 --- a/src/slic3r/GUI/CalibrationWizardPresetPage.cpp +++ b/src/slic3r/GUI/CalibrationWizardPresetPage.cpp @@ -1073,7 +1073,7 @@ bool CalibrationPresetPage::is_filaments_compatiable(const std::vector } if (!Print::check_multi_filaments_compatibility(filament_types)) { - error_tips = _u8L("Cannot print multiple filaments which have large difference of temperature together. Otherwise, the extruder and nozzle may be blocked or damaged during printing"); + error_tips = _u8L("Detected both high and low temperature materials. Mixed printing may result in extruder clogging, nozzle damage, or layer adhesion issues."); return false; } diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index ea945bec76e..fbb5e2d6775 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -14701,16 +14701,19 @@ bool Plater::sync_filament_temp_mixing_notification() StringObjectException err; err.type = STRING_EXCEPT_FILAMENTS_DIFFERENT_TEMP; - err.string = _u8L("Cannot print multiple filaments which have large difference of " - "temperature together. Otherwise, the extruder and nozzle may " - "be blocked or damaged during printing."); if (wxGetApp().app_config->get_bool("allow_filament_temp_mixing")) { get_notification_manager()->push_notification( NotificationType::ValidateWarning, NotificationManager::NotificationLevel::WarningNotificationLevel, - _u8L("WARNING:") + "\n" + err.string); + _u8L("WARNING:") + "\n" + + _u8L("Detected both high and low temperature materials. Mixed printing may " + "result in extruder clogging, nozzle damage, or layer adhesion issues.")); } else { + err.string = _u8L("Detected both high and low temperature materials. Mixed printing may " + "result in extruder clogging, nozzle damage, or layer adhesion issues. " + "To continue printing, enable \"Allow mixed printing of high and low " + "temperature materials\" in Preferences."); get_notification_manager()->push_validate_error_notification(err); get_partplate_list().get_curr_plate()->update_apply_result_invalid(true); } diff --git a/src/slic3r/GUI/Preferences.cpp b/src/slic3r/GUI/Preferences.cpp index 6433ca8a475..82291ebe2ca 100644 --- a/src/slic3r/GUI/Preferences.cpp +++ b/src/slic3r/GUI/Preferences.cpp @@ -1241,14 +1241,14 @@ wxWindow* PreferencesDialog::create_general_page() if (!new_val) return false; - wxString msg = _L("混合使用打印温度差异较大的耗材,可能导致:\n" - "· 喷嘴阻塞\n" - "· 喷嘴损坏\n" - "· 层间粘附问题\n\n" - "仍要启用高/低温耗材混合打印吗?"); - MessageDialog dlg(this, msg, _L("高/低温材料混打风险"), wxICON_WARNING | wxOK | wxCANCEL); - dlg.SetButtonLabel(wxID_OK, _L("确认")); - dlg.SetButtonLabel(wxID_CANCEL, _L("取消")); + wxString msg = _L("Mixing materials with significantly different printing temperatures may result in:\n" + "· Extruder clogging\n" + "· Nozzle damage\n" + "· Layer adhesion issues\n\n" + "Do you want to enable this feature?"); + MessageDialog dlg(this, msg, _L("High and Low Temperature Material Mixing Risk"), wxICON_WARNING | wxOK | wxCANCEL); + dlg.SetButtonLabel(wxID_OK, _L("Confirm")); + dlg.SetButtonLabel(wxID_CANCEL, _L("Cancel")); return dlg.ShowModal() == wxID_OK; }); auto camera_orbit_mult = create_camera_orbit_mult_input(_L("Orbit speed multiplier"), page, _L("Multiplies the orbit speed for finer or coarser camera movement.")); From 585adaf72b7f0ce1544639559373a96b1e07e67a Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Fri, 5 Jun 2026 11:59:54 +0800 Subject: [PATCH 033/135] Add Chinese translations for filament temp mixing UI strings Co-Authored-By: Claude Opus 4.7 --- .../i18n/zh_CN/Snapmaker_Orca_zh_CN.po | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/localization/i18n/zh_CN/Snapmaker_Orca_zh_CN.po b/localization/i18n/zh_CN/Snapmaker_Orca_zh_CN.po index 3450984c8f6..dbf7798c6ab 100644 --- a/localization/i18n/zh_CN/Snapmaker_Orca_zh_CN.po +++ b/localization/i18n/zh_CN/Snapmaker_Orca_zh_CN.po @@ -15242,4 +15242,43 @@ msgid "\nMerging this filament will invalidate these mixed filament configuratio msgstr "\n合并该耗材将导致这些混色耗材配置失效。是否继续?" msgid "Filament %d ratio is too high. Mix may be affected." +msgstr "" + +msgid "High and Low Temperature Material Mixing Risk" +msgstr "高/低温材料混打风险" + +msgid "" +"Mixing materials with significantly different printing temperatures may result in:\n" +"· Extruder clogging\n" +"· Nozzle damage\n" +"· Layer adhesion issues\n" +"\n" +"Do you want to enable this feature?" +msgstr "" +"混合使用打印温度差异较大的材料,可能导致:\n" +"· 挤出机堵塞\n" +"· 喷嘴损坏\n" +"· 层间粘附问题\n" +"\n" +"确认启用此功能吗?" + +msgid "Confirm" +msgstr "确认" + +msgid "Cancel" +msgstr "取消" + +msgid "" +"Detected both high and low temperature materials. Mixed printing may " +"result in extruder clogging, nozzle damage, or layer adhesion issues." +msgstr "检测到高温材料与低温材料同时使用。混合打印可能导致挤出机堵塞、喷嘴损坏或层间粘附问题。" + +msgid "" +"Detected both high and low temperature materials. Mixed printing may " +"result in extruder clogging, nozzle damage, or layer adhesion issues. " +"To continue printing, enable \"Allow mixed printing of high and low " +"temperature materials\" in Preferences." +msgstr "" +"检测到高温材料与低温材料同时使用。混合打印可能导致挤出机堵塞、喷嘴损坏或层间粘附问题。" +"如需继续打印,请在「偏好设置」中开启 \"允许高/低温材料混合打印\"。" msgstr "耗材%d参与混色的比例过高,可能会影响混色效果。" \ No newline at end of file From 457bfb01af914fa9e089ad0de52bba29c4413955 Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Fri, 5 Jun 2026 12:00:56 +0800 Subject: [PATCH 034/135] Remove duplicate Cancel msgid from zh_CN translation Co-Authored-By: Claude Opus 4.7 --- localization/i18n/zh_CN/Snapmaker_Orca_zh_CN.po | 3 --- 1 file changed, 3 deletions(-) diff --git a/localization/i18n/zh_CN/Snapmaker_Orca_zh_CN.po b/localization/i18n/zh_CN/Snapmaker_Orca_zh_CN.po index dbf7798c6ab..d4cb9502660 100644 --- a/localization/i18n/zh_CN/Snapmaker_Orca_zh_CN.po +++ b/localization/i18n/zh_CN/Snapmaker_Orca_zh_CN.po @@ -15265,9 +15265,6 @@ msgstr "" msgid "Confirm" msgstr "确认" -msgid "Cancel" -msgstr "取消" - msgid "" "Detected both high and low temperature materials. Mixed printing may " "result in extruder clogging, nozzle damage, or layer adhesion issues." From 1cbad2dc8c6cf60072632cd4940b60a886207708 Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Fri, 5 Jun 2026 12:07:31 +0800 Subject: [PATCH 035/135] Use single-line gettext strings for reliable translation matching Multi-line string literals in _u8L() may not match multi-line msgid entries in .po files during gettext lookup. Consolidate to single-line strings in both C++ source and .po msgid for the filament temp mixing messages. Co-Authored-By: Claude Opus 4.7 --- localization/i18n/zh_CN/Snapmaker_Orca_zh_CN.po | 15 +++------------ src/slic3r/GUI/Plater.cpp | 8 ++------ 2 files changed, 5 insertions(+), 18 deletions(-) diff --git a/localization/i18n/zh_CN/Snapmaker_Orca_zh_CN.po b/localization/i18n/zh_CN/Snapmaker_Orca_zh_CN.po index d4cb9502660..6f181d38fd9 100644 --- a/localization/i18n/zh_CN/Snapmaker_Orca_zh_CN.po +++ b/localization/i18n/zh_CN/Snapmaker_Orca_zh_CN.po @@ -15265,17 +15265,8 @@ msgstr "" msgid "Confirm" msgstr "确认" -msgid "" -"Detected both high and low temperature materials. Mixed printing may " -"result in extruder clogging, nozzle damage, or layer adhesion issues." +msgid "Detected both high and low temperature materials. Mixed printing may result in extruder clogging, nozzle damage, or layer adhesion issues." msgstr "检测到高温材料与低温材料同时使用。混合打印可能导致挤出机堵塞、喷嘴损坏或层间粘附问题。" -msgid "" -"Detected both high and low temperature materials. Mixed printing may " -"result in extruder clogging, nozzle damage, or layer adhesion issues. " -"To continue printing, enable \"Allow mixed printing of high and low " -"temperature materials\" in Preferences." -msgstr "" -"检测到高温材料与低温材料同时使用。混合打印可能导致挤出机堵塞、喷嘴损坏或层间粘附问题。" -"如需继续打印,请在「偏好设置」中开启 \"允许高/低温材料混合打印\"。" -msgstr "耗材%d参与混色的比例过高,可能会影响混色效果。" \ No newline at end of file +msgid "Detected both high and low temperature materials. Mixed printing may result in extruder clogging, nozzle damage, or layer adhesion issues. To continue printing, enable \"Allow mixed printing of high and low temperature materials\" in Preferences." +msgstr "检测到高温材料与低温材料同时使用。混合打印可能导致挤出机堵塞、喷嘴损坏或层间粘附问题。如需继续打印,请在「偏好设置」中开启 \"允许高/低温材料混合打印\"。" \ No newline at end of file diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index fbb5e2d6775..cecb26a21d9 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -14707,13 +14707,9 @@ bool Plater::sync_filament_temp_mixing_notification() NotificationType::ValidateWarning, NotificationManager::NotificationLevel::WarningNotificationLevel, _u8L("WARNING:") + "\n" + - _u8L("Detected both high and low temperature materials. Mixed printing may " - "result in extruder clogging, nozzle damage, or layer adhesion issues.")); + _u8L("Detected both high and low temperature materials. Mixed printing may result in extruder clogging, nozzle damage, or layer adhesion issues.")); } else { - err.string = _u8L("Detected both high and low temperature materials. Mixed printing may " - "result in extruder clogging, nozzle damage, or layer adhesion issues. " - "To continue printing, enable \"Allow mixed printing of high and low " - "temperature materials\" in Preferences."); + err.string = _u8L("Detected both high and low temperature materials. Mixed printing may result in extruder clogging, nozzle damage, or layer adhesion issues. To continue printing, enable \"Allow mixed printing of high and low temperature materials\" in Preferences."); get_notification_manager()->push_validate_error_notification(err); get_partplate_list().get_curr_plate()->update_apply_result_invalid(true); } From 6deac999c68480695193470c8cf273801f6322f8 Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Fri, 5 Jun 2026 14:39:06 +0800 Subject: [PATCH 036/135] Remove duplicate Confirm msgid from zh_CN .po file Co-Authored-By: Claude Opus 4.7 --- localization/i18n/zh_CN/Snapmaker_Orca_zh_CN.po | 3 --- 1 file changed, 3 deletions(-) diff --git a/localization/i18n/zh_CN/Snapmaker_Orca_zh_CN.po b/localization/i18n/zh_CN/Snapmaker_Orca_zh_CN.po index 6f181d38fd9..b4b88c5ff3e 100644 --- a/localization/i18n/zh_CN/Snapmaker_Orca_zh_CN.po +++ b/localization/i18n/zh_CN/Snapmaker_Orca_zh_CN.po @@ -15262,9 +15262,6 @@ msgstr "" "\n" "确认启用此功能吗?" -msgid "Confirm" -msgstr "确认" - msgid "Detected both high and low temperature materials. Mixed printing may result in extruder clogging, nozzle damage, or layer adhesion issues." msgstr "检测到高温材料与低温材料同时使用。混合打印可能导致挤出机堵塞、喷嘴损坏或层间粘附问题。" From a5fca4cee2f692fa54748d32948e6f30867b03a2 Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Fri, 5 Jun 2026 14:57:00 +0800 Subject: [PATCH 037/135] Add missing Chinese translations for filament mixing checkbox Co-Authored-By: Claude Opus 4.7 --- localization/i18n/zh_CN/Snapmaker_Orca_zh_CN.po | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/localization/i18n/zh_CN/Snapmaker_Orca_zh_CN.po b/localization/i18n/zh_CN/Snapmaker_Orca_zh_CN.po index b4b88c5ff3e..9b4b11f4e8d 100644 --- a/localization/i18n/zh_CN/Snapmaker_Orca_zh_CN.po +++ b/localization/i18n/zh_CN/Snapmaker_Orca_zh_CN.po @@ -15266,4 +15266,10 @@ msgid "Detected both high and low temperature materials. Mixed printing may resu msgstr "检测到高温材料与低温材料同时使用。混合打印可能导致挤出机堵塞、喷嘴损坏或层间粘附问题。" msgid "Detected both high and low temperature materials. Mixed printing may result in extruder clogging, nozzle damage, or layer adhesion issues. To continue printing, enable \"Allow mixed printing of high and low temperature materials\" in Preferences." -msgstr "检测到高温材料与低温材料同时使用。混合打印可能导致挤出机堵塞、喷嘴损坏或层间粘附问题。如需继续打印,请在「偏好设置」中开启 \"允许高/低温材料混合打印\"。" \ No newline at end of file +msgstr "检测到高温材料与低温材料同时使用。混合打印可能导致挤出机堵塞、喷嘴损坏或层间粘附问题。如需继续打印,请在「偏好设置」中开启 \"允许高/低温材料混合打印\"。" + +msgid "Allow high/low temperature filament mixing" +msgstr "允许高/低温材料混合打印" + +msgid "If enabled, allows printing with both high-temperature and low-temperature filaments simultaneously." +msgstr "启用后允许高温材料与低温材料同时打印" \ No newline at end of file From 5d4c7c6cbe0c8babb9cf171b68f07a16f29424c7 Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Fri, 5 Jun 2026 19:37:19 +0800 Subject: [PATCH 038/135] fix: improve filament temp mixing detection coverage - Detect global Process filament assignments (wall_filament, solid_infill_filament, support_filament, support_interface_filament, wipe_tower_filament) in addition to plate-level overrides. Uses p->config which is updated in real-time by on_config_change(). - Remove sparse_infill_filament from checked keys: it is not user-configurable in the UI but has a hardcoded default of 1 in some process presets, causing false positives. - Trigger sync after object-mode config changes in TabPrintModel::on_value_change. - Trigger sync after moving instances between plates (PartPlateList::add_to_plate). - Trigger sync after clearing all objects from the current plate (remove_curr_plate_all). - Update Preferences::create_item_checkbox declaration to match the cherry-picked implementation which adds an optional confirm_cb parameter. Co-Authored-By: Claude Opus 4.7 --- src/slic3r/GUI/PartPlate.cpp | 2 ++ src/slic3r/GUI/Plater.cpp | 11 +++++++++-- src/slic3r/GUI/Preferences.hpp | 3 ++- src/slic3r/GUI/Tab.cpp | 3 +++ 4 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/slic3r/GUI/PartPlate.cpp b/src/slic3r/GUI/PartPlate.cpp index 09367325d6e..2006285a2cb 100644 --- a/src/slic3r/GUI/PartPlate.cpp +++ b/src/slic3r/GUI/PartPlate.cpp @@ -4556,6 +4556,8 @@ int PartPlateList::add_to_plate(int obj_id, int instance_id, int plate_id) } ret = plate->add_instance(obj_id, instance_id, true); + wxGetApp().plater()->sync_filament_temp_mixing_notification(); + return ret; } diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index cecb26a21d9..06fbe674c25 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -6043,6 +6043,8 @@ void Plater::priv::remove_curr_plate_all() SingleSnapshot ss(q); view3D->remove_curr_plate_all(); this->sidebar->obj_list()->update_selections(); + + q->sync_filament_temp_mixing_notification(); } void Plater::priv::select_all() @@ -14622,7 +14624,7 @@ bool Plater::check_filament_temp_mixing() // Helper: collect used filament slots from a single config auto collect_from_cfg = [&](const DynamicPrintConfig& cfg) { - static const char* keys_1based[] = {"wall_filament", "sparse_infill_filament", "solid_infill_filament"}; + static const char* keys_1based[] = {"wall_filament", "solid_infill_filament"}; for (auto key : keys_1based) { auto* opt = cfg.option(key); if (opt && opt->value >= 1 && opt->value <= num_filaments) @@ -14636,7 +14638,11 @@ bool Plater::check_filament_temp_mixing() } }; - // Collect from current plate's config only + // Collect from the Plater's working config for global Process settings. + // Defaults are 0 (skipped by the >= 1 check). + collect_from_cfg(*this->config()); + + // Also collect from current plate's config for any plate-level overrides PartPlate* curr_plate = p->partplate_list.get_curr_plate(); if (curr_plate) collect_from_cfg(*curr_plate->config()); @@ -14672,6 +14678,7 @@ bool Plater::check_filament_temp_mixing() // (the edited preset config hasn't been committed yet). PresetBundle* bundle = wxGetApp().preset_bundle; bool has_high = false, has_low = false; + for (int slot : used_slots) { if (slot < (int)bundle->filament_presets.size()) { const Preset* preset = bundle->filaments.find_preset( diff --git a/src/slic3r/GUI/Preferences.hpp b/src/slic3r/GUI/Preferences.hpp index e65ae52d35a..6fbf5d19ce9 100644 --- a/src/slic3r/GUI/Preferences.hpp +++ b/src/slic3r/GUI/Preferences.hpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include "Widgets/ComboBox.hpp" @@ -110,7 +111,7 @@ class PreferencesDialog : public DPIDialog wxBoxSizer *create_item_region_combobox(wxString title, wxWindow *parent, wxString tooltip, std::vector vlist); wxBoxSizer *create_item_language_combobox(wxString title, wxWindow *parent, wxString tooltip, int padding_left, std::string param, std::vector vlist); wxBoxSizer *create_item_loglevel_combobox(wxString title, wxWindow *parent, wxString tooltip, std::vector vlist); - wxBoxSizer *create_item_checkbox(wxString title, wxWindow *parent, wxString tooltip, int padding_left, std::string param); + wxBoxSizer *create_item_checkbox(wxString title, wxWindow *parent, wxString tooltip, int padding_left, std::string param, std::function confirm_cb = nullptr); wxBoxSizer *create_item_darkmode_checkbox(wxString title, wxWindow *parent, wxString tooltip, int padding_left, std::string param); void set_dark_mode(); wxBoxSizer *create_item_button(wxString title, wxString title2, wxWindow *parent, wxString tooltip, wxString tooltip2, std::function onclick, bool button_on_left = false); diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index 5b6ec2b9559..102d7807c5e 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -3119,6 +3119,9 @@ void TabPrintModel::on_value_change(const std::string& opt_key, const boost::any notify_changed(config.first); } wxGetApp().params_panel()->notify_object_config_changed(); + + // Real-time high/low temperature filament mixing check after object-level config change + wxGetApp().plater()->sync_filament_temp_mixing_notification(); } void TabPrintModel::reload_config() From 0ec81441727a32b1808a08f30e6c67ef57d182be Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Mon, 8 Jun 2026 10:30:38 +0800 Subject: [PATCH 039/135] fix: rebuild filament mixing feature on current main base MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The original cherry-pick fca2f3d2c4 used --theirs conflict resolution which overwrote Plater.cpp and Print.cpp with older versions, accidentally dropping 6194 lines of code from main. This commit properly re-applies only the filament mixing additions: Plater.cpp: - Add check_filament_temp_mixing() — detects high/low temp mixing across wall, solid infill, support, support interface, and wipe tower filaments - Add sync_filament_temp_mixing_notification() — updates notification state - Add sync calls in: Sidebar::update_presets, on_config_change, update_background_process, remove_curr_plate_all - Add pre-slice confirmation dialog when allow_filament_temp_mixing is on Print.cpp: - Replace nozzles<2 restriction with simpler extruders>1 guard so the filament mixing check covers multi-nozzle printers too Co-Authored-By: Claude Opus 4.7 --- src/libslic3r/Print.cpp | 553 +++- src/slic3r/GUI/Plater.cpp | 5844 +++++++++++++++++++++++++++++++++++-- 2 files changed, 6213 insertions(+), 184 deletions(-) diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index e856242ece9..1e2148afcc4 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -8,6 +8,7 @@ #include "Flow.hpp" #include "Geometry/ConvexHull.hpp" #include "I18N.hpp" +#include "LocalZOrderOptimizer.hpp" #include "ShortestPath.hpp" #include "Thread.hpp" #include "Time.hpp" @@ -53,6 +54,453 @@ template class PrintState; PrintRegion::PrintRegion(const PrintRegionConfig &config) : PrintRegion(config, config.hash()) {} PrintRegion::PrintRegion(PrintRegionConfig &&config) : PrintRegion(std::move(config), config.hash()) {} +namespace { + +constexpr double LOCAL_Z_PERIMETER_MASK_EXPAND_MM = 0.10; + +struct LocalZWipeTowerToolchange +{ + unsigned int old_tool { 0 }; + unsigned int new_tool { 0 }; +}; + +struct LocalZWipeTowerPassRef +{ + size_t layer_to_print_idx { 0 }; + const SubLayerPlan *plan { nullptr }; + std::vector extruders; +}; + +static inline ExPolygons local_z_compensate_masks_for_wipe_tower(const ExPolygons &src_masks, + const float delta_scaled, + const bool fallback_to_source) +{ + if (src_masks.empty() || std::abs(delta_scaled) <= EPSILON) + return src_masks; + + ExPolygons compensated = offset_ex(src_masks, delta_scaled); + if (!compensated.empty() && compensated.size() > 1) + compensated = union_ex(compensated); + + if (compensated.empty() && fallback_to_source) + return src_masks; + return compensated; +} + +static bool local_z_segments_exist(Polylines segments) +{ + for (Polyline &segment : segments) { + if (segment.is_valid()) + return true; + } + return false; +} + +static bool extrusion_collection_has_local_z_segment(const ExtrusionEntityCollection &source, + const ExPolygons &include_masks) +{ + if (source.entities.empty() || include_masks.empty()) + return false; + + ExtrusionEntityCollection flattened = source.flatten(false); + for (const ExtrusionEntity *entity : flattened.entities) { + if (const auto *path = dynamic_cast(entity)) { + if (local_z_segments_exist(intersection_pl(Polylines{path->polyline}, include_masks))) + return true; + } else if (const auto *multipath = dynamic_cast(entity)) { + for (const ExtrusionPath &path : multipath->paths) { + if (local_z_segments_exist(intersection_pl(Polylines{path.polyline}, include_masks))) + return true; + } + } else if (const auto *loop = dynamic_cast(entity)) { + for (const ExtrusionPath &path : loop->paths) { + if (local_z_segments_exist(intersection_pl(Polylines{path.polyline}, include_masks))) + return true; + } + } + } + + return false; +} + +static bool layer_has_local_z_extrusions(const Layer &layer, const ExPolygons &pass_masks, bool include_infill) +{ + if (pass_masks.empty()) + return false; + + for (const LayerRegion *layer_region : layer.regions()) { + for (const ExtrusionEntity *entity : layer_region->perimeters.entities) { + const auto *extrusions = dynamic_cast(entity); + if (extrusions == nullptr) + continue; + if (extrusion_collection_has_local_z_segment(*extrusions, pass_masks)) + return true; + } + if (!include_infill) + continue; + for (const ExtrusionEntity *entity : layer_region->fills.entities) { + const auto *extrusions = dynamic_cast(entity); + if (extrusions == nullptr) + continue; + if (extrusion_collection_has_local_z_segment(*extrusions, pass_masks)) + return true; + } + } + + return false; +} + +static inline int shared_local_z_extruder_for_wipe_tower(const std::vector &lhs, + const std::vector &rhs) +{ + for (unsigned int extruder_id : lhs) { + if (std::find(rhs.begin(), rhs.end(), extruder_id) != rhs.end()) + return static_cast(extruder_id); + } + return -1; +} + +static std::vector rotate_extruders_to_start_with(const std::vector &extruders, + unsigned int start_extruder) +{ + std::vector rotated = extruders; + auto it = std::find(rotated.begin(), rotated.end(), start_extruder); + if (it != rotated.end()) + std::rotate(rotated.begin(), it, rotated.end()); + return rotated; +} + +static std::vector collect_local_z_wipe_tower_toolchanges( + const Print &print, + const std::vector &layers, + int start_extruder) +{ + std::vector pass_refs; + const bool local_z_whole_objects_enabled = print.full_print_config().opt_bool("dithering_local_z_whole_objects"); + const bool local_z_infill_enabled = print.full_print_config().opt_bool("dithering_local_z_infill"); + const float local_z_perimeter_mask_expand = float(scale_(LOCAL_Z_PERIMETER_MASK_EXPAND_MM)); + + for (size_t layer_to_print_idx = 0; layer_to_print_idx < layers.size(); ++layer_to_print_idx) { + const GCode::LayerToPrint &layer_to_print = layers[layer_to_print_idx]; + if (layer_to_print.object_layer == nullptr) + continue; + + const PrintObject *print_object = + layer_to_print.original_object != nullptr ? layer_to_print.original_object : layer_to_print.object(); + if (print_object == nullptr) + continue; + + const size_t layer_id = size_t(layer_to_print.object_layer->id()); + const auto &intervals = print_object->local_z_intervals(); + const auto &plans = print_object->local_z_sublayer_plan(); + const auto interval_it = std::find_if(intervals.begin(), intervals.end(), [layer_id](const LocalZInterval &interval) { + return interval.layer_id == layer_id; + }); + if (interval_it == intervals.end() || !interval_it->has_mixed_paint || interval_it->sublayer_count <= 1 || + interval_it->first_sublayer_idx >= plans.size()) { + continue; + } + + const size_t first_idx = interval_it->first_sublayer_idx; + const size_t end_idx = std::min(plans.size(), first_idx + interval_it->sublayer_count); + for (size_t plan_idx = first_idx; plan_idx < end_idx; ++plan_idx) { + const SubLayerPlan &plan = plans[plan_idx]; + if (!plan.split_interval) + continue; + + const size_t plan_mask_slots = + std::max(plan.painted_masks_by_extruder.size(), plan.fixed_painted_masks_by_extruder.size()); + std::vector compensated_masks_by_extruder(plan_mask_slots, ExPolygons()); + + ExPolygons fixed_raw_masks_union; + for (const ExPolygons &fixed_masks : plan.fixed_painted_masks_by_extruder) { + if (!fixed_masks.empty()) + append(fixed_raw_masks_union, fixed_masks); + } + if (!fixed_raw_masks_union.empty() && fixed_raw_masks_union.size() > 1) + fixed_raw_masks_union = union_ex(fixed_raw_masks_union); + + const ExPolygons fixed_compensated_guard = + fixed_raw_masks_union.empty() ? + ExPolygons() : + local_z_compensate_masks_for_wipe_tower(fixed_raw_masks_union, local_z_perimeter_mask_expand, true); + + for (size_t extruder_id = 0; extruder_id < plan_mask_slots; ++extruder_id) { + const ExPolygons mixed_raw_masks = + extruder_id < plan.painted_masks_by_extruder.size() ? plan.painted_masks_by_extruder[extruder_id] : ExPolygons(); + const ExPolygons fixed_raw_masks = + extruder_id < plan.fixed_painted_masks_by_extruder.size() ? plan.fixed_painted_masks_by_extruder[extruder_id] : + ExPolygons(); + if (mixed_raw_masks.empty() && fixed_raw_masks.empty()) + continue; + + ExPolygons compensated; + if (!mixed_raw_masks.empty()) { + ExPolygons compensated_mixed = + local_z_compensate_masks_for_wipe_tower(mixed_raw_masks, local_z_perimeter_mask_expand, true); + if (local_z_whole_objects_enabled && !fixed_compensated_guard.empty()) + compensated_mixed = diff_ex(compensated_mixed, fixed_compensated_guard); + if (!compensated_mixed.empty()) + append(compensated, compensated_mixed); + } + if (!fixed_raw_masks.empty()) + append(compensated, fixed_raw_masks); + if (!compensated.empty() && compensated.size() > 1) + compensated = union_ex(compensated); + compensated_masks_by_extruder[extruder_id] = std::move(compensated); + } + + LocalZWipeTowerPassRef pass_ref; + pass_ref.layer_to_print_idx = layer_to_print_idx; + pass_ref.plan = &plan; + for (size_t extruder_id = 0; extruder_id < plan.painted_masks_by_extruder.size(); ++extruder_id) { + if (extruder_id >= compensated_masks_by_extruder.size()) + continue; + const ExPolygons &pass_masks = compensated_masks_by_extruder[extruder_id]; + if (pass_masks.empty()) + continue; + if (layer_has_local_z_extrusions(*layer_to_print.object_layer, pass_masks, local_z_infill_enabled)) + pass_ref.extruders.push_back(unsigned(extruder_id)); + } + + if (!pass_ref.extruders.empty()) + pass_refs.emplace_back(std::move(pass_ref)); + } + } + + std::sort(pass_refs.begin(), pass_refs.end(), [](const LocalZWipeTowerPassRef &lhs, const LocalZWipeTowerPassRef &rhs) { + assert(lhs.plan != nullptr && rhs.plan != nullptr); + if (lhs.plan->print_z != rhs.plan->print_z) + return lhs.plan->print_z < rhs.plan->print_z; + if (lhs.layer_to_print_idx != rhs.layer_to_print_idx) + return lhs.layer_to_print_idx < rhs.layer_to_print_idx; + return lhs.plan->pass_index < rhs.plan->pass_index; + }); + + auto collect_toolchanges_legacy = [&](int start_tool) { + std::vector legacy_toolchanges; + int active_extruder = start_tool; + size_t pass_ref_idx = 0; + while (pass_ref_idx < pass_refs.size()) { + size_t pass_group_end = pass_ref_idx + 1; + while (pass_group_end < pass_refs.size() && + std::abs(pass_refs[pass_ref_idx].plan->print_z - pass_refs[pass_group_end].plan->print_z) <= EPSILON) { + ++pass_group_end; + } + + std::vector pass_group_extruders; + for (size_t group_idx = pass_ref_idx; group_idx < pass_group_end; ++group_idx) + for (unsigned int extruder_id : pass_refs[group_idx].extruders) + if (std::find(pass_group_extruders.begin(), pass_group_extruders.end(), extruder_id) == pass_group_extruders.end()) + pass_group_extruders.push_back(extruder_id); + + std::vector next_group_extruders; + if (pass_group_end < pass_refs.size()) { + size_t next_group_end = pass_group_end + 1; + while (next_group_end < pass_refs.size() && + std::abs(pass_refs[pass_group_end].plan->print_z - pass_refs[next_group_end].plan->print_z) <= EPSILON) { + ++next_group_end; + } + for (size_t group_idx = pass_group_end; group_idx < next_group_end; ++group_idx) + for (unsigned int extruder_id : pass_refs[group_idx].extruders) + if (std::find(next_group_extruders.begin(), next_group_extruders.end(), extruder_id) == next_group_extruders.end()) + next_group_extruders.push_back(extruder_id); + } + + const int preferred_last_extruder = + shared_local_z_extruder_for_wipe_tower(pass_group_extruders, next_group_extruders); + const std::vector ordered_group_extruders = + LocalZOrderOptimizer::order_bucket_extruders(pass_group_extruders, active_extruder, preferred_last_extruder); + + for (unsigned int extruder_id : ordered_group_extruders) { + if (active_extruder >= 0 && active_extruder != int(extruder_id)) + legacy_toolchanges.push_back(LocalZWipeTowerToolchange{unsigned(active_extruder), extruder_id}); + active_extruder = int(extruder_id); + } + + pass_ref_idx = pass_group_end; + } + + return legacy_toolchanges; + }; + + const bool dependency_chain_mode = + !pass_refs.empty() && + std::all_of(pass_refs.begin(), pass_refs.end(), [](const LocalZWipeTowerPassRef &pass_ref) { + return pass_ref.plan != nullptr && pass_ref.plan->dependency_group != 0; + }); + if (!dependency_chain_mode) + return collect_toolchanges_legacy(start_extruder); + + struct ChainKey { + size_t layer_to_print_idx { 0 }; + size_t dependency_group { 0 }; + + bool operator<(const ChainKey &rhs) const + { + if (layer_to_print_idx != rhs.layer_to_print_idx) + return layer_to_print_idx < rhs.layer_to_print_idx; + return dependency_group < rhs.dependency_group; + } + }; + struct PassState { + const LocalZWipeTowerPassRef *pass_ref { nullptr }; + std::vector remaining_extruders; + size_t chain_idx { 0 }; + size_t chain_pos { 0 }; + bool ready { false }; + bool completed { false }; + }; + + std::map chain_index_by_key; + std::vector> chains; + std::vector pass_states; + pass_states.reserve(pass_refs.size()); + for (const LocalZWipeTowerPassRef &pass_ref : pass_refs) { + ChainKey chain_key { pass_ref.layer_to_print_idx, pass_ref.plan->dependency_group }; + auto [it_chain, inserted] = chain_index_by_key.emplace(chain_key, chains.size()); + if (inserted) + chains.emplace_back(); + + const size_t chain_idx = it_chain->second; + const size_t pass_state_idx = pass_states.size(); + pass_states.push_back(PassState{ &pass_ref, pass_ref.extruders, chain_idx, 0, false, false }); + chains[chain_idx].push_back(pass_state_idx); + } + + for (std::vector &chain : chains) { + std::sort(chain.begin(), chain.end(), [&pass_states](size_t lhs_idx, size_t rhs_idx) { + const SubLayerPlan &lhs = *pass_states[lhs_idx].pass_ref->plan; + const SubLayerPlan &rhs = *pass_states[rhs_idx].pass_ref->plan; + if (lhs.dependency_order != rhs.dependency_order) + return lhs.dependency_order < rhs.dependency_order; + if (std::abs(lhs.print_z - rhs.print_z) > EPSILON) + return lhs.print_z < rhs.print_z; + return lhs.pass_index < rhs.pass_index; + }); + for (size_t chain_pos = 0; chain_pos < chain.size(); ++chain_pos) + pass_states[chain[chain_pos]].chain_pos = chain_pos; + if (!chain.empty()) + pass_states[chain.front()].ready = true; + } + + auto pass_contains_extruder = [](const PassState &pass_state, unsigned int extruder_id) { + return std::find(pass_state.remaining_extruders.begin(), pass_state.remaining_extruders.end(), extruder_id) != + pass_state.remaining_extruders.end(); + }; + + auto choose_ready_extruder = [&](int active_extruder) -> int { + std::vector ready_extruders; + for (const PassState &pass_state : pass_states) { + if (!pass_state.ready || pass_state.completed) + continue; + for (unsigned int extruder_id : pass_state.remaining_extruders) + if (std::find(ready_extruders.begin(), ready_extruders.end(), extruder_id) == ready_extruders.end()) + ready_extruders.push_back(extruder_id); + } + if (ready_extruders.empty()) + return -1; + if (active_extruder >= 0 && + std::find(ready_extruders.begin(), ready_extruders.end(), unsigned(active_extruder)) != ready_extruders.end()) { + return active_extruder; + } + + int best_extruder = -1; + size_t best_ready_count = 0; + size_t best_future_count = 0; + for (unsigned int extruder_id : ready_extruders) { + size_t ready_count = 0; + size_t future_count = 0; + for (const PassState &pass_state : pass_states) { + if (pass_state.completed || !pass_contains_extruder(pass_state, extruder_id)) + continue; + ++future_count; + if (pass_state.ready) + ++ready_count; + } + + if (best_extruder < 0 || + ready_count > best_ready_count || + (ready_count == best_ready_count && future_count > best_future_count) || + (ready_count == best_ready_count && future_count == best_future_count && extruder_id < unsigned(best_extruder))) { + best_extruder = int(extruder_id); + best_ready_count = ready_count; + best_future_count = future_count; + } + } + return best_extruder; + }; + + std::vector toolchanges; + int active_extruder = start_extruder; + size_t completed_passes = 0; + while (completed_passes < pass_states.size()) { + const int chosen_extruder = choose_ready_extruder(active_extruder); + if (chosen_extruder < 0) { + BOOST_LOG_TRIVIAL(warning) << "Local-Z wipe tower dependency scheduler deadlocked, falling back" + << " start_extruder=" << start_extruder + << " pass_count=" << pass_refs.size(); + return collect_toolchanges_legacy(start_extruder); + } + + if (active_extruder >= 0 && active_extruder != chosen_extruder) + toolchanges.push_back(LocalZWipeTowerToolchange{unsigned(active_extruder), unsigned(chosen_extruder)}); + active_extruder = chosen_extruder; + + bool completed_any = false; + std::vector newly_completed; + for (size_t pass_state_idx = 0; pass_state_idx < pass_states.size(); ++pass_state_idx) { + PassState &pass_state = pass_states[pass_state_idx]; + if (!pass_state.ready || pass_state.completed) + continue; + + auto it_extruder = std::find(pass_state.remaining_extruders.begin(), + pass_state.remaining_extruders.end(), + unsigned(chosen_extruder)); + if (it_extruder == pass_state.remaining_extruders.end()) + continue; + + pass_state.remaining_extruders.erase(it_extruder); + completed_any = true; + if (pass_state.remaining_extruders.empty()) + newly_completed.push_back(pass_state_idx); + } + + if (!completed_any) { + BOOST_LOG_TRIVIAL(warning) << "Local-Z wipe tower dependency scheduler made no progress, falling back" + << " start_extruder=" << start_extruder + << " active_extruder=" << active_extruder + << " chosen_extruder=" << chosen_extruder + << " pass_count=" << pass_refs.size(); + return collect_toolchanges_legacy(start_extruder); + } + + for (size_t pass_state_idx : newly_completed) { + PassState &pass_state = pass_states[pass_state_idx]; + if (pass_state.completed) + continue; + + pass_state.ready = false; + pass_state.completed = true; + ++completed_passes; + + const std::vector &chain = chains[pass_state.chain_idx]; + const size_t next_chain_pos = pass_state.chain_pos + 1; + if (next_chain_pos < chain.size()) + pass_states[chain[next_chain_pos]].ready = true; + } + } + + BOOST_LOG_TRIVIAL(info) << "Local-Z wipe tower dependency scheduler" + << " start_extruder=" << start_extruder + << " pass_count=" << pass_refs.size() + << " chain_count=" << chains.size() + << " toolchanges=" << toolchanges.size(); + return toolchanges; +} + +} // namespace + //BBS // ORCA: Now this is a parameter //float Print::min_skirt_length = 0; @@ -247,6 +695,19 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n || opt_key == "filament_shrinkage_compensation_z" || opt_key == "resolution" || opt_key == "precise_z_height" + || opt_key == "dithering_z_step_size" + || opt_key == "dithering_local_z_mode" + || opt_key == "dithering_local_z_whole_objects" + || opt_key == "dithering_local_z_direct_multicolor" + || opt_key == "dithering_step_painted_zones_only" + || opt_key == "mixed_filament_gradient_mode" + || opt_key == "mixed_filament_height_lower_bound" + || opt_key == "mixed_filament_height_upper_bound" + || opt_key == "mixed_filament_advanced_dithering" + || opt_key == "mixed_filament_component_bias_enabled" + || opt_key == "mixed_filament_surface_indentation" + || opt_key == "mixed_filament_region_collapse" + || opt_key == "mixed_filament_definitions" // Spiral Vase forces different kind of slicing than the normal model: // In Spiral Vase mode, holes are closed and only the largest area contour is kept at each layer. // Therefore toggling the Spiral Vase on / off requires complete reslicing. @@ -316,6 +777,7 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n || opt_key == "wipe_tower_fillet_wall" || opt_key == "wipe_tower_filament" || opt_key == "wiping_volumes_extruders" + || opt_key == "dithering_local_z_infill" || opt_key == "enable_filament_ramming" || opt_key == "purge_in_prime_tower" || opt_key == "z_offset" @@ -478,10 +940,11 @@ std::vector Print::extruders(bool conside_custom_gcode) const if (conside_custom_gcode) { //BBS - int num_extruders = m_config.filament_colour.size(); + const size_t num_physical = m_config.filament_colour.size(); + const size_t num_filaments = m_mixed_filament_mgr.total_filaments(num_physical); if (m_model.plates_custom_gcodes.find(m_model.curr_plate_index) != m_model.plates_custom_gcodes.end()) { for (auto item : m_model.plates_custom_gcodes.at(m_model.curr_plate_index).gcodes) { - if (item.type == CustomGCode::Type::ToolChange && item.extruder <= num_extruders) + if (item.type == CustomGCode::Type::ToolChange && item.extruder <= int(num_filaments)) extruders.push_back((unsigned int)(item.extruder - 1)); } } @@ -1064,17 +1527,13 @@ StringObjectException Print::check_multi_filament_valid(const Print& print) { auto print_config = print.config(); std::vector extruders = print.extruders(); + std::vector filament_types; + filament_types.reserve(extruders.size()); - bool has_high = false, has_low = false; - for (const auto& extruder_idx : extruders) { - if (extruder_idx < print_config.filament_is_high_temperature.size()) { - if (print_config.filament_is_high_temperature.get_at(extruder_idx)) - has_high = true; - else - has_low = true; - } - } - if (has_high && has_low) + for (const auto& extruder_idx : extruders) + filament_types.push_back(print_config.filament_type.get_at(extruder_idx)); + + if (!check_multi_filaments_compatibility(filament_types)) return {L("Cannot print multiple filaments which have large difference of temperature together. Otherwise, the extruder and nozzle may be blocked or damaged during printing.")}; return {std::string()}; @@ -1089,6 +1548,7 @@ boost::regex regex_g92e0 { "^[ \\t]*[gG]92[ \\t]*[eE](0(\\.0*)?|\\.0+)[ \\t]*(;. StringObjectException Print::validate(StringObjectException *warning, Polygons* collison_polygons, std::vector>* height_polygons) const { std::vector extruders = this->extruders(); + unsigned int nozzles = m_config.nozzle_diameter.size(); if (m_objects.empty()) return {std::string()}; @@ -1096,6 +1556,17 @@ StringObjectException Print::validate(StringObjectException *warning, Polygons* if (extruders.empty()) return { L("No extrusions under current settings.") }; + // Check for high/low temperature filament mixing. The check uses + // filament_is_high_temperature from each filament preset. This guard + // works for both single-nozzle and multi-nozzle printers. + if (extruders.size() > 1) { + auto ret = check_multi_filament_valid(*this); + if (!ret.string.empty()) { + ret.type = STRING_EXCEPT_FILAMENTS_DIFFERENT_TEMP; + return ret; + } + } + if (m_config.print_sequence == PrintSequence::ByObject) { if (m_config.timelapse_type == TimelapseType::tlSmooth) return {L("Smooth mode of timelapse is not supported when \"by object\" sequence is enabled.")}; @@ -1146,7 +1617,7 @@ StringObjectException Print::validate(StringObjectException *warning, Polygons* layer_height_profiles.assign(m_objects.size(), std::vector()); std::vector &profile = layer_height_profiles[print_object_idx]; if (profile.empty()) - PrintObject::update_layer_height_profile(*print_object.model_object(), print_object.slicing_parameters(), profile); + PrintObject::update_layer_height_profile(*print_object.model_object(), print_object.slicing_parameters(), profile, &print_object); return profile; }; @@ -2840,6 +3311,8 @@ void Print::_make_wipe_tower() // Initialize the wipe tower. WipeTower2 wipe_tower(m_config, m_default_region_config, m_plate_index, m_origin, wipe_volumes, m_wipe_tower_data.tool_ordering.first_extruder()); + const std::vector>> layers_to_print = GCode::collect_layers_to_print(*this); + size_t layers_to_print_idx = 0; // wipe_tower.set_retract(); // wipe_tower.set_zhop(); @@ -2858,10 +3331,52 @@ void Print::_make_wipe_tower() for (auto &layer_tools : m_wipe_tower_data.tool_ordering.layer_tools()) { // for all layers if (!layer_tools.has_wipe_tower) continue; + while (layers_to_print_idx + 1 < layers_to_print.size() && + layers_to_print[layers_to_print_idx].first + EPSILON < layer_tools.print_z) { + ++layers_to_print_idx; + } + + const std::vector *layers_with_same_print_z = nullptr; + if (layers_to_print_idx < layers_to_print.size() && + std::abs(layers_to_print[layers_to_print_idx].first - layer_tools.print_z) <= EPSILON) { + layers_with_same_print_z = &layers_to_print[layers_to_print_idx].second; + } + bool first_layer = &layer_tools == &m_wipe_tower_data.tool_ordering.front(); + + if (layers_with_same_print_z != nullptr) { + const std::vector local_z_toolchanges = + collect_local_z_wipe_tower_toolchanges(*this, *layers_with_same_print_z, int(current_extruder_id)); + if (!local_z_toolchanges.empty()) { + std::ostringstream local_z_sequence; + for (size_t toolchange_idx = 0; toolchange_idx < local_z_toolchanges.size(); ++toolchange_idx) { + if (toolchange_idx != 0) + local_z_sequence << ","; + local_z_sequence << local_z_toolchanges[toolchange_idx].old_tool << "->" + << local_z_toolchanges[toolchange_idx].new_tool; + } + + BOOST_LOG_TRIVIAL(debug) << "Local-Z wipe tower preplan" + << " print_z=" << layer_tools.print_z + << " start_tool=" << current_extruder_id + << " nominal_toolchanges=" << layer_tools.extruders.size() + << " local_z_toolchanges=" << local_z_toolchanges.size() + << " sequence=" << local_z_sequence.str(); + } + for (const LocalZWipeTowerToolchange &toolchange : local_z_toolchanges) { + wipe_tower.plan_local_z_toolchange((float) layer_tools.print_z, (float) layer_tools.wipe_tower_layer_height, + toolchange.old_tool, toolchange.new_tool, (float) m_config.prime_volume); + } + if (!local_z_toolchanges.empty()) + current_extruder_id = local_z_toolchanges.back().new_tool; + } + + const std::vector nominal_layer_extruders = + rotate_extruders_to_start_with(layer_tools.extruders, current_extruder_id); + wipe_tower.plan_toolchange((float) layer_tools.print_z, (float) layer_tools.wipe_tower_layer_height, current_extruder_id, current_extruder_id, false); - for (const auto extruder_id : layer_tools.extruders) { + for (const auto extruder_id : nominal_layer_extruders) { if ((first_layer && extruder_id == m_wipe_tower_data.tool_ordering.all_extruders().back()) || extruder_id != current_extruder_id) { float volume_to_wipe = m_config.prime_volume; @@ -2885,6 +3400,7 @@ void Print::_make_wipe_tower() current_extruder_id = extruder_id; } } + layer_tools.wiping_extrusions().ensure_perimeters_infills_order(*this); if (&layer_tools == &m_wipe_tower_data.tool_ordering.back() || (&layer_tools + 1)->wipe_tower_partitions == 0) break; @@ -2895,8 +3411,12 @@ void Print::_make_wipe_tower() m_wipe_tower_data.tool_changes.reserve(m_wipe_tower_data.tool_ordering.layer_tools().size()); m_wipe_tower_data.local_z_tool_changes.reserve(m_wipe_tower_data.tool_ordering.layer_tools().size()); wipe_tower.generate(m_wipe_tower_data.tool_changes, m_wipe_tower_data.local_z_tool_changes); + BOOST_LOG_TRIVIAL(debug) << "Wipe tower generation completed" + << " nominal_layers=" << m_wipe_tower_data.tool_changes.size() + << " local_z_layers=" << m_wipe_tower_data.local_z_tool_changes.size(); m_wipe_tower_data.depth = wipe_tower.get_depth(); m_wipe_tower_data.z_and_depth_pairs = wipe_tower.get_z_and_depth_pairs(); + m_wipe_tower_data.local_z_reserve_boxes = wipe_tower.get_local_z_reserve_boxes(); m_wipe_tower_data.brim_width = wipe_tower.get_brim_width(); m_wipe_tower_data.height = wipe_tower.get_wipe_tower_height(); @@ -4361,7 +4881,10 @@ BoundingBoxf3 PrintInstance::get_bounding_box() { Polygon PrintInstance::get_convex_hull_2d() { Polygon poly = print_object->model_object()->convex_hull_2d(model_instance->get_matrix()); - poly.douglas_peucker(0.1); + // Change the distance threshold of the Douglas-Peucker algorithm to 0.5 millimeter and reduce the number of points + poly.douglas_peucker(scale_(0.5)); + // Round coordinates to 0.1mm grid to limit decimal places + poly.round_to_grid(scale_(0.1)); return poly; } diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 06fbe674c25..c122e09b77c 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -1,15 +1,32 @@ #include "Plater.hpp" +#include "MixedFilamentDialog.hpp" +#include "MixedGradientSelector.hpp" +#include "MixedColorMatchPanel.hpp" +#include "MixedFilamentBadge.hpp" +#include "MixedFilamentColorMapPanel.hpp" #include "libslic3r/Config.hpp" +#include "libslic3r/MixedFilament.hpp" +#include "libslic3r/filament_mixer.h" #include "common_func/common_func.hpp" #include +#include +#include +#include #include +#include #include +#include +#include +#include #include #include #include +#include #include #include +#include +#include #include #include #include @@ -35,14 +52,23 @@ #include #include #include +#include +#include #include #include +#include +#include +#include +#include +#include #ifdef _WIN32 #include #include #include #endif #include +#include +#include #include #include @@ -106,6 +132,7 @@ #include "ModelMall.hpp" #include "ConfigWizard.hpp" #include "../Utils/ASCIIFolding.hpp" +#include "../Utils/ColorSpaceConvert.hpp" #include "../Utils/FixModelByWin10.hpp" #include "../Utils/UndoRedo.hpp" #include "../Utils/PresetUpdater.hpp" @@ -208,6 +235,7 @@ wxDEFINE_EVENT(EVT_ADD_FILAMENT, SimpleEvent); wxDEFINE_EVENT(EVT_DEL_FILAMENT, SimpleEvent); wxDEFINE_EVENT(EVT_ADD_CUSTOM_FILAMENT, ColorEvent); + #define PRINTER_THUMBNAIL_SIZE (wxSize(FromDIP(48), FromDIP(48))) #define PRINTER_THUMBNAIL_SIZE_SMALL (wxSize(FromDIP(32), FromDIP(32))) #define PRINTER_PANEL_SIZE_SMALL (wxSize(FromDIP(98), FromDIP(68))) @@ -692,6 +720,7 @@ struct Sidebar::priv wxComboBox* m_comboBox_print_preset; wxStaticLine* m_staticline1; StaticBox* m_panel_filament_title; + ScalableButton* m_filament_config_icon = nullptr; wxStaticText* m_staticText_filament_settings; ScalableButton * m_bpButton_add_filament; ScalableButton * m_bpButton_del_filament; @@ -700,6 +729,40 @@ struct Sidebar::priv int m_menu_filament_id = -1; wxPanel* m_panel_filament_content; wxScrolledWindow* m_scrolledWindow_filament_content; + + // Mixed (virtual) filaments panel - collapsible like Printer/Filament sections + StaticBox* m_panel_mixed_filaments_title = nullptr; // Collapsible title bar + wxPanel* m_panel_mixed_filaments_content = nullptr; // Content panel + wxBoxSizer* m_sizer_mixed_filaments_content = nullptr; // Content sizer + ScalableButton* m_mixed_filaments_icon = nullptr; // Icon + wxStaticText* m_staticText_mixed_filaments = nullptr; // Title text + Button* m_btn_add_gradient = nullptr; // Add gradient button + Button* m_btn_add_pattern = nullptr; // Add pattern button + Button* m_btn_add_color = nullptr; // Add color-match button + Button* m_btn_toggle_mixed_filaments = nullptr; // Collapse/expand toggle button + bool m_mixed_filaments_collapsed = false; // Collapse state + bool m_skip_mixed_filament_sync_once = false; // Local edits already mutated manager in place. + std::unordered_set m_expanded_mixed_filament_rows; // Expanded row editors + struct MixedFilamentRowBinding { + size_t mixed_id = size_t(-1); + wxWindow *row = nullptr; + }; + std::vector m_mixed_filament_row_bindings; + std::vector m_mixed_filament_ui_order; + bool m_mixed_filament_drag_active = false; + size_t m_mixed_filament_drag_source_mixed_id = size_t(-1); + // Physical filament scrolled window + wxScrolledWindow* m_scrolled_filaments = nullptr; + wxPanel* m_panel_scrolled_filament_content = nullptr; + // Color mix panel + StaticBox* m_panel_physical_filaments_title = nullptr; + StaticBox* m_panel_color_mix_title = nullptr; + wxPanel* m_panel_color_mix_content = nullptr; + wxScrolledWindow* m_scrolled_color_mix = nullptr; + ScalableButton* m_color_mix_icon = nullptr; + ScalableButton* m_btn_add_color_mix = nullptr; + ScalableButton* m_btn_del_color_mix = nullptr; + wxStaticLine* m_staticline2; wxPanel* m_panel_project_title; ScalableButton* m_filament_icon = nullptr; @@ -899,6 +962,7 @@ std::vector get_min_flush_volumes(const DynamicPrintConfig& full_config) return extra_flush_volumes; } + // Sidebar / public struct DynamicFilamentList : DynamicList @@ -996,6 +1060,528 @@ struct DynamicFilamentList1Based : DynamicFilamentList }; +class MixedFilamentColorMatchDialog : public DPIDialog +{ +public: + MixedFilamentColorMatchDialog(wxWindow* parent, const std::vector& physical_colors, const wxColour& initial_color) + : DPIDialog(parent ? parent : static_cast(wxGetApp().mainframe), + wxID_ANY, + _L("Add Color"), + wxDefaultPosition, + wxDefaultSize, + wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER) + , m_physical_colors(physical_colors) + { + m_recipe_timer.SetOwner(this); + m_loading_timer.SetOwner(this); + m_display_context = build_mixed_filament_display_context(m_physical_colors); + + m_palette.reserve(m_physical_colors.size()); + for (const std::string& hex : m_physical_colors) + m_palette.emplace_back(parse_mixed_color(hex)); + + const wxColour safe_initial = initial_color.IsOk() ? + initial_color : + (m_palette.size() >= 2 ? blend_pair_filament_mixer(m_palette[0], m_palette[1], 0.5f) : + wxColour("#26A69A")); + std::vector initial_weights(m_palette.size(), 0); + if (!initial_weights.empty()) + initial_weights[0] = 100; + if (initial_weights.size() >= 2) { + initial_weights[0] = 50; + initial_weights[1] = 50; + } + + std::vector filament_ids; + filament_ids.reserve(m_palette.size()); + for (size_t idx = 0; idx < m_palette.size(); ++idx) + filament_ids.emplace_back(unsigned(idx + 1)); + + SetMinSize(wxSize(FromDIP(430), FromDIP(520))); + + auto* root = new wxBoxSizer(wxVERTICAL); + auto* description = new wxStaticText(this, wxID_ANY, + _L("Pick from the current filament gamut. The dialog previews the closest 2-color, 3-color, " + "or 4-color FilamentMixer recipe before it is added.")); + description->Wrap(FromDIP(390)); + root->Add(description, 0, wxEXPAND | wxALL, FromDIP(12)); + + m_color_map = new MixedFilamentColorMapPanel(this, filament_ids, m_palette, initial_weights, wxSize(FromDIP(260), FromDIP(260))); + root->Add(m_color_map, 1, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(12)); + + auto* hex_row = new wxBoxSizer(wxHORIZONTAL); + hex_row->Add(new wxStaticText(this, wxID_ANY, _L("Hex")), 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(8)); + m_hex_input = new wxTextCtrl(this, wxID_ANY, normalize_color_match_hex(safe_initial.GetAsString(wxC2S_HTML_SYNTAX)), + wxDefaultPosition, wxDefaultSize, wxTE_PROCESS_ENTER); + m_hex_input->SetToolTip(_L("Enter a hex color like #00FF88. The picker will snap to the closest supported FilamentMixer color.")); + hex_row->Add(m_hex_input, 1, wxALIGN_CENTER_VERTICAL); + hex_row->AddSpacer(FromDIP(8)); + m_classic_picker = new wxColourPickerCtrl(this, wxID_ANY, safe_initial); + m_classic_picker->SetToolTip(_L("Classic color picker. The result will snap to the closest supported FilamentMixer color.")); + hex_row->Add(m_classic_picker, 0, wxALIGN_CENTER_VERTICAL); + root->Add(hex_row, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, FromDIP(12)); + + auto* range_row = new wxBoxSizer(wxHORIZONTAL); + range_row->Add(new wxStaticText(this, wxID_ANY, _L("Range")), 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(8)); + m_range_slider = new wxSlider(this, wxID_ANY, m_min_component_percent, 0, 50); + m_range_slider->SetToolTip(_L("Minimum percent for each participating color. Higher values block highly skewed mixes.")); + range_row->Add(m_range_slider, 1, wxALIGN_CENTER_VERTICAL); + range_row->AddSpacer(FromDIP(8)); + m_range_value = new wxStaticText(this, wxID_ANY, wxEmptyString); + range_row->Add(m_range_value, 0, wxALIGN_CENTER_VERTICAL); + root->Add(range_row, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, FromDIP(12)); + + auto* summary_grid = new wxFlexGridSizer(2, FromDIP(8), FromDIP(8)); + summary_grid->AddGrowableCol(1, 1); + + summary_grid->Add(new wxStaticText(this, wxID_ANY, _L("Requested")), 0, wxALIGN_CENTER_VERTICAL); + auto* selected_row = new wxBoxSizer(wxHORIZONTAL); + m_selected_preview = new wxPanel(this, wxID_ANY, wxDefaultPosition, wxSize(FromDIP(72), FromDIP(24)), wxBORDER_SIMPLE); + selected_row->Add(m_selected_preview, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(8)); + m_selected_label = new wxStaticText(this, wxID_ANY, wxEmptyString); + selected_row->Add(m_selected_label, 1, wxALIGN_CENTER_VERTICAL); + summary_grid->Add(selected_row, 1, wxEXPAND); + + summary_grid->Add(new wxStaticText(this, wxID_ANY, _L("Creates")), 0, wxALIGN_CENTER_VERTICAL); + auto* recipe_row = new wxBoxSizer(wxHORIZONTAL); + m_recipe_preview = new wxPanel(this, wxID_ANY, wxDefaultPosition, wxSize(FromDIP(72), FromDIP(24)), wxBORDER_SIMPLE); + recipe_row->Add(m_recipe_preview, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(8)); + m_recipe_label = new wxStaticText(this, wxID_ANY, wxEmptyString); + m_recipe_label->Wrap(FromDIP(280)); + recipe_row->Add(m_recipe_label, 1, wxALIGN_CENTER_VERTICAL); + summary_grid->Add(recipe_row, 1, wxEXPAND); + + root->Add(summary_grid, 0, wxEXPAND | wxALL, FromDIP(12)); + + m_delta_label = new wxStaticText(this, wxID_ANY, wxEmptyString); + root->Add(m_delta_label, 0, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(12)); + + m_presets_label = new wxStaticText(this, wxID_ANY, _L("Exact preset mixes")); + root->Add(m_presets_label, 0, wxLEFT | wxRIGHT | wxTOP, FromDIP(12)); + m_presets_host = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxSize(-1, FromDIP(96)), wxVSCROLL | wxBORDER_SIMPLE); + m_presets_host->SetScrollRate(FromDIP(6), FromDIP(6)); + m_presets_sizer = new wxWrapSizer(wxHORIZONTAL, wxWRAPSIZER_DEFAULT_FLAGS); + m_presets_host->SetSizer(m_presets_sizer); + root->Add(m_presets_host, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, FromDIP(12)); + + m_error_label = new wxStaticText(this, wxID_ANY, wxEmptyString); + m_error_label->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#D32F2F"))); + root->Add(m_error_label, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, FromDIP(12)); + + if (wxSizer* button_sizer = CreateStdDialogButtonSizer(wxOK | wxCANCEL)) + root->Add(button_sizer, 0, wxEXPAND | wxALL, FromDIP(12)); + + m_loading_panel = new wxPanel(this, wxID_ANY); + m_loading_panel->SetMinSize(wxSize(-1, FromDIP(24))); + auto* loading_row = new wxBoxSizer(wxHORIZONTAL); + m_loading_label = new wxStaticText(m_loading_panel, wxID_ANY, " "); + loading_row->Add(m_loading_label, 1, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(8)); + m_loading_gauge = new wxGauge(m_loading_panel, wxID_ANY, 100, wxDefaultPosition, wxSize(FromDIP(150), FromDIP(8)), + wxGA_HORIZONTAL | wxGA_SMOOTH); + m_loading_gauge->SetValue(0); + m_loading_gauge->Enable(false); + loading_row->Add(m_loading_gauge, 0, wxALIGN_CENTER_VERTICAL); + m_loading_panel->SetSizer(loading_row); + root->Add(m_loading_panel, 0, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, FromDIP(12)); + + SetSizerAndFit(root); + + m_selected_target = safe_initial; + m_requested_target = safe_initial; + if (m_color_map) + m_color_map->set_min_component_percent(m_min_component_percent); + update_range_label(); + rebuild_presets_ui(); + sync_inputs_to_requested(); + update_dialog_state(); + + if (m_color_map) { + m_color_map->Bind(wxEVT_SLIDER, [this](wxCommandEvent&) { + if (!m_color_map) + return; + request_recipe_match(m_color_map->selected_color(), true, _L("Matching closest supported mix...")); + }); + } + + if (m_hex_input) { + m_hex_input->Bind(wxEVT_TEXT_ENTER, [this](wxCommandEvent&) { apply_hex_input(true); }); + m_hex_input->Bind(wxEVT_KILL_FOCUS, [this](wxFocusEvent& evt) { + apply_hex_input(false); + evt.Skip(); + }); + } + if (m_classic_picker) { + m_classic_picker->Bind(wxEVT_COLOURPICKER_CHANGED, [this](wxColourPickerEvent& evt) { + if (m_syncing_inputs) + return; + apply_requested_target(evt.GetColour()); + }); + } + if (m_range_slider) { + m_range_slider->Bind(wxEVT_SLIDER, [this](wxCommandEvent&) { + m_min_component_percent = m_range_slider ? std::clamp(m_range_slider->GetValue(), 0, 50) : m_min_component_percent; + update_range_label(); + if (m_color_map) + m_color_map->set_min_component_percent(m_min_component_percent); + rebuild_presets_ui(); + request_recipe_match(m_requested_target, true, _L("Matching closest supported mix...")); + }); + } + + Bind(wxEVT_TIMER, [this](wxTimerEvent&) { refresh_selected_recipe(); }, m_recipe_timer.GetId()); + Bind( + wxEVT_TIMER, + [this](wxTimerEvent&) { + if (m_loading_gauge && m_recipe_loading) + m_loading_gauge->Pulse(); + }, + m_loading_timer.GetId()); + if (wxWindow* ok_button = FindWindow(wxID_OK)) { + ok_button->Bind(wxEVT_BUTTON, [this](wxCommandEvent& evt) { + if (m_recipe_refresh_pending) + refresh_selected_recipe(); + if (m_recipe_loading || !m_selected_recipe.valid) + return; + evt.Skip(); + }); + } + + Bind(wxEVT_CLOSE_WINDOW, [this](wxCloseEvent&) { EndModal(wxID_CANCEL); }); + + CentreOnParent(); + wxGetApp().UpdateDlgDarkUI(this); + } + + ~MixedFilamentColorMatchDialog() override + { + if (m_recipe_timer.IsRunning()) + m_recipe_timer.Stop(); + if (m_loading_timer.IsRunning()) + m_loading_timer.Stop(); + } + + void begin_initial_recipe_load() { request_recipe_match(m_requested_target, false, _L("Calculating closest supported mix...")); } + + MixedColorMatchRecipeResult selected_recipe() const { return m_selected_recipe; } + + void on_dpi_changed(const wxRect& suggested_rect) override + { + wxUnusedVar(suggested_rect); + Layout(); + Fit(); + Refresh(); + } + +private: + void sync_recipe_preview(MixedColorMatchRecipeResult& recipe, const wxColour* requested_target = nullptr) + { + if (!recipe.valid) + return; + + recipe.preview_color = compute_color_match_recipe_display_color(recipe, m_display_context); + if (requested_target != nullptr && requested_target->IsOk()) + recipe.delta_e = color_delta_e00(*requested_target, recipe.preview_color); + } + + void update_range_label() + { + if (m_range_value) + m_range_value->SetLabel(wxString::Format(_L("%d%% min"), m_min_component_percent)); + } + + void rebuild_presets_ui() + { + if (!m_presets_host || !m_presets_sizer || !m_presets_label) + return; + + m_presets = build_color_match_presets(m_physical_colors, m_min_component_percent); + for (MixedColorMatchRecipeResult& preset : m_presets) + sync_recipe_preview(preset); + + m_presets_host->Freeze(); + while (m_presets_sizer->GetItemCount() > 0) { + wxSizerItem* item = m_presets_sizer->GetItem(size_t(0)); + wxWindow* window = item ? item->GetWindow() : nullptr; + m_presets_sizer->Remove(0); + if (window) + window->Destroy(); + } + + for (const MixedColorMatchRecipeResult& preset : m_presets) { + auto* button = new wxBitmapButton(m_presets_host, wxID_ANY, + make_color_match_swatch_bitmap(preset.preview_color, wxSize(FromDIP(30), FromDIP(20))), + wxDefaultPosition, wxDefaultSize, wxBU_EXACTFIT); + const wxString tooltip = from_u8(summarize_color_match_recipe(preset)) + "\n" + + normalize_color_match_hex(preset.preview_color.GetAsString(wxC2S_HTML_SYNTAX)); + button->SetToolTip(tooltip); + button->Bind(wxEVT_BUTTON, [this, preset](wxCommandEvent&) { apply_preset(preset); }); + m_presets_sizer->Add(button, 0, wxALL, FromDIP(2)); + } + + m_presets_host->FitInside(); + const bool show_presets = !m_presets.empty(); + m_presets_label->Show(show_presets); + m_presets_host->Show(show_presets); + m_presets_host->Thaw(); + } + + void set_recipe_loading(bool loading, const wxString& message) + { + m_recipe_loading = loading; + if (!message.empty()) + m_loading_message = message; + + if (m_loading_label) + m_loading_label->SetLabel(loading ? m_loading_message : wxString(" ")); + if (m_loading_gauge) { + if (loading) { + m_loading_gauge->Enable(true); + m_loading_gauge->Pulse(); + if (!m_loading_timer.IsRunning()) + m_loading_timer.Start(100); + } else { + if (m_loading_timer.IsRunning()) + m_loading_timer.Stop(); + m_loading_gauge->SetValue(0); + m_loading_gauge->Enable(false); + } + } + } + + void sync_inputs_to_requested() + { + m_syncing_inputs = true; + if (m_hex_input) + m_hex_input->ChangeValue(normalize_color_match_hex(m_requested_target.GetAsString(wxC2S_HTML_SYNTAX))); + if (m_classic_picker) + m_classic_picker->SetColour(m_requested_target); + m_syncing_inputs = false; + } + + bool apply_requested_target(const wxColour& requested_target) + { + request_recipe_match(requested_target, false, _L("Matching closest supported mix...")); + return true; + } + + bool apply_hex_input(bool show_invalid_error) + { + if (!m_hex_input || m_syncing_inputs) + return false; + + wxColour parsed; + if (!try_parse_color_match_hex(m_hex_input->GetValue(), parsed)) { + if (show_invalid_error && m_error_label) + m_error_label->SetLabel(_L("Use a valid hex color like #00FF88.")); + return false; + } + + return apply_requested_target(parsed); + } + + void request_recipe_match(const wxColour& requested_target, bool debounce, const wxString& loading_message) + { + m_requested_target = requested_target; + m_selected_target = requested_target; + sync_inputs_to_requested(); + + ++m_recipe_request_token; + set_recipe_loading(true, loading_message); + + if (m_recipe_timer.IsRunning()) + m_recipe_timer.Stop(); + m_recipe_refresh_pending = debounce; + update_dialog_state(); + + if (debounce) { + m_recipe_timer.StartOnce(120); + return; + } + + launch_recipe_match(m_recipe_request_token, requested_target); + } + + void refresh_selected_recipe() + { + m_recipe_refresh_pending = false; + launch_recipe_match(m_recipe_request_token, m_requested_target); + } + + void launch_recipe_match(size_t request_token, const wxColour& requested_target) + { + const std::vector physical_colors = m_physical_colors; + const int min_component_percent = m_min_component_percent; + wxWeakRef weak_self(this); + std::thread([weak_self, physical_colors, requested_target, request_token, min_component_percent]() { + MixedColorMatchRecipeResult recipe = build_best_color_match_recipe(physical_colors, requested_target, min_component_percent); + wxGetApp().CallAfter([weak_self, requested_target, recipe = std::move(recipe), request_token]() mutable { + if (!weak_self) + return; + auto* self = static_cast(weak_self.get()); + self->handle_recipe_result(request_token, requested_target, std::move(recipe)); + }); + }).detach(); + } + + void handle_recipe_result(size_t request_token, const wxColour& requested_target, MixedColorMatchRecipeResult recipe) + { + if (request_token != m_recipe_request_token) + return; + + m_has_recipe_result = true; + m_selected_recipe = std::move(recipe); + sync_recipe_preview(m_selected_recipe, &requested_target); + set_recipe_loading(false, wxEmptyString); + + if (m_selected_recipe.valid) { + m_selected_target = m_selected_recipe.preview_color; + if (m_color_map) + m_color_map->set_normalized_weights(expand_color_match_recipe_weights(m_selected_recipe, m_palette.size()), false); + sync_inputs_to_requested(); + } else { + m_selected_target = requested_target; + } + + update_dialog_state(); + } + + void apply_preset(MixedColorMatchRecipeResult preset) + { + preset.delta_e = 0.0; + sync_recipe_preview(preset); + ++m_recipe_request_token; + m_requested_target = preset.preview_color; + m_selected_target = preset.preview_color; + m_selected_recipe = std::move(preset); + m_has_recipe_result = true; + m_recipe_refresh_pending = false; + if (m_recipe_timer.IsRunning()) + m_recipe_timer.Stop(); + set_recipe_loading(false, wxEmptyString); + if (m_color_map) + m_color_map->set_normalized_weights(expand_color_match_recipe_weights(m_selected_recipe, m_palette.size()), false); + sync_inputs_to_requested(); + update_dialog_state(); + } + + void update_dialog_state() + { + const wxColour fallback = wxColour("#26A69A"); + if (m_selected_preview) { + m_selected_preview->SetBackgroundColour(m_requested_target.IsOk() ? m_requested_target : fallback); + m_selected_preview->Refresh(); + } + if (m_selected_label) + m_selected_label->SetLabel(m_requested_target.IsOk() ? + normalize_color_match_hex(m_requested_target.GetAsString(wxC2S_HTML_SYNTAX)) : + normalize_color_match_hex(fallback.GetAsString(wxC2S_HTML_SYNTAX))); + + const bool valid = m_selected_recipe.valid; + const wxColour recipe_color = (valid && m_selected_recipe.preview_color.IsOk()) ? + m_selected_recipe.preview_color : + (m_requested_target.IsOk() ? m_requested_target : fallback); + if (m_recipe_preview) { + m_recipe_preview->SetBackgroundColour(recipe_color); + m_recipe_preview->Refresh(); + } + if (m_recipe_label) { + if (m_recipe_loading) { + m_recipe_label->SetLabel(m_loading_message); + } else if (valid) { + const wxString recipe_summary = from_u8(summarize_color_match_recipe(m_selected_recipe)); + const wxString recipe_hex = normalize_color_match_hex(recipe_color.GetAsString(wxC2S_HTML_SYNTAX)); + m_recipe_label->SetLabel(recipe_summary + " " + recipe_hex); + } else if (m_has_recipe_result) { + m_recipe_label->SetLabel(_L("No supported 2-color, 3-color, or 4-color recipe found.")); + } else { + m_recipe_label->SetLabel(wxEmptyString); + } + } + if (m_delta_label) { + if (m_recipe_loading && m_requested_target.IsOk()) { + m_delta_label->SetLabel( + wxString::Format(_L("Matching %s..."), normalize_color_match_hex(m_requested_target.GetAsString(wxC2S_HTML_SYNTAX)))); + } else if (valid && m_requested_target.IsOk()) { + m_delta_label->SetLabel(wxString::Format(_L("Requested %s, closest recipe delta: %.2f"), + normalize_color_match_hex(m_requested_target.GetAsString(wxC2S_HTML_SYNTAX)), + m_selected_recipe.delta_e)); + } else { + m_delta_label->SetLabel(wxEmptyString); + } + } + if (m_error_label) { + if (m_recipe_loading) + m_error_label->SetLabel(wxEmptyString); + else if (!valid && m_has_recipe_result) + m_error_label->SetLabel( + _L("Unable to create a color mix from the current physical filament colors within the selected range.")); + else if (m_hex_input && !m_syncing_inputs) { + wxColour parsed; + if (!try_parse_color_match_hex(m_hex_input->GetValue(), parsed)) + m_error_label->SetLabel(_L("Use a valid hex color like #00FF88.")); + else + m_error_label->SetLabel(wxEmptyString); + } else { + m_error_label->SetLabel(wxEmptyString); + } + } + if (wxWindow* ok_button = FindWindow(wxID_OK)) + ok_button->Enable(valid && !m_recipe_loading && !m_recipe_refresh_pending); + + Layout(); + } + +private: + std::vector m_physical_colors; + MixedFilamentDisplayContext m_display_context; + std::vector m_palette; + std::vector m_presets; + MixedFilamentColorMapPanel* m_color_map = nullptr; + wxTextCtrl* m_hex_input = nullptr; + wxColourPickerCtrl* m_classic_picker = nullptr; + wxSlider* m_range_slider = nullptr; + wxStaticText* m_range_value = nullptr; + wxStaticText* m_presets_label = nullptr; + wxScrolledWindow* m_presets_host = nullptr; + wxWrapSizer* m_presets_sizer = nullptr; + wxPanel* m_loading_panel = nullptr; + wxStaticText* m_loading_label = nullptr; + wxGauge* m_loading_gauge = nullptr; + wxPanel* m_selected_preview = nullptr; + wxStaticText* m_selected_label = nullptr; + wxPanel* m_recipe_preview = nullptr; + wxStaticText* m_recipe_label = nullptr; + wxStaticText* m_delta_label = nullptr; + wxStaticText* m_error_label = nullptr; + wxColour m_requested_target{wxColour("#26A69A")}; + wxColour m_selected_target{wxColour("#26A69A")}; + MixedColorMatchRecipeResult m_selected_recipe; + wxTimer m_recipe_timer; + wxTimer m_loading_timer; + wxString m_loading_message; + size_t m_recipe_request_token{0}; + int m_min_component_percent{15}; + bool m_has_recipe_result{false}; + bool m_recipe_loading{false}; + bool m_recipe_refresh_pending{false}; + bool m_syncing_inputs{false}; +}; + + +MixedColorMatchRecipeResult prompt_best_color_match_recipe(wxWindow* parent, + const std::vector& physical_colors, + const wxColour& initial_color) +{ + MixedFilamentColorMatchDialog dlg(parent, physical_colors, initial_color); + dlg.begin_initial_recipe_load(); + if (dlg.ShowModal() != wxID_OK) { + MixedColorMatchRecipeResult cancelled; + cancelled.cancelled = true; + return cancelled; + } + + return dlg.selected_recipe(); +} static DynamicFilamentList dynamic_filament_list; static DynamicFilamentList1Based dynamic_filament_list_1_based; @@ -1024,11 +1610,6 @@ Sidebar::Sidebar(Plater *parent) Choice::register_dynamic_list("wipe_tower_filament", &dynamic_filament_list); p->scrolled = new wxPanel(this); - // p->scrolled->SetScrollbars(0, 100, 1, 2); // ys_DELETE_after_testing. pixelsPerUnitY = 100 - // but this cause the bad layout of the sidebar, when all infoboxes appear. - // As a result we can see the empty block at the bottom of the sidebar - // But if we set this value to 5, layout will be better - //p->scrolled->SetScrollRate(0, 5); p->scrolled->SetBackgroundColour(*wxWHITE); @@ -1432,7 +2013,7 @@ Sidebar::Sidebar(Plater *parent) p->m_panel_filament_title->SetBackgroundColor2(0xF1F1F1); p->m_panel_filament_title->Bind(wxEVT_LEFT_UP, [this](wxMouseEvent &e) { if (e.GetPosition().x > (p->m_flushing_volume_btn->IsShown() - ? p->m_flushing_volume_btn->GetPosition().x : (p->m_bpButton_add_filament->GetPosition().x - FromDIP(30)))) // ORCA exclude area of del button from titlebar collapse/expand feature to fix undesired collapse when user spams del filament button + ? p->m_flushing_volume_btn->GetPosition().x : (p->m_bpButton_ams_filament->GetPosition().x - FromDIP(30)))) return; if (p->m_panel_filament_content->GetMaxHeight() == 0) p->m_panel_filament_content->SetMaxSize({-1, -1}); @@ -1443,11 +2024,10 @@ Sidebar::Sidebar(Plater *parent) wxBoxSizer* bSizer39; bSizer39 = new wxBoxSizer( wxHORIZONTAL ); - p->m_filament_icon = new ScalableButton(p->m_panel_filament_title, wxID_ANY, "filament"); - p->m_staticText_filament_settings = new Label(p->m_panel_filament_title, _L("Filament"), LB_PROPAGATE_MOUSE_EVENT); - bSizer39->Add(p->m_filament_icon, 0, wxALIGN_CENTER | wxLEFT, FromDIP(SidebarProps::TitlebarMargin())); - bSizer39->AddSpacer(FromDIP(SidebarProps::ElementSpacing())); - bSizer39->Add( p->m_staticText_filament_settings, 0, wxALIGN_CENTER ); + p->m_filament_config_icon = new ScalableButton(p->m_panel_filament_title, wxID_ANY, "filament_group"); + bSizer39->Add(p->m_filament_config_icon, 0, wxALIGN_CENTER | wxLEFT, FromDIP(SidebarProps::TitlebarMargin())); + p->m_staticText_filament_settings = new Label(p->m_panel_filament_title, _L("Filament Management"), LB_PROPAGATE_MOUSE_EVENT); + bSizer39->Add( p->m_staticText_filament_settings, 0, wxALIGN_CENTER | wxLEFT, FromDIP(SidebarProps::TitlebarMargin())); bSizer39->Add(FromDIP(10), 0, 0, 0, 0); bSizer39->SetMinSize(-1, FromDIP(30)); @@ -1477,7 +2057,7 @@ Sidebar::Sidebar(Plater *parent) ConfigOptionFloat* flush_multi_opt = project_config.option("flush_multiplier"); float flush_multiplier = flush_multi_opt ? flush_multi_opt->getFloat() : 1.f; - const std::vector extruder_colours = wxGetApp().plater()->get_extruder_colors_from_plater_config(); + const std::vector extruder_colours = wxGetApp().plater()->get_extruder_colors_from_plater_config(nullptr, false); const auto& full_config = wxGetApp().preset_bundle->full_config(); const auto& extra_flush_volumes = get_min_flush_volumes(full_config); WipingDialog dlg(parent, cast(init_matrix), cast(init_extruders), extruder_colours, extra_flush_volumes, flush_multiplier); @@ -1498,57 +2078,6 @@ Sidebar::Sidebar(Plater *parent) bSizer39->Add(p->m_flushing_volume_btn, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(5)); bSizer39->Hide(p->m_flushing_volume_btn); - ScalableButton* add_btn = new ScalableButton(p->m_panel_filament_title, wxID_ANY, "add_filament"); - add_btn->SetToolTip(_L("Add one filament")); - add_btn->Bind(wxEVT_BUTTON, [this, scrolled_sizer](wxCommandEvent& e){ - // Orca: limit filament choices to MAXIMUM_EXTRUDER_NUMBER - if (p->combos_filament.size() >= MAXIMUM_EXTRUDER_NUMBER) - return; - - int filament_count = p->combos_filament.size() + 1; - wxColour new_col = Plater::get_next_color_for_filament(); - std::string new_color = new_col.GetAsString(wxC2S_HTML_SYNTAX).ToStdString(); - wxGetApp().preset_bundle->set_num_filaments(filament_count, new_color); - wxGetApp().plater()->on_filaments_change(filament_count); - wxGetApp().get_tab(Preset::TYPE_PRINT)->update(); - wxGetApp().preset_bundle->export_selections(*wxGetApp().app_config); - auto_calc_flushing_volumes(filament_count - 1); - }); - p->m_bpButton_add_filament = add_btn; - - // ORCA Moved add button after delete button to prevent add button position change when remove icon automatically hidden - - ScalableButton* del_btn = new ScalableButton(p->m_panel_filament_title, wxID_ANY, "delete_filament"); - del_btn->SetToolTip(_L("Remove last filament")); - del_btn->Bind(wxEVT_BUTTON, [this, scrolled_sizer](wxCommandEvent &e) { - if (p->combos_filament.size() <= 1) - return; - - size_t filament_count = p->combos_filament.size() - 1; - if (wxGetApp().preset_bundle->is_the_only_edited_filament(filament_count) || (filament_count == 1)) { - wxGetApp().get_tab(Preset::TYPE_FILAMENT)->select_preset(wxGetApp().preset_bundle->filament_presets[0], false, "", true); - } - - if (p->editing_filament >= filament_count) { - p->editing_filament = -1; - } - - wxGetApp().preset_bundle->set_num_filaments(filament_count); - wxGetApp().plater()->on_filaments_change(filament_count); - wxGetApp().get_tab(Preset::TYPE_PRINT)->update(); - wxGetApp().preset_bundle->export_selections(*wxGetApp().app_config); - }); - p->m_bpButton_del_filament = del_btn; - - bSizer39->Add(del_btn, 0, wxALIGN_CENTER | wxLEFT, FromDIP(SidebarProps::IconSpacing())); - bSizer39->Add(add_btn, 0, wxALIGN_CENTER | wxLEFT, FromDIP(SidebarProps::IconSpacing())); // ORCA Moved add button after delete button to prevent add button position change when remove icon automatically hidden - bSizer39->AddSpacer(FromDIP(20)); - - if (p->combos_filament.size() <= 1) { // ORCA Fix Flushing button and Delete filament button not hidden on launch while only 1 filament exist - bSizer39->Hide(p->m_flushing_volume_btn); - bSizer39->Hide(p->m_bpButton_del_filament); // ORCA: Hide delete filament button if there is only one filament - } - ams_btn = new ScalableButton(p->m_panel_filament_title, wxID_ANY, "ams_fila_sync", wxEmptyString, wxDefaultSize, wxDefaultPosition, wxBU_EXACTFIT | wxNO_BORDER, false, 16); // ORCA match icon size with other icons as 16x16 ams_btn->SetToolTip(_L("Synchronize filament list from AMS")); @@ -1580,17 +2109,27 @@ Sidebar::Sidebar(Plater *parent) //wxBoxSizer* bSizer_filament_content; //bSizer_filament_content = new wxBoxSizer( wxHORIZONTAL ); - // BBS: filament double columns + // BBS: filament double columns, wrapped in scrolled window (max 3 rows) p->sizer_filaments = new wxBoxSizer(wxHORIZONTAL); p->sizer_filaments->Add(new wxBoxSizer(wxVERTICAL), 1, wxEXPAND); p->sizer_filaments->Add(new wxBoxSizer(wxVERTICAL), 1, wxEXPAND); + p->m_scrolled_filaments = new wxScrolledWindow(p->m_panel_filament_content, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL); + p->m_scrolled_filaments->SetScrollRate(0, 5); + p->m_scrolled_filaments->SetBackgroundColour(*wxWHITE); + p->m_panel_scrolled_filament_content = new wxPanel(p->m_scrolled_filaments, wxID_ANY); + p->m_panel_scrolled_filament_content->SetBackgroundColour(*wxWHITE); + p->m_panel_scrolled_filament_content->SetSizer(p->sizer_filaments); + auto* scrolled_fila_sizer = new wxBoxSizer(wxVERTICAL); + scrolled_fila_sizer->Add(p->m_panel_scrolled_filament_content, 0, wxEXPAND); + p->m_scrolled_filaments->SetSizer(scrolled_fila_sizer); + p->combos_filament.push_back(nullptr); /* first filament item */ // init_filament_combo(&p->combos_filament[0], 0); - p->combos_filament[0] = new PlaterPresetComboBox(p->m_panel_filament_content, Preset::TYPE_FILAMENT); + p->combos_filament[0] = new PlaterPresetComboBox(p->m_panel_scrolled_filament_content, Preset::TYPE_FILAMENT); auto combo_and_btn_sizer = new wxBoxSizer(wxHORIZONTAL); // BBS: filament double columns combo_and_btn_sizer->AddSpacer(FromDIP(SidebarProps::ContentMargin())); @@ -1604,7 +2143,7 @@ Sidebar::Sidebar(Plater *parent) edit_btn->SetBackgroundColour(wxColour(255, 255, 255)); edit_btn->SetToolTip(_L("Click to edit preset"));*/ - ScalableButton* edit_btn = new ScalableButton(p->m_panel_filament_content, wxID_ANY, "menu_filament"); + ScalableButton* edit_btn = new ScalableButton(p->m_panel_scrolled_filament_content, wxID_ANY, "menu_filament"); edit_btn->SetToolTip(_L("Click to edit preset")); PlaterPresetComboBox* combobox = p->combos_filament[0]; @@ -1626,14 +2165,250 @@ Sidebar::Sidebar(Plater *parent) //bSizer_filament_content->Add(p->sizer_filaments, 1, wxALIGN_CENTER | wxALL); wxSizer *sizer_filaments2 = new wxBoxSizer(wxVERTICAL); - sizer_filaments2->AddSpacer(FromDIP(16)); - sizer_filaments2->Add(p->sizer_filaments, 0, wxEXPAND, 0); - sizer_filaments2->AddSpacer(FromDIP(16)); + // --- Filaments title bar (same level as Color Mix title) --- + p->m_panel_physical_filaments_title = new StaticBox(p->m_panel_filament_content, wxID_ANY, wxDefaultPosition, + wxDefaultSize, wxTAB_TRAVERSAL | wxBORDER_NONE); + p->m_panel_physical_filaments_title->SetBackgroundColor(title_bg); + p->m_panel_physical_filaments_title->SetBackgroundColor2(0xF1F1F1); + p->m_panel_physical_filaments_title->SetMinSize(wxSize(-1, FromDIP(30))); + p->m_panel_physical_filaments_title->SetMaxSize(wxSize(-1, FromDIP(30))); + p->m_filament_icon = new ScalableButton(p->m_panel_physical_filaments_title, wxID_ANY, "filament"); + auto* physical_label = new Label(p->m_panel_physical_filaments_title, _L("Filaments"), LB_PROPAGATE_MOUSE_EVENT); + auto* h_physical_title = new wxBoxSizer(wxHORIZONTAL); + auto* white_left_f = new wxPanel(p->m_panel_physical_filaments_title, wxID_ANY, wxDefaultPosition, wxSize(FromDIP(SidebarProps::ContentMargin()), -1)); + white_left_f->SetBackgroundColour(*wxWHITE); + h_physical_title->Add(white_left_f, 0, wxEXPAND | wxTOP | wxBOTTOM, 0); + h_physical_title->Add(p->m_filament_icon, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(SidebarProps::TitlebarMargin())); + h_physical_title->AddSpacer(FromDIP(SidebarProps::ElementSpacing())); + h_physical_title->Add(physical_label, 0, wxALIGN_CENTER_VERTICAL); + h_physical_title->AddStretchSpacer(); + + // Delete filament button — delegates to delete_filament for consistent remap behavior + ScalableButton* del_btn = new ScalableButton(p->m_panel_physical_filaments_title, wxID_ANY, "delete_filament"); + del_btn->SetToolTip(_L("Remove last filament")); + del_btn->Bind(wxEVT_BUTTON, [this](wxCommandEvent &e) { + if (p->combos_filament.size() <= 1) + return; + delete_filament(size_t(-1), -1); + }); + p->m_bpButton_del_filament = del_btn; + + // Add filament button + ScalableButton* add_btn = new ScalableButton(p->m_panel_physical_filaments_title, wxID_ANY, "add_filament"); + add_btn->SetToolTip(_L("Add one filament")); + add_btn->Bind(wxEVT_BUTTON, [this](wxCommandEvent& e){ + if (p->combos_filament.size() >= MAXIMUM_EXTRUDER_NUMBER) + return; + PresetBundle* pb = wxGetApp().preset_bundle; + if (!pb || pb->mixed_filaments.total_filaments(p->combos_filament.size()) >= MAXIMUM_FILAMENT_NUMBER) + return; + int filament_count = p->combos_filament.size() + 1; + wxGetApp().plater()->confirm_auto_generated_gradients(filament_count); + wxColour new_col = Plater::get_next_color_for_filament(); + std::string new_color = new_col.GetAsString(wxC2S_HTML_SYNTAX).ToStdString(); + pb->set_num_filaments(filament_count, new_color); + wxGetApp().plater()->on_filaments_change(filament_count); + wxGetApp().get_tab(Preset::TYPE_PRINT)->update(); + pb->export_selections(*wxGetApp().app_config); + auto_calc_flushing_volumes(filament_count - 1); + }); + p->m_bpButton_add_filament = add_btn; + + h_physical_title->Add(del_btn, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(4)); + h_physical_title->Add(add_btn, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(8)); + auto* white_right_f = new wxPanel(p->m_panel_physical_filaments_title, wxID_ANY, wxDefaultPosition, wxSize(FromDIP(SidebarProps::ContentMargin()), -1)); + white_right_f->SetBackgroundColour(*wxWHITE); + h_physical_title->Add(white_right_f, 0, wxEXPAND | wxTOP | wxBOTTOM, 0); + p->m_panel_physical_filaments_title->SetSizer(h_physical_title); + p->m_panel_physical_filaments_title->Layout(); + + if (p->combos_filament.size() <= 1) + h_physical_title->Hide(p->m_bpButton_del_filament); + + sizer_filaments2->AddSpacer(FromDIP(8)); + sizer_filaments2->Add(p->m_panel_physical_filaments_title, 0, wxEXPAND, 0); + sizer_filaments2->AddSpacer(FromDIP(8)); + sizer_filaments2->Add(p->m_scrolled_filaments, 0, wxEXPAND, 0); + sizer_filaments2->AddSpacer(FromDIP(8)); + // --- Color Mix Panel (inside filament content, same level as filaments) --- + init_color_mix_panel(p->m_panel_filament_content, sizer_filaments2); p->m_panel_filament_content->SetSizer(sizer_filaments2); p->m_panel_filament_content->Layout(); scrolled_sizer->Add(p->m_panel_filament_content, 0, wxEXPAND, 0); } + // --- Mixed Filaments Panel (Collapsible) --- + { + // Create title bar (StaticBox for collapsible header) + p->m_panel_mixed_filaments_title = new StaticBox(p->scrolled, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL | wxBORDER_NONE); + p->m_panel_mixed_filaments_title->SetBackgroundColor(title_bg); + p->m_panel_mixed_filaments_title->SetBackgroundColor2(0xF1F1F1); + + // Create icon + p->m_mixed_filaments_icon = new ScalableButton(p->m_panel_mixed_filaments_title, wxID_ANY, "filament"); + + // Create title text + p->m_staticText_mixed_filaments = new Label(p->m_panel_mixed_filaments_title, _L("Mixed Filaments"), LB_PROPAGATE_MOUSE_EVENT); + + // Create "Add Gradient" button + p->m_btn_add_gradient = new Button(p->m_panel_mixed_filaments_title, _L("Add Gradient")); + p->m_btn_add_gradient->SetStyle(ButtonStyle::Confirm, ButtonType::Compact); + p->m_btn_add_gradient->Bind(wxEVT_BUTTON, [this](wxCommandEvent& e) { + // Add gradient mixed filament + if (wxGetApp().preset_bundle) { + auto &mgr = wxGetApp().preset_bundle->mixed_filaments; + // Get physical filament colors + ConfigOptionStrings *co = wxGetApp().preset_bundle->project_config.option("filament_colour"); + std::vector colors = co ? co->values : std::vector(); + // Add a custom gradient (50% mix) + mgr.add_custom_filament(1, 2, 50, colors); + // Persist the custom entries so they survive the clear/load cycle in update_mixed_filament_panel + if (ConfigOptionString *opt = wxGetApp().preset_bundle->project_config.option("mixed_filament_definitions")) + opt->value = mgr.serialize_custom_entries(); + update_mixed_filament_panel(false); + m_scrolled_sizer->Layout(); + } + }); + + // Create "Add Pattern" button + p->m_btn_add_pattern = new Button(p->m_panel_mixed_filaments_title, _L("Add Pattern")); + p->m_btn_add_pattern->SetStyle(ButtonStyle::Confirm, ButtonType::Compact); + p->m_btn_add_pattern->Bind(wxEVT_BUTTON, [this](wxCommandEvent& e) { + // Add pattern mixed filament + if (wxGetApp().preset_bundle) { + auto &mgr = wxGetApp().preset_bundle->mixed_filaments; + // Get physical filament colors + ConfigOptionStrings *co = wxGetApp().preset_bundle->project_config.option("filament_colour"); + std::vector colors = co ? co->values : std::vector(); + // Add a custom pattern filament (will be configured by user) + mgr.add_custom_filament(1, 2, 50, colors); + // Set manual pattern for the newly added filament + auto &mfs = mgr.mixed_filaments(); + if (!mfs.empty()) { + mfs.back().manual_pattern = "12"; + mfs.back().custom = true; + } + // Persist the custom entries so they survive the clear/load cycle in update_mixed_filament_panel + if (ConfigOptionString *opt = wxGetApp().preset_bundle->project_config.option("mixed_filament_definitions")) + opt->value = mgr.serialize_custom_entries(); + update_mixed_filament_panel(false); + m_scrolled_sizer->Layout(); + } + }); + + // Create "Add Color" button + p->m_btn_add_color = new Button(p->m_panel_mixed_filaments_title, _L("Add Color")); + p->m_btn_add_color->SetStyle(ButtonStyle::Confirm, ButtonType::Compact); + p->m_btn_add_color->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { + if (wxGetApp().preset_bundle == nullptr) + return; + + ConfigOptionStrings *co = wxGetApp().preset_bundle->project_config.option("filament_colour"); + const std::vector colors = co ? co->values : std::vector(); + if (colors.size() < 2) + return; + + const MixedColorMatchRecipeResult recipe = + prompt_best_color_match_recipe(this, colors, Plater::get_next_color_for_filament()); + if (recipe.cancelled) + return; + if (!recipe.valid) { + show_error(this, _L("Unable to create a color match from the current physical filament colors.")); + return; + } + + const MixedFilamentDisplayContext display_context = build_mixed_filament_display_context(colors); + auto &mgr = wxGetApp().preset_bundle->mixed_filaments; + mgr.set_display_context(display_context); + mgr.add_custom_filament(recipe.component_a, recipe.component_b, recipe.mix_b_percent, colors); + auto &mfs = mgr.mixed_filaments(); + if (!mfs.empty()) { + MixedFilament &created = mfs.back(); + created.manual_pattern = recipe.manual_pattern; + created.mix_b_percent = recipe.mix_b_percent; + created.gradient_component_ids = recipe.gradient_component_ids; + created.gradient_component_weights = recipe.gradient_component_weights; + created.pointillism_all_filaments = false; + created.distribution_mode = recipe.gradient_component_ids.empty() ? int(MixedFilament::Simple) : int(MixedFilament::LayerCycle); + created.custom = true; + created.display_color = compute_color_match_recipe_display_color(recipe, display_context).GetAsString(wxC2S_HTML_SYNTAX).ToStdString(); + } + + if (ConfigOptionString *opt = wxGetApp().preset_bundle->project_config.option("mixed_filament_definitions")) + opt->value = mgr.serialize_custom_entries(); + update_mixed_filament_panel(false); + m_scrolled_sizer->Layout(); + }); + + // Create horizontal sizer for title bar + wxBoxSizer* h_sizer_mixed_title = new wxBoxSizer(wxHORIZONTAL); + h_sizer_mixed_title->Add(p->m_mixed_filaments_icon, 0, wxALIGN_CENTER | wxLEFT, FromDIP(SidebarProps::TitlebarMargin())); + h_sizer_mixed_title->AddSpacer(FromDIP(SidebarProps::ElementSpacing())); + h_sizer_mixed_title->Add(p->m_staticText_mixed_filaments, 0, wxALIGN_CENTER); + h_sizer_mixed_title->AddStretchSpacer(); + h_sizer_mixed_title->Add(p->m_btn_add_gradient, 0, wxALIGN_CENTER | wxRIGHT, FromDIP(SidebarProps::ElementSpacing())); + h_sizer_mixed_title->Add(p->m_btn_add_pattern, 0, wxALIGN_CENTER | wxRIGHT, FromDIP(SidebarProps::ElementSpacing())); + h_sizer_mixed_title->Add(p->m_btn_add_color, 0, wxALIGN_CENTER | wxRIGHT, FromDIP(SidebarProps::TitlebarMargin())); + h_sizer_mixed_title->SetMinSize(-1, FromDIP(30)); + + p->m_panel_mixed_filaments_title->SetSizer(h_sizer_mixed_title); + p->m_panel_mixed_filaments_title->Layout(); + + // Add splitter line before title + auto spliter_mixed_1 = new ::StaticLine(p->scrolled); + spliter_mixed_1->SetLineColour("#A6A9AA"); + scrolled_sizer->Add(spliter_mixed_1, 0, wxEXPAND); + + // Add title bar to scrolled sizer + scrolled_sizer->Add(p->m_panel_mixed_filaments_title, 0, wxEXPAND | wxALL, 0); + + // Add splitter line after title + auto spliter_mixed_2 = new ::StaticLine(p->scrolled); + spliter_mixed_2->SetLineColour("#CECECE"); + scrolled_sizer->Add(spliter_mixed_2, 0, wxEXPAND); + + // Create content panel (collapsible) + p->m_panel_mixed_filaments_content = new wxPanel(p->scrolled, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL); + { + const bool is_dark = wxGetApp().dark_mode(); + p->m_panel_mixed_filaments_content->SetBackgroundColour(is_dark ? wxColour(45, 45, 49) : wxColour(255, 255, 255)); + } + + // Content sizer - store in member variable for later use + p->m_sizer_mixed_filaments_content = new wxBoxSizer(wxVERTICAL); + p->m_sizer_mixed_filaments_content->AddSpacer(FromDIP(SidebarProps::ContentMargin())); + p->m_panel_mixed_filaments_content->SetSizer(p->m_sizer_mixed_filaments_content); + p->m_panel_mixed_filaments_content->Layout(); + + // Add content panel to scrolled sizer + scrolled_sizer->Add(p->m_panel_mixed_filaments_content, 0, wxEXPAND, 0); + + // Bind collapse/expand event to title bar + p->m_panel_mixed_filaments_title->Bind(wxEVT_LEFT_UP, [this](wxMouseEvent& e) { + // Exclude button areas from collapse/expand + int button_left = p->m_panel_mixed_filaments_title->GetClientSize().x; + auto consider_button = [&button_left](wxWindow *button) { + if (button && button->IsShown()) + button_left = std::min(button_left, button->GetPosition().x); + }; + consider_button(p->m_btn_add_gradient); + consider_button(p->m_btn_add_pattern); + consider_button(p->m_btn_add_color); + if (e.GetPosition().x > button_left - FromDIP(12)) + return; + + if (p->m_panel_mixed_filaments_content->GetMaxHeight() == 0) + p->m_panel_mixed_filaments_content->SetMaxSize({-1, -1}); + else + p->m_panel_mixed_filaments_content->SetMaxSize({-1, 0}); + m_scrolled_sizer->Layout(); + }); + + // Initially hidden until 2+ filaments + p->m_panel_mixed_filaments_title->Hide(); + p->m_panel_mixed_filaments_content->Hide(); + } + { //add project title auto params_panel = ((MainFrame*)parent->GetParent())->m_param_panel; @@ -1749,7 +2524,7 @@ void Sidebar::create_printer_preset() void Sidebar::init_filament_combo(PlaterPresetComboBox **combo, const int filament_idx) { - *combo = new PlaterPresetComboBox(p->m_panel_filament_content, Slic3r::Preset::TYPE_FILAMENT); + *combo = new PlaterPresetComboBox(p->m_panel_scrolled_filament_content, Slic3r::Preset::TYPE_FILAMENT); (*combo)->set_filament_idx(filament_idx); auto combo_and_btn_sizer = new wxBoxSizer(wxHORIZONTAL); @@ -1778,7 +2553,7 @@ void Sidebar::init_filament_combo(PlaterPresetComboBox **combo, const int filame combo_and_btn_sizer->Add(32 * em / 10, 0, 0, 0, 0); combo_and_btn_sizer->Add(del_btn, 0, wxALIGN_CENTER_VERTICAL, 5 * em / 10); */ - ScalableButton* edit_btn = new ScalableButton(p->m_panel_filament_content, wxID_ANY, "menu_filament"); + ScalableButton* edit_btn = new ScalableButton(p->m_panel_scrolled_filament_content, wxID_ANY, "menu_filament"); edit_btn->SetToolTip(_L("Click to edit preset")); PlaterPresetComboBox* combobox = (*combo); @@ -1941,11 +2716,13 @@ void Sidebar::update_all_preset_comboboxes(bool reload_printer_view) } if (cfg.opt_bool("pellet_modded_printer")) { - p->m_staticText_filament_settings->SetLabel(_L("Pellets")); + p->m_staticText_filament_settings->SetLabel(_L("Pellet Configuration")); p->m_filament_icon->SetBitmap_("pellets"); + p->m_filament_config_icon->SetBitmap_("filament_group"); } else { - p->m_staticText_filament_settings->SetLabel(_L("Filament")); + p->m_staticText_filament_settings->SetLabel(_L("Filament Management")); p->m_filament_icon->SetBitmap_("filament"); + p->m_filament_config_icon->SetBitmap_("filament_group"); } show_SEMM_buttons(/*cfg.opt_bool("single_extruder_multi_material")*/true); @@ -2223,6 +3000,7 @@ void Sidebar::msw_rescale() p->m_printerinfo_syncbtn->msw_rescale(); p->m_printer_setting->msw_rescale(); p->m_filament_icon->msw_rescale(); + p->m_color_mix_icon->msw_rescale(); p->m_bpButton_add_filament->msw_rescale(); p->m_bpButton_del_filament->msw_rescale(); p->m_bpButton_ams_filament->msw_rescale(); @@ -2295,6 +3073,7 @@ void Sidebar::sys_color_changed() p->m_printerinfo_syncbtn->msw_rescale(); p->m_printer_setting->msw_rescale(); p->m_filament_icon->msw_rescale(); + p->m_color_mix_icon->msw_rescale(); p->m_bpButton_add_filament->msw_rescale(); p->m_bpButton_del_filament->msw_rescale(); p->m_bpButton_ams_filament->msw_rescale(); @@ -2370,8 +3149,31 @@ void Sidebar::on_filaments_change(size_t num_filaments) { auto& choices = combos_filament(); - if (num_filaments == choices.size()) + if (num_filaments == choices.size()) { + // Project load may keep the same physical filament count while mixed + // definitions changed. Refresh mixed panel even without count changes. + const bool sync_manager = !p->m_skip_mixed_filament_sync_once; + p->m_skip_mixed_filament_sync_once = false; + update_ui_from_settings(); + update_dynamic_filament_list(); + update_mixed_filament_panel(sync_manager); + + // Recalc scrolled filament window height (max 3 rows, matches color mix) + if (p->m_scrolled_filaments && p->m_panel_scrolled_filament_content) { + p->m_panel_scrolled_filament_content->Layout(); + const wxSize content_best = p->m_panel_scrolled_filament_content->GetBestSize(); + const int row_count = ((int)num_filaments + 1) / 2; // 2-column grid + const int desired_h = row_count > 3 + ? (content_best.GetHeight() * 3) / std::max(1, row_count) + : content_best.GetHeight(); + p->m_scrolled_filaments->SetMinSize({-1, desired_h}); + p->m_scrolled_filaments->SetMaxSize({-1, desired_h}); + } + Layout(); return; + } + + p->m_skip_mixed_filament_sync_once = false; if (choices.size() == 1 || num_filaments == 1) choices[0]->GetDropDown().Invalidate(); @@ -2399,36 +3201,4017 @@ void Sidebar::on_filaments_change(size_t num_filaments) if (p->m_flushing_volume_btn != nullptr && sizer != nullptr) { if (num_filaments > 1) { sizer->Show(p->m_flushing_volume_btn); - sizer->Show(p->m_bpButton_del_filament); // ORCA: Show delete filament button if multiple filaments } else { sizer->Hide(p->m_flushing_volume_btn); - sizer->Hide(p->m_bpButton_del_filament); // ORCA: Hide delete filament button if there is only one filament + } + } + if (p->m_bpButton_del_filament != nullptr && p->m_panel_physical_filaments_title != nullptr) { + auto* inner_sizer = p->m_panel_physical_filaments_title->GetSizer(); + if (inner_sizer) { + if (num_filaments > 1) + inner_sizer->Show(p->m_bpButton_del_filament); + else + inner_sizer->Hide(p->m_bpButton_del_filament); } } + // Recalc scrolled filament window height (max 3 rows, matches color mix) + if (p->m_scrolled_filaments && p->m_panel_scrolled_filament_content) { + p->m_panel_scrolled_filament_content->Layout(); + const wxSize content_best = p->m_panel_scrolled_filament_content->GetBestSize(); + const int row_count = ((int)num_filaments + 1) / 2; // 2-column grid + const int desired_h = row_count > 3 + ? (content_best.GetHeight() / std::max(1, row_count)) * 3 + : content_best.GetHeight(); + p->m_scrolled_filaments->SetMinSize({-1, desired_h}); + p->m_scrolled_filaments->SetMaxSize({-1, desired_h}); + } + Layout(); + wxWeakRef weak_this(this); + wxTheApp->CallAfter([weak_this]() { + Sidebar* sidebar = weak_this.get(); + if (sidebar && sidebar->p && sidebar->p->m_scrolled_filaments) { + int vh = sidebar->p->m_scrolled_filaments->GetVirtualSize().y; + int ch = sidebar->p->m_scrolled_filaments->GetClientSize().y; + sidebar->p->m_scrolled_filaments->Scroll(0, std::max(0, vh - ch)); + } + }); p->m_panel_filament_title->Refresh(); update_ui_from_settings(); update_dynamic_filament_list(); -} + update_mixed_filament_panel(); + update_color_mix_panel(); -void Sidebar::add_filament() { - if (p->combos_filament.size() >= MAXIMUM_EXTRUDER_NUMBER) return; - wxColour new_col = Plater::get_next_color_for_filament(); - add_custom_filament(new_col); + // Disable add buttons when combined filament limit reached + if (PresetBundle *pb = wxGetApp().preset_bundle) { + const bool can_add = pb->mixed_filaments.total_filaments(combos_filament().size()) < MAXIMUM_FILAMENT_NUMBER; + if (p->m_bpButton_add_filament) + p->m_bpButton_add_filament->Enable(can_add); + if (p->m_btn_add_color_mix) + p->m_btn_add_color_mix->Enable(can_add); + } } -void Sidebar::on_filaments_delete(size_t filament_id) + +class MixedGradientWeightsDialog : public wxDialog { - auto& choices = combos_filament(); +public: + MixedGradientWeightsDialog(wxWindow *parent, + const std::vector &filament_ids, + const std::vector &palette, + const std::vector &initial_weights) + : wxDialog(parent, wxID_ANY, _L("Gradient Mix Weights"), wxDefaultPosition, wxDefaultSize, + wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER) + { + m_colors.reserve(filament_ids.size()); + m_weights = normalize_color_match_weights(initial_weights, filament_ids.size()); + for (const unsigned int filament_id : filament_ids) { + if (filament_id >= 1 && filament_id <= palette.size()) + m_colors.emplace_back(palette[filament_id - 1]); + else + m_colors.emplace_back(wxColour("#26A69A")); + } + if (m_colors.empty()) + m_colors.emplace_back(wxColour("#26A69A")); + + auto *root = new wxBoxSizer(wxVERTICAL); + auto *hint = new wxStaticText(this, wxID_ANY, _L("Pick a point in the gradient map to control multi-filament mix.")); + root->Add(hint, 0, wxEXPAND | wxALL, FromDIP(10)); + + m_color_map = new MixedFilamentColorMapPanel(this, filament_ids, palette, initial_weights, + wxSize(FromDIP(240), FromDIP(240))); + root->Add(m_color_map, 1, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, FromDIP(10)); + + for (size_t i = 0; i < filament_ids.size(); ++i) { + auto *row = new wxBoxSizer(wxHORIZONTAL); + wxPanel *chip = new wxPanel(this, wxID_ANY, wxDefaultPosition, wxSize(FromDIP(18), FromDIP(18)), wxBORDER_SIMPLE); + chip->SetBackgroundColour(m_colors[i]); + row->Add(chip, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(6)); + row->Add(new wxStaticText(this, wxID_ANY, wxString::Format("F%d", int(filament_ids[i]))), + 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(8)); + auto *label = new wxStaticText(this, wxID_ANY, wxString::Format("%d%%", m_weights[i])); + label->SetFont(Label::Body_12); + row->Add(label, 0, wxALIGN_CENTER_VERTICAL); + root->Add(row, 0, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, FromDIP(8)); + m_weight_labels.emplace_back(label); + } + + root->Add(CreateSeparatedButtonSizer(wxOK | wxCANCEL), 0, wxEXPAND | wxALL, FromDIP(8)); + SetSizerAndFit(root); + SetMinSize(wxSize(FromDIP(380), std::max(GetSize().GetHeight(), FromDIP(460)))); + update_weight_labels(); + + Bind(wxEVT_CLOSE_WINDOW, [this](wxCloseEvent&) { EndModal(wxID_CANCEL); }); + + if (m_color_map) { + m_color_map->Bind(wxEVT_SLIDER, [this](wxCommandEvent &) { + m_weights = m_color_map ? m_color_map->normalized_weights() : m_weights; + update_weight_labels(); + }); + } + } - if (filament_id >= choices.size()) - return; + std::vector normalized_weights() const + { + return m_color_map ? m_color_map->normalized_weights() : m_weights; + } - if (choices.size() == 1) - choices[0]->GetDropDown().Invalidate(); +private: + void update_weight_labels() + { + for (size_t i = 0; i < m_weight_labels.size() && i < m_weights.size(); ++i) { + if (m_weight_labels[i]) + m_weight_labels[i]->SetLabel(wxString::Format("%d%%", m_weights[i])); + } + Layout(); + } - wxWindowUpdateLocker noUpdates_scrolled_panel(this); +private: + MixedFilamentColorMapPanel *m_color_map { nullptr }; + std::vector m_colors; + std::vector m_weights; + std::vector m_weight_labels; +}; + +// Forward declaration for MixedMixPreview (defined below) +class MixedMixPreview; + +// Inline editor panel for configuring a single mixed filament +class MixedFilamentConfigPanel : public wxPanel +{ +public: + using OnChangeFn = std::function; + + MixedFilamentConfigPanel(wxWindow *parent, + size_t mixed_id, + const MixedFilament &mf, + size_t num_physical, + const std::vector &physical_colors, + const std::vector &nozzle_diameters, + const std::vector &palette, + const MixedFilamentPreviewSettings &preview_settings, + bool bias_mode_enabled, + OnChangeFn on_change = {}); + + // Get the updated mixed filament data + MixedFilament get_mixed_filament() const { return m_mf; } + bool has_changes() const { return m_has_changes; } + static int effective_local_z_preview_mix_b_percent(const MixedFilament &mf, + const MixedFilamentPreviewSettings &preview_settings); + +private: + void build_ui(); + void update_preview(); + void update_local_z_breakdown(); + void update_component_picker_visuals(); + + size_t m_mixed_id; + MixedFilament m_mf; + size_t m_num_physical; + std::vector m_physical_colors; + std::vector m_nozzle_diameters; + std::vector m_palette; + MixedFilamentPreviewSettings m_preview_settings; + bool m_bias_mode_enabled = false; + bool m_has_changes = false; + + wxChoice *m_choice_a = nullptr; + wxChoice *m_choice_b = nullptr; + wxChoice *m_choice_c = nullptr; + wxChoice *m_choice_d = nullptr; + wxPanel *m_picker_a_container = nullptr; + wxPanel *m_picker_b_container = nullptr; + wxPanel *m_picker_c_container = nullptr; + wxPanel *m_picker_d_container = nullptr; + wxPanel *m_picker_a_swatch = nullptr; + wxPanel *m_picker_b_swatch = nullptr; + wxPanel *m_picker_c_swatch = nullptr; + wxPanel *m_picker_d_swatch = nullptr; + wxStaticText *m_picker_a_label = nullptr; + wxStaticText *m_picker_b_label = nullptr; + wxStaticText *m_picker_c_label = nullptr; + wxStaticText *m_picker_d_label = nullptr; + wxPanel *m_surface_offset_target_container = nullptr; + wxPanel *m_surface_offset_target_swatch = nullptr; + wxStaticText *m_surface_offset_target_label = nullptr; + MixedGradientSelector *m_blend_selector = nullptr; + wxStaticText *m_blend_label = nullptr; + wxTextCtrl *m_pattern_ctrl = nullptr; + wxCheckBox *m_local_z_limit_checkbox = nullptr; + wxSpinCtrl *m_local_z_limit_spin = nullptr; + wxSpinCtrlDouble *m_surface_offset_spin = nullptr; + std::vector m_pattern_quick_buttons; + MixedMixPreview *m_mix_preview = nullptr; + wxStaticText *m_breakdown_label = nullptr; + wxPanel *m_swatch = nullptr; + std::shared_ptr> m_selected_weight_state; + OnChangeFn m_on_change; + + // Helper functions (copied from update_mixed_filament_panel) + static std::vector decode_gradient_ids(const std::string &s); + static std::string encode_gradient_ids(const std::vector &ids); + static std::vector decode_manual_pattern_ids(const std::string &pattern, + unsigned int a, + unsigned int b, + size_t num_physical, + size_t wall_loops = 0); + static std::vector decode_gradient_weights(const std::string &s, size_t n); + static std::vector normalize_gradient_weights(const std::vector &w, size_t n); + static std::string encode_gradient_weights(const std::vector &w); + static std::vector build_weighted_pair_sequence(unsigned int a, unsigned int b, int percent_b, bool limit_cycle = false); + static std::vector build_weighted_multi_sequence(const std::vector &ids, + const std::vector &weights, + size_t max_cycle_limit = 0); + static std::string summarize_sequence(const std::vector &seq); + static std::string summarize_local_z_breakdown(const MixedFilament &mf, + const std::vector &weights, + const MixedFilamentPreviewSettings &preview_settings); + static std::string blend_from_sequence(const std::vector &colors, const std::vector &seq, const std::string &fallback); + static std::vector build_local_z_preview_pass_heights(double nominal_layer_height, + double lower_bound, + double upper_bound, + double preferred_a_height, + double preferred_b_height, + int mix_b_percent, + int max_sublayers_limit); +}; + +class MixedMixPreview : public wxPanel +{ +public: + explicit MixedMixPreview(wxWindow *parent) + : wxPanel(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxBORDER_NONE) + { + SetBackgroundStyle(wxBG_STYLE_PAINT); + SetMinSize(wxSize(FromDIP(120), FromDIP(20))); + Bind(wxEVT_PAINT, &MixedMixPreview::on_paint, this); + } + + void set_data(const std::vector &palette, + const std::vector &sequence, + bool same_layer_mode, + const std::vector &surface_offsets_mm, + const wxColour &fallback, + const wxString &left_overlay, + const wxString &right_overlay) + { + m_palette = palette; + m_sequence = sequence; + m_same_layer = same_layer_mode; + m_surface_offsets_mm = surface_offsets_mm; + m_fallback = fallback; + m_left_overlay = left_overlay; + m_right_overlay = right_overlay; + Refresh(); + } + +private: + wxRect preview_rect() const + { + const int margin_x = FromDIP(1); + const int margin_y = FromDIP(1); + const wxSize sz = GetClientSize(); + return wxRect(margin_x, margin_y, std::max(1, sz.GetWidth() - margin_x * 2), std::max(1, sz.GetHeight() - margin_y * 2)); + } + + wxColour color_for_extruder(unsigned int extruder_id) const + { + if (extruder_id >= 1 && extruder_id <= m_palette.size()) + return m_palette[extruder_id - 1]; + return m_fallback; + } + + double max_active_surface_offset_mm() const + { + double max_offset = 0.0; + for (double offset_mm : m_surface_offsets_mm) + max_offset = std::max(max_offset, std::abs(offset_mm)); + return std::max(0.001, max_offset); + } + + int slot_inset_for_extruder(unsigned int extruder_id, int slot_extent) const + { + if (extruder_id == 0 || extruder_id >= m_surface_offsets_mm.size() || slot_extent <= 2) + return 0; + + const double offset_mm = m_surface_offsets_mm[extruder_id]; + if (std::abs(offset_mm) <= EPSILON) + return 0; + + const double normalized = std::clamp(std::abs(offset_mm) / max_active_surface_offset_mm(), 0.0, 1.0); + const int inset = int(std::round(normalized * slot_extent * 0.45)) * (offset_mm < 0.0 ? -1 : 1); + return std::clamp(inset, -std::max(0, slot_extent / 2), std::max(0, slot_extent / 2)); + } + + void on_paint(wxPaintEvent &) + { + wxAutoBufferedPaintDC dc(this); + dc.SetBackground(wxBrush(GetBackgroundColour())); + dc.Clear(); + + const wxRect rect = preview_rect(); + dc.SetPen(*wxTRANSPARENT_PEN); + dc.SetBrush(wxBrush(m_fallback)); + dc.DrawRectangle(rect); + + if (!m_sequence.empty()) { + if (m_same_layer) { + // Same-layer preview: full-height stripe lines. + const int stripes = 24; + const int stripe_w = std::max(1, rect.GetWidth() / stripes); + const size_t seq_len = m_sequence.size(); + for (int s = 0; s < stripes; ++s) { + const size_t idx = size_t(s % int(seq_len)); + const unsigned int extruder_id = m_sequence[idx]; + dc.SetBrush(wxBrush(color_for_extruder(m_sequence[idx]))); + const int x = rect.GetLeft() + s * stripe_w; + const int w = (s == stripes - 1) ? (rect.GetRight() - x + 1) : stripe_w; + const int inset = slot_inset_for_extruder(extruder_id, w); + wxRect draw_rect(x + inset / 2, rect.GetTop(), std::max(1, w - inset), rect.GetHeight()); + draw_rect.Intersect(rect); + if (draw_rect.GetWidth() > 0) + dc.DrawRectangle(draw_rect); + } + } else { + const int bars = 24; + const int bar_w = std::max(1, rect.GetWidth() / bars); + for (int i = 0; i < bars; ++i) { + size_t idx = 0; + if (m_sequence.size() > size_t(bars)) + idx = (size_t(i) * m_sequence.size()) / size_t(bars); + else + idx = size_t(i) % m_sequence.size(); + const unsigned int extruder_id = m_sequence[idx]; + dc.SetBrush(wxBrush(color_for_extruder(extruder_id))); + const int x = rect.GetLeft() + i * bar_w; + const int w = (i == bars - 1) ? (rect.GetRight() - x + 1) : bar_w; + const int inset = slot_inset_for_extruder(extruder_id, w); + wxRect draw_rect(x + inset / 2, rect.GetTop(), std::max(1, w - inset), rect.GetHeight()); + draw_rect.Intersect(rect); + if (draw_rect.GetWidth() > 0) + dc.DrawRectangle(draw_rect); + } + } + } + + auto draw_outlined_text = [this, &dc](const wxString &text, int x, int y) { + if (text.empty()) + return; + dc.SetTextForeground(wxColour(255, 255, 255)); + const int outline_radius = std::max(2, FromDIP(2)); + for (int ox = -outline_radius; ox <= outline_radius; ++ox) { + for (int oy = -outline_radius; oy <= outline_radius; ++oy) { + if (ox == 0 && oy == 0) + continue; + dc.DrawText(text, x + ox, y + oy); + } + } + dc.SetTextForeground(wxColour(22, 22, 22)); + dc.DrawText(text, x, y); + }; + + wxCoord left_w = 0, left_h = 0; + wxCoord right_w = 0, right_h = 0; + dc.GetTextExtent(m_left_overlay, &left_w, &left_h); + dc.GetTextExtent(m_right_overlay, &right_w, &right_h); + const int text_y = rect.GetTop() + std::max(0, (rect.GetHeight() - int(std::max(left_h, right_h))) / 2); + const int pad = FromDIP(6); + if (!m_left_overlay.empty()) + draw_outlined_text(m_left_overlay, rect.GetLeft() + pad, text_y); + if (!m_right_overlay.empty()) + draw_outlined_text(m_right_overlay, rect.GetRight() - pad - int(right_w), text_y); + + const bool is_dark = wxGetApp().dark_mode(); + dc.SetPen(wxPen(is_dark ? wxColour(110, 110, 110) : wxColour(170, 170, 170), 1)); + dc.SetBrush(*wxTRANSPARENT_BRUSH); + dc.DrawRectangle(rect); + } + +private: + std::vector m_palette; + std::vector m_sequence; + std::vector m_surface_offsets_mm; + bool m_same_layer { false }; + wxColour m_fallback { wxColour(38, 166, 154) }; + wxString m_left_overlay; + wxString m_right_overlay; +}; + +// Implementation of MixedFilamentConfigPanel helper functions +static std::vector build_grouped_manual_pattern_preview_sequence(const std::string &pattern, + unsigned int component_a, + unsigned int component_b, + size_t num_physical, + size_t wall_loops) +{ + std::vector sequence; + if (num_physical == 0) + return sequence; + + const std::string normalized = MixedFilamentManager::normalize_manual_pattern(pattern); + if (normalized.empty()) + return sequence; + + const std::vector groups = MixedFilamentManager::split_pattern_groups(normalized); + if (groups.empty()) + return sequence; + + MixedFilament dummy_mf; + dummy_mf.component_a = component_a; + dummy_mf.component_b = component_b; + + if (groups.size() == 1) { + const std::vector tokens = + MixedFilamentManager::split_pattern_group_to_tokens(groups[0], num_physical); + sequence.reserve(tokens.size()); + for (const std::string &token : tokens) { + const unsigned int extruder_id = + MixedFilamentManager::physical_filament_from_token(token, dummy_mf, num_physical); + if (extruder_id != 0) + sequence.emplace_back(extruder_id); + } + return sequence; + } + + std::vector> group_tokens; + group_tokens.reserve(groups.size()); + for (const std::string &group : groups) + group_tokens.push_back(MixedFilamentManager::split_pattern_group_to_tokens(group, num_physical)); + + constexpr size_t k_max_preview_cycle = 48; + size_t cycle = 1; + for (const auto &tokens : group_tokens) { + if (tokens.empty()) + continue; + cycle = std::lcm(cycle, tokens.size()); + if (cycle >= k_max_preview_cycle) { + cycle = k_max_preview_cycle; + break; + } + } + + const size_t preview_wall_loops = std::max(1, wall_loops == 0 ? groups.size() : wall_loops); + sequence.reserve(preview_wall_loops * cycle); + for (size_t layer_idx = 0; layer_idx < cycle; ++layer_idx) { + for (size_t wall_idx = 0; wall_idx < preview_wall_loops; ++wall_idx) { + const auto &tokens = group_tokens[std::min(wall_idx, group_tokens.size() - 1)]; + if (tokens.empty()) + continue; + const std::string &token = tokens[layer_idx % tokens.size()]; + const unsigned int extruder_id = + MixedFilamentManager::physical_filament_from_token(token, dummy_mf, num_physical); + if (extruder_id != 0) + sequence.emplace_back(extruder_id); + } + } + + return sequence; +} + +std::vector MixedFilamentConfigPanel::decode_gradient_ids(const std::string &s) +{ + return MixedFilamentManager::decode_gradient_component_ids(s, 0); +} + +std::string MixedFilamentConfigPanel::encode_gradient_ids(const std::vector &ids) +{ + return MixedFilamentManager::encode_gradient_component_ids(ids); +} + +std::vector MixedFilamentConfigPanel::decode_manual_pattern_ids(const std::string &pattern, + unsigned int a, + unsigned int b, + size_t num_physical, + size_t wall_loops) +{ + return build_grouped_manual_pattern_preview_sequence(pattern, a, b, num_physical, wall_loops); +} + +std::vector MixedFilamentConfigPanel::decode_gradient_weights(const std::string &s, size_t n) +{ + std::vector w; + if (s.empty() || n == 0) + return w; + + std::string token; + for (const char c : s) { + if (c >= '0' && c <= '9') { + token.push_back(c); + continue; + } + if (!token.empty()) { + w.emplace_back(std::max(0, std::atoi(token.c_str()))); + token.clear(); + } + } + if (!token.empty()) + w.emplace_back(std::max(0, std::atoi(token.c_str()))); + if (w.size() != n) + w.clear(); + return w; +} + +std::vector MixedFilamentConfigPanel::normalize_gradient_weights(const std::vector &w, size_t n) +{ + std::vector out = w; + if (out.size() != n) out.assign(n, n > 0 ? int(100 / n) : 0); + int sum = 0; + for (int &v : out) { v = std::max(0, v); sum += v; } + if (sum <= 0 && n > 0) { out.assign(n, 0); out[0] = 100; return out; } + std::vector rem(n, 0.); + int assigned = 0; + for (size_t i = 0; i < n; ++i) { + const double exact = 100.0 * double(out[i]) / double(sum); + out[i] = int(std::floor(exact)); + rem[i] = exact - double(out[i]); + assigned += out[i]; + } + int missing = std::max(0, 100 - assigned); + while (missing > 0) { + size_t best = 0; + double best_rem = -1.0; + for (size_t i = 0; i < rem.size(); ++i) { + if (rem[i] > best_rem) { best_rem = rem[i]; best = i; } + } + ++out[best]; + rem[best] = 0.0; + --missing; + } + return out; +} + +std::string MixedFilamentConfigPanel::encode_gradient_weights(const std::vector &w) +{ + std::ostringstream out; + for (size_t i = 0; i < w.size(); ++i) { + if (i > 0) + out << '/'; + out << std::max(0, w[i]); + } + return out.str(); +} + +namespace { + +std::pair effective_pair_preview_ratios(int percent_b) +{ + const int mix_b = std::clamp(percent_b, 0, 100); + int ratio_a = 1; + int ratio_b = 0; + + if (mix_b >= 100) { + ratio_a = 0; + ratio_b = 1; + } else if (mix_b > 0) { + const int pct_b = mix_b; + const int pct_a = 100 - pct_b; + const bool b_is_major = pct_b >= pct_a; + const int major_pct = b_is_major ? pct_b : pct_a; + const int minor_pct = b_is_major ? pct_a : pct_b; + const int major_layers = + std::max(1, int(std::lround(double(major_pct) / double(std::max(1, minor_pct))))); + ratio_a = b_is_major ? 1 : major_layers; + ratio_b = b_is_major ? major_layers : 1; + } + + if (ratio_a > 0 && ratio_b > 0) { + const int g = std::gcd(ratio_a, ratio_b); + if (g > 1) { + ratio_a /= g; + ratio_b /= g; + } + } + + return { std::max(0, ratio_a), std::max(0, ratio_b) }; +} + +std::vector build_effective_pair_preview_sequence(unsigned int component_a, + unsigned int component_b, + int percent_b, + bool limit_cycle) +{ + std::vector sequence; + if (component_a == 0 || component_b == 0 || component_a == component_b) + return sequence; + + auto [ratio_a, ratio_b] = effective_pair_preview_ratios(percent_b); + constexpr int k_max_cycle = 24; + if (limit_cycle && ratio_a > 0 && ratio_b > 0 && ratio_a + ratio_b > k_max_cycle) { + const double scale = double(k_max_cycle) / double(ratio_a + ratio_b); + ratio_a = std::max(1, int(std::round(double(ratio_a) * scale))); + ratio_b = std::max(1, int(std::round(double(ratio_b) * scale))); + } + if (ratio_a == 0 && ratio_b == 0) + ratio_a = 1; + + const int cycle = std::max(1, ratio_a + ratio_b); + sequence.reserve(size_t(cycle)); + for (int pos = 0; pos < cycle; ++pos) { + const int b_before = (pos * ratio_b) / cycle; + const int b_after = ((pos + 1) * ratio_b) / cycle; + sequence.emplace_back((b_after > b_before) ? component_b : component_a); + } + return sequence; +} + +std::string format_preview_sequence_percent(int count, int total) +{ + if (count <= 0 || total <= 0) + return ""; + + const double percent = 100.0 * double(count) / double(total); + const double rounded_tenths = std::round(percent * 10.0) / 10.0; + const double nearest_integer = std::round(rounded_tenths); + if (std::abs(rounded_tenths - nearest_integer) < 1e-6) + return wxString::Format("%d%%", int(nearest_integer)).ToStdString(); + return wxString::Format("%.1f%%", rounded_tenths).ToStdString(); +} + +} // namespace + +std::vector MixedFilamentConfigPanel::build_weighted_pair_sequence(unsigned int a, + unsigned int b, + int percent_b, + bool limit_cycle) +{ + return build_effective_pair_preview_sequence(a, b, percent_b, limit_cycle); +} + +static void reduce_weight_counts_to_cycle_limit(std::vector &counts, size_t cycle_limit) +{ + if (counts.empty() || cycle_limit == 0) + return; + + int total = std::accumulate(counts.begin(), counts.end(), 0); + if (total <= 0 || size_t(total) <= cycle_limit) + return; + + std::vector positive_indices; + positive_indices.reserve(counts.size()); + for (size_t i = 0; i < counts.size(); ++i) + if (counts[i] > 0) + positive_indices.emplace_back(i); + + if (positive_indices.empty()) { + counts.assign(counts.size(), 0); + return; + } + + std::vector reduced(counts.size(), 0); + if (cycle_limit < positive_indices.size()) { + std::sort(positive_indices.begin(), positive_indices.end(), [&counts](size_t lhs, size_t rhs) { + if (counts[lhs] != counts[rhs]) + return counts[lhs] > counts[rhs]; + return lhs < rhs; + }); + for (size_t i = 0; i < cycle_limit; ++i) + reduced[positive_indices[i]] = 1; + counts = std::move(reduced); + return; + } + + size_t remaining_slots = cycle_limit; + for (const size_t idx : positive_indices) { + reduced[idx] = 1; + --remaining_slots; + } + + int total_extras = 0; + std::vector extra_counts(counts.size(), 0); + for (const size_t idx : positive_indices) { + extra_counts[idx] = std::max(0, counts[idx] - 1); + total_extras += extra_counts[idx]; + } + if (remaining_slots == 0 || total_extras <= 0) { + counts = std::move(reduced); + return; + } + + std::vector remainders(counts.size(), -1.0); + size_t assigned_slots = 0; + for (const size_t idx : positive_indices) { + if (extra_counts[idx] == 0) + continue; + const double exact = double(remaining_slots) * double(extra_counts[idx]) / double(total_extras); + const int assigned = int(std::floor(exact)); + reduced[idx] += assigned; + assigned_slots += size_t(assigned); + remainders[idx] = exact - double(assigned); + } + + size_t missing_slots = remaining_slots > assigned_slots ? (remaining_slots - assigned_slots) : size_t(0); + while (missing_slots > 0) { + size_t best_idx = size_t(-1); + double best_remainder = -1.0; + int best_extra = -1; + for (const size_t idx : positive_indices) { + if (extra_counts[idx] == 0) + continue; + if (remainders[idx] > best_remainder || + (std::abs(remainders[idx] - best_remainder) <= 1e-9 && extra_counts[idx] > best_extra) || + (std::abs(remainders[idx] - best_remainder) <= 1e-9 && extra_counts[idx] == best_extra && idx < best_idx)) { + best_idx = idx; + best_remainder = remainders[idx]; + best_extra = extra_counts[idx]; + } + } + if (best_idx == size_t(-1)) + break; + ++reduced[best_idx]; + remainders[best_idx] = -1.0; + --missing_slots; + } + + counts = std::move(reduced); +} + +std::vector MixedFilamentConfigPanel::build_weighted_multi_sequence(const std::vector &ids, + const std::vector &weights, + size_t max_cycle_limit) +{ + std::vector seq; + if (ids.empty()) + return seq; + + std::vector filtered_ids; + std::vector counts; + filtered_ids.reserve(ids.size()); + counts.reserve(ids.size()); + + std::vector normalized = normalize_gradient_weights(weights, ids.size()); + for (size_t i = 0; i < ids.size(); ++i) { + const int weight = (i < normalized.size()) ? std::max(0, normalized[i]) : 0; + if (weight <= 0) + continue; + filtered_ids.emplace_back(ids[i]); + counts.emplace_back(weight); + } + if (filtered_ids.empty()) { + filtered_ids = ids; + counts.assign(ids.size(), 1); + } + + int g = 0; + for (const int c : counts) + g = std::gcd(g, std::max(1, c)); + if (g > 1) { + for (int &c : counts) + c = std::max(1, c / g); + } + + constexpr size_t k_max_cycle = 48; + const size_t effective_cycle_limit = + max_cycle_limit > 0 ? std::min(k_max_cycle, std::max(1, max_cycle_limit)) : k_max_cycle; + reduce_weight_counts_to_cycle_limit(counts, effective_cycle_limit); + + std::vector reduced_ids; + std::vector reduced_counts; + reduced_ids.reserve(filtered_ids.size()); + reduced_counts.reserve(counts.size()); + for (size_t i = 0; i < counts.size(); ++i) { + if (counts[i] <= 0) + continue; + reduced_ids.emplace_back(filtered_ids[i]); + reduced_counts.emplace_back(counts[i]); + } + if (reduced_ids.empty()) + return seq; + filtered_ids = std::move(reduced_ids); + counts = std::move(reduced_counts); + + const int total = std::accumulate(counts.begin(), counts.end(), 0); + if (total <= 0) + return seq; + + const size_t cycle = size_t(total); + + seq.reserve(cycle); + std::vector emitted(counts.size(), 0); + for (size_t pos = 0; pos < cycle; ++pos) { + size_t best_idx = 0; + double best_score = -1e9; + for (size_t i = 0; i < counts.size(); ++i) { + const double target = double(pos + 1) * double(counts[i]) / double(total); + const double score = target - double(emitted[i]); + if (score > best_score) { + best_score = score; + best_idx = i; + } + } + ++emitted[best_idx]; + seq.emplace_back(filtered_ids[best_idx]); + } + if (seq.empty()) + seq = filtered_ids; + return seq; +} + + +std::vector MixedFilamentConfigPanel::build_local_z_preview_pass_heights(double nominal_layer_height, + double lower_bound, + double upper_bound, + double preferred_a_height, + double preferred_b_height, + int mix_b_percent, + int max_sublayers_limit) +{ + if (nominal_layer_height <= EPSILON) + return {}; + + const double base_height = nominal_layer_height; + const double lo = std::max(0.01, lower_bound); + const double hi = std::max(lo, upper_bound); + const size_t max_passes_limit = max_sublayers_limit >= 2 ? size_t(max_sublayers_limit) : size_t(0); + + auto fit_pass_heights_to_interval = [](std::vector &passes, double total_height, double local_lo, double local_hi) { + if (passes.empty() || total_height <= EPSILON) + return false; + + const auto within = [local_lo, local_hi](double value) { + return value >= local_lo - 1e-6 && value <= local_hi + 1e-6; + }; + + double sum = 0.0; + for (const double h : passes) + sum += h; + + double delta = total_height - sum; + if (std::abs(delta) > 1e-6) { + if (delta > 0.0) { + for (double &h : passes) { + if (delta <= 1e-6) + break; + const double room = local_hi - h; + if (room <= 1e-6) + continue; + const double take = std::min(room, delta); + h += take; + delta -= take; + } + } else { + for (auto it = passes.rbegin(); it != passes.rend() && delta < -1e-6; ++it) { + const double room = *it - local_lo; + if (room <= 1e-6) + continue; + const double take = std::min(room, -delta); + *it -= take; + delta += take; + } + } + } + + if (std::abs(delta) > 1e-6) + return false; + return std::all_of(passes.begin(), passes.end(), within); + }; + + auto build_uniform = [&fit_pass_heights_to_interval, base_height, lo, hi, max_passes_limit]() { + std::vector out; + size_t min_passes = size_t(std::max(1.0, std::ceil((base_height - EPSILON) / hi))); + size_t max_passes = size_t(std::max(1.0, std::floor((base_height + EPSILON) / lo))); + size_t pass_count = min_passes; + + if (max_passes >= min_passes) { + const double target_step = 0.5 * (lo + hi); + const size_t target_passes = + size_t(std::max(1.0, std::llround(base_height / std::max(target_step, EPSILON)))); + pass_count = std::clamp(target_passes, min_passes, max_passes); + } + + if (max_passes_limit > 0 && pass_count > max_passes_limit) + pass_count = max_passes_limit; + + if (pass_count == 1 && base_height >= 2.0 * lo - EPSILON && max_passes >= 2) + pass_count = 2; + + if (pass_count <= 1) { + out.emplace_back(base_height); + return out; + } + + out.assign(pass_count, base_height / double(pass_count)); + double accumulated = 0.0; + for (size_t i = 0; i + 1 < out.size(); ++i) + accumulated += out[i]; + out.back() = std::max(EPSILON, base_height - accumulated); + if (!fit_pass_heights_to_interval(out, base_height, lo, hi) && max_passes_limit == 0) { + out.assign(pass_count, base_height / double(pass_count)); + accumulated = 0.0; + for (size_t i = 0; i + 1 < out.size(); ++i) + accumulated += out[i]; + out.back() = std::max(EPSILON, base_height - accumulated); + } + return out; + }; + + auto build_alternating = [&build_uniform, &fit_pass_heights_to_interval, base_height, lo, hi, max_passes_limit](double gradient_h_a, double gradient_h_b) { + if (base_height < 2.0 * lo - EPSILON) + return std::vector{ base_height }; + + const double cycle_h = std::max(EPSILON, gradient_h_a + gradient_h_b); + const double ratio_a = std::clamp(gradient_h_a / cycle_h, 0.0, 1.0); + + size_t min_passes = size_t(std::max(2.0, std::ceil((base_height - EPSILON) / hi))); + if ((min_passes % 2) != 0) + ++min_passes; + + size_t max_passes = size_t(std::max(2.0, std::floor((base_height + EPSILON) / lo))); + if ((max_passes % 2) != 0) + --max_passes; + if (max_passes_limit > 0) { + size_t capped_limit = std::max(2, max_passes_limit); + if ((capped_limit % 2) != 0) + --capped_limit; + if (capped_limit >= 2) + max_passes = std::min(max_passes, capped_limit); + } + if (max_passes < 2) + return build_uniform(); + if (min_passes > max_passes) + min_passes = max_passes; + if (min_passes < 2) + min_passes = 2; + if ((min_passes % 2) != 0) + ++min_passes; + if (min_passes > max_passes) + return build_uniform(); + + const double target_step = 0.5 * (lo + hi); + size_t target_passes = + size_t(std::max(2.0, std::llround(base_height / std::max(target_step, EPSILON)))); + if ((target_passes % 2) != 0) { + const size_t round_up = (target_passes < max_passes) ? (target_passes + 1) : max_passes; + const size_t round_down = (target_passes > min_passes) ? (target_passes - 1) : min_passes; + if (round_up > max_passes) + target_passes = round_down; + else if (round_down < min_passes) + target_passes = round_up; + else + target_passes = ((round_up - target_passes) <= (target_passes - round_down)) ? round_up : round_down; + } + target_passes = std::clamp(target_passes, min_passes, max_passes); + + bool has_best = false; + std::vector best_passes; + double best_ratio_error = 0.0; + size_t best_pass_distance = 0; + double best_max_height = 0.0; + size_t best_pass_count = 0; + + for (size_t pass_count = min_passes; pass_count <= max_passes; pass_count += 2) { + const size_t pair_count = pass_count / 2; + if (pair_count == 0) + continue; + const double pair_h = base_height / double(pair_count); + + const double h_a_min = std::max(lo, pair_h - hi); + const double h_a_max = std::min(hi, pair_h - lo); + if (h_a_min > h_a_max + EPSILON) + continue; + + const double h_a = std::clamp(pair_h * ratio_a, h_a_min, h_a_max); + const double h_b = pair_h - h_a; + + std::vector out; + out.reserve(pass_count); + for (size_t pair_idx = 0; pair_idx < pair_count; ++pair_idx) { + out.emplace_back(h_a); + out.emplace_back(h_b); + } + if (!fit_pass_heights_to_interval(out, base_height, lo, hi)) + continue; + + const double ratio_actual = (h_a + h_b > EPSILON) ? (h_a / (h_a + h_b)) : 0.5; + const double ratio_error = std::abs(ratio_actual - ratio_a); + const size_t pass_distance = + (pass_count > target_passes) ? (pass_count - target_passes) : (target_passes - pass_count); + const double max_height = std::max(h_a, h_b); + + const bool better_ratio = !has_best || (ratio_error + 1e-6 < best_ratio_error); + const bool similar_ratio = has_best && std::abs(ratio_error - best_ratio_error) <= 1e-6; + const bool better_distance = similar_ratio && (pass_distance < best_pass_distance); + const bool similar_distance = similar_ratio && (pass_distance == best_pass_distance); + const bool better_max_height = similar_distance && (max_height + 1e-6 < best_max_height); + const bool similar_max_height = similar_distance && std::abs(max_height - best_max_height) <= 1e-6; + const bool better_pass_count = similar_max_height && (pass_count > best_pass_count); + + if (better_ratio || better_distance || better_max_height || better_pass_count) { + has_best = true; + best_passes = std::move(out); + best_ratio_error = ratio_error; + best_pass_distance = pass_distance; + best_max_height = max_height; + best_pass_count = pass_count; + } + } + + return has_best ? best_passes : build_uniform(); + }; + + if (preferred_a_height > EPSILON || preferred_b_height > EPSILON) { + std::vector cadence_unit; + if (preferred_a_height > EPSILON) + cadence_unit.push_back(std::clamp(preferred_a_height, lo, hi)); + if (preferred_b_height > EPSILON) + cadence_unit.push_back(std::clamp(preferred_b_height, lo, hi)); + + if (!cadence_unit.empty()) { + std::vector out; + out.reserve(size_t(std::ceil(base_height / lo)) + 2); + + double z_used = 0.0; + size_t idx = 0; + size_t guard = 0; + while (z_used + cadence_unit[idx] < base_height - EPSILON && guard++ < 100000) { + out.push_back(cadence_unit[idx]); + z_used += cadence_unit[idx]; + idx = (idx + 1) % cadence_unit.size(); + } + + const double remainder = base_height - z_used; + if (remainder > EPSILON) + out.push_back(remainder); + + if (fit_pass_heights_to_interval(out, base_height, lo, hi) && + (max_passes_limit == 0 || out.size() <= max_passes_limit)) + return out; + } + + if (preferred_a_height > EPSILON && preferred_b_height > EPSILON) + return build_alternating(preferred_a_height, preferred_b_height); + return build_uniform(); + } + + const int mix_b = std::clamp(mix_b_percent, 0, 100); + const double pct_b = double(mix_b) / 100.0; + const double pct_a = 1.0 - pct_b; + const double gradient_h_a = lo + pct_a * (hi - lo); + const double gradient_h_b = lo + pct_b * (hi - lo); + return build_alternating(gradient_h_a, gradient_h_b); +} + +int MixedFilamentConfigPanel::effective_local_z_preview_mix_b_percent(const MixedFilament &mf, + const MixedFilamentPreviewSettings &preview_settings) +{ + return Slic3r::mixed_filament_effective_local_z_preview_mix_b_percent(mf, preview_settings); +} + +static double mixed_filament_reference_nozzle_mm(unsigned int component_a, + unsigned int component_b, + const std::vector &nozzle_diameters) +{ + std::vector samples; + samples.reserve(2); + + auto append_if_valid = [&samples, &nozzle_diameters](unsigned int component_id) { + if (component_id >= 1 && component_id <= nozzle_diameters.size()) + samples.emplace_back(std::max(0.05, nozzle_diameters[size_t(component_id - 1)])); + }; + + append_if_valid(component_a); + append_if_valid(component_b); + + if (samples.empty()) + return 0.4; + return std::accumulate(samples.begin(), samples.end(), 0.0) / double(samples.size()); +} + +static double mixed_filament_bias_limit_mm(const MixedFilament &mf, const std::vector &nozzle_diameters) +{ + const double reference_nozzle_mm = mixed_filament_reference_nozzle_mm(mf.component_a, mf.component_b, nozzle_diameters); + return MixedFilamentManager::max_pair_bias_mm(float(reference_nozzle_mm)); +} + +static float mixed_filament_single_surface_offset_value(const MixedFilament &mf, + const std::vector &nozzle_diameters) +{ + const double reference_nozzle_mm = mixed_filament_reference_nozzle_mm(mf.component_a, mf.component_b, nozzle_diameters); + return MixedFilamentManager::bias_ui_value_from_surface_offsets( + mf.component_a_surface_offset, + mf.component_b_surface_offset, + float(reference_nozzle_mm)); +} + +static std::pair mixed_filament_single_surface_offset_pair(const MixedFilament &mf, + float value, + const std::vector &nozzle_diameters) +{ + const double reference_nozzle_mm = mixed_filament_reference_nozzle_mm(mf.component_a, mf.component_b, nozzle_diameters); + return MixedFilamentManager::surface_offset_pair_from_signed_bias(value, float(reference_nozzle_mm)); +} + +static std::string mixed_filament_apparent_pair_summary(const MixedFilament &mf, + const MixedFilamentPreviewSettings &preview_settings, + const std::vector &nozzle_diameters, + bool bias_mode_enabled) +{ + if (!Slic3r::mixed_filament_supports_bias_apparent_color(mf, preview_settings, bias_mode_enabled)) + return {}; + + const int base_b = MixedFilamentConfigPanel::effective_local_z_preview_mix_b_percent(mf, preview_settings); + const int base_a = 100 - base_b; + const auto [apparent_a, apparent_b] = + Slic3r::mixed_filament_apparent_pair_percentages(mf, preview_settings, nozzle_diameters, bias_mode_enabled); + + if (std::abs(mf.component_a_surface_offset - mf.component_b_surface_offset) > 1e-4f && + (apparent_a != base_a || apparent_b != base_b)) { + std::ostringstream ss; + ss << '~' << apparent_a << '/' << apparent_b; + return ss.str(); + } + + std::ostringstream ss; + ss << apparent_a << "%/" << apparent_b << '%'; + return ss.str(); +} + + +static std::vector build_display_weighted_multi_sequence(const std::vector &ids, + const std::vector &weights, + size_t max_cycle_limit = 0) +{ + if (ids.empty()) + return {}; + + std::vector filtered_ids; + std::vector counts; + filtered_ids.reserve(ids.size()); + counts.reserve(ids.size()); + + const std::vector normalized = normalize_color_match_weights(weights, ids.size()); + for (size_t idx = 0; idx < ids.size(); ++idx) { + const int weight = idx < normalized.size() ? std::max(0, normalized[idx]) : 0; + if (weight <= 0) + continue; + filtered_ids.emplace_back(ids[idx]); + counts.emplace_back(weight); + } + if (filtered_ids.empty()) { + filtered_ids = ids; + counts.assign(ids.size(), 1); + } + + int g = 0; + for (const int count : counts) + g = std::gcd(g, std::max(1, count)); + if (g > 1) { + for (int &count : counts) + count = std::max(1, count / g); + } + + constexpr size_t k_max_cycle = 48; + const size_t effective_cycle_limit = + max_cycle_limit > 0 ? std::min(k_max_cycle, std::max(1, max_cycle_limit)) : k_max_cycle; + reduce_weight_counts_to_cycle_limit(counts, effective_cycle_limit); + + std::vector reduced_ids; + std::vector reduced_counts; + reduced_ids.reserve(filtered_ids.size()); + reduced_counts.reserve(counts.size()); + for (size_t idx = 0; idx < counts.size(); ++idx) { + if (counts[idx] <= 0) + continue; + reduced_ids.emplace_back(filtered_ids[idx]); + reduced_counts.emplace_back(counts[idx]); + } + if (reduced_ids.empty()) + return {}; + filtered_ids = std::move(reduced_ids); + counts = std::move(reduced_counts); + + const int total = std::accumulate(counts.begin(), counts.end(), 0); + if (total <= 0) + return std::vector(filtered_ids.begin(), filtered_ids.end()); + + const size_t cycle = size_t(total); + + std::vector sequence; + sequence.reserve(cycle); + std::vector emitted(counts.size(), 0); + for (size_t pos = 0; pos < cycle; ++pos) { + size_t best_idx = 0; + double best_score = -1e9; + for (size_t idx = 0; idx < counts.size(); ++idx) { + const double target = double(pos + 1) * double(counts[idx]) / double(total); + const double score = target - double(emitted[idx]); + if (score > best_score) { + best_score = score; + best_idx = idx; + } + } + ++emitted[best_idx]; + sequence.emplace_back(filtered_ids[best_idx]); + } + if (sequence.empty()) + sequence = filtered_ids; + return sequence; +} + +static std::string blend_display_color_from_sequence(const std::vector &colors, + size_t num_physical, + const std::vector &sequence, + const std::string &fallback) +{ + if (colors.empty() || sequence.empty() || num_physical == 0) + return fallback; + + std::vector counts(num_physical + 1, size_t(0)); + size_t total = 0; + for (const unsigned int id : sequence) { + if (id == 0 || id > num_physical) + continue; + ++counts[id]; + ++total; + } + if (total == 0) + return fallback; + + unsigned int first_id = 0; + for (size_t id = 1; id <= num_physical; ++id) { + if (counts[id] > 0) { + first_id = unsigned(id); + break; + } + } + if (first_id == 0 || first_id > colors.size()) + return fallback; + + std::string blended = colors[first_id - 1]; + int accumulated = int(counts[first_id]); + for (size_t id = size_t(first_id + 1); id <= num_physical; ++id) { + if (counts[id] == 0 || id > colors.size()) + continue; + blended = MixedFilamentManager::blend_color(blended, colors[id - 1], accumulated, int(counts[id])); + accumulated += int(counts[id]); + } + + return blended; +} + + +std::string MixedFilamentConfigPanel::summarize_sequence(const std::vector &seq) +{ + if (seq.empty()) return ""; + std::unordered_map counts; + for (unsigned int id : seq) counts[id]++; + std::vector> sorted; + for (auto &kv : counts) sorted.emplace_back(kv.second, kv.first); + std::sort(sorted.begin(), sorted.end(), std::greater<>()); + std::string out; + for (auto &p : sorted) { + if (!out.empty()) out += "/"; + out += format_preview_sequence_percent(p.first, int(seq.size())); + } + return out; +} + +std::string MixedFilamentConfigPanel::summarize_local_z_breakdown(const MixedFilament &mf, + const std::vector &weights, + const MixedFilamentPreviewSettings &preview_settings) +{ + const std::string normalized_pattern = MixedFilamentManager::normalize_manual_pattern(mf.manual_pattern); + if (!normalized_pattern.empty()) + return "Local-Z breakdown: manual pattern rows do not use pair decomposition."; + + if (mf.distribution_mode == int(MixedFilament::SameLayerPointillisme)) + return "Local-Z breakdown: same-layer mode does not use local-Z pair decomposition."; + + auto pair_name = [](unsigned int a, unsigned int b) { + std::ostringstream ss; + ss << 'F' << a << "+F" << b; + return ss.str(); + }; + auto pair_split = [](unsigned int a, unsigned int b, int weight_a, int weight_b) { + const int safe_a = std::max(0, weight_a); + const int safe_b = std::max(0, weight_b); + const int total = std::max(1, safe_a + safe_b); + const int pct_a = int(std::lround(100.0 * double(safe_a) / double(total))); + const int pct_b = std::max(0, 100 - pct_a); + + std::ostringstream ss; + ss << 'F' << a << "/F" << b << " " << safe_a << ':' << safe_b << " (" << pct_a << '/' << pct_b << ')'; + return ss.str(); + }; + auto cadence_entry = [&pair_name](unsigned int a, unsigned int b, int weight, int total) { + const int pct = int(std::lround(100.0 * double(std::max(0, weight)) / double(std::max(1, total)))); + std::ostringstream ss; + ss << pair_name(a, b) << ' ' << pct << '%'; + return ss.str(); + }; + + const std::vector ids = decode_gradient_ids(mf.gradient_component_ids); + if (preview_settings.local_z_mode && preview_settings.local_z_direct_multicolor && ids.size() >= 3) { + const std::vector normalized = normalize_gradient_weights(weights, ids.size()); + const size_t effective_sublayers = + mf.local_z_max_sublayers >= 2 ? size_t(std::max(2, mf.local_z_max_sublayers)) : ids.size(); + + std::ostringstream ss; + ss << "Local-Z direct multicolor solver: "; + for (size_t idx = 0; idx < ids.size(); ++idx) { + if (idx > 0) + ss << ", "; + const int pct = idx < normalized.size() ? normalized[idx] : 0; + ss << 'F' << ids[idx] << ' ' << pct << '%'; + } + ss << ".\nCarry-over error is distributed directly across all " << ids.size() + << " components instead of collapsing them into pair cadence."; + if (mf.local_z_max_sublayers >= 2) + ss << "\nEffective Local-Z cap: up to " << effective_sublayers << " sublayers per nominal layer."; + return ss.str(); + } + + if (ids.size() >= 4) { + const std::vector normalized = normalize_gradient_weights(weights, ids.size()); + const std::vector pair_tokens = { 1, 2 }; + const std::vector pair_weights = { + std::max(1, normalized[0] + normalized[1]), + std::max(1, normalized[2] + normalized[3]) + }; + const size_t max_pair_layers = + (preview_settings.local_z_mode && mf.local_z_max_sublayers >= 2) ? + std::max(1, size_t(mf.local_z_max_sublayers) / 2) : + size_t(0); + const std::vector uncapped_pair_sequence = build_weighted_multi_sequence(pair_tokens, pair_weights); + const std::vector effective_pair_sequence = + max_pair_layers > 0 ? build_weighted_multi_sequence(pair_tokens, pair_weights, max_pair_layers) : uncapped_pair_sequence; + const std::vector &pair_sequence = effective_pair_sequence.empty() ? uncapped_pair_sequence : effective_pair_sequence; + const int pair_ab_weight = int(std::count(pair_sequence.begin(), pair_sequence.end(), 1u)); + const int pair_cd_weight = int(std::count(pair_sequence.begin(), pair_sequence.end(), 2u)); + const int pair_total = std::max(1, int(pair_sequence.size())); + + std::ostringstream ss; + ss << "Local-Z layer cadence: " + << cadence_entry(ids[0], ids[1], pair_ab_weight, pair_total) + << ", " + << cadence_entry(ids[2], ids[3], pair_cd_weight, pair_total) + << ".\nPair splits: " + << pair_split(ids[0], ids[1], normalized[0], normalized[1]) + << ", " + << pair_split(ids[2], ids[3], normalized[2], normalized[3]) + << '.'; + if (!preview_settings.local_z_mode && mf.local_z_max_sublayers >= 2) + ss << "\nSaved row limit will apply when Local-Z dithering mode is enabled in print settings."; + if (preview_settings.local_z_mode && mf.local_z_max_sublayers >= 2) { + ss << "\nEffective Local-Z stack: " << (pair_total * 2) << " sublayers over " << pair_total << " pair layers"; + if (uncapped_pair_sequence.size() > pair_sequence.size()) + ss << " (uncapped " << (uncapped_pair_sequence.size() * 2) << ')'; + ss << '.'; + } + return ss.str(); + } + + if (ids.size() == 3) { + const std::vector normalized = normalize_gradient_weights(weights, ids.size()); + const std::vector pair_tokens = { 1, 2, 3 }; + const std::vector pair_weights = { + std::max(1, normalized[0] + normalized[1]), + std::max(1, normalized[0] + normalized[2]), + std::max(1, normalized[1] + normalized[2]) + }; + const size_t max_pair_layers = + (preview_settings.local_z_mode && mf.local_z_max_sublayers >= 2) ? + std::max(1, size_t(mf.local_z_max_sublayers) / 2) : + size_t(0); + const std::vector uncapped_pair_sequence = build_weighted_multi_sequence(pair_tokens, pair_weights); + const std::vector effective_pair_sequence = + max_pair_layers > 0 ? build_weighted_multi_sequence(pair_tokens, pair_weights, max_pair_layers) : uncapped_pair_sequence; + const std::vector &pair_sequence = effective_pair_sequence.empty() ? uncapped_pair_sequence : effective_pair_sequence; + const int pair_ab_weight = int(std::count(pair_sequence.begin(), pair_sequence.end(), 1u)); + const int pair_ac_weight = int(std::count(pair_sequence.begin(), pair_sequence.end(), 2u)); + const int pair_bc_weight = int(std::count(pair_sequence.begin(), pair_sequence.end(), 3u)); + const int pair_total = std::max(1, int(pair_sequence.size())); + + std::ostringstream ss; + ss << "Local-Z layer cadence: " + << cadence_entry(ids[0], ids[1], pair_ab_weight, pair_total) + << ", " + << cadence_entry(ids[0], ids[2], pair_ac_weight, pair_total) + << ", " + << cadence_entry(ids[1], ids[2], pair_bc_weight, pair_total) + << ".\nPair splits: " + << pair_split(ids[0], ids[1], normalized[0], normalized[1]) + << ", " + << pair_split(ids[0], ids[2], normalized[0], normalized[2]) + << ", " + << pair_split(ids[1], ids[2], normalized[1], normalized[2]) + << '.'; + if (!preview_settings.local_z_mode && mf.local_z_max_sublayers >= 2) + ss << "\nSaved row limit will apply when Local-Z dithering mode is enabled in print settings."; + if (preview_settings.local_z_mode && mf.local_z_max_sublayers >= 2) { + ss << "\nEffective Local-Z stack: " << (pair_total * 2) << " sublayers over " << pair_total << " pair layers"; + if (uncapped_pair_sequence.size() > pair_sequence.size()) + ss << " (uncapped " << (uncapped_pair_sequence.size() * 2) << ')'; + ss << '.'; + } + return ss.str(); + } + + if (mf.component_a >= 1 && mf.component_b >= 1 && mf.component_a != mf.component_b) { + const int pct_b = std::clamp(mf.mix_b_percent, 0, 100); + const int pct_a = 100 - pct_b; + std::ostringstream ss; + ss << "Local-Z pair split: requested F" << mf.component_a << "/F" << mf.component_b + << ' ' << pct_a << '/' << pct_b; + if (preview_settings.local_z_mode) { + const std::vector effective_passes = build_local_z_preview_pass_heights(preview_settings.nominal_layer_height, + preview_settings.mixed_lower_bound, + preview_settings.mixed_upper_bound, + preview_settings.preferred_a_height, + preview_settings.preferred_b_height, + mf.mix_b_percent, + 0); + if (!effective_passes.empty()) { + const int effective_pct_b = effective_local_z_preview_mix_b_percent(mf, preview_settings); + ss << ", effective " << (100 - effective_pct_b) << '/' << effective_pct_b + << " over " << effective_passes.size() << " sublayers"; + } + } + ss << '.'; + return ss.str(); + } + + return "Local-Z breakdown: unavailable."; +} + +std::string MixedFilamentConfigPanel::blend_from_sequence(const std::vector &colors, const std::vector &seq, const std::string &fallback) +{ + if (colors.empty() || seq.empty()) + return fallback; + + std::vector counts(colors.size() + 1, size_t(0)); + size_t total = 0; + for (const unsigned int id : seq) { + if (id == 0 || id > colors.size()) + continue; + ++counts[id]; + ++total; + } + if (total == 0) + return fallback; + + unsigned int first_id = 0; + for (size_t id = 1; id <= colors.size(); ++id) { + if (counts[id] > 0) { + first_id = unsigned(id); + break; + } + } + if (first_id == 0 || first_id > colors.size()) + return fallback; + + std::string blended = colors[first_id - 1]; + int acc = int(counts[first_id]); + for (size_t id = size_t(first_id + 1); id <= colors.size(); ++id) { + if (counts[id] == 0) + continue; + blended = MixedFilamentManager::blend_color(blended, colors[id - 1], acc, int(counts[id])); + acc += int(counts[id]); + } + + return blended; +} + +MixedFilamentConfigPanel::MixedFilamentConfigPanel(wxWindow *parent, + size_t mixed_id, + const MixedFilament &mf, + size_t num_physical, + const std::vector &physical_colors, + const std::vector &nozzle_diameters, + const std::vector &palette, + const MixedFilamentPreviewSettings &preview_settings, + bool bias_mode_enabled, + OnChangeFn on_change) + : wxPanel(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL | wxBORDER_NONE) + , m_mixed_id(mixed_id) + , m_mf(mf) + , m_num_physical(num_physical) + , m_physical_colors(physical_colors) + , m_nozzle_diameters(nozzle_diameters) + , m_palette(palette) + , m_preview_settings(preview_settings) + , m_bias_mode_enabled(bias_mode_enabled) + , m_selected_weight_state(std::make_shared>()) + , m_on_change(on_change) +{ + if (parent) + SetBackgroundColour(parent->GetBackgroundColour()); + else + SetBackgroundColour(wxGetApp().dark_mode() ? wxColour(52, 52, 56) : wxColour(255, 255, 255)); + build_ui(); +} + +void MixedFilamentConfigPanel::build_ui() +{ + const int gap = FromDIP(6); + const int compact_gap = std::max(FromDIP(2), gap / 3); + const bool is_dark = wxGetApp().dark_mode(); + const wxColour panel_bg = GetBackgroundColour().IsOk() ? GetBackgroundColour() : + (is_dark ? wxColour(52, 52, 56) : wxColour(255, 255, 255)); + SetBackgroundColour(panel_bg); + auto *root = new wxBoxSizer(wxVERTICAL); + + // Filament choices + wxArrayString filament_choices; + for (size_t i = 0; i < m_num_physical; ++i) + filament_choices.Add(wxString::Format("F%d", int(i + 1))); + wxArrayString optional_filament_choices; + optional_filament_choices.Add(_L("None")); + for (size_t i = 0; i < m_num_physical; ++i) + optional_filament_choices.Add(wxString::Format("F%d", int(i + 1))); + + const int component_a = std::clamp(int(m_mf.component_a), 1, int(m_num_physical)); + const int component_b = std::clamp(int(m_mf.component_b), 1, int(m_num_physical)); + + const std::vector initial_gradient_ids = decode_gradient_ids(m_mf.gradient_component_ids); + if (m_mf.distribution_mode == int(MixedFilament::SameLayerPointillisme)) { + m_mf.distribution_mode = initial_gradient_ids.size() >= 3 ? int(MixedFilament::LayerCycle) : int(MixedFilament::Simple); + m_mf.pointillism_all_filaments = false; + } + const int stored_distribution_mode = std::clamp(m_mf.distribution_mode, + int(MixedFilament::LayerCycle), + int(MixedFilament::Simple)); + const int row_distribution_mode = initial_gradient_ids.size() >= 3 ? + (stored_distribution_mode == int(MixedFilament::Simple) ? int(MixedFilament::LayerCycle) : stored_distribution_mode) : + int(MixedFilament::Simple); + m_mf.distribution_mode = row_distribution_mode; + const bool multi_gradient_row = row_distribution_mode != int(MixedFilament::Simple) && initial_gradient_ids.size() >= 3; + const int selection_c = initial_gradient_ids.size() >= 3 ? int(initial_gradient_ids[2]) : 0; + const int selection_d = initial_gradient_ids.size() >= 4 ? int(initial_gradient_ids[3]) : 0; + + // Hidden data controls used as backing state for swatch pickers. + m_choice_a = new wxChoice(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, filament_choices); + m_choice_b = new wxChoice(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, filament_choices); + m_choice_a->SetSelection(component_a - 1); + m_choice_b->SetSelection(component_b - 1); + m_choice_a->Hide(); + m_choice_b->Hide(); + if (multi_gradient_row) { + m_choice_c = new wxChoice(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, optional_filament_choices); + m_choice_c->SetSelection(std::clamp(selection_c, 0, int(m_num_physical))); + m_choice_c->Hide(); + if (initial_gradient_ids.size() >= 4) { + m_choice_d = new wxChoice(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, optional_filament_choices); + m_choice_d->SetSelection(std::clamp(selection_d, 0, int(m_num_physical))); + m_choice_d->Hide(); + } + } + + auto create_component_picker = [this, gap](wxPanel *&container_out, wxPanel *&swatch_out, wxStaticText *&label_out, const wxString &tooltip) { + const int inner_gap = std::max(FromDIP(1), gap / 4); + const bool local_is_dark = wxGetApp().dark_mode(); + const wxColour local_picker_bg = local_is_dark ? wxColour(64, 64, 70) : wxColour(255, 255, 255); + const wxColour local_picker_text = local_is_dark ? wxColour(230, 230, 230) : wxColour(32, 32, 32); + container_out = new wxPanel(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxBORDER_SIMPLE); + container_out->SetBackgroundColour(local_picker_bg); + const wxSize picker_size(FromDIP(38), FromDIP(22)); + container_out->SetMinSize(picker_size); + container_out->SetMaxSize(picker_size); + + auto *container_sizer = new wxBoxSizer(wxHORIZONTAL); + swatch_out = new wxPanel(container_out, wxID_ANY, wxDefaultPosition, wxSize(FromDIP(12), FromDIP(12)), wxBORDER_SIMPLE); + swatch_out->SetMinSize(wxSize(FromDIP(12), FromDIP(12))); + swatch_out->SetToolTip(tooltip); + label_out = new wxStaticText(container_out, wxID_ANY, wxEmptyString); + label_out->SetForegroundColour(local_picker_text); + label_out->SetToolTip(tooltip); + + auto *content_sizer = new wxBoxSizer(wxHORIZONTAL); + content_sizer->Add(swatch_out, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, inner_gap); + content_sizer->Add(label_out, 0, wxALIGN_CENTER_VERTICAL); + container_sizer->AddStretchSpacer(1); + container_sizer->Add(content_sizer, 0, wxALIGN_CENTER_VERTICAL); + container_sizer->AddStretchSpacer(1); + container_out->SetSizer(container_sizer); + container_out->SetToolTip(tooltip); + container_out->SetCursor(wxCursor(wxCURSOR_HAND)); + swatch_out->SetCursor(wxCursor(wxCURSOR_HAND)); + label_out->SetCursor(wxCursor(wxCURSOR_HAND)); + }; + + create_component_picker(m_picker_a_container, m_picker_a_swatch, m_picker_a_label, _L("Click to choose a physical filament color")); + create_component_picker(m_picker_b_container, m_picker_b_swatch, m_picker_b_label, _L("Click to choose a physical filament color")); + if (m_choice_c) + create_component_picker(m_picker_c_container, m_picker_c_swatch, m_picker_c_label, _L("Click to choose a physical filament color")); + if (m_choice_d) + create_component_picker(m_picker_d_container, m_picker_d_swatch, m_picker_d_label, _L("Click to choose a physical filament color")); + update_component_picker_visuals(); + + // Check for pattern mode + const std::string normalized_pattern = MixedFilamentManager::normalize_manual_pattern(m_mf.manual_pattern); + const bool pattern_row_mode = !normalized_pattern.empty(); + + auto *picker_row = new wxBoxSizer(wxHORIZONTAL); + if (!pattern_row_mode) { + auto add_picker = [this, picker_row, gap](wxPanel *container, bool &first_picker) { + if (!container) + return; + if (!first_picker) + picker_row->Add(new wxStaticText(this, wxID_ANY, "+"), 0, wxALIGN_CENTER_VERTICAL | wxLEFT | wxRIGHT, std::max(FromDIP(2), gap / 2)); + picker_row->Add(container, 0, wxALIGN_CENTER_VERTICAL); + first_picker = false; + }; + + bool first_picker = true; + add_picker(m_picker_a_container, first_picker); + add_picker(m_picker_b_container, first_picker); + add_picker(m_picker_c_container, first_picker); + add_picker(m_picker_d_container, first_picker); + } else { + if (m_picker_a_container) m_picker_a_container->Hide(); + if (m_picker_b_container) m_picker_b_container->Hide(); + if (m_picker_c_container) m_picker_c_container->Hide(); + if (m_picker_d_container) m_picker_d_container->Hide(); + } + root->Add(picker_row, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, gap); + + // Pattern controls (if pattern mode) + if (pattern_row_mode) { + auto *pattern_row = new wxBoxSizer(wxHORIZONTAL); + auto *pattern_label = new wxStaticText(this, wxID_ANY, _L("Pattern")); + pattern_label->SetForegroundColour(is_dark ? wxColour(236, 236, 236) : wxColour(20, 20, 20)); + pattern_row->Add(pattern_label, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, gap); + m_pattern_ctrl = new wxTextCtrl(this, wxID_ANY, from_u8(normalized_pattern), wxDefaultPosition, + wxSize(FromDIP(200), -1), wxTE_PROCESS_ENTER); + m_pattern_ctrl->SetToolTip(_L("Manual repeating pattern. Digits 1-9 for filament IDs 1-9. " + "Use [N] for IDs >= 10 (e.g. [12]). " + "Use commas to define per-perimeter groups, e.g. 12,21. " + "Example: 11112222, 12,21, or 1234.")); + pattern_row->Add(m_pattern_ctrl, 1, wxALIGN_CENTER_VERTICAL); + root->Add(pattern_row, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, gap); + + auto *quick_buttons = new wxBoxSizer(wxHORIZONTAL); + for (size_t fid = 0; fid < m_num_physical; ++fid) { + wxButton *btn = new wxButton(this, wxID_ANY, wxString::Format("%d", int(fid + 1)), + wxDefaultPosition, wxSize(FromDIP(24), FromDIP(22)), wxBU_EXACTFIT); + const wxColour chip_color = (fid < m_palette.size()) ? m_palette[fid] : wxColour("#26A69A"); + btn->SetBackgroundColour(chip_color); + btn->SetToolTip(wxString::Format(_L("Append filament %d to pattern"), int(fid + 1))); + quick_buttons->Add(btn, 0, wxRIGHT, FromDIP(4)); + m_pattern_quick_buttons.emplace_back(btn); + } + auto *filaments_label = new wxStaticText(this, wxID_ANY, _L("Filaments")); + filaments_label->SetForegroundColour(is_dark ? wxColour(236, 236, 236) : wxColour(20, 20, 20)); + picker_row->Add(filaments_label, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, std::max(FromDIP(3), gap / 2)); + picker_row->Add(quick_buttons, 0, wxALIGN_CENTER_VERTICAL); + } else { + // Blend selector for non-pattern mode + const bool simple_mode = row_distribution_mode == int(MixedFilament::Simple); + std::vector selected_gradient_ids = simple_mode ? std::vector() : initial_gradient_ids; + if (selected_gradient_ids.size() < 3) selected_gradient_ids.clear(); + if (selected_gradient_ids.empty()) { + selected_gradient_ids.emplace_back(unsigned(component_a)); + if (component_b != component_a) selected_gradient_ids.emplace_back(unsigned(component_b)); + } + const bool multi_gradient_mode = selected_gradient_ids.size() >= 3; + *m_selected_weight_state = normalize_gradient_weights( + decode_gradient_weights(m_mf.gradient_component_weights, selected_gradient_ids.size()), + selected_gradient_ids.size()); + + wxColour color_a = (component_a >= 1 && component_a <= int(m_palette.size())) ? m_palette[component_a - 1] : wxColour("#26A69A"); + wxColour color_b = (component_b >= 1 && component_b <= int(m_palette.size())) ? m_palette[component_b - 1] : wxColour("#26A69A"); + m_blend_selector = new MixedGradientSelector(this, color_a, color_b, std::clamp(m_mf.mix_b_percent, 0, 100)); + m_blend_selector->SetBackgroundColour(panel_bg); + m_blend_label = nullptr; + picker_row->AddSpacer(gap); + picker_row->Add(m_blend_selector, 1, wxEXPAND | wxALIGN_CENTER_VERTICAL | wxLEFT, gap); + + if (m_blend_selector) { + std::vector corner_colors; + corner_colors.reserve(selected_gradient_ids.size()); + for (const unsigned int id : selected_gradient_ids) { + if (id >= 1 && id <= m_palette.size()) + corner_colors.emplace_back(m_palette[id - 1]); + } + if (!simple_mode && corner_colors.size() >= 3) + m_blend_selector->set_multi_preview(corner_colors, *m_selected_weight_state); + } + } + + // Preview + auto *preview_row = new wxBoxSizer(wxHORIZONTAL); + m_mix_preview = new MixedMixPreview(this); + m_mix_preview->SetBackgroundColour(panel_bg); + preview_row->Add(m_mix_preview, 1, wxEXPAND | wxALIGN_CENTER_VERTICAL | wxRIGHT, compact_gap); + + auto *bias_controls = new wxBoxSizer(wxHORIZONTAL); + const float initial_surface_offset_value = mixed_filament_single_surface_offset_value(m_mf, m_nozzle_diameters); + const double initial_bias_limit = mixed_filament_bias_limit_mm(m_mf, m_nozzle_diameters); + const wxString bias_tooltip = + _L("Positive bias recesses the second filament in the pair; negative bias recesses the first filament.\n\n" + "The color chip shows which filament the current value affects.\n\n" + "Grouped wall patterns and Local-Z dithering ignore it."); + + auto *surface_offset_label = new wxStaticText(this, wxID_ANY, _L("Bias")); + surface_offset_label->SetForegroundColour(is_dark ? wxColour(236, 236, 236) : wxColour(20, 20, 20)); + surface_offset_label->SetToolTip(bias_tooltip); + bias_controls->Add(surface_offset_label, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, compact_gap); + + create_component_picker(m_surface_offset_target_container, + m_surface_offset_target_swatch, + m_surface_offset_target_label, + bias_tooltip); + if (m_surface_offset_target_container) + m_surface_offset_target_container->SetCursor(wxCursor(wxCURSOR_ARROW)); + if (m_surface_offset_target_swatch) + m_surface_offset_target_swatch->SetCursor(wxCursor(wxCURSOR_ARROW)); + if (m_surface_offset_target_label) + m_surface_offset_target_label->SetCursor(wxCursor(wxCURSOR_ARROW)); + bias_controls->Add(m_surface_offset_target_container, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, compact_gap); + + m_surface_offset_spin = new wxSpinCtrlDouble(this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize(FromDIP(58), -1), + wxSP_ARROW_KEYS | wxALIGN_RIGHT | wxTE_PROCESS_ENTER, + -initial_bias_limit, initial_bias_limit, + std::clamp(double(initial_surface_offset_value), -initial_bias_limit, initial_bias_limit), 0.001); + m_surface_offset_spin->SetDigits(3); + m_surface_offset_spin->SetToolTip(bias_tooltip); + bias_controls->Add(m_surface_offset_spin, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, compact_gap); + + auto *surface_offset_units = new wxStaticText(this, wxID_ANY, _L("mm")); + surface_offset_units->SetForegroundColour(is_dark ? wxColour(210, 210, 210) : wxColour(72, 72, 72)); + surface_offset_units->SetToolTip(bias_tooltip); + bias_controls->Add(surface_offset_units, 0, wxALIGN_CENTER_VERTICAL); + if (m_bias_mode_enabled) + preview_row->Add(bias_controls, 0, wxALIGN_CENTER_VERTICAL); + else { + surface_offset_label->Hide(); + if (m_surface_offset_target_container) + m_surface_offset_target_container->Hide(); + if (m_surface_offset_spin) + m_surface_offset_spin->Hide(); + surface_offset_units->Hide(); + } + root->Add(preview_row, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, gap); + + if (m_bias_mode_enabled) { + const auto initial_surface_offset_pair = + mixed_filament_single_surface_offset_pair(m_mf, initial_surface_offset_value, m_nozzle_diameters); + m_mf.component_a_surface_offset = initial_surface_offset_pair.first; + m_mf.component_b_surface_offset = initial_surface_offset_pair.second; + } + + const bool initial_component_surface_offsets_supported = m_bias_mode_enabled && + !pattern_row_mode && + row_distribution_mode != int(MixedFilament::SameLayerPointillisme) && + !m_preview_settings.local_z_mode; + if (m_surface_offset_spin) + m_surface_offset_spin->Enable(initial_component_surface_offsets_supported); + + const bool local_z_limit_supported = multi_gradient_row && + row_distribution_mode != int(MixedFilament::SameLayerPointillisme); + if (local_z_limit_supported) { + auto *local_z_limit_row = new wxBoxSizer(wxHORIZONTAL); + m_local_z_limit_checkbox = new wxCheckBox(this, wxID_ANY, _L("Limit Local-Z")); + m_local_z_limit_checkbox->SetValue(m_mf.local_z_max_sublayers >= 2); + m_local_z_limit_checkbox->SetForegroundColour(is_dark ? wxColour(236, 236, 236) : wxColour(20, 20, 20)); + m_local_z_limit_checkbox->SetToolTip( + _L("Store a per-color Local-Z cadence cap. It applies when Local-Z dithering mode is enabled in print settings.")); + local_z_limit_row->Add(m_local_z_limit_checkbox, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, gap); + + auto *local_z_limit_label = new wxStaticText(this, wxID_ANY, _L("Max sublayers")); + local_z_limit_label->SetForegroundColour(is_dark ? wxColour(236, 236, 236) : wxColour(20, 20, 20)); + local_z_limit_row->Add(local_z_limit_label, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, std::max(FromDIP(3), gap / 2)); + + const int initial_local_z_limit = std::max(2, m_mf.local_z_max_sublayers > 0 ? m_mf.local_z_max_sublayers : 6); + m_local_z_limit_spin = new wxSpinCtrl(this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize(FromDIP(72), -1), + wxSP_ARROW_KEYS | wxALIGN_RIGHT | wxTE_PROCESS_ENTER, 2, 999, initial_local_z_limit); + m_local_z_limit_spin->SetToolTip( + _L("Maximum number of Local-Z sublayers this color may use before its cadence repeats.")); + local_z_limit_row->Add(m_local_z_limit_spin, 0, wxALIGN_CENTER_VERTICAL); + + const bool enable_local_z_limit_controls = m_local_z_limit_checkbox->GetValue(); + m_local_z_limit_spin->Enable(enable_local_z_limit_controls); + root->Add(local_z_limit_row, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, gap); + } + + m_breakdown_label = new wxStaticText(this, wxID_ANY, wxEmptyString); + m_breakdown_label->SetForegroundColour(is_dark ? wxColour(210, 210, 210) : wxColour(72, 72, 72)); + m_breakdown_label->Wrap(FromDIP(360)); + root->Add(m_breakdown_label, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, gap); + + // Bind events + auto apply_changes = [this]() { + m_has_changes = true; + + double surface_offset_value = 0.0; + if (m_surface_offset_spin) { + surface_offset_value = m_surface_offset_spin->GetValue(); +#if !defined(wxHAS_NATIVE_SPINCTRLDOUBLE) + if (wxTextCtrl *text = m_surface_offset_spin->GetText()) { + double parsed_value = 0.0; + if (text->GetValue().ToDouble(&parsed_value)) + surface_offset_value = parsed_value; + } +#endif + } + + int a = std::clamp(m_choice_a->GetSelection() + 1, 1, int(m_num_physical)); + int b = std::clamp(m_choice_b->GetSelection() + 1, 1, int(m_num_physical)); + if (a == b && m_num_physical > 1) { + b = (a == int(m_num_physical)) ? 1 : a + 1; + m_choice_b->SetSelection(b - 1); + } + update_component_picker_visuals(); + + if (m_local_z_limit_spin) + m_local_z_limit_spin->Enable(m_local_z_limit_checkbox != nullptr && + m_local_z_limit_checkbox->GetValue()); + + m_mf.component_a = unsigned(a); + m_mf.component_b = unsigned(b); + if (m_bias_mode_enabled) { + const double bias_limit = mixed_filament_bias_limit_mm(m_mf, m_nozzle_diameters); + const float clamped_surface_offset_value = std::clamp(float(surface_offset_value), -float(bias_limit), float(bias_limit)); + const auto surface_offset_pair = + mixed_filament_single_surface_offset_pair(m_mf, clamped_surface_offset_value, m_nozzle_diameters); + m_mf.component_a_surface_offset = surface_offset_pair.first; + m_mf.component_b_surface_offset = surface_offset_pair.second; + if (m_surface_offset_spin) + m_surface_offset_spin->SetValue(clamped_surface_offset_value); + } + m_mf.local_z_max_sublayers = + (m_local_z_limit_checkbox != nullptr && m_local_z_limit_checkbox->GetValue() && m_local_z_limit_spin != nullptr) ? + std::max(2, m_local_z_limit_spin->GetValue()) : + 0; + + bool simple_mode = true; + bool same_layer_mode = false; + int preview_mix_b_percent = std::clamp(m_mf.mix_b_percent, 0, 100); + std::vector preview_sequence; + + if (m_pattern_ctrl) { + m_mf.distribution_mode = int(MixedFilament::Simple); + std::string normalized = MixedFilamentManager::normalize_manual_pattern(into_u8(m_pattern_ctrl->GetValue())); + if (normalized.empty()) normalized = "12"; + if (into_u8(m_pattern_ctrl->GetValue()) != normalized) + m_pattern_ctrl->ChangeValue(from_u8(normalized)); + m_mf.manual_pattern = normalized; + m_mf.mix_b_percent = MixedFilamentManager::mix_percent_from_manual_pattern(normalized); + m_mf.pointillism_all_filaments = false; + m_mf.gradient_component_ids.clear(); + m_mf.gradient_component_weights.clear(); + preview_sequence = decode_manual_pattern_ids(m_mf.manual_pattern, + m_mf.component_a, + m_mf.component_b, + m_num_physical, + m_preview_settings.wall_loops); + } else { + std::vector selected_ids; + selected_ids.reserve(4); + auto add_unique = [&selected_ids](unsigned int id) { + if (id == 0) return; + if (std::find(selected_ids.begin(), selected_ids.end(), id) == selected_ids.end()) + selected_ids.emplace_back(id); + }; + add_unique(unsigned(a)); + add_unique(unsigned(b)); + if (m_choice_c && m_choice_c->GetSelection() > 0) + add_unique(unsigned(m_choice_c->GetSelection())); + if (m_choice_d && m_choice_d->GetSelection() > 0) + add_unique(unsigned(m_choice_d->GetSelection())); + const bool multi_gradient_mode = selected_ids.size() >= 3; + m_mf.distribution_mode = multi_gradient_mode ? int(MixedFilament::LayerCycle) : int(MixedFilament::Simple); + simple_mode = m_mf.distribution_mode == int(MixedFilament::Simple); + m_mf.mix_b_percent = std::clamp(m_blend_selector ? m_blend_selector->value() : 50, 0, 100); + m_mf.manual_pattern.clear(); + m_mf.pointillism_all_filaments = false; + + const wxColour color_a = (a >= 1 && a <= int(m_palette.size())) ? m_palette[size_t(a - 1)] : wxColour("#26A69A"); + const wxColour color_b = (b >= 1 && b <= int(m_palette.size())) ? m_palette[size_t(b - 1)] : wxColour("#26A69A"); + if (m_blend_selector) { + if (!simple_mode && multi_gradient_mode) { + std::vector corner_colors; + corner_colors.reserve(selected_ids.size()); + for (const unsigned int id : selected_ids) { + if (id >= 1 && id <= m_palette.size()) + corner_colors.emplace_back(m_palette[id - 1]); + } + if (corner_colors.size() >= 3) + m_blend_selector->set_multi_preview(corner_colors, *m_selected_weight_state); + else + m_blend_selector->set_colors(color_a, color_b); + } else { + m_blend_selector->set_colors(color_a, color_b); + } + } + + if (multi_gradient_mode) { + const std::vector decoded_weights = + decode_gradient_weights(m_mf.gradient_component_weights, selected_ids.size()); + if (m_selected_weight_state->size() != selected_ids.size()) + *m_selected_weight_state = decoded_weights; + *m_selected_weight_state = normalize_gradient_weights(*m_selected_weight_state, selected_ids.size()); + m_mf.gradient_component_ids = encode_gradient_ids(selected_ids); + m_mf.gradient_component_weights = encode_gradient_weights(*m_selected_weight_state); + preview_sequence = build_weighted_multi_sequence(selected_ids, *m_selected_weight_state); + } else { + m_mf.gradient_component_ids.clear(); + m_mf.gradient_component_weights.clear(); + preview_mix_b_percent = effective_local_z_preview_mix_b_percent(m_mf, m_preview_settings); + preview_sequence = build_weighted_pair_sequence(m_mf.component_a, m_mf.component_b, preview_mix_b_percent, same_layer_mode); + } + } + m_mf.custom = true; + + const std::vector selected_gradient_ids = decode_gradient_ids(m_mf.gradient_component_ids); + const bool component_surface_offsets_supported = m_bias_mode_enabled && + (m_pattern_ctrl == nullptr) && + !same_layer_mode && + !m_preview_settings.local_z_mode; + if (m_surface_offset_spin) + m_surface_offset_spin->Enable(component_surface_offsets_supported); + if (preview_sequence.empty()) + preview_sequence = build_weighted_pair_sequence(m_mf.component_a, m_mf.component_b, preview_mix_b_percent, same_layer_mode); + + if (m_blend_selector && selected_gradient_ids.size() >= 3) { + std::vector corner_colors; + corner_colors.reserve(selected_gradient_ids.size()); + for (const unsigned int id : selected_gradient_ids) { + if (id >= 1 && id <= m_palette.size()) + corner_colors.emplace_back(m_palette[id - 1]); + } + if (corner_colors.size() >= 3) + m_blend_selector->set_multi_preview(corner_colors, *m_selected_weight_state); + } + + if (Slic3r::mixed_filament_supports_bias_apparent_color(m_mf, m_preview_settings, m_bias_mode_enabled) && + m_mf.component_a >= 1 && m_mf.component_b >= 1 && + m_mf.component_a <= m_physical_colors.size() && m_mf.component_b <= m_physical_colors.size()) { + const auto [apparent_pct_a, apparent_pct_b] = + Slic3r::mixed_filament_apparent_pair_percentages(m_mf, m_preview_settings, m_nozzle_diameters, m_bias_mode_enabled); + m_mf.display_color = MixedFilamentManager::blend_color( + m_physical_colors[size_t(m_mf.component_a - 1)], + m_physical_colors[size_t(m_mf.component_b - 1)], + apparent_pct_a, + apparent_pct_b); + } else if (selected_gradient_ids.size() >= 3 || !preview_sequence.empty()) { + m_mf.display_color = blend_from_sequence(m_physical_colors, preview_sequence, "#26A69A"); + if (m_blend_label) { + if (selected_gradient_ids.size() >= 3) { + m_blend_label->SetLabel(wxString::Format(_L("%d-color layer cycle"), int(selected_gradient_ids.size()))); + } else { + m_blend_label->SetLabel(wxString::Format(simple_mode ? _L("Simple %d%%/%d%%") : _L("%d%%/%d%%"), + 100 - preview_mix_b_percent, preview_mix_b_percent)); + } + } + } else { + m_mf.display_color = MixedFilamentManager::blend_color( + m_physical_colors[size_t(a - 1)], m_physical_colors[size_t(b - 1)], + 100 - preview_mix_b_percent, preview_mix_b_percent); + if (m_blend_label) + m_blend_label->SetLabel(wxString::Format(simple_mode ? _L("Simple %d%%/%d%%") : _L("%d%%/%d%%"), + 100 - preview_mix_b_percent, preview_mix_b_percent)); + } + + if (m_mix_preview) { + const std::string bias_summary = + mixed_filament_apparent_pair_summary(m_mf, m_preview_settings, m_nozzle_diameters, m_bias_mode_enabled); + const std::string summary = bias_summary.empty() ? summarize_sequence(preview_sequence) : bias_summary; + std::vector preview_surface_offsets(m_palette.size() + 1, 0.0); + if (m_bias_mode_enabled && m_mf.component_a >= 1 && m_mf.component_a < preview_surface_offsets.size()) + preview_surface_offsets[m_mf.component_a] = double(m_mf.component_a_surface_offset); + if (m_bias_mode_enabled && m_mf.component_b >= 1 && m_mf.component_b < preview_surface_offsets.size()) + preview_surface_offsets[m_mf.component_b] = double(m_mf.component_b_surface_offset); + m_mix_preview->set_data(m_palette, preview_sequence, same_layer_mode, preview_surface_offsets, wxColour(m_mf.display_color), + _L("Preview"), summary.empty() ? wxString() : from_u8(summary)); + } + update_local_z_breakdown(); + if (m_swatch) { + m_swatch->SetBackgroundColour(wxColour(m_mf.display_color)); + m_swatch->Refresh(); + } + if (m_on_change) + m_on_change(m_mf); + }; + + auto make_color_chip_bitmap = [this](const wxColour &color) { + const int chip_size = FromDIP(14); + wxBitmap bmp(chip_size, chip_size); + wxMemoryDC dc(bmp); + dc.SetBackground(wxBrush(wxColour(255, 255, 255))); + dc.Clear(); + dc.SetPen(wxPen(wxColour(120, 120, 120))); + dc.SetBrush(wxBrush(color)); + dc.DrawRectangle(0, 0, chip_size, chip_size); + dc.SelectObject(wxNullBitmap); + return bmp; + }; + + auto bind_component_picker_popup = [this, apply_changes, make_color_chip_bitmap](wxWindow *target, wxChoice *backing_choice) { + if (!target || !backing_choice) + return; + + target->Bind(wxEVT_LEFT_UP, [this, apply_changes, make_color_chip_bitmap, backing_choice](wxMouseEvent &) { + if (m_num_physical == 0) + return; + + const bool allow_none = backing_choice->GetCount() == unsigned(m_num_physical + 1); + wxMenu menu; + std::vector item_ids; + item_ids.reserve(m_num_physical + (allow_none ? 1 : 0)); + if (allow_none) { + const int item_id = wxWindow::NewControlId(); + item_ids.emplace_back(item_id); + menu.Append(item_id, backing_choice->GetSelection() == 0 ? _L("None (Selected)") : _L("None")); + } + for (size_t i = 0; i < m_num_physical; ++i) { + const int item_id = wxWindow::NewControlId(); + item_ids.emplace_back(item_id); + const int selection_index = allow_none ? int(i + 1) : int(i); + const bool is_selected = selection_index == backing_choice->GetSelection(); + const wxString item_label = wxString::Format("F%d%s", int(i + 1), is_selected ? " (Selected)" : ""); + auto *menu_item = new wxMenuItem(&menu, item_id, item_label, wxEmptyString, wxITEM_NORMAL); + const wxColour item_color = (i < m_palette.size()) ? m_palette[i] : wxColour("#26A69A"); + menu_item->SetBitmap(make_color_chip_bitmap(item_color)); + menu.Append(menu_item); + } + + menu.Bind(wxEVT_COMMAND_MENU_SELECTED, [apply_changes, backing_choice, item_ids](wxCommandEvent &evt) { + const auto it = std::find(item_ids.begin(), item_ids.end(), evt.GetId()); + if (it == item_ids.end()) + return; + const int selection = int(std::distance(item_ids.begin(), it)); + backing_choice->SetSelection(selection); + apply_changes(); + }); + PopupMenu(&menu); + }); + }; + + bind_component_picker_popup(m_picker_a_container, m_choice_a); + bind_component_picker_popup(m_picker_a_swatch, m_choice_a); + bind_component_picker_popup(m_picker_a_label, m_choice_a); + bind_component_picker_popup(m_picker_b_container, m_choice_b); + bind_component_picker_popup(m_picker_b_swatch, m_choice_b); + bind_component_picker_popup(m_picker_b_label, m_choice_b); + bind_component_picker_popup(m_picker_c_container, m_choice_c); + bind_component_picker_popup(m_picker_c_swatch, m_choice_c); + bind_component_picker_popup(m_picker_c_label, m_choice_c); + bind_component_picker_popup(m_picker_d_container, m_choice_d); + bind_component_picker_popup(m_picker_d_swatch, m_choice_d); + bind_component_picker_popup(m_picker_d_label, m_choice_d); + + m_choice_a->Bind(wxEVT_CHOICE, [apply_changes](wxCommandEvent&) { apply_changes(); }); + m_choice_b->Bind(wxEVT_CHOICE, [apply_changes](wxCommandEvent&) { apply_changes(); }); + if (m_choice_c) + m_choice_c->Bind(wxEVT_CHOICE, [apply_changes](wxCommandEvent&) { apply_changes(); }); + if (m_choice_d) + m_choice_d->Bind(wxEVT_CHOICE, [apply_changes](wxCommandEvent&) { apply_changes(); }); + if (m_blend_selector) + m_blend_selector->Bind(wxEVT_SLIDER, [apply_changes](wxCommandEvent&) { apply_changes(); }); + if (m_local_z_limit_checkbox) + m_local_z_limit_checkbox->Bind(wxEVT_CHECKBOX, [apply_changes](wxCommandEvent &) { apply_changes(); }); + if (m_local_z_limit_spin) { + m_local_z_limit_spin->Bind(wxEVT_SPINCTRL, [apply_changes](wxCommandEvent &) { apply_changes(); }); + m_local_z_limit_spin->Bind(wxEVT_TEXT_ENTER, [apply_changes](wxCommandEvent &) { apply_changes(); }); + m_local_z_limit_spin->Bind(wxEVT_KILL_FOCUS, [apply_changes](wxFocusEvent &evt) { + apply_changes(); + evt.Skip(); + }); + } + if (m_surface_offset_spin) { + m_surface_offset_spin->Bind(wxEVT_SPINCTRLDOUBLE, [apply_changes](wxSpinDoubleEvent &) { apply_changes(); }); + m_surface_offset_spin->Bind(wxEVT_TEXT_ENTER, [apply_changes](wxCommandEvent &) { apply_changes(); }); + m_surface_offset_spin->Bind(wxEVT_KILL_FOCUS, [apply_changes](wxFocusEvent &evt) { + apply_changes(); + evt.Skip(); + }); + } + + if (m_blend_selector) { + m_blend_selector->Bind(wxEVT_BUTTON, [this, apply_changes](wxCommandEvent&) { + if (!m_blend_selector->is_multi_mode()) return; + std::vector selected_ids; + auto add_unique = [&selected_ids](unsigned int id) { if (id > 0 && std::find(selected_ids.begin(), selected_ids.end(), id) == selected_ids.end()) selected_ids.emplace_back(id); }; + add_unique(unsigned(std::clamp(m_choice_a ? (m_choice_a->GetSelection() + 1) : 0, 1, int(m_num_physical)))); + add_unique(unsigned(std::clamp(m_choice_b ? (m_choice_b->GetSelection() + 1) : 0, 1, int(m_num_physical)))); + if (m_choice_c && m_choice_c->GetSelection() > 0) add_unique(unsigned(m_choice_c->GetSelection())); + if (m_choice_d && m_choice_d->GetSelection() > 0) add_unique(unsigned(m_choice_d->GetSelection())); + if (selected_ids.size() < 3) return; + const std::vector initial_weights = normalize_gradient_weights(*m_selected_weight_state, selected_ids.size()); + MixedGradientWeightsDialog dlg(this, selected_ids, m_palette, initial_weights); + if (dlg.ShowModal() != wxID_OK) return; + *m_selected_weight_state = dlg.normalized_weights(); + apply_changes(); + }); + } + + if (m_pattern_ctrl) { + auto append_pattern_token = [this](int filament_id) { + if (!m_pattern_ctrl || filament_id <= 0) return; + if (filament_id >= 10) + m_pattern_ctrl->AppendText(wxString::Format("[%d]", filament_id)); + else + m_pattern_ctrl->AppendText(wxString::Format("%d", filament_id)); + }; + m_pattern_ctrl->Bind(wxEVT_TEXT_ENTER, [apply_changes](wxCommandEvent&) { apply_changes(); }); + m_pattern_ctrl->Bind(wxEVT_KILL_FOCUS, [apply_changes](wxFocusEvent &evt) { apply_changes(); evt.Skip(); }); + for (size_t fid = 0; fid < m_pattern_quick_buttons.size(); ++fid) { + wxButton *btn = m_pattern_quick_buttons[fid]; + if (btn) { + const int filament_id = int(fid + 1); + btn->Bind(wxEVT_BUTTON, [apply_changes, append_pattern_token, filament_id](wxCommandEvent&) { + append_pattern_token(filament_id); + apply_changes(); + }); + } + } + } + + update_component_picker_visuals(); + SetSizer(root); + Layout(); + SetMinSize(wxSize(-1, GetBestSize().GetHeight())); + update_preview(); +} + +void MixedFilamentConfigPanel::update_component_picker_visuals() +{ + auto update_one = [this](wxChoice *choice, wxPanel *container, wxPanel *swatch, wxStaticText *label) { + if (!choice) + return; + int sel = choice->GetSelection(); + const bool allow_none = choice->GetCount() == unsigned(m_num_physical + 1); + if (sel < 0 && m_num_physical > 0) { + sel = 0; + choice->SetSelection(sel); + } + if (sel < 0) + return; + + if (allow_none && sel == 0) { + const wxColour none_color = wxGetApp().dark_mode() ? wxColour(86, 86, 92) : wxColour(224, 224, 224); + if (swatch) { + swatch->SetBackgroundColour(none_color); + swatch->Refresh(); + } + if (label) + label->SetLabel(_L("None")); + if (container) { + container->Layout(); + container->Refresh(); + } + return; + } + + const int color_idx = allow_none ? sel - 1 : sel; + const wxColour color = (color_idx >= 0 && size_t(color_idx) < m_palette.size()) ? m_palette[size_t(color_idx)] : wxColour("#26A69A"); + if (swatch) { + swatch->SetBackgroundColour(color); + swatch->Refresh(); + } + if (label) + label->SetLabel(wxString::Format("F%d", color_idx + 1)); + if (container) { + container->Layout(); + container->Refresh(); + } + }; + + update_one(m_choice_a, m_picker_a_container, m_picker_a_swatch, m_picker_a_label); + update_one(m_choice_b, m_picker_b_container, m_picker_b_swatch, m_picker_b_label); + update_one(m_choice_c, m_picker_c_container, m_picker_c_swatch, m_picker_c_label); + update_one(m_choice_d, m_picker_d_container, m_picker_d_swatch, m_picker_d_label); + + if (m_surface_offset_target_container || m_surface_offset_target_swatch || m_surface_offset_target_label || m_surface_offset_spin) { + const int a_filament = std::clamp(m_choice_a ? (m_choice_a->GetSelection() + 1) : int(m_mf.component_a), 1, int(std::max(1, m_num_physical))); + const int b_filament = std::clamp(m_choice_b ? (m_choice_b->GetSelection() + 1) : int(m_mf.component_b), 1, int(std::max(1, m_num_physical))); + MixedFilament active_pair = m_mf; + active_pair.component_a = unsigned(a_filament); + active_pair.component_b = unsigned(b_filament); + double signed_bias_value = mixed_filament_single_surface_offset_value(active_pair, m_nozzle_diameters); + + if (m_surface_offset_spin && m_bias_mode_enabled) { + const double bias_limit = mixed_filament_bias_limit_mm(active_pair, m_nozzle_diameters); + m_surface_offset_spin->SetRange(-bias_limit, bias_limit); + signed_bias_value = m_surface_offset_spin->GetValue(); + } + + const int active_filament = signed_bias_value < -EPSILON ? a_filament : b_filament; + const int color_idx = active_filament - 1; + const wxColour color = (color_idx >= 0 && size_t(color_idx) < m_palette.size()) ? m_palette[size_t(color_idx)] : wxColour("#26A69A"); + if (m_surface_offset_target_swatch) { + m_surface_offset_target_swatch->SetBackgroundColour(color); + m_surface_offset_target_swatch->Refresh(); + } + if (m_surface_offset_target_label) + m_surface_offset_target_label->SetLabel(wxString::Format("F%d", active_filament)); + if (m_surface_offset_target_container) { + m_surface_offset_target_container->Layout(); + m_surface_offset_target_container->Refresh(); + } + } +} + +void MixedFilamentConfigPanel::update_preview() +{ + const bool simple_mode = m_mf.distribution_mode == int(MixedFilament::Simple); + const bool same_layer_mode = m_mf.distribution_mode == int(MixedFilament::SameLayerPointillisme); + const std::string normalized_pattern = MixedFilamentManager::normalize_manual_pattern(m_mf.manual_pattern); + const bool pattern_row_mode = !normalized_pattern.empty(); + + std::vector initial_sequence; + if (pattern_row_mode) { + initial_sequence = decode_manual_pattern_ids(normalized_pattern, + m_mf.component_a, + m_mf.component_b, + m_num_physical, + m_preview_settings.wall_loops); + } else { + std::vector initial_gradient_ids = simple_mode ? std::vector() : decode_gradient_ids(m_mf.gradient_component_ids); + if (initial_gradient_ids.size() >= 3) + initial_sequence = build_weighted_multi_sequence(initial_gradient_ids, *m_selected_weight_state); + else + initial_sequence = build_weighted_pair_sequence(m_mf.component_a, + m_mf.component_b, + effective_local_z_preview_mix_b_percent(m_mf, m_preview_settings), + same_layer_mode); + + if (m_blend_selector && initial_gradient_ids.size() >= 3) { + std::vector corner_colors; + corner_colors.reserve(initial_gradient_ids.size()); + for (const unsigned int id : initial_gradient_ids) { + if (id >= 1 && id <= m_palette.size()) + corner_colors.emplace_back(m_palette[id - 1]); + } + if (corner_colors.size() >= 3) + m_blend_selector->set_multi_preview(corner_colors, *m_selected_weight_state); + } + } + + if (m_mix_preview) { + if (Slic3r::mixed_filament_supports_bias_apparent_color(m_mf, m_preview_settings, m_bias_mode_enabled) && + m_mf.component_a >= 1 && m_mf.component_b >= 1 && + m_mf.component_a <= m_physical_colors.size() && m_mf.component_b <= m_physical_colors.size()) { + const auto [apparent_pct_a, apparent_pct_b] = + Slic3r::mixed_filament_apparent_pair_percentages(m_mf, m_preview_settings, m_nozzle_diameters, m_bias_mode_enabled); + m_mf.display_color = MixedFilamentManager::blend_color( + m_physical_colors[size_t(m_mf.component_a - 1)], + m_physical_colors[size_t(m_mf.component_b - 1)], + apparent_pct_a, + apparent_pct_b); + } + + const std::string bias_summary = + mixed_filament_apparent_pair_summary(m_mf, m_preview_settings, m_nozzle_diameters, m_bias_mode_enabled); + const std::string summary = bias_summary.empty() ? summarize_sequence(initial_sequence) : bias_summary; + std::vector preview_surface_offsets(m_palette.size() + 1, 0.0); + if (m_bias_mode_enabled && m_mf.component_a >= 1 && m_mf.component_a < preview_surface_offsets.size()) + preview_surface_offsets[m_mf.component_a] = double(m_mf.component_a_surface_offset); + if (m_bias_mode_enabled && m_mf.component_b >= 1 && m_mf.component_b < preview_surface_offsets.size()) + preview_surface_offsets[m_mf.component_b] = double(m_mf.component_b_surface_offset); + m_mix_preview->set_data(m_palette, initial_sequence, same_layer_mode, preview_surface_offsets, wxColour(m_mf.display_color), + _L("Preview"), summary.empty() ? wxString() : from_u8(summary)); + } + update_local_z_breakdown(); +} + +void MixedFilamentConfigPanel::update_local_z_breakdown() +{ + if (!m_breakdown_label) + return; + + std::vector weights = *m_selected_weight_state; + const std::vector ids = decode_gradient_ids(m_mf.gradient_component_ids); + if (!ids.empty()) + weights = normalize_gradient_weights(weights, ids.size()); + + const std::string breakdown = summarize_local_z_breakdown(m_mf, weights, m_preview_settings); + m_breakdown_label->SetLabel(from_u8(breakdown)); + m_breakdown_label->Wrap(FromDIP(360)); + m_breakdown_label->Show(!breakdown.empty()); + Layout(); +} + +class MixedFilamentDragHandle : public wxPanel +{ +public: + MixedFilamentDragHandle(wxWindow *parent, const wxColour &dot_color, const wxColour &bg_color) + : wxPanel(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxBORDER_NONE) + , m_dot_color(dot_color) + { + const wxSize handle_size = parent ? parent->FromDIP(wxSize(14, 18)) : wxSize(14, 18); + SetMinSize(handle_size); + SetMaxSize(handle_size); + SetInitialSize(handle_size); + SetBackgroundStyle(wxBG_STYLE_PAINT); + SetBackgroundColour(bg_color); + SetCursor(wxCursor(wxCURSOR_SIZING)); + Bind(wxEVT_PAINT, &MixedFilamentDragHandle::on_paint, this); + } + + void set_colors(const wxColour &dot_color, const wxColour &bg_color) + { + m_dot_color = dot_color; + SetBackgroundColour(bg_color); + Refresh(); + } + +private: + void on_paint(wxPaintEvent &) + { + wxAutoBufferedPaintDC dc(this); + dc.SetBackground(wxBrush(GetBackgroundColour())); + dc.Clear(); + dc.SetPen(*wxTRANSPARENT_PEN); + dc.SetBrush(wxBrush(m_dot_color)); + + const wxSize size = GetClientSize(); + const int radius = std::max(1, FromDIP(1)); + const int left_x = std::max(radius, size.x / 2 - FromDIP(2)); + const int right_x = std::min(size.x - radius - 1, size.x / 2 + FromDIP(2)); + const int top_y = std::max(radius + 1, size.y / 2 - FromDIP(5)); + const int gap_y = FromDIP(4); + + for (int row = 0; row < 3; ++row) { + const int y = top_y + row * gap_y; + dc.DrawCircle(wxPoint(left_x, y), radius); + dc.DrawCircle(wxPoint(right_x, y), radius); + } + } + + wxColour m_dot_color; +}; + +static std::vector build_mixed_filament_ui_indices(const std::vector &mixed, + const std::vector &preferred_order) +{ + std::vector ordered_indices; + std::vector used(mixed.size(), false); + + for (const uint64_t stable_id : preferred_order) { + for (size_t idx = 0; idx < mixed.size(); ++idx) { + const MixedFilament &entry = mixed[idx]; + if (used[idx] || entry.deleted || entry.stable_id != stable_id) + continue; + used[idx] = true; + ordered_indices.emplace_back(idx); + break; + } + } + + for (size_t idx = 0; idx < mixed.size(); ++idx) { + if (used[idx] || mixed[idx].deleted) + continue; + ordered_indices.emplace_back(idx); + } + + return ordered_indices; +} + +void Sidebar::init_color_mix_panel(wxWindow* parent, wxSizer* sizer) +{ + // Title bar + p->m_panel_color_mix_title = new StaticBox(parent, wxID_ANY, wxDefaultPosition, + wxDefaultSize, wxTAB_TRAVERSAL | wxBORDER_NONE); + p->m_panel_color_mix_title->SetBackgroundColor(wxColour(248, 248, 248)); + p->m_panel_color_mix_title->SetBackgroundColor2(0xF1F1F1); + p->m_panel_color_mix_title->SetMinSize(wxSize(-1, FromDIP(30))); + p->m_panel_color_mix_title->SetMaxSize(wxSize(-1, FromDIP(30))); + + p->m_color_mix_icon = new ScalableButton(p->m_panel_color_mix_title, wxID_ANY, "color_palette"); + auto* label = new Label(p->m_panel_color_mix_title, _L("Color Mixing"), LB_PROPAGATE_MOUSE_EVENT); + + p->m_btn_del_color_mix = new ScalableButton(p->m_panel_color_mix_title, wxID_ANY, "delete_filament"); + p->m_btn_add_color_mix = new ScalableButton(p->m_panel_color_mix_title, wxID_ANY, "add_filament"); + + auto* h_title = new wxBoxSizer(wxHORIZONTAL); + auto* white_left_c = new wxPanel(p->m_panel_color_mix_title, wxID_ANY, wxDefaultPosition, wxSize(FromDIP(SidebarProps::ContentMargin()), -1)); + white_left_c->SetBackgroundColour(StateColor::darkModeColorFor(*wxWHITE)); + h_title->Add(white_left_c, 0, wxEXPAND | wxTOP | wxBOTTOM, 0); + h_title->Add(p->m_color_mix_icon, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(SidebarProps::TitlebarMargin())); + h_title->AddSpacer(FromDIP(SidebarProps::ElementSpacing())); + h_title->Add(label, 0, wxALIGN_CENTER_VERTICAL); + h_title->AddStretchSpacer(); + h_title->Add(p->m_btn_del_color_mix, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(4)); + h_title->Add(p->m_btn_add_color_mix, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(8)); + auto* white_right_c = new wxPanel(p->m_panel_color_mix_title, wxID_ANY, wxDefaultPosition, wxSize(FromDIP(SidebarProps::ContentMargin()), -1)); + white_right_c->SetBackgroundColour(StateColor::darkModeColorFor(*wxWHITE)); + h_title->Add(white_right_c, 0, wxEXPAND | wxTOP | wxBOTTOM, 0); + p->m_panel_color_mix_title->SetSizer(h_title); + p->m_panel_color_mix_title->Layout(); + + // Scrolled window for content with max height of 3 rows + p->m_scrolled_color_mix = new wxScrolledWindow(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL); + p->m_scrolled_color_mix->SetScrollRate(0, 5); + p->m_scrolled_color_mix->SetBackgroundColour(StateColor::darkModeColorFor(*wxWHITE)); + + // Content panel — match physical filament content panel (sizer set dynamically in update) + p->m_panel_color_mix_content = new wxPanel(p->m_scrolled_color_mix, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL); + p->m_panel_color_mix_content->SetBackgroundColour(StateColor::darkModeColorFor(*wxWHITE)); + + // Add content panel to scrolled window + auto* scrolled_sizer = new wxBoxSizer(wxVERTICAL); + scrolled_sizer->Add(p->m_panel_color_mix_content, 0, wxEXPAND); + p->m_scrolled_color_mix->SetSizer(scrolled_sizer); + + sizer->Add(p->m_panel_color_mix_title, 0, wxEXPAND, 0); + sizer->Add(p->m_scrolled_color_mix, 0, wxEXPAND, 0); + + // Add button: open dialog to create new mix + p->m_btn_add_color_mix->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { + PresetBundle* pb = wxGetApp().preset_bundle; + if (!pb) return; + + auto* co = pb->project_config.option("filament_colour"); + const std::vector colors = co ? co->values : std::vector{}; + if (colors.size() < 2) return; + + MixedFilamentDialog dlg(wxGetApp().mainframe, colors); + if (dlg.ShowModal() != wxID_OK) return; + + auto& mgr = pb->mixed_filaments; + if (mgr.total_filaments(colors.size()) >= MAXIMUM_FILAMENT_NUMBER) return; + const MixedFilament& r = dlg.GetResult(); + mgr.add_custom_filament(r.component_a, r.component_b, r.mix_b_percent, colors); + auto& mfs = mgr.mixed_filaments(); + if (!mfs.empty()) { + mfs.back().distribution_mode = r.distribution_mode; + mfs.back().manual_pattern = r.manual_pattern; + mfs.back().gradient_component_ids = r.gradient_component_ids; + mfs.back().gradient_component_weights = r.gradient_component_weights; + mfs.back().ratio_a = r.ratio_a; + mfs.back().ratio_b = r.ratio_b; + mfs.back().local_z_max_sublayers = r.local_z_max_sublayers; + mfs.back().gradient_enabled = r.gradient_enabled; + mfs.back().gradient_start = r.gradient_start; + mfs.back().gradient_end = r.gradient_end; + mfs.back().display_color = r.display_color; + mfs.back().ui_mode = r.ui_mode; + mfs.back().custom = true; + } + if (auto* opt = pb->project_config.option("mixed_filament_definitions")) + opt->value = mgr.serialize_custom_entries(); + wxGetApp().plater()->post_slice_state_change_update(); + wxGetApp().plater()->on_filaments_change(p->combos_filament.size()); + update_color_mix_panel(); + m_scrolled_sizer->Layout(); + }); + + // Delete button: remove last custom entry + p->m_btn_del_color_mix->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { + PresetBundle* pb = wxGetApp().preset_bundle; + if (!pb) return; + + auto& mgr = pb->mixed_filaments; + auto& mfs = mgr.mixed_filaments(); + for (int i = static_cast(mfs.size()) - 1; i >= 0; --i) { + if (mfs[i].custom && !mfs[i].deleted) { + mfs[i].deleted = true; + break; + } + } + if (auto* opt = pb->project_config.option("mixed_filament_definitions")) + opt->value = mgr.serialize_custom_entries(); + wxGetApp().plater()->post_slice_state_change_update(); + wxGetApp().plater()->on_filaments_change(p->combos_filament.size()); + update_color_mix_panel(); + m_scrolled_sizer->Layout(); + }); + + // Initial visibility: hide if fewer than 2 physical filaments + update_color_mix_panel(); +} + +void Sidebar::update_color_mix_panel() +{ + if (!p->m_panel_color_mix_content) return; + + auto* co = wxGetApp().preset_bundle + ? wxGetApp().preset_bundle->project_config.option("filament_colour") + : nullptr; + const int n_physical = co ? static_cast(co->values.size()) : 0; + const bool show = (n_physical >= 2); + p->m_panel_color_mix_title->Show(show); + p->m_scrolled_color_mix->Show(show); + if (!show) { + return; + } + + wxWindowUpdateLocker no_updates(p->m_panel_color_mix_content); + p->m_panel_color_mix_content->DestroyChildren(); + + auto* preset_bundle = wxGetApp().preset_bundle; + const size_t num_physical = p->combos_filament.size(); + + std::vector physical_colors = co->values; + physical_colors.resize(num_physical, "#26A69A"); + + std::vector nozzle_diameters(num_physical, 0.4); + if (const ConfigOptionFloats* opt = preset_bundle->printers.get_edited_preset().config.option("nozzle_diameter")) { + const size_t opt_count = opt->values.size(); + if (opt_count > 0) + for (size_t i = 0; i < num_physical; ++i) + nozzle_diameters[i] = std::max(0.05, opt->get_at(unsigned(std::min(i, opt_count - 1)))); + } + + float lower_bound = 0.04f, upper_bound = 0.16f; + if (preset_bundle->project_config.has("mixed_filament_height_lower_bound")) + lower_bound = std::max(0.01f, float(preset_bundle->project_config.opt_float("mixed_filament_height_lower_bound"))); + if (preset_bundle->project_config.has("mixed_filament_height_upper_bound")) + upper_bound = std::max(lower_bound, float(preset_bundle->project_config.opt_float("mixed_filament_height_upper_bound"))); + + bool local_z_mode = false; + if (const ConfigOptionBool* opt = preset_bundle->project_config.option("dithering_local_z_mode")) + local_z_mode = opt->value; + + bool component_bias_enabled = false; + if (const ConfigOptionBool* opt = preset_bundle->project_config.option("mixed_filament_component_bias_enabled")) + component_bias_enabled = opt->value; + + const MixedFilamentPreviewSettings preview_settings { + 0.2f, lower_bound, upper_bound, 0.f, 0.f, local_z_mode, false, 1 + }; + const MixedFilamentDisplayContext display_context { + num_physical, physical_colors, nozzle_diameters, preview_settings, component_bias_enabled + }; + preset_bundle->mixed_filaments.set_display_context(display_context); + + auto& mfs = preset_bundle->mixed_filaments.mixed_filaments(); + bool any_visible = false; + for (const MixedFilament& mf : mfs) + if (!mf.deleted) { any_visible = true; break; } + + if (!any_visible) { + p->m_scrolled_color_mix->Hide(); + p->m_btn_del_color_mix->Hide(); + p->m_btn_add_color_mix->SetBitmap_("icon_add_circle"); + m_scrolled_sizer->Layout(); + return; + } + p->m_scrolled_color_mix->Show(); + p->m_btn_del_color_mix->Show(); + p->m_btn_add_color_mix->SetBitmap_("add_filament"); + + // 2-column grid matching the physical filament panel layout + auto* grid_sizer = new wxBoxSizer(wxHORIZONTAL); + auto* col0 = new wxBoxSizer(wxVERTICAL); + auto* col1 = new wxBoxSizer(wxVERTICAL); + grid_sizer->Add(col0, 1, wxEXPAND); + grid_sizer->Add(col1, 1, wxEXPAND); + + int visible_idx = 0; + for (size_t i = 0; i < mfs.size(); ++i) { + MixedFilament& mf = mfs[i]; + if (mf.deleted) continue; + + const std::string synced_color = compute_mixed_filament_display_color(mf, display_context); + if (mf.display_color != synced_color) + mf.display_color = synced_color; + + const int virtual_id = static_cast(num_physical) + visible_idx + 1; + + // Badge button: colored background + virtual filament number with gradient support + auto* badge = new MixedFilamentBadge(p->m_panel_color_mix_content, wxID_ANY, + virtual_id, mf, display_context); + + const std::string normalized_pattern_cm = MixedFilamentManager::normalize_manual_pattern(mf.manual_pattern); + std::vector gradient_ids = MixedFilamentManager::decode_gradient_component_ids(mf.gradient_component_ids, 0); + const bool z_gradient_tile = mf.gradient_enabled && mf.component_a != mf.component_b + && normalized_pattern_cm.empty() && gradient_ids.size() < 3; + wxString lbl; + if (!normalized_pattern_cm.empty()) + lbl = wxString(summarize_cycle_pattern_text(normalized_pattern_cm, mf, int(num_physical))); + else if (gradient_ids.size() >= 3) { + // parse weights + const size_t n = gradient_ids.size(); + std::vector weights; + { + std::string token; + for (const char c : mf.gradient_component_weights) { + if (c >= '0' && c <= '9') { token.push_back(c); continue; } + if (!token.empty()) { weights.emplace_back(std::max(0, std::atoi(token.c_str()))); token.clear(); } + } + if (!token.empty()) weights.emplace_back(std::max(0, std::atoi(token.c_str()))); + if (weights.size() != n) weights.assign(n, int(100 / n)); + } + // normalize to 100 + int sum = 0; for (int v : weights) sum += v; + if (sum <= 0) { weights.assign(n, 0); weights[0] = 100; sum = 100; } + for (size_t k = 0; k < n; ++k) { + const unsigned int fid = gradient_ids[k]; + const int pct = int(std::round(100.0 * weights[k] / sum)); + if (k > 0) lbl += "+"; + lbl += wxString::Format("F%u %d%%", fid, pct); + } + } else if (z_gradient_tile) { + const unsigned from_id = + mf.gradient_start >= mf.gradient_end ? mf.component_a : mf.component_b; + const unsigned to_id = + mf.gradient_start >= mf.gradient_end ? mf.component_b : mf.component_a; + lbl = wxString::Format("F%u->F%u", from_id, to_id); + } else { + const int pct_b = std::clamp(mf.mix_b_percent, 0, 100); + const int pct_a = 100 - pct_b; + lbl = wxString::Format("F%u %d%%+F%u %d%%", mf.component_a, pct_a, mf.component_b, pct_b); + if (mf.distribution_mode != int(MixedFilament::Simple)) + for (unsigned int fid : gradient_ids) + lbl += wxString::Format("+F%u", fid); + } + + bool has_error = !is_filament_compatible(mf); + + // Create a panel with border for the text + auto* name_panel = new wxPanel(p->m_panel_color_mix_content, wxID_ANY); + name_panel->SetBackgroundColour(StateColor::darkModeColorFor(*wxWHITE)); + + auto* name_sizer = new wxBoxSizer(wxHORIZONTAL); + + // Add error icon if there's an error + if (has_error) { + name_sizer->AddSpacer(FromDIP(8)); + ScalableBitmap error_bmp(name_panel, "error_icon_red_exclamation", 14); + auto* error_icon = new wxStaticBitmap(name_panel, wxID_ANY, error_bmp.bmp()); + name_sizer->Add(error_icon, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(4)); + } + + auto* name_btn = new wxStaticText(name_panel, wxID_ANY, lbl, wxDefaultPosition, wxDefaultSize, 0); + name_btn->SetBackgroundColour(StateColor::darkModeColorFor(*wxWHITE)); + name_btn->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#262E30"))); + name_btn->SetCursor(wxCursor(wxCURSOR_HAND)); + name_btn->SetMinSize(wxSize(0, -1)); // allow sizer to shrink below text width + + int name_flags = wxEXPAND | (has_error ? (wxTOP | wxBOTTOM | wxRIGHT) : wxALL); + name_sizer->Add(name_btn, 1, name_flags, FromDIP(8)); + name_panel->SetSizer(name_sizer); + name_panel->SetMinSize(wxSize(name_panel->FromDIP(100), name_panel->FromDIP(30))); + name_panel->SetMaxSize(wxSize(-1, name_panel->FromDIP(30))); + + // Use wxControl::Ellipsize on resize to match combo truncation behavior + name_panel->Bind(wxEVT_SIZE, [lbl, name_btn, name_panel](wxSizeEvent& evt) { + name_panel->Layout(); // force sizer layout so name_btn has its current size + int avail = name_btn->GetSize().x; + if (avail > 0) { + wxClientDC dc(name_btn); + dc.SetFont(name_btn->GetFont()); + wxString ellipsized = wxControl::Ellipsize(lbl, dc, wxELLIPSIZE_END, avail); + name_btn->SetLabel(ellipsized); + } + evt.Skip(); + }); + + // Add border to the panel + name_panel->Bind(wxEVT_PAINT, [](wxPaintEvent& evt) { + wxPanel* panel = dynamic_cast(evt.GetEventObject()); + if (!panel) return; + + wxPaintDC dc(panel); + wxRect rect = panel->GetClientRect(); + + // Draw border matching combo (#dbdbdb, 1px) + dc.SetPen(wxPen(StateColor::darkModeColorFor(wxColour(0xdb, 0xdb, 0xdb)), 1)); + dc.SetBrush(*wxTRANSPARENT_BRUSH); + dc.DrawRectangle(rect); + }); + + name_btn->SetToolTip(lbl); + name_btn->Bind(wxEVT_LEFT_DOWN, [this, i](wxMouseEvent&) { + auto* co = wxGetApp().preset_bundle->project_config.option("filament_colour"); + const std::vector colors = co ? co->values : std::vector{}; + if (colors.size() < 2) return; + auto& mgr = wxGetApp().preset_bundle->mixed_filaments; + auto& mfs2 = mgr.mixed_filaments(); + if (i >= mfs2.size()) return; + MixedFilamentDialog dlg(wxGetApp().mainframe, colors, mfs2[i]); + if (dlg.ShowModal() != wxID_OK) return; + const MixedFilament& r = dlg.GetResult(); + mfs2[i].component_a = r.component_a; + mfs2[i].component_b = r.component_b; + mfs2[i].mix_b_percent = r.mix_b_percent; + mfs2[i].distribution_mode = r.distribution_mode; + mfs2[i].manual_pattern = r.manual_pattern; + mfs2[i].gradient_component_ids = r.gradient_component_ids; + mfs2[i].gradient_component_weights = r.gradient_component_weights; + mfs2[i].ratio_a = r.ratio_a; + mfs2[i].ratio_b = r.ratio_b; + mfs2[i].local_z_max_sublayers = r.local_z_max_sublayers; + mfs2[i].gradient_enabled = r.gradient_enabled; + mfs2[i].gradient_start = r.gradient_start; + mfs2[i].gradient_end = r.gradient_end; + mfs2[i].display_color = r.display_color; + mfs2[i].ui_mode = r.ui_mode; + mfs2[i].custom = true; + if (auto* opt = wxGetApp().preset_bundle->project_config.option("mixed_filament_definitions")) + opt->value = mgr.serialize_custom_entries(); + wxGetApp().plater()->post_slice_state_change_update(); + wxGetApp().plater()->on_filaments_change(p->combos_filament.size()); + wxWeakRef weak_this(this); + wxTheApp->CallAfter([weak_this]() { + Sidebar* sidebar = weak_this.get(); + if (sidebar) { + sidebar->update_color_mix_panel(); + sidebar->m_scrolled_sizer->Layout(); + } + }); + }); + + auto* menu_btn = new ScalableButton(p->m_panel_color_mix_content, wxID_ANY, "menu_filament"); + menu_btn->SetToolTip(_L("Options")); + menu_btn->Bind(wxEVT_BUTTON, [this, i, visible_idx, num_physical, menu_btn](wxCommandEvent&) { + wxMenu menu; + const int edit_id = wxWindow::NewControlId(); + const int del_id = wxWindow::NewControlId(); + const int merge_to_id = wxWindow::NewControlId(); + + menu.Append(edit_id, _L("Edit")); + menu.Bind(wxEVT_MENU, [this, i](wxCommandEvent&) { + auto* co = wxGetApp().preset_bundle->project_config.option("filament_colour"); + const std::vector colors = co ? co->values : std::vector{}; + if (colors.size() < 2) return; + auto& mgr = wxGetApp().preset_bundle->mixed_filaments; + auto& mfs2 = mgr.mixed_filaments(); + if (i >= mfs2.size()) return; + MixedFilamentDialog dlg(wxGetApp().mainframe, colors, mfs2[i]); + if (dlg.ShowModal() != wxID_OK) return; + const MixedFilament& r = dlg.GetResult(); + mfs2[i].component_a = r.component_a; + mfs2[i].component_b = r.component_b; + mfs2[i].mix_b_percent = r.mix_b_percent; + mfs2[i].distribution_mode = r.distribution_mode; + mfs2[i].manual_pattern = r.manual_pattern; + mfs2[i].gradient_component_ids = r.gradient_component_ids; + mfs2[i].gradient_component_weights = r.gradient_component_weights; + mfs2[i].ratio_a = r.ratio_a; + mfs2[i].ratio_b = r.ratio_b; + mfs2[i].local_z_max_sublayers = r.local_z_max_sublayers; + mfs2[i].gradient_enabled = r.gradient_enabled; + mfs2[i].gradient_start = r.gradient_start; + mfs2[i].gradient_end = r.gradient_end; + mfs2[i].ui_mode = r.ui_mode; + mfs2[i].custom = true; + if (auto* opt = wxGetApp().preset_bundle->project_config.option("mixed_filament_definitions")) + opt->value = mgr.serialize_custom_entries(); + wxGetApp().plater()->post_slice_state_change_update(); + wxGetApp().plater()->on_filaments_change(p->combos_filament.size()); + wxWeakRef weak_this(this); + wxTheApp->CallAfter([weak_this]() { + Sidebar* sidebar = weak_this.get(); + if (sidebar) { + sidebar->update_color_mix_panel(); + sidebar->m_scrolled_sizer->Layout(); + } + }); + }, edit_id); + + // Add "Merge with" submenu - allows merging to any other filament (physical or mixed) + // Build list of all available target filaments + wxMenu* merge_submenu = new wxMenu(); + + // Get physical filament icons + std::vector icons = get_extruder_color_icons(true); + + // Add physical filaments as targets + for (size_t phys_idx = 0; phys_idx < num_physical; ++phys_idx) { + const int target_id = wxWindow::NewControlId(); + auto preset = wxGetApp().preset_bundle->filaments.find_preset(wxGetApp().preset_bundle->filament_presets[phys_idx]); + wxString target_label = preset ? from_u8(preset->label(false)) : wxString::Format(_L("Filament %d"), phys_idx + 1); + + // Use icon if available + wxMenuItem* item = nullptr; + if (phys_idx < icons.size() && icons[phys_idx]) { + item = new wxMenuItem(merge_submenu, target_id, target_label); + item->SetBitmap(*icons[phys_idx]); + merge_submenu->Append(item); + } else { + merge_submenu->Append(target_id, target_label); + } + + merge_submenu->Bind(wxEVT_MENU, [this, visible_idx, phys_idx, num_physical](wxCommandEvent&) { + // Source: mixed filament with visible_idx (0-based in visible list) + // Target: physical filament phys_idx (0-based) + // Mixed filament virtual ID = num_physical + visible_idx + 1 (1-based), convert to 0-based + size_t source_virtual_id = num_physical + visible_idx; + change_filament(source_virtual_id, phys_idx); + }, target_id); + } + + // Add other mixed filaments as targets + auto& mgr = wxGetApp().preset_bundle->mixed_filaments; + auto& mfs_for_menu = mgr.mixed_filaments(); + const size_t total_mixed = mfs_for_menu.size(); + + // Get icon dimensions for mixed filaments + const double em = Slic3r::GUI::wxGetApp().em_unit(); + const int icon_width = lround(2 * em); + const int icon_height = lround(2 * em); + + size_t target_visible_idx = 0; + for (size_t j = 0; j < total_mixed; ++j) { + if (mfs_for_menu[j].deleted) continue; + + // Skip self (compare by visible index) + if (target_visible_idx == visible_idx) { + target_visible_idx++; + continue; + } + + const int target_virtual_id = static_cast(num_physical) + target_visible_idx + 1; + const wxString target_label = wxString::Format(_L("Mixed Filament %d"), target_virtual_id); + const int target_id = wxWindow::NewControlId(); + + // Create colored bitmap for mixed filament — gradient filaments get a gradient icon + MixedFilamentDisplayContext menu_ctx; + { + auto* co2 = wxGetApp().preset_bundle->project_config.option("filament_colour"); + menu_ctx.physical_colors = co2 ? co2->values : std::vector{}; + menu_ctx.num_physical = num_physical; + } + wxBitmap* mixed_bmp = create_mixed_filament_menu_bitmap( + mfs_for_menu[j], menu_ctx, icon_width, icon_height, + wxString::Format("%d", target_virtual_id)); + + wxMenuItem* item = new wxMenuItem(merge_submenu, target_id, target_label); + item->SetBitmap(*mixed_bmp); + merge_submenu->Append(item); + + merge_submenu->Bind(wxEVT_MENU, [this, visible_idx, target_visible_idx, num_physical](wxCommandEvent&) { + // Source: mixed filament with visible_idx (0-based in visible list) + // Target: mixed filament with target_visible_idx (0-based in visible list) + // Virtual ID (0-based) = num_physical + visible_idx + size_t source_virtual_id = num_physical + visible_idx; + size_t target_virtual_id = num_physical + target_visible_idx; + change_filament(source_virtual_id, target_virtual_id); + }, target_id); + + target_visible_idx++; + } + + menu.AppendSubMenu(merge_submenu, _L("Merge with")); + + menu.Append(del_id, _L("Delete")); + menu.Bind(wxEVT_MENU, [this, i, num_physical](wxCommandEvent&) { + auto& mgr2 = wxGetApp().preset_bundle->mixed_filaments; + auto& mfs2 = mgr2.mixed_filaments(); + const std::vector old_mixed = mfs2; + if (i < mfs2.size()) { mfs2[i].deleted = true; mfs2[i].enabled = false; } + if (auto* opt = wxGetApp().preset_bundle->project_config.option("mixed_filament_definitions")) + opt->value = mgr2.serialize_custom_entries(); + wxGetApp().preset_bundle->update_mixed_filament_id_remap(old_mixed, num_physical, num_physical, i); + wxGetApp().plater()->post_slice_state_change_update(); + wxGetApp().plater()->on_filaments_change(num_physical); + wxWeakRef weak_this(this); + wxTheApp->CallAfter([weak_this]() { + Sidebar* sidebar = weak_this.get(); + if (sidebar) { + sidebar->update_color_mix_panel(); + sidebar->m_scrolled_sizer->Layout(); + } + }); + }, del_id); + + wxPoint pt{0, menu_btn->GetSize().GetHeight()}; + pt = menu_btn->ClientToScreen(pt); + pt = wxGetApp().mainframe->ScreenToClient(pt); + wxGetApp().mainframe->PopupMenu(&menu, pt); + }); + + auto* cell = new wxBoxSizer(wxHORIZONTAL); + // Match physical filament asymmetric layout: left column gets left spacer, right doesn't + if (visible_idx % 2 == 0) + cell->AddSpacer(FromDIP(SidebarProps::ContentMargin())); + cell->Add(badge, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(SidebarProps::ElementSpacing()) - FromDIP(2)); + cell->Add(name_panel, 1, wxEXPAND | wxALL, FromDIP(2)); + cell->Add(menu_btn, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(SidebarProps::ElementSpacing()) - FromDIP(2)); + cell->AddSpacer(FromDIP(SidebarProps::ContentMargin())); + + (visible_idx % 2 == 0 ? col0 : col1)->Add(cell, 0, wxEXPAND | wxBOTTOM, FromDIP(4)); + + ++visible_idx; + } + + // If odd count, pad right column so left column doesn't stretch + if (visible_idx % 2 == 1) + col1->AddStretchSpacer(1); + + // Wrap grid in vertical sizer for padding, then set as panel sizer (matches physical structure) + auto* wrapper = new wxBoxSizer(wxVERTICAL); + wrapper->Add(grid_sizer, 0, wxEXPAND | wxTOP | wxBOTTOM, FromDIP(8)); + p->m_panel_color_mix_content->SetSizer(wrapper); + p->m_panel_color_mix_content->Layout(); + + // Dynamic height: grow with rows up to 3, only cap when > 3 rows + const wxSize content_best = p->m_panel_color_mix_content->GetBestSize(); + const int row_count = (visible_idx + 1) / 2; // rows (2 columns) + const int desired_h = row_count > 3 + ? (content_best.GetHeight() / row_count) * 3 + : content_best.GetHeight(); + p->m_scrolled_color_mix->SetMinSize({-1, desired_h}); + p->m_scrolled_color_mix->SetMaxSize({-1, desired_h}); + + m_scrolled_sizer->Layout(); + wxWeakRef weak_this(this); + wxTheApp->CallAfter([weak_this]() { + Sidebar* sidebar = weak_this.get(); + if (sidebar && sidebar->p && sidebar->p->m_scrolled_color_mix) { + int vh = sidebar->p->m_scrolled_color_mix->GetVirtualSize().y; + int ch = sidebar->p->m_scrolled_color_mix->GetClientSize().y; + sidebar->p->m_scrolled_color_mix->Scroll(0, std::max(0, vh - ch)); + } + }); + p->m_panel_color_mix_content->Refresh(); + + // Disable add buttons when combined filament limit reached + if (preset_bundle) { + const bool can_add = preset_bundle->mixed_filaments.total_filaments(num_physical) < MAXIMUM_FILAMENT_NUMBER; + if (p->m_bpButton_add_filament) + p->m_bpButton_add_filament->Enable(can_add); + if (p->m_btn_add_color_mix) + p->m_btn_add_color_mix->Enable(can_add); + } +} + +void Sidebar::update_mixed_filament_panel(bool sync_manager) +{ + // Check for new collapsible structure + if (!p->m_panel_mixed_filaments_title || !p->m_panel_mixed_filaments_content) + return; + + wxWindowUpdateLocker noUpdates_sidebar(this); + wxWindowUpdateLocker noUpdates_mixed_panel(p->m_panel_mixed_filaments_content); + + auto refresh_model_canvas_colors = []() { + Plater *plater = wxGetApp().plater(); + if (plater == nullptr) + return; + + auto refresh_canvas = [](GLCanvas3D *canvas) { + if (canvas == nullptr || !canvas->is_initialized()) + return; + canvas->update_volumes_colors_by_extruder(); + canvas->render(); + }; + + refresh_canvas(plater->get_view3D_canvas3D()); + refresh_canvas(plater->get_assmeble_canvas3D()); + }; + + int prev_rows_view_y = 0; + for (wxWindow *child : p->m_panel_mixed_filaments_content->GetChildren()) { + if (auto *scrolled = dynamic_cast(child)) { + int tmp_x = 0; + scrolled->GetViewStart(&tmp_x, &prev_rows_view_y); + break; + } + } + + auto *preset_bundle = wxGetApp().preset_bundle; + if (!preset_bundle) + return; + DynamicPrintConfig *print_cfg = &preset_bundle->prints.get_edited_preset().config; + + const size_t num_physical = p->combos_filament.size(); + ConfigOptionStrings *color_opt = preset_bundle->project_config.option("filament_colour"); + std::vector physical_colors = color_opt ? color_opt->values : std::vector(); + physical_colors.resize(num_physical, "#26A69A"); + std::vector nozzle_diameters(num_physical, 0.4); + if (const ConfigOptionFloats *opt = preset_bundle->printers.get_edited_preset().config.option("nozzle_diameter")) { + const size_t opt_count = opt->values.size(); + if (opt_count > 0) { + for (size_t i = 0; i < num_physical; ++i) + nozzle_diameters[i] = std::max(0.05, opt->get_at(unsigned(std::min(i, opt_count - 1)))); + } + } + + auto get_mixed_bool = [preset_bundle, print_cfg](const std::string &key, bool fallback) { + if (const ConfigOptionBool *opt = preset_bundle->project_config.option(key)) + return opt->value; + if (const ConfigOptionInt *opt = preset_bundle->project_config.option(key)) + return opt->value != 0; + if (print_cfg) { + if (const ConfigOptionBool *opt = print_cfg->option(key)) + return opt->value; + if (const ConfigOptionInt *opt = print_cfg->option(key)) + return opt->value != 0; + } + return fallback; + }; + auto get_mixed_mode = [preset_bundle, print_cfg](bool fallback) { + if (const ConfigOptionBool *opt = preset_bundle->project_config.option("mixed_filament_gradient_mode")) + return opt->value; + if (const ConfigOptionInt *opt = preset_bundle->project_config.option("mixed_filament_gradient_mode")) + return opt->value != 0; + if (print_cfg) { + if (const ConfigOptionBool *opt = print_cfg->option("mixed_filament_gradient_mode")) + return opt->value; + if (const ConfigOptionInt *opt = print_cfg->option("mixed_filament_gradient_mode")) + return opt->value != 0; + } + return fallback; + }; + auto get_mixed_float = [preset_bundle, print_cfg](const std::string &key, float fallback) { + if (preset_bundle->project_config.has(key)) + return float(preset_bundle->project_config.opt_float(key)); + if (print_cfg && print_cfg->has(key)) + return float(print_cfg->opt_float(key)); + return fallback; + }; + auto get_mixed_string = [preset_bundle, print_cfg](const std::string &key, const std::string &fallback = std::string()) { + std::string project_value; + if (preset_bundle->project_config.has(key)) + project_value = preset_bundle->project_config.opt_string(key); + if (!project_value.empty()) + return project_value; + if (print_cfg && print_cfg->has(key)) { + const std::string print_value = print_cfg->opt_string(key); + if (!print_value.empty()) + return print_value; + } + return project_value.empty() ? fallback : project_value; + }; + auto set_mixed_float = [preset_bundle, print_cfg](const std::string &key, float value) { + if (print_cfg) { + if (ConfigOptionFloat *opt = print_cfg->option(key)) + opt->value = value; + } + if (ConfigOptionFloat *opt = preset_bundle->project_config.option(key)) + opt->value = value; + else + preset_bundle->project_config.set_key_value(key, new ConfigOptionFloat(value)); + }; + auto set_mixed_string = [preset_bundle, print_cfg](const std::string &key, const std::string &value) { + if (print_cfg) { + if (ConfigOptionString *opt = print_cfg->option(key)) + opt->value = value; + } + if (ConfigOptionString *opt = preset_bundle->project_config.option(key)) + opt->value = value; + else + preset_bundle->project_config.set_key_value(key, new ConfigOptionString(value)); + }; + auto set_mixed_bool = [preset_bundle, print_cfg](const std::string &key, bool value) { + if (print_cfg) { + if (ConfigOptionBool *opt = print_cfg->option(key)) + opt->value = value; + else if (ConfigOptionInt *opt = print_cfg->option(key)) + opt->value = value ? 1 : 0; + } + if (ConfigOptionBool *opt = preset_bundle->project_config.option(key)) + opt->value = value; + else if (ConfigOptionInt *opt = preset_bundle->project_config.option(key)) + opt->value = value ? 1 : 0; + else + preset_bundle->project_config.set_key_value(key, new ConfigOptionBool(value)); + }; + auto set_mixed_mode = [preset_bundle, print_cfg](bool enabled) { + if (print_cfg) { + if (ConfigOptionBool *opt = print_cfg->option("mixed_filament_gradient_mode")) + opt->value = enabled; + else if (ConfigOptionInt *opt = print_cfg->option("mixed_filament_gradient_mode")) + opt->value = enabled ? 1 : 0; + } + if (ConfigOptionBool *opt = preset_bundle->project_config.option("mixed_filament_gradient_mode")) + opt->value = enabled; + else if (ConfigOptionInt *opt = preset_bundle->project_config.option("mixed_filament_gradient_mode")) + opt->value = enabled ? 1 : 0; + else + preset_bundle->project_config.set_key_value("mixed_filament_gradient_mode", new ConfigOptionBool(enabled)); + }; + auto notify_mixed_change = [print_cfg]() { + if (!print_cfg) + return; + if (auto *print_tab = wxGetApp().get_tab(Preset::TYPE_PRINT)) + print_tab->update_dirty(); + if (wxGetApp().mainframe) + wxGetApp().mainframe->on_config_changed(print_cfg); + }; + auto decode_gradient_ids = [](const std::string &encoded) { + return MixedFilamentManager::decode_gradient_component_ids(encoded, 0); + }; + auto encode_gradient_ids = [](const std::vector &ids) { + return MixedFilamentManager::encode_gradient_component_ids(ids); + }; + auto decode_gradient_weights = [](const std::string &encoded, size_t expected_count) { + std::vector out; + if (encoded.empty() || expected_count == 0) + return out; + std::string token; + for (const char c : encoded) { + if (c >= '0' && c <= '9') { + token.push_back(c); + continue; + } + if (!token.empty()) { + out.emplace_back(std::max(0, std::atoi(token.c_str()))); + token.clear(); + } + } + if (!token.empty()) + out.emplace_back(std::max(0, std::atoi(token.c_str()))); + if (out.size() != expected_count) + out.clear(); + return out; + }; + auto normalize_gradient_weights = [](const std::vector &weights, size_t n) { + std::vector out = weights; + if (out.size() != n) + out.assign(n, (n > 0) ? int(100 / n) : 0); + int sum = 0; + for (int &v : out) { + v = std::max(0, v); + sum += v; + } + if (sum <= 0 && n > 0) { + out.assign(n, 0); + out[0] = 100; + return out; + } + std::vector rem(n, 0.); + int assigned = 0; + for (size_t i = 0; i < n; ++i) { + const double exact = 100.0 * double(out[i]) / double(sum); + out[i] = int(std::floor(exact)); + rem[i] = exact - double(out[i]); + assigned += out[i]; + } + int missing = std::max(0, 100 - assigned); + while (missing > 0) { + size_t best_idx = 0; + double best_rem = -1.0; + for (size_t i = 0; i < rem.size(); ++i) { + if (rem[i] > best_rem) { + best_rem = rem[i]; + best_idx = i; + } + } + ++out[best_idx]; + rem[best_idx] = 0.0; + --missing; + } + return out; + }; + auto encode_gradient_weights = [](const std::vector &weights) { + std::ostringstream ss; + for (size_t i = 0; i < weights.size(); ++i) { + if (i > 0) + ss << '/'; + ss << std::max(0, weights[i]); + } + return ss.str(); + }; + auto build_weighted_multi_sequence = [normalize_gradient_weights](const std::vector &ids, + const std::vector &weights, + size_t max_cycle_limit) { + if (ids.empty()) + return std::vector(); + + std::vector filtered_ids; + std::vector counts; + filtered_ids.reserve(ids.size()); + counts.reserve(ids.size()); + + std::vector normalized = normalize_gradient_weights(weights, ids.size()); + for (size_t i = 0; i < ids.size(); ++i) { + const int weight = (i < normalized.size()) ? std::max(0, normalized[i]) : 0; + if (weight <= 0) + continue; + filtered_ids.emplace_back(ids[i]); + counts.emplace_back(weight); + } + if (filtered_ids.empty()) { + filtered_ids = ids; + counts.assign(ids.size(), 1); + } + + int g = 0; + for (const int c : counts) + g = std::gcd(g, std::max(1, c)); + if (g > 1) { + for (int &c : counts) + c = std::max(1, c / g); + } + + constexpr size_t k_max_cycle = 48; + const size_t effective_cycle_limit = + max_cycle_limit > 0 ? std::min(k_max_cycle, std::max(1, max_cycle_limit)) : k_max_cycle; + reduce_weight_counts_to_cycle_limit(counts, effective_cycle_limit); + + std::vector reduced_ids; + std::vector reduced_counts; + reduced_ids.reserve(filtered_ids.size()); + reduced_counts.reserve(counts.size()); + for (size_t i = 0; i < counts.size(); ++i) { + if (counts[i] <= 0) + continue; + reduced_ids.emplace_back(filtered_ids[i]); + reduced_counts.emplace_back(counts[i]); + } + if (reduced_ids.empty()) + return std::vector(); + filtered_ids = std::move(reduced_ids); + counts = std::move(reduced_counts); + + const int total = std::accumulate(counts.begin(), counts.end(), 0); + if (total <= 0) + return std::vector(filtered_ids.begin(), filtered_ids.end()); + + const size_t cycle = size_t(total); + + std::vector sequence; + sequence.reserve(cycle); + std::vector emitted(counts.size(), 0); + for (size_t pos = 0; pos < cycle; ++pos) { + size_t best_idx = 0; + double best_score = -1e9; + for (size_t i = 0; i < counts.size(); ++i) { + const double target = double(pos + 1) * double(counts[i]) / double(total); + const double score = target - double(emitted[i]); + if (score > best_score) { + best_score = score; + best_idx = i; + } + } + ++emitted[best_idx]; + sequence.emplace_back(filtered_ids[best_idx]); + } + if (sequence.empty()) + sequence = filtered_ids; + return sequence; + }; + auto decode_manual_pattern_ids = [num_physical](const std::string &pattern, + unsigned int component_a, + unsigned int component_b, + size_t wall_loops) { + return build_grouped_manual_pattern_preview_sequence(pattern, component_a, component_b, num_physical, wall_loops); + }; + const bool height_weighted_mode = get_mixed_mode(false); + int gradient_mode = height_weighted_mode ? 1 : 0; + float lower_bound = std::max(0.01f, get_mixed_float("mixed_filament_height_lower_bound", 0.04f)); + float upper_bound = std::max(lower_bound, get_mixed_float("mixed_filament_height_upper_bound", 0.16f)); + float preferred_local_z_a = std::max(0.f, get_mixed_float("mixed_color_layer_height_a", 0.f)); + float preferred_local_z_b = std::max(0.f, get_mixed_float("mixed_color_layer_height_b", 0.f)); + float nominal_layer_height = 0.2f; + if (print_cfg && print_cfg->has("layer_height")) + nominal_layer_height = float(print_cfg->opt_float("layer_height")); + nominal_layer_height = std::max(0.01f, nominal_layer_height); + size_t wall_loops = 1; + if (print_cfg && print_cfg->has("wall_loops")) + wall_loops = std::max(1, size_t(std::max(1, print_cfg->opt_int("wall_loops")))); + const bool local_z_mode = get_mixed_bool("dithering_local_z_mode", false); + const bool local_z_direct_multicolor = + get_mixed_bool("dithering_local_z_direct_multicolor", false) && + preferred_local_z_a <= EPSILON && + preferred_local_z_b <= EPSILON; + const bool component_bias_enabled = get_mixed_bool("mixed_filament_component_bias_enabled", false); + float pointillism_pixel_size = std::max(0.f, get_mixed_float("mixed_filament_pointillism_pixel_size", 0.f)); + float pointillism_line_gap = std::max(0.f, get_mixed_float("mixed_filament_pointillism_line_gap", 0.f)); + float mixed_surface_indentation = std::clamp(get_mixed_float("mixed_filament_surface_indentation", 0.f), -2.f, 2.f); + bool advanced_dithering = get_mixed_bool("mixed_filament_advanced_dithering", false); + const std::string mixed_definitions = get_mixed_string("mixed_filament_definitions"); + const MixedFilamentPreviewSettings preview_settings { + nominal_layer_height, + lower_bound, + upper_bound, + preferred_local_z_a, + preferred_local_z_b, + local_z_mode, + local_z_direct_multicolor, + wall_loops + }; + const MixedFilamentDisplayContext display_context { + num_physical, + physical_colors, + nozzle_diameters, + preview_settings, + component_bias_enabled + }; + auto summarize_sequence = [num_physical](const std::vector &sequence) { + if (sequence.empty() || num_physical == 0) + return std::string(); + std::vector counts(num_physical + 1, size_t(0)); + size_t total = 0; + for (const unsigned int id : sequence) { + if (id == 0 || id > num_physical) + continue; + ++counts[id]; + ++total; + } + if (total == 0) + return std::string(); + std::ostringstream ss; + bool first = true; + for (size_t id = 1; id <= num_physical; ++id) { + if (counts[id] == 0) + continue; + const int pct = int(std::lround(100.0 * double(counts[id]) / double(total))); + if (!first) + ss << " "; + first = false; + ss << "F" << id << ":" << pct << "%"; + } + return ss.str(); + }; + auto compute_entry_display_color = [display_context](const MixedFilament &entry) { + return compute_mixed_filament_display_color(entry, display_context); + }; + + auto &mixed_mgr = preset_bundle->mixed_filaments; + mixed_mgr.set_display_context(display_context); + if (sync_manager) { + mixed_mgr.auto_generate(physical_colors); + mixed_mgr.clear_custom_entries(); + mixed_mgr.load_custom_entries(mixed_definitions, physical_colors); + mixed_mgr.apply_gradient_settings(gradient_mode, lower_bound, upper_bound, advanced_dithering); + } + + if (component_bias_enabled) { + for (MixedFilament &entry : mixed_mgr.mixed_filaments()) { + const float bias_value = mixed_filament_single_surface_offset_value(entry, nozzle_diameters); + const auto balanced_pair = mixed_filament_single_surface_offset_pair(entry, bias_value, nozzle_diameters); + entry.component_a_surface_offset = balanced_pair.first; + entry.component_b_surface_offset = balanced_pair.second; + } + } + + // During project load, sidebar may refresh before physical filament combos + // finish syncing. Avoid overwriting persisted mixed definitions while the + // physical filament set is incomplete. + if (num_physical >= 2) { + set_mixed_mode(height_weighted_mode); + set_mixed_bool("mixed_filament_component_bias_enabled", component_bias_enabled); + set_mixed_float("mixed_filament_height_lower_bound", lower_bound); + set_mixed_float("mixed_filament_height_upper_bound", upper_bound); + set_mixed_float("mixed_color_layer_height_a", preferred_local_z_a); + set_mixed_float("mixed_color_layer_height_b", preferred_local_z_b); + set_mixed_float("mixed_filament_pointillism_pixel_size", pointillism_pixel_size); + set_mixed_float("mixed_filament_pointillism_line_gap", pointillism_line_gap); + set_mixed_float("mixed_filament_surface_indentation", mixed_surface_indentation); + set_mixed_string("mixed_filament_definitions", mixed_mgr.serialize_custom_entries()); + } + + auto &mixed = mixed_mgr.mixed_filaments(); + const std::vector ordered_mixed_indices = build_mixed_filament_ui_indices(mixed, p->m_mixed_filament_ui_order); + std::vector sanitized_mixed_ui_order_ids; + sanitized_mixed_ui_order_ids.reserve(ordered_mixed_indices.size()); + for (const size_t mixed_id : ordered_mixed_indices) { + if (mixed_id < mixed.size() && mixed[mixed_id].stable_id != 0) + sanitized_mixed_ui_order_ids.emplace_back(mixed[mixed_id].stable_id); + } + p->m_mixed_filament_ui_order = std::move(sanitized_mixed_ui_order_ids); + + p->m_mixed_filament_drag_active = false; + p->m_mixed_filament_drag_source_mixed_id = size_t(-1); + p->m_mixed_filament_row_bindings.clear(); + + const int compact_gap_x = FromDIP(6); + const int compact_gap_y = FromDIP(4); + const int compact_row_pad = FromDIP(6); + const bool is_dark = wxGetApp().dark_mode(); + const wxColour mixed_rows_bg = is_dark ? wxColour(45, 45, 49) : wxColour(246, 248, 251); + const wxColour mixed_row_bg = is_dark ? wxColour(52, 52, 56) : wxColour(255, 255, 255); + const wxColour mixed_row_hover_bg = is_dark ? wxColour(62, 62, 68) : wxColour(241, 247, 255); + const wxColour mixed_text_fg = is_dark ? wxColour(232, 232, 232) : wxColour(20, 20, 20); + const wxColour mixed_summary_fg = is_dark ? wxColour(182, 182, 182) : wxColour(96, 96, 96); + p->m_panel_mixed_filaments_content->SetBackgroundColour(mixed_rows_bg); + + // Get the content sizer and clear it + wxSizer *content_sizer = p->m_panel_mixed_filaments_content->GetSizer(); + if (content_sizer) + content_sizer->Clear(true); + + // Re-add the top margin spacer that was added in constructor but cleared above + if (content_sizer) + content_sizer->AddSpacer(FromDIP(SidebarProps::ContentMargin())); + + // Update button states (buttons are now in title bar, created in constructor) + if (p->m_btn_add_gradient) + p->m_btn_add_gradient->Enable(num_physical >= 2); + if (p->m_btn_add_pattern) + p->m_btn_add_pattern->Enable(num_physical >= 2); + if (p->m_btn_add_color) + p->m_btn_add_color->Enable(num_physical >= 2); + + // Mixed Filaments panel is hidden + p->m_panel_mixed_filaments_title->Hide(); + p->m_panel_mixed_filaments_content->Hide(); + Layout(); + refresh_model_canvas_colors(); + wxWeakRef weakSelf(this); + wxTheApp->CallAfter([weakSelf]() { + if (weakSelf) weakSelf->update_color_mix_panel(); + }); + return; + +#if 0 // Mixed Filaments panel UI — hidden, preserved for potential future re-enablement + + // Reset the max size in case it was collapsed + p->m_panel_mixed_filaments_content->SetMaxSize({-1, -1}); + + auto *rows_scroller = new wxScrolledWindow(p->m_panel_mixed_filaments_content, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL | wxTAB_TRAVERSAL); + rows_scroller->SetScrollRate(0, FromDIP(6)); + rows_scroller->ShowScrollbars(wxSHOW_SB_NEVER, wxSHOW_SB_DEFAULT); + rows_scroller->SetBackgroundColour(mixed_rows_bg); + auto *rows_sizer = new wxBoxSizer(wxVERTICAL); + rows_scroller->SetSizer(rows_sizer); + + if (mixed.empty()) { + auto *empty_label = new wxStaticText(rows_scroller, wxID_ANY, + _L("No mixed filaments yet. Use Add Gradient, Add Pattern, or Add Color to create one.")); + empty_label->SetForegroundColour(mixed_summary_fg); + empty_label->SetFont(::Label::Body_13); + empty_label->Wrap(FromDIP(360)); + rows_sizer->Add(empty_label, 0, wxALL | wxEXPAND, FromDIP(12)); + rows_scroller->Layout(); + rows_scroller->FitInside(); + const int empty_content_h = empty_label->GetBestSize().GetHeight() + FromDIP(28); + const int empty_rows_h = std::max(FromDIP(86), empty_content_h); + rows_scroller->SetMinSize(wxSize(-1, empty_rows_h)); + rows_scroller->SetMaxSize(wxSize(-1, empty_rows_h)); + if (content_sizer) + content_sizer->Add(rows_scroller, 0, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, FromDIP(SidebarProps::ContentMargin())); + p->m_panel_mixed_filaments_content->Layout(); + Layout(); + refresh_model_canvas_colors(); + update_color_mix_panel(); + return; + } + + auto adjust_rows_scroller_height = [this, rows_scroller]() { + if (!rows_scroller) + return; + const int min_h = FromDIP(68); + const int collapsed_max_h = FromDIP(220); + int two_rows_cap_h = collapsed_max_h; + const auto &children = rows_scroller->GetChildren(); + if (!children.empty()) { + std::vector heights; + heights.reserve(children.GetCount()); + for (wxWindowList::compatibility_iterator it = children.GetFirst(); it; it = it->GetNext()) { + wxWindow *child = it->GetData(); + wxPanel *panel = dynamic_cast(child); + if (!panel) + continue; + heights.emplace_back(std::max(panel->GetSize().GetHeight(), panel->GetBestSize().GetHeight())); + } + if (!heights.empty()) { + std::sort(heights.begin(), heights.end(), std::greater()); + const size_t keep = std::min(2, heights.size()); + int rows_h = 0; + for (size_t i = 0; i < keep; ++i) + rows_h += heights[i]; + if (keep > 1) + rows_h += int(keep - 1) * FromDIP(2); + rows_h += FromDIP(8); + two_rows_cap_h = std::max(collapsed_max_h, rows_h); + } + } + + const int max_h = p->m_expanded_mixed_filament_rows.empty() ? collapsed_max_h : two_rows_cap_h; + const int content_h = std::max(0, rows_scroller->GetVirtualSize().GetHeight()); + const int desired_h = std::clamp(content_h, min_h, max_h); + rows_scroller->SetMinSize(wxSize(-1, desired_h)); + rows_scroller->SetMaxSize(wxSize(-1, desired_h)); + }; + + for (auto it = p->m_expanded_mixed_filament_rows.begin(); it != p->m_expanded_mixed_filament_rows.end();) { + if (*it >= mixed.size() || mixed[*it].deleted) + it = p->m_expanded_mixed_filament_rows.erase(it); + else + ++it; + } + + std::vector palette; + palette.reserve(physical_colors.size()); + for (const std::string &hex : physical_colors) + palette.emplace_back(parse_mixed_color(hex)); + + auto mixed_summary_text = [decode_gradient_ids](const MixedFilament &entry) { + const std::string normalized_pattern = MixedFilamentManager::normalize_manual_pattern(entry.manual_pattern); + if (!entry.custom) + return wxString::Format("(Filament %u + Filament %u)", unsigned(entry.component_a), unsigned(entry.component_b)); + if (!normalized_pattern.empty()) + return _L("(Pattern)"); + if (decode_gradient_ids(entry.gradient_component_ids).size() >= 3) + return _L("(Color)"); + return wxString::Format("(F%u + F%u)", unsigned(entry.component_a), unsigned(entry.component_b)); + }; + + auto apply_mixed_entry_changes = [this, preset_bundle, print_cfg, num_physical](size_t mixed_id, + const MixedFilament &updated_mf, + bool preserve_enabled = false, + bool rebuild_virtual_id_remap = false) { + if (!preset_bundle) + return; + + auto &mgr = preset_bundle->mixed_filaments; + auto &mfs = mgr.mixed_filaments(); + if (mixed_id >= mfs.size()) + return; + + const std::vector old_mixed = rebuild_virtual_id_remap ? mfs : std::vector(); + MixedFilament merged = updated_mf; + if (preserve_enabled) + merged.enabled = mfs[mixed_id].enabled; + mfs[mixed_id] = merged; + + const std::string serialized = mgr.serialize_custom_entries(); + if (print_cfg) { + if (ConfigOptionString *opt = print_cfg->option("mixed_filament_definitions")) + opt->value = serialized; + else + print_cfg->set_key_value("mixed_filament_definitions", new ConfigOptionString(serialized)); + } + if (ConfigOptionString *opt = preset_bundle->project_config.option("mixed_filament_definitions")) + opt->value = serialized; + else + preset_bundle->project_config.set_key_value("mixed_filament_definitions", new ConfigOptionString(serialized)); + + if (print_cfg) { + if (auto *print_tab = wxGetApp().get_tab(Preset::TYPE_PRINT)) + print_tab->update_dirty(); + if (wxGetApp().mainframe) + wxGetApp().mainframe->on_config_changed(print_cfg); + } + if (wxGetApp().plater()) + wxGetApp().plater()->update_project_dirty_from_presets(); + + if (rebuild_virtual_id_remap) + preset_bundle->update_mixed_filament_id_remap(old_mixed, num_physical, num_physical); + + int mode = 0; + if (const ConfigOptionBool *opt = preset_bundle->project_config.option("mixed_filament_gradient_mode")) + mode = opt->value ? 1 : 0; + else if (const ConfigOptionInt *opt = preset_bundle->project_config.option("mixed_filament_gradient_mode")) + mode = opt->value != 0 ? 1 : 0; + float lo = preset_bundle->project_config.has("mixed_filament_height_lower_bound") ? + float(preset_bundle->project_config.opt_float("mixed_filament_height_lower_bound")) : 0.04f; + float hi = preset_bundle->project_config.has("mixed_filament_height_upper_bound") ? + float(preset_bundle->project_config.opt_float("mixed_filament_height_upper_bound")) : 0.16f; + bool advanced = false; + if (const ConfigOptionBool *opt = preset_bundle->project_config.option("mixed_filament_advanced_dithering")) + advanced = opt->value; + mode = std::clamp(mode, 0, 1); + lo = std::max(0.01f, lo); + hi = std::max(lo, hi); + mgr.apply_gradient_settings(mode, lo, hi, advanced); + update_dynamic_filament_list(); + + if (rebuild_virtual_id_remap && wxGetApp().plater()) { + p->m_skip_mixed_filament_sync_once = true; + wxGetApp().plater()->on_filaments_change(num_physical); + } + }; + + auto current_mixed_filament_ui_order = [this, &mixed]() { + std::vector ordered_ids; + ordered_ids.reserve(p->m_mixed_filament_row_bindings.size()); + for (const auto &binding : p->m_mixed_filament_row_bindings) { + if (binding.mixed_id < mixed.size() && mixed[binding.mixed_id].stable_id != 0) + ordered_ids.emplace_back(mixed[binding.mixed_id].stable_id); + } + return ordered_ids; + }; + + auto drop_insert_position = [this]() { + const wxPoint mouse_pos = wxGetMousePosition(); + size_t visible_idx = 0; + for (const auto &binding : p->m_mixed_filament_row_bindings) { + if (binding.row == nullptr || !binding.row->IsShown()) + continue; + + const wxPoint top_left = binding.row->ClientToScreen(wxPoint(0, 0)); + const int row_h = std::max(binding.row->GetSize().GetHeight(), binding.row->GetBestSize().GetHeight()); + const int center_y = top_left.y + row_h / 2; + if (mouse_pos.y < center_y) + return visible_idx; + + ++visible_idx; + } + return visible_idx; + }; + + for (size_t display_mixed_idx = 0; display_mixed_idx < ordered_mixed_indices.size(); ++display_mixed_idx) { + const size_t mixed_id = ordered_mixed_indices[display_mixed_idx]; + MixedFilament &mf = mixed[mixed_id]; + const bool auto_row = !mf.custom; + + auto *row = new wxPanel(rows_scroller, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxBORDER_NONE); + row->SetBackgroundColour(mixed_row_bg); + auto *row_sizer = new wxBoxSizer(wxVERTICAL); + p->m_mixed_filament_row_bindings.push_back({mixed_id, row}); + + auto *header_panel = new wxPanel(row, wxID_ANY); + header_panel->SetBackgroundColour(mixed_row_bg); + auto *header_sizer = new wxBoxSizer(wxHORIZONTAL); + + const std::string synced_color = compute_entry_display_color(mf); + if (mf.display_color != synced_color) + mf.display_color = synced_color; + auto *drag_handle = new MixedFilamentDragHandle(header_panel, mixed_summary_fg, mixed_row_bg); + drag_handle->SetToolTip(_L("Drag to reorder mixed filaments in this panel.")); + header_sizer->Add(drag_handle, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, compact_gap_x); + + wxColour swatch_color = parse_mixed_color(mf.display_color); + auto *swatch = new wxPanel(header_panel, wxID_ANY, wxDefaultPosition, wxSize(FromDIP(12), FromDIP(12))); + swatch->SetBackgroundColour(swatch_color); + swatch->SetMinSize(wxSize(FromDIP(12), FromDIP(12))); + header_sizer->Add(swatch, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, compact_gap_x); + + const int virtual_filament_id = int(num_physical + display_mixed_idx + 1); + auto *name_label = new wxStaticText(header_panel, wxID_ANY, wxString::Format("Mixed Filament %d", virtual_filament_id)); + name_label->SetForegroundColour(mixed_text_fg); + header_sizer->Add(name_label, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, compact_gap_x); + + auto *summary_label = new wxStaticText(header_panel, wxID_ANY, mixed_summary_text(mf)); + summary_label->SetForegroundColour(mixed_summary_fg); + header_sizer->Add(summary_label, 1, wxALIGN_CENTER_VERTICAL | wxLEFT, compact_gap_x); + + header_sizer->AddStretchSpacer(1); + + auto *enabled_chk = new wxCheckBox(header_panel, wxID_ANY, _L("Enabled")); + enabled_chk->SetValue(mf.enabled); + enabled_chk->SetForegroundColour(mixed_text_fg); + header_sizer->Add(enabled_chk, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, compact_gap_x); + enabled_chk->Bind(wxEVT_LEFT_UP, [](wxMouseEvent &evt) { + evt.StopPropagation(); + evt.Skip(); + }); + enabled_chk->Bind(wxEVT_CHECKBOX, [mixed_id, enabled_chk, apply_mixed_entry_changes, preset_bundle](wxCommandEvent &) { + if (!preset_bundle || !enabled_chk) + return; + auto &mgr = preset_bundle->mixed_filaments; + auto &mfs = mgr.mixed_filaments(); + if (mixed_id >= mfs.size()) + return; + MixedFilament updated = mfs[mixed_id]; + updated.enabled = enabled_chk->GetValue(); + apply_mixed_entry_changes(mixed_id, updated, false, true); + }); + + auto *del_btn = new ScalableButton(header_panel, wxID_ANY, "cross"); + del_btn->SetToolTip(_L("Delete mixed filament")); + header_sizer->Add(del_btn, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, compact_gap_x); + + del_btn->Bind(wxEVT_BUTTON, [this, mixed_id, num_physical, set_mixed_string, notify_mixed_change](wxCommandEvent&) { + if (wxGetApp().preset_bundle) { + auto &mgr = wxGetApp().preset_bundle->mixed_filaments; + auto &mfs = mgr.mixed_filaments(); + if (mixed_id < mfs.size()) { + const std::vector old_mixed = mfs; + auto canonical_pair = [](unsigned int a, unsigned int b) { + return std::make_pair(std::min(a, b), std::max(a, b)); + }; + MixedFilament &target = mfs[mixed_id]; + const auto target_pair = canonical_pair(target.component_a, target.component_b); + const bool valid_auto_pair = target_pair.first >= 1 && + target_pair.second >= 1 && + target_pair.first <= num_physical && + target_pair.second <= num_physical && + target_pair.first != target_pair.second; + if (target.custom && target.origin_auto && valid_auto_pair) { + bool tombstoned_existing_auto = false; + for (size_t idx = 0; idx < mfs.size(); ++idx) { + if (idx == mixed_id) + continue; + MixedFilament &candidate = mfs[idx]; + if (candidate.custom) + continue; + if (canonical_pair(candidate.component_a, candidate.component_b) != target_pair) + continue; + candidate.deleted = true; + candidate.enabled = false; + tombstoned_existing_auto = true; + break; + } + + if (tombstoned_existing_auto) { + mfs.erase(mfs.begin() + mixed_id); + } else { + target.component_a = target_pair.first; + target.component_b = target_pair.second; + target.mix_b_percent = 50; + target.ratio_a = 1; + target.ratio_b = 1; + target.manual_pattern.clear(); + target.gradient_component_ids.clear(); + target.gradient_component_weights.clear(); + target.pointillism_all_filaments = false; + target.distribution_mode = int(MixedFilament::Simple); + target.custom = false; + target.origin_auto = true; + target.deleted = true; + target.enabled = false; + } + } else if (target.custom) { + mfs.erase(mfs.begin() + mixed_id); + } else { + target.deleted = true; + target.enabled = false; + } + p->m_expanded_mixed_filament_rows.clear(); + set_mixed_string("mixed_filament_definitions", mgr.serialize_custom_entries()); + wxGetApp().preset_bundle->update_mixed_filament_id_remap(old_mixed, num_physical, num_physical, mixed_id); + notify_mixed_change(); + if (wxGetApp().plater()) + wxGetApp().plater()->update_project_dirty_from_presets(); + if (wxGetApp().plater()) { + p->m_skip_mixed_filament_sync_once = true; + wxGetApp().plater()->on_filaments_change(num_physical); + } + } + } + }); + + header_panel->SetSizer(header_sizer); + row_sizer->Add(header_panel, 0, wxEXPAND | wxALL, 0); + + auto *editor_host = new wxPanel(row, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxBORDER_NONE); + editor_host->SetBackgroundColour(mixed_row_bg); + auto *editor_sizer = new wxBoxSizer(wxVERTICAL); + editor_host->SetSizer(editor_sizer); + editor_host->Hide(); + row_sizer->Add(editor_host, 0, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, compact_row_pad); + + auto set_row_hover = [row, header_panel, editor_host, drag_handle, mixed_summary_fg, mixed_row_bg, mixed_row_hover_bg](bool hovered) { + const wxColour bg = hovered ? mixed_row_hover_bg : mixed_row_bg; + if (row) row->SetBackgroundColour(bg); + if (header_panel) header_panel->SetBackgroundColour(bg); + if (editor_host) editor_host->SetBackgroundColour(bg); + if (drag_handle) drag_handle->set_colors(mixed_summary_fg, bg); + if (row) row->Refresh(); + if (header_panel) header_panel->Refresh(); + if (editor_host) editor_host->Refresh(); + }; + + auto row_contains_mouse = [row]() { + if (!row) + return false; + const wxPoint mouse_pos = wxGetMousePosition(); + const wxPoint local = row->ScreenToClient(mouse_pos); + return row->GetClientRect().Contains(local); + }; + + auto ensure_editor = [this, mixed_id, num_physical, physical_colors, nozzle_diameters, palette, preview_settings, component_bias_enabled, preset_bundle, + editor_host, editor_sizer, swatch, summary_label, header_panel, row, + rows_scroller, mixed_summary_text, apply_mixed_entry_changes]() { + if (!preset_bundle || !editor_sizer || editor_sizer->GetItemCount() > 0) + return; + + auto &mgr = preset_bundle->mixed_filaments; + auto &mfs = mgr.mixed_filaments(); + if (mixed_id >= mfs.size()) + return; + + auto *editor = new MixedFilamentConfigPanel(editor_host, mixed_id, mfs[mixed_id], num_physical, physical_colors, nozzle_diameters, palette, preview_settings, + component_bias_enabled, + [this, mixed_id, swatch, summary_label, header_panel, row, rows_scroller, mixed_summary_text, apply_mixed_entry_changes](const MixedFilament &updated_mf) { + apply_mixed_entry_changes(mixed_id, updated_mf, true); + + if (swatch) { + swatch->SetBackgroundColour(parse_mixed_color(updated_mf.display_color)); + swatch->Refresh(); + } + if (summary_label) { + summary_label->SetLabel(mixed_summary_text(updated_mf)); + } + if (header_panel) + header_panel->Layout(); + if (row) + row->Layout(); + if (rows_scroller) { + rows_scroller->Layout(); + rows_scroller->FitInside(); + } + }); + + editor_sizer->Add(editor, 0, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, FromDIP(4)); + editor_host->Layout(); + }; + + auto toggle_editor = [this, mixed_id, editor_host, ensure_editor, rows_scroller, adjust_rows_scroller_height]() { + if (!editor_host || !rows_scroller) + return; + + if (editor_host->IsShown()) { + editor_host->Hide(); + p->m_expanded_mixed_filament_rows.erase(mixed_id); + } else { + ensure_editor(); + editor_host->Show(); + p->m_expanded_mixed_filament_rows.insert(mixed_id); + } + + rows_scroller->Layout(); + rows_scroller->FitInside(); + adjust_rows_scroller_height(); + p->m_panel_mixed_filaments_content->Layout(); + m_scrolled_sizer->Layout(); + Layout(); + }; + + auto bind_toggle_target = [&toggle_editor](wxWindow *target) { + if (!target) + return; + target->SetCursor(wxCursor(wxCURSOR_HAND)); + target->Bind(wxEVT_LEFT_UP, [toggle_editor](wxMouseEvent &) { + toggle_editor(); + }); + }; + + auto bind_hover_target = [set_row_hover, row_contains_mouse](wxWindow *target) { + if (!target) + return; + target->Bind(wxEVT_ENTER_WINDOW, [set_row_hover](wxMouseEvent &evt) { + set_row_hover(true); + evt.Skip(); + }); + target->Bind(wxEVT_LEAVE_WINDOW, [set_row_hover, row_contains_mouse](wxMouseEvent &evt) { + set_row_hover(row_contains_mouse()); + evt.Skip(); + }); + }; + + auto release_drag_capture = [this]() { + p->m_mixed_filament_drag_active = false; + p->m_mixed_filament_drag_source_mixed_id = size_t(-1); + }; + + auto bind_drag_target = [this, + mixed_id, + &mixed, + drop_insert_position, + current_mixed_filament_ui_order, + release_drag_capture](wxWindow *target) { + if (!target) + return; + + target->Bind(wxEVT_LEFT_DOWN, [this, mixed_id, target](wxMouseEvent &evt) { + if (!target) + return; + p->m_mixed_filament_drag_active = true; + p->m_mixed_filament_drag_source_mixed_id = mixed_id; + if (!target->HasCapture()) + target->CaptureMouse(); + evt.StopPropagation(); + }); + + target->Bind(wxEVT_MOTION, [this](wxMouseEvent &evt) { + if (p->m_mixed_filament_drag_active) + evt.StopPropagation(); + }); + + target->Bind(wxEVT_LEFT_UP, [this, &mixed, target, drop_insert_position, current_mixed_filament_ui_order](wxMouseEvent &evt) { + if (target && target->HasCapture()) + target->ReleaseMouse(); + + if (!p->m_mixed_filament_drag_active || p->m_mixed_filament_drag_source_mixed_id >= mixed.size()) { + p->m_mixed_filament_drag_active = false; + p->m_mixed_filament_drag_source_mixed_id = size_t(-1); + evt.StopPropagation(); + return; + } + + const size_t source_mixed_id = p->m_mixed_filament_drag_source_mixed_id; + p->m_mixed_filament_drag_active = false; + p->m_mixed_filament_drag_source_mixed_id = size_t(-1); + + std::vector current_mixed_ids; + current_mixed_ids.reserve(p->m_mixed_filament_row_bindings.size()); + for (const auto &binding : p->m_mixed_filament_row_bindings) { + if (binding.mixed_id < mixed.size() && !mixed[binding.mixed_id].deleted) + current_mixed_ids.emplace_back(binding.mixed_id); + } + + const auto source_it = std::find(current_mixed_ids.begin(), current_mixed_ids.end(), source_mixed_id); + if (source_it == current_mixed_ids.end()) { + evt.StopPropagation(); + return; + } + + const size_t source_pos = size_t(std::distance(current_mixed_ids.begin(), source_it)); + size_t insert_pos = drop_insert_position(); + insert_pos = std::min(insert_pos, current_mixed_ids.size()); + + current_mixed_ids.erase(source_it); + if (insert_pos > source_pos) + --insert_pos; + insert_pos = std::min(insert_pos, current_mixed_ids.size()); + current_mixed_ids.insert(current_mixed_ids.begin() + ptrdiff_t(insert_pos), source_mixed_id); + + std::vector reordered_stable_ids; + reordered_stable_ids.reserve(current_mixed_ids.size()); + for (const size_t row_mixed_id : current_mixed_ids) { + if (row_mixed_id < mixed.size() && mixed[row_mixed_id].stable_id != 0) + reordered_stable_ids.emplace_back(mixed[row_mixed_id].stable_id); + } + + if (reordered_stable_ids != current_mixed_filament_ui_order()) { + p->m_mixed_filament_ui_order = std::move(reordered_stable_ids); + update_mixed_filament_panel(false); + } + + evt.StopPropagation(); + }); + + target->Bind(wxEVT_MOUSE_CAPTURE_LOST, [release_drag_capture](wxMouseCaptureLostEvent &) { + release_drag_capture(); + }); + }; + + header_panel->SetToolTip(auto_row ? + _L("Click to edit automatic mixed filament settings (saved as custom).") : + _L("Click to expand/retract mixed filament settings")); + bind_toggle_target(row); + bind_toggle_target(header_panel); + bind_toggle_target(name_label); + bind_toggle_target(summary_label); + bind_toggle_target(swatch); + bind_hover_target(row); + bind_hover_target(header_panel); + bind_hover_target(name_label); + bind_hover_target(summary_label); + bind_hover_target(swatch); + bind_hover_target(drag_handle); + bind_drag_target(drag_handle); + + del_btn->Bind(wxEVT_LEFT_UP, [](wxMouseEvent &evt) { + evt.StopPropagation(); + evt.Skip(); + }); + + if (p->m_expanded_mixed_filament_rows.count(mixed_id) != 0) { + ensure_editor(); + editor_host->Show(); + } + + row->SetSizer(row_sizer); + rows_sizer->Add(row, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, FromDIP(2)); + rows_sizer->AddSpacer(FromDIP(2)); + } + + rows_sizer->AddSpacer(FromDIP(2)); + rows_scroller->Layout(); + rows_scroller->FitInside(); + adjust_rows_scroller_height(); + if (prev_rows_view_y > 0) + rows_scroller->Scroll(0, prev_rows_view_y); + + content_sizer->Add(rows_scroller, 0, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(2)); + content_sizer->AddSpacer(FromDIP(2)); + p->m_panel_mixed_filaments_content->Layout(); + m_scrolled_sizer->Layout(); + Layout(); + refresh_model_canvas_colors(); + update_color_mix_panel(); +#endif // Mixed Filaments panel UI +} + +std::vector Sidebar::get_ui_ordered_filament_ids() const +{ + const size_t num_physical = static_cast(std::max(wxGetApp().filaments_cnt(), 0)); + std::vector ordered_filament_ids; + ordered_filament_ids.reserve(num_physical); + for (size_t idx = 0; idx < num_physical; ++idx) + ordered_filament_ids.emplace_back(unsigned(idx + 1)); + + if (wxGetApp().preset_bundle == nullptr) + return ordered_filament_ids; + + const auto &mixed = wxGetApp().preset_bundle->mixed_filaments.mixed_filaments(); + if (mixed.empty()) + return ordered_filament_ids; + + const std::vector ordered_mixed_indices = build_mixed_filament_ui_indices(mixed, p->m_mixed_filament_ui_order); + std::vector actual_filament_id_by_mixed_idx(mixed.size(), 0); + unsigned int next_filament_id = unsigned(num_physical + 1); + for (size_t mixed_idx = 0; mixed_idx < mixed.size(); ++mixed_idx) { + if (!mixed[mixed_idx].enabled || mixed[mixed_idx].deleted) + continue; + actual_filament_id_by_mixed_idx[mixed_idx] = next_filament_id++; + } + + ordered_filament_ids.reserve(size_t(next_filament_id - 1)); + for (const size_t mixed_idx : ordered_mixed_indices) { + if (mixed_idx >= actual_filament_id_by_mixed_idx.size()) + continue; + const unsigned int actual_filament_id = actual_filament_id_by_mixed_idx[mixed_idx]; + if (actual_filament_id != 0) + ordered_filament_ids.emplace_back(actual_filament_id); + } + + return ordered_filament_ids; +} + +void Sidebar::add_filament() { + if (p->combos_filament.size() >= MAXIMUM_EXTRUDER_NUMBER) return; + PresetBundle* pb = wxGetApp().preset_bundle; + if (!pb || pb->mixed_filaments.total_filaments(p->combos_filament.size()) >= MAXIMUM_FILAMENT_NUMBER) return; + wxColour new_col = Plater::get_next_color_for_filament(); + add_custom_filament(new_col); +} + +void Sidebar::on_filaments_delete(size_t filament_id) +{ + auto& choices = combos_filament(); + + p->m_skip_mixed_filament_sync_once = false; + + if (filament_id >= choices.size()) + return; + + if (choices.size() <= 2) + choices[0]->GetDropDown().Invalidate(); + + wxWindowUpdateLocker noUpdates_scrolled_panel(this); // delete UI item if (filament_id < p->combos_filament.size()) { @@ -2463,14 +7246,55 @@ void Sidebar::on_filaments_delete(size_t filament_id) sizer->Hide(p->m_flushing_volume_btn); } + if (p->m_bpButton_del_filament != nullptr && p->m_panel_physical_filaments_title != nullptr) { + auto* inner_sizer = p->m_panel_physical_filaments_title->GetSizer(); + if (inner_sizer) { + if (p->combos_filament.size() > 1) + inner_sizer->Show(p->m_bpButton_del_filament); + else + inner_sizer->Hide(p->m_bpButton_del_filament); + } + } + for (size_t idx = filament_id; idx < p->combos_filament.size(); ++idx) { p->combos_filament[idx]->update(); } + // Recalc scrolled filament window height (max 3 rows) + if (p->m_scrolled_filaments && p->m_panel_scrolled_filament_content) { + p->m_panel_scrolled_filament_content->Layout(); + const wxSize content_best = p->m_panel_scrolled_filament_content->GetBestSize(); + const int row_count = ((int)p->combos_filament.size() + 1) / 2; + const int desired_h = row_count > 3 + ? (content_best.GetHeight() / std::max(1, row_count)) * 3 + : content_best.GetHeight(); + p->m_scrolled_filaments->SetMinSize({-1, desired_h}); + p->m_scrolled_filaments->SetMaxSize({-1, desired_h}); + } + Layout(); + wxWeakRef weak_this(this); + wxTheApp->CallAfter([weak_this]() { + Sidebar* sidebar = weak_this.get(); + if (sidebar && sidebar->p && sidebar->p->m_scrolled_filaments) { + int vh = sidebar->p->m_scrolled_filaments->GetVirtualSize().y; + int ch = sidebar->p->m_scrolled_filaments->GetClientSize().y; + sidebar->p->m_scrolled_filaments->Scroll(0, std::max(0, vh - ch)); + } + }); p->m_panel_filament_title->Refresh(); update_ui_from_settings(); - dynamic_filament_list.update(); + update_dynamic_filament_list(); + update_mixed_filament_panel(); + update_color_mix_panel(); + + if (PresetBundle *pb = wxGetApp().preset_bundle) { + const bool can_add = pb->mixed_filaments.total_filaments(p->combos_filament.size()) < MAXIMUM_FILAMENT_NUMBER; + if (p->m_bpButton_add_filament) + p->m_bpButton_add_filament->Enable(can_add); + if (p->m_btn_add_color_mix) + p->m_btn_add_color_mix->Enable(can_add); + } } void Sidebar::edit_filament() { @@ -2480,31 +7304,198 @@ void Sidebar::edit_filament() { p->editing_filament = p->m_menu_filament_id; // sync with TabPresetComboxBox's m_filament_idx } -void Sidebar::update_color_mix_panel() +// Helper function: Check if target mixed filament depends on source physical filament +static bool mixed_filament_uses_physical(const MixedFilament* target_mf, unsigned int source_physical_1based) { - // Stub: mixed filament color panel not yet implemented in this version. -} + if (!target_mf) + return false; -void Sidebar::update_mixed_filament_panel(bool sync_manager) -{ - // Stub: mixed filament panel not yet implemented in this version. + // Check manual_pattern tokens (resolve order #1) + const std::string norm = MixedFilamentManager::normalize_manual_pattern(target_mf->manual_pattern); + if (!norm.empty()) { + const auto groups = MixedFilamentManager::split_pattern_groups(norm); + for (const std::string &group : groups) { + const auto tokens = MixedFilamentManager::split_pattern_group_to_tokens(group, 0); + for (const std::string &token : tokens) { + if (MixedFilamentManager::physical_filament_from_token(token, *target_mf, MixedFilamentManager::kMaxPhysicalFilaments) == source_physical_1based) + return true; + } + } + } + + // Check gradient components (resolve order #2). + // Only check when there is no manual_pattern; a pattern already resolves + // every token, so gradient IDs would be a false positive at worst. + if (norm.empty()) { + const std::vector ids = MixedFilamentManager::decode_gradient_component_ids(target_mf->gradient_component_ids, 0); + for (unsigned int id : ids) { + if (id == source_physical_1based) + return true; + } + } + + // Check if target mixed filament uses source physical filament as component + // (resolve order #3). Only reached when the mixed filament has no manual_pattern, + // because in cycle mode pattern tokens "1"/"2" already cover component_a/b. + if (norm.empty() && (target_mf->component_a == source_physical_1based || target_mf->component_b == source_physical_1based)) { + return true; + } + + return false; } -std::vector Sidebar::get_ui_ordered_filament_ids() const +void Sidebar::change_filament(size_t from_id, size_t to_id) { - const size_t num_physical = static_cast(std::max(wxGetApp().filaments_cnt(), 0)); - std::vector ordered_filament_ids; - ordered_filament_ids.reserve(num_physical); - for (size_t idx = 0; idx < num_physical; ++idx) - ordered_filament_ids.emplace_back(unsigned(idx + 1)); - return ordered_filament_ids; -} + // 1. Parameter preprocessing + if (from_id == size_t(-2)) + from_id = p->m_menu_filament_id; + if (from_id == size_t(-1)) + from_id = p->combos_filament.size() - 1; + if (from_id == to_id) + return; + auto& pb = *wxGetApp().preset_bundle; + const size_t num_physical = pb.filament_presets.size(); + // 2. Determine source and target types + // Note: filament IDs here are 0-based, but is_mixed expects 1-based + bool from_is_mixed = pb.mixed_filaments.is_mixed((unsigned int)(from_id + 1), num_physical); + bool to_is_mixed = pb.mixed_filaments.is_mixed((unsigned int)(to_id + 1), num_physical); -void Sidebar::change_filament(size_t from_id, size_t to_id) + // 3. Dependency check: physical → mixed + if (!from_is_mixed && to_is_mixed) { + const MixedFilament* target_mf = pb.mixed_filaments.mixed_filament_from_id((unsigned int)(to_id + 1), num_physical); + unsigned int from_1based = (unsigned int)(from_id + 1); + + if (mixed_filament_uses_physical(target_mf, from_1based)) { + MessageDialog dlg(wxGetApp().plater(), + _L("The target mixed filament uses this physical filament as a component. " + "Merging will remove this physical filament and may invalidate the mixed filament. Continue?"), + _L("Warning"), wxOK | wxCANCEL | wxICON_WARNING); + int ret = dlg.ShowModal(); + if (ret != wxID_OK) + return; + } + } + + // 3b. Check for mixed filaments that depend on the source physical filament + if (!from_is_mixed) { + unsigned int from_1based = (unsigned int)(from_id + 1); + std::vector dependent_mixed_indices = pb.mixed_filaments.mixed_filaments_using_physical(from_1based); + + // If there are dependent mixed filaments, show warning dialog + if (!dependent_mixed_indices.empty()) { + wxString msg = _L("This filament is used in the following mixed filament configurations:\n\n"); + + const auto& mfs = pb.mixed_filaments.mixed_filaments(); + size_t visible_idx = 0; + for (size_t j = 0; j < mfs.size(); ++j) { + if (mfs[j].deleted) continue; + + // Check if this is one of the dependent mixed filaments + bool is_dependent = std::find(dependent_mixed_indices.begin(), dependent_mixed_indices.end(), j) + != dependent_mixed_indices.end(); + + if (is_dependent) { + const int virtual_id = static_cast(num_physical) + visible_idx + 1; + msg += wxString::Format(_L("• Mixed Filament %d\n"), virtual_id); + } + + visible_idx++; + } + + msg += _L("\nMerging this filament will invalidate these mixed filament configurations. Continue?"); + + MessageDialog dlg(wxGetApp().plater(), msg, _L("Warning"), wxOK | wxCANCEL | wxICON_WARNING); + int ret = dlg.ShowModal(); + if (ret != wxID_OK) + return; + } + } + + // 4. Execute merge based on scenario + if (from_is_mixed) { + // Mixed → Physical or Mixed → Mixed + merge_mixed_filament(from_id, to_id); + } else { + // Physical → Physical or Physical → Mixed + delete_filament(from_id, int(to_id)); + } +} + +void Sidebar::merge_mixed_filament(size_t from_id, size_t to_id) { - delete_filament(from_id, int(to_id)); + // Merge a mixed filament into another filament (physical or mixed) + // This marks the source as deleted and remaps all objects using it + + PresetBundle* preset_bundle = wxGetApp().preset_bundle; + if (!preset_bundle) return; + auto& pb = *preset_bundle; + const size_t num_physical = pb.filament_presets.size(); + + // Validate parameters + size_t total_filaments = pb.mixed_filaments.total_filaments(num_physical); + if (from_id >= total_filaments || to_id >= total_filaments) { + BOOST_LOG_TRIVIAL(error) << "merge_mixed_filament: Invalid filament ID. from_id=" + << from_id << " to_id=" << to_id << " total=" << total_filaments; + return; + } + + // Prevent self-merge + if (from_id == to_id) { + BOOST_LOG_TRIVIAL(warning) << "merge_mixed_filament: Cannot merge filament into itself: " << from_id; + return; + } + + // Verify source is actually a mixed filament + if (!pb.mixed_filaments.is_mixed((unsigned int)(from_id + 1), num_physical)) { + BOOST_LOG_TRIVIAL(error) << "merge_mixed_filament: Source filament " << from_id + << " is not a mixed filament"; + return; + } + + // Get source mixed filament index + int source_mixed_idx = pb.mixed_filaments.mixed_index_from_filament_id((unsigned int)(from_id + 1), num_physical); + if (source_mixed_idx < 0) { + BOOST_LOG_TRIVIAL(error) << "merge_mixed_filament: Cannot find mixed filament index for ID " << from_id; + return; + } + + auto& mfs = pb.mixed_filaments.mixed_filaments(); + if ((size_t)source_mixed_idx >= mfs.size()) { + BOOST_LOG_TRIVIAL(error) << "merge_mixed_filament: Mixed filament index " << source_mixed_idx + << " out of range (size=" << mfs.size() << ")"; + return; + } + + BOOST_LOG_TRIVIAL(info) << "Merging mixed filament " << from_id << " into filament " << to_id; + + // Build remap table using PresetBundle method + pb.build_merge_filament_remap(from_id, to_id, total_filaments); + + // Mark source mixed filament as deleted + mfs[source_mixed_idx].deleted = true; + mfs[source_mixed_idx].enabled = false; + + // Persist changes + if (auto* opt = pb.project_config.option("mixed_filament_definitions")) + opt->value = pb.mixed_filaments.serialize_custom_entries(); + + // Save mixed snapshot + std::vector is_mixed_snapshot; + if (auto* opt = pb.project_config.option("filament_is_mixed")) + is_mixed_snapshot = opt->values; + + // Update objects to use new filament IDs + size_t total_after = pb.mixed_filaments.total_filaments(num_physical); + wxGetApp().plater()->on_filaments_delete(total_after, from_id, -1, is_mixed_snapshot); + + BOOST_LOG_TRIVIAL(info) << "Mixed filament merge completed. Total filaments after: " << total_after; + + // Update UI + update_color_mix_panel(); + m_scrolled_sizer->Layout(); + wxGetApp().plater()->update(); } void Sidebar::delete_filament(size_t filament_id, int replace_filament_id) @@ -2520,25 +7511,149 @@ void Sidebar::delete_filament(size_t filament_id, int replace_filament_id) filament_id = filament_count; } - if (filament_id > filament_count) + size_t total_filaments = wxGetApp().preset_bundle->filament_presets.size(); + if (filament_id > filament_count && filament_id >= total_filaments) return; - if (wxGetApp().preset_bundle->is_the_only_edited_filament(filament_id) || (filament_id == 0)) { - wxGetApp().get_tab(Preset::TYPE_FILAMENT)->select_preset(wxGetApp().preset_bundle->filament_presets[0], false, "", true); + bool is_mixed = (filament_id >= p->combos_filament.size()); + + // Check for mixed filaments that depend on the source physical filament (before deletion) + // Skip this dialog when called from change_filament() (merge path), since change_filament() + // already showed its own confirmation dialog for the same dependent mixed filaments. + if (!is_mixed && replace_filament_id < 0) { + auto& pb = *wxGetApp().preset_bundle; + const size_t num_physical = pb.filament_presets.size(); + unsigned int filament_1based = (unsigned int)(filament_id + 1); + std::vector dependent_mixed_indices = pb.mixed_filaments.mixed_filaments_using_physical(filament_1based); + + // If there are dependent mixed filaments, show warning dialog + if (!dependent_mixed_indices.empty()) { + wxString msg = _L("This filament is used in the following mixed filament configurations:\n\n"); + + const auto& mfs = pb.mixed_filaments.mixed_filaments(); + size_t visible_idx = 0; + for (size_t j = 0; j < mfs.size(); ++j) { + if (mfs[j].deleted) continue; + + // Check if this is one of the dependent mixed filaments + bool is_dependent = std::find(dependent_mixed_indices.begin(), dependent_mixed_indices.end(), j) + != dependent_mixed_indices.end(); + + if (is_dependent) { + const int virtual_id = static_cast(num_physical) + visible_idx + 1; + msg += wxString::Format(_L("• Mixed Filament %d\n"), virtual_id); + } + + visible_idx++; + } + + msg += _L("\nDeleting this filament will invalidate these mixed filament configurations. Continue?"); + + MessageDialog dlg(wxGetApp().plater(), msg, _L("Warning"), wxOK | wxCANCEL | wxICON_WARNING); + int ret = dlg.ShowModal(); + if (ret != wxID_OK) + return; + } } - if (p->editing_filament == filament_id || p->editing_filament >= filament_count) { - p->editing_filament = -1; + PresetBundle* preset_bundle = wxGetApp().preset_bundle; + if (!preset_bundle) return; + + if (!is_mixed) { + if (preset_bundle->is_the_only_edited_filament(filament_id) || (filament_id == 0)) { + wxGetApp().get_tab(Preset::TYPE_FILAMENT)->select_preset(preset_bundle->filament_presets[0], false, "", true); + } + if (p->editing_filament == filament_id || p->editing_filament >= filament_count) { + p->editing_filament = -1; + } } - wxGetApp().preset_bundle->update_num_filaments(filament_id); - wxGetApp().plater()->get_partplate_list().on_filament_deleted(filament_count, filament_id); - + std::vector is_mixed_snapshot; + if (auto* opt = preset_bundle->project_config.option("filament_is_mixed")) + is_mixed_snapshot = opt->values; + + auto& pb = *preset_bundle; + size_t old_num_physical = pb.filament_presets.size(); + size_t old_total_filaments = pb.mixed_filaments.total_filaments(old_num_physical); + + // Check if this is a physical filament being merged into a mixed filament + bool is_physical_to_mixed_merge = !is_mixed && replace_filament_id >= (int)old_num_physical; + + // Check if target mixed filament depends on source physical filament + bool target_depends_on_source = false; + + if (is_physical_to_mixed_merge) { + // Get target mixed filament + const MixedFilament* target_mf = pb.mixed_filaments.mixed_filament_from_id( + (unsigned int)(replace_filament_id + 1), old_num_physical); + unsigned int source_1based = (unsigned int)(filament_id + 1); + + // Use helper function to check dependency + target_depends_on_source = mixed_filament_uses_physical(target_mf, source_1based); + + BOOST_LOG_TRIVIAL(info) << "Physical to mixed merge: source=" << filament_id + << " target=" << replace_filament_id + << " target_depends_on_source=" << target_depends_on_source; + } + + // Case 2: If target mixed doesn't depend on source physical, use remap mechanism + // This is similar to merge_mixed_filament() - we use a remap table to redirect objects + // to the target mixed filament, then delete the physical filament + if (is_physical_to_mixed_merge && !target_depends_on_source) { + // Build custom remap table that accounts for physical filament deletion + // When a physical filament is deleted, mixed filament virtual IDs change + pb.build_merge_filament_remap(filament_id, replace_filament_id, old_total_filaments, old_num_physical); + + BOOST_LOG_TRIVIAL(info) << "Built custom remap for physical to mixed merge (accounts for virtual ID changes)"; + + // Call on_filaments_delete with -1 to trigger remap usage + // This updates object colors using the remap table + wxGetApp().plater()->on_filaments_delete(old_total_filaments, filament_id, -1, is_mixed_snapshot); + + // Now delete the physical filament + pb.update_num_filaments(filament_id); + pb.consume_last_filament_id_remap(); // discard the remap built by update_num_filaments + wxGetApp().plater()->get_partplate_list().on_filament_deleted( + pb.filament_presets.size(), filament_id); + + BOOST_LOG_TRIVIAL(info) << "Physical to mixed merge completed using custom remap mechanism"; + + // Update UI + wxGetApp().get_tab(Preset::TYPE_PRINT)->update(); + wxGetApp().preset_bundle->export_selections(*wxGetApp().app_config); + wxGetApp().plater()->update(); + return; + } + // Case 1: If target mixed depends on source physical, delete the physical filament + // Let on_filaments_delete use default behavior (reset to default) + else if (is_physical_to_mixed_merge && target_depends_on_source) { + BOOST_LOG_TRIVIAL(info) << "Physical to mixed merge: target depends on source, will delete physical and reset to default"; + } + + // Delete the physical filament (this also removes mixed filaments that depend on it) + pb.update_num_filaments(filament_id); + size_t new_num_physical = pb.filament_presets.size(); + + size_t total_after_delete = pb.mixed_filaments.total_filaments(new_num_physical); + wxGetApp().plater()->get_partplate_list().on_filament_deleted(total_after_delete, filament_id); + + int final_replace_id; + if (is_physical_to_mixed_merge && target_depends_on_source) { + // Case 1: target mixed was also deleted by remove_physical_filament + final_replace_id = -1; + } else { + int adjusted = replace_filament_id; + if (replace_filament_id >= (int)old_num_physical) + adjusted = int(new_num_physical + (size_t(replace_filament_id) - old_num_physical)); + else if (replace_filament_id > (int)filament_id) + adjusted = replace_filament_id - 1; + final_replace_id = adjusted; + } + + wxGetApp().plater()->on_filaments_delete(total_after_delete, filament_id, + final_replace_id, + is_mixed_snapshot); - // wxGetApp().plater()->on_filaments_change(filament_count); - wxGetApp().plater()->on_filaments_delete(filament_count, filament_id, - replace_filament_id > (int) filament_id ? (replace_filament_id - 1) : replace_filament_id); - // wxGetApp().plater()->sidebar().on_filaments_delete(filament_id); wxGetApp().get_tab(Preset::TYPE_PRINT)->update(); wxGetApp().preset_bundle->export_selections(*wxGetApp().app_config); @@ -2547,13 +7662,16 @@ void Sidebar::delete_filament(size_t filament_id, int replace_filament_id) void Sidebar::add_custom_filament(wxColour new_col) { if (p->combos_filament.size() >= MAXIMUM_EXTRUDER_NUMBER) return; + PresetBundle* pb = wxGetApp().preset_bundle; + if (!pb || pb->mixed_filaments.total_filaments(p->combos_filament.size()) >= MAXIMUM_FILAMENT_NUMBER) return; int filament_count = p->combos_filament.size() + 1; + wxGetApp().plater()->confirm_auto_generated_gradients(filament_count); std::string new_color = new_col.GetAsString(wxC2S_HTML_SYNTAX).ToStdString(); - wxGetApp().preset_bundle->set_num_filaments(filament_count, new_color); + pb->set_num_filaments(filament_count, new_color); wxGetApp().plater()->on_filaments_change(filament_count); wxGetApp().get_tab(Preset::TYPE_PRINT)->update(); - wxGetApp().preset_bundle->export_selections(*wxGetApp().app_config); + pb->export_selections(*wxGetApp().app_config); auto_calc_flushing_volumes(filament_count - 1); } @@ -3126,7 +8244,7 @@ void Sidebar::auto_calc_flushing_volumes(const int modify_id) int m_max_flush_volume = Slic3r::g_max_flush_volume; unsigned int m_number_of_extruders = (int)(sqrt(init_matrix.size()) + 0.001); - const std::vector extruder_colours = wxGetApp().plater()->get_extruder_colors_from_plater_config(); + const std::vector extruder_colours = wxGetApp().plater()->get_extruder_colors_from_plater_config(nullptr, false); std::vector> multi_colours; // Support for multi-color filament @@ -3394,9 +8512,13 @@ struct Plater::priv static const std::regex pattern_prusa; bool m_is_dark = false; + size_t m_last_auto_gradient_prompt_physical_count = 0; + bool m_last_auto_gradient_prompt_accepted = false; priv(Plater *q, MainFrame *main_frame); ~priv(); + bool confirm_auto_generated_gradients(wxWindow *parent, size_t num_physical); + void set_auto_generated_gradient_decision(size_t num_physical, bool create_auto_gradients); bool need_update() const { return m_need_update; } @@ -3867,10 +8989,10 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame) "extruder_colour", "filament_colour", "material_colour", "printable_height", "printer_model", "printer_technology", // These values are necessary to construct SlicingParameters by the Canvas3D variable layer height editor. "layer_height", "initial_layer_print_height", "min_layer_height", "max_layer_height", - "brim_width", "wall_loops", "wall_filament", "sparse_infill_density", "sparse_infill_filament", "top_shell_layers", + "brim_width", "wall_loops", "wall_filament", "sparse_infill_density", "enable_infill_filament_override", "infill_filament_use_base_first_layers", "infill_filament_use_base_last_layers", "sparse_infill_filament", "top_shell_layers", "enable_support", "support_filament", "support_interface_filament", "support_top_z_distance", "support_bottom_z_distance", "raft_layers", - "wipe_tower_rotation_angle", "wipe_tower_cone_angle", "wipe_tower_extra_spacing", "wipe_tower_extra_flow", "wipe_tower_max_purge_speed", + "wipe_tower_rotation_angle", "wipe_tower_cone_angle", "wipe_tower_extra_spacing", "wipe_tower_extra_flow", "local_z_wipe_tower_purge_lines", "wipe_tower_max_purge_speed", "wipe_tower_wall_type", "wipe_tower_extra_rib_length","wipe_tower_rib_width","wipe_tower_fillet_wall", "wipe_tower_filament", "best_object_pos" @@ -4849,6 +9971,32 @@ std::vector Plater::priv::load_files(const std::vector& input_ << boost::format(", plate_data.size %1%, project_preset.size %2%, is_bbs_3mf %3%, file_version %4% \n") % plate_data.size() % project_presets.size() % (en_3mf_file_type == En3mfType::From_BBS) % file_version.to_string(); + auto imported_string_count = [&config_loaded](const char *key) -> size_t { + if (const auto *opt = config_loaded.option(key)) + return opt->values.size(); + return 0; + }; + auto imported_float_count = [&config_loaded](const char *key) -> size_t { + if (const auto *opt = config_loaded.option(key)) + return opt->values.size(); + return 0; + }; + + std::vector imported_filament_colors; + size_t imported_physical_filaments = 0; + if (const auto *filament_colors_opt = config_loaded.option("filament_colour")) { + imported_filament_colors = filament_colors_opt->values; + imported_physical_filaments = imported_filament_colors.size(); + } + if (imported_physical_filaments == 0) + imported_physical_filaments = imported_string_count("filament_settings_id"); + if (imported_physical_filaments == 0) + imported_physical_filaments = imported_string_count("filament_ids"); + if (imported_physical_filaments == 0) + imported_physical_filaments = imported_string_count("default_filament_colour"); + if (imported_physical_filaments == 0) + imported_physical_filaments = imported_float_count("nozzle_diameter"); + // 1. add extruder for prusa model if the number of existing extruders is not enough // 2. add extruder for BBS or Other model if only import geometry if (en_3mf_file_type == En3mfType::From_Prusa || (load_model && !load_config)) { @@ -4861,10 +10009,66 @@ std::vector Plater::priv::load_files(const std::vector& input_ } } int size = extruderIds.size() == 0 ? 0 : *(extruderIds.rbegin()); + const bool geometry_only_project_import = load_model && !load_config && imported_physical_filaments > 0; + const size_t desired_physical_filaments = geometry_only_project_import ? + std::min({imported_physical_filaments, size_t(MAXIMUM_EXTRUDER_NUMBER), size_t(MAXIMUM_FILAMENT_NUMBER)}) : 0; + BOOST_LOG_TRIVIAL(info) << "3MF geometry import filament detection" + << " imported_physical=" << imported_physical_filaments + << " imported_colors=" << imported_filament_colors.size() + << " config_filament_settings_id=" << imported_string_count("filament_settings_id") + << " config_filament_ids=" << imported_string_count("filament_ids") + << " config_default_filament_colour=" << imported_string_count("default_filament_colour") + << " config_nozzle_diameter=" << imported_float_count("nozzle_diameter") + << " model_max_extruder=" << size + << " geometry_only_project_import=" << (geometry_only_project_import ? 1 : 0); + if (geometry_only_project_import) + size = int(desired_physical_filaments); + + PresetBundle *preset_bundle = wxGetApp().preset_bundle; + if (geometry_only_project_import && preset_bundle != nullptr) { + const size_t current_num_filaments = preset_bundle->filament_presets.size(); + const bool current_project_empty = this->model.objects.empty(); + if (current_project_empty) { + static const t_config_option_keys imported_project_option_keys = { + "filament_colour", + "mixed_filament_definitions", + "mixed_filament_gradient_mode", + "mixed_filament_height_lower_bound", + "mixed_filament_height_upper_bound", + "mixed_filament_advanced_dithering", + "mixed_filament_pointillism_pixel_size", + "mixed_filament_pointillism_line_gap", + "mixed_filament_component_bias_enabled", + "mixed_filament_surface_indentation" + }; + preset_bundle->project_config.apply_only(config_loaded, imported_project_option_keys, true); + if (current_num_filaments != desired_physical_filaments) { + q->confirm_auto_generated_gradients(desired_physical_filaments); + preset_bundle->set_num_filaments(unsigned(desired_physical_filaments)); + } else + preset_bundle->update_multi_material_filament_presets(); + BOOST_LOG_TRIVIAL(info) << "3MF geometry import applied imported project config" + << " current_num_filaments=" << current_num_filaments + << " desired_physical_filaments=" << desired_physical_filaments + << " mixed_enabled=" << preset_bundle->mixed_filaments.enabled_count(); + wxGetApp().plater()->on_filaments_change(desired_physical_filaments); + } else if (current_num_filaments < desired_physical_filaments) { + std::vector new_colors; + if (imported_filament_colors.size() > current_num_filaments) { + new_colors.assign(imported_filament_colors.begin() + current_num_filaments, + imported_filament_colors.begin() + desired_physical_filaments); + } + q->confirm_auto_generated_gradients(desired_physical_filaments); + preset_bundle->set_num_filaments(unsigned(desired_physical_filaments), new_colors); + wxGetApp().plater()->on_filaments_change(desired_physical_filaments); + } + } int filament_size = sidebar->combos_filament().size(); - while (filament_size < MAXIMUM_EXTRUDER_NUMBER && filament_size < size) { + const int filament_limit = std::min({size, int(MAXIMUM_EXTRUDER_NUMBER), int(MAXIMUM_FILAMENT_NUMBER)}); + while (filament_size < filament_limit) { int filament_count = filament_size + 1; + wxGetApp().plater()->confirm_auto_generated_gradients(filament_count); wxColour new_col = Plater::get_next_color_for_filament(); std::string new_color = new_col.GetAsString(wxC2S_HTML_SYNTAX).ToStdString(); wxGetApp().preset_bundle->set_num_filaments(filament_count, new_color); @@ -5204,6 +10408,10 @@ std::vector Plater::priv::load_files(const std::vector& input_ // BBS: add preset combo box re-active logic // currently found only needs re-active here wxGetApp().load_current_presets(false, false); + // Some preset-tab refresh paths rebuild printer/filament UI from the + // active presets but do not preserve the mixed manager instance. + // Rebuild it explicitly from project_config before clamping object IDs. + preset_bundle->update_multi_material_filament_presets(); // Update filament colors for the MM-printer profile in the full config // to avoid black (default) colors for Extruders in the ObjectList, // when for extruder colors are used filament colors @@ -6028,8 +11236,6 @@ void Plater::priv::object_list_changed() main_frame->update_slice_print_status(MainFrame::eEventObjectUpdate, can_slice); wxGetApp().params_panel()->notify_object_config_changed(); - - q->sync_filament_temp_mixing_notification(); } void Plater::priv::select_curr_plate_all() @@ -6526,18 +11732,21 @@ unsigned int Plater::priv::update_background_process(bool force_validation, bool if (err.string.empty()) { // Validate passed, but also check filament temp mixing as a final - // guard. The Print::validate() extruders() check may miss some - // cases (e.g. wall_filament changes that haven't propagated to - // PrintRegions yet). check_filament_temp_mixing() reads directly - // from full_config() and is always current. + // guard. check_filament_temp_mixing() reads directly from preset + // configs and catches cases that Print::validate() may miss (e.g. + // wall_filament changes that haven't propagated to PrintRegions yet). bool filament_ok = q->sync_filament_temp_mixing_notification(); if (filament_ok) { + this->partplate_list.get_curr_plate()->update_apply_result_invalid(false); notification_manager->set_all_slicing_errors_gray(true); - process_validation_warning(warning); + notification_manager->close_notification_of_type(NotificationType::ValidateError); } if (invalidated != Print::APPLY_STATUS_UNCHANGED && background_processing_enabled()) return_state |= UPDATE_BACKGROUND_PROCESS_RESTART; + // Pass a warning from validation and either show a notification, + // or hide the old one. + process_validation_warning(warning); if (printer_technology == ptFFF) { view3D->get_canvas3d()->reset_sequential_print_clearance(); view3D->get_canvas3d()->set_as_dirty(); @@ -7447,7 +12656,8 @@ void Plater::priv::set_current_panel(wxPanel* panel, bool no_slice) BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": from set_current_panel, no_slice %1%, export_in_progress %2%, model_fits %3%, m_is_slicing %4%")%no_slice%export_in_progress%model_fits%m_is_slicing; - if (!no_slice && !this->model.objects.empty() && !export_in_progress && model_fits && current_has_print_instances) + if (!no_slice && !this->model.objects.empty() && !export_in_progress && model_fits && current_has_print_instances + && !current_plate->is_slice_result_valid()) { //if already running in background, not relice here //BBS: add more judge for slicing @@ -7853,6 +13063,7 @@ void Plater::priv::on_select_preset(wxCommandEvent &evt) wxGetApp().plater()->update_project_dirty_from_presets(); wxGetApp().preset_bundle->export_selections(*wxGetApp().app_config); sidebar->update_dynamic_filament_list(); + sidebar->update_color_mix_panel(); bool flag_is_change = is_support_filament(idx); if (flag != flag_is_change) { sidebar->auto_calc_flushing_volumes(idx); @@ -8434,15 +13645,6 @@ void Plater::priv::on_action_open_project(SimpleEvent&) void Plater::priv::on_action_slice_plate(SimpleEvent&) { if (q != nullptr) { - // Pre-slice confirmation for high/low temp filament mixing - if (wxGetApp().app_config->get_bool("allow_filament_temp_mixing") - && !q->check_filament_temp_mixing()) { - wxMessageDialog dlg(q, _L("该组合可能导致风险,是否继续"), - _L("确认"), wxYES_NO | wxICON_WARNING); - if (dlg.ShowModal() != wxID_YES) - return; - } - BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << ":received slice plate event\n" ; //BBS update extruder params and speed table before slicing const Slic3r::DynamicPrintConfig& config = wxGetApp().preset_bundle->full_config(); @@ -8453,15 +13655,7 @@ void Plater::priv::on_action_slice_plate(SimpleEvent&) Model::setExtruderParams(config, numExtruders); Model::setPrintSpeedTable(config, print_config); m_slice_all = false; - q->reslice(); - q->select_view_3D("Preview"); - } -} -//BBS: GUI refactor: slice all -void Plater::priv::on_action_slice_all(SimpleEvent&) -{ - if (q != nullptr) { // Pre-slice confirmation for high/low temp filament mixing if (wxGetApp().app_config->get_bool("allow_filament_temp_mixing") && !q->check_filament_temp_mixing()) { @@ -8471,6 +13665,15 @@ void Plater::priv::on_action_slice_all(SimpleEvent&) return; } + q->reslice(); + q->select_view_3D("Preview"); + } +} + +//BBS: GUI refactor: slice all +void Plater::priv::on_action_slice_all(SimpleEvent&) +{ + if (q != nullptr) { BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << ":received slice project event\n" ; //BBS update extruder params and speed table before slicing const Slic3r::DynamicPrintConfig& config = wxGetApp().preset_bundle->full_config(); @@ -8480,6 +13683,16 @@ void Plater::priv::on_action_slice_all(SimpleEvent&) Model::setExtruderParams(config, numExtruders); Model::setPrintSpeedTable(config, print_config); + + // Pre-slice confirmation for high/low temp filament mixing + if (wxGetApp().app_config->get_bool("allow_filament_temp_mixing") + && !q->check_filament_temp_mixing()) { + wxMessageDialog dlg(q, _L("该组合可能导致风险,是否继续"), + _L("确认"), wxYES_NO | wxICON_WARNING); + if (dlg.ShowModal() != wxID_YES) + return; + } + m_slice_all = true; m_slice_all_only_has_gcode = true; m_cur_slice_plate = 0; @@ -8744,6 +13957,76 @@ void Plater::priv::on_repair_model(wxCommandEvent &event) wxGetApp().obj_list()->fix_through_netfabb(); } +bool Plater::priv::confirm_auto_generated_gradients(wxWindow *parent, size_t num_physical) +{ + auto *app_config = wxGetApp().app_config; + if (app_config == nullptr) + return MixedFilamentManager::auto_generate_enabled(); + + const bool pref_enabled = app_config->get_bool("auto_generate_gradients"); + if (!pref_enabled) { + m_last_auto_gradient_prompt_physical_count = 0; + m_last_auto_gradient_prompt_accepted = false; + MixedFilamentManager::set_auto_generate_enabled(false); + return false; + } + + if (num_physical <= 4) { + m_last_auto_gradient_prompt_physical_count = 0; + m_last_auto_gradient_prompt_accepted = false; + MixedFilamentManager::set_auto_generate_enabled(true); + return true; + } + + if (parent == nullptr || !parent->IsShownOnScreen()) { + m_last_auto_gradient_prompt_physical_count = 0; + m_last_auto_gradient_prompt_accepted = false; + MixedFilamentManager::set_auto_generate_enabled(true); + return true; + } + + if (m_last_auto_gradient_prompt_physical_count == num_physical) { + MixedFilamentManager::set_auto_generate_enabled(m_last_auto_gradient_prompt_accepted); + return m_last_auto_gradient_prompt_accepted; + } + + const size_t auto_gradient_count = num_physical * (num_physical - 1) / 2; + const wxString message = wxString::Format( + _L("Using %d physical filaments will create %d auto-generated gradients.\nDo you want to create them now?"), + int(num_physical), + int(auto_gradient_count)); + const int result = MessageDialog(parent, + message, + wxString(SLIC3R_APP_FULL_NAME) + " - " + _L("Auto gradients"), + wxYES_NO | wxYES_DEFAULT | wxCENTRE | wxICON_QUESTION) + .ShowModal(); + const bool accepted = result == wxID_YES; + m_last_auto_gradient_prompt_physical_count = num_physical; + m_last_auto_gradient_prompt_accepted = accepted; + MixedFilamentManager::set_auto_generate_enabled(accepted); + return accepted; +} + +void Plater::priv::set_auto_generated_gradient_decision(size_t num_physical, bool create_auto_gradients) +{ + m_last_auto_gradient_prompt_physical_count = num_physical; + m_last_auto_gradient_prompt_accepted = create_auto_gradients; + MixedFilamentManager::set_auto_generate_enabled(create_auto_gradients); +} + +bool Plater::confirm_auto_generated_gradients(size_t num_physical) +{ + return p != nullptr ? p->confirm_auto_generated_gradients(this, num_physical) : MixedFilamentManager::auto_generate_enabled(); +} + +void Plater::set_auto_generated_gradient_decision(size_t num_physical, bool create_auto_gradients) +{ + if (p != nullptr) + p->set_auto_generated_gradient_decision(num_physical, create_auto_gradients); + else + MixedFilamentManager::set_auto_generate_enabled(create_auto_gradients); +} + void Plater::priv::on_filament_color_changed(wxCommandEvent &event) { //q->update_all_plate_thumbnails(true); @@ -8757,6 +14040,13 @@ void Plater::priv::on_filament_color_changed(wxCommandEvent &event) if (wxGetApp().app_config->get("auto_calculate") == "true") { sidebar->auto_calc_flushing_volumes(modify_id); } + + // Regenerate mixed filaments and refresh the mixed panel only. Color + // changes do not alter filament IDs, so the full on_filaments_change() + // path is unnecessary and can re-enter UI rebuilds mid-update. + wxGetApp().preset_bundle->update_multi_material_filament_presets(); + sidebar->update_mixed_filament_panel(); + sidebar->update_color_mix_panel(); } void Plater::priv::install_network_plugin(wxCommandEvent &event) @@ -8828,6 +14118,9 @@ void Plater::priv::apply_color_mode() m_aui_mgr.GetArtProvider()->SetColour(wxAUI_DOCKART_INACTIVE_CAPTION_TEXT_COLOUR, *wxWHITE); m_aui_mgr.GetArtProvider()->SetColour(wxAUI_DOCKART_SASH_COLOUR, sash_color); m_aui_mgr.GetArtProvider()->SetColour(wxAUI_DOCKART_BORDER_COLOUR, is_dark ? *wxBLACK : wxColour(165, 165, 165)); + + if (sidebar) + sidebar->update_color_mix_panel(); } static void get_position(wxWindowBase* child, wxWindowBase* until_parent, int& x, int& y) { @@ -14095,6 +19388,38 @@ void Plater::send_gcode_legacy(int plate_idx, Export3mfProgressFn proFn, bool us // if physical_printer is selected, send gcode for this printer // DynamicPrintConfig* physical_printer_config = wxGetApp().preset_bundle->physical_printers.get_selected_printer_config(); + auto prepare_upload_filename_for_dialog = [this, use_3mf](fs::path output_file) { + output_file = fs::path(Slic3r::fold_utf8_to_ascii(output_file.string())); + if (use_3mf) + output_file.replace_extension("3mf"); + + PartPlate *current_plate = this->get_partplate_list().get_curr_plate(); + if (current_plate != nullptr) { + const Print *current_print = current_plate->fff_print(); + if (current_print != nullptr && !current_print->print_statistics().estimated_normal_print_time.empty()) + return fs::path(current_print->print_statistics().finalize_output_path(output_file.string())); + } + + if (current_plate != nullptr && current_plate->is_slice_result_valid() && current_plate->get_slice_result() != nullptr) { + const auto &estimated_stats = current_plate->get_slice_result()->print_statistics; + const float normal_time = estimated_stats.modes[static_cast(PrintEstimatedStatistics::ETimeMode::Normal)].time; + if (normal_time > 0.0f) { + std::string filename = output_file.string(); + const std::string normal_time_str = short_time(get_time_dhms(normal_time)); + boost::replace_all(filename, "{print_time}", normal_time_str); + boost::replace_all(filename, "{normal_print_time}", normal_time_str); + + const float silent_time = estimated_stats.modes[static_cast(PrintEstimatedStatistics::ETimeMode::Stealth)].time; + if (silent_time > 0.0f) + boost::replace_all(filename, "{silent_print_time}", short_time(get_time_dhms(silent_time))); + + output_file = fs::path(filename); + } + } + + return output_file; + }; + // 校验机型 auto devices = wxGetApp().app_config->get_devices(); std::string connect_preset = ""; @@ -14159,10 +19484,7 @@ void Plater::send_gcode_legacy(int plate_idx, Export3mfProgressFn proFn, bool us show_error(this, ex.what(), false); return; } - default_output_file = fs::path(Slic3r::fold_utf8_to_ascii(default_output_file.string())); - if (use_3mf) { - default_output_file.replace_extension("3mf"); - } + default_output_file = prepare_upload_filename_for_dialog(std::move(default_output_file)); // get file path auto file_path = get_partplate_list().get_curr_plate()->get_tmp_gcode_path(); @@ -14233,10 +19555,7 @@ void Plater::send_gcode_legacy(int plate_idx, Export3mfProgressFn proFn, bool us show_error(this, ex.what(), false); return; } - default_output_file = fs::path(Slic3r::fold_utf8_to_ascii(default_output_file.string())); - if (use_3mf) { - default_output_file.replace_extension("3mf"); - } + default_output_file = prepare_upload_filename_for_dialog(std::move(default_output_file)); // Repetier specific: Query the server for the list of file groups. wxArrayString groups; @@ -14514,19 +19833,58 @@ void Plater::on_filaments_delete(size_t num_filaments, size_t filament_id, int r part_plate->update_first_layer_print_sequence_when_delete_filament(filament_id); }*/ - // update mmu info + // Consume remap before updating volumes + // This is used when merging mixed filaments to properly remap object filament IDs + PresetBundle *preset_bundle = wxGetApp().preset_bundle; + std::vector id_remap; + if (preset_bundle != nullptr) + id_remap = preset_bundle->consume_last_filament_id_remap(); + + // Build state map for remap if available. + // Use the remap for both pure-delete and merge paths so that mixed + // filaments deleted by remove_physical_filament are correctly mapped + // to NONE instead of being shifted onto wrong IDs. + EnforcerBlockerStateMap state_map; + bool should_remap_states = false; + if (!id_remap.empty()) { + should_remap_states = true; + if (replace_filament_id >= 0) { + // Merge: inject the merge target into the remap so the deleted + // physical filament maps to the target instead of 0. + size_t old_1based = filament_id + 1; + size_t new_1based = replace_filament_id + 1; + if (old_1based < id_remap.size()) + id_remap[old_1based] = (unsigned int)new_1based; + } + for (size_t i = 0; i < state_map.size(); ++i) + state_map[i] = EnforcerBlockerType(i); + for (size_t i = 1; i < state_map.size(); ++i) { + const unsigned int mapped = i < id_remap.size() ? id_remap[i] : 0; + if (mapped == 0 || mapped >= state_map.size() || mapped > num_filaments) + state_map[i] = EnforcerBlockerType::NONE; + else + state_map[i] = EnforcerBlockerType(mapped); + } + } + + // update mmu paint data for (ModelObject* mo : wxGetApp().model().objects) { for (ModelVolume* mv : mo->volumes) { - mv->update_extruder_count_when_delete_filament(num_filaments, filament_id + 1, - replace_filament_id + 1); // this function is 1 base + if (should_remap_states) { + mv->remap_extruder_ids(num_filaments, state_map); + } else { + mv->update_extruder_count_when_delete_filament(num_filaments, filament_id + 1, + replace_filament_id + 1); // this function is 1 base + } } } // update UI sidebar().on_filaments_delete(filament_id); - // update global support filament - static const char* keys[] = {"support_filament", "support_interface_filament"}; + // update global feature filament selections + static const char* keys[] = {"wall_filament", "sparse_infill_filament", "solid_infill_filament", + "support_filament", "support_interface_filament"}; for (auto key : keys) if (p->config->has(key)) { if (p->config->opt_int(key) == filament_id + 1) @@ -14564,6 +19922,132 @@ void Plater::on_filaments_change(size_t num_filaments) { // only update elements in plater update_filament_colors_in_full_config(); + + const size_t old_num_filaments = sidebar().combos_filament().size(); + const bool auto_generate_before = MixedFilamentManager::auto_generate_enabled(); + const bool allow_auto_gradients = p->confirm_auto_generated_gradients(this, num_filaments); + auto summarize_uint_vector = [](const std::vector &values, size_t max_items = 24) { + std::string out = "["; + const size_t n = std::min(values.size(), max_items); + for (size_t i = 0; i < n; ++i) { + if (i > 0) + out += ","; + out += std::to_string(values[i]); + } + if (values.size() > n) + out += ",..."; + out += "]"; + return out; + }; + auto summarize_used_states = [](const std::vector &used, size_t max_items = 24) { + std::string out = "["; + size_t total = 0; + size_t emitted = 0; + for (size_t i = 1; i < used.size(); ++i) { + if (!used[i]) + continue; + ++total; + if (emitted < max_items) { + if (emitted > 0) + out += ","; + out += std::to_string(i); + ++emitted; + } + } + if (total > emitted) + out += ",..."; + out += "] total=" + std::to_string(total); + return out; + }; + PresetBundle *preset_bundle = wxGetApp().preset_bundle; + if (preset_bundle != nullptr && auto_generate_before && !allow_auto_gradients) + preset_bundle->update_multi_material_filament_presets(size_t(-1), old_num_filaments); + // Consume remap before sidebar refresh, which may trigger config sync + // paths that regenerate mixed filaments and clear this remap buffer. + std::vector id_remap; + if (preset_bundle != nullptr) + id_remap = preset_bundle->consume_last_filament_id_remap(); + + size_t total_filaments = num_filaments; + if (preset_bundle != nullptr) + total_filaments = preset_bundle->mixed_filaments.total_filaments(num_filaments); + + EnforcerBlockerStateMap state_map; + for (size_t i = 0; i < state_map.size(); ++i) + state_map[i] = EnforcerBlockerType(i); + + bool have_explicit_remap = false; + bool should_remap_states = false; + if (!id_remap.empty()) { + have_explicit_remap = true; + should_remap_states = true; + for (size_t i = 1; i < state_map.size(); ++i) { + const unsigned int mapped = i < id_remap.size() ? id_remap[i] : 0; + if (mapped == 0 || mapped >= state_map.size() || mapped > total_filaments) + state_map[i] = EnforcerBlockerType::NONE; + else + state_map[i] = EnforcerBlockerType(mapped); + } + } + + size_t changed_entries = 0; + std::string changed_map_preview = "["; + for (size_t i = 1; i < state_map.size(); ++i) { + const unsigned int mapped = unsigned(state_map[i]); + if (mapped == i) + continue; + ++changed_entries; + if (changed_entries <= 24) { + if (changed_entries > 1) + changed_map_preview += ","; + changed_map_preview += std::to_string(i) + "->" + std::to_string(mapped); + } + } + if (changed_entries > 24) + changed_map_preview += ",..."; + changed_map_preview += "]"; + BOOST_LOG_TRIVIAL(warning) << "MF_REMAP on_filaments_change" + << " old_physical=" << old_num_filaments + << " new_physical=" << num_filaments + << " total_filaments=" << total_filaments + << " id_remap_size=" << id_remap.size() + << " id_remap=" << summarize_uint_vector(id_remap) + << " explicit_remap=" << (have_explicit_remap ? 1 : 0) + << " should_remap_states=" << (should_remap_states ? 1 : 0) + << " changed_entries=" << changed_entries + << " changed_map=" << changed_map_preview; + + size_t obj_idx = 0; + for (ModelObject* mo : wxGetApp().model().objects) { + size_t vol_idx = 0; + for (ModelVolume* mv : mo->volumes) { + std::string used_before; + const bool has_mmu_paint = (mv != nullptr && !mv->mmu_segmentation_facets.empty()); + if (has_mmu_paint) + used_before = summarize_used_states(mv->mmu_segmentation_facets.get_data().used_states); + + if (should_remap_states) + mv->remap_extruder_ids(total_filaments, state_map); + else + mv->update_extruder_count(total_filaments); + + if (has_mmu_paint) { + const std::string used_after = summarize_used_states(mv->mmu_segmentation_facets.get_data().used_states); + BOOST_LOG_TRIVIAL(warning) << "MF_REMAP volume" + << " obj_idx=" << obj_idx + << " vol_idx=" << vol_idx + << " obj_name=" << mo->name + << " vol_name=" << mv->name + << " before=" << used_before + << " after=" << used_after; + } + ++vol_idx; + } + ++obj_idx; + } + + // Keep UI refresh after model remap. Some UI update paths may trigger + // scene/model sync that assumes already-remapped MMU state. sidebar().on_filaments_change(num_filaments); sidebar().obj_list()->update_objects_list_filament_column(num_filaments); @@ -14572,12 +20056,6 @@ void Plater::on_filaments_change(size_t num_filaments) PartPlate* part_plate = plate_list.get_plate(i); part_plate->update_first_layer_print_sequence(num_filaments); } - - for (ModelObject* mo : wxGetApp().model().objects) { - for (ModelVolume* mv : mo->volumes) { - mv->update_extruder_count(num_filaments); - } - } } void Plater::on_bed_type_change(BedType bed_type) @@ -14622,7 +20100,6 @@ bool Plater::check_filament_temp_mixing() int num_filaments = (int)ft_opt->values.size(); std::set used_slots; - // Helper: collect used filament slots from a single config auto collect_from_cfg = [&](const DynamicPrintConfig& cfg) { static const char* keys_1based[] = {"wall_filament", "solid_infill_filament"}; for (auto key : keys_1based) { @@ -14794,6 +20271,9 @@ void Plater::on_config_change(const DynamicPrintConfig &config) } // Orca: update when *_filament changed else if (opt_key == "support_interface_filament" || opt_key == "support_filament" || opt_key == "wall_filament" || + opt_key == "enable_infill_filament_override" || + opt_key == "infill_filament_use_base_first_layers" || + opt_key == "infill_filament_use_base_last_layers" || opt_key == "sparse_infill_filament" || opt_key == "solid_infill_filament") { update_scheduled = true; } @@ -14812,7 +20292,6 @@ void Plater::on_config_change(const DynamicPrintConfig &config) update_title_dirty_status(); } - // Real-time high/low temperature filament mixing check sync_filament_temp_mixing_notification(); } @@ -14896,12 +20375,25 @@ std::vector Plater::get_extruder_colors_from_plater_config(const GC if (wxGetApp().is_gcode_viewer() && result != nullptr) return result->extruder_colors; else { + if (wxGetApp().preset_bundle == nullptr) + return {}; + const Slic3r::DynamicPrintConfig* config = &wxGetApp().preset_bundle->project_config; std::vector filament_colors; if (!config->has("filament_colour")) // in case of a SLA print return filament_colors; filament_colors = (config->option("filament_colour"))->values; + const size_t num_physical = static_cast(std::max(wxGetApp().filaments_cnt(), 0)); + filament_colors.resize(num_physical, "#26A69A"); + + if (include_mixed) { + // Append display colours for enabled mixed (virtual) filaments. + const auto &mixed_mgr = wxGetApp().preset_bundle->mixed_filaments; + for (const auto &dc : mixed_mgr.display_colors()) + filament_colors.push_back(dc); + } + return filament_colors; } } @@ -16150,6 +21642,20 @@ void Plater::reset_gcode_toolpaths() p->reset_gcode_toolpaths(); } +void Plater::post_slice_state_change_update() +{ + p->partplate_list.invalid_all_slice_result(); + reset_gcode_toolpaths(); + wxGetApp().mainframe->update_slice_print_status(MainFrame::SlicePrintEventType::eEventSliceUpdate, true, false); + CallAfter([this]() { + GLCanvas3D* canvas = get_current_canvas3D(); + if (canvas) { + canvas->set_as_dirty(); + canvas->request_extra_frame(); + } + }); +} + const Mouse3DController& Plater::get_mouse3d_controller() const { return p->mouse3d_controller; From 83b44fbc405cab978788aed50e31b556d277e62c Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Mon, 8 Jun 2026 18:57:27 +0800 Subject: [PATCH 040/135] Fix filament temperature mixing notifications --- src/libslic3r/Print.cpp | 11 -- src/slic3r/GUI/GLCanvas3D.cpp | 2 +- src/slic3r/GUI/GUI_ObjectList.cpp | 6 +- .../GUI/Gizmos/GLGizmoMmuSegmentation.cpp | 2 +- src/slic3r/GUI/NotificationManager.cpp | 19 +++ src/slic3r/GUI/NotificationManager.hpp | 5 + src/slic3r/GUI/PartPlate.cpp | 2 +- src/slic3r/GUI/Plater.cpp | 158 ++++++++++++------ src/slic3r/GUI/Plater.hpp | 12 +- src/slic3r/GUI/Preferences.cpp | 3 + src/slic3r/GUI/Tab.cpp | 3 +- 11 files changed, 152 insertions(+), 71 deletions(-) diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index 1e2148afcc4..354bdc34918 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -1556,17 +1556,6 @@ StringObjectException Print::validate(StringObjectException *warning, Polygons* if (extruders.empty()) return { L("No extrusions under current settings.") }; - // Check for high/low temperature filament mixing. The check uses - // filament_is_high_temperature from each filament preset. This guard - // works for both single-nozzle and multi-nozzle printers. - if (extruders.size() > 1) { - auto ret = check_multi_filament_valid(*this); - if (!ret.string.empty()) { - ret.type = STRING_EXCEPT_FILAMENTS_DIFFERENT_TEMP; - return ret; - } - } - if (m_config.print_sequence == PrintSequence::ByObject) { if (m_config.timelapse_type == TimelapseType::tlSmooth) return {L("Smooth mode of timelapse is not supported when \"by object\" sequence is enabled.")}; diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp index 8538040aeff..bb63deaefbb 100644 --- a/src/slic3r/GUI/GLCanvas3D.cpp +++ b/src/slic3r/GUI/GLCanvas3D.cpp @@ -4823,7 +4823,7 @@ void GLCanvas3D::do_move(const std::string& snapshot_type) reset_sequential_print_clearance(); - wxGetApp().plater()->sync_filament_temp_mixing_notification(); + wxGetApp().plater()->notify_filament_usage_changed(); m_dirty = true; } diff --git a/src/slic3r/GUI/GUI_ObjectList.cpp b/src/slic3r/GUI/GUI_ObjectList.cpp index ea2064334f0..59073f2de0c 100644 --- a/src/slic3r/GUI/GUI_ObjectList.cpp +++ b/src/slic3r/GUI/GUI_ObjectList.cpp @@ -1100,7 +1100,7 @@ void ObjectList::update_filament_in_config(const wxDataViewItem& item) // update scene wxGetApp().plater()->update(); - wxGetApp().plater()->sync_filament_temp_mixing_notification(); + wxGetApp().plater()->notify_filament_usage_changed(); } void ObjectList::update_name_in_model(const wxDataViewItem& item) const @@ -5923,7 +5923,7 @@ void ObjectList::set_extruder_for_selected_items(const int extruder) // update scene wxGetApp().plater()->update(); - wxGetApp().plater()->sync_filament_temp_mixing_notification(); + wxGetApp().plater()->notify_filament_usage_changed(); // BBS: update extruder/filament column Refresh(); @@ -5983,7 +5983,7 @@ void ObjectList::reload_all_plates(bool notify_partplate) wxGetApp().plater()->update(); // update printable states on canvas wxGetApp().plater()->get_view3D_canvas3D()->update_instance_printable_state_for_objects(obj_idxs); - wxGetApp().plater()->sync_filament_temp_mixing_notification(); + wxGetApp().plater()->notify_filament_usage_changed(); } void ObjectList::on_plate_selected(int plate_index) diff --git a/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp b/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp index 0435434a4e9..82aba7c88cc 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp @@ -1000,7 +1000,7 @@ void GLGizmoMmuSegmentation::update_model_object() wxGetApp().obj_list()->update_info_items(obj_idx); wxGetApp().plater()->get_partplate_list().notify_instance_update(obj_idx, 0); m_parent.post_event(SimpleEvent(EVT_GLCANVAS_SCHEDULE_BACKGROUND_PROCESS)); - wxGetApp().plater()->sync_filament_temp_mixing_notification(); + wxGetApp().plater()->notify_filament_usage_changed(); } } diff --git a/src/slic3r/GUI/NotificationManager.cpp b/src/slic3r/GUI/NotificationManager.cpp index 0ff7d4d3d18..f2be434c92b 100644 --- a/src/slic3r/GUI/NotificationManager.cpp +++ b/src/slic3r/GUI/NotificationManager.cpp @@ -1836,6 +1836,16 @@ void NotificationManager::push_validate_error_notification(StringObjectException set_slicing_progress_hidden(); } +void NotificationManager::close_validate_error_notification(const std::string& text) +{ + close_notification_of_type_and_text(NotificationType::ValidateError, _u8L("Error:") + "\n" + text); +} + +void NotificationManager::close_validate_warning_notification(const std::string& text) +{ + close_notification_of_type_and_text(NotificationType::ValidateWarning, _u8L("WARNING:") + "\n" + text); +} + void NotificationManager::push_slicing_error_notification(const std::string &text, std::vector objs) { std::vector ids; @@ -2003,6 +2013,15 @@ void NotificationManager::close_notification_of_type(const NotificationType type } } } + +void NotificationManager::close_notification_of_type_and_text(const NotificationType type, const std::string& text) +{ + for (std::unique_ptr& notification : m_pop_notifications) { + if (notification->get_type() == type && notification->compare_text(text)) + notification->close(); + } +} + void NotificationManager::remove_slicing_warnings_of_released_objects(const std::vector& living_oids) { for (std::unique_ptr ¬ification : m_pop_notifications) diff --git a/src/slic3r/GUI/NotificationManager.hpp b/src/slic3r/GUI/NotificationManager.hpp index dce17f6080c..27c51f7cd5a 100644 --- a/src/slic3r/GUI/NotificationManager.hpp +++ b/src/slic3r/GUI/NotificationManager.hpp @@ -199,6 +199,8 @@ class NotificationManager void stop_delayed_notifications_of_type(const NotificationType type); // Creates Validate Error notification with a custom text and no fade out. void push_validate_error_notification(StringObjectException const & error); + void close_validate_error_notification(const std::string& text); + void close_validate_warning_notification(const std::string& text); // print host upload void push_upload_job_notification(int id, float filesize, const std::string& filename, const std::string& host, float percentage = 0); void set_upload_job_notification_percentage(int id, const std::string& filename, const std::string& host, float percentage); @@ -844,6 +846,7 @@ class NotificationManager //can be used to create custom notification bool push_notification_data(const NotificationData& notification_data, int timestamp); bool push_notification_data(std::unique_ptr notification, int timestamp); + void close_notification_of_type_and_text(const NotificationType type, const std::string& text); // Delayed notifications goes first to the m_waiting_notifications vector and only after remaining time is <= 0 // and condition callback is success, notification is regular pushed from update function. // Otherwise another delay interval waiting. Timestamp is 0. @@ -898,6 +901,8 @@ class NotificationManager NotificationType::PlaterWarning, NotificationType::ProgressBar, NotificationType::PrintHostUpload, + NotificationType::ValidateError, + NotificationType::ValidateWarning, NotificationType::SimplifySuggestion }; //prepared (basic) notifications diff --git a/src/slic3r/GUI/PartPlate.cpp b/src/slic3r/GUI/PartPlate.cpp index 2006285a2cb..0a5a0b444da 100644 --- a/src/slic3r/GUI/PartPlate.cpp +++ b/src/slic3r/GUI/PartPlate.cpp @@ -4556,7 +4556,7 @@ int PartPlateList::add_to_plate(int obj_id, int instance_id, int plate_id) } ret = plate->add_instance(obj_id, instance_id, true); - wxGetApp().plater()->sync_filament_temp_mixing_notification(); + wxGetApp().plater()->notify_filament_usage_changed(); return ret; } diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index c122e09b77c..197de29938f 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -201,6 +201,16 @@ static const std::pair THUMBNAIL_SIZE_3MF = { 512, 5 namespace Slic3r { namespace GUI { +static std::string filament_temp_mixing_warning_text() +{ + return _u8L("Detected both high and low temperature materials. Mixed printing may result in extruder clogging, nozzle damage, or layer adhesion issues."); +} + +static std::string filament_temp_mixing_error_text() +{ + return _u8L("Detected both high and low temperature materials. Mixed printing may result in extruder clogging, nozzle damage, or layer adhesion issues. To continue printing, enable \"Allow mixed printing of high and low temperature materials\" in Preferences."); +} + wxDEFINE_EVENT(EVT_SCHEDULE_BACKGROUND_PROCESS, SimpleEvent); wxDEFINE_EVENT(EVT_SLICING_UPDATE, SlicingStatusEvent); wxDEFINE_EVENT(EVT_SLICING_COMPLETED, wxCommandEvent); @@ -231,6 +241,7 @@ wxDEFINE_EVENT(EVT_PRINT_FROM_SDCARD_VIEW, SimpleEvent); wxDEFINE_EVENT(EVT_CREATE_FILAMENT, SimpleEvent); wxDEFINE_EVENT(EVT_MODIFY_FILAMENT, SimpleEvent); +wxDEFINE_EVENT(EVT_FILAMENT_USAGE_CHANGED, SimpleEvent); wxDEFINE_EVENT(EVT_ADD_FILAMENT, SimpleEvent); wxDEFINE_EVENT(EVT_DEL_FILAMENT, SimpleEvent); wxDEFINE_EVENT(EVT_ADD_CUSTOM_FILAMENT, ColorEvent); @@ -2869,8 +2880,7 @@ void Sidebar::update_presets(Preset::Type preset_type) update_dynamic_filament_list(); - // Real-time high/low temperature filament mixing check after filament preset change - p->plater->sync_filament_temp_mixing_notification(); + p->plater->notify_filament_usage_changed(); break; } @@ -8409,6 +8419,7 @@ struct Plater::priv // PIMPL back pointer ("Q-Pointer") Plater *q; MainFrame *main_frame; + bool filament_usage_sync_pending = false; MenuFactory menus; @@ -9057,6 +9068,10 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame) this->q->Bind(EVT_CREATE_FILAMENT, &priv::on_create_filament, this); this->q->Bind(EVT_MODIFY_FILAMENT, &priv::on_modify_filament, this); this->q->Bind(EVT_ADD_CUSTOM_FILAMENT, &priv::on_add_custom_filament, this); + this->q->Bind(EVT_FILAMENT_USAGE_CHANGED, [this](SimpleEvent&) { + filament_usage_sync_pending = false; + this->q->sync_filament_temp_mixing_notification(); + }); main_frame->m_tabpanel->Bind(wxEVT_NOTEBOOK_PAGE_CHANGING, &priv::on_tab_selection_changing, this); auto* panel_3d = new wxPanel(q); @@ -11250,7 +11265,7 @@ void Plater::priv::remove_curr_plate_all() view3D->remove_curr_plate_all(); this->sidebar->obj_list()->update_selections(); - q->sync_filament_temp_mixing_notification(); + q->notify_filament_usage_changed(); } void Plater::priv::select_all() @@ -11282,6 +11297,7 @@ void Plater::priv::remove(size_t obj_idx) // Delete object from Sidebar list. Do it after update, so that the GLScene selection is updated with the modified model. sidebar->obj_list()->delete_object_from_list(obj_idx); object_list_changed(); + q->notify_filament_usage_changed(); } @@ -11320,6 +11336,8 @@ bool Plater::priv::delete_object_from_model(size_t obj_idx, bool refresh_immedia object_list_changed(); } + q->notify_filament_usage_changed(); + return true; } @@ -11352,6 +11370,7 @@ void Plater::priv::delete_all_objects_from_model() //BBS model.calib_pa_pattern.reset(); model.plates_custom_gcodes.clear(); + q->notify_filament_usage_changed(); } void Plater::priv::reset(bool apply_presets_change) @@ -11731,6 +11750,10 @@ unsigned int Plater::priv::update_background_process(bool force_validation, bool BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": validate err=%1%, warning=%2%")%err.string%warning.string; if (err.string.empty()) { + // Pass a warning from validation and either show a notification, + // or hide the old one. + process_validation_warning(warning); + // Validate passed, but also check filament temp mixing as a final // guard. check_filament_temp_mixing() reads directly from preset // configs and catches cases that Print::validate() may miss (e.g. @@ -11744,9 +11767,6 @@ unsigned int Plater::priv::update_background_process(bool force_validation, bool if (invalidated != Print::APPLY_STATUS_UNCHANGED && background_processing_enabled()) return_state |= UPDATE_BACKGROUND_PROCESS_RESTART; - // Pass a warning from validation and either show a notification, - // or hide the old one. - process_validation_warning(warning); if (printer_technology == ptFFF) { view3D->get_canvas3d()->reset_sequential_print_clearance(); view3D->get_canvas3d()->set_as_dirty(); @@ -13656,14 +13676,8 @@ void Plater::priv::on_action_slice_plate(SimpleEvent&) Model::setPrintSpeedTable(config, print_config); m_slice_all = false; - // Pre-slice confirmation for high/low temp filament mixing - if (wxGetApp().app_config->get_bool("allow_filament_temp_mixing") - && !q->check_filament_temp_mixing()) { - wxMessageDialog dlg(q, _L("该组合可能导致风险,是否继续"), - _L("确认"), wxYES_NO | wxICON_WARNING); - if (dlg.ShowModal() != wxID_YES) - return; - } + if (!q->confirm_filament_temp_mixing_before_slice()) + return; q->reslice(); q->select_view_3D("Preview"); @@ -13684,14 +13698,8 @@ void Plater::priv::on_action_slice_all(SimpleEvent&) Model::setExtruderParams(config, numExtruders); Model::setPrintSpeedTable(config, print_config); - // Pre-slice confirmation for high/low temp filament mixing - if (wxGetApp().app_config->get_bool("allow_filament_temp_mixing") - && !q->check_filament_temp_mixing()) { - wxMessageDialog dlg(q, _L("该组合可能导致风险,是否继续"), - _L("确认"), wxYES_NO | wxICON_WARNING); - if (dlg.ShowModal() != wxID_YES) - return; - } + if (!q->confirm_filament_temp_mixing_before_slice()) + return; m_slice_all = true; m_slice_all_only_has_gcode = true; @@ -17152,7 +17160,10 @@ std::vector Plater::load_files(const std::vector& input_files, p->m_slice_all_only_has_gcode = false; //BBS: wish to reset all plates stats item selected state when load a new file p->preview->get_canvas3d()->reset_select_plate_toolbar_selection(); - return p->load_files(input_files, strategy, ask_multi); + std::vector loaded = p->load_files(input_files, strategy, ask_multi); + if (!loaded.empty()) + notify_filament_usage_changed(); + return loaded; } // To be called when providing a list of files to the GUI slic3r on command line. @@ -17162,7 +17173,7 @@ std::vector Plater::load_files(const std::vector& input_fil paths.reserve(input_files.size()); for (const std::string& path : input_files) paths.emplace_back(path); - return p->load_files(paths, strategy, ask_multi); + return load_files(paths, strategy, ask_multi); } bool Plater::preview_zip_archive(const boost::filesystem::path& archive_path) @@ -18049,8 +18060,10 @@ void Plater::remove_selected() //BBS delete current selected // p->view3D->delete_selected(); GLCanvas3D* canvas = p->get_current_canvas3D(); - if (canvas) + if (canvas) { canvas->delete_selected(); + notify_filament_usage_changed(); + } } void Plater::increase_instances(size_t num) @@ -20100,6 +20113,27 @@ bool Plater::check_filament_temp_mixing() int num_filaments = (int)ft_opt->values.size(); std::set used_slots; + PartPlate* curr_plate = p->partplate_list.get_curr_plate(); + auto object_is_on_curr_plate = [curr_plate](size_t obj_idx, const ModelObject* mo) { + if (!curr_plate) + return true; + for (int inst_idx = 0; inst_idx < (int)mo->instances.size(); ++inst_idx) { + if (curr_plate->contain_instance((int)obj_idx, inst_idx)) + return true; + } + return false; + }; + + bool has_object_on_curr_plate = false; + for (size_t obj_idx = 0; obj_idx < wxGetApp().model().objects.size(); ++obj_idx) { + if (object_is_on_curr_plate(obj_idx, wxGetApp().model().objects[obj_idx])) { + has_object_on_curr_plate = true; + break; + } + } + if (!has_object_on_curr_plate) + return true; + auto collect_from_cfg = [&](const DynamicPrintConfig& cfg) { static const char* keys_1based[] = {"wall_filament", "solid_infill_filament"}; for (auto key : keys_1based) { @@ -20120,23 +20154,13 @@ bool Plater::check_filament_temp_mixing() collect_from_cfg(*this->config()); // Also collect from current plate's config for any plate-level overrides - PartPlate* curr_plate = p->partplate_list.get_curr_plate(); if (curr_plate) collect_from_cfg(*curr_plate->config()); // Collect from ModelVolume painting extruders for objects on the current plate for (size_t obj_idx = 0; obj_idx < wxGetApp().model().objects.size(); ++obj_idx) { const ModelObject* mo = wxGetApp().model().objects[obj_idx]; - bool on_curr_plate = false; - if (curr_plate) { - for (int inst_idx = 0; inst_idx < (int)mo->instances.size(); ++inst_idx) { - if (curr_plate->contain_instance((int)obj_idx, inst_idx)) { - on_curr_plate = true; - break; - } - } - } - if (!on_curr_plate) + if (!object_is_on_curr_plate(obj_idx, mo)) continue; for (const ModelVolume* mv : mo->volumes) { for (int eid : mv->get_extruders()) { @@ -20174,30 +20198,62 @@ bool Plater::check_filament_temp_mixing() return compatible; } +Plater::FilamentTempMixingState Plater::get_filament_temp_mixing_state() +{ + if (check_filament_temp_mixing()) + return FilamentTempMixingState::Compatible; + + return wxGetApp().app_config->get_bool("allow_filament_temp_mixing") ? + FilamentTempMixingState::AllowedWarning : + FilamentTempMixingState::BlockedError; +} + bool Plater::sync_filament_temp_mixing_notification() { - if (check_filament_temp_mixing()) { - get_notification_manager()->close_notification_of_type(NotificationType::ValidateError); - get_notification_manager()->close_notification_of_type(NotificationType::ValidateWarning); + switch (get_filament_temp_mixing_state()) { + case FilamentTempMixingState::Compatible: + get_notification_manager()->close_validate_error_notification(filament_temp_mixing_error_text()); + get_notification_manager()->close_validate_warning_notification(filament_temp_mixing_warning_text()); get_partplate_list().get_curr_plate()->update_apply_result_invalid(false); return true; - } - - StringObjectException err; - err.type = STRING_EXCEPT_FILAMENTS_DIFFERENT_TEMP; - - if (wxGetApp().app_config->get_bool("allow_filament_temp_mixing")) { + case FilamentTempMixingState::AllowedWarning: + get_notification_manager()->close_validate_error_notification(filament_temp_mixing_error_text()); + get_partplate_list().get_curr_plate()->update_apply_result_invalid(false); get_notification_manager()->push_notification( NotificationType::ValidateWarning, NotificationManager::NotificationLevel::WarningNotificationLevel, - _u8L("WARNING:") + "\n" + - _u8L("Detected both high and low temperature materials. Mixed printing may result in extruder clogging, nozzle damage, or layer adhesion issues.")); - } else { - err.string = _u8L("Detected both high and low temperature materials. Mixed printing may result in extruder clogging, nozzle damage, or layer adhesion issues. To continue printing, enable \"Allow mixed printing of high and low temperature materials\" in Preferences."); + _u8L("WARNING:") + "\n" + filament_temp_mixing_warning_text()); + return true; + case FilamentTempMixingState::BlockedError: { + StringObjectException err; + err.type = STRING_EXCEPT_FILAMENTS_DIFFERENT_TEMP; + err.string = filament_temp_mixing_error_text(); + get_notification_manager()->close_validate_warning_notification(filament_temp_mixing_warning_text()); get_notification_manager()->push_validate_error_notification(err); get_partplate_list().get_curr_plate()->update_apply_result_invalid(true); + return false; } - return false; + } + + return true; +} + +bool Plater::confirm_filament_temp_mixing_before_slice() +{ + if (get_filament_temp_mixing_state() != FilamentTempMixingState::AllowedWarning) + return true; + + wxMessageDialog dlg(this, _L("该组合可能导致风险,是否继续"), _L("确认"), wxYES_NO | wxICON_WARNING); + return dlg.ShowModal() == wxID_YES; +} + +void Plater::notify_filament_usage_changed() +{ + if (p->filament_usage_sync_pending) + return; + + p->filament_usage_sync_pending = true; + wxQueueEvent(this, new SimpleEvent(EVT_FILAMENT_USAGE_CHANGED, this)); } void Plater::on_config_change(const DynamicPrintConfig &config) @@ -20292,7 +20348,7 @@ void Plater::on_config_change(const DynamicPrintConfig &config) update_title_dirty_status(); } - sync_filament_temp_mixing_notification(); + notify_filament_usage_changed(); } void Plater::set_bed_shape() const diff --git a/src/slic3r/GUI/Plater.hpp b/src/slic3r/GUI/Plater.hpp index a11ef680bef..429f635cee2 100644 --- a/src/slic3r/GUI/Plater.hpp +++ b/src/slic3r/GUI/Plater.hpp @@ -104,6 +104,7 @@ wxDECLARE_EVENT(EVT_GLCANVAS_COLOR_MODE_CHANGED, SimpleEvent); wxDECLARE_EVENT(EVT_PRINT_FROM_SDCARD_VIEW, SimpleEvent); wxDECLARE_EVENT(EVT_CREATE_FILAMENT, SimpleEvent); wxDECLARE_EVENT(EVT_MODIFY_FILAMENT, SimpleEvent); +wxDECLARE_EVENT(EVT_FILAMENT_USAGE_CHANGED, SimpleEvent); wxDECLARE_EVENT(EVT_ADD_FILAMENT, SimpleEvent); wxDECLARE_EVENT(EVT_DEL_FILAMENT, SimpleEvent); using ColorEvent = Event; @@ -231,6 +232,11 @@ class Plater: public wxPanel { public: using fs_path = boost::filesystem::path; + enum class FilamentTempMixingState { + Compatible, + AllowedWarning, + BlockedError + }; Plater(wxWindow *parent, MainFrame *main_frame); Plater(Plater &&) = delete; @@ -510,9 +516,13 @@ class Plater: public wxPanel /// Check whether high-temperature and low-temperature filaments are mixed on the current plate. /// Returns true if compatible (no mixing), false if mixing is detected and error notification is shown. bool check_filament_temp_mixing(); + FilamentTempMixingState get_filament_temp_mixing_state(); /// Sync notification state with current filament temp mixing status. - /// Returns true if compatible (no mixing), false if mixing detected. + /// Returns true if slicing is allowed, false if high/low temperature mixing blocks slicing. bool sync_filament_temp_mixing_notification(); + bool confirm_filament_temp_mixing_before_slice(); + /// Queue a single UI sync after filament preset/assignment/plate usage changes. + void notify_filament_usage_changed(); void force_filament_colors_update(); void force_print_bed_update(); // On activating the parent window. diff --git a/src/slic3r/GUI/Preferences.cpp b/src/slic3r/GUI/Preferences.cpp index 82291ebe2ca..9ab828e277c 100644 --- a/src/slic3r/GUI/Preferences.cpp +++ b/src/slic3r/GUI/Preferences.cpp @@ -762,6 +762,9 @@ wxBoxSizer *PreferencesDialog::create_item_checkbox(wxString title, wxWindow *pa app_config->set_bool(param, checkbox->GetValue()); app_config->save(); + if (param == "allow_filament_temp_mixing" && wxGetApp().plater()) + wxGetApp().plater()->notify_filament_usage_changed(); + if (param == PRIVACY_POLICY_FLAGS) { app_config->set("app", PRIVACY_POLICY_FLAGS, checkbox->GetValue()); diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index 102d7807c5e..e2a889f50c1 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -3120,8 +3120,7 @@ void TabPrintModel::on_value_change(const std::string& opt_key, const boost::any } wxGetApp().params_panel()->notify_object_config_changed(); - // Real-time high/low temperature filament mixing check after object-level config change - wxGetApp().plater()->sync_filament_temp_mixing_notification(); + wxGetApp().plater()->notify_filament_usage_changed(); } void TabPrintModel::reload_config() From fc070f9633324dea386b3e64ca18a695edf83b82 Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Mon, 8 Jun 2026 19:26:17 +0800 Subject: [PATCH 041/135] =?UTF-8?q?Fix=EF=BC=9A=E4=BF=AE=E6=94=B9=E5=85=B3?= =?UTF-8?q?=E4=BA=8E=E9=A1=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/slic3r/GUI/AboutDialog.cpp | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/slic3r/GUI/AboutDialog.cpp b/src/slic3r/GUI/AboutDialog.cpp index 51cc92c1fc0..f32ba083061 100644 --- a/src/slic3r/GUI/AboutDialog.cpp +++ b/src/slic3r/GUI/AboutDialog.cpp @@ -285,14 +285,15 @@ AboutDialog::AboutDialog() // SM Beta - text_list.push_back(_L("Note: The firmware version of SnapmakerU1 should be no less than") + " " + std::string(MIN_FIRM_VER)); - text_list.push_back(_L("Snapmaker Orca is forked from Orca Slicer by SoftFever.")); - text_list.push_back(_L("OrcaSlicer is forked from Bambu Studio by Bambu Lab.")); - text_list.push_back(_L("Bambu Studio is forked from PrusaSlicer by Prusa Research, which is from Slic3r by Alessandro Ranellucci and the RepRap community. Orca Slicer incorporates a lot of features from SuperSlicer by @supermerill.")); - text_list.push_back(_L("Snapmaker Orca integrates some features from the 3D printing open-source community, with specific attributions provided in the code comments.")); - text_list.push_back(_L("")); - text_list.push_back(_L("Snapmaker Orca is licensed under the GNU Affero General Public License, version 3.")); - + // text_list.push_back(_L("Note: The firmware version of SnapmakerU1 should be no less than") + " " + std::string(MIN_FIRM_VER)); + // text_list.push_back(_L("Snapmaker Orca is forked from Orca Slicer by SoftFever.")); + // text_list.push_back(_L("OrcaSlicer is forked from Bambu Studio by Bambu Lab.")); + // text_list.push_back(_L("Bambu Studio is forked from PrusaSlicer by Prusa Research, which is from Slic3r by Alessandro Ranellucci and the RepRap community. Orca Slicer incorporates a lot of features from SuperSlicer by @supermerill.")); + // text_list.push_back(_L("Snapmaker Orca integrates some features from the 3D printing open-source community, with specific attributions provided in the code comments.")); + // text_list.push_back(_L("")); + // text_list.push_back(_L("Snapmaker Orca is licensed under the GNU Affero General Public License, version 3.")); + text_list.push_back(_L("2026-06-08:顶盖二期1.4.0版本")); + text_sizer->Add( 0, 0, 0, wxTOP, FromDIP(33)); bool is_zh = wxGetApp().app_config->get("language") == "zh_CN"; for (int i = 0; i < text_list.size(); i++) From 9855115a3d988266dac593ee9b5e3e6f3ac99828 Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Tue, 9 Jun 2026 18:18:47 +0800 Subject: [PATCH 042/135] Fix filament temperature mixing slice guards --- src/slic3r/GUI/GCodeViewer.cpp | 18 ++- src/slic3r/GUI/GLCanvas3D.cpp | 18 ++- src/slic3r/GUI/MainFrame.cpp | 7 +- src/slic3r/GUI/PartPlate.cpp | 4 + src/slic3r/GUI/Plater.cpp | 275 +++++++++++++++++++++++++-------- src/slic3r/GUI/Plater.hpp | 28 +++- 6 files changed, 271 insertions(+), 79 deletions(-) diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp index 23f9788899a..0bf41c17264 100644 --- a/src/slic3r/GUI/GCodeViewer.cpp +++ b/src/slic3r/GUI/GCodeViewer.cpp @@ -4049,10 +4049,10 @@ void GCodeViewer::render_shells(int canvas_width, int canvas_height) //BBS void GCodeViewer::render_all_plates_stats(const std::vector& gcode_result_list, bool show /*= true*/) const { - if (!show) + if (!show || gcode_result_list.empty()) return; - for (auto gcode_result : gcode_result_list) { - if (gcode_result->moves.size() == 0) + for (const GCodeProcessorResult* gcode_result : gcode_result_list) { + if (gcode_result == nullptr || gcode_result->moves.empty()) return; } ImGuiWrapper& imgui = *wxGetApp().imgui(); @@ -4076,7 +4076,7 @@ void GCodeViewer::render_all_plates_stats(const std::vector filament_colors; decode_colors(wxGetApp().plater()->get_extruder_colors_from_plater_config(gcode_result_list.back()), filament_colors); - for (int i = 0; i < filament_colors.size(); i++) { + for (size_t i = 0; i < filament_colors.size(); i++) { filament_colors[i] = adjust_color_for_rendering(filament_colors[i]); } @@ -4170,6 +4170,10 @@ void GCodeViewer::render_all_plates_stats(const std::vector(extruder_id) >= filament_diameters.size() || + static_cast(extruder_id) >= filament_densities.size()) + return std::pair{ 0.0, 0.0 }; + double koef = imperial_units ? 1.0 / GizmoObjectManipulation::in_to_mm : 0.001; std::pair ret = { koef * volume / (PI * sqr(0.5 * filament_diameters[extruder_id])), volume * filament_densities[extruder_id] * 0.001 }; @@ -4183,6 +4187,9 @@ void GCodeViewer::render_all_plates_stats(const std::vectorget_partplate_list(); for (auto plate : plate_list.get_nonempty_plate_list()) { + if (plate == nullptr || plate->get_slice_result() == nullptr) + continue; + auto plate_print_statistics = plate->get_slice_result()->print_statistics; for (const auto& [extruder_id, model_volume] : plate_print_statistics.model_volumes_per_extruder) { model_volume_of_extruders_all_plates[extruder_id] += model_volume; @@ -4201,7 +4208,8 @@ void GCodeViewer::render_all_plates_stats(const std::vectorget_print((PrintBase **) &print, nullptr, nullptr); - total_cost_all_plates += print->print_statistics().total_cost; + if (print != nullptr) + total_cost_all_plates += print->print_statistics().total_cost; } for (auto it = model_volume_of_extruders_all_plates.begin(); it != model_volume_of_extruders_all_plates.end(); it++) diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp index bb63deaefbb..595fe4861b8 100644 --- a/src/slic3r/GUI/GLCanvas3D.cpp +++ b/src/slic3r/GUI/GLCanvas3D.cpp @@ -7931,16 +7931,19 @@ void GLCanvas3D::_render_imgui_select_plate_toolbar() } if (m_sel_plate_toolbar.show_stats_item) { all_plates_stats_item->percent = 0.0f; + const std::vector nonempty_plate_list = plate_list.get_nonempty_plate_list(); + const std::vector nonempty_plate_results = plate_list.get_nonempty_plates_slice_results(); - for (auto plate : plate_list.get_nonempty_plate_list()) { + for (PartPlate* plate : nonempty_plate_list) { if (plate->is_slice_result_valid() && plate->is_slice_result_ready_for_print()) sliced_plates_cnt++; } - all_plates_stats_item->percent = (float)(sliced_plates_cnt) / (float)(plate_list.get_nonempty_plate_list().size()) * 100.0f; + if (!nonempty_plate_list.empty()) + all_plates_stats_item->percent = static_cast(sliced_plates_cnt) / static_cast(nonempty_plate_list.size()) * 100.0f; if (all_plates_stats_item->percent == 0.0f) all_plates_stats_item->slice_state = IMToolbarItem::SliceState::UNSLICED; - else if (sliced_plates_cnt == plate_list.get_nonempty_plate_list().size()) + else if (sliced_plates_cnt == nonempty_plate_list.size()) all_plates_stats_item->slice_state = IMToolbarItem::SliceState::SLICED; else if (all_plates_stats_item->percent < 100.0f) all_plates_stats_item->slice_state = IMToolbarItem::SliceState::SLICING; @@ -7955,17 +7958,18 @@ void GLCanvas3D::_render_imgui_select_plate_toolbar() // Changing parameters does not invalid all plates, need extra logic to validate bool gcode_result_valid = true; - for (auto gcode_result : plate_list.get_nonempty_plates_slice_results()) { - if (gcode_result->moves.size() == 0) { + for (const GCodeProcessorResult* gcode_result : nonempty_plate_results) { + if (gcode_result == nullptr || gcode_result->moves.empty()) { gcode_result_valid = false; + break; } } if (all_plates_stats_item->selected && all_plates_stats_item->slice_state == IMToolbarItem::SliceState::SLICED && gcode_result_valid) { - m_gcode_viewer.render_all_plates_stats(plate_list.get_nonempty_plates_slice_results()); + m_gcode_viewer.render_all_plates_stats(nonempty_plate_results); m_render_preview = false; } else{ - m_gcode_viewer.render_all_plates_stats(plate_list.get_nonempty_plates_slice_results(), false); + m_gcode_viewer.render_all_plates_stats(nonempty_plate_results, false); m_render_preview = true; } }else diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index 96b1b08a3af..66ff3ab24f2 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -1891,8 +1891,7 @@ bool MainFrame::get_enable_slice_status() { enable = false; }*/ - //always enable slice_all button - enable = true; + enable = m_plater->has_sliceable_plate_for_slice_all(); } else if (m_slice_select == eSlicePlate) { @@ -1904,6 +1903,10 @@ bool MainFrame::get_enable_slice_status() { enable = false; } + else if (m_plater->is_plate_blocked_by_filament_temp_mixing(part_plate_list.get_curr_plate_index())) + { + enable = false; + } } BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": m_slice_select %1%, enable= %2% ")%m_slice_select %enable; diff --git a/src/slic3r/GUI/PartPlate.cpp b/src/slic3r/GUI/PartPlate.cpp index b29ea6d70f7..9d9a1ab7252 100644 --- a/src/slic3r/GUI/PartPlate.cpp +++ b/src/slic3r/GUI/PartPlate.cpp @@ -4039,6 +4039,8 @@ int PartPlateList::select_plate(int index) return -1; } + const int old_plate_index = m_current_plate; + // BBS: erase unnecessary snapshot if (get_curr_plate_index() != index && m_intialized) { if (m_plater) @@ -4052,6 +4054,8 @@ int PartPlateList::select_plate(int index) m_current_plate = index; m_plate_list[m_current_plate]->set_selected(); + if (old_plate_index != m_current_plate && wxGetApp().plater()) + wxGetApp().plater()->notify_filament_usage_changed(); //BBS if(m_model) diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 69fb304d96a..2c43b5f75e4 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -211,6 +211,72 @@ static std::string filament_temp_mixing_error_text() return _u8L("Detected both high and low temperature materials. Mixed printing may result in extruder clogging, nozzle damage, or layer adhesion issues. To continue printing, enable \"Allow mixed printing of high and low temperature materials\" in Preferences."); } +static bool model_object_is_on_plate(PartPlate* plate, size_t obj_idx, const ModelObject* model_object) +{ + if (model_object == nullptr) + return false; + + if (plate == nullptr) + return true; + + const int object_index = static_cast(obj_idx); + const int instance_count = static_cast(model_object->instances.size()); + for (int instance_index = 0; instance_index < instance_count; ++instance_index) + { + if (plate->contain_instance(object_index, instance_index)) + return true; + } + + return false; +} + +static void collect_filament_slots_from_config( + const DynamicPrintConfig& config, + int num_filaments, + std::set& used_slots) +{ + static const std::vector keys_1based = { + "wall_filament", + "sparse_infill_filament", + "solid_infill_filament" + }; + for (const char* key : keys_1based) + { + const ConfigOptionInt* option = config.option(key); + if (option != nullptr && option->value >= 1 && option->value <= num_filaments) + used_slots.insert(option->value - 1); + } + + static const std::vector keys_0based = { + "support_filament", + "support_interface_filament", + "wipe_tower_filament" + }; + for (const char* key : keys_0based) + { + const ConfigOptionInt* option = config.option(key); + if (option != nullptr && option->value >= 1 && option->value <= num_filaments) + used_slots.insert(option->value - 1); + } + + const ConfigOptionInt* extruder_option = config.option("extruder"); + if (extruder_option != nullptr && extruder_option->value >= 1 && extruder_option->value <= num_filaments) + used_slots.insert(extruder_option->value - 1); +} + +static void collect_filament_slots_from_model_config( + const ModelConfigObject& config, + int num_filaments, + std::set& used_slots) +{ + if (!config.has("extruder")) + return; + + const int extruder_id = config.extruder(); + if (extruder_id >= 1 && extruder_id <= num_filaments) + used_slots.insert(extruder_id - 1); +} + wxDEFINE_EVENT(EVT_SCHEDULE_BACKGROUND_PROCESS, SimpleEvent); wxDEFINE_EVENT(EVT_SLICING_UPDATE, SlicingStatusEvent); wxDEFINE_EVENT(EVT_SLICING_COMPLETED, wxCommandEvent); @@ -8420,6 +8486,9 @@ struct Plater::priv Plater *q; MainFrame *main_frame; bool filament_usage_sync_pending = false; + bool filament_temp_mixing_notification_initialized = false; + int filament_temp_mixing_notification_plate = -1; + FilamentTempMixingState filament_temp_mixing_notification_state = FilamentTempMixingState::Compatible; MenuFactory menus; @@ -13594,7 +13663,11 @@ void Plater::priv::on_process_completed(SlicingProcessCompletedEvent &evt) else { BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(":slicing all, plate %1% finished, start next slice...")%m_cur_slice_plate; - m_cur_slice_plate++; + m_cur_slice_plate = q->find_next_sliceable_plate_for_slice_all(m_cur_slice_plate + 1); + if (m_cur_slice_plate < 0) { + m_is_slicing = false; + return; + } q->Freeze(); q->select_plate(m_cur_slice_plate); @@ -13698,12 +13771,16 @@ void Plater::priv::on_action_slice_all(SimpleEvent&) Model::setExtruderParams(config, numExtruders); Model::setPrintSpeedTable(config, print_config); - if (!q->confirm_filament_temp_mixing_before_slice()) + if (!q->confirm_filament_temp_mixing_before_slice_all()) return; m_slice_all = true; m_slice_all_only_has_gcode = true; - m_cur_slice_plate = 0; + m_cur_slice_plate = q->find_next_sliceable_plate_for_slice_all(0); + if (m_cur_slice_plate < 0) { + m_slice_all = false; + return; + } //select plate q->select_plate(m_cur_slice_plate); q->reslice(); @@ -20103,69 +20180,52 @@ void Plater::config_change_notification(const DynamicPrintConfig &config, const // notification for more options } -bool Plater::check_filament_temp_mixing() +bool Plater::check_filament_temp_mixing(int plate_index) { + const int plate_count = p->partplate_list.get_plate_count(); + if (plate_index < 0 || plate_index >= plate_count) + return true; + const DynamicPrintConfig& full_cfg = wxGetApp().preset_bundle->full_config(); - auto* ft_opt = full_cfg.option("filament_type"); - if (!ft_opt || ft_opt->values.empty()) + const ConfigOptionStrings* filament_type_option = full_cfg.option("filament_type"); + if (filament_type_option == nullptr || filament_type_option->values.empty()) return true; - int num_filaments = (int)ft_opt->values.size(); + const int num_filaments = static_cast(filament_type_option->values.size()); std::set used_slots; - PartPlate* curr_plate = p->partplate_list.get_curr_plate(); - auto object_is_on_curr_plate = [curr_plate](size_t obj_idx, const ModelObject* mo) { - if (!curr_plate) - return true; - for (int inst_idx = 0; inst_idx < (int)mo->instances.size(); ++inst_idx) { - if (curr_plate->contain_instance((int)obj_idx, inst_idx)) - return true; - } - return false; - }; + PartPlate* plate = p->partplate_list.get_plate(plate_index); - bool has_object_on_curr_plate = false; + bool has_object_on_plate = false; for (size_t obj_idx = 0; obj_idx < wxGetApp().model().objects.size(); ++obj_idx) { - if (object_is_on_curr_plate(obj_idx, wxGetApp().model().objects[obj_idx])) { - has_object_on_curr_plate = true; + const ModelObject* model_object = wxGetApp().model().objects[obj_idx]; + if (model_object_is_on_plate(plate, obj_idx, model_object)) { + has_object_on_plate = true; break; } } - if (!has_object_on_curr_plate) + if (!has_object_on_plate) return true; - auto collect_from_cfg = [&](const DynamicPrintConfig& cfg) { - static const char* keys_1based[] = {"wall_filament", "solid_infill_filament"}; - for (auto key : keys_1based) { - auto* opt = cfg.option(key); - if (opt && opt->value >= 1 && opt->value <= num_filaments) - used_slots.insert(opt->value - 1); - } - static const char* keys_0based[] = {"support_filament", "support_interface_filament", "wipe_tower_filament"}; - for (auto key : keys_0based) { - auto* opt = cfg.option(key); - if (opt && opt->value >= 1 && opt->value <= num_filaments) - used_slots.insert(opt->value - 1); - } - }; - // Collect from the Plater's working config for global Process settings. // Defaults are 0 (skipped by the >= 1 check). - collect_from_cfg(*this->config()); + collect_filament_slots_from_config(*this->config(), num_filaments, used_slots); // Also collect from current plate's config for any plate-level overrides - if (curr_plate) - collect_from_cfg(*curr_plate->config()); + if (plate) + collect_filament_slots_from_config(*plate->config(), num_filaments, used_slots); // Collect from ModelVolume painting extruders for objects on the current plate for (size_t obj_idx = 0; obj_idx < wxGetApp().model().objects.size(); ++obj_idx) { - const ModelObject* mo = wxGetApp().model().objects[obj_idx]; - if (!object_is_on_curr_plate(obj_idx, mo)) + const ModelObject* model_object = wxGetApp().model().objects[obj_idx]; + if (!model_object_is_on_plate(plate, obj_idx, model_object)) continue; - for (const ModelVolume* mv : mo->volumes) { - for (int eid : mv->get_extruders()) { - if (eid >= 1 && eid <= num_filaments) - used_slots.insert(eid - 1); + collect_filament_slots_from_model_config(model_object->config, num_filaments, used_slots); + for (const ModelVolume* model_volume : model_object->volumes) { + collect_filament_slots_from_model_config(model_volume->config, num_filaments, used_slots); + for (int extruder_id : model_volume->get_extruders()) { + if (extruder_id >= 1 && extruder_id <= num_filaments) + used_slots.insert(extruder_id - 1); } } } @@ -20181,11 +20241,11 @@ bool Plater::check_filament_temp_mixing() bool has_high = false, has_low = false; for (int slot : used_slots) { - if (slot < (int)bundle->filament_presets.size()) { + if (slot < static_cast(bundle->filament_presets.size())) { const Preset* preset = bundle->filaments.find_preset( bundle->filament_presets[slot], true); - if (preset) { - bool is_high = preset->config.opt_bool("filament_is_high_temperature", 0); + if (preset != nullptr) { + const bool is_high = preset->config.opt_bool("filament_is_high_temperature", 0); if (is_high) has_high = true; else @@ -20193,14 +20253,19 @@ bool Plater::check_filament_temp_mixing() } } } - bool compatible = !(has_high && has_low); + const bool compatible = !(has_high && has_low); return compatible; } -Plater::FilamentTempMixingState Plater::get_filament_temp_mixing_state() +bool Plater::check_filament_temp_mixing() +{ + return check_filament_temp_mixing(p->partplate_list.get_curr_plate_index()); +} + +Plater::FilamentTempMixingState Plater::get_filament_temp_mixing_state(int plate_index) { - if (check_filament_temp_mixing()) + if (check_filament_temp_mixing(plate_index)) return FilamentTempMixingState::Compatible; return wxGetApp().app_config->get_bool("allow_filament_temp_mixing") ? @@ -20208,42 +20273,125 @@ Plater::FilamentTempMixingState Plater::get_filament_temp_mixing_state() FilamentTempMixingState::BlockedError; } +Plater::FilamentTempMixingState Plater::get_filament_temp_mixing_state() +{ + return get_filament_temp_mixing_state(p->partplate_list.get_curr_plate_index()); +} + +bool Plater::is_plate_blocked_by_filament_temp_mixing(int plate_index) +{ + return get_filament_temp_mixing_state(plate_index) == FilamentTempMixingState::BlockedError; +} + +bool Plater::has_sliceable_plate_for_slice_all() +{ + return find_next_sliceable_plate_for_slice_all(0) >= 0; +} + +int Plater::find_next_sliceable_plate_for_slice_all(int start_plate_index) +{ + const int plate_count = p->partplate_list.get_plate_count(); + if (start_plate_index < 0) + start_plate_index = 0; + + for (int plate_index = start_plate_index; plate_index < plate_count; ++plate_index) + { + PartPlate* plate = p->partplate_list.get_plate(plate_index); + if (plate != nullptr && plate->can_slice() && !is_plate_blocked_by_filament_temp_mixing(plate_index)) + return plate_index; + } + + return -1; +} + bool Plater::sync_filament_temp_mixing_notification() { - switch (get_filament_temp_mixing_state()) { + const int curr_plate_index = get_partplate_list().get_curr_plate_index(); + PartPlate* curr_plate = get_partplate_list().get_curr_plate(); + const FilamentTempMixingState mixing_state = get_filament_temp_mixing_state(curr_plate_index); + const bool notification_state_changed = + !p->filament_temp_mixing_notification_initialized || + p->filament_temp_mixing_notification_plate != curr_plate_index || + p->filament_temp_mixing_notification_state != mixing_state; + bool slicing_allowed = true; + + switch (mixing_state) { case FilamentTempMixingState::Compatible: get_notification_manager()->close_validate_error_notification(filament_temp_mixing_error_text()); get_notification_manager()->close_validate_warning_notification(filament_temp_mixing_warning_text()); get_partplate_list().get_curr_plate()->update_apply_result_invalid(false); - return true; + slicing_allowed = true; + break; case FilamentTempMixingState::AllowedWarning: get_notification_manager()->close_validate_error_notification(filament_temp_mixing_error_text()); get_partplate_list().get_curr_plate()->update_apply_result_invalid(false); - get_notification_manager()->push_notification( - NotificationType::ValidateWarning, - NotificationManager::NotificationLevel::WarningNotificationLevel, - _u8L("WARNING:") + "\n" + filament_temp_mixing_warning_text()); - return true; + if (notification_state_changed) { + get_notification_manager()->push_notification( + NotificationType::ValidateWarning, + NotificationManager::NotificationLevel::WarningNotificationLevel, + _u8L("WARNING:") + "\n" + filament_temp_mixing_warning_text()); + } + slicing_allowed = true; + break; case FilamentTempMixingState::BlockedError: { StringObjectException err; err.type = STRING_EXCEPT_FILAMENTS_DIFFERENT_TEMP; err.string = filament_temp_mixing_error_text(); get_notification_manager()->close_validate_warning_notification(filament_temp_mixing_warning_text()); - get_notification_manager()->push_validate_error_notification(err); + if (notification_state_changed) + get_notification_manager()->push_validate_error_notification(err); get_partplate_list().get_curr_plate()->update_apply_result_invalid(true); - return false; + slicing_allowed = false; + break; } } - return true; + p->filament_temp_mixing_notification_initialized = true; + p->filament_temp_mixing_notification_plate = curr_plate_index; + p->filament_temp_mixing_notification_state = mixing_state; + + const bool can_slice = curr_plate != nullptr && curr_plate->can_slice() && slicing_allowed; + p->main_frame->update_slice_print_status(MainFrame::eEventPlateUpdate, can_slice); + return slicing_allowed; } bool Plater::confirm_filament_temp_mixing_before_slice() { - if (get_filament_temp_mixing_state() != FilamentTempMixingState::AllowedWarning) + switch (get_filament_temp_mixing_state()) { + case FilamentTempMixingState::Compatible: + return true; + case FilamentTempMixingState::BlockedError: + sync_filament_temp_mixing_notification(); + return false; + case FilamentTempMixingState::AllowedWarning: + break; + } + + wxMessageDialog dlg(this, _L("This material combination may cause risks. Do you want to continue?"), + _L("Confirm"), wxYES_NO | wxICON_WARNING); + return dlg.ShowModal() == wxID_YES; +} + +bool Plater::confirm_filament_temp_mixing_before_slice_all() +{ + if (!has_sliceable_plate_for_slice_all()) + return false; + + bool has_allowed_warning = false; + for (int plate_index = 0; plate_index < p->partplate_list.get_plate_count(); ++plate_index) + { + if (get_filament_temp_mixing_state(plate_index) == FilamentTempMixingState::AllowedWarning) + { + has_allowed_warning = true; + break; + } + } + + if (!has_allowed_warning) return true; - wxMessageDialog dlg(this, _L("该组合可能导致风险,是否继续"), _L("确认"), wxYES_NO | wxICON_WARNING); + wxMessageDialog dlg(this, _L("This material combination may cause risks. Do you want to continue?"), + _L("Confirm"), wxYES_NO | wxICON_WARNING); return dlg.ShowModal() == wxID_YES; } @@ -21087,6 +21235,7 @@ int Plater::select_plate(int plate_index, bool need_slice) SimpleEvent event(EVT_GLCANVAS_PLATE_SELECT); p->on_plate_selected(event); + notify_filament_usage_changed(); BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(" %1%: plate %2%, return %3%")%__LINE__ %plate_index %ret; return ret; diff --git a/src/slic3r/GUI/Plater.hpp b/src/slic3r/GUI/Plater.hpp index 429f635cee2..f7bf0e93df7 100644 --- a/src/slic3r/GUI/Plater.hpp +++ b/src/slic3r/GUI/Plater.hpp @@ -513,14 +513,38 @@ class Plater: public wxPanel bool update_filament_colors_in_full_config(); void config_change_notification(const DynamicPrintConfig &config, const std::string& key); void on_config_change(const DynamicPrintConfig &config); - /// Check whether high-temperature and low-temperature filaments are mixed on the current plate. - /// Returns true if compatible (no mixing), false if mixing is detected and error notification is shown. + /// @brief Check whether high-temperature and low-temperature filaments are mixed on the current plate. + /// @return True if compatible; false if high/low temperature materials are mixed. bool check_filament_temp_mixing(); + /// @brief Check whether high-temperature and low-temperature filaments are mixed on a specific plate. + /// @param plate_index Plate index to check. + /// @return True if compatible or plate index is invalid; false if high/low temperature materials are mixed. + bool check_filament_temp_mixing(int plate_index); + /// @brief Get high/low temperature material mixing state for the current plate. + /// @return Current plate material mixing state. FilamentTempMixingState get_filament_temp_mixing_state(); + /// @brief Get high/low temperature material mixing state for a specific plate. + /// @param plate_index Plate index to check. + /// @return Plate material mixing state. + FilamentTempMixingState get_filament_temp_mixing_state(int plate_index); + /// @brief Check whether a specific plate is blocked by high/low temperature material mixing. + /// @param plate_index Plate index to check. + /// @return True if slicing this plate is blocked; otherwise false. + bool is_plate_blocked_by_filament_temp_mixing(int plate_index); + /// @brief Check whether slice-all has at least one plate that can be sliced. + /// @return True if any plate can be sliced and is not blocked by material mixing. + bool has_sliceable_plate_for_slice_all(); + /// @brief Find the next plate that can be sliced by slice-all. + /// @param start_plate_index First plate index to check. + /// @return Plate index if found; otherwise -1. + int find_next_sliceable_plate_for_slice_all(int start_plate_index); /// Sync notification state with current filament temp mixing status. /// Returns true if slicing is allowed, false if high/low temperature mixing blocks slicing. bool sync_filament_temp_mixing_notification(); bool confirm_filament_temp_mixing_before_slice(); + /// @brief Confirm warning-level high/low temperature material mixing before slicing all plates. + /// @return True if slice-all can continue; otherwise false. + bool confirm_filament_temp_mixing_before_slice_all(); /// Queue a single UI sync after filament preset/assignment/plate usage changes. void notify_filament_usage_changed(); void force_filament_colors_update(); From 4084f178fd44a1ec2e12554e0e520a4daf6183af Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Wed, 10 Jun 2026 12:00:44 +0800 Subject: [PATCH 043/135] Fix filament temperature mixing slice guards Co-Authored-By: Claude Opus 4.7 --- src/slic3r/GUI/MainFrame.cpp | 15 +++++---- src/slic3r/GUI/Plater.cpp | 60 ++++++++++++++++++++++++++++++++---- src/slic3r/GUI/Plater.hpp | 2 ++ 3 files changed, 63 insertions(+), 14 deletions(-) diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index 66ff3ab24f2..8ed5b47530d 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -565,7 +565,7 @@ DPIFrame(NULL, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, BORDERLESS_FRAME_ } return;} #endif - if (evt.CmdDown() && evt.GetKeyCode() == 'R') { if (m_slice_enable) { wxGetApp().plater()->update(true, true); wxPostEvent(m_plater, SimpleEvent(EVT_GLTOOLBAR_SLICE_PLATE)); this->m_tabpanel->SetSelection(tpPreview); } return; } + if (evt.CmdDown() && evt.GetKeyCode() == 'R') { if (m_slice_enable) { wxGetApp().plater()->update(true, true); wxPostEvent(m_plater, SimpleEvent(EVT_GLTOOLBAR_SLICE_PLATE)); } return; } if (evt.CmdDown() && evt.ShiftDown() && evt.GetKeyCode() == 'G') { m_plater->apply_background_progress(); m_print_enable = get_enable_print_status(); @@ -1659,7 +1659,6 @@ wxBoxSizer* MainFrame::create_side_tools() else wxPostEvent(m_plater, SimpleEvent(EVT_GLTOOLBAR_SLICE_PLATE)); - this->m_tabpanel->SetSelection(tpPreview); }); m_print_btn->Bind(wxEVT_BUTTON, [this](wxCommandEvent& event) @@ -2054,11 +2053,11 @@ void MainFrame::update_slice_print_status(SlicePrintEventType event, bool can_sl { bool enable_print = true, enable_slice = true; - if (!can_slice) - { - if (m_slice_select == eSlicePlate) - enable_slice = false; - } + if (event == eEventPlateUpdate) + enable_slice = get_enable_slice_status(); + else if (!can_slice) + enable_slice = false; + if (!can_print) enable_print = false; @@ -2070,7 +2069,7 @@ void MainFrame::update_slice_print_status(SlicePrintEventType event, bool can_sl } //process slice logic - if (enable_slice) + if (event != eEventPlateUpdate && enable_slice) { enable_slice = get_enable_slice_status(); } diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 2c43b5f75e4..b98fc24109d 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -9398,7 +9398,21 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame) q->Bind(EVT_EXPORT_FINISHED, &priv::on_export_finished, this); q->Bind(EVT_GLVIEWTOOLBAR_3D, [q](SimpleEvent&) { q->select_view_3D("3D"); }); //BBS: set on_slice to false - q->Bind(EVT_GLVIEWTOOLBAR_PREVIEW, [q](SimpleEvent&) { q->select_view_3D("Preview", false); }); + q->Bind(EVT_GLVIEWTOOLBAR_PREVIEW, [q](SimpleEvent&) { + if (q->is_view3D_shown()) { + if (q->has_sliceable_plate_for_slice_all()) { + wxGetApp().mainframe->select_tab(size_t(MainFrame::tp3DEditor)); + wxPostEvent(q, SimpleEvent(EVT_GLTOOLBAR_SLICE_ALL)); + return; + } + if (q->is_plate_blocked_by_filament_temp_mixing(q->get_partplate_list().get_curr_plate_index())) { + wxGetApp().mainframe->select_tab(size_t(MainFrame::tp3DEditor)); + q->sync_filament_temp_mixing_notification(); + return; + } + } + q->select_view_3D("Preview", false); + }); q->Bind(EVT_GLTOOLBAR_SLICE_PLATE, &priv::on_action_slice_plate, this); q->Bind(EVT_GLTOOLBAR_SLICE_ALL, &priv::on_action_slice_all, this); q->Bind(EVT_GLTOOLBAR_PRINT_PLATE, &priv::on_action_print_plate, this); @@ -11833,7 +11847,10 @@ unsigned int Plater::priv::update_background_process(bool force_validation, bool notification_manager->set_all_slicing_errors_gray(true); notification_manager->close_notification_of_type(NotificationType::ValidateError); } - if (invalidated != Print::APPLY_STATUS_UNCHANGED && background_processing_enabled()) + else { + return_state |= UPDATE_BACKGROUND_PROCESS_INVALID; + } + if (filament_ok && invalidated != Print::APPLY_STATUS_UNCHANGED && background_processing_enabled()) return_state |= UPDATE_BACKGROUND_PROCESS_RESTART; if (printer_technology == ptFFF) { @@ -13749,7 +13766,7 @@ void Plater::priv::on_action_slice_plate(SimpleEvent&) Model::setPrintSpeedTable(config, print_config); m_slice_all = false; - if (!q->confirm_filament_temp_mixing_before_slice()) + if (!q->guard_before_slice_plate()) return; q->reslice(); @@ -13771,7 +13788,7 @@ void Plater::priv::on_action_slice_all(SimpleEvent&) Model::setExtruderParams(config, numExtruders); Model::setPrintSpeedTable(config, print_config); - if (!q->confirm_filament_temp_mixing_before_slice_all()) + if (!q->guard_before_slice_all()) return; m_slice_all = true; @@ -19409,6 +19426,18 @@ int Plater::start_next_slice() // Stop arrange and (or) optimize rotation tasks. //this->stop_jobs(); + if (is_plate_blocked_by_filament_temp_mixing(p->partplate_list.get_curr_plate_index())) { + sync_filament_temp_mixing_notification(); + if (p->m_slice_all) { + SlicingProcessCompletedEvent evt(EVT_PROCESS_COMPLETED, 0, + SlicingProcessCompletedEvent::Finished, nullptr); + wxQueueEvent(this, evt.Clone()); + return 0; + } + p->process_completed_with_error = p->partplate_list.get_curr_plate_index(); + return -1; + } + //FIXME Don't reslice if export of G-code or sending to OctoPrint is running. // bitmask of UpdateBackgroundProcessReturnState unsigned int state = this->p->update_background_process(true, false, false); @@ -20355,6 +20384,17 @@ bool Plater::sync_filament_temp_mixing_notification() return slicing_allowed; } +bool Plater::guard_before_slice_plate() +{ + sync_filament_temp_mixing_notification(); + return confirm_filament_temp_mixing_before_slice(); +} + +bool Plater::guard_before_slice_all() +{ + return confirm_filament_temp_mixing_before_slice_all(); +} + bool Plater::confirm_filament_temp_mixing_before_slice() { switch (get_filament_temp_mixing_state()) { @@ -20380,7 +20420,9 @@ bool Plater::confirm_filament_temp_mixing_before_slice_all() bool has_allowed_warning = false; for (int plate_index = 0; plate_index < p->partplate_list.get_plate_count(); ++plate_index) { - if (get_filament_temp_mixing_state(plate_index) == FilamentTempMixingState::AllowedWarning) + PartPlate* plate = p->partplate_list.get_plate(plate_index); + if (plate != nullptr && plate->can_slice() && + get_filament_temp_mixing_state(plate_index) == FilamentTempMixingState::AllowedWarning) { has_allowed_warning = true; break; @@ -21135,6 +21177,8 @@ int Plater::select_plate(int plate_index, bool need_slice) p->process_completed_with_error = -1; p->m_slice_all = false; reset_gcode_toolpaths(); + if (!guard_before_slice_plate()) + return ret; reslice(); } else { @@ -21189,8 +21233,11 @@ int Plater::select_plate(int plate_index, bool need_slice) //p->process_completed_with_error = -1; p->m_slice_all = false; reset_gcode_toolpaths(); - if (model_fits && !validate_err) + if (model_fits && !validate_err) { + if (!guard_before_slice_plate()) + return ret; reslice(); + } else { p->main_frame->update_slice_print_status(MainFrame::eEventPlateUpdate, false); //sometimes the previous print's sliced result is still valid, but the newly added object is laid over the boundary @@ -21235,6 +21282,7 @@ int Plater::select_plate(int plate_index, bool need_slice) SimpleEvent event(EVT_GLCANVAS_PLATE_SELECT); p->on_plate_selected(event); + sync_filament_temp_mixing_notification(); notify_filament_usage_changed(); BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(" %1%: plate %2%, return %3%")%__LINE__ %plate_index %ret; diff --git a/src/slic3r/GUI/Plater.hpp b/src/slic3r/GUI/Plater.hpp index f7bf0e93df7..1280f3529dc 100644 --- a/src/slic3r/GUI/Plater.hpp +++ b/src/slic3r/GUI/Plater.hpp @@ -541,6 +541,8 @@ class Plater: public wxPanel /// Sync notification state with current filament temp mixing status. /// Returns true if slicing is allowed, false if high/low temperature mixing blocks slicing. bool sync_filament_temp_mixing_notification(); + bool guard_before_slice_plate(); + bool guard_before_slice_all(); bool confirm_filament_temp_mixing_before_slice(); /// @brief Confirm warning-level high/low temperature material mixing before slicing all plates. /// @return True if slice-all can continue; otherwise false. From 8f8b89793283f15a1cc42a75cf240828ae6febf2 Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Wed, 10 Jun 2026 14:25:40 +0800 Subject: [PATCH 044/135] fix: add solid_infill_filament to Plater config for filament temp mixing detection The solid_infill_filament key was missing from p->config's default key list, causing collect_filament_slots_from_config() to never detect solid infill filament usage. This made the high/low temperature mixing guard silently skip solid infill while correctly catching all other auxiliary materials. Co-Authored-By: Claude Opus 4.7 --- src/slic3r/GUI/Plater.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index b98fc24109d..fd708b23268 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -9069,7 +9069,7 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame) "extruder_colour", "filament_colour", "material_colour", "printable_height", "printer_model", "printer_technology", // These values are necessary to construct SlicingParameters by the Canvas3D variable layer height editor. "layer_height", "initial_layer_print_height", "min_layer_height", "max_layer_height", - "brim_width", "wall_loops", "wall_filament", "sparse_infill_density", "sparse_infill_filament", "top_shell_layers", + "brim_width", "wall_loops", "wall_filament", "sparse_infill_density", "sparse_infill_filament", "solid_infill_filament", "top_shell_layers", "enable_support", "support_filament", "support_interface_filament", "support_top_z_distance", "support_bottom_z_distance", "raft_layers", "wipe_tower_rotation_angle", "wipe_tower_cone_angle", "wipe_tower_extra_spacing", "wipe_tower_extra_flow", "local_z_wipe_tower_purge_lines", "wipe_tower_max_purge_speed", From 8606acebcd67743ee871967dcea78ae69e36bf17 Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Wed, 10 Jun 2026 14:28:41 +0800 Subject: [PATCH 045/135] Fix tab out of sync when switching between Prepare and Preview views Co-Authored-By: Claude Opus 4.7 --- src/slic3r/GUI/Plater.cpp | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index fd708b23268..fa408f7abe6 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -8523,6 +8523,7 @@ struct Plater::priv PartPlateList partplate_list; //BBS: add a flag to ignore cancel event bool m_ignore_event{false}; + bool m_in_select_view_3D{false}; bool m_slice_all{false}; bool m_is_slicing {false}; bool m_is_publishing {false}; @@ -9703,12 +9704,17 @@ void Plater::priv::apply_free_camera_correction(bool apply/* = true*/) //BBS: add no slice option void Plater::priv::select_view_3D(const std::string& name, bool no_slice) { + if (m_in_select_view_3D) + return; + m_in_select_view_3D = true; + if (name == "3D") { BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << __LINE__ << "select view3D"; if (q->only_gcode_mode() || q->using_exported_file()) { BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format("goto preview page when loading gcode/exported_3mf"); } set_current_panel(view3D, no_slice); + wxGetApp().mainframe->select_tab(size_t(MainFrame::tp3DEditor)); } else if (name == "Preview") { BOOST_LOG_TRIVIAL(info) << "select preview"; @@ -9721,10 +9727,12 @@ void Plater::priv::select_view_3D(const std::string& name, bool no_slice) Model::setExtruderParams(config, numExtruders); Model::setPrintSpeedTable(config, print_config); set_current_panel(preview, no_slice); + wxGetApp().mainframe->select_tab(size_t(MainFrame::tpPreview)); } else if (name == "Assemble") { BOOST_LOG_TRIVIAL(info) << "select assemble view"; set_current_panel(assemble_view, no_slice); + wxGetApp().mainframe->select_tab(size_t(MainFrame::tp3DEditor)); } //BBS update selection @@ -9732,6 +9740,7 @@ void Plater::priv::select_view_3D(const std::string& name, bool no_slice) selection_changed(); apply_free_camera_correction(false); + m_in_select_view_3D = false; } void Plater::priv::select_next_view_3D() From c7a52493b848b16259ca874de715bdcf7ca4b4f1 Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Wed, 10 Jun 2026 14:31:49 +0800 Subject: [PATCH 046/135] fix: per-plate filament temp mixing check using global config check_filament_temp_mixing incorrectly used the Plater's global config (p->config) when checking per-plate filament temperature mixing. This caused false positives on plates that don't use mixing when a 3MF project contains both mixed and non-mixed plates. Remove the global config collection and instead resolve the default extruder for objects with extruder=0 from full_config(), so only the filament slots actually used by each plate's objects are considered. Co-Authored-By: Claude Opus 4.7 --- src/slic3r/GUI/Plater.cpp | 44 +++++++++++++++++++++------------------ 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index fa408f7abe6..c17bba08e75 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -264,19 +264,6 @@ static void collect_filament_slots_from_config( used_slots.insert(extruder_option->value - 1); } -static void collect_filament_slots_from_model_config( - const ModelConfigObject& config, - int num_filaments, - std::set& used_slots) -{ - if (!config.has("extruder")) - return; - - const int extruder_id = config.extruder(); - if (extruder_id >= 1 && extruder_id <= num_filaments) - used_slots.insert(extruder_id - 1); -} - wxDEFINE_EVENT(EVT_SCHEDULE_BACKGROUND_PROCESS, SimpleEvent); wxDEFINE_EVENT(EVT_SLICING_UPDATE, SlicingStatusEvent); wxDEFINE_EVENT(EVT_SLICING_COMPLETED, wxCommandEvent); @@ -20245,22 +20232,31 @@ bool Plater::check_filament_temp_mixing(int plate_index) if (!has_object_on_plate) return true; - // Collect from the Plater's working config for global Process settings. - // Defaults are 0 (skipped by the >= 1 check). - collect_filament_slots_from_config(*this->config(), num_filaments, used_slots); - // Also collect from current plate's config for any plate-level overrides if (plate) collect_filament_slots_from_config(*plate->config(), num_filaments, used_slots); - // Collect from ModelVolume painting extruders for objects on the current plate + // Collect from ModelVolume painting extruders and object configs for + // objects on the current plate. Track whether any object relies on the + // global default extruder so we can resolve it at the end. + bool uses_default_extruder = false; for (size_t obj_idx = 0; obj_idx < wxGetApp().model().objects.size(); ++obj_idx) { const ModelObject* model_object = wxGetApp().model().objects[obj_idx]; if (!model_object_is_on_plate(plate, obj_idx, model_object)) continue; - collect_filament_slots_from_model_config(model_object->config, num_filaments, used_slots); + + const int obj_extruder = model_object->config.extruder(); + if (obj_extruder >= 1 && obj_extruder <= num_filaments) + used_slots.insert(obj_extruder - 1); + else if (obj_extruder == 0) + uses_default_extruder = true; + for (const ModelVolume* model_volume : model_object->volumes) { - collect_filament_slots_from_model_config(model_volume->config, num_filaments, used_slots); + if (model_volume->config.has("extruder")) { + const int vol_extruder = model_volume->config.extruder(); + if (vol_extruder >= 1 && vol_extruder <= num_filaments) + used_slots.insert(vol_extruder - 1); + } for (int extruder_id : model_volume->get_extruders()) { if (extruder_id >= 1 && extruder_id <= num_filaments) used_slots.insert(extruder_id - 1); @@ -20268,6 +20264,14 @@ bool Plater::check_filament_temp_mixing(int plate_index) } } + // Resolve the global default extruder if any object on this plate + // uses extruder=0 (meaning "inherit from global config"). + if (uses_default_extruder) { + const ConfigOptionInt* extruder_opt = full_cfg.option("extruder"); + if (extruder_opt != nullptr && extruder_opt->value >= 1 && extruder_opt->value <= num_filaments) + used_slots.insert(extruder_opt->value - 1); + } + if (used_slots.empty()) return true; From 205dcecda1a4cb858f0b71e3b1157763715e784a Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Wed, 10 Jun 2026 15:16:37 +0800 Subject: [PATCH 047/135] Fix tab view sync and filament temp mixing notification lifecycle - Sync main tab with view panel in select_view_3D via select_tab - Skip redundant GL toolbar event post when view already matches tab - Allow Preview tab switch when plate is blocked by temp mixing - Always re-push filament temp mixing notification to handle external dismiss Co-Authored-By: Claude Opus 4.7 --- src/slic3r/GUI/MainFrame.cpp | 6 ++-- src/slic3r/GUI/Plater.cpp | 66 +++++++++++++++++------------------- 2 files changed, 36 insertions(+), 36 deletions(-) diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index 8ed5b47530d..de62b14f38a 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -1043,11 +1043,13 @@ void MainFrame::init_tabpanel() { m_last_selected_tab = m_tabpanel->GetSelection(); if (panel == m_plater) { if (sel == tp3DEditor) { - wxPostEvent(m_plater, SimpleEvent(EVT_GLVIEWTOOLBAR_3D)); + if (!m_plater || !m_plater->is_view3D_shown()) + wxPostEvent(m_plater, SimpleEvent(EVT_GLVIEWTOOLBAR_3D)); m_param_panel->OnActivate(); } else if (sel == tpPreview) { - wxPostEvent(m_plater, SimpleEvent(EVT_GLVIEWTOOLBAR_PREVIEW)); + if (!m_plater || m_plater->is_view3D_shown()) + wxPostEvent(m_plater, SimpleEvent(EVT_GLVIEWTOOLBAR_PREVIEW)); m_param_panel->OnActivate(); } } diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index c17bba08e75..844522e9b67 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -264,6 +264,19 @@ static void collect_filament_slots_from_config( used_slots.insert(extruder_option->value - 1); } +static void collect_filament_slots_from_model_config( + const ModelConfigObject& config, + int num_filaments, + std::set& used_slots) +{ + if (!config.has("extruder")) + return; + + const int extruder_id = config.extruder(); + if (extruder_id >= 1 && extruder_id <= num_filaments) + used_slots.insert(extruder_id - 1); +} + wxDEFINE_EVENT(EVT_SCHEDULE_BACKGROUND_PROCESS, SimpleEvent); wxDEFINE_EVENT(EVT_SLICING_UPDATE, SlicingStatusEvent); wxDEFINE_EVENT(EVT_SLICING_COMPLETED, wxCommandEvent); @@ -8510,7 +8523,6 @@ struct Plater::priv PartPlateList partplate_list; //BBS: add a flag to ignore cancel event bool m_ignore_event{false}; - bool m_in_select_view_3D{false}; bool m_slice_all{false}; bool m_is_slicing {false}; bool m_is_publishing {false}; @@ -9394,8 +9406,8 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame) return; } if (q->is_plate_blocked_by_filament_temp_mixing(q->get_partplate_list().get_curr_plate_index())) { - wxGetApp().mainframe->select_tab(size_t(MainFrame::tp3DEditor)); q->sync_filament_temp_mixing_notification(); + q->select_view_3D("Preview", true); return; } } @@ -9691,10 +9703,6 @@ void Plater::priv::apply_free_camera_correction(bool apply/* = true*/) //BBS: add no slice option void Plater::priv::select_view_3D(const std::string& name, bool no_slice) { - if (m_in_select_view_3D) - return; - m_in_select_view_3D = true; - if (name == "3D") { BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << __LINE__ << "select view3D"; if (q->only_gcode_mode() || q->using_exported_file()) { @@ -9727,7 +9735,6 @@ void Plater::priv::select_view_3D(const std::string& name, bool no_slice) selection_changed(); apply_free_camera_correction(false); - m_in_select_view_3D = false; } void Plater::priv::select_next_view_3D() @@ -20232,31 +20239,27 @@ bool Plater::check_filament_temp_mixing(int plate_index) if (!has_object_on_plate) return true; + // Collect from the Plater's working config for global Process settings. + // Defaults are 0 (skipped by the >= 1 check). + collect_filament_slots_from_config(*this->config(), num_filaments, used_slots); + // Also collect from current plate's config for any plate-level overrides if (plate) collect_filament_slots_from_config(*plate->config(), num_filaments, used_slots); - // Collect from ModelVolume painting extruders and object configs for - // objects on the current plate. Track whether any object relies on the - // global default extruder so we can resolve it at the end. + // Collect from ModelVolume painting extruders for objects on the + // current plate. Also track whether any object relies on the global + // default extruder (extruder=0) so we can resolve it at the end. bool uses_default_extruder = false; for (size_t obj_idx = 0; obj_idx < wxGetApp().model().objects.size(); ++obj_idx) { const ModelObject* model_object = wxGetApp().model().objects[obj_idx]; if (!model_object_is_on_plate(plate, obj_idx, model_object)) continue; - - const int obj_extruder = model_object->config.extruder(); - if (obj_extruder >= 1 && obj_extruder <= num_filaments) - used_slots.insert(obj_extruder - 1); - else if (obj_extruder == 0) + collect_filament_slots_from_model_config(model_object->config, num_filaments, used_slots); + if (model_object->config.extruder() == 0) uses_default_extruder = true; - for (const ModelVolume* model_volume : model_object->volumes) { - if (model_volume->config.has("extruder")) { - const int vol_extruder = model_volume->config.extruder(); - if (vol_extruder >= 1 && vol_extruder <= num_filaments) - used_slots.insert(vol_extruder - 1); - } + collect_filament_slots_from_model_config(model_volume->config, num_filaments, used_slots); for (int extruder_id : model_volume->get_extruders()) { if (extruder_id >= 1 && extruder_id <= num_filaments) used_slots.insert(extruder_id - 1); @@ -20265,7 +20268,9 @@ bool Plater::check_filament_temp_mixing(int plate_index) } // Resolve the global default extruder if any object on this plate - // uses extruder=0 (meaning "inherit from global config"). + // uses extruder=0. p->config does not include the "extruder" key + // (it is not in the initializer list at priv constructor), so we + // must read it from full_config() instead. if (uses_default_extruder) { const ConfigOptionInt* extruder_opt = full_cfg.option("extruder"); if (extruder_opt != nullptr && extruder_opt->value >= 1 && extruder_opt->value <= num_filaments) @@ -20351,10 +20356,6 @@ bool Plater::sync_filament_temp_mixing_notification() const int curr_plate_index = get_partplate_list().get_curr_plate_index(); PartPlate* curr_plate = get_partplate_list().get_curr_plate(); const FilamentTempMixingState mixing_state = get_filament_temp_mixing_state(curr_plate_index); - const bool notification_state_changed = - !p->filament_temp_mixing_notification_initialized || - p->filament_temp_mixing_notification_plate != curr_plate_index || - p->filament_temp_mixing_notification_state != mixing_state; bool slicing_allowed = true; switch (mixing_state) { @@ -20367,12 +20368,10 @@ bool Plater::sync_filament_temp_mixing_notification() case FilamentTempMixingState::AllowedWarning: get_notification_manager()->close_validate_error_notification(filament_temp_mixing_error_text()); get_partplate_list().get_curr_plate()->update_apply_result_invalid(false); - if (notification_state_changed) { - get_notification_manager()->push_notification( - NotificationType::ValidateWarning, - NotificationManager::NotificationLevel::WarningNotificationLevel, - _u8L("WARNING:") + "\n" + filament_temp_mixing_warning_text()); - } + get_notification_manager()->push_notification( + NotificationType::ValidateWarning, + NotificationManager::NotificationLevel::WarningNotificationLevel, + _u8L("WARNING:") + "\n" + filament_temp_mixing_warning_text()); slicing_allowed = true; break; case FilamentTempMixingState::BlockedError: { @@ -20380,8 +20379,7 @@ bool Plater::sync_filament_temp_mixing_notification() err.type = STRING_EXCEPT_FILAMENTS_DIFFERENT_TEMP; err.string = filament_temp_mixing_error_text(); get_notification_manager()->close_validate_warning_notification(filament_temp_mixing_warning_text()); - if (notification_state_changed) - get_notification_manager()->push_validate_error_notification(err); + get_notification_manager()->push_validate_error_notification(err); get_partplate_list().get_curr_plate()->update_apply_result_invalid(true); slicing_allowed = false; break; From d7906addc8cd750978d3a884ba52775cae9c64a7 Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Wed, 10 Jun 2026 15:47:57 +0800 Subject: [PATCH 048/135] fix: unify filament temp mixing confirm dialog style and add translations Replace wxMessageDialog with MessageDialog in both mixing confirm functions to match the "High and Low Temperature Material Mixing Risk" dialog style in Preferences. Change dialog title to "Confirm slicing" with custom Confirm/Cancel button labels. Add Chinese translations for "Confirm slicing" and the mixing risk message text. Co-Authored-By: Claude Opus 4.7 --- localization/i18n/Snapmaker_Orca.pot | 6 ++++++ localization/i18n/zh_CN/Snapmaker_Orca_zh_CN.po | 6 ++++++ src/slic3r/GUI/Plater.cpp | 16 ++++++++++------ 3 files changed, 22 insertions(+), 6 deletions(-) diff --git a/localization/i18n/Snapmaker_Orca.pot b/localization/i18n/Snapmaker_Orca.pot index e0de29e2733..bbccf182eda 100644 --- a/localization/i18n/Snapmaker_Orca.pot +++ b/localization/i18n/Snapmaker_Orca.pot @@ -2850,6 +2850,12 @@ msgstr "" msgid "Confirm" msgstr "" +msgid "Confirm slicing" +msgstr "" + +msgid "This material combination may cause risks. Do you want to continue?" +msgstr "" + msgid "Close" msgstr "" diff --git a/localization/i18n/zh_CN/Snapmaker_Orca_zh_CN.po b/localization/i18n/zh_CN/Snapmaker_Orca_zh_CN.po index 9b4b11f4e8d..690f452293a 100644 --- a/localization/i18n/zh_CN/Snapmaker_Orca_zh_CN.po +++ b/localization/i18n/zh_CN/Snapmaker_Orca_zh_CN.po @@ -2826,6 +2826,9 @@ msgstr "AMS 材料设置" msgid "Confirm" msgstr "确定" +msgid "Confirm slicing" +msgstr "请确认是否切片" + msgid "Close" msgstr "关闭" @@ -15268,6 +15271,9 @@ msgstr "检测到高温材料与低温材料同时使用。混合打印可能导 msgid "Detected both high and low temperature materials. Mixed printing may result in extruder clogging, nozzle damage, or layer adhesion issues. To continue printing, enable \"Allow mixed printing of high and low temperature materials\" in Preferences." msgstr "检测到高温材料与低温材料同时使用。混合打印可能导致挤出机堵塞、喷嘴损坏或层间粘附问题。如需继续打印,请在「偏好设置」中开启 \"允许高/低温材料混合打印\"。" +msgid "This material combination may cause risks. Do you want to continue?" +msgstr "此材料组合可能存在风险,是否继续?" + msgid "Allow high/low temperature filament mixing" msgstr "允许高/低温材料混合打印" diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 844522e9b67..b2b7abcdf5e 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -20418,9 +20418,11 @@ bool Plater::confirm_filament_temp_mixing_before_slice() break; } - wxMessageDialog dlg(this, _L("This material combination may cause risks. Do you want to continue?"), - _L("Confirm"), wxYES_NO | wxICON_WARNING); - return dlg.ShowModal() == wxID_YES; + MessageDialog dlg(this, _L("This material combination may cause risks. Do you want to continue?"), + _L("Confirm slicing"), wxICON_WARNING | wxOK | wxCANCEL); + dlg.SetButtonLabel(wxID_OK, _L("Confirm")); + dlg.SetButtonLabel(wxID_CANCEL, _L("Cancel")); + return dlg.ShowModal() == wxID_OK; } bool Plater::confirm_filament_temp_mixing_before_slice_all() @@ -20443,9 +20445,11 @@ bool Plater::confirm_filament_temp_mixing_before_slice_all() if (!has_allowed_warning) return true; - wxMessageDialog dlg(this, _L("This material combination may cause risks. Do you want to continue?"), - _L("Confirm"), wxYES_NO | wxICON_WARNING); - return dlg.ShowModal() == wxID_YES; + MessageDialog dlg(this, _L("This material combination may cause risks. Do you want to continue?"), + _L("Confirm slicing"), wxICON_WARNING | wxOK | wxCANCEL); + dlg.SetButtonLabel(wxID_OK, _L("Confirm")); + dlg.SetButtonLabel(wxID_CANCEL, _L("Cancel")); + return dlg.ShowModal() == wxID_OK; } void Plater::notify_filament_usage_changed() From d373bab187368e6b9b9edc01daffe3ad5b5b3e3e Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Mon, 8 Jun 2026 18:57:27 +0800 Subject: [PATCH 049/135] Fix filament temperature mixing notifications --- src/libslic3r/Print.cpp | 11 -- src/slic3r/GUI/GLCanvas3D.cpp | 2 +- src/slic3r/GUI/GUI_ObjectList.cpp | 6 +- .../GUI/Gizmos/GLGizmoMmuSegmentation.cpp | 2 +- src/slic3r/GUI/NotificationManager.cpp | 19 +++ src/slic3r/GUI/NotificationManager.hpp | 5 + src/slic3r/GUI/PartPlate.cpp | 2 +- src/slic3r/GUI/Plater.cpp | 158 ++++++++++++------ src/slic3r/GUI/Plater.hpp | 12 +- src/slic3r/GUI/Preferences.cpp | 3 + src/slic3r/GUI/Tab.cpp | 3 +- 11 files changed, 152 insertions(+), 71 deletions(-) diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index 1e2148afcc4..354bdc34918 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -1556,17 +1556,6 @@ StringObjectException Print::validate(StringObjectException *warning, Polygons* if (extruders.empty()) return { L("No extrusions under current settings.") }; - // Check for high/low temperature filament mixing. The check uses - // filament_is_high_temperature from each filament preset. This guard - // works for both single-nozzle and multi-nozzle printers. - if (extruders.size() > 1) { - auto ret = check_multi_filament_valid(*this); - if (!ret.string.empty()) { - ret.type = STRING_EXCEPT_FILAMENTS_DIFFERENT_TEMP; - return ret; - } - } - if (m_config.print_sequence == PrintSequence::ByObject) { if (m_config.timelapse_type == TimelapseType::tlSmooth) return {L("Smooth mode of timelapse is not supported when \"by object\" sequence is enabled.")}; diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp index 8538040aeff..bb63deaefbb 100644 --- a/src/slic3r/GUI/GLCanvas3D.cpp +++ b/src/slic3r/GUI/GLCanvas3D.cpp @@ -4823,7 +4823,7 @@ void GLCanvas3D::do_move(const std::string& snapshot_type) reset_sequential_print_clearance(); - wxGetApp().plater()->sync_filament_temp_mixing_notification(); + wxGetApp().plater()->notify_filament_usage_changed(); m_dirty = true; } diff --git a/src/slic3r/GUI/GUI_ObjectList.cpp b/src/slic3r/GUI/GUI_ObjectList.cpp index ea2064334f0..59073f2de0c 100644 --- a/src/slic3r/GUI/GUI_ObjectList.cpp +++ b/src/slic3r/GUI/GUI_ObjectList.cpp @@ -1100,7 +1100,7 @@ void ObjectList::update_filament_in_config(const wxDataViewItem& item) // update scene wxGetApp().plater()->update(); - wxGetApp().plater()->sync_filament_temp_mixing_notification(); + wxGetApp().plater()->notify_filament_usage_changed(); } void ObjectList::update_name_in_model(const wxDataViewItem& item) const @@ -5923,7 +5923,7 @@ void ObjectList::set_extruder_for_selected_items(const int extruder) // update scene wxGetApp().plater()->update(); - wxGetApp().plater()->sync_filament_temp_mixing_notification(); + wxGetApp().plater()->notify_filament_usage_changed(); // BBS: update extruder/filament column Refresh(); @@ -5983,7 +5983,7 @@ void ObjectList::reload_all_plates(bool notify_partplate) wxGetApp().plater()->update(); // update printable states on canvas wxGetApp().plater()->get_view3D_canvas3D()->update_instance_printable_state_for_objects(obj_idxs); - wxGetApp().plater()->sync_filament_temp_mixing_notification(); + wxGetApp().plater()->notify_filament_usage_changed(); } void ObjectList::on_plate_selected(int plate_index) diff --git a/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp b/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp index 0435434a4e9..82aba7c88cc 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp @@ -1000,7 +1000,7 @@ void GLGizmoMmuSegmentation::update_model_object() wxGetApp().obj_list()->update_info_items(obj_idx); wxGetApp().plater()->get_partplate_list().notify_instance_update(obj_idx, 0); m_parent.post_event(SimpleEvent(EVT_GLCANVAS_SCHEDULE_BACKGROUND_PROCESS)); - wxGetApp().plater()->sync_filament_temp_mixing_notification(); + wxGetApp().plater()->notify_filament_usage_changed(); } } diff --git a/src/slic3r/GUI/NotificationManager.cpp b/src/slic3r/GUI/NotificationManager.cpp index 0ff7d4d3d18..f2be434c92b 100644 --- a/src/slic3r/GUI/NotificationManager.cpp +++ b/src/slic3r/GUI/NotificationManager.cpp @@ -1836,6 +1836,16 @@ void NotificationManager::push_validate_error_notification(StringObjectException set_slicing_progress_hidden(); } +void NotificationManager::close_validate_error_notification(const std::string& text) +{ + close_notification_of_type_and_text(NotificationType::ValidateError, _u8L("Error:") + "\n" + text); +} + +void NotificationManager::close_validate_warning_notification(const std::string& text) +{ + close_notification_of_type_and_text(NotificationType::ValidateWarning, _u8L("WARNING:") + "\n" + text); +} + void NotificationManager::push_slicing_error_notification(const std::string &text, std::vector objs) { std::vector ids; @@ -2003,6 +2013,15 @@ void NotificationManager::close_notification_of_type(const NotificationType type } } } + +void NotificationManager::close_notification_of_type_and_text(const NotificationType type, const std::string& text) +{ + for (std::unique_ptr& notification : m_pop_notifications) { + if (notification->get_type() == type && notification->compare_text(text)) + notification->close(); + } +} + void NotificationManager::remove_slicing_warnings_of_released_objects(const std::vector& living_oids) { for (std::unique_ptr ¬ification : m_pop_notifications) diff --git a/src/slic3r/GUI/NotificationManager.hpp b/src/slic3r/GUI/NotificationManager.hpp index dce17f6080c..27c51f7cd5a 100644 --- a/src/slic3r/GUI/NotificationManager.hpp +++ b/src/slic3r/GUI/NotificationManager.hpp @@ -199,6 +199,8 @@ class NotificationManager void stop_delayed_notifications_of_type(const NotificationType type); // Creates Validate Error notification with a custom text and no fade out. void push_validate_error_notification(StringObjectException const & error); + void close_validate_error_notification(const std::string& text); + void close_validate_warning_notification(const std::string& text); // print host upload void push_upload_job_notification(int id, float filesize, const std::string& filename, const std::string& host, float percentage = 0); void set_upload_job_notification_percentage(int id, const std::string& filename, const std::string& host, float percentage); @@ -844,6 +846,7 @@ class NotificationManager //can be used to create custom notification bool push_notification_data(const NotificationData& notification_data, int timestamp); bool push_notification_data(std::unique_ptr notification, int timestamp); + void close_notification_of_type_and_text(const NotificationType type, const std::string& text); // Delayed notifications goes first to the m_waiting_notifications vector and only after remaining time is <= 0 // and condition callback is success, notification is regular pushed from update function. // Otherwise another delay interval waiting. Timestamp is 0. @@ -898,6 +901,8 @@ class NotificationManager NotificationType::PlaterWarning, NotificationType::ProgressBar, NotificationType::PrintHostUpload, + NotificationType::ValidateError, + NotificationType::ValidateWarning, NotificationType::SimplifySuggestion }; //prepared (basic) notifications diff --git a/src/slic3r/GUI/PartPlate.cpp b/src/slic3r/GUI/PartPlate.cpp index 736943da928..b29ea6d70f7 100644 --- a/src/slic3r/GUI/PartPlate.cpp +++ b/src/slic3r/GUI/PartPlate.cpp @@ -4527,7 +4527,7 @@ int PartPlateList::add_to_plate(int obj_id, int instance_id, int plate_id) } ret = plate->add_instance(obj_id, instance_id, true); - wxGetApp().plater()->sync_filament_temp_mixing_notification(); + wxGetApp().plater()->notify_filament_usage_changed(); return ret; } diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index ceae4a70298..69fb304d96a 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -201,6 +201,16 @@ static const std::pair THUMBNAIL_SIZE_3MF = { 512, 5 namespace Slic3r { namespace GUI { +static std::string filament_temp_mixing_warning_text() +{ + return _u8L("Detected both high and low temperature materials. Mixed printing may result in extruder clogging, nozzle damage, or layer adhesion issues."); +} + +static std::string filament_temp_mixing_error_text() +{ + return _u8L("Detected both high and low temperature materials. Mixed printing may result in extruder clogging, nozzle damage, or layer adhesion issues. To continue printing, enable \"Allow mixed printing of high and low temperature materials\" in Preferences."); +} + wxDEFINE_EVENT(EVT_SCHEDULE_BACKGROUND_PROCESS, SimpleEvent); wxDEFINE_EVENT(EVT_SLICING_UPDATE, SlicingStatusEvent); wxDEFINE_EVENT(EVT_SLICING_COMPLETED, wxCommandEvent); @@ -231,6 +241,7 @@ wxDEFINE_EVENT(EVT_PRINT_FROM_SDCARD_VIEW, SimpleEvent); wxDEFINE_EVENT(EVT_CREATE_FILAMENT, SimpleEvent); wxDEFINE_EVENT(EVT_MODIFY_FILAMENT, SimpleEvent); +wxDEFINE_EVENT(EVT_FILAMENT_USAGE_CHANGED, SimpleEvent); wxDEFINE_EVENT(EVT_ADD_FILAMENT, SimpleEvent); wxDEFINE_EVENT(EVT_DEL_FILAMENT, SimpleEvent); wxDEFINE_EVENT(EVT_ADD_CUSTOM_FILAMENT, ColorEvent); @@ -2869,8 +2880,7 @@ void Sidebar::update_presets(Preset::Type preset_type) update_dynamic_filament_list(); - // Real-time high/low temperature filament mixing check after filament preset change - p->plater->sync_filament_temp_mixing_notification(); + p->plater->notify_filament_usage_changed(); break; } @@ -8409,6 +8419,7 @@ struct Plater::priv // PIMPL back pointer ("Q-Pointer") Plater *q; MainFrame *main_frame; + bool filament_usage_sync_pending = false; MenuFactory menus; @@ -9057,6 +9068,10 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame) this->q->Bind(EVT_CREATE_FILAMENT, &priv::on_create_filament, this); this->q->Bind(EVT_MODIFY_FILAMENT, &priv::on_modify_filament, this); this->q->Bind(EVT_ADD_CUSTOM_FILAMENT, &priv::on_add_custom_filament, this); + this->q->Bind(EVT_FILAMENT_USAGE_CHANGED, [this](SimpleEvent&) { + filament_usage_sync_pending = false; + this->q->sync_filament_temp_mixing_notification(); + }); main_frame->m_tabpanel->Bind(wxEVT_NOTEBOOK_PAGE_CHANGING, &priv::on_tab_selection_changing, this); auto* panel_3d = new wxPanel(q); @@ -11250,7 +11265,7 @@ void Plater::priv::remove_curr_plate_all() view3D->remove_curr_plate_all(); this->sidebar->obj_list()->update_selections(); - q->sync_filament_temp_mixing_notification(); + q->notify_filament_usage_changed(); } void Plater::priv::select_all() @@ -11282,6 +11297,7 @@ void Plater::priv::remove(size_t obj_idx) // Delete object from Sidebar list. Do it after update, so that the GLScene selection is updated with the modified model. sidebar->obj_list()->delete_object_from_list(obj_idx); object_list_changed(); + q->notify_filament_usage_changed(); } @@ -11320,6 +11336,8 @@ bool Plater::priv::delete_object_from_model(size_t obj_idx, bool refresh_immedia object_list_changed(); } + q->notify_filament_usage_changed(); + return true; } @@ -11352,6 +11370,7 @@ void Plater::priv::delete_all_objects_from_model() //BBS model.calib_pa_pattern.reset(); model.plates_custom_gcodes.clear(); + q->notify_filament_usage_changed(); } void Plater::priv::reset(bool apply_presets_change) @@ -11731,6 +11750,10 @@ unsigned int Plater::priv::update_background_process(bool force_validation, bool BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": validate err=%1%, warning=%2%")%err.string%warning.string; if (err.string.empty()) { + // Pass a warning from validation and either show a notification, + // or hide the old one. + process_validation_warning(warning); + // Validate passed, but also check filament temp mixing as a final // guard. check_filament_temp_mixing() reads directly from preset // configs and catches cases that Print::validate() may miss (e.g. @@ -11744,9 +11767,6 @@ unsigned int Plater::priv::update_background_process(bool force_validation, bool if (invalidated != Print::APPLY_STATUS_UNCHANGED && background_processing_enabled()) return_state |= UPDATE_BACKGROUND_PROCESS_RESTART; - // Pass a warning from validation and either show a notification, - // or hide the old one. - process_validation_warning(warning); if (printer_technology == ptFFF) { view3D->get_canvas3d()->reset_sequential_print_clearance(); view3D->get_canvas3d()->set_as_dirty(); @@ -13656,14 +13676,8 @@ void Plater::priv::on_action_slice_plate(SimpleEvent&) Model::setPrintSpeedTable(config, print_config); m_slice_all = false; - // Pre-slice confirmation for high/low temp filament mixing - if (wxGetApp().app_config->get_bool("allow_filament_temp_mixing") - && !q->check_filament_temp_mixing()) { - wxMessageDialog dlg(q, _L("该组合可能导致风险,是否继续"), - _L("确认"), wxYES_NO | wxICON_WARNING); - if (dlg.ShowModal() != wxID_YES) - return; - } + if (!q->confirm_filament_temp_mixing_before_slice()) + return; q->reslice(); q->select_view_3D("Preview"); @@ -13684,14 +13698,8 @@ void Plater::priv::on_action_slice_all(SimpleEvent&) Model::setExtruderParams(config, numExtruders); Model::setPrintSpeedTable(config, print_config); - // Pre-slice confirmation for high/low temp filament mixing - if (wxGetApp().app_config->get_bool("allow_filament_temp_mixing") - && !q->check_filament_temp_mixing()) { - wxMessageDialog dlg(q, _L("该组合可能导致风险,是否继续"), - _L("确认"), wxYES_NO | wxICON_WARNING); - if (dlg.ShowModal() != wxID_YES) - return; - } + if (!q->confirm_filament_temp_mixing_before_slice()) + return; m_slice_all = true; m_slice_all_only_has_gcode = true; @@ -17152,7 +17160,10 @@ std::vector Plater::load_files(const std::vector& input_files, p->m_slice_all_only_has_gcode = false; //BBS: wish to reset all plates stats item selected state when load a new file p->preview->get_canvas3d()->reset_select_plate_toolbar_selection(); - return p->load_files(input_files, strategy, ask_multi); + std::vector loaded = p->load_files(input_files, strategy, ask_multi); + if (!loaded.empty()) + notify_filament_usage_changed(); + return loaded; } // To be called when providing a list of files to the GUI slic3r on command line. @@ -17162,7 +17173,7 @@ std::vector Plater::load_files(const std::vector& input_fil paths.reserve(input_files.size()); for (const std::string& path : input_files) paths.emplace_back(path); - return p->load_files(paths, strategy, ask_multi); + return load_files(paths, strategy, ask_multi); } bool Plater::preview_zip_archive(const boost::filesystem::path& archive_path) @@ -18049,8 +18060,10 @@ void Plater::remove_selected() //BBS delete current selected // p->view3D->delete_selected(); GLCanvas3D* canvas = p->get_current_canvas3D(); - if (canvas) + if (canvas) { canvas->delete_selected(); + notify_filament_usage_changed(); + } } void Plater::increase_instances(size_t num) @@ -20100,6 +20113,27 @@ bool Plater::check_filament_temp_mixing() int num_filaments = (int)ft_opt->values.size(); std::set used_slots; + PartPlate* curr_plate = p->partplate_list.get_curr_plate(); + auto object_is_on_curr_plate = [curr_plate](size_t obj_idx, const ModelObject* mo) { + if (!curr_plate) + return true; + for (int inst_idx = 0; inst_idx < (int)mo->instances.size(); ++inst_idx) { + if (curr_plate->contain_instance((int)obj_idx, inst_idx)) + return true; + } + return false; + }; + + bool has_object_on_curr_plate = false; + for (size_t obj_idx = 0; obj_idx < wxGetApp().model().objects.size(); ++obj_idx) { + if (object_is_on_curr_plate(obj_idx, wxGetApp().model().objects[obj_idx])) { + has_object_on_curr_plate = true; + break; + } + } + if (!has_object_on_curr_plate) + return true; + auto collect_from_cfg = [&](const DynamicPrintConfig& cfg) { static const char* keys_1based[] = {"wall_filament", "solid_infill_filament"}; for (auto key : keys_1based) { @@ -20120,23 +20154,13 @@ bool Plater::check_filament_temp_mixing() collect_from_cfg(*this->config()); // Also collect from current plate's config for any plate-level overrides - PartPlate* curr_plate = p->partplate_list.get_curr_plate(); if (curr_plate) collect_from_cfg(*curr_plate->config()); // Collect from ModelVolume painting extruders for objects on the current plate for (size_t obj_idx = 0; obj_idx < wxGetApp().model().objects.size(); ++obj_idx) { const ModelObject* mo = wxGetApp().model().objects[obj_idx]; - bool on_curr_plate = false; - if (curr_plate) { - for (int inst_idx = 0; inst_idx < (int)mo->instances.size(); ++inst_idx) { - if (curr_plate->contain_instance((int)obj_idx, inst_idx)) { - on_curr_plate = true; - break; - } - } - } - if (!on_curr_plate) + if (!object_is_on_curr_plate(obj_idx, mo)) continue; for (const ModelVolume* mv : mo->volumes) { for (int eid : mv->get_extruders()) { @@ -20174,30 +20198,62 @@ bool Plater::check_filament_temp_mixing() return compatible; } +Plater::FilamentTempMixingState Plater::get_filament_temp_mixing_state() +{ + if (check_filament_temp_mixing()) + return FilamentTempMixingState::Compatible; + + return wxGetApp().app_config->get_bool("allow_filament_temp_mixing") ? + FilamentTempMixingState::AllowedWarning : + FilamentTempMixingState::BlockedError; +} + bool Plater::sync_filament_temp_mixing_notification() { - if (check_filament_temp_mixing()) { - get_notification_manager()->close_notification_of_type(NotificationType::ValidateError); - get_notification_manager()->close_notification_of_type(NotificationType::ValidateWarning); + switch (get_filament_temp_mixing_state()) { + case FilamentTempMixingState::Compatible: + get_notification_manager()->close_validate_error_notification(filament_temp_mixing_error_text()); + get_notification_manager()->close_validate_warning_notification(filament_temp_mixing_warning_text()); get_partplate_list().get_curr_plate()->update_apply_result_invalid(false); return true; - } - - StringObjectException err; - err.type = STRING_EXCEPT_FILAMENTS_DIFFERENT_TEMP; - - if (wxGetApp().app_config->get_bool("allow_filament_temp_mixing")) { + case FilamentTempMixingState::AllowedWarning: + get_notification_manager()->close_validate_error_notification(filament_temp_mixing_error_text()); + get_partplate_list().get_curr_plate()->update_apply_result_invalid(false); get_notification_manager()->push_notification( NotificationType::ValidateWarning, NotificationManager::NotificationLevel::WarningNotificationLevel, - _u8L("WARNING:") + "\n" + - _u8L("Detected both high and low temperature materials. Mixed printing may result in extruder clogging, nozzle damage, or layer adhesion issues.")); - } else { - err.string = _u8L("Detected both high and low temperature materials. Mixed printing may result in extruder clogging, nozzle damage, or layer adhesion issues. To continue printing, enable \"Allow mixed printing of high and low temperature materials\" in Preferences."); + _u8L("WARNING:") + "\n" + filament_temp_mixing_warning_text()); + return true; + case FilamentTempMixingState::BlockedError: { + StringObjectException err; + err.type = STRING_EXCEPT_FILAMENTS_DIFFERENT_TEMP; + err.string = filament_temp_mixing_error_text(); + get_notification_manager()->close_validate_warning_notification(filament_temp_mixing_warning_text()); get_notification_manager()->push_validate_error_notification(err); get_partplate_list().get_curr_plate()->update_apply_result_invalid(true); + return false; } - return false; + } + + return true; +} + +bool Plater::confirm_filament_temp_mixing_before_slice() +{ + if (get_filament_temp_mixing_state() != FilamentTempMixingState::AllowedWarning) + return true; + + wxMessageDialog dlg(this, _L("该组合可能导致风险,是否继续"), _L("确认"), wxYES_NO | wxICON_WARNING); + return dlg.ShowModal() == wxID_YES; +} + +void Plater::notify_filament_usage_changed() +{ + if (p->filament_usage_sync_pending) + return; + + p->filament_usage_sync_pending = true; + wxQueueEvent(this, new SimpleEvent(EVT_FILAMENT_USAGE_CHANGED, this)); } void Plater::on_config_change(const DynamicPrintConfig &config) @@ -20289,7 +20345,7 @@ void Plater::on_config_change(const DynamicPrintConfig &config) update_title_dirty_status(); } - sync_filament_temp_mixing_notification(); + notify_filament_usage_changed(); } void Plater::set_bed_shape() const diff --git a/src/slic3r/GUI/Plater.hpp b/src/slic3r/GUI/Plater.hpp index a11ef680bef..429f635cee2 100644 --- a/src/slic3r/GUI/Plater.hpp +++ b/src/slic3r/GUI/Plater.hpp @@ -104,6 +104,7 @@ wxDECLARE_EVENT(EVT_GLCANVAS_COLOR_MODE_CHANGED, SimpleEvent); wxDECLARE_EVENT(EVT_PRINT_FROM_SDCARD_VIEW, SimpleEvent); wxDECLARE_EVENT(EVT_CREATE_FILAMENT, SimpleEvent); wxDECLARE_EVENT(EVT_MODIFY_FILAMENT, SimpleEvent); +wxDECLARE_EVENT(EVT_FILAMENT_USAGE_CHANGED, SimpleEvent); wxDECLARE_EVENT(EVT_ADD_FILAMENT, SimpleEvent); wxDECLARE_EVENT(EVT_DEL_FILAMENT, SimpleEvent); using ColorEvent = Event; @@ -231,6 +232,11 @@ class Plater: public wxPanel { public: using fs_path = boost::filesystem::path; + enum class FilamentTempMixingState { + Compatible, + AllowedWarning, + BlockedError + }; Plater(wxWindow *parent, MainFrame *main_frame); Plater(Plater &&) = delete; @@ -510,9 +516,13 @@ class Plater: public wxPanel /// Check whether high-temperature and low-temperature filaments are mixed on the current plate. /// Returns true if compatible (no mixing), false if mixing is detected and error notification is shown. bool check_filament_temp_mixing(); + FilamentTempMixingState get_filament_temp_mixing_state(); /// Sync notification state with current filament temp mixing status. - /// Returns true if compatible (no mixing), false if mixing detected. + /// Returns true if slicing is allowed, false if high/low temperature mixing blocks slicing. bool sync_filament_temp_mixing_notification(); + bool confirm_filament_temp_mixing_before_slice(); + /// Queue a single UI sync after filament preset/assignment/plate usage changes. + void notify_filament_usage_changed(); void force_filament_colors_update(); void force_print_bed_update(); // On activating the parent window. diff --git a/src/slic3r/GUI/Preferences.cpp b/src/slic3r/GUI/Preferences.cpp index 82291ebe2ca..9ab828e277c 100644 --- a/src/slic3r/GUI/Preferences.cpp +++ b/src/slic3r/GUI/Preferences.cpp @@ -762,6 +762,9 @@ wxBoxSizer *PreferencesDialog::create_item_checkbox(wxString title, wxWindow *pa app_config->set_bool(param, checkbox->GetValue()); app_config->save(); + if (param == "allow_filament_temp_mixing" && wxGetApp().plater()) + wxGetApp().plater()->notify_filament_usage_changed(); + if (param == PRIVACY_POLICY_FLAGS) { app_config->set("app", PRIVACY_POLICY_FLAGS, checkbox->GetValue()); diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index c9847509324..e8717247e47 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -3110,8 +3110,7 @@ void TabPrintModel::on_value_change(const std::string& opt_key, const boost::any } wxGetApp().params_panel()->notify_object_config_changed(); - // Real-time high/low temperature filament mixing check after object-level config change - wxGetApp().plater()->sync_filament_temp_mixing_notification(); + wxGetApp().plater()->notify_filament_usage_changed(); } void TabPrintModel::reload_config() From ca7f8d43b87f4ea3c07faf59356d8f2cd07ffdb5 Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Tue, 9 Jun 2026 18:18:47 +0800 Subject: [PATCH 050/135] Fix filament temperature mixing slice guards --- src/slic3r/GUI/GCodeViewer.cpp | 18 ++- src/slic3r/GUI/GLCanvas3D.cpp | 18 ++- src/slic3r/GUI/MainFrame.cpp | 7 +- src/slic3r/GUI/PartPlate.cpp | 4 + src/slic3r/GUI/Plater.cpp | 275 +++++++++++++++++++++++++-------- src/slic3r/GUI/Plater.hpp | 28 +++- 6 files changed, 271 insertions(+), 79 deletions(-) diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp index 23f9788899a..0bf41c17264 100644 --- a/src/slic3r/GUI/GCodeViewer.cpp +++ b/src/slic3r/GUI/GCodeViewer.cpp @@ -4049,10 +4049,10 @@ void GCodeViewer::render_shells(int canvas_width, int canvas_height) //BBS void GCodeViewer::render_all_plates_stats(const std::vector& gcode_result_list, bool show /*= true*/) const { - if (!show) + if (!show || gcode_result_list.empty()) return; - for (auto gcode_result : gcode_result_list) { - if (gcode_result->moves.size() == 0) + for (const GCodeProcessorResult* gcode_result : gcode_result_list) { + if (gcode_result == nullptr || gcode_result->moves.empty()) return; } ImGuiWrapper& imgui = *wxGetApp().imgui(); @@ -4076,7 +4076,7 @@ void GCodeViewer::render_all_plates_stats(const std::vector filament_colors; decode_colors(wxGetApp().plater()->get_extruder_colors_from_plater_config(gcode_result_list.back()), filament_colors); - for (int i = 0; i < filament_colors.size(); i++) { + for (size_t i = 0; i < filament_colors.size(); i++) { filament_colors[i] = adjust_color_for_rendering(filament_colors[i]); } @@ -4170,6 +4170,10 @@ void GCodeViewer::render_all_plates_stats(const std::vector(extruder_id) >= filament_diameters.size() || + static_cast(extruder_id) >= filament_densities.size()) + return std::pair{ 0.0, 0.0 }; + double koef = imperial_units ? 1.0 / GizmoObjectManipulation::in_to_mm : 0.001; std::pair ret = { koef * volume / (PI * sqr(0.5 * filament_diameters[extruder_id])), volume * filament_densities[extruder_id] * 0.001 }; @@ -4183,6 +4187,9 @@ void GCodeViewer::render_all_plates_stats(const std::vectorget_partplate_list(); for (auto plate : plate_list.get_nonempty_plate_list()) { + if (plate == nullptr || plate->get_slice_result() == nullptr) + continue; + auto plate_print_statistics = plate->get_slice_result()->print_statistics; for (const auto& [extruder_id, model_volume] : plate_print_statistics.model_volumes_per_extruder) { model_volume_of_extruders_all_plates[extruder_id] += model_volume; @@ -4201,7 +4208,8 @@ void GCodeViewer::render_all_plates_stats(const std::vectorget_print((PrintBase **) &print, nullptr, nullptr); - total_cost_all_plates += print->print_statistics().total_cost; + if (print != nullptr) + total_cost_all_plates += print->print_statistics().total_cost; } for (auto it = model_volume_of_extruders_all_plates.begin(); it != model_volume_of_extruders_all_plates.end(); it++) diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp index bb63deaefbb..595fe4861b8 100644 --- a/src/slic3r/GUI/GLCanvas3D.cpp +++ b/src/slic3r/GUI/GLCanvas3D.cpp @@ -7931,16 +7931,19 @@ void GLCanvas3D::_render_imgui_select_plate_toolbar() } if (m_sel_plate_toolbar.show_stats_item) { all_plates_stats_item->percent = 0.0f; + const std::vector nonempty_plate_list = plate_list.get_nonempty_plate_list(); + const std::vector nonempty_plate_results = plate_list.get_nonempty_plates_slice_results(); - for (auto plate : plate_list.get_nonempty_plate_list()) { + for (PartPlate* plate : nonempty_plate_list) { if (plate->is_slice_result_valid() && plate->is_slice_result_ready_for_print()) sliced_plates_cnt++; } - all_plates_stats_item->percent = (float)(sliced_plates_cnt) / (float)(plate_list.get_nonempty_plate_list().size()) * 100.0f; + if (!nonempty_plate_list.empty()) + all_plates_stats_item->percent = static_cast(sliced_plates_cnt) / static_cast(nonempty_plate_list.size()) * 100.0f; if (all_plates_stats_item->percent == 0.0f) all_plates_stats_item->slice_state = IMToolbarItem::SliceState::UNSLICED; - else if (sliced_plates_cnt == plate_list.get_nonempty_plate_list().size()) + else if (sliced_plates_cnt == nonempty_plate_list.size()) all_plates_stats_item->slice_state = IMToolbarItem::SliceState::SLICED; else if (all_plates_stats_item->percent < 100.0f) all_plates_stats_item->slice_state = IMToolbarItem::SliceState::SLICING; @@ -7955,17 +7958,18 @@ void GLCanvas3D::_render_imgui_select_plate_toolbar() // Changing parameters does not invalid all plates, need extra logic to validate bool gcode_result_valid = true; - for (auto gcode_result : plate_list.get_nonempty_plates_slice_results()) { - if (gcode_result->moves.size() == 0) { + for (const GCodeProcessorResult* gcode_result : nonempty_plate_results) { + if (gcode_result == nullptr || gcode_result->moves.empty()) { gcode_result_valid = false; + break; } } if (all_plates_stats_item->selected && all_plates_stats_item->slice_state == IMToolbarItem::SliceState::SLICED && gcode_result_valid) { - m_gcode_viewer.render_all_plates_stats(plate_list.get_nonempty_plates_slice_results()); + m_gcode_viewer.render_all_plates_stats(nonempty_plate_results); m_render_preview = false; } else{ - m_gcode_viewer.render_all_plates_stats(plate_list.get_nonempty_plates_slice_results(), false); + m_gcode_viewer.render_all_plates_stats(nonempty_plate_results, false); m_render_preview = true; } }else diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index 96b1b08a3af..66ff3ab24f2 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -1891,8 +1891,7 @@ bool MainFrame::get_enable_slice_status() { enable = false; }*/ - //always enable slice_all button - enable = true; + enable = m_plater->has_sliceable_plate_for_slice_all(); } else if (m_slice_select == eSlicePlate) { @@ -1904,6 +1903,10 @@ bool MainFrame::get_enable_slice_status() { enable = false; } + else if (m_plater->is_plate_blocked_by_filament_temp_mixing(part_plate_list.get_curr_plate_index())) + { + enable = false; + } } BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": m_slice_select %1%, enable= %2% ")%m_slice_select %enable; diff --git a/src/slic3r/GUI/PartPlate.cpp b/src/slic3r/GUI/PartPlate.cpp index b29ea6d70f7..9d9a1ab7252 100644 --- a/src/slic3r/GUI/PartPlate.cpp +++ b/src/slic3r/GUI/PartPlate.cpp @@ -4039,6 +4039,8 @@ int PartPlateList::select_plate(int index) return -1; } + const int old_plate_index = m_current_plate; + // BBS: erase unnecessary snapshot if (get_curr_plate_index() != index && m_intialized) { if (m_plater) @@ -4052,6 +4054,8 @@ int PartPlateList::select_plate(int index) m_current_plate = index; m_plate_list[m_current_plate]->set_selected(); + if (old_plate_index != m_current_plate && wxGetApp().plater()) + wxGetApp().plater()->notify_filament_usage_changed(); //BBS if(m_model) diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 69fb304d96a..2c43b5f75e4 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -211,6 +211,72 @@ static std::string filament_temp_mixing_error_text() return _u8L("Detected both high and low temperature materials. Mixed printing may result in extruder clogging, nozzle damage, or layer adhesion issues. To continue printing, enable \"Allow mixed printing of high and low temperature materials\" in Preferences."); } +static bool model_object_is_on_plate(PartPlate* plate, size_t obj_idx, const ModelObject* model_object) +{ + if (model_object == nullptr) + return false; + + if (plate == nullptr) + return true; + + const int object_index = static_cast(obj_idx); + const int instance_count = static_cast(model_object->instances.size()); + for (int instance_index = 0; instance_index < instance_count; ++instance_index) + { + if (plate->contain_instance(object_index, instance_index)) + return true; + } + + return false; +} + +static void collect_filament_slots_from_config( + const DynamicPrintConfig& config, + int num_filaments, + std::set& used_slots) +{ + static const std::vector keys_1based = { + "wall_filament", + "sparse_infill_filament", + "solid_infill_filament" + }; + for (const char* key : keys_1based) + { + const ConfigOptionInt* option = config.option(key); + if (option != nullptr && option->value >= 1 && option->value <= num_filaments) + used_slots.insert(option->value - 1); + } + + static const std::vector keys_0based = { + "support_filament", + "support_interface_filament", + "wipe_tower_filament" + }; + for (const char* key : keys_0based) + { + const ConfigOptionInt* option = config.option(key); + if (option != nullptr && option->value >= 1 && option->value <= num_filaments) + used_slots.insert(option->value - 1); + } + + const ConfigOptionInt* extruder_option = config.option("extruder"); + if (extruder_option != nullptr && extruder_option->value >= 1 && extruder_option->value <= num_filaments) + used_slots.insert(extruder_option->value - 1); +} + +static void collect_filament_slots_from_model_config( + const ModelConfigObject& config, + int num_filaments, + std::set& used_slots) +{ + if (!config.has("extruder")) + return; + + const int extruder_id = config.extruder(); + if (extruder_id >= 1 && extruder_id <= num_filaments) + used_slots.insert(extruder_id - 1); +} + wxDEFINE_EVENT(EVT_SCHEDULE_BACKGROUND_PROCESS, SimpleEvent); wxDEFINE_EVENT(EVT_SLICING_UPDATE, SlicingStatusEvent); wxDEFINE_EVENT(EVT_SLICING_COMPLETED, wxCommandEvent); @@ -8420,6 +8486,9 @@ struct Plater::priv Plater *q; MainFrame *main_frame; bool filament_usage_sync_pending = false; + bool filament_temp_mixing_notification_initialized = false; + int filament_temp_mixing_notification_plate = -1; + FilamentTempMixingState filament_temp_mixing_notification_state = FilamentTempMixingState::Compatible; MenuFactory menus; @@ -13594,7 +13663,11 @@ void Plater::priv::on_process_completed(SlicingProcessCompletedEvent &evt) else { BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(":slicing all, plate %1% finished, start next slice...")%m_cur_slice_plate; - m_cur_slice_plate++; + m_cur_slice_plate = q->find_next_sliceable_plate_for_slice_all(m_cur_slice_plate + 1); + if (m_cur_slice_plate < 0) { + m_is_slicing = false; + return; + } q->Freeze(); q->select_plate(m_cur_slice_plate); @@ -13698,12 +13771,16 @@ void Plater::priv::on_action_slice_all(SimpleEvent&) Model::setExtruderParams(config, numExtruders); Model::setPrintSpeedTable(config, print_config); - if (!q->confirm_filament_temp_mixing_before_slice()) + if (!q->confirm_filament_temp_mixing_before_slice_all()) return; m_slice_all = true; m_slice_all_only_has_gcode = true; - m_cur_slice_plate = 0; + m_cur_slice_plate = q->find_next_sliceable_plate_for_slice_all(0); + if (m_cur_slice_plate < 0) { + m_slice_all = false; + return; + } //select plate q->select_plate(m_cur_slice_plate); q->reslice(); @@ -20103,69 +20180,52 @@ void Plater::config_change_notification(const DynamicPrintConfig &config, const // notification for more options } -bool Plater::check_filament_temp_mixing() +bool Plater::check_filament_temp_mixing(int plate_index) { + const int plate_count = p->partplate_list.get_plate_count(); + if (plate_index < 0 || plate_index >= plate_count) + return true; + const DynamicPrintConfig& full_cfg = wxGetApp().preset_bundle->full_config(); - auto* ft_opt = full_cfg.option("filament_type"); - if (!ft_opt || ft_opt->values.empty()) + const ConfigOptionStrings* filament_type_option = full_cfg.option("filament_type"); + if (filament_type_option == nullptr || filament_type_option->values.empty()) return true; - int num_filaments = (int)ft_opt->values.size(); + const int num_filaments = static_cast(filament_type_option->values.size()); std::set used_slots; - PartPlate* curr_plate = p->partplate_list.get_curr_plate(); - auto object_is_on_curr_plate = [curr_plate](size_t obj_idx, const ModelObject* mo) { - if (!curr_plate) - return true; - for (int inst_idx = 0; inst_idx < (int)mo->instances.size(); ++inst_idx) { - if (curr_plate->contain_instance((int)obj_idx, inst_idx)) - return true; - } - return false; - }; + PartPlate* plate = p->partplate_list.get_plate(plate_index); - bool has_object_on_curr_plate = false; + bool has_object_on_plate = false; for (size_t obj_idx = 0; obj_idx < wxGetApp().model().objects.size(); ++obj_idx) { - if (object_is_on_curr_plate(obj_idx, wxGetApp().model().objects[obj_idx])) { - has_object_on_curr_plate = true; + const ModelObject* model_object = wxGetApp().model().objects[obj_idx]; + if (model_object_is_on_plate(plate, obj_idx, model_object)) { + has_object_on_plate = true; break; } } - if (!has_object_on_curr_plate) + if (!has_object_on_plate) return true; - auto collect_from_cfg = [&](const DynamicPrintConfig& cfg) { - static const char* keys_1based[] = {"wall_filament", "solid_infill_filament"}; - for (auto key : keys_1based) { - auto* opt = cfg.option(key); - if (opt && opt->value >= 1 && opt->value <= num_filaments) - used_slots.insert(opt->value - 1); - } - static const char* keys_0based[] = {"support_filament", "support_interface_filament", "wipe_tower_filament"}; - for (auto key : keys_0based) { - auto* opt = cfg.option(key); - if (opt && opt->value >= 1 && opt->value <= num_filaments) - used_slots.insert(opt->value - 1); - } - }; - // Collect from the Plater's working config for global Process settings. // Defaults are 0 (skipped by the >= 1 check). - collect_from_cfg(*this->config()); + collect_filament_slots_from_config(*this->config(), num_filaments, used_slots); // Also collect from current plate's config for any plate-level overrides - if (curr_plate) - collect_from_cfg(*curr_plate->config()); + if (plate) + collect_filament_slots_from_config(*plate->config(), num_filaments, used_slots); // Collect from ModelVolume painting extruders for objects on the current plate for (size_t obj_idx = 0; obj_idx < wxGetApp().model().objects.size(); ++obj_idx) { - const ModelObject* mo = wxGetApp().model().objects[obj_idx]; - if (!object_is_on_curr_plate(obj_idx, mo)) + const ModelObject* model_object = wxGetApp().model().objects[obj_idx]; + if (!model_object_is_on_plate(plate, obj_idx, model_object)) continue; - for (const ModelVolume* mv : mo->volumes) { - for (int eid : mv->get_extruders()) { - if (eid >= 1 && eid <= num_filaments) - used_slots.insert(eid - 1); + collect_filament_slots_from_model_config(model_object->config, num_filaments, used_slots); + for (const ModelVolume* model_volume : model_object->volumes) { + collect_filament_slots_from_model_config(model_volume->config, num_filaments, used_slots); + for (int extruder_id : model_volume->get_extruders()) { + if (extruder_id >= 1 && extruder_id <= num_filaments) + used_slots.insert(extruder_id - 1); } } } @@ -20181,11 +20241,11 @@ bool Plater::check_filament_temp_mixing() bool has_high = false, has_low = false; for (int slot : used_slots) { - if (slot < (int)bundle->filament_presets.size()) { + if (slot < static_cast(bundle->filament_presets.size())) { const Preset* preset = bundle->filaments.find_preset( bundle->filament_presets[slot], true); - if (preset) { - bool is_high = preset->config.opt_bool("filament_is_high_temperature", 0); + if (preset != nullptr) { + const bool is_high = preset->config.opt_bool("filament_is_high_temperature", 0); if (is_high) has_high = true; else @@ -20193,14 +20253,19 @@ bool Plater::check_filament_temp_mixing() } } } - bool compatible = !(has_high && has_low); + const bool compatible = !(has_high && has_low); return compatible; } -Plater::FilamentTempMixingState Plater::get_filament_temp_mixing_state() +bool Plater::check_filament_temp_mixing() +{ + return check_filament_temp_mixing(p->partplate_list.get_curr_plate_index()); +} + +Plater::FilamentTempMixingState Plater::get_filament_temp_mixing_state(int plate_index) { - if (check_filament_temp_mixing()) + if (check_filament_temp_mixing(plate_index)) return FilamentTempMixingState::Compatible; return wxGetApp().app_config->get_bool("allow_filament_temp_mixing") ? @@ -20208,42 +20273,125 @@ Plater::FilamentTempMixingState Plater::get_filament_temp_mixing_state() FilamentTempMixingState::BlockedError; } +Plater::FilamentTempMixingState Plater::get_filament_temp_mixing_state() +{ + return get_filament_temp_mixing_state(p->partplate_list.get_curr_plate_index()); +} + +bool Plater::is_plate_blocked_by_filament_temp_mixing(int plate_index) +{ + return get_filament_temp_mixing_state(plate_index) == FilamentTempMixingState::BlockedError; +} + +bool Plater::has_sliceable_plate_for_slice_all() +{ + return find_next_sliceable_plate_for_slice_all(0) >= 0; +} + +int Plater::find_next_sliceable_plate_for_slice_all(int start_plate_index) +{ + const int plate_count = p->partplate_list.get_plate_count(); + if (start_plate_index < 0) + start_plate_index = 0; + + for (int plate_index = start_plate_index; plate_index < plate_count; ++plate_index) + { + PartPlate* plate = p->partplate_list.get_plate(plate_index); + if (plate != nullptr && plate->can_slice() && !is_plate_blocked_by_filament_temp_mixing(plate_index)) + return plate_index; + } + + return -1; +} + bool Plater::sync_filament_temp_mixing_notification() { - switch (get_filament_temp_mixing_state()) { + const int curr_plate_index = get_partplate_list().get_curr_plate_index(); + PartPlate* curr_plate = get_partplate_list().get_curr_plate(); + const FilamentTempMixingState mixing_state = get_filament_temp_mixing_state(curr_plate_index); + const bool notification_state_changed = + !p->filament_temp_mixing_notification_initialized || + p->filament_temp_mixing_notification_plate != curr_plate_index || + p->filament_temp_mixing_notification_state != mixing_state; + bool slicing_allowed = true; + + switch (mixing_state) { case FilamentTempMixingState::Compatible: get_notification_manager()->close_validate_error_notification(filament_temp_mixing_error_text()); get_notification_manager()->close_validate_warning_notification(filament_temp_mixing_warning_text()); get_partplate_list().get_curr_plate()->update_apply_result_invalid(false); - return true; + slicing_allowed = true; + break; case FilamentTempMixingState::AllowedWarning: get_notification_manager()->close_validate_error_notification(filament_temp_mixing_error_text()); get_partplate_list().get_curr_plate()->update_apply_result_invalid(false); - get_notification_manager()->push_notification( - NotificationType::ValidateWarning, - NotificationManager::NotificationLevel::WarningNotificationLevel, - _u8L("WARNING:") + "\n" + filament_temp_mixing_warning_text()); - return true; + if (notification_state_changed) { + get_notification_manager()->push_notification( + NotificationType::ValidateWarning, + NotificationManager::NotificationLevel::WarningNotificationLevel, + _u8L("WARNING:") + "\n" + filament_temp_mixing_warning_text()); + } + slicing_allowed = true; + break; case FilamentTempMixingState::BlockedError: { StringObjectException err; err.type = STRING_EXCEPT_FILAMENTS_DIFFERENT_TEMP; err.string = filament_temp_mixing_error_text(); get_notification_manager()->close_validate_warning_notification(filament_temp_mixing_warning_text()); - get_notification_manager()->push_validate_error_notification(err); + if (notification_state_changed) + get_notification_manager()->push_validate_error_notification(err); get_partplate_list().get_curr_plate()->update_apply_result_invalid(true); - return false; + slicing_allowed = false; + break; } } - return true; + p->filament_temp_mixing_notification_initialized = true; + p->filament_temp_mixing_notification_plate = curr_plate_index; + p->filament_temp_mixing_notification_state = mixing_state; + + const bool can_slice = curr_plate != nullptr && curr_plate->can_slice() && slicing_allowed; + p->main_frame->update_slice_print_status(MainFrame::eEventPlateUpdate, can_slice); + return slicing_allowed; } bool Plater::confirm_filament_temp_mixing_before_slice() { - if (get_filament_temp_mixing_state() != FilamentTempMixingState::AllowedWarning) + switch (get_filament_temp_mixing_state()) { + case FilamentTempMixingState::Compatible: + return true; + case FilamentTempMixingState::BlockedError: + sync_filament_temp_mixing_notification(); + return false; + case FilamentTempMixingState::AllowedWarning: + break; + } + + wxMessageDialog dlg(this, _L("This material combination may cause risks. Do you want to continue?"), + _L("Confirm"), wxYES_NO | wxICON_WARNING); + return dlg.ShowModal() == wxID_YES; +} + +bool Plater::confirm_filament_temp_mixing_before_slice_all() +{ + if (!has_sliceable_plate_for_slice_all()) + return false; + + bool has_allowed_warning = false; + for (int plate_index = 0; plate_index < p->partplate_list.get_plate_count(); ++plate_index) + { + if (get_filament_temp_mixing_state(plate_index) == FilamentTempMixingState::AllowedWarning) + { + has_allowed_warning = true; + break; + } + } + + if (!has_allowed_warning) return true; - wxMessageDialog dlg(this, _L("该组合可能导致风险,是否继续"), _L("确认"), wxYES_NO | wxICON_WARNING); + wxMessageDialog dlg(this, _L("This material combination may cause risks. Do you want to continue?"), + _L("Confirm"), wxYES_NO | wxICON_WARNING); return dlg.ShowModal() == wxID_YES; } @@ -21087,6 +21235,7 @@ int Plater::select_plate(int plate_index, bool need_slice) SimpleEvent event(EVT_GLCANVAS_PLATE_SELECT); p->on_plate_selected(event); + notify_filament_usage_changed(); BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(" %1%: plate %2%, return %3%")%__LINE__ %plate_index %ret; return ret; diff --git a/src/slic3r/GUI/Plater.hpp b/src/slic3r/GUI/Plater.hpp index 429f635cee2..f7bf0e93df7 100644 --- a/src/slic3r/GUI/Plater.hpp +++ b/src/slic3r/GUI/Plater.hpp @@ -513,14 +513,38 @@ class Plater: public wxPanel bool update_filament_colors_in_full_config(); void config_change_notification(const DynamicPrintConfig &config, const std::string& key); void on_config_change(const DynamicPrintConfig &config); - /// Check whether high-temperature and low-temperature filaments are mixed on the current plate. - /// Returns true if compatible (no mixing), false if mixing is detected and error notification is shown. + /// @brief Check whether high-temperature and low-temperature filaments are mixed on the current plate. + /// @return True if compatible; false if high/low temperature materials are mixed. bool check_filament_temp_mixing(); + /// @brief Check whether high-temperature and low-temperature filaments are mixed on a specific plate. + /// @param plate_index Plate index to check. + /// @return True if compatible or plate index is invalid; false if high/low temperature materials are mixed. + bool check_filament_temp_mixing(int plate_index); + /// @brief Get high/low temperature material mixing state for the current plate. + /// @return Current plate material mixing state. FilamentTempMixingState get_filament_temp_mixing_state(); + /// @brief Get high/low temperature material mixing state for a specific plate. + /// @param plate_index Plate index to check. + /// @return Plate material mixing state. + FilamentTempMixingState get_filament_temp_mixing_state(int plate_index); + /// @brief Check whether a specific plate is blocked by high/low temperature material mixing. + /// @param plate_index Plate index to check. + /// @return True if slicing this plate is blocked; otherwise false. + bool is_plate_blocked_by_filament_temp_mixing(int plate_index); + /// @brief Check whether slice-all has at least one plate that can be sliced. + /// @return True if any plate can be sliced and is not blocked by material mixing. + bool has_sliceable_plate_for_slice_all(); + /// @brief Find the next plate that can be sliced by slice-all. + /// @param start_plate_index First plate index to check. + /// @return Plate index if found; otherwise -1. + int find_next_sliceable_plate_for_slice_all(int start_plate_index); /// Sync notification state with current filament temp mixing status. /// Returns true if slicing is allowed, false if high/low temperature mixing blocks slicing. bool sync_filament_temp_mixing_notification(); bool confirm_filament_temp_mixing_before_slice(); + /// @brief Confirm warning-level high/low temperature material mixing before slicing all plates. + /// @return True if slice-all can continue; otherwise false. + bool confirm_filament_temp_mixing_before_slice_all(); /// Queue a single UI sync after filament preset/assignment/plate usage changes. void notify_filament_usage_changed(); void force_filament_colors_update(); From 64b3d4fe02c8563c24cb55c8853691e9935010ad Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Wed, 10 Jun 2026 12:00:44 +0800 Subject: [PATCH 051/135] Fix filament temperature mixing slice guards Co-Authored-By: Claude Opus 4.7 --- src/slic3r/GUI/MainFrame.cpp | 15 +++++---- src/slic3r/GUI/Plater.cpp | 60 ++++++++++++++++++++++++++++++++---- src/slic3r/GUI/Plater.hpp | 2 ++ 3 files changed, 63 insertions(+), 14 deletions(-) diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index 66ff3ab24f2..8ed5b47530d 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -565,7 +565,7 @@ DPIFrame(NULL, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, BORDERLESS_FRAME_ } return;} #endif - if (evt.CmdDown() && evt.GetKeyCode() == 'R') { if (m_slice_enable) { wxGetApp().plater()->update(true, true); wxPostEvent(m_plater, SimpleEvent(EVT_GLTOOLBAR_SLICE_PLATE)); this->m_tabpanel->SetSelection(tpPreview); } return; } + if (evt.CmdDown() && evt.GetKeyCode() == 'R') { if (m_slice_enable) { wxGetApp().plater()->update(true, true); wxPostEvent(m_plater, SimpleEvent(EVT_GLTOOLBAR_SLICE_PLATE)); } return; } if (evt.CmdDown() && evt.ShiftDown() && evt.GetKeyCode() == 'G') { m_plater->apply_background_progress(); m_print_enable = get_enable_print_status(); @@ -1659,7 +1659,6 @@ wxBoxSizer* MainFrame::create_side_tools() else wxPostEvent(m_plater, SimpleEvent(EVT_GLTOOLBAR_SLICE_PLATE)); - this->m_tabpanel->SetSelection(tpPreview); }); m_print_btn->Bind(wxEVT_BUTTON, [this](wxCommandEvent& event) @@ -2054,11 +2053,11 @@ void MainFrame::update_slice_print_status(SlicePrintEventType event, bool can_sl { bool enable_print = true, enable_slice = true; - if (!can_slice) - { - if (m_slice_select == eSlicePlate) - enable_slice = false; - } + if (event == eEventPlateUpdate) + enable_slice = get_enable_slice_status(); + else if (!can_slice) + enable_slice = false; + if (!can_print) enable_print = false; @@ -2070,7 +2069,7 @@ void MainFrame::update_slice_print_status(SlicePrintEventType event, bool can_sl } //process slice logic - if (enable_slice) + if (event != eEventPlateUpdate && enable_slice) { enable_slice = get_enable_slice_status(); } diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 2c43b5f75e4..b98fc24109d 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -9398,7 +9398,21 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame) q->Bind(EVT_EXPORT_FINISHED, &priv::on_export_finished, this); q->Bind(EVT_GLVIEWTOOLBAR_3D, [q](SimpleEvent&) { q->select_view_3D("3D"); }); //BBS: set on_slice to false - q->Bind(EVT_GLVIEWTOOLBAR_PREVIEW, [q](SimpleEvent&) { q->select_view_3D("Preview", false); }); + q->Bind(EVT_GLVIEWTOOLBAR_PREVIEW, [q](SimpleEvent&) { + if (q->is_view3D_shown()) { + if (q->has_sliceable_plate_for_slice_all()) { + wxGetApp().mainframe->select_tab(size_t(MainFrame::tp3DEditor)); + wxPostEvent(q, SimpleEvent(EVT_GLTOOLBAR_SLICE_ALL)); + return; + } + if (q->is_plate_blocked_by_filament_temp_mixing(q->get_partplate_list().get_curr_plate_index())) { + wxGetApp().mainframe->select_tab(size_t(MainFrame::tp3DEditor)); + q->sync_filament_temp_mixing_notification(); + return; + } + } + q->select_view_3D("Preview", false); + }); q->Bind(EVT_GLTOOLBAR_SLICE_PLATE, &priv::on_action_slice_plate, this); q->Bind(EVT_GLTOOLBAR_SLICE_ALL, &priv::on_action_slice_all, this); q->Bind(EVT_GLTOOLBAR_PRINT_PLATE, &priv::on_action_print_plate, this); @@ -11833,7 +11847,10 @@ unsigned int Plater::priv::update_background_process(bool force_validation, bool notification_manager->set_all_slicing_errors_gray(true); notification_manager->close_notification_of_type(NotificationType::ValidateError); } - if (invalidated != Print::APPLY_STATUS_UNCHANGED && background_processing_enabled()) + else { + return_state |= UPDATE_BACKGROUND_PROCESS_INVALID; + } + if (filament_ok && invalidated != Print::APPLY_STATUS_UNCHANGED && background_processing_enabled()) return_state |= UPDATE_BACKGROUND_PROCESS_RESTART; if (printer_technology == ptFFF) { @@ -13749,7 +13766,7 @@ void Plater::priv::on_action_slice_plate(SimpleEvent&) Model::setPrintSpeedTable(config, print_config); m_slice_all = false; - if (!q->confirm_filament_temp_mixing_before_slice()) + if (!q->guard_before_slice_plate()) return; q->reslice(); @@ -13771,7 +13788,7 @@ void Plater::priv::on_action_slice_all(SimpleEvent&) Model::setExtruderParams(config, numExtruders); Model::setPrintSpeedTable(config, print_config); - if (!q->confirm_filament_temp_mixing_before_slice_all()) + if (!q->guard_before_slice_all()) return; m_slice_all = true; @@ -19409,6 +19426,18 @@ int Plater::start_next_slice() // Stop arrange and (or) optimize rotation tasks. //this->stop_jobs(); + if (is_plate_blocked_by_filament_temp_mixing(p->partplate_list.get_curr_plate_index())) { + sync_filament_temp_mixing_notification(); + if (p->m_slice_all) { + SlicingProcessCompletedEvent evt(EVT_PROCESS_COMPLETED, 0, + SlicingProcessCompletedEvent::Finished, nullptr); + wxQueueEvent(this, evt.Clone()); + return 0; + } + p->process_completed_with_error = p->partplate_list.get_curr_plate_index(); + return -1; + } + //FIXME Don't reslice if export of G-code or sending to OctoPrint is running. // bitmask of UpdateBackgroundProcessReturnState unsigned int state = this->p->update_background_process(true, false, false); @@ -20355,6 +20384,17 @@ bool Plater::sync_filament_temp_mixing_notification() return slicing_allowed; } +bool Plater::guard_before_slice_plate() +{ + sync_filament_temp_mixing_notification(); + return confirm_filament_temp_mixing_before_slice(); +} + +bool Plater::guard_before_slice_all() +{ + return confirm_filament_temp_mixing_before_slice_all(); +} + bool Plater::confirm_filament_temp_mixing_before_slice() { switch (get_filament_temp_mixing_state()) { @@ -20380,7 +20420,9 @@ bool Plater::confirm_filament_temp_mixing_before_slice_all() bool has_allowed_warning = false; for (int plate_index = 0; plate_index < p->partplate_list.get_plate_count(); ++plate_index) { - if (get_filament_temp_mixing_state(plate_index) == FilamentTempMixingState::AllowedWarning) + PartPlate* plate = p->partplate_list.get_plate(plate_index); + if (plate != nullptr && plate->can_slice() && + get_filament_temp_mixing_state(plate_index) == FilamentTempMixingState::AllowedWarning) { has_allowed_warning = true; break; @@ -21135,6 +21177,8 @@ int Plater::select_plate(int plate_index, bool need_slice) p->process_completed_with_error = -1; p->m_slice_all = false; reset_gcode_toolpaths(); + if (!guard_before_slice_plate()) + return ret; reslice(); } else { @@ -21189,8 +21233,11 @@ int Plater::select_plate(int plate_index, bool need_slice) //p->process_completed_with_error = -1; p->m_slice_all = false; reset_gcode_toolpaths(); - if (model_fits && !validate_err) + if (model_fits && !validate_err) { + if (!guard_before_slice_plate()) + return ret; reslice(); + } else { p->main_frame->update_slice_print_status(MainFrame::eEventPlateUpdate, false); //sometimes the previous print's sliced result is still valid, but the newly added object is laid over the boundary @@ -21235,6 +21282,7 @@ int Plater::select_plate(int plate_index, bool need_slice) SimpleEvent event(EVT_GLCANVAS_PLATE_SELECT); p->on_plate_selected(event); + sync_filament_temp_mixing_notification(); notify_filament_usage_changed(); BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(" %1%: plate %2%, return %3%")%__LINE__ %plate_index %ret; diff --git a/src/slic3r/GUI/Plater.hpp b/src/slic3r/GUI/Plater.hpp index f7bf0e93df7..1280f3529dc 100644 --- a/src/slic3r/GUI/Plater.hpp +++ b/src/slic3r/GUI/Plater.hpp @@ -541,6 +541,8 @@ class Plater: public wxPanel /// Sync notification state with current filament temp mixing status. /// Returns true if slicing is allowed, false if high/low temperature mixing blocks slicing. bool sync_filament_temp_mixing_notification(); + bool guard_before_slice_plate(); + bool guard_before_slice_all(); bool confirm_filament_temp_mixing_before_slice(); /// @brief Confirm warning-level high/low temperature material mixing before slicing all plates. /// @return True if slice-all can continue; otherwise false. From 2cd1e1b7d54c746f86a332df03564eaac5377376 Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Wed, 10 Jun 2026 14:25:40 +0800 Subject: [PATCH 052/135] fix: add solid_infill_filament to Plater config for filament temp mixing detection The solid_infill_filament key was missing from p->config's default key list, causing collect_filament_slots_from_config() to never detect solid infill filament usage. This made the high/low temperature mixing guard silently skip solid infill while correctly catching all other auxiliary materials. Co-Authored-By: Claude Opus 4.7 --- src/slic3r/GUI/Plater.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index b98fc24109d..fd708b23268 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -9069,7 +9069,7 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame) "extruder_colour", "filament_colour", "material_colour", "printable_height", "printer_model", "printer_technology", // These values are necessary to construct SlicingParameters by the Canvas3D variable layer height editor. "layer_height", "initial_layer_print_height", "min_layer_height", "max_layer_height", - "brim_width", "wall_loops", "wall_filament", "sparse_infill_density", "sparse_infill_filament", "top_shell_layers", + "brim_width", "wall_loops", "wall_filament", "sparse_infill_density", "sparse_infill_filament", "solid_infill_filament", "top_shell_layers", "enable_support", "support_filament", "support_interface_filament", "support_top_z_distance", "support_bottom_z_distance", "raft_layers", "wipe_tower_rotation_angle", "wipe_tower_cone_angle", "wipe_tower_extra_spacing", "wipe_tower_extra_flow", "local_z_wipe_tower_purge_lines", "wipe_tower_max_purge_speed", From b1f322b5be4f3052f9d03e522ccde4e64adf3b75 Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Wed, 10 Jun 2026 14:28:41 +0800 Subject: [PATCH 053/135] Fix tab out of sync when switching between Prepare and Preview views Co-Authored-By: Claude Opus 4.7 --- src/slic3r/GUI/Plater.cpp | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index fd708b23268..fa408f7abe6 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -8523,6 +8523,7 @@ struct Plater::priv PartPlateList partplate_list; //BBS: add a flag to ignore cancel event bool m_ignore_event{false}; + bool m_in_select_view_3D{false}; bool m_slice_all{false}; bool m_is_slicing {false}; bool m_is_publishing {false}; @@ -9703,12 +9704,17 @@ void Plater::priv::apply_free_camera_correction(bool apply/* = true*/) //BBS: add no slice option void Plater::priv::select_view_3D(const std::string& name, bool no_slice) { + if (m_in_select_view_3D) + return; + m_in_select_view_3D = true; + if (name == "3D") { BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << __LINE__ << "select view3D"; if (q->only_gcode_mode() || q->using_exported_file()) { BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format("goto preview page when loading gcode/exported_3mf"); } set_current_panel(view3D, no_slice); + wxGetApp().mainframe->select_tab(size_t(MainFrame::tp3DEditor)); } else if (name == "Preview") { BOOST_LOG_TRIVIAL(info) << "select preview"; @@ -9721,10 +9727,12 @@ void Plater::priv::select_view_3D(const std::string& name, bool no_slice) Model::setExtruderParams(config, numExtruders); Model::setPrintSpeedTable(config, print_config); set_current_panel(preview, no_slice); + wxGetApp().mainframe->select_tab(size_t(MainFrame::tpPreview)); } else if (name == "Assemble") { BOOST_LOG_TRIVIAL(info) << "select assemble view"; set_current_panel(assemble_view, no_slice); + wxGetApp().mainframe->select_tab(size_t(MainFrame::tp3DEditor)); } //BBS update selection @@ -9732,6 +9740,7 @@ void Plater::priv::select_view_3D(const std::string& name, bool no_slice) selection_changed(); apply_free_camera_correction(false); + m_in_select_view_3D = false; } void Plater::priv::select_next_view_3D() From 78a10be3f4070dcff0583a9495abaf191565f244 Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Wed, 10 Jun 2026 14:31:49 +0800 Subject: [PATCH 054/135] fix: per-plate filament temp mixing check using global config check_filament_temp_mixing incorrectly used the Plater's global config (p->config) when checking per-plate filament temperature mixing. This caused false positives on plates that don't use mixing when a 3MF project contains both mixed and non-mixed plates. Remove the global config collection and instead resolve the default extruder for objects with extruder=0 from full_config(), so only the filament slots actually used by each plate's objects are considered. Co-Authored-By: Claude Opus 4.7 --- src/slic3r/GUI/Plater.cpp | 44 +++++++++++++++++++++------------------ 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index fa408f7abe6..c17bba08e75 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -264,19 +264,6 @@ static void collect_filament_slots_from_config( used_slots.insert(extruder_option->value - 1); } -static void collect_filament_slots_from_model_config( - const ModelConfigObject& config, - int num_filaments, - std::set& used_slots) -{ - if (!config.has("extruder")) - return; - - const int extruder_id = config.extruder(); - if (extruder_id >= 1 && extruder_id <= num_filaments) - used_slots.insert(extruder_id - 1); -} - wxDEFINE_EVENT(EVT_SCHEDULE_BACKGROUND_PROCESS, SimpleEvent); wxDEFINE_EVENT(EVT_SLICING_UPDATE, SlicingStatusEvent); wxDEFINE_EVENT(EVT_SLICING_COMPLETED, wxCommandEvent); @@ -20245,22 +20232,31 @@ bool Plater::check_filament_temp_mixing(int plate_index) if (!has_object_on_plate) return true; - // Collect from the Plater's working config for global Process settings. - // Defaults are 0 (skipped by the >= 1 check). - collect_filament_slots_from_config(*this->config(), num_filaments, used_slots); - // Also collect from current plate's config for any plate-level overrides if (plate) collect_filament_slots_from_config(*plate->config(), num_filaments, used_slots); - // Collect from ModelVolume painting extruders for objects on the current plate + // Collect from ModelVolume painting extruders and object configs for + // objects on the current plate. Track whether any object relies on the + // global default extruder so we can resolve it at the end. + bool uses_default_extruder = false; for (size_t obj_idx = 0; obj_idx < wxGetApp().model().objects.size(); ++obj_idx) { const ModelObject* model_object = wxGetApp().model().objects[obj_idx]; if (!model_object_is_on_plate(plate, obj_idx, model_object)) continue; - collect_filament_slots_from_model_config(model_object->config, num_filaments, used_slots); + + const int obj_extruder = model_object->config.extruder(); + if (obj_extruder >= 1 && obj_extruder <= num_filaments) + used_slots.insert(obj_extruder - 1); + else if (obj_extruder == 0) + uses_default_extruder = true; + for (const ModelVolume* model_volume : model_object->volumes) { - collect_filament_slots_from_model_config(model_volume->config, num_filaments, used_slots); + if (model_volume->config.has("extruder")) { + const int vol_extruder = model_volume->config.extruder(); + if (vol_extruder >= 1 && vol_extruder <= num_filaments) + used_slots.insert(vol_extruder - 1); + } for (int extruder_id : model_volume->get_extruders()) { if (extruder_id >= 1 && extruder_id <= num_filaments) used_slots.insert(extruder_id - 1); @@ -20268,6 +20264,14 @@ bool Plater::check_filament_temp_mixing(int plate_index) } } + // Resolve the global default extruder if any object on this plate + // uses extruder=0 (meaning "inherit from global config"). + if (uses_default_extruder) { + const ConfigOptionInt* extruder_opt = full_cfg.option("extruder"); + if (extruder_opt != nullptr && extruder_opt->value >= 1 && extruder_opt->value <= num_filaments) + used_slots.insert(extruder_opt->value - 1); + } + if (used_slots.empty()) return true; From 7b7187ce4723d2439bff379396608ae46e8d0c03 Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Wed, 10 Jun 2026 15:16:37 +0800 Subject: [PATCH 055/135] Fix tab view sync and filament temp mixing notification lifecycle - Sync main tab with view panel in select_view_3D via select_tab - Skip redundant GL toolbar event post when view already matches tab - Allow Preview tab switch when plate is blocked by temp mixing - Always re-push filament temp mixing notification to handle external dismiss Co-Authored-By: Claude Opus 4.7 --- src/slic3r/GUI/MainFrame.cpp | 6 ++-- src/slic3r/GUI/Plater.cpp | 66 +++++++++++++++++------------------- 2 files changed, 36 insertions(+), 36 deletions(-) diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index 8ed5b47530d..de62b14f38a 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -1043,11 +1043,13 @@ void MainFrame::init_tabpanel() { m_last_selected_tab = m_tabpanel->GetSelection(); if (panel == m_plater) { if (sel == tp3DEditor) { - wxPostEvent(m_plater, SimpleEvent(EVT_GLVIEWTOOLBAR_3D)); + if (!m_plater || !m_plater->is_view3D_shown()) + wxPostEvent(m_plater, SimpleEvent(EVT_GLVIEWTOOLBAR_3D)); m_param_panel->OnActivate(); } else if (sel == tpPreview) { - wxPostEvent(m_plater, SimpleEvent(EVT_GLVIEWTOOLBAR_PREVIEW)); + if (!m_plater || m_plater->is_view3D_shown()) + wxPostEvent(m_plater, SimpleEvent(EVT_GLVIEWTOOLBAR_PREVIEW)); m_param_panel->OnActivate(); } } diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index c17bba08e75..844522e9b67 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -264,6 +264,19 @@ static void collect_filament_slots_from_config( used_slots.insert(extruder_option->value - 1); } +static void collect_filament_slots_from_model_config( + const ModelConfigObject& config, + int num_filaments, + std::set& used_slots) +{ + if (!config.has("extruder")) + return; + + const int extruder_id = config.extruder(); + if (extruder_id >= 1 && extruder_id <= num_filaments) + used_slots.insert(extruder_id - 1); +} + wxDEFINE_EVENT(EVT_SCHEDULE_BACKGROUND_PROCESS, SimpleEvent); wxDEFINE_EVENT(EVT_SLICING_UPDATE, SlicingStatusEvent); wxDEFINE_EVENT(EVT_SLICING_COMPLETED, wxCommandEvent); @@ -8510,7 +8523,6 @@ struct Plater::priv PartPlateList partplate_list; //BBS: add a flag to ignore cancel event bool m_ignore_event{false}; - bool m_in_select_view_3D{false}; bool m_slice_all{false}; bool m_is_slicing {false}; bool m_is_publishing {false}; @@ -9394,8 +9406,8 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame) return; } if (q->is_plate_blocked_by_filament_temp_mixing(q->get_partplate_list().get_curr_plate_index())) { - wxGetApp().mainframe->select_tab(size_t(MainFrame::tp3DEditor)); q->sync_filament_temp_mixing_notification(); + q->select_view_3D("Preview", true); return; } } @@ -9691,10 +9703,6 @@ void Plater::priv::apply_free_camera_correction(bool apply/* = true*/) //BBS: add no slice option void Plater::priv::select_view_3D(const std::string& name, bool no_slice) { - if (m_in_select_view_3D) - return; - m_in_select_view_3D = true; - if (name == "3D") { BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << __LINE__ << "select view3D"; if (q->only_gcode_mode() || q->using_exported_file()) { @@ -9727,7 +9735,6 @@ void Plater::priv::select_view_3D(const std::string& name, bool no_slice) selection_changed(); apply_free_camera_correction(false); - m_in_select_view_3D = false; } void Plater::priv::select_next_view_3D() @@ -20232,31 +20239,27 @@ bool Plater::check_filament_temp_mixing(int plate_index) if (!has_object_on_plate) return true; + // Collect from the Plater's working config for global Process settings. + // Defaults are 0 (skipped by the >= 1 check). + collect_filament_slots_from_config(*this->config(), num_filaments, used_slots); + // Also collect from current plate's config for any plate-level overrides if (plate) collect_filament_slots_from_config(*plate->config(), num_filaments, used_slots); - // Collect from ModelVolume painting extruders and object configs for - // objects on the current plate. Track whether any object relies on the - // global default extruder so we can resolve it at the end. + // Collect from ModelVolume painting extruders for objects on the + // current plate. Also track whether any object relies on the global + // default extruder (extruder=0) so we can resolve it at the end. bool uses_default_extruder = false; for (size_t obj_idx = 0; obj_idx < wxGetApp().model().objects.size(); ++obj_idx) { const ModelObject* model_object = wxGetApp().model().objects[obj_idx]; if (!model_object_is_on_plate(plate, obj_idx, model_object)) continue; - - const int obj_extruder = model_object->config.extruder(); - if (obj_extruder >= 1 && obj_extruder <= num_filaments) - used_slots.insert(obj_extruder - 1); - else if (obj_extruder == 0) + collect_filament_slots_from_model_config(model_object->config, num_filaments, used_slots); + if (model_object->config.extruder() == 0) uses_default_extruder = true; - for (const ModelVolume* model_volume : model_object->volumes) { - if (model_volume->config.has("extruder")) { - const int vol_extruder = model_volume->config.extruder(); - if (vol_extruder >= 1 && vol_extruder <= num_filaments) - used_slots.insert(vol_extruder - 1); - } + collect_filament_slots_from_model_config(model_volume->config, num_filaments, used_slots); for (int extruder_id : model_volume->get_extruders()) { if (extruder_id >= 1 && extruder_id <= num_filaments) used_slots.insert(extruder_id - 1); @@ -20265,7 +20268,9 @@ bool Plater::check_filament_temp_mixing(int plate_index) } // Resolve the global default extruder if any object on this plate - // uses extruder=0 (meaning "inherit from global config"). + // uses extruder=0. p->config does not include the "extruder" key + // (it is not in the initializer list at priv constructor), so we + // must read it from full_config() instead. if (uses_default_extruder) { const ConfigOptionInt* extruder_opt = full_cfg.option("extruder"); if (extruder_opt != nullptr && extruder_opt->value >= 1 && extruder_opt->value <= num_filaments) @@ -20351,10 +20356,6 @@ bool Plater::sync_filament_temp_mixing_notification() const int curr_plate_index = get_partplate_list().get_curr_plate_index(); PartPlate* curr_plate = get_partplate_list().get_curr_plate(); const FilamentTempMixingState mixing_state = get_filament_temp_mixing_state(curr_plate_index); - const bool notification_state_changed = - !p->filament_temp_mixing_notification_initialized || - p->filament_temp_mixing_notification_plate != curr_plate_index || - p->filament_temp_mixing_notification_state != mixing_state; bool slicing_allowed = true; switch (mixing_state) { @@ -20367,12 +20368,10 @@ bool Plater::sync_filament_temp_mixing_notification() case FilamentTempMixingState::AllowedWarning: get_notification_manager()->close_validate_error_notification(filament_temp_mixing_error_text()); get_partplate_list().get_curr_plate()->update_apply_result_invalid(false); - if (notification_state_changed) { - get_notification_manager()->push_notification( - NotificationType::ValidateWarning, - NotificationManager::NotificationLevel::WarningNotificationLevel, - _u8L("WARNING:") + "\n" + filament_temp_mixing_warning_text()); - } + get_notification_manager()->push_notification( + NotificationType::ValidateWarning, + NotificationManager::NotificationLevel::WarningNotificationLevel, + _u8L("WARNING:") + "\n" + filament_temp_mixing_warning_text()); slicing_allowed = true; break; case FilamentTempMixingState::BlockedError: { @@ -20380,8 +20379,7 @@ bool Plater::sync_filament_temp_mixing_notification() err.type = STRING_EXCEPT_FILAMENTS_DIFFERENT_TEMP; err.string = filament_temp_mixing_error_text(); get_notification_manager()->close_validate_warning_notification(filament_temp_mixing_warning_text()); - if (notification_state_changed) - get_notification_manager()->push_validate_error_notification(err); + get_notification_manager()->push_validate_error_notification(err); get_partplate_list().get_curr_plate()->update_apply_result_invalid(true); slicing_allowed = false; break; From 62454d9c96a424e360a11aa3089369b23b427225 Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Wed, 10 Jun 2026 15:47:57 +0800 Subject: [PATCH 056/135] fix: unify filament temp mixing confirm dialog style and add translations Replace wxMessageDialog with MessageDialog in both mixing confirm functions to match the "High and Low Temperature Material Mixing Risk" dialog style in Preferences. Change dialog title to "Confirm slicing" with custom Confirm/Cancel button labels. Add Chinese translations for "Confirm slicing" and the mixing risk message text. Co-Authored-By: Claude Opus 4.7 --- localization/i18n/Snapmaker_Orca.pot | 6 ++++++ localization/i18n/zh_CN/Snapmaker_Orca_zh_CN.po | 6 ++++++ src/slic3r/GUI/Plater.cpp | 16 ++++++++++------ 3 files changed, 22 insertions(+), 6 deletions(-) diff --git a/localization/i18n/Snapmaker_Orca.pot b/localization/i18n/Snapmaker_Orca.pot index e0de29e2733..bbccf182eda 100644 --- a/localization/i18n/Snapmaker_Orca.pot +++ b/localization/i18n/Snapmaker_Orca.pot @@ -2850,6 +2850,12 @@ msgstr "" msgid "Confirm" msgstr "" +msgid "Confirm slicing" +msgstr "" + +msgid "This material combination may cause risks. Do you want to continue?" +msgstr "" + msgid "Close" msgstr "" diff --git a/localization/i18n/zh_CN/Snapmaker_Orca_zh_CN.po b/localization/i18n/zh_CN/Snapmaker_Orca_zh_CN.po index 9b4b11f4e8d..690f452293a 100644 --- a/localization/i18n/zh_CN/Snapmaker_Orca_zh_CN.po +++ b/localization/i18n/zh_CN/Snapmaker_Orca_zh_CN.po @@ -2826,6 +2826,9 @@ msgstr "AMS 材料设置" msgid "Confirm" msgstr "确定" +msgid "Confirm slicing" +msgstr "请确认是否切片" + msgid "Close" msgstr "关闭" @@ -15268,6 +15271,9 @@ msgstr "检测到高温材料与低温材料同时使用。混合打印可能导 msgid "Detected both high and low temperature materials. Mixed printing may result in extruder clogging, nozzle damage, or layer adhesion issues. To continue printing, enable \"Allow mixed printing of high and low temperature materials\" in Preferences." msgstr "检测到高温材料与低温材料同时使用。混合打印可能导致挤出机堵塞、喷嘴损坏或层间粘附问题。如需继续打印,请在「偏好设置」中开启 \"允许高/低温材料混合打印\"。" +msgid "This material combination may cause risks. Do you want to continue?" +msgstr "此材料组合可能存在风险,是否继续?" + msgid "Allow high/low temperature filament mixing" msgstr "允许高/低温材料混合打印" diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 844522e9b67..b2b7abcdf5e 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -20418,9 +20418,11 @@ bool Plater::confirm_filament_temp_mixing_before_slice() break; } - wxMessageDialog dlg(this, _L("This material combination may cause risks. Do you want to continue?"), - _L("Confirm"), wxYES_NO | wxICON_WARNING); - return dlg.ShowModal() == wxID_YES; + MessageDialog dlg(this, _L("This material combination may cause risks. Do you want to continue?"), + _L("Confirm slicing"), wxICON_WARNING | wxOK | wxCANCEL); + dlg.SetButtonLabel(wxID_OK, _L("Confirm")); + dlg.SetButtonLabel(wxID_CANCEL, _L("Cancel")); + return dlg.ShowModal() == wxID_OK; } bool Plater::confirm_filament_temp_mixing_before_slice_all() @@ -20443,9 +20445,11 @@ bool Plater::confirm_filament_temp_mixing_before_slice_all() if (!has_allowed_warning) return true; - wxMessageDialog dlg(this, _L("This material combination may cause risks. Do you want to continue?"), - _L("Confirm"), wxYES_NO | wxICON_WARNING); - return dlg.ShowModal() == wxID_YES; + MessageDialog dlg(this, _L("This material combination may cause risks. Do you want to continue?"), + _L("Confirm slicing"), wxICON_WARNING | wxOK | wxCANCEL); + dlg.SetButtonLabel(wxID_OK, _L("Confirm")); + dlg.SetButtonLabel(wxID_CANCEL, _L("Cancel")); + return dlg.ShowModal() == wxID_OK; } void Plater::notify_filament_usage_changed() From 3332c0e86af95e703b7fc651343db9039d2c2251 Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Thu, 11 Jun 2026 12:12:07 +0800 Subject: [PATCH 057/135] fix: exclude global Process config from per-plate filament temp mixing check The global Plater config's wall_filament/solid_infill_filament etc. were unconditionally collected for every plate, causing false positives when a filament assigned in the global config was not actually used on a given plate (e.g., solid_infill_filament=3(ABS) flagged Plate 2 which only used extruder=0(PLA-AERO)). Remove collect_filament_slots_from_config(*this->config(), ...) so the per-plate check only considers plate-level overrides, object/volume extruder assignments, and global default extruder resolution. Co-Authored-By: Claude Opus 4.7 --- src/slic3r/GUI/Plater.cpp | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index b2b7abcdf5e..1774b68c0e0 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -20239,10 +20239,6 @@ bool Plater::check_filament_temp_mixing(int plate_index) if (!has_object_on_plate) return true; - // Collect from the Plater's working config for global Process settings. - // Defaults are 0 (skipped by the >= 1 check). - collect_filament_slots_from_config(*this->config(), num_filaments, used_slots); - // Also collect from current plate's config for any plate-level overrides if (plate) collect_filament_slots_from_config(*plate->config(), num_filaments, used_slots); From f92eb7d9091f49c4291018ed3a28ec1792eb635d Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Thu, 11 Jun 2026 15:26:53 +0800 Subject: [PATCH 058/135] fix: "Slice All" button stuck disabled after loading multi-plate project After loading a 3MF with plates that have different filament temp mixing states, update_slice_print_status(eEventSliceUpdate, false) from update_background_process hard-disabled the button without consulting get_enable_slice_status(), which correctly checks all plates in eSliceAll mode. Also initialize m_apply_invalid for all plates synchronously after load instead of waiting for the async event on the current plate only. Co-Authored-By: Claude Opus 4.7 --- src/slic3r/GUI/MainFrame.cpp | 5 ++++- src/slic3r/GUI/Plater.cpp | 18 +++++++++++++++++- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index de62b14f38a..b9d5fd1e61e 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -2058,7 +2058,10 @@ void MainFrame::update_slice_print_status(SlicePrintEventType event, bool can_sl if (event == eEventPlateUpdate) enable_slice = get_enable_slice_status(); else if (!can_slice) - enable_slice = false; + // Don't hard-disable the slice button; let get_enable_slice_status() + // decide. In eSliceAll mode other plates may still be sliceable even + // if the caller thinks the current plate isn't. + enable_slice = get_enable_slice_status(); if (!can_print) enable_print = false; diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 1774b68c0e0..de8deb9fae6 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -17258,8 +17258,24 @@ std::vector Plater::load_files(const std::vector& input_files, //BBS: wish to reset all plates stats item selected state when load a new file p->preview->get_canvas3d()->reset_select_plate_toolbar_selection(); std::vector loaded = p->load_files(input_files, strategy, ask_multi); - if (!loaded.empty()) + if (!loaded.empty()) { + // After loading a project, initialize the filament temp mixing state + // for ALL plates, not just the current one. This ensures each plate's + // m_apply_invalid flag is correct so that "Slice All" mode correctly + // detects which plates are sliceable. + PartPlateList& plate_list = get_partplate_list(); + for (int i = 0; i < plate_list.get_plate_count(); ++i) { + FilamentTempMixingState state = get_filament_temp_mixing_state(i); + PartPlate* plate = plate_list.get_plate(i); + if (plate) { + plate->update_apply_result_invalid( + state == FilamentTempMixingState::BlockedError); + } + } notify_filament_usage_changed(); + // Force a sync for the current plate's notification display + sync_filament_temp_mixing_notification(); + } return loaded; } From 3ba61358bba7f76cf8cc925809f02334bd50c327 Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Thu, 11 Jun 2026 17:00:32 +0800 Subject: [PATCH 059/135] fix: restore filament temp mixing validation in Print::validate for CLI, fix style issues - Restore check_multi_filament_valid() in Print::validate() so CLI slicing is protected against high/low temperature filament mixing (the GUI layer has its own per-plate check that runs before validate()). This also reactivates the CLI_FILAMENTS_DIFFERENT_TEMP error path. - Fix Allman brace style for all new code in Plater.cpp (18 occurrences). - Wrap two long string literals (206, 211) to fit within 120 characters. - Add const qualifier on immutable local variable in load_files(). - Add Doxygen comments for guard_before_slice_plate/all and confirm_filament_temp_mixing_before_slice in Plater.hpp. - Fix indentation consistency in NotificationManager.hpp. Co-Authored-By: Claude Opus 4.7 --- src/libslic3r/Print.cpp | 9 ++++ src/slic3r/GUI/NotificationManager.hpp | 6 +-- src/slic3r/GUI/Plater.cpp | 72 ++++++++++++++++++-------- src/slic3r/GUI/Plater.hpp | 7 ++- 4 files changed, 67 insertions(+), 27 deletions(-) diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index 354bdc34918..0b8655fbf83 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -1556,6 +1556,15 @@ StringObjectException Print::validate(StringObjectException *warning, Polygons* if (extruders.empty()) return { L("No extrusions under current settings.") }; + // Check for high/low temperature filament mixing. + if (extruders.size() > 1) { + auto ret = check_multi_filament_valid(*this); + if (!ret.string.empty()) { + ret.type = STRING_EXCEPT_FILAMENTS_DIFFERENT_TEMP; + return ret; + } + } + if (m_config.print_sequence == PrintSequence::ByObject) { if (m_config.timelapse_type == TimelapseType::tlSmooth) return {L("Smooth mode of timelapse is not supported when \"by object\" sequence is enabled.")}; diff --git a/src/slic3r/GUI/NotificationManager.hpp b/src/slic3r/GUI/NotificationManager.hpp index 27c51f7cd5a..dd3d4ff9457 100644 --- a/src/slic3r/GUI/NotificationManager.hpp +++ b/src/slic3r/GUI/NotificationManager.hpp @@ -901,9 +901,9 @@ class NotificationManager NotificationType::PlaterWarning, NotificationType::ProgressBar, NotificationType::PrintHostUpload, - NotificationType::ValidateError, - NotificationType::ValidateWarning, - NotificationType::SimplifySuggestion + NotificationType::ValidateError, + NotificationType::ValidateWarning, + NotificationType::SimplifySuggestion }; //prepared (basic) notifications // non-static so its not loaded too early. If static, the translations wont load correctly. diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index de8deb9fae6..0793408fe47 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -203,12 +203,18 @@ namespace GUI { static std::string filament_temp_mixing_warning_text() { - return _u8L("Detected both high and low temperature materials. Mixed printing may result in extruder clogging, nozzle damage, or layer adhesion issues."); + return _u8L("Detected both high and low temperature materials. " + "Mixed printing may result in extruder clogging, " + "nozzle damage, or layer adhesion issues."); } static std::string filament_temp_mixing_error_text() { - return _u8L("Detected both high and low temperature materials. Mixed printing may result in extruder clogging, nozzle damage, or layer adhesion issues. To continue printing, enable \"Allow mixed printing of high and low temperature materials\" in Preferences."); + return _u8L("Detected both high and low temperature materials. " + "Mixed printing may result in extruder clogging, " + "nozzle damage, or layer adhesion issues. " + "To continue printing, enable \"Allow mixed printing " + "of high and low temperature materials\" in Preferences."); } static bool model_object_is_on_plate(PartPlate* plate, size_t obj_idx, const ModelObject* model_object) @@ -13684,7 +13690,8 @@ void Plater::priv::on_process_completed(SlicingProcessCompletedEvent &evt) { BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(":slicing all, plate %1% finished, start next slice...")%m_cur_slice_plate; m_cur_slice_plate = q->find_next_sliceable_plate_for_slice_all(m_cur_slice_plate + 1); - if (m_cur_slice_plate < 0) { + if (m_cur_slice_plate < 0) + { m_is_slicing = false; return; } @@ -13797,7 +13804,8 @@ void Plater::priv::on_action_slice_all(SimpleEvent&) m_slice_all = true; m_slice_all_only_has_gcode = true; m_cur_slice_plate = q->find_next_sliceable_plate_for_slice_all(0); - if (m_cur_slice_plate < 0) { + if (m_cur_slice_plate < 0) + { m_slice_all = false; return; } @@ -17258,16 +17266,19 @@ std::vector Plater::load_files(const std::vector& input_files, //BBS: wish to reset all plates stats item selected state when load a new file p->preview->get_canvas3d()->reset_select_plate_toolbar_selection(); std::vector loaded = p->load_files(input_files, strategy, ask_multi); - if (!loaded.empty()) { + if (!loaded.empty()) + { // After loading a project, initialize the filament temp mixing state // for ALL plates, not just the current one. This ensures each plate's // m_apply_invalid flag is correct so that "Slice All" mode correctly // detects which plates are sliceable. PartPlateList& plate_list = get_partplate_list(); - for (int i = 0; i < plate_list.get_plate_count(); ++i) { - FilamentTempMixingState state = get_filament_temp_mixing_state(i); + for (int i = 0; i < plate_list.get_plate_count(); ++i) + { + const FilamentTempMixingState state = get_filament_temp_mixing_state(i); PartPlate* plate = plate_list.get_plate(i); - if (plate) { + if (plate) + { plate->update_apply_result_invalid( state == FilamentTempMixingState::BlockedError); } @@ -19445,9 +19456,11 @@ int Plater::start_next_slice() // Stop arrange and (or) optimize rotation tasks. //this->stop_jobs(); - if (is_plate_blocked_by_filament_temp_mixing(p->partplate_list.get_curr_plate_index())) { + if (is_plate_blocked_by_filament_temp_mixing(p->partplate_list.get_curr_plate_index())) + { sync_filament_temp_mixing_notification(); - if (p->m_slice_all) { + if (p->m_slice_all) + { SlicingProcessCompletedEvent evt(EVT_PROCESS_COMPLETED, 0, SlicingProcessCompletedEvent::Finished, nullptr); wxQueueEvent(this, evt.Clone()); @@ -20245,9 +20258,11 @@ bool Plater::check_filament_temp_mixing(int plate_index) PartPlate* plate = p->partplate_list.get_plate(plate_index); bool has_object_on_plate = false; - for (size_t obj_idx = 0; obj_idx < wxGetApp().model().objects.size(); ++obj_idx) { + for (size_t obj_idx = 0; obj_idx < wxGetApp().model().objects.size(); ++obj_idx) + { const ModelObject* model_object = wxGetApp().model().objects[obj_idx]; - if (model_object_is_on_plate(plate, obj_idx, model_object)) { + if (model_object_is_on_plate(plate, obj_idx, model_object)) + { has_object_on_plate = true; break; } @@ -20263,16 +20278,19 @@ bool Plater::check_filament_temp_mixing(int plate_index) // current plate. Also track whether any object relies on the global // default extruder (extruder=0) so we can resolve it at the end. bool uses_default_extruder = false; - for (size_t obj_idx = 0; obj_idx < wxGetApp().model().objects.size(); ++obj_idx) { + for (size_t obj_idx = 0; obj_idx < wxGetApp().model().objects.size(); ++obj_idx) + { const ModelObject* model_object = wxGetApp().model().objects[obj_idx]; if (!model_object_is_on_plate(plate, obj_idx, model_object)) continue; collect_filament_slots_from_model_config(model_object->config, num_filaments, used_slots); if (model_object->config.extruder() == 0) uses_default_extruder = true; - for (const ModelVolume* model_volume : model_object->volumes) { + for (const ModelVolume* model_volume : model_object->volumes) + { collect_filament_slots_from_model_config(model_volume->config, num_filaments, used_slots); - for (int extruder_id : model_volume->get_extruders()) { + for (int extruder_id : model_volume->get_extruders()) + { if (extruder_id >= 1 && extruder_id <= num_filaments) used_slots.insert(extruder_id - 1); } @@ -20283,7 +20301,8 @@ bool Plater::check_filament_temp_mixing(int plate_index) // uses extruder=0. p->config does not include the "extruder" key // (it is not in the initializer list at priv constructor), so we // must read it from full_config() instead. - if (uses_default_extruder) { + if (uses_default_extruder) + { const ConfigOptionInt* extruder_opt = full_cfg.option("extruder"); if (extruder_opt != nullptr && extruder_opt->value >= 1 && extruder_opt->value <= num_filaments) used_slots.insert(extruder_opt->value - 1); @@ -20299,11 +20318,14 @@ bool Plater::check_filament_temp_mixing(int plate_index) PresetBundle* bundle = wxGetApp().preset_bundle; bool has_high = false, has_low = false; - for (int slot : used_slots) { - if (slot < static_cast(bundle->filament_presets.size())) { + for (int slot : used_slots) + { + if (slot < static_cast(bundle->filament_presets.size())) + { const Preset* preset = bundle->filaments.find_preset( bundle->filament_presets[slot], true); - if (preset != nullptr) { + if (preset != nullptr) + { const bool is_high = preset->config.opt_bool("filament_is_high_temperature", 0); if (is_high) has_high = true; @@ -20370,7 +20392,8 @@ bool Plater::sync_filament_temp_mixing_notification() const FilamentTempMixingState mixing_state = get_filament_temp_mixing_state(curr_plate_index); bool slicing_allowed = true; - switch (mixing_state) { + switch (mixing_state) + { case FilamentTempMixingState::Compatible: get_notification_manager()->close_validate_error_notification(filament_temp_mixing_error_text()); get_notification_manager()->close_validate_warning_notification(filament_temp_mixing_warning_text()); @@ -20420,7 +20443,8 @@ bool Plater::guard_before_slice_all() bool Plater::confirm_filament_temp_mixing_before_slice() { - switch (get_filament_temp_mixing_state()) { + switch (get_filament_temp_mixing_state()) + { case FilamentTempMixingState::Compatible: return true; case FilamentTempMixingState::BlockedError: @@ -21260,12 +21284,14 @@ int Plater::select_plate(int plate_index, bool need_slice) //p->process_completed_with_error = -1; p->m_slice_all = false; reset_gcode_toolpaths(); - if (model_fits && !validate_err) { + if (model_fits && !validate_err) + { if (!guard_before_slice_plate()) return ret; reslice(); } - else { + else + { p->main_frame->update_slice_print_status(MainFrame::eEventPlateUpdate, false); //sometimes the previous print's sliced result is still valid, but the newly added object is laid over the boundary //then the print toolpath will be shown, so we should not refresh print here, only onload shell diff --git a/src/slic3r/GUI/Plater.hpp b/src/slic3r/GUI/Plater.hpp index 1280f3529dc..9d3873a9c21 100644 --- a/src/slic3r/GUI/Plater.hpp +++ b/src/slic3r/GUI/Plater.hpp @@ -232,7 +232,8 @@ class Plater: public wxPanel { public: using fs_path = boost::filesystem::path; - enum class FilamentTempMixingState { + enum class FilamentTempMixingState + { Compatible, AllowedWarning, BlockedError @@ -541,8 +542,12 @@ class Plater: public wxPanel /// Sync notification state with current filament temp mixing status. /// Returns true if slicing is allowed, false if high/low temperature mixing blocks slicing. bool sync_filament_temp_mixing_notification(); + /// Check and guard filament temp mixing before slicing current plate. bool guard_before_slice_plate(); + /// Check and guard filament temp mixing before slicing all plates. bool guard_before_slice_all(); + /// @brief Show confirmation dialog for allowed high/low temperature mixing before slice. + /// @return True if slicing can proceed; false if blocked or user cancelled. bool confirm_filament_temp_mixing_before_slice(); /// @brief Confirm warning-level high/low temperature material mixing before slicing all plates. /// @return True if slice-all can continue; otherwise false. From 3c6b7cc701d5dfeab97a0cb841b8a03b544874db Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Thu, 11 Jun 2026 20:11:13 +0800 Subject: [PATCH 060/135] fix: conditional global key collection and CLI-only filament temp mixing guard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Plater.cpp: collect wall/sparse/solid_infill_filament from global process config only when objects on the plate use extruder=0 (default). Global feature keys (wipe_tower/support) are always collected. This fixes both missed detection (plate with e=0 objects whose wall_filament differs from wipe_tower) and ensures correctness. - Snapmaker_Orca.cpp: add explicit filament temp mixing check before Print::validate() in CLI slicing path, using the public check_multi_filaments_compatibility(). CLI has no preference concept so mixing always blocks. - Print.cpp: keep validate() clean — the check belongs in the respective entry points (GUI: sync_filament_temp_mixing_notification, CLI: explicit guard) rather than in the shared validate() path. Co-Authored-By: Claude Opus 4.7 --- src/Snapmaker_Orca.cpp | 21 +++++++++++++++++++++ src/libslic3r/Print.cpp | 9 --------- src/slic3r/GUI/Plater.cpp | 39 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 60 insertions(+), 9 deletions(-) diff --git a/src/Snapmaker_Orca.cpp b/src/Snapmaker_Orca.cpp index 6f173e1b046..365be8d3679 100644 --- a/src/Snapmaker_Orca.cpp +++ b/src/Snapmaker_Orca.cpp @@ -4991,6 +4991,27 @@ int CLI::run(int argc, char **argv) print->apply(model, new_print_config); BOOST_LOG_TRIVIAL(info) << boost::format("set no_check to %1%:")%no_check; print->set_no_check_flag(no_check);//BBS + // CLI guard: check high/low temp filament mixing. + // The GUI has Plater::sync_filament_temp_mixing_notification() + // which respects user preferences. For CLI we always block. + if (print_fff) { + const auto& pcfg = print_fff->config(); + const auto& extruders = print_fff->extruders(); + std::vector ftypes; + ftypes.reserve(extruders.size()); + for (unsigned int eid : extruders) + ftypes.push_back(pcfg.filament_type.get_at(eid)); + if (!Print::check_multi_filaments_compatibility(ftypes)) { + boost::nowide::cerr + << "Cannot print multiple filaments which have " + "large difference of temperature together." + << std::endl; + record_exit_reson(outfile_dir, CLI_FILAMENTS_DIFFERENT_TEMP, + index + 1, cli_errors[CLI_FILAMENTS_DIFFERENT_TEMP], + sliced_info); + flush_and_exit(CLI_FILAMENTS_DIFFERENT_TEMP); + } + } StringObjectException warning; auto err = print->validate(&warning); if (!err.string.empty()) { diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index 0b8655fbf83..354bdc34918 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -1556,15 +1556,6 @@ StringObjectException Print::validate(StringObjectException *warning, Polygons* if (extruders.empty()) return { L("No extrusions under current settings.") }; - // Check for high/low temperature filament mixing. - if (extruders.size() > 1) { - auto ret = check_multi_filament_valid(*this); - if (!ret.string.empty()) { - ret.type = STRING_EXCEPT_FILAMENTS_DIFFERENT_TEMP; - return ret; - } - } - if (m_config.print_sequence == PrintSequence::ByObject) { if (m_config.timelapse_type == TimelapseType::tlSmooth) return {L("Smooth mode of timelapse is not supported when \"by object\" sequence is enabled.")}; diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 0793408fe47..7fc7c9c2d5c 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -20297,6 +20297,45 @@ bool Plater::check_filament_temp_mixing(int plate_index) } } + // Collect from the Plater working config. The approach balances + // sensitivity against false positives: + // - Global features (wipe tower, support) always apply → always collected. + // - Feature-specific keys (wall_filament, infill) depend on the global + // process defaults. They are only collected when at least one object + // on the plate uses the default extruder (e=0), which means those + // defaults WILL affect the actual slicing output. + { + // Always collect: features that cannot be overridden per-object. + static const std::vector always_collect = { + "wipe_tower_filament", + "support_filament", + "support_interface_filament" + }; + for (const char* key : always_collect) + { + const ConfigOptionInt* option = this->config()->option(key); + if (option != nullptr && option->value >= 1 && option->value <= num_filaments) + used_slots.insert(option->value - 1); + } + + // If any object uses e=0, the global process defaults for + // wall / infill extruders apply and must be collected. + if (uses_default_extruder) + { + static const std::vector default_keys = { + "wall_filament", + "sparse_infill_filament", + "solid_infill_filament" + }; + for (const char* key : default_keys) + { + const ConfigOptionInt* option = this->config()->option(key); + if (option != nullptr && option->value >= 1 && option->value <= num_filaments) + used_slots.insert(option->value - 1); + } + } + } + // Resolve the global default extruder if any object on this plate // uses extruder=0. p->config does not include the "extruder" key // (it is not in the initializer list at priv constructor), so we From d553307333fe73e569c77a5e968c799a21ae1534 Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Fri, 12 Jun 2026 11:36:56 +0800 Subject: [PATCH 061/135] fix: collect per-object feature keys in filament temp mixing check MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit collect_filament_slots_from_model_config() only checked the extruder key, missing per-object overrides of wall_filament, solid_infill_filament, etc. When a user sets a different filament for solid infill at the object level, the check failed to detect high/low temperature mixing even though the slicer would actually use that filament during printing. Now also collects: wall_filament, sparse_infill_filament, solid_infill_filament, support_filament, support_interface_filament, wipe_tower_filament — matching the actual behavior of apply_to_print_region_config() which applies these per-object keys. Co-Authored-By: Claude Opus 4.7 --- src/slic3r/GUI/Plater.cpp | 30 +++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 7fc7c9c2d5c..0f5964f296a 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -275,12 +275,32 @@ static void collect_filament_slots_from_model_config( int num_filaments, std::set& used_slots) { - if (!config.has("extruder")) - return; + if (config.has("extruder")) + { + const int extruder_id = config.extruder(); + if (extruder_id >= 1 && extruder_id <= num_filaments) + used_slots.insert(extruder_id - 1); + } - const int extruder_id = config.extruder(); - if (extruder_id >= 1 && extruder_id <= num_filaments) - used_slots.insert(extruder_id - 1); + // Per-object feature-specific keys (wall_filament, etc.) may be + // overridden independently of the object's primary extruder. + static const std::vector feature_keys = { + "wall_filament", + "sparse_infill_filament", + "solid_infill_filament", + "support_filament", + "support_interface_filament", + "wipe_tower_filament" + }; + for (const char* key : feature_keys) + { + if (config.has(key)) + { + const int val = config.opt_int(key); + if (val >= 1 && val <= num_filaments) + used_slots.insert(val - 1); + } + } } wxDEFINE_EVENT(EVT_SCHEDULE_BACKGROUND_PROCESS, SimpleEvent); From 4de5831c2ba8d199d8c0a8750e1e1d0dcdec96d6 Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Fri, 12 Jun 2026 14:38:47 +0800 Subject: [PATCH 062/135] fix: skip already-sliced plates in slice-all confirmation dialog confirm_filament_temp_mixing_before_slice_all() checked all sliceable plates regardless of whether they already had valid slice results. After slicing one mixed plate and returning to slice-all, the dialog appeared again for that already-sliced plate -- a pointless prompt. Now only unsliced plates with AllowedWarning trigger the dialog. Co-Authored-By: Claude Opus 4.7 --- src/slic3r/GUI/Plater.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 0f5964f296a..f19b91e9178 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -20525,11 +20525,14 @@ bool Plater::confirm_filament_temp_mixing_before_slice_all() if (!has_sliceable_plate_for_slice_all()) return false; + // Only count plates that can be sliced AND haven't been sliced + // yet. Already-sliced plates don't need re-confirmation. bool has_allowed_warning = false; for (int plate_index = 0; plate_index < p->partplate_list.get_plate_count(); ++plate_index) { PartPlate* plate = p->partplate_list.get_plate(plate_index); if (plate != nullptr && plate->can_slice() && + !plate->is_slice_result_valid() && get_filament_temp_mixing_state(plate_index) == FilamentTempMixingState::AllowedWarning) { has_allowed_warning = true; From ce71dba45295f6c6a92de91366d35e77eece096f Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Mon, 15 Jun 2026 09:50:42 +0800 Subject: [PATCH 063/135] fix: wxEntry encoding mismatch when exe path contains non-ASCII characters boost::nowide::narrow() converts argv[0] to UTF-8, but wxEntry expects the system ANSI codepage (CP_ACP). On Windows, rebuild argv[0] from GetModuleFileNameW with proper CP_ACP conversion to avoid "Command line argument 0 couldn't be converted to Unicode" warning from wxWidgets. Co-Authored-By: Claude Opus 4.7 --- src/slic3r/GUI/GUI_Init.cpp | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/src/slic3r/GUI/GUI_Init.cpp b/src/slic3r/GUI/GUI_Init.cpp index 96f377e8263..ec22e5770a5 100644 --- a/src/slic3r/GUI/GUI_Init.cpp +++ b/src/slic3r/GUI/GUI_Init.cpp @@ -16,6 +16,11 @@ #include #include +#ifdef _WIN32 + #include + #include +#endif + #if __APPLE__ #include #endif // __APPLE__ @@ -53,6 +58,32 @@ int GUI_Run(GUI_InitParams ¶ms) GUI::GUI_App::SetInstance(gui); gui->init_params = ¶ms; +#ifdef _WIN32 + // argv[0] from boost::nowide::narrow() is UTF-8, but wxEntry expects + // the system ANSI codepage (CP_ACP). When the executable path contains + // non-ASCII characters, the encoding mismatch causes wxWidgets to + // show: "Command line argument 0 couldn't be converted to Unicode". + // Rebuild argv[0] from the real module path using the proper codepage. + { + wchar_t module_path[MAX_PATH + 1] = {0}; + GetModuleFileNameW(nullptr, module_path, MAX_PATH); + char module_ansi[MAX_PATH * 2 + 1] = {0}; + WideCharToMultiByte(CP_ACP, WC_NO_BEST_FIT_CHARS, + module_path, -1, module_ansi, + sizeof(module_ansi), nullptr, nullptr); + if (module_ansi[0] == '\0') { + // Fallback: conversion failed, use ASCII-safe short path + wchar_t short_path[MAX_PATH + 1] = {0}; + GetShortPathNameW(module_path, short_path, MAX_PATH); + WideCharToMultiByte(CP_ACP, 0, short_path, -1, + module_ansi, sizeof(module_ansi), + nullptr, nullptr); + } + int safe_argc = 1; + char* safe_argv[] = { const_cast(module_ansi), nullptr }; + return wxEntry(safe_argc, safe_argv); + } +#else if (params.argc > 1) { // STUDIO-273 wxWidgets report error when opening some files with specific names // wxWidgets does not handle parameters, so intercept parameters here, only keep the app name @@ -63,6 +94,7 @@ int GUI_Run(GUI_InitParams ¶ms) } else { return wxEntry(params.argc, params.argv); } +#endif } catch (const Slic3r::Exception &ex) { BOOST_LOG_TRIVIAL(error) << ex.what() << std::endl; wxMessageBox(boost::nowide::widen(ex.what()), _L("Snapmaker Orca GUI initialization failed"), wxICON_STOP); From 9b674a79fb04759b288b1dbf08f16260e3fa7153 Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Mon, 15 Jun 2026 18:22:49 +0800 Subject: [PATCH 064/135] =?UTF-8?q?fix:=20address=20code=20review=20findin?= =?UTF-8?q?gs=20=E2=80=94=20brace=20style,=20API=20return=20checks,=20atom?= =?UTF-8?q?ic=20flag?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix Allman brace style violations in Snapmaker_Orca.cpp and NotificationManager.cpp (opening brace on same line → own line) - Check WideCharToMultiByte return value instead of assuming failure when output buffer is empty; also check short-path fallback result - Replace bool filament_usage_sync_pending with std::atomic to avoid potential races when notify_filament_usage_changed is called from multiple code paths in quick succession Co-Authored-By: Claude Opus 4.7 --- src/Snapmaker_Orca.cpp | 3 ++- src/slic3r/GUI/GUI_Init.cpp | 21 ++++++++++++++------- src/slic3r/GUI/NotificationManager.cpp | 3 ++- src/slic3r/GUI/Plater.cpp | 3 ++- 4 files changed, 20 insertions(+), 10 deletions(-) diff --git a/src/Snapmaker_Orca.cpp b/src/Snapmaker_Orca.cpp index 365be8d3679..2e2f4421e3e 100644 --- a/src/Snapmaker_Orca.cpp +++ b/src/Snapmaker_Orca.cpp @@ -4994,7 +4994,8 @@ int CLI::run(int argc, char **argv) // CLI guard: check high/low temp filament mixing. // The GUI has Plater::sync_filament_temp_mixing_notification() // which respects user preferences. For CLI we always block. - if (print_fff) { + if (print_fff) + { const auto& pcfg = print_fff->config(); const auto& extruders = print_fff->extruders(); std::vector ftypes; diff --git a/src/slic3r/GUI/GUI_Init.cpp b/src/slic3r/GUI/GUI_Init.cpp index ec22e5770a5..84f10ec3997 100644 --- a/src/slic3r/GUI/GUI_Init.cpp +++ b/src/slic3r/GUI/GUI_Init.cpp @@ -68,16 +68,23 @@ int GUI_Run(GUI_InitParams ¶ms) wchar_t module_path[MAX_PATH + 1] = {0}; GetModuleFileNameW(nullptr, module_path, MAX_PATH); char module_ansi[MAX_PATH * 2 + 1] = {0}; - WideCharToMultiByte(CP_ACP, WC_NO_BEST_FIT_CHARS, - module_path, -1, module_ansi, - sizeof(module_ansi), nullptr, nullptr); - if (module_ansi[0] == '\0') { + const int converted = WideCharToMultiByte( + CP_ACP, WC_NO_BEST_FIT_CHARS, + module_path, -1, module_ansi, + sizeof(module_ansi), nullptr, nullptr); + if (converted == 0) + { // Fallback: conversion failed, use ASCII-safe short path wchar_t short_path[MAX_PATH + 1] = {0}; GetShortPathNameW(module_path, short_path, MAX_PATH); - WideCharToMultiByte(CP_ACP, 0, short_path, -1, - module_ansi, sizeof(module_ansi), - nullptr, nullptr); + if (GetLastError() == ERROR_INSUFFICIENT_BUFFER || + WideCharToMultiByte(CP_ACP, 0, short_path, -1, + module_ansi, sizeof(module_ansi), + nullptr, nullptr) == 0) + { + // Both primary and fallback failed; keep empty string, + // wxEntry will use its own default behavior. + } } int safe_argc = 1; char* safe_argv[] = { const_cast(module_ansi), nullptr }; diff --git a/src/slic3r/GUI/NotificationManager.cpp b/src/slic3r/GUI/NotificationManager.cpp index f2be434c92b..5b2dff06b95 100644 --- a/src/slic3r/GUI/NotificationManager.cpp +++ b/src/slic3r/GUI/NotificationManager.cpp @@ -2016,7 +2016,8 @@ void NotificationManager::close_notification_of_type(const NotificationType type void NotificationManager::close_notification_of_type_and_text(const NotificationType type, const std::string& text) { - for (std::unique_ptr& notification : m_pop_notifications) { + for (std::unique_ptr& notification : m_pop_notifications) + { if (notification->get_type() == type && notification->compare_text(text)) notification->close(); } diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index f19b91e9178..9c77b02c98b 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -8511,7 +8512,7 @@ struct Plater::priv // PIMPL back pointer ("Q-Pointer") Plater *q; MainFrame *main_frame; - bool filament_usage_sync_pending = false; + std::atomic filament_usage_sync_pending{false}; bool filament_temp_mixing_notification_initialized = false; int filament_temp_mixing_notification_plate = -1; FilamentTempMixingState filament_temp_mixing_notification_state = FilamentTempMixingState::Compatible; From eea4180ab3246ab110a7fc1320a0e3a1012fd0d0 Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Tue, 16 Jun 2026 14:38:34 +0800 Subject: [PATCH 065/135] fix: use atomic exchange in notify_filament_usage_changed and remove redundant trigger in select_plate - Replace check-then-set pattern with exchange(true) for correct atomic test-and-set semantics in notify_filament_usage_changed(). - Remove notify_filament_usage_changed() call from select_plate() since sync_filament_temp_mixing_notification() is already called directly. Co-Authored-By: Claude Opus 4.7 --- src/slic3r/GUI/Plater.cpp | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 80266064f8b..0c3e89e0f3f 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -20553,10 +20553,9 @@ bool Plater::confirm_filament_temp_mixing_before_slice_all() void Plater::notify_filament_usage_changed() { - if (p->filament_usage_sync_pending) + if (p->filament_usage_sync_pending.exchange(true)) return; - p->filament_usage_sync_pending = true; wxQueueEvent(this, new SimpleEvent(EVT_FILAMENT_USAGE_CHANGED, this)); } @@ -21399,7 +21398,6 @@ int Plater::select_plate(int plate_index, bool need_slice) SimpleEvent event(EVT_GLCANVAS_PLATE_SELECT); p->on_plate_selected(event); sync_filament_temp_mixing_notification(); - notify_filament_usage_changed(); BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(" %1%: plate %2%, return %3%")%__LINE__ %plate_index %ret; return ret; From 6087b1e2d608cfcf79e09e5bbcc1cb7b0a301c31 Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Wed, 20 May 2026 17:28:35 +0800 Subject: [PATCH 066/135] =?UTF-8?q?fix=EF=BC=9A=E4=BF=AE=E6=94=B9=E7=BC=96?= =?UTF-8?q?=E8=AF=91=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CMakeLists.txt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index c509e8d7287..5313ddb10d5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,9 @@ cmake_minimum_required(VERSION 3.13) +# Allow building with CMake newer than 3.28 without failing on dependency cmake +# config files that use cmake_policy(VERSION ...3.28) range syntax +set(CMAKE_POLICY_VERSION_MINIMUM 3.28) + # Verify that your CMake version is exactly 3.31.x series or lower on windows if ( ((MSVC) OR (WIN32)) AND (${CMAKE_VERSION} VERSION_GREATER_EQUAL "4.0") ) message(FATAL_ERROR "Only cmake versions between 3.13.x and 3.31.x is supported on windows. Detected version: ${CMAKE_VERSION}") From 1416345c1e4a28821dc8d34ce4367984d879a9c7 Mon Sep 17 00:00:00 2001 From: Joyx-SnapMaker <1906956558@qq.com> Date: Thu, 16 Apr 2026 17:01:23 +0800 Subject: [PATCH 067/135] feat: add configurable option for top cover requirement --- src/libslic3r/Preset.cpp | 3 ++- src/libslic3r/PrintConfig.cpp | 6 ++++++ src/libslic3r/PrintConfig.hpp | 1 + src/slic3r/GUI/Tab.cpp | 1 + 4 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index 7b6058ccdf7..8882ce9a3af 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -946,7 +946,8 @@ static std::vector s_Preset_filament_options { "graphic_effect_plate_temp", "graphic_effect_plate_temp_initial_layer", // "bed_type", //BBS:temperature_vitrification - "temperature_vitrification", "reduce_fan_stop_start_freq","dont_slow_down_outer_wall", "slow_down_for_layer_cooling", "fan_min_speed", + "temperature_vitrification", "filament_is_high_temperature", "reduce_fan_stop_start_freq", "dont_slow_down_outer_wall", + "slow_down_for_layer_cooling", "fan_min_speed", "fan_max_speed", "enable_overhang_bridge_fan", "overhang_fan_speed", "overhang_fan_threshold", "close_fan_the_first_x_layers", "full_fan_speed_layer", "fan_cooling_layer_time", "slow_down_layer_time", "slow_down_min_speed", "filament_start_gcode", "filament_end_gcode", //exhaust fan control diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index ac67d087502..66b0c29a7ba 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -2391,6 +2391,12 @@ void PrintConfigDef::init_fff_params() def->mode = comAdvanced; def->set_default_value(new ConfigOptionBools { false }); + def = this->add("filament_is_high_temperature", coBools); + def->label = L("Is high-temperature filament"); + def->tooltip = L("Indicates whether this is a high-temperature filament that requires elevated printing temperatures."); + def->mode = comSimple; + def->set_default_value(new ConfigOptionBools{false}); + // BBS def = this->add("temperature_vitrification", coInts); def->label = L("Softening temperature"); diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index 0143f1f1e43..9f669e30c81 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -1180,6 +1180,7 @@ PRINT_CONFIG_CLASS_DEFINE( ((ConfigOptionFloats, filament_cost)) ((ConfigOptionStrings, default_filament_colour)) ((ConfigOptionInts, temperature_vitrification)) //BBS + ((ConfigOptionBools, filament_is_high_temperature)) ((ConfigOptionFloats, filament_max_volumetric_speed)) ((ConfigOptionInts, required_nozzle_HRC)) // BBS diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index da234f7fce2..e69f7b475e8 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -3690,6 +3690,7 @@ void TabFilament::build() optgroup->append_single_option_line("filament_cost"); //BBS optgroup->append_single_option_line("temperature_vitrification"); + optgroup->append_single_option_line("filament_is_high_temperature"); optgroup->append_single_option_line("idle_temperature"); optgroup->append_single_option_line("filament_tower_ironing_area"); Line line = { L("Recommended nozzle temperature"), L("Recommended nozzle temperature range of this filament. 0 means no set") }; From 5d4b42703857ce90d212ef76961c5432c5798caf Mon Sep 17 00:00:00 2001 From: Joyx-SnapMaker <1906956558@qq.com> Date: Tue, 21 Apr 2026 18:01:25 +0800 Subject: [PATCH 068/135] feat: add translation # Conflicts: # src/libslic3r/Preset.cpp # src/libslic3r/PrintConfig.cpp --- localization/i18n/zh_CN/Snapmaker_Orca_zh_CN.po | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/localization/i18n/zh_CN/Snapmaker_Orca_zh_CN.po b/localization/i18n/zh_CN/Snapmaker_Orca_zh_CN.po index 394b539f446..38532da3efc 100644 --- a/localization/i18n/zh_CN/Snapmaker_Orca_zh_CN.po +++ b/localization/i18n/zh_CN/Snapmaker_Orca_zh_CN.po @@ -10041,6 +10041,12 @@ msgstr "支撑材料" msgid "Support material is commonly used to print supports and support interfaces." msgstr "支撑材料通常用于打印支撑体和支撑接触面" +msgid "Is high-temperature filament" +msgstr "是否为高温耗材" + +msgid "Indicates whether this is a high-temperature filament that requires elevated printing temperatures." +msgstr "指示此耗材是否为需要较高打印温度的高温耗材。" + msgid "Softening temperature" msgstr "软化温度" From d9b6febfb8f79bad272116a3db4285f244f4d31d Mon Sep 17 00:00:00 2001 From: Joyx-SnapMaker <1906956558@qq.com> Date: Fri, 24 Apr 2026 16:01:23 +0800 Subject: [PATCH 069/135] feat: Add the "Is It a High-Temperature Consumable" attribute to the consumable filament, and simultaneously modify the printer's initial gcode. --- .../Snapmaker/filament/Snapmaker PETG HF @U1 0.2 nozzle.json | 3 ++- .../profiles/Snapmaker/machine/Snapmaker U1 (0.2 nozzle).json | 4 ++-- .../profiles/Snapmaker/machine/Snapmaker U1 (0.4 nozzle).json | 4 ++-- .../profiles/Snapmaker/machine/Snapmaker U1 (0.6 nozzle).json | 4 ++-- .../profiles/Snapmaker/machine/Snapmaker U1 (0.8 nozzle).json | 4 ++-- 5 files changed, 10 insertions(+), 9 deletions(-) diff --git a/resources/profiles/Snapmaker/filament/Snapmaker PETG HF @U1 0.2 nozzle.json b/resources/profiles/Snapmaker/filament/Snapmaker PETG HF @U1 0.2 nozzle.json index cc0f417368b..18c5143f559 100644 --- a/resources/profiles/Snapmaker/filament/Snapmaker PETG HF @U1 0.2 nozzle.json +++ b/resources/profiles/Snapmaker/filament/Snapmaker PETG HF @U1 0.2 nozzle.json @@ -107,5 +107,6 @@ ], "graphic_effect_plate_temp": ["0"], "graphic_effect_plate_temp_initial_layer": ["0"], - "filament_z_hop_types": ["Slope Lift"] + "filament_z_hop_types": ["Slope Lift"], + "filament_is_high_temperature": ["0"] } \ No newline at end of file diff --git a/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.2 nozzle).json b/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.2 nozzle).json index 98a2586871e..7456699be5c 100644 --- a/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.2 nozzle).json +++ b/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.2 nozzle).json @@ -35,7 +35,7 @@ "0", "0" ], - "machine_end_gcode": "{if print_sequence == \\\"by object\\\"}\\nG91\\nG1 X2 Y2 Z1 F24000\\nG90\\nG1 Z{max_layer_z+2} F600\\n{endif}\\nPRINT_END\\nTIMELAPSE_STOP", + "machine_end_gcode": " PRINT_END\nTIMELAPSE_STOP", "machine_max_jerk_z": [ "3", "0.4" @@ -48,7 +48,7 @@ "20", "12" ], - "machine_start_gcode": ";===== date: 20260128 =====================\\n\\nPRINT_START\\nDEFECT_DETECTION_START\\nSET_PRINT_STATS_INFO TOTAL_LAYER={total_layer_count} CURRENT_LAYER=0\\nTIMELAPSE_START\\nM140 S{bed_temperature_initial_layer_single}\\nM104 T{initial_extruder} S140\\nM204 S10000\\n\\nG28 X Y\\nDEFECT_DETECT_NOODLE_FIRST\\n;===== 床面异物检测 ========\\nT{initial_extruder}\\nG90\\nDEFECT_DETECTION_DETECT_BED\\n;===== 取放头检测 =================\\nSM_PRINT_CHECK_SWITCH_EXTRUDER\\n\\n;===== 自动进料 & 挤出流量 & 预挤出 ======================\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=1 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=0\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=0\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=2 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=1\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=1\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=3 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=2\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=2\\nSM_PRINT_AUTO_FEED EXTRUDER=3\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=3\\nM104 S0 T0 A0\\nM104 S0 T1 A0\\nM104 S0 T2 A0\\nM104 S0 T3 A0\\nM104 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\n\\n;===== 粗回零 =================\\nT{initial_extruder}\\nM106 S255\\nM106 P2 S0\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\nROUGHLY_CLEAN_NOZZLE_WITH_DISCARD\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nG28 Z I140 J140\\n\\n;===== 检测钢板 =================\\nDETECT_BED_PLATE\\n\\n;===== 深度清洁喷嘴 =================\\nG90\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 S{nozzle_temperature[initial_extruder] - 50}\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_1\\nM104 S{nozzle_temperature[initial_extruder] - 90}\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_2\\n\\n;===== 精回零 =================\\nM106 S255\\nM109 S{nozzle_temperature[initial_extruder] - 90}\\nM190 S{bed_temperature_initial_layer_single}\\nM107 P2\\nG90\\nG0 Z5 F10000\\n\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{else} \\nG28 Z \\n{endif} \\n\\n\\n;===== 热床调平 =================\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{else} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 \\n{endif} \\n\\n\\n;===== 画起始线 =================\\nG90\\nG1 Z1.5\\nG0 X85 Y1 Z2 F18000\\nM109 S{nozzle_temperature_initial_layer[initial_extruder]}\\nG1 Z0.2\\nM83\\nG1 X185 E15 F360\\nG1 Z1.5\\n\\nG90\\nM106 S0", + "machine_start_gcode": ";===== date: 20260128 =====================\\n\\nPRINT_START\\nDEFECT_DETECTION_START\\nSET_PRINT_STATS_INFO TOTAL_LAYER={total_layer_count} CURRENT_LAYER=0\\nTIMELAPSE_START\\nM140 S{bed_temperature_initial_layer_single}\\nM104 T{initial_extruder} S140\\nM204 S10000\\n{if filament_is_high_temperature[0]}\\n; 保温\\n\tSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=180\\n{else}\\n{if temperature_vitrification[0]<=50}\\n; 强冷\\nSET_PURIFIER_MODE MODE=1 DESIRE_TEMP=42 ALARM_TEMP=45 FAN_SPEED=0.6\\nDELAY_OFF=0\t\t\\n{else}\\n; 弱冷\\nSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=0 FAN_SPEED=0.6 DELAY_OFF=180\t\t\\n{endif}\\n{endif}\\nG28 X Y\\n;===== 床面异物检测 ========\\nT{initial_extruder}\\nG90\\nDEFECT_DETECTION_DETECT_BED\\n;===== 取放头检测 =================\\nSM_PRINT_CHECK_SWITCH_EXTRUDER\\n\\n;===== 自动进料 & 挤出流量 & 预挤出 ======================\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=1 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=0\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=0\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=2 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=1\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=1\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=3 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=2\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=2\\nSM_PRINT_AUTO_FEED EXTRUDER=3\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=3\\nM104 S0 T0 A0\\nM104 S0 T1 A0\\nM104 S0 T2 A0\\nM104 S0 T3 A0\\nM104 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\n\\n;===== 粗回零 =================\\nT{initial_extruder}\\nM106 S255\\nM106 P2 S0\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\nROUGHLY_CLEAN_NOZZLE_WITH_DISCARD\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nG28 Z I140 J140\\n\\n;===== 检测钢板 =================\\nDETECT_BED_PLATE\\n\\n;===== 深度清洁喷嘴 =================\\nG90\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 S{nozzle_temperature[initial_extruder] - 50}\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_1\\nM104 S{nozzle_temperature[initial_extruder] - 90}\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_2\\n\\n;===== 精回零 =================\\nM106 S255\\nM109 S{nozzle_temperature[initial_extruder] - 90}\\nM190 S{bed_temperature_initial_layer_single}\\nM107 P2\\nG90\\nG0 Z5 F10000\\nWAIT_CHAMBER_TEMP TIMEOUT=180\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{else} \\nG28 Z \\n{endif} \\n\\n\\n;===== 热床调平 =================\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{else} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 \\n{endif} \\n\\n\\n;===== 画起始线 =================\\nG90\\nG1 Z1.5\\nG0 X85 Y1 Z2 F18000\\nM109 S{nozzle_temperature_initial_layer[initial_extruder]}\\nG1 Z0.2\\nM83\\nG1 X185 E15 F360\\nG1 Z1.5\\n\\nG90\\nM106 S0", "machine_tool_change_time": "5", "max_layer_height": [ "0.14", diff --git a/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.4 nozzle).json b/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.4 nozzle).json index 06b63eb9af4..95ff6d82d6b 100644 --- a/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.4 nozzle).json +++ b/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.4 nozzle).json @@ -35,7 +35,7 @@ "0", "0" ], - "machine_end_gcode": "{if print_sequence == \\\"by object\\\"}\\nG91\\nG1 X2 Y2 Z1 F24000\\nG90\\nG1 Z{max_layer_z+2} F600\\n{endif}\\nPRINT_END\\nTIMELAPSE_STOP", + "machine_end_gcode": " PRINT_END\nTIMELAPSE_STOP", "machine_max_jerk_z": [ "3", "0.4" @@ -48,7 +48,7 @@ "20", "12" ], - "machine_start_gcode": ";===== date: 20260128 =====================\\n\\nPRINT_START\\nDEFECT_DETECTION_START\\nSET_PRINT_STATS_INFO TOTAL_LAYER={total_layer_count} CURRENT_LAYER=0\\nTIMELAPSE_START\\nM140 S{bed_temperature_initial_layer_single}\\nM104 T{initial_extruder} S140\\nM204 S10000\\n\\nG28 X Y\\nDEFECT_DETECT_NOODLE_FIRST\\n;===== 床面异物检测 ========\\nT{initial_extruder}\\nG90\\nDEFECT_DETECTION_DETECT_BED\\n;===== 取放头检测 =================\\nSM_PRINT_CHECK_SWITCH_EXTRUDER\\n\\n;===== 自动进料 & 挤出流量 & 预挤出 ======================\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=1 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=0\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=0\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=2 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=1\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=1\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=3 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=2\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=2\\nSM_PRINT_AUTO_FEED EXTRUDER=3\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=3\\nM104 S0 T0 A0\\nM104 S0 T1 A0\\nM104 S0 T2 A0\\nM104 S0 T3 A0\\nM104 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\n\\n;===== 粗回零 =================\\nT{initial_extruder}\\nM106 S255\\nM106 P2 S0\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\nROUGHLY_CLEAN_NOZZLE_WITH_DISCARD\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nG28 Z I140 J140\\n\\n;===== 检测钢板 =================\\nDETECT_BED_PLATE\\n\\n;===== 深度清洁喷嘴 =================\\nG90\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 S{nozzle_temperature[initial_extruder] - 50}\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_1\\nM104 S{nozzle_temperature[initial_extruder] - 90}\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_2\\n\\n;===== 精回零 =================\\nM106 S255\\nM109 S{nozzle_temperature[initial_extruder] - 90}\\nM190 S{bed_temperature_initial_layer_single}\\nM107 P2\\nG90\\nG0 Z5 F10000\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{else} \\nG28 Z \\n{endif} \\n\\n\\n;===== 热床调平 =================\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{else} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 \\n{endif} \\n\\n\\n;===== 画起始线 =================\\nG90\\nG1 Z1.5\\nG0 X85 Y1 Z2 F18000\\nM109 S{nozzle_temperature_initial_layer[initial_extruder]}\\nG1 Z0.2\\nM83\\nG1 X185 E15 F360\\nG1 Z1.5\\n\\nG90\\nM106 S0", + "machine_start_gcode": ";===== date: 20260128 =====================\\n\\nPRINT_START\\nDEFECT_DETECTION_START\\nSET_PRINT_STATS_INFO TOTAL_LAYER={total_layer_count} CURRENT_LAYER=0\\nTIMELAPSE_START\\nM140 S{bed_temperature_initial_layer_single}\\nM104 T{initial_extruder} S140\\nM204 S10000\\n{if filament_is_high_temperature[0]}\\n; 保温\\n\tSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=180\\n{else}\\n{if temperature_vitrification[0]<=50}\\n; 强冷\\nSET_PURIFIER_MODE MODE=1 DESIRE_TEMP=42 ALARM_TEMP=45 FAN_SPEED=0.6\\nDELAY_OFF=0\t\t\\n{else}\\n; 弱冷\\nSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=0 FAN_SPEED=0.6 DELAY_OFF=180\t\t\\n{endif}\\n{endif}\\n\\nG28 X Y\\n;===== 床面异物检测 ========\\nT{initial_extruder}\\nG90\\nDEFECT_DETECTION_DETECT_BED\\n;===== 取放头检测 =================\\nSM_PRINT_CHECK_SWITCH_EXTRUDER\\n\\n;===== 自动进料 & 挤出流量 & 预挤出 ======================\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=1 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=0\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=0\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=2 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=1\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=1\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=3 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=2\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=2\\nSM_PRINT_AUTO_FEED EXTRUDER=3\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=3\\nM104 S0 T0 A0\\nM104 S0 T1 A0\\nM104 S0 T2 A0\\nM104 S0 T3 A0\\nM104 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\n\\n;===== 粗回零 =================\\nT{initial_extruder}\\nM106 S255\\nM106 P2 S0\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\nROUGHLY_CLEAN_NOZZLE_WITH_DISCARD\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nG28 Z I140 J140\\n\\n;===== 检测钢板 =================\\nDETECT_BED_PLATE\\n\\n;===== 深度清洁喷嘴 =================\\nG90\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 S{nozzle_temperature[initial_extruder] - 50}\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_1\\nM104 S{nozzle_temperature[initial_extruder] - 90}\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_2\\n\\n;===== 精回零 =================\\nM106 S255\\nM109 S{nozzle_temperature[initial_extruder] - 90}\\nM190 S{bed_temperature_initial_layer_single}\\nM107 P2\\nG90\\nG0 Z5 F10000\\nWAIT_CHAMBER_TEMP TIMEOUT=180\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{else} \\nG28 Z \\n{endif} \\n\\n\\n;===== 热床调平 =================\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{else} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 \\n{endif} \\n\\n\\n;===== 画起始线 =================\\nG90\\nG1 Z1.5\\nG0 X85 Y1 Z2 F18000\\nM109 S{nozzle_temperature_initial_layer[initial_extruder]}\\nG1 Z0.2\\nM83\\nG1 X185 E15 F360\\nG1 Z1.5\\n\\nG90\\nM106 S0", "machine_tool_change_time": "5", "max_layer_height": [ "0.32", diff --git a/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.6 nozzle).json b/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.6 nozzle).json index 9c54ab2e190..40d8bf7e5da 100644 --- a/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.6 nozzle).json +++ b/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.6 nozzle).json @@ -35,7 +35,7 @@ "0", "0" ], - "machine_end_gcode": "{if print_sequence == \\\"by object\\\"}\\nG91\\nG1 X2 Y2 Z1 F24000\\nG90\\nG1 Z{max_layer_z+2} F600\\n{endif}\\nPRINT_END\\nTIMELAPSE_STOP", + "machine_end_gcode": " PRINT_END\nTIMELAPSE_STOP", "machine_max_jerk_z": [ "3", "0.4" @@ -48,7 +48,7 @@ "20", "12" ], - "machine_start_gcode": ";===== date: 20260128 =====================\\n\\nPRINT_START\\nDEFECT_DETECTION_START\\nSET_PRINT_STATS_INFO TOTAL_LAYER={total_layer_count} CURRENT_LAYER=0\\nTIMELAPSE_START\\nM140 S{bed_temperature_initial_layer_single}\\nM104 T{initial_extruder} S140\\nM204 S10000\\n\\nG28 X Y\\nDEFECT_DETECT_NOODLE_FIRST\\n;===== 床面异物检测 ========\\nT{initial_extruder}\\nG90\\nDEFECT_DETECTION_DETECT_BED\\n;===== 取放头检测 =================\\nSM_PRINT_CHECK_SWITCH_EXTRUDER\\n\\n;===== 自动进料 & 挤出流量 & 预挤出 ======================\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=1 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=0\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=0\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=2 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=1\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=1\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=3 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=2\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=2\\nSM_PRINT_AUTO_FEED EXTRUDER=3\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=3\\nM104 S0 T0 A0\\nM104 S0 T1 A0\\nM104 S0 T2 A0\\nM104 S0 T3 A0\\nM104 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\n\\n;===== 粗回零 =================\\nT{initial_extruder}\\nM106 S255\\nM106 P2 S0\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\nROUGHLY_CLEAN_NOZZLE_WITH_DISCARD\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nG28 Z I140 J140\\n\\n;===== 检测钢板 =================\\nDETECT_BED_PLATE\\n\\n;===== 深度清洁喷嘴 =================\\nG90\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 S{nozzle_temperature[initial_extruder] - 50}\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_1\\nM104 S{nozzle_temperature[initial_extruder] - 90}\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_2\\n\\n;===== 精回零 =================\\nM106 S255\\nM109 S{nozzle_temperature[initial_extruder] - 90}\\nM190 S{bed_temperature_initial_layer_single}\\nM107 P2\\nG90\\nG0 Z5 F10000\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{else} \\nG28 Z \\n{endif} \\n\\n\\n;===== 热床调平 =================\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{else} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 \\n{endif} \\n\\n\\n;===== 画起始线 =================\\nG90\\nG1 Z1.5\\nG0 X85 Y1 Z2 F18000\\nM109 S{nozzle_temperature_initial_layer[initial_extruder]}\\nG1 Z0.2\\nM83\\nG1 X185 E15 F360\\nG1 Z1.5\\n\\nG90\\nM106 S0", + "machine_start_gcode": ";===== date: 20260128 =====================\\n\\nPRINT_START\\nDEFECT_DETECTION_START\\nSET_PRINT_STATS_INFO TOTAL_LAYER={total_layer_count} CURRENT_LAYER=0\\nTIMELAPSE_START\\nM140 S{bed_temperature_initial_layer_single}\\nM104 T{initial_extruder} S140\\nM204 S10000\\n{if filament_is_high_temperature[0]}\\n; 保温\\n\tSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=180\\n{else}\\n{if temperature_vitrification[0]<=50}\\n; 强冷\\nSET_PURIFIER_MODE MODE=1 DESIRE_TEMP=42 ALARM_TEMP=45 FAN_SPEED=0.6\\nDELAY_OFF=0\t\t\\n{else}\\n; 弱冷\\nSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=0 FAN_SPEED=0.6 DELAY_OFF=180\t\t\\n{endif}\\n{endif}\\nG28 X Y\\n;===== 床面异物检测 ========\\nT{initial_extruder}\\nG90\\nDEFECT_DETECTION_DETECT_BED\\n;===== 取放头检测 =================\\nSM_PRINT_CHECK_SWITCH_EXTRUDER\\n\\n;===== 自动进料 & 挤出流量 & 预挤出 ======================\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=1 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=0\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=0\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=2 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=1\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=1\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=3 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=2\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=2\\nSM_PRINT_AUTO_FEED EXTRUDER=3\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=3\\nM104 S0 T0 A0\\nM104 S0 T1 A0\\nM104 S0 T2 A0\\nM104 S0 T3 A0\\nM104 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\n\\n;===== 粗回零 =================\\nT{initial_extruder}\\nM106 S255\\nM106 P2 S0\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\nROUGHLY_CLEAN_NOZZLE_WITH_DISCARD\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nG28 Z I140 J140\\n\\n;===== 检测钢板 =================\\nDETECT_BED_PLATE\\n\\n;===== 深度清洁喷嘴 =================\\nG90\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 S{nozzle_temperature[initial_extruder] - 50}\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_1\\nM104 S{nozzle_temperature[initial_extruder] - 90}\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_2\\n\\n;===== 精回零 =================\\nM106 S255\\nM109 S{nozzle_temperature[initial_extruder] - 90}\\nM190 S{bed_temperature_initial_layer_single}\\nM107 P2\\nG90\\nG0 Z5 F10000\\nWAIT_CHAMBER_TEMP TIMEOUT=180\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{else} \\nG28 Z \\n{endif} \\n\\n\\n;===== 热床调平 =================\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{else} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 \\n{endif} \\n\\n\\n;===== 画起始线 =================\\nG90\\nG1 Z1.5\\nG0 X85 Y1 Z2 F18000\\nM109 S{nozzle_temperature_initial_layer[initial_extruder]}\\nG1 Z0.2\\nM83\\nG1 X185 E15 F360\\nG1 Z1.5\\n\\nG90\\nM106 S0", "machine_tool_change_time": "5", "max_layer_height": [ "0.42", diff --git a/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.8 nozzle).json b/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.8 nozzle).json index 82d0b26ac24..94d6eeeafe6 100644 --- a/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.8 nozzle).json +++ b/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.8 nozzle).json @@ -35,7 +35,7 @@ "0", "0" ], - "machine_end_gcode": "{if print_sequence == \\\"by object\\\"}\\nG91\\nG1 X2 Y2 Z1 F24000\\nG90\\nG1 Z{max_layer_z+2} F600\\n{endif}\\nPRINT_END\\nTIMELAPSE_STOP", + "machine_end_gcode": " PRINT_END\nTIMELAPSE_STOP", "machine_max_jerk_z": [ "3", "0.4" @@ -48,7 +48,7 @@ "20", "12" ], - "machine_start_gcode": ";===== date: 20260128 =====================\\n\\nPRINT_START\\nDEFECT_DETECTION_START\\nSET_PRINT_STATS_INFO TOTAL_LAYER={total_layer_count} CURRENT_LAYER=0\\nTIMELAPSE_START\\nM140 S{bed_temperature_initial_layer_single}\\nM104 T{initial_extruder} S140\\nM204 S10000\\n\\nG28 X Y\\nDEFECT_DETECT_NOODLE_FIRST\\n;===== 床面异物检测 ========\\nT{initial_extruder}\\nG90\\nDEFECT_DETECTION_DETECT_BED\\n;===== 取放头检测 =================\\nSM_PRINT_CHECK_SWITCH_EXTRUDER\\n\\n;===== 自动进料 & 挤出流量 & 预挤出 ======================\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=1 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=0\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=0\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=2 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=1\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=1\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=3 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=2\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=2\\nSM_PRINT_AUTO_FEED EXTRUDER=3\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=3\\nM104 S0 T0 A0\\nM104 S0 T1 A0\\nM104 S0 T2 A0\\nM104 S0 T3 A0\\nM104 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\n\\n;===== 粗回零 =================\\nT{initial_extruder}\\nM106 S255\\nM106 P2 S0\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\nROUGHLY_CLEAN_NOZZLE_WITH_DISCARD\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nG28 Z I140 J140\\n\\n;===== 检测钢板 =================\\nDETECT_BED_PLATE\\n\\n;===== 深度清洁喷嘴 =================\\nG90\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 S{nozzle_temperature[initial_extruder] - 50}\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_1\\nM104 S{nozzle_temperature[initial_extruder] - 90}\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_2\\n\\n;===== 精回零 =================\\nM106 S255\\nM109 S{nozzle_temperature[initial_extruder] - 90}\\nM190 S{bed_temperature_initial_layer_single}\\nM107 P2\\nG90\\nG0 Z5 F10000\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{else} \\nG28 Z \\n{endif} \\n\\n\\n;===== 热床调平 =================\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{else} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 \\n{endif} \\n\\n\\n;===== 画起始线 =================\\nG90\\nG1 Z1.5\\nG0 X85 Y1 Z2 F18000\\nM109 S{nozzle_temperature_initial_layer[initial_extruder]}\\nG1 Z0.2\\nM83\\nG1 X185 E15 F360\\nG1 Z1.5\\n\\nG90\\nM106 S0", + "machine_start_gcode": ";===== date: 20260128 =====================\\n\\nPRINT_START\\nDEFECT_DETECTION_START\\nSET_PRINT_STATS_INFO TOTAL_LAYER={total_layer_count} CURRENT_LAYER=0\\nTIMELAPSE_START\\nM140 S{bed_temperature_initial_layer_single}\\nM104 T{initial_extruder} S140\\nM204 S10000\\n{if filament_is_high_temperature[0]}\\n; 保温\\n\tSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=180\\n{else}\\n{if temperature_vitrification[0]<=50}\\n; 强冷\\nSET_PURIFIER_MODE MODE=1 DESIRE_TEMP=42 ALARM_TEMP=45 FAN_SPEED=0.6\\nDELAY_OFF=0\t\t\\n{else}\\n; 弱冷\\nSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=0 FAN_SPEED=0.6 DELAY_OFF=180\t\t\\n{endif}\\n{endif}\\nG28 X Y\\n;===== 床面异物检测 ========\\nT{initial_extruder}\\nG90\\nDEFECT_DETECTION_DETECT_BED\\n;===== 取放头检测 =================\\nSM_PRINT_CHECK_SWITCH_EXTRUDER\\n\\n;===== 自动进料 & 挤出流量 & 预挤出 ======================\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=1 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=0\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=0\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=2 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=1\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=1\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=3 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=2\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=2\\nSM_PRINT_AUTO_FEED EXTRUDER=3\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=3\\nM104 S0 T0 A0\\nM104 S0 T1 A0\\nM104 S0 T2 A0\\nM104 S0 T3 A0\\nM104 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\n\\n;===== 粗回零 =================\\nT{initial_extruder}\\nM106 S255\\nM106 P2 S0\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\nROUGHLY_CLEAN_NOZZLE_WITH_DISCARD\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nG28 Z I140 J140\\n\\n;===== 检测钢板 =================\\nDETECT_BED_PLATE\\n\\n;===== 深度清洁喷嘴 =================\\nG90\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 S{nozzle_temperature[initial_extruder] - 50}\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_1\\nM104 S{nozzle_temperature[initial_extruder] - 90}\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_2\\n\\n;===== 精回零 =================\\nM106 S255\\nM109 S{nozzle_temperature[initial_extruder] - 90}\\nM190 S{bed_temperature_initial_layer_single}\\nM107 P2\\nG90\\nG0 Z5 F10000\\nWAIT_CHAMBER_TEMP TIMEOUT=180\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{else} \\nG28 Z \\n{endif} \\n\\n\\n;===== 热床调平 =================\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{else} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 \\n{endif} \\n\\n\\n;===== 画起始线 =================\\nG90\\nG1 Z1.5\\nG0 X85 Y1 Z2 F18000\\nM109 S{nozzle_temperature_initial_layer[initial_extruder]}\\nG1 Z0.2\\nM83\\nG1 X185 E15 F360\\nG1 Z1.5\\n\\nG90\\nM106 S0", "machine_tool_change_time": "5", "max_layer_height": [ "0.56", From fe0d211e6bc15821cfe1ec8c79816b0f9f471b1c Mon Sep 17 00:00:00 2001 From: Joyx-SnapMaker <1906956558@qq.com> Date: Mon, 27 Apr 2026 18:26:51 +0800 Subject: [PATCH 070/135] =?UTF-8?q?Fix=EF=BC=9ACorrect=20the=20incorrect?= =?UTF-8?q?=20G-code=20code?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../profiles/Snapmaker/machine/Snapmaker U1 (0.2 nozzle).json | 2 +- .../profiles/Snapmaker/machine/Snapmaker U1 (0.4 nozzle).json | 2 +- .../profiles/Snapmaker/machine/Snapmaker U1 (0.6 nozzle).json | 2 +- .../profiles/Snapmaker/machine/Snapmaker U1 (0.8 nozzle).json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.2 nozzle).json b/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.2 nozzle).json index 7456699be5c..48c14548825 100644 --- a/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.2 nozzle).json +++ b/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.2 nozzle).json @@ -48,7 +48,7 @@ "20", "12" ], - "machine_start_gcode": ";===== date: 20260128 =====================\\n\\nPRINT_START\\nDEFECT_DETECTION_START\\nSET_PRINT_STATS_INFO TOTAL_LAYER={total_layer_count} CURRENT_LAYER=0\\nTIMELAPSE_START\\nM140 S{bed_temperature_initial_layer_single}\\nM104 T{initial_extruder} S140\\nM204 S10000\\n{if filament_is_high_temperature[0]}\\n; 保温\\n\tSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=180\\n{else}\\n{if temperature_vitrification[0]<=50}\\n; 强冷\\nSET_PURIFIER_MODE MODE=1 DESIRE_TEMP=42 ALARM_TEMP=45 FAN_SPEED=0.6\\nDELAY_OFF=0\t\t\\n{else}\\n; 弱冷\\nSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=0 FAN_SPEED=0.6 DELAY_OFF=180\t\t\\n{endif}\\n{endif}\\nG28 X Y\\n;===== 床面异物检测 ========\\nT{initial_extruder}\\nG90\\nDEFECT_DETECTION_DETECT_BED\\n;===== 取放头检测 =================\\nSM_PRINT_CHECK_SWITCH_EXTRUDER\\n\\n;===== 自动进料 & 挤出流量 & 预挤出 ======================\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=1 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=0\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=0\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=2 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=1\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=1\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=3 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=2\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=2\\nSM_PRINT_AUTO_FEED EXTRUDER=3\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=3\\nM104 S0 T0 A0\\nM104 S0 T1 A0\\nM104 S0 T2 A0\\nM104 S0 T3 A0\\nM104 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\n\\n;===== 粗回零 =================\\nT{initial_extruder}\\nM106 S255\\nM106 P2 S0\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\nROUGHLY_CLEAN_NOZZLE_WITH_DISCARD\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nG28 Z I140 J140\\n\\n;===== 检测钢板 =================\\nDETECT_BED_PLATE\\n\\n;===== 深度清洁喷嘴 =================\\nG90\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 S{nozzle_temperature[initial_extruder] - 50}\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_1\\nM104 S{nozzle_temperature[initial_extruder] - 90}\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_2\\n\\n;===== 精回零 =================\\nM106 S255\\nM109 S{nozzle_temperature[initial_extruder] - 90}\\nM190 S{bed_temperature_initial_layer_single}\\nM107 P2\\nG90\\nG0 Z5 F10000\\nWAIT_CHAMBER_TEMP TIMEOUT=180\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{else} \\nG28 Z \\n{endif} \\n\\n\\n;===== 热床调平 =================\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{else} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 \\n{endif} \\n\\n\\n;===== 画起始线 =================\\nG90\\nG1 Z1.5\\nG0 X85 Y1 Z2 F18000\\nM109 S{nozzle_temperature_initial_layer[initial_extruder]}\\nG1 Z0.2\\nM83\\nG1 X185 E15 F360\\nG1 Z1.5\\n\\nG90\\nM106 S0", + "machine_start_gcode": ";===== date: 20260128 =====================\\n\\nPRINT_START\\nDEFECT_DETECTION_START\\nSET_PRINT_STATS_INFO TOTAL_LAYER={total_layer_count} CURRENT_LAYER=0\\nTIMELAPSE_START\\nM140 S{bed_temperature_initial_layer_single}\\nM104 T{initial_extruder} S140\\nM204 S10000\\n{if filament_is_high_temperature[0]}\\n; 保温\\n\tSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=180\\n{else}\\n{if temperature_vitrification[0]<=50}\\n; 强冷\\nSET_PURIFIER_MODE MODE=1 DESIRE_TEMP=42 ALARM_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=0\t\t\\n{else}\\n; 弱冷\\nSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=0 FAN_SPEED=0.6 DELAY_OFF=180\t\t\\n{endif}\\n{endif}\\nG28 X Y\\n;===== 床面异物检测 ========\\nT{initial_extruder}\\nG90\\nDEFECT_DETECTION_DETECT_BED\\n;===== 取放头检测 =================\\nSM_PRINT_CHECK_SWITCH_EXTRUDER\\n\\n;===== 自动进料 & 挤出流量 & 预挤出 ======================\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=1 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=0\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=0\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=2 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=1\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=1\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=3 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=2\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=2\\nSM_PRINT_AUTO_FEED EXTRUDER=3\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=3\\nM104 S0 T0 A0\\nM104 S0 T1 A0\\nM104 S0 T2 A0\\nM104 S0 T3 A0\\nM104 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\n\\n;===== 粗回零 =================\\nT{initial_extruder}\\nM106 S255\\nM106 P2 S0\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\nROUGHLY_CLEAN_NOZZLE_WITH_DISCARD\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nG28 Z I140 J140\\n\\n;===== 检测钢板 =================\\nDETECT_BED_PLATE\\n\\n;===== 深度清洁喷嘴 =================\\nG90\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 S{nozzle_temperature[initial_extruder] - 50}\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_1\\nM104 S{nozzle_temperature[initial_extruder] - 90}\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_2\\n\\n;===== 精回零 =================\\nM106 S255\\nM109 S{nozzle_temperature[initial_extruder] - 90}\\nM190 S{bed_temperature_initial_layer_single}\\nM107 P2\\nG90\\nG0 Z5 F10000\\nWAIT_CHAMBER_TEMP TIMEOUT=180\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{else} \\nG28 Z \\n{endif} \\n\\n\\n;===== 热床调平 =================\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{else} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 \\n{endif} \\n\\n\\n;===== 画起始线 =================\\nG90\\nG1 Z1.5\\nG0 X85 Y1 Z2 F18000\\nM109 S{nozzle_temperature_initial_layer[initial_extruder]}\\nG1 Z0.2\\nM83\\nG1 X185 E15 F360\\nG1 Z1.5\\n\\nG90\\nM106 S0", "machine_tool_change_time": "5", "max_layer_height": [ "0.14", diff --git a/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.4 nozzle).json b/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.4 nozzle).json index 95ff6d82d6b..2d1c8ae5ac5 100644 --- a/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.4 nozzle).json +++ b/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.4 nozzle).json @@ -48,7 +48,7 @@ "20", "12" ], - "machine_start_gcode": ";===== date: 20260128 =====================\\n\\nPRINT_START\\nDEFECT_DETECTION_START\\nSET_PRINT_STATS_INFO TOTAL_LAYER={total_layer_count} CURRENT_LAYER=0\\nTIMELAPSE_START\\nM140 S{bed_temperature_initial_layer_single}\\nM104 T{initial_extruder} S140\\nM204 S10000\\n{if filament_is_high_temperature[0]}\\n; 保温\\n\tSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=180\\n{else}\\n{if temperature_vitrification[0]<=50}\\n; 强冷\\nSET_PURIFIER_MODE MODE=1 DESIRE_TEMP=42 ALARM_TEMP=45 FAN_SPEED=0.6\\nDELAY_OFF=0\t\t\\n{else}\\n; 弱冷\\nSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=0 FAN_SPEED=0.6 DELAY_OFF=180\t\t\\n{endif}\\n{endif}\\n\\nG28 X Y\\n;===== 床面异物检测 ========\\nT{initial_extruder}\\nG90\\nDEFECT_DETECTION_DETECT_BED\\n;===== 取放头检测 =================\\nSM_PRINT_CHECK_SWITCH_EXTRUDER\\n\\n;===== 自动进料 & 挤出流量 & 预挤出 ======================\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=1 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=0\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=0\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=2 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=1\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=1\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=3 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=2\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=2\\nSM_PRINT_AUTO_FEED EXTRUDER=3\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=3\\nM104 S0 T0 A0\\nM104 S0 T1 A0\\nM104 S0 T2 A0\\nM104 S0 T3 A0\\nM104 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\n\\n;===== 粗回零 =================\\nT{initial_extruder}\\nM106 S255\\nM106 P2 S0\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\nROUGHLY_CLEAN_NOZZLE_WITH_DISCARD\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nG28 Z I140 J140\\n\\n;===== 检测钢板 =================\\nDETECT_BED_PLATE\\n\\n;===== 深度清洁喷嘴 =================\\nG90\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 S{nozzle_temperature[initial_extruder] - 50}\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_1\\nM104 S{nozzle_temperature[initial_extruder] - 90}\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_2\\n\\n;===== 精回零 =================\\nM106 S255\\nM109 S{nozzle_temperature[initial_extruder] - 90}\\nM190 S{bed_temperature_initial_layer_single}\\nM107 P2\\nG90\\nG0 Z5 F10000\\nWAIT_CHAMBER_TEMP TIMEOUT=180\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{else} \\nG28 Z \\n{endif} \\n\\n\\n;===== 热床调平 =================\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{else} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 \\n{endif} \\n\\n\\n;===== 画起始线 =================\\nG90\\nG1 Z1.5\\nG0 X85 Y1 Z2 F18000\\nM109 S{nozzle_temperature_initial_layer[initial_extruder]}\\nG1 Z0.2\\nM83\\nG1 X185 E15 F360\\nG1 Z1.5\\n\\nG90\\nM106 S0", + "machine_start_gcode": ";===== date: 20260128 =====================\\n\\nPRINT_START\\nDEFECT_DETECTION_START\\nSET_PRINT_STATS_INFO TOTAL_LAYER={total_layer_count} CURRENT_LAYER=0\\nTIMELAPSE_START\\nM140 S{bed_temperature_initial_layer_single}\\nM104 T{initial_extruder} S140\\nM204 S10000\\n{if filament_is_high_temperature[0]}\\n; 保温\\n\tSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=180\\n{else}\\n{if temperature_vitrification[0]<=50}\\n; 强冷\\nSET_PURIFIER_MODE MODE=1 DESIRE_TEMP=42 ALARM_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=0\t\t\\n{else}\\n; 弱冷\\nSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=0 FAN_SPEED=0.6 DELAY_OFF=180\t\t\\n{endif}\\n{endif}\\n\\nG28 X Y\\n;===== 床面异物检测 ========\\nT{initial_extruder}\\nG90\\nDEFECT_DETECTION_DETECT_BED\\n;===== 取放头检测 =================\\nSM_PRINT_CHECK_SWITCH_EXTRUDER\\n\\n;===== 自动进料 & 挤出流量 & 预挤出 ======================\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=1 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=0\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=0\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=2 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=1\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=1\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=3 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=2\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=2\\nSM_PRINT_AUTO_FEED EXTRUDER=3\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=3\\nM104 S0 T0 A0\\nM104 S0 T1 A0\\nM104 S0 T2 A0\\nM104 S0 T3 A0\\nM104 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\n\\n;===== 粗回零 =================\\nT{initial_extruder}\\nM106 S255\\nM106 P2 S0\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\nROUGHLY_CLEAN_NOZZLE_WITH_DISCARD\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nG28 Z I140 J140\\n\\n;===== 检测钢板 =================\\nDETECT_BED_PLATE\\n\\n;===== 深度清洁喷嘴 =================\\nG90\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 S{nozzle_temperature[initial_extruder] - 50}\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_1\\nM104 S{nozzle_temperature[initial_extruder] - 90}\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_2\\n\\n;===== 精回零 =================\\nM106 S255\\nM109 S{nozzle_temperature[initial_extruder] - 90}\\nM190 S{bed_temperature_initial_layer_single}\\nM107 P2\\nG90\\nG0 Z5 F10000\\nWAIT_CHAMBER_TEMP TIMEOUT=180\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{else} \\nG28 Z \\n{endif} \\n\\n\\n;===== 热床调平 =================\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{else} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 \\n{endif} \\n\\n\\n;===== 画起始线 =================\\nG90\\nG1 Z1.5\\nG0 X85 Y1 Z2 F18000\\nM109 S{nozzle_temperature_initial_layer[initial_extruder]}\\nG1 Z0.2\\nM83\\nG1 X185 E15 F360\\nG1 Z1.5\\n\\nG90\\nM106 S0", "machine_tool_change_time": "5", "max_layer_height": [ "0.32", diff --git a/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.6 nozzle).json b/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.6 nozzle).json index 40d8bf7e5da..cfec4db7c79 100644 --- a/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.6 nozzle).json +++ b/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.6 nozzle).json @@ -48,7 +48,7 @@ "20", "12" ], - "machine_start_gcode": ";===== date: 20260128 =====================\\n\\nPRINT_START\\nDEFECT_DETECTION_START\\nSET_PRINT_STATS_INFO TOTAL_LAYER={total_layer_count} CURRENT_LAYER=0\\nTIMELAPSE_START\\nM140 S{bed_temperature_initial_layer_single}\\nM104 T{initial_extruder} S140\\nM204 S10000\\n{if filament_is_high_temperature[0]}\\n; 保温\\n\tSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=180\\n{else}\\n{if temperature_vitrification[0]<=50}\\n; 强冷\\nSET_PURIFIER_MODE MODE=1 DESIRE_TEMP=42 ALARM_TEMP=45 FAN_SPEED=0.6\\nDELAY_OFF=0\t\t\\n{else}\\n; 弱冷\\nSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=0 FAN_SPEED=0.6 DELAY_OFF=180\t\t\\n{endif}\\n{endif}\\nG28 X Y\\n;===== 床面异物检测 ========\\nT{initial_extruder}\\nG90\\nDEFECT_DETECTION_DETECT_BED\\n;===== 取放头检测 =================\\nSM_PRINT_CHECK_SWITCH_EXTRUDER\\n\\n;===== 自动进料 & 挤出流量 & 预挤出 ======================\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=1 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=0\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=0\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=2 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=1\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=1\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=3 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=2\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=2\\nSM_PRINT_AUTO_FEED EXTRUDER=3\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=3\\nM104 S0 T0 A0\\nM104 S0 T1 A0\\nM104 S0 T2 A0\\nM104 S0 T3 A0\\nM104 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\n\\n;===== 粗回零 =================\\nT{initial_extruder}\\nM106 S255\\nM106 P2 S0\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\nROUGHLY_CLEAN_NOZZLE_WITH_DISCARD\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nG28 Z I140 J140\\n\\n;===== 检测钢板 =================\\nDETECT_BED_PLATE\\n\\n;===== 深度清洁喷嘴 =================\\nG90\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 S{nozzle_temperature[initial_extruder] - 50}\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_1\\nM104 S{nozzle_temperature[initial_extruder] - 90}\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_2\\n\\n;===== 精回零 =================\\nM106 S255\\nM109 S{nozzle_temperature[initial_extruder] - 90}\\nM190 S{bed_temperature_initial_layer_single}\\nM107 P2\\nG90\\nG0 Z5 F10000\\nWAIT_CHAMBER_TEMP TIMEOUT=180\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{else} \\nG28 Z \\n{endif} \\n\\n\\n;===== 热床调平 =================\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{else} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 \\n{endif} \\n\\n\\n;===== 画起始线 =================\\nG90\\nG1 Z1.5\\nG0 X85 Y1 Z2 F18000\\nM109 S{nozzle_temperature_initial_layer[initial_extruder]}\\nG1 Z0.2\\nM83\\nG1 X185 E15 F360\\nG1 Z1.5\\n\\nG90\\nM106 S0", + "machine_start_gcode": ";===== date: 20260128 =====================\\n\\nPRINT_START\\nDEFECT_DETECTION_START\\nSET_PRINT_STATS_INFO TOTAL_LAYER={total_layer_count} CURRENT_LAYER=0\\nTIMELAPSE_START\\nM140 S{bed_temperature_initial_layer_single}\\nM104 T{initial_extruder} S140\\nM204 S10000\\n{if filament_is_high_temperature[0]}\\n; 保温\\n\tSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=180\\n{else}\\n{if temperature_vitrification[0]<=50}\\n; 强冷\\nSET_PURIFIER_MODE MODE=1 DESIRE_TEMP=42 ALARM_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=0\t\t\\n{else}\\n; 弱冷\\nSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=0 FAN_SPEED=0.6 DELAY_OFF=180\t\t\\n{endif}\\n{endif}\\nG28 X Y\\n;===== 床面异物检测 ========\\nT{initial_extruder}\\nG90\\nDEFECT_DETECTION_DETECT_BED\\n;===== 取放头检测 =================\\nSM_PRINT_CHECK_SWITCH_EXTRUDER\\n\\n;===== 自动进料 & 挤出流量 & 预挤出 ======================\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=1 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=0\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=0\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=2 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=1\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=1\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=3 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=2\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=2\\nSM_PRINT_AUTO_FEED EXTRUDER=3\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=3\\nM104 S0 T0 A0\\nM104 S0 T1 A0\\nM104 S0 T2 A0\\nM104 S0 T3 A0\\nM104 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\n\\n;===== 粗回零 =================\\nT{initial_extruder}\\nM106 S255\\nM106 P2 S0\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\nROUGHLY_CLEAN_NOZZLE_WITH_DISCARD\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nG28 Z I140 J140\\n\\n;===== 检测钢板 =================\\nDETECT_BED_PLATE\\n\\n;===== 深度清洁喷嘴 =================\\nG90\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 S{nozzle_temperature[initial_extruder] - 50}\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_1\\nM104 S{nozzle_temperature[initial_extruder] - 90}\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_2\\n\\n;===== 精回零 =================\\nM106 S255\\nM109 S{nozzle_temperature[initial_extruder] - 90}\\nM190 S{bed_temperature_initial_layer_single}\\nM107 P2\\nG90\\nG0 Z5 F10000\\nWAIT_CHAMBER_TEMP TIMEOUT=180\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{else} \\nG28 Z \\n{endif} \\n\\n\\n;===== 热床调平 =================\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{else} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 \\n{endif} \\n\\n\\n;===== 画起始线 =================\\nG90\\nG1 Z1.5\\nG0 X85 Y1 Z2 F18000\\nM109 S{nozzle_temperature_initial_layer[initial_extruder]}\\nG1 Z0.2\\nM83\\nG1 X185 E15 F360\\nG1 Z1.5\\n\\nG90\\nM106 S0", "machine_tool_change_time": "5", "max_layer_height": [ "0.42", diff --git a/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.8 nozzle).json b/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.8 nozzle).json index 94d6eeeafe6..12692f2bd96 100644 --- a/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.8 nozzle).json +++ b/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.8 nozzle).json @@ -48,7 +48,7 @@ "20", "12" ], - "machine_start_gcode": ";===== date: 20260128 =====================\\n\\nPRINT_START\\nDEFECT_DETECTION_START\\nSET_PRINT_STATS_INFO TOTAL_LAYER={total_layer_count} CURRENT_LAYER=0\\nTIMELAPSE_START\\nM140 S{bed_temperature_initial_layer_single}\\nM104 T{initial_extruder} S140\\nM204 S10000\\n{if filament_is_high_temperature[0]}\\n; 保温\\n\tSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=180\\n{else}\\n{if temperature_vitrification[0]<=50}\\n; 强冷\\nSET_PURIFIER_MODE MODE=1 DESIRE_TEMP=42 ALARM_TEMP=45 FAN_SPEED=0.6\\nDELAY_OFF=0\t\t\\n{else}\\n; 弱冷\\nSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=0 FAN_SPEED=0.6 DELAY_OFF=180\t\t\\n{endif}\\n{endif}\\nG28 X Y\\n;===== 床面异物检测 ========\\nT{initial_extruder}\\nG90\\nDEFECT_DETECTION_DETECT_BED\\n;===== 取放头检测 =================\\nSM_PRINT_CHECK_SWITCH_EXTRUDER\\n\\n;===== 自动进料 & 挤出流量 & 预挤出 ======================\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=1 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=0\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=0\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=2 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=1\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=1\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=3 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=2\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=2\\nSM_PRINT_AUTO_FEED EXTRUDER=3\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=3\\nM104 S0 T0 A0\\nM104 S0 T1 A0\\nM104 S0 T2 A0\\nM104 S0 T3 A0\\nM104 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\n\\n;===== 粗回零 =================\\nT{initial_extruder}\\nM106 S255\\nM106 P2 S0\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\nROUGHLY_CLEAN_NOZZLE_WITH_DISCARD\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nG28 Z I140 J140\\n\\n;===== 检测钢板 =================\\nDETECT_BED_PLATE\\n\\n;===== 深度清洁喷嘴 =================\\nG90\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 S{nozzle_temperature[initial_extruder] - 50}\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_1\\nM104 S{nozzle_temperature[initial_extruder] - 90}\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_2\\n\\n;===== 精回零 =================\\nM106 S255\\nM109 S{nozzle_temperature[initial_extruder] - 90}\\nM190 S{bed_temperature_initial_layer_single}\\nM107 P2\\nG90\\nG0 Z5 F10000\\nWAIT_CHAMBER_TEMP TIMEOUT=180\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{else} \\nG28 Z \\n{endif} \\n\\n\\n;===== 热床调平 =================\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{else} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 \\n{endif} \\n\\n\\n;===== 画起始线 =================\\nG90\\nG1 Z1.5\\nG0 X85 Y1 Z2 F18000\\nM109 S{nozzle_temperature_initial_layer[initial_extruder]}\\nG1 Z0.2\\nM83\\nG1 X185 E15 F360\\nG1 Z1.5\\n\\nG90\\nM106 S0", + "machine_start_gcode": ";===== date: 20260128 =====================\\n\\nPRINT_START\\nDEFECT_DETECTION_START\\nSET_PRINT_STATS_INFO TOTAL_LAYER={total_layer_count} CURRENT_LAYER=0\\nTIMELAPSE_START\\nM140 S{bed_temperature_initial_layer_single}\\nM104 T{initial_extruder} S140\\nM204 S10000\\n{if filament_is_high_temperature[0]}\\n; 保温\\n\tSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=180\\n{else}\\n{if temperature_vitrification[0]<=50}\\n; 强冷\\nSET_PURIFIER_MODE MODE=1 DESIRE_TEMP=42 ALARM_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=0\t\t\\n{else}\\n; 弱冷\\nSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=0 FAN_SPEED=0.6 DELAY_OFF=180\t\t\\n{endif}\\n{endif}\\nG28 X Y\\n;===== 床面异物检测 ========\\nT{initial_extruder}\\nG90\\nDEFECT_DETECTION_DETECT_BED\\n;===== 取放头检测 =================\\nSM_PRINT_CHECK_SWITCH_EXTRUDER\\n\\n;===== 自动进料 & 挤出流量 & 预挤出 ======================\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=1 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=0\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=0\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=2 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=1\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=1\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=3 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=2\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=2\\nSM_PRINT_AUTO_FEED EXTRUDER=3\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=3\\nM104 S0 T0 A0\\nM104 S0 T1 A0\\nM104 S0 T2 A0\\nM104 S0 T3 A0\\nM104 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\n\\n;===== 粗回零 =================\\nT{initial_extruder}\\nM106 S255\\nM106 P2 S0\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\nROUGHLY_CLEAN_NOZZLE_WITH_DISCARD\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nG28 Z I140 J140\\n\\n;===== 检测钢板 =================\\nDETECT_BED_PLATE\\n\\n;===== 深度清洁喷嘴 =================\\nG90\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 S{nozzle_temperature[initial_extruder] - 50}\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_1\\nM104 S{nozzle_temperature[initial_extruder] - 90}\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_2\\n\\n;===== 精回零 =================\\nM106 S255\\nM109 S{nozzle_temperature[initial_extruder] - 90}\\nM190 S{bed_temperature_initial_layer_single}\\nM107 P2\\nG90\\nG0 Z5 F10000\\nWAIT_CHAMBER_TEMP TIMEOUT=180\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{else} \\nG28 Z \\n{endif} \\n\\n\\n;===== 热床调平 =================\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{else} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 \\n{endif} \\n\\n\\n;===== 画起始线 =================\\nG90\\nG1 Z1.5\\nG0 X85 Y1 Z2 F18000\\nM109 S{nozzle_temperature_initial_layer[initial_extruder]}\\nG1 Z0.2\\nM83\\nG1 X185 E15 F360\\nG1 Z1.5\\n\\nG90\\nM106 S0", "machine_tool_change_time": "5", "max_layer_height": [ "0.56", From 85e30e090f0fe8f09e0577d364fb7b331134f153 Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Thu, 21 May 2026 18:27:29 +0800 Subject: [PATCH 071/135] =?UTF-8?q?fix=EF=BC=9AThe=20top=20cover=20mode=20?= =?UTF-8?q?is=20determined=20based=20on=20the=20temperature=20characterist?= =?UTF-8?q?ics=20of=20the=20internal=20consumables.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../machine/Snapmaker U1 (0.2 nozzle).json | 4 ++-- .../machine/Snapmaker U1 (0.4 nozzle).json | 4 ++-- .../machine/Snapmaker U1 (0.6 nozzle).json | 4 ++-- .../machine/Snapmaker U1 (0.8 nozzle).json | 4 ++-- src/libslic3r/GCode.cpp | 19 +++++++++++++++++++ 5 files changed, 27 insertions(+), 8 deletions(-) diff --git a/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.2 nozzle).json b/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.2 nozzle).json index 48c14548825..c3ef510ca5e 100644 --- a/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.2 nozzle).json +++ b/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.2 nozzle).json @@ -35,7 +35,7 @@ "0", "0" ], - "machine_end_gcode": " PRINT_END\nTIMELAPSE_STOP", + "machine_end_gcode": "{if print_sequence == \\\"by object\\\"}\\nG91\\nG1 X2 Y2 Z1 F24000\\nG90\\nG1 Z{max_layer_z+2} F600\\n{endif}\\nPRINT_END\\nTIMELAPSE_STOP", "machine_max_jerk_z": [ "3", "0.4" @@ -48,7 +48,7 @@ "20", "12" ], - "machine_start_gcode": ";===== date: 20260128 =====================\\n\\nPRINT_START\\nDEFECT_DETECTION_START\\nSET_PRINT_STATS_INFO TOTAL_LAYER={total_layer_count} CURRENT_LAYER=0\\nTIMELAPSE_START\\nM140 S{bed_temperature_initial_layer_single}\\nM104 T{initial_extruder} S140\\nM204 S10000\\n{if filament_is_high_temperature[0]}\\n; 保温\\n\tSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=180\\n{else}\\n{if temperature_vitrification[0]<=50}\\n; 强冷\\nSET_PURIFIER_MODE MODE=1 DESIRE_TEMP=42 ALARM_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=0\t\t\\n{else}\\n; 弱冷\\nSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=0 FAN_SPEED=0.6 DELAY_OFF=180\t\t\\n{endif}\\n{endif}\\nG28 X Y\\n;===== 床面异物检测 ========\\nT{initial_extruder}\\nG90\\nDEFECT_DETECTION_DETECT_BED\\n;===== 取放头检测 =================\\nSM_PRINT_CHECK_SWITCH_EXTRUDER\\n\\n;===== 自动进料 & 挤出流量 & 预挤出 ======================\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=1 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=0\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=0\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=2 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=1\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=1\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=3 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=2\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=2\\nSM_PRINT_AUTO_FEED EXTRUDER=3\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=3\\nM104 S0 T0 A0\\nM104 S0 T1 A0\\nM104 S0 T2 A0\\nM104 S0 T3 A0\\nM104 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\n\\n;===== 粗回零 =================\\nT{initial_extruder}\\nM106 S255\\nM106 P2 S0\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\nROUGHLY_CLEAN_NOZZLE_WITH_DISCARD\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nG28 Z I140 J140\\n\\n;===== 检测钢板 =================\\nDETECT_BED_PLATE\\n\\n;===== 深度清洁喷嘴 =================\\nG90\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 S{nozzle_temperature[initial_extruder] - 50}\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_1\\nM104 S{nozzle_temperature[initial_extruder] - 90}\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_2\\n\\n;===== 精回零 =================\\nM106 S255\\nM109 S{nozzle_temperature[initial_extruder] - 90}\\nM190 S{bed_temperature_initial_layer_single}\\nM107 P2\\nG90\\nG0 Z5 F10000\\nWAIT_CHAMBER_TEMP TIMEOUT=180\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{else} \\nG28 Z \\n{endif} \\n\\n\\n;===== 热床调平 =================\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{else} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 \\n{endif} \\n\\n\\n;===== 画起始线 =================\\nG90\\nG1 Z1.5\\nG0 X85 Y1 Z2 F18000\\nM109 S{nozzle_temperature_initial_layer[initial_extruder]}\\nG1 Z0.2\\nM83\\nG1 X185 E15 F360\\nG1 Z1.5\\n\\nG90\\nM106 S0", + "machine_start_gcode": ";===== date: 20260128 =====================\\n\\nPRINT_START\\nDEFECT_DETECTION_START\\nSET_PRINT_STATS_INFO TOTAL_LAYER={total_layer_count} CURRENT_LAYER=0\\nTIMELAPSE_START\\nM140 S{bed_temperature_initial_layer_single}\\nM104 T{initial_extruder} S140\\nM204 S10000\\n{if chamber_cooling_mode==0}\\n; 保温\\nSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=180\\n{elsif chamber_cooling_mode==2}\\n; 强冷\\nSET_PURIFIER_MODE MODE=1 DESIRE_TEMP=42 ALARM_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=0\\n{else}\\n; 弱冷\\nSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=0 FAN_SPEED=0.6 DELAY_OFF=180\t\t\\n{endif}\\nG28 X Y\\n;===== 床面异物检测 ========\\nT{initial_extruder}\\nG90\\nDEFECT_DETECTION_DETECT_BED\\n;===== 取放头检测 =================\\nSM_PRINT_CHECK_SWITCH_EXTRUDER\\n\\n;===== 自动进料 & 挤出流量 & 预挤出 ======================\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=1 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=0\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=0\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=2 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=1\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=1\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=3 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=2\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=2\\nSM_PRINT_AUTO_FEED EXTRUDER=3\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=3\\nM104 S0 T0 A0\\nM104 S0 T1 A0\\nM104 S0 T2 A0\\nM104 S0 T3 A0\\nM104 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\n\\n;===== 粗回零 =================\\nT{initial_extruder}\\nM106 S255\\nM106 P2 S0\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\nROUGHLY_CLEAN_NOZZLE_WITH_DISCARD\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nG28 Z I140 J140\\n\\n;===== 检测钢板 =================\\nDETECT_BED_PLATE\\n\\n;===== 深度清洁喷嘴 =================\\nG90\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 S{nozzle_temperature[initial_extruder] - 50}\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_1\\nM104 S{nozzle_temperature[initial_extruder] - 90}\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_2\\n\\n;===== 精回零 =================\\nM106 S255\\nM109 S{nozzle_temperature[initial_extruder] - 90}\\nM190 S{bed_temperature_initial_layer_single}\\nM107 P2\\nG90\\nG0 Z5 F10000\\nWAIT_CHAMBER_TEMP TIMEOUT=180\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{else} \\nG28 Z \\n{endif} \\n\\n\\n;===== 热床调平 =================\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{else} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 \\n{endif} \\n\\n\\n;===== 画起始线 =================\\nG90\\nG1 Z1.5\\nG0 X85 Y1 Z2 F18000\\nM109 S{nozzle_temperature_initial_layer[initial_extruder]}\\nG1 Z0.2\\nM83\\nG1 X185 E15 F360\\nG1 Z1.5\\n\\nG90\\nM106 S0", "machine_tool_change_time": "5", "max_layer_height": [ "0.14", diff --git a/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.4 nozzle).json b/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.4 nozzle).json index 2d1c8ae5ac5..c9d896e1303 100644 --- a/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.4 nozzle).json +++ b/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.4 nozzle).json @@ -35,7 +35,7 @@ "0", "0" ], - "machine_end_gcode": " PRINT_END\nTIMELAPSE_STOP", + "machine_end_gcode": "{if print_sequence == \\\"by object\\\"}\\nG91\\nG1 X2 Y2 Z1 F24000\\nG90\\nG1 Z{max_layer_z+2} F600\\n{endif}\\nPRINT_END\\nTIMELAPSE_STOP", "machine_max_jerk_z": [ "3", "0.4" @@ -48,7 +48,7 @@ "20", "12" ], - "machine_start_gcode": ";===== date: 20260128 =====================\\n\\nPRINT_START\\nDEFECT_DETECTION_START\\nSET_PRINT_STATS_INFO TOTAL_LAYER={total_layer_count} CURRENT_LAYER=0\\nTIMELAPSE_START\\nM140 S{bed_temperature_initial_layer_single}\\nM104 T{initial_extruder} S140\\nM204 S10000\\n{if filament_is_high_temperature[0]}\\n; 保温\\n\tSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=180\\n{else}\\n{if temperature_vitrification[0]<=50}\\n; 强冷\\nSET_PURIFIER_MODE MODE=1 DESIRE_TEMP=42 ALARM_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=0\t\t\\n{else}\\n; 弱冷\\nSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=0 FAN_SPEED=0.6 DELAY_OFF=180\t\t\\n{endif}\\n{endif}\\n\\nG28 X Y\\n;===== 床面异物检测 ========\\nT{initial_extruder}\\nG90\\nDEFECT_DETECTION_DETECT_BED\\n;===== 取放头检测 =================\\nSM_PRINT_CHECK_SWITCH_EXTRUDER\\n\\n;===== 自动进料 & 挤出流量 & 预挤出 ======================\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=1 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=0\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=0\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=2 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=1\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=1\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=3 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=2\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=2\\nSM_PRINT_AUTO_FEED EXTRUDER=3\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=3\\nM104 S0 T0 A0\\nM104 S0 T1 A0\\nM104 S0 T2 A0\\nM104 S0 T3 A0\\nM104 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\n\\n;===== 粗回零 =================\\nT{initial_extruder}\\nM106 S255\\nM106 P2 S0\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\nROUGHLY_CLEAN_NOZZLE_WITH_DISCARD\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nG28 Z I140 J140\\n\\n;===== 检测钢板 =================\\nDETECT_BED_PLATE\\n\\n;===== 深度清洁喷嘴 =================\\nG90\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 S{nozzle_temperature[initial_extruder] - 50}\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_1\\nM104 S{nozzle_temperature[initial_extruder] - 90}\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_2\\n\\n;===== 精回零 =================\\nM106 S255\\nM109 S{nozzle_temperature[initial_extruder] - 90}\\nM190 S{bed_temperature_initial_layer_single}\\nM107 P2\\nG90\\nG0 Z5 F10000\\nWAIT_CHAMBER_TEMP TIMEOUT=180\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{else} \\nG28 Z \\n{endif} \\n\\n\\n;===== 热床调平 =================\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{else} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 \\n{endif} \\n\\n\\n;===== 画起始线 =================\\nG90\\nG1 Z1.5\\nG0 X85 Y1 Z2 F18000\\nM109 S{nozzle_temperature_initial_layer[initial_extruder]}\\nG1 Z0.2\\nM83\\nG1 X185 E15 F360\\nG1 Z1.5\\n\\nG90\\nM106 S0", + "machine_start_gcode": ";===== date: 20260128 =====================\\n\\nPRINT_START\\nDEFECT_DETECTION_START\\nSET_PRINT_STATS_INFO TOTAL_LAYER={total_layer_count} CURRENT_LAYER=0\\nTIMELAPSE_START\\nM140 S{bed_temperature_initial_layer_single}\\nM104 T{initial_extruder} S140\\nM204 S10000\\n{if chamber_cooling_mode==0}\\n; 保温\\nSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=180\\n{elsif chamber_cooling_mode==2}\\n; 强冷\\nSET_PURIFIER_MODE MODE=1 DESIRE_TEMP=42 ALARM_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=0\\n{else}\\n; 弱冷\\nSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=0 FAN_SPEED=0.6 DELAY_OFF=180\t\t\\n{endif}\\nG28 X Y\\n;===== 床面异物检测 ========\\nT{initial_extruder}\\nG90\\nDEFECT_DETECTION_DETECT_BED\\n;===== 取放头检测 =================\\nSM_PRINT_CHECK_SWITCH_EXTRUDER\\n\\n;===== 自动进料 & 挤出流量 & 预挤出 ======================\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=1 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=0\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=0\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=2 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=1\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=1\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=3 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=2\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=2\\nSM_PRINT_AUTO_FEED EXTRUDER=3\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=3\\nM104 S0 T0 A0\\nM104 S0 T1 A0\\nM104 S0 T2 A0\\nM104 S0 T3 A0\\nM104 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\n\\n;===== 粗回零 =================\\nT{initial_extruder}\\nM106 S255\\nM106 P2 S0\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\nROUGHLY_CLEAN_NOZZLE_WITH_DISCARD\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nG28 Z I140 J140\\n\\n;===== 检测钢板 =================\\nDETECT_BED_PLATE\\n\\n;===== 深度清洁喷嘴 =================\\nG90\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 S{nozzle_temperature[initial_extruder] - 50}\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_1\\nM104 S{nozzle_temperature[initial_extruder] - 90}\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_2\\n\\n;===== 精回零 =================\\nM106 S255\\nM109 S{nozzle_temperature[initial_extruder] - 90}\\nM190 S{bed_temperature_initial_layer_single}\\nM107 P2\\nG90\\nG0 Z5 F10000\\nWAIT_CHAMBER_TEMP TIMEOUT=180\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{else} \\nG28 Z \\n{endif} \\n\\n\\n;===== 热床调平 =================\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{else} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 \\n{endif} \\n\\n\\n;===== 画起始线 =================\\nG90\\nG1 Z1.5\\nG0 X85 Y1 Z2 F18000\\nM109 S{nozzle_temperature_initial_layer[initial_extruder]}\\nG1 Z0.2\\nM83\\nG1 X185 E15 F360\\nG1 Z1.5\\n\\nG90\\nM106 S0", "machine_tool_change_time": "5", "max_layer_height": [ "0.32", diff --git a/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.6 nozzle).json b/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.6 nozzle).json index cfec4db7c79..441e975af86 100644 --- a/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.6 nozzle).json +++ b/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.6 nozzle).json @@ -35,7 +35,7 @@ "0", "0" ], - "machine_end_gcode": " PRINT_END\nTIMELAPSE_STOP", + "machine_end_gcode": "{if print_sequence == \\\"by object\\\"}\\nG91\\nG1 X2 Y2 Z1 F24000\\nG90\\nG1 Z{max_layer_z+2} F600\\n{endif}\\nPRINT_END\\nTIMELAPSE_STOP", "machine_max_jerk_z": [ "3", "0.4" @@ -48,7 +48,7 @@ "20", "12" ], - "machine_start_gcode": ";===== date: 20260128 =====================\\n\\nPRINT_START\\nDEFECT_DETECTION_START\\nSET_PRINT_STATS_INFO TOTAL_LAYER={total_layer_count} CURRENT_LAYER=0\\nTIMELAPSE_START\\nM140 S{bed_temperature_initial_layer_single}\\nM104 T{initial_extruder} S140\\nM204 S10000\\n{if filament_is_high_temperature[0]}\\n; 保温\\n\tSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=180\\n{else}\\n{if temperature_vitrification[0]<=50}\\n; 强冷\\nSET_PURIFIER_MODE MODE=1 DESIRE_TEMP=42 ALARM_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=0\t\t\\n{else}\\n; 弱冷\\nSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=0 FAN_SPEED=0.6 DELAY_OFF=180\t\t\\n{endif}\\n{endif}\\nG28 X Y\\n;===== 床面异物检测 ========\\nT{initial_extruder}\\nG90\\nDEFECT_DETECTION_DETECT_BED\\n;===== 取放头检测 =================\\nSM_PRINT_CHECK_SWITCH_EXTRUDER\\n\\n;===== 自动进料 & 挤出流量 & 预挤出 ======================\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=1 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=0\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=0\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=2 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=1\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=1\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=3 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=2\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=2\\nSM_PRINT_AUTO_FEED EXTRUDER=3\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=3\\nM104 S0 T0 A0\\nM104 S0 T1 A0\\nM104 S0 T2 A0\\nM104 S0 T3 A0\\nM104 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\n\\n;===== 粗回零 =================\\nT{initial_extruder}\\nM106 S255\\nM106 P2 S0\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\nROUGHLY_CLEAN_NOZZLE_WITH_DISCARD\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nG28 Z I140 J140\\n\\n;===== 检测钢板 =================\\nDETECT_BED_PLATE\\n\\n;===== 深度清洁喷嘴 =================\\nG90\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 S{nozzle_temperature[initial_extruder] - 50}\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_1\\nM104 S{nozzle_temperature[initial_extruder] - 90}\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_2\\n\\n;===== 精回零 =================\\nM106 S255\\nM109 S{nozzle_temperature[initial_extruder] - 90}\\nM190 S{bed_temperature_initial_layer_single}\\nM107 P2\\nG90\\nG0 Z5 F10000\\nWAIT_CHAMBER_TEMP TIMEOUT=180\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{else} \\nG28 Z \\n{endif} \\n\\n\\n;===== 热床调平 =================\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{else} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 \\n{endif} \\n\\n\\n;===== 画起始线 =================\\nG90\\nG1 Z1.5\\nG0 X85 Y1 Z2 F18000\\nM109 S{nozzle_temperature_initial_layer[initial_extruder]}\\nG1 Z0.2\\nM83\\nG1 X185 E15 F360\\nG1 Z1.5\\n\\nG90\\nM106 S0", + "machine_start_gcode": ";===== date: 20260128 =====================\\n\\nPRINT_START\\nDEFECT_DETECTION_START\\nSET_PRINT_STATS_INFO TOTAL_LAYER={total_layer_count} CURRENT_LAYER=0\\nTIMELAPSE_START\\nM140 S{bed_temperature_initial_layer_single}\\nM104 T{initial_extruder} S140\\nM204 S10000\\n{if chamber_cooling_mode==0}\\n; 保温\\nSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=180\\n{elsif chamber_cooling_mode==2}\\n; 强冷\\nSET_PURIFIER_MODE MODE=1 DESIRE_TEMP=42 ALARM_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=0\\n{else}\\n; 弱冷\\nSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=0 FAN_SPEED=0.6 DELAY_OFF=180\t\t\\n{endif}\\nG28 X Y\\n;===== 床面异物检测 ========\\nT{initial_extruder}\\nG90\\nDEFECT_DETECTION_DETECT_BED\\n;===== 取放头检测 =================\\nSM_PRINT_CHECK_SWITCH_EXTRUDER\\n\\n;===== 自动进料 & 挤出流量 & 预挤出 ======================\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=1 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=0\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=0\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=2 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=1\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=1\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=3 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=2\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=2\\nSM_PRINT_AUTO_FEED EXTRUDER=3\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=3\\nM104 S0 T0 A0\\nM104 S0 T1 A0\\nM104 S0 T2 A0\\nM104 S0 T3 A0\\nM104 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\n\\n;===== 粗回零 =================\\nT{initial_extruder}\\nM106 S255\\nM106 P2 S0\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\nROUGHLY_CLEAN_NOZZLE_WITH_DISCARD\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nG28 Z I140 J140\\n\\n;===== 检测钢板 =================\\nDETECT_BED_PLATE\\n\\n;===== 深度清洁喷嘴 =================\\nG90\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 S{nozzle_temperature[initial_extruder] - 50}\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_1\\nM104 S{nozzle_temperature[initial_extruder] - 90}\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_2\\n\\n;===== 精回零 =================\\nM106 S255\\nM109 S{nozzle_temperature[initial_extruder] - 90}\\nM190 S{bed_temperature_initial_layer_single}\\nM107 P2\\nG90\\nG0 Z5 F10000\\nWAIT_CHAMBER_TEMP TIMEOUT=180\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{else} \\nG28 Z \\n{endif} \\n\\n\\n;===== 热床调平 =================\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{else} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 \\n{endif} \\n\\n\\n;===== 画起始线 =================\\nG90\\nG1 Z1.5\\nG0 X85 Y1 Z2 F18000\\nM109 S{nozzle_temperature_initial_layer[initial_extruder]}\\nG1 Z0.2\\nM83\\nG1 X185 E15 F360\\nG1 Z1.5\\n\\nG90\\nM106 S0", "machine_tool_change_time": "5", "max_layer_height": [ "0.42", diff --git a/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.8 nozzle).json b/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.8 nozzle).json index 12692f2bd96..1c8255b1505 100644 --- a/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.8 nozzle).json +++ b/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.8 nozzle).json @@ -35,7 +35,7 @@ "0", "0" ], - "machine_end_gcode": " PRINT_END\nTIMELAPSE_STOP", + "machine_end_gcode": "{if print_sequence == \\\"by object\\\"}\\nG91\\nG1 X2 Y2 Z1 F24000\\nG90\\nG1 Z{max_layer_z+2} F600\\n{endif}\\nPRINT_END\\nTIMELAPSE_STOP", "machine_max_jerk_z": [ "3", "0.4" @@ -48,7 +48,7 @@ "20", "12" ], - "machine_start_gcode": ";===== date: 20260128 =====================\\n\\nPRINT_START\\nDEFECT_DETECTION_START\\nSET_PRINT_STATS_INFO TOTAL_LAYER={total_layer_count} CURRENT_LAYER=0\\nTIMELAPSE_START\\nM140 S{bed_temperature_initial_layer_single}\\nM104 T{initial_extruder} S140\\nM204 S10000\\n{if filament_is_high_temperature[0]}\\n; 保温\\n\tSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=180\\n{else}\\n{if temperature_vitrification[0]<=50}\\n; 强冷\\nSET_PURIFIER_MODE MODE=1 DESIRE_TEMP=42 ALARM_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=0\t\t\\n{else}\\n; 弱冷\\nSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=0 FAN_SPEED=0.6 DELAY_OFF=180\t\t\\n{endif}\\n{endif}\\nG28 X Y\\n;===== 床面异物检测 ========\\nT{initial_extruder}\\nG90\\nDEFECT_DETECTION_DETECT_BED\\n;===== 取放头检测 =================\\nSM_PRINT_CHECK_SWITCH_EXTRUDER\\n\\n;===== 自动进料 & 挤出流量 & 预挤出 ======================\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=1 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=0\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=0\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=2 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=1\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=1\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=3 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=2\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=2\\nSM_PRINT_AUTO_FEED EXTRUDER=3\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=3\\nM104 S0 T0 A0\\nM104 S0 T1 A0\\nM104 S0 T2 A0\\nM104 S0 T3 A0\\nM104 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\n\\n;===== 粗回零 =================\\nT{initial_extruder}\\nM106 S255\\nM106 P2 S0\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\nROUGHLY_CLEAN_NOZZLE_WITH_DISCARD\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nG28 Z I140 J140\\n\\n;===== 检测钢板 =================\\nDETECT_BED_PLATE\\n\\n;===== 深度清洁喷嘴 =================\\nG90\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 S{nozzle_temperature[initial_extruder] - 50}\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_1\\nM104 S{nozzle_temperature[initial_extruder] - 90}\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_2\\n\\n;===== 精回零 =================\\nM106 S255\\nM109 S{nozzle_temperature[initial_extruder] - 90}\\nM190 S{bed_temperature_initial_layer_single}\\nM107 P2\\nG90\\nG0 Z5 F10000\\nWAIT_CHAMBER_TEMP TIMEOUT=180\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{else} \\nG28 Z \\n{endif} \\n\\n\\n;===== 热床调平 =================\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{else} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 \\n{endif} \\n\\n\\n;===== 画起始线 =================\\nG90\\nG1 Z1.5\\nG0 X85 Y1 Z2 F18000\\nM109 S{nozzle_temperature_initial_layer[initial_extruder]}\\nG1 Z0.2\\nM83\\nG1 X185 E15 F360\\nG1 Z1.5\\n\\nG90\\nM106 S0", + "machine_start_gcode": ";===== date: 20260128 =====================\\n\\nPRINT_START\\nDEFECT_DETECTION_START\\nSET_PRINT_STATS_INFO TOTAL_LAYER={total_layer_count} CURRENT_LAYER=0\\nTIMELAPSE_START\\nM140 S{bed_temperature_initial_layer_single}\\nM104 T{initial_extruder} S140\\nM204 S10000\\n{if chamber_cooling_mode==0}\\n; 保温\\nSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=180\\n{elsif chamber_cooling_mode==2}\\n; 强冷\\nSET_PURIFIER_MODE MODE=1 DESIRE_TEMP=42 ALARM_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=0\\n{else}\\n; 弱冷\\nSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=0 FAN_SPEED=0.6 DELAY_OFF=180\t\t\\n{endif}\\nG28 X Y\\n;===== 床面异物检测 ========\\nT{initial_extruder}\\nG90\\nDEFECT_DETECTION_DETECT_BED\\n;===== 取放头检测 =================\\nSM_PRINT_CHECK_SWITCH_EXTRUDER\\n\\n;===== 自动进料 & 挤出流量 & 预挤出 ======================\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=1 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=0\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=0\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=2 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=1\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=1\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=3 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=2\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=2\\nSM_PRINT_AUTO_FEED EXTRUDER=3\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=3\\nM104 S0 T0 A0\\nM104 S0 T1 A0\\nM104 S0 T2 A0\\nM104 S0 T3 A0\\nM104 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\n\\n;===== 粗回零 =================\\nT{initial_extruder}\\nM106 S255\\nM106 P2 S0\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\nROUGHLY_CLEAN_NOZZLE_WITH_DISCARD\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nG28 Z I140 J140\\n\\n;===== 检测钢板 =================\\nDETECT_BED_PLATE\\n\\n;===== 深度清洁喷嘴 =================\\nG90\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 S{nozzle_temperature[initial_extruder] - 50}\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_1\\nM104 S{nozzle_temperature[initial_extruder] - 90}\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_2\\n\\n;===== 精回零 =================\\nM106 S255\\nM109 S{nozzle_temperature[initial_extruder] - 90}\\nM190 S{bed_temperature_initial_layer_single}\\nM107 P2\\nG90\\nG0 Z5 F10000\\nWAIT_CHAMBER_TEMP TIMEOUT=180\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{else} \\nG28 Z \\n{endif} \\n\\n\\n;===== 热床调平 =================\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{else} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 \\n{endif} \\n\\n\\n;===== 画起始线 =================\\nG90\\nG1 Z1.5\\nG0 X85 Y1 Z2 F18000\\nM109 S{nozzle_temperature_initial_layer[initial_extruder]}\\nG1 Z0.2\\nM83\\nG1 X185 E15 F360\\nG1 Z1.5\\n\\nG90\\nM106 S0", "machine_tool_change_time": "5", "max_layer_height": [ "0.56", diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index 08c5afe0ebf..946b4d85ec4 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -2698,6 +2698,25 @@ void GCode::_do_export(Print& print, GCodeOutputStream& file, ThumbnailsGenerato this->placeholder_parser().set("scan_first_layer", new ConfigOptionBool(false)); } } + + // Compute chamber cooling mode based on all filaments used on this plate. + // 0 = keep warm (all high-temp), 1 = weak cooling, 2 = strong cooling. + { + int chamber_cooling_mode = 0; + for (unsigned int extruder : tool_ordering.all_extruders()) { + if (!m_config.filament_is_high_temperature.get_at(extruder)) { + int vitrification = m_config.temperature_vitrification.get_at(extruder); + if (vitrification <= 50) { + chamber_cooling_mode = 2; + break; + } else if (vitrification <= 70) { + chamber_cooling_mode = 1; + } + } + } + this->placeholder_parser().set("chamber_cooling_mode", new ConfigOptionInt(chamber_cooling_mode)); + } + std::string machine_start_gcode = this->placeholder_parser_process("machine_start_gcode", print.config().machine_start_gcode.value, initial_extruder_id); if (print.config().gcode_flavor != gcfKlipper) { From e8e8ba5951e1e53be6d56876b8ba1af9ec15fcfe Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Mon, 25 May 2026 16:23:28 +0800 Subject: [PATCH 072/135] feat: add real-time high/low temperature filament mixing detection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the outdated filament_info.json-based classification with per-preset filament_is_high_temperature config. Detect mixing whenever filament assignments change on the current plate (wall, infill, solid infill, support, support interface, wipe tower, and multi-material painting). Detection hooks: - Plater::on_config_change() — config load, extruder count change, import - Sidebar::update_presets() — sidebar filament combo selection Enforcement layers (three-stage guard against notification flicker): 1. check_filament_temp_mixing() shows error notification + marks plate invalid immediately on config change (CallAfter-deferred with serial cancellation to prevent races between the two hooks). 2. Print::validate() runs check_multi_filament_valid() using filament_is_high_temperature (was: filament_info.json + nozzles<2 restriction removed). 3. Plater::priv::update() re-checks after background validate passes, preventing the validate cycle from silently closing the notification. The old Print::validate() check (nozzles<2 && extruders>1) is replaced with a simpler extruders>1 guard that works for multi-nozzle printers too. --- src/libslic3r/Print.cpp | 554 +--- src/slic3r/GUI/Plater.cpp | 5899 ++----------------------------------- 2 files changed, 263 insertions(+), 6190 deletions(-) diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index eab14f0d449..57e74558c90 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -8,7 +8,6 @@ #include "Flow.hpp" #include "Geometry/ConvexHull.hpp" #include "I18N.hpp" -#include "LocalZOrderOptimizer.hpp" #include "ShortestPath.hpp" #include "Thread.hpp" #include "Time.hpp" @@ -54,453 +53,6 @@ template class PrintState; PrintRegion::PrintRegion(const PrintRegionConfig &config) : PrintRegion(config, config.hash()) {} PrintRegion::PrintRegion(PrintRegionConfig &&config) : PrintRegion(std::move(config), config.hash()) {} -namespace { - -constexpr double LOCAL_Z_PERIMETER_MASK_EXPAND_MM = 0.10; - -struct LocalZWipeTowerToolchange -{ - unsigned int old_tool { 0 }; - unsigned int new_tool { 0 }; -}; - -struct LocalZWipeTowerPassRef -{ - size_t layer_to_print_idx { 0 }; - const SubLayerPlan *plan { nullptr }; - std::vector extruders; -}; - -static inline ExPolygons local_z_compensate_masks_for_wipe_tower(const ExPolygons &src_masks, - const float delta_scaled, - const bool fallback_to_source) -{ - if (src_masks.empty() || std::abs(delta_scaled) <= EPSILON) - return src_masks; - - ExPolygons compensated = offset_ex(src_masks, delta_scaled); - if (!compensated.empty() && compensated.size() > 1) - compensated = union_ex(compensated); - - if (compensated.empty() && fallback_to_source) - return src_masks; - return compensated; -} - -static bool local_z_segments_exist(Polylines segments) -{ - for (Polyline &segment : segments) { - if (segment.is_valid()) - return true; - } - return false; -} - -static bool extrusion_collection_has_local_z_segment(const ExtrusionEntityCollection &source, - const ExPolygons &include_masks) -{ - if (source.entities.empty() || include_masks.empty()) - return false; - - ExtrusionEntityCollection flattened = source.flatten(false); - for (const ExtrusionEntity *entity : flattened.entities) { - if (const auto *path = dynamic_cast(entity)) { - if (local_z_segments_exist(intersection_pl(Polylines{path->polyline}, include_masks))) - return true; - } else if (const auto *multipath = dynamic_cast(entity)) { - for (const ExtrusionPath &path : multipath->paths) { - if (local_z_segments_exist(intersection_pl(Polylines{path.polyline}, include_masks))) - return true; - } - } else if (const auto *loop = dynamic_cast(entity)) { - for (const ExtrusionPath &path : loop->paths) { - if (local_z_segments_exist(intersection_pl(Polylines{path.polyline}, include_masks))) - return true; - } - } - } - - return false; -} - -static bool layer_has_local_z_extrusions(const Layer &layer, const ExPolygons &pass_masks, bool include_infill) -{ - if (pass_masks.empty()) - return false; - - for (const LayerRegion *layer_region : layer.regions()) { - for (const ExtrusionEntity *entity : layer_region->perimeters.entities) { - const auto *extrusions = dynamic_cast(entity); - if (extrusions == nullptr) - continue; - if (extrusion_collection_has_local_z_segment(*extrusions, pass_masks)) - return true; - } - if (!include_infill) - continue; - for (const ExtrusionEntity *entity : layer_region->fills.entities) { - const auto *extrusions = dynamic_cast(entity); - if (extrusions == nullptr) - continue; - if (extrusion_collection_has_local_z_segment(*extrusions, pass_masks)) - return true; - } - } - - return false; -} - -static inline int shared_local_z_extruder_for_wipe_tower(const std::vector &lhs, - const std::vector &rhs) -{ - for (unsigned int extruder_id : lhs) { - if (std::find(rhs.begin(), rhs.end(), extruder_id) != rhs.end()) - return static_cast(extruder_id); - } - return -1; -} - -static std::vector rotate_extruders_to_start_with(const std::vector &extruders, - unsigned int start_extruder) -{ - std::vector rotated = extruders; - auto it = std::find(rotated.begin(), rotated.end(), start_extruder); - if (it != rotated.end()) - std::rotate(rotated.begin(), it, rotated.end()); - return rotated; -} - -static std::vector collect_local_z_wipe_tower_toolchanges( - const Print &print, - const std::vector &layers, - int start_extruder) -{ - std::vector pass_refs; - const bool local_z_whole_objects_enabled = print.full_print_config().opt_bool("dithering_local_z_whole_objects"); - const bool local_z_infill_enabled = print.full_print_config().opt_bool("dithering_local_z_infill"); - const float local_z_perimeter_mask_expand = float(scale_(LOCAL_Z_PERIMETER_MASK_EXPAND_MM)); - - for (size_t layer_to_print_idx = 0; layer_to_print_idx < layers.size(); ++layer_to_print_idx) { - const GCode::LayerToPrint &layer_to_print = layers[layer_to_print_idx]; - if (layer_to_print.object_layer == nullptr) - continue; - - const PrintObject *print_object = - layer_to_print.original_object != nullptr ? layer_to_print.original_object : layer_to_print.object(); - if (print_object == nullptr) - continue; - - const size_t layer_id = size_t(layer_to_print.object_layer->id()); - const auto &intervals = print_object->local_z_intervals(); - const auto &plans = print_object->local_z_sublayer_plan(); - const auto interval_it = std::find_if(intervals.begin(), intervals.end(), [layer_id](const LocalZInterval &interval) { - return interval.layer_id == layer_id; - }); - if (interval_it == intervals.end() || !interval_it->has_mixed_paint || interval_it->sublayer_count <= 1 || - interval_it->first_sublayer_idx >= plans.size()) { - continue; - } - - const size_t first_idx = interval_it->first_sublayer_idx; - const size_t end_idx = std::min(plans.size(), first_idx + interval_it->sublayer_count); - for (size_t plan_idx = first_idx; plan_idx < end_idx; ++plan_idx) { - const SubLayerPlan &plan = plans[plan_idx]; - if (!plan.split_interval) - continue; - - const size_t plan_mask_slots = - std::max(plan.painted_masks_by_extruder.size(), plan.fixed_painted_masks_by_extruder.size()); - std::vector compensated_masks_by_extruder(plan_mask_slots, ExPolygons()); - - ExPolygons fixed_raw_masks_union; - for (const ExPolygons &fixed_masks : plan.fixed_painted_masks_by_extruder) { - if (!fixed_masks.empty()) - append(fixed_raw_masks_union, fixed_masks); - } - if (!fixed_raw_masks_union.empty() && fixed_raw_masks_union.size() > 1) - fixed_raw_masks_union = union_ex(fixed_raw_masks_union); - - const ExPolygons fixed_compensated_guard = - fixed_raw_masks_union.empty() ? - ExPolygons() : - local_z_compensate_masks_for_wipe_tower(fixed_raw_masks_union, local_z_perimeter_mask_expand, true); - - for (size_t extruder_id = 0; extruder_id < plan_mask_slots; ++extruder_id) { - const ExPolygons mixed_raw_masks = - extruder_id < plan.painted_masks_by_extruder.size() ? plan.painted_masks_by_extruder[extruder_id] : ExPolygons(); - const ExPolygons fixed_raw_masks = - extruder_id < plan.fixed_painted_masks_by_extruder.size() ? plan.fixed_painted_masks_by_extruder[extruder_id] : - ExPolygons(); - if (mixed_raw_masks.empty() && fixed_raw_masks.empty()) - continue; - - ExPolygons compensated; - if (!mixed_raw_masks.empty()) { - ExPolygons compensated_mixed = - local_z_compensate_masks_for_wipe_tower(mixed_raw_masks, local_z_perimeter_mask_expand, true); - if (local_z_whole_objects_enabled && !fixed_compensated_guard.empty()) - compensated_mixed = diff_ex(compensated_mixed, fixed_compensated_guard); - if (!compensated_mixed.empty()) - append(compensated, compensated_mixed); - } - if (!fixed_raw_masks.empty()) - append(compensated, fixed_raw_masks); - if (!compensated.empty() && compensated.size() > 1) - compensated = union_ex(compensated); - compensated_masks_by_extruder[extruder_id] = std::move(compensated); - } - - LocalZWipeTowerPassRef pass_ref; - pass_ref.layer_to_print_idx = layer_to_print_idx; - pass_ref.plan = &plan; - for (size_t extruder_id = 0; extruder_id < plan.painted_masks_by_extruder.size(); ++extruder_id) { - if (extruder_id >= compensated_masks_by_extruder.size()) - continue; - const ExPolygons &pass_masks = compensated_masks_by_extruder[extruder_id]; - if (pass_masks.empty()) - continue; - if (layer_has_local_z_extrusions(*layer_to_print.object_layer, pass_masks, local_z_infill_enabled)) - pass_ref.extruders.push_back(unsigned(extruder_id)); - } - - if (!pass_ref.extruders.empty()) - pass_refs.emplace_back(std::move(pass_ref)); - } - } - - std::sort(pass_refs.begin(), pass_refs.end(), [](const LocalZWipeTowerPassRef &lhs, const LocalZWipeTowerPassRef &rhs) { - assert(lhs.plan != nullptr && rhs.plan != nullptr); - if (lhs.plan->print_z != rhs.plan->print_z) - return lhs.plan->print_z < rhs.plan->print_z; - if (lhs.layer_to_print_idx != rhs.layer_to_print_idx) - return lhs.layer_to_print_idx < rhs.layer_to_print_idx; - return lhs.plan->pass_index < rhs.plan->pass_index; - }); - - auto collect_toolchanges_legacy = [&](int start_tool) { - std::vector legacy_toolchanges; - int active_extruder = start_tool; - size_t pass_ref_idx = 0; - while (pass_ref_idx < pass_refs.size()) { - size_t pass_group_end = pass_ref_idx + 1; - while (pass_group_end < pass_refs.size() && - std::abs(pass_refs[pass_ref_idx].plan->print_z - pass_refs[pass_group_end].plan->print_z) <= EPSILON) { - ++pass_group_end; - } - - std::vector pass_group_extruders; - for (size_t group_idx = pass_ref_idx; group_idx < pass_group_end; ++group_idx) - for (unsigned int extruder_id : pass_refs[group_idx].extruders) - if (std::find(pass_group_extruders.begin(), pass_group_extruders.end(), extruder_id) == pass_group_extruders.end()) - pass_group_extruders.push_back(extruder_id); - - std::vector next_group_extruders; - if (pass_group_end < pass_refs.size()) { - size_t next_group_end = pass_group_end + 1; - while (next_group_end < pass_refs.size() && - std::abs(pass_refs[pass_group_end].plan->print_z - pass_refs[next_group_end].plan->print_z) <= EPSILON) { - ++next_group_end; - } - for (size_t group_idx = pass_group_end; group_idx < next_group_end; ++group_idx) - for (unsigned int extruder_id : pass_refs[group_idx].extruders) - if (std::find(next_group_extruders.begin(), next_group_extruders.end(), extruder_id) == next_group_extruders.end()) - next_group_extruders.push_back(extruder_id); - } - - const int preferred_last_extruder = - shared_local_z_extruder_for_wipe_tower(pass_group_extruders, next_group_extruders); - const std::vector ordered_group_extruders = - LocalZOrderOptimizer::order_bucket_extruders(pass_group_extruders, active_extruder, preferred_last_extruder); - - for (unsigned int extruder_id : ordered_group_extruders) { - if (active_extruder >= 0 && active_extruder != int(extruder_id)) - legacy_toolchanges.push_back(LocalZWipeTowerToolchange{unsigned(active_extruder), extruder_id}); - active_extruder = int(extruder_id); - } - - pass_ref_idx = pass_group_end; - } - - return legacy_toolchanges; - }; - - const bool dependency_chain_mode = - !pass_refs.empty() && - std::all_of(pass_refs.begin(), pass_refs.end(), [](const LocalZWipeTowerPassRef &pass_ref) { - return pass_ref.plan != nullptr && pass_ref.plan->dependency_group != 0; - }); - if (!dependency_chain_mode) - return collect_toolchanges_legacy(start_extruder); - - struct ChainKey { - size_t layer_to_print_idx { 0 }; - size_t dependency_group { 0 }; - - bool operator<(const ChainKey &rhs) const - { - if (layer_to_print_idx != rhs.layer_to_print_idx) - return layer_to_print_idx < rhs.layer_to_print_idx; - return dependency_group < rhs.dependency_group; - } - }; - struct PassState { - const LocalZWipeTowerPassRef *pass_ref { nullptr }; - std::vector remaining_extruders; - size_t chain_idx { 0 }; - size_t chain_pos { 0 }; - bool ready { false }; - bool completed { false }; - }; - - std::map chain_index_by_key; - std::vector> chains; - std::vector pass_states; - pass_states.reserve(pass_refs.size()); - for (const LocalZWipeTowerPassRef &pass_ref : pass_refs) { - ChainKey chain_key { pass_ref.layer_to_print_idx, pass_ref.plan->dependency_group }; - auto [it_chain, inserted] = chain_index_by_key.emplace(chain_key, chains.size()); - if (inserted) - chains.emplace_back(); - - const size_t chain_idx = it_chain->second; - const size_t pass_state_idx = pass_states.size(); - pass_states.push_back(PassState{ &pass_ref, pass_ref.extruders, chain_idx, 0, false, false }); - chains[chain_idx].push_back(pass_state_idx); - } - - for (std::vector &chain : chains) { - std::sort(chain.begin(), chain.end(), [&pass_states](size_t lhs_idx, size_t rhs_idx) { - const SubLayerPlan &lhs = *pass_states[lhs_idx].pass_ref->plan; - const SubLayerPlan &rhs = *pass_states[rhs_idx].pass_ref->plan; - if (lhs.dependency_order != rhs.dependency_order) - return lhs.dependency_order < rhs.dependency_order; - if (std::abs(lhs.print_z - rhs.print_z) > EPSILON) - return lhs.print_z < rhs.print_z; - return lhs.pass_index < rhs.pass_index; - }); - for (size_t chain_pos = 0; chain_pos < chain.size(); ++chain_pos) - pass_states[chain[chain_pos]].chain_pos = chain_pos; - if (!chain.empty()) - pass_states[chain.front()].ready = true; - } - - auto pass_contains_extruder = [](const PassState &pass_state, unsigned int extruder_id) { - return std::find(pass_state.remaining_extruders.begin(), pass_state.remaining_extruders.end(), extruder_id) != - pass_state.remaining_extruders.end(); - }; - - auto choose_ready_extruder = [&](int active_extruder) -> int { - std::vector ready_extruders; - for (const PassState &pass_state : pass_states) { - if (!pass_state.ready || pass_state.completed) - continue; - for (unsigned int extruder_id : pass_state.remaining_extruders) - if (std::find(ready_extruders.begin(), ready_extruders.end(), extruder_id) == ready_extruders.end()) - ready_extruders.push_back(extruder_id); - } - if (ready_extruders.empty()) - return -1; - if (active_extruder >= 0 && - std::find(ready_extruders.begin(), ready_extruders.end(), unsigned(active_extruder)) != ready_extruders.end()) { - return active_extruder; - } - - int best_extruder = -1; - size_t best_ready_count = 0; - size_t best_future_count = 0; - for (unsigned int extruder_id : ready_extruders) { - size_t ready_count = 0; - size_t future_count = 0; - for (const PassState &pass_state : pass_states) { - if (pass_state.completed || !pass_contains_extruder(pass_state, extruder_id)) - continue; - ++future_count; - if (pass_state.ready) - ++ready_count; - } - - if (best_extruder < 0 || - ready_count > best_ready_count || - (ready_count == best_ready_count && future_count > best_future_count) || - (ready_count == best_ready_count && future_count == best_future_count && extruder_id < unsigned(best_extruder))) { - best_extruder = int(extruder_id); - best_ready_count = ready_count; - best_future_count = future_count; - } - } - return best_extruder; - }; - - std::vector toolchanges; - int active_extruder = start_extruder; - size_t completed_passes = 0; - while (completed_passes < pass_states.size()) { - const int chosen_extruder = choose_ready_extruder(active_extruder); - if (chosen_extruder < 0) { - BOOST_LOG_TRIVIAL(warning) << "Local-Z wipe tower dependency scheduler deadlocked, falling back" - << " start_extruder=" << start_extruder - << " pass_count=" << pass_refs.size(); - return collect_toolchanges_legacy(start_extruder); - } - - if (active_extruder >= 0 && active_extruder != chosen_extruder) - toolchanges.push_back(LocalZWipeTowerToolchange{unsigned(active_extruder), unsigned(chosen_extruder)}); - active_extruder = chosen_extruder; - - bool completed_any = false; - std::vector newly_completed; - for (size_t pass_state_idx = 0; pass_state_idx < pass_states.size(); ++pass_state_idx) { - PassState &pass_state = pass_states[pass_state_idx]; - if (!pass_state.ready || pass_state.completed) - continue; - - auto it_extruder = std::find(pass_state.remaining_extruders.begin(), - pass_state.remaining_extruders.end(), - unsigned(chosen_extruder)); - if (it_extruder == pass_state.remaining_extruders.end()) - continue; - - pass_state.remaining_extruders.erase(it_extruder); - completed_any = true; - if (pass_state.remaining_extruders.empty()) - newly_completed.push_back(pass_state_idx); - } - - if (!completed_any) { - BOOST_LOG_TRIVIAL(warning) << "Local-Z wipe tower dependency scheduler made no progress, falling back" - << " start_extruder=" << start_extruder - << " active_extruder=" << active_extruder - << " chosen_extruder=" << chosen_extruder - << " pass_count=" << pass_refs.size(); - return collect_toolchanges_legacy(start_extruder); - } - - for (size_t pass_state_idx : newly_completed) { - PassState &pass_state = pass_states[pass_state_idx]; - if (pass_state.completed) - continue; - - pass_state.ready = false; - pass_state.completed = true; - ++completed_passes; - - const std::vector &chain = chains[pass_state.chain_idx]; - const size_t next_chain_pos = pass_state.chain_pos + 1; - if (next_chain_pos < chain.size()) - pass_states[chain[next_chain_pos]].ready = true; - } - } - - BOOST_LOG_TRIVIAL(info) << "Local-Z wipe tower dependency scheduler" - << " start_extruder=" << start_extruder - << " pass_count=" << pass_refs.size() - << " chain_count=" << chains.size() - << " toolchanges=" << toolchanges.size(); - return toolchanges; -} - -} // namespace - //BBS // ORCA: Now this is a parameter //float Print::min_skirt_length = 0; @@ -695,19 +247,6 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n || opt_key == "filament_shrinkage_compensation_z" || opt_key == "resolution" || opt_key == "precise_z_height" - || opt_key == "dithering_z_step_size" - || opt_key == "dithering_local_z_mode" - || opt_key == "dithering_local_z_whole_objects" - || opt_key == "dithering_local_z_direct_multicolor" - || opt_key == "dithering_step_painted_zones_only" - || opt_key == "mixed_filament_gradient_mode" - || opt_key == "mixed_filament_height_lower_bound" - || opt_key == "mixed_filament_height_upper_bound" - || opt_key == "mixed_filament_advanced_dithering" - || opt_key == "mixed_filament_component_bias_enabled" - || opt_key == "mixed_filament_surface_indentation" - || opt_key == "mixed_filament_region_collapse" - || opt_key == "mixed_filament_definitions" // Spiral Vase forces different kind of slicing than the normal model: // In Spiral Vase mode, holes are closed and only the largest area contour is kept at each layer. // Therefore toggling the Spiral Vase on / off requires complete reslicing. @@ -779,7 +318,6 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n || opt_key == "wipe_tower_wall_gap" || opt_key == "wipe_tower_filament" || opt_key == "wiping_volumes_extruders" - || opt_key == "dithering_local_z_infill" || opt_key == "enable_filament_ramming" || opt_key == "purge_in_prime_tower" || opt_key == "z_offset" @@ -942,11 +480,10 @@ std::vector Print::extruders(bool conside_custom_gcode) const if (conside_custom_gcode) { //BBS - const size_t num_physical = m_config.filament_colour.size(); - const size_t num_filaments = m_mixed_filament_mgr.total_filaments(num_physical); + int num_extruders = m_config.filament_colour.size(); if (m_model.plates_custom_gcodes.find(m_model.curr_plate_index) != m_model.plates_custom_gcodes.end()) { for (auto item : m_model.plates_custom_gcodes.at(m_model.curr_plate_index).gcodes) { - if (item.type == CustomGCode::Type::ToolChange && item.extruder <= int(num_filaments)) + if (item.type == CustomGCode::Type::ToolChange && item.extruder <= num_extruders) extruders.push_back((unsigned int)(item.extruder - 1)); } } @@ -1529,13 +1066,17 @@ StringObjectException Print::check_multi_filament_valid(const Print& print) { auto print_config = print.config(); std::vector extruders = print.extruders(); - std::vector filament_types; - filament_types.reserve(extruders.size()); - for (const auto& extruder_idx : extruders) - filament_types.push_back(print_config.filament_type.get_at(extruder_idx)); - - if (!check_multi_filaments_compatibility(filament_types)) + bool has_high = false, has_low = false; + for (const auto& extruder_idx : extruders) { + if (extruder_idx < print_config.filament_is_high_temperature.size()) { + if (print_config.filament_is_high_temperature.get_at(extruder_idx)) + has_high = true; + else + has_low = true; + } + } + if (has_high && has_low) return {L("Cannot print multiple filaments which have large difference of temperature together. Otherwise, the extruder and nozzle may be blocked or damaged during printing.")}; return {std::string()}; @@ -1550,7 +1091,6 @@ boost::regex regex_g92e0 { "^[ \\t]*[gG]92[ \\t]*[eE](0(\\.0*)?|\\.0+)[ \\t]*(;. StringObjectException Print::validate(StringObjectException *warning, Polygons* collison_polygons, std::vector>* height_polygons) const { std::vector extruders = this->extruders(); - unsigned int nozzles = m_config.nozzle_diameter.size(); if (m_objects.empty()) return {std::string()}; @@ -1558,10 +1098,13 @@ StringObjectException Print::validate(StringObjectException *warning, Polygons* if (extruders.empty()) return { L("No extrusions under current settings.") }; - if (nozzles < 2 && extruders.size() > 1 && m_config.print_sequence != PrintSequence::ByObject) { + // High/low temperature filament mixing check — enforced at both the + // real-time UI level (Plater::check_filament_temp_mixing) and here in + // validate() so that the background process does not clear the error + // notification (see Plater::priv::update line ~6507). + if (extruders.size() > 1) { auto ret = check_multi_filament_valid(*this); - if (!ret.string.empty()) - { + if (!ret.string.empty()) { ret.type = STRING_EXCEPT_FILAMENTS_DIFFERENT_TEMP; return ret; } @@ -1617,7 +1160,7 @@ StringObjectException Print::validate(StringObjectException *warning, Polygons* layer_height_profiles.assign(m_objects.size(), std::vector()); std::vector &profile = layer_height_profiles[print_object_idx]; if (profile.empty()) - PrintObject::update_layer_height_profile(*print_object.model_object(), print_object.slicing_parameters(), profile, &print_object); + PrintObject::update_layer_height_profile(*print_object.model_object(), print_object.slicing_parameters(), profile); return profile; }; @@ -3311,8 +2854,6 @@ void Print::_make_wipe_tower() // Initialize the wipe tower. WipeTower2 wipe_tower(m_config, m_default_region_config, m_plate_index, m_origin, wipe_volumes, m_wipe_tower_data.tool_ordering.first_extruder()); - const std::vector>> layers_to_print = GCode::collect_layers_to_print(*this); - size_t layers_to_print_idx = 0; // wipe_tower.set_retract(); // wipe_tower.set_zhop(); @@ -3331,52 +2872,10 @@ void Print::_make_wipe_tower() for (auto &layer_tools : m_wipe_tower_data.tool_ordering.layer_tools()) { // for all layers if (!layer_tools.has_wipe_tower) continue; - while (layers_to_print_idx + 1 < layers_to_print.size() && - layers_to_print[layers_to_print_idx].first + EPSILON < layer_tools.print_z) { - ++layers_to_print_idx; - } - - const std::vector *layers_with_same_print_z = nullptr; - if (layers_to_print_idx < layers_to_print.size() && - std::abs(layers_to_print[layers_to_print_idx].first - layer_tools.print_z) <= EPSILON) { - layers_with_same_print_z = &layers_to_print[layers_to_print_idx].second; - } - bool first_layer = &layer_tools == &m_wipe_tower_data.tool_ordering.front(); - - if (layers_with_same_print_z != nullptr) { - const std::vector local_z_toolchanges = - collect_local_z_wipe_tower_toolchanges(*this, *layers_with_same_print_z, int(current_extruder_id)); - if (!local_z_toolchanges.empty()) { - std::ostringstream local_z_sequence; - for (size_t toolchange_idx = 0; toolchange_idx < local_z_toolchanges.size(); ++toolchange_idx) { - if (toolchange_idx != 0) - local_z_sequence << ","; - local_z_sequence << local_z_toolchanges[toolchange_idx].old_tool << "->" - << local_z_toolchanges[toolchange_idx].new_tool; - } - - BOOST_LOG_TRIVIAL(debug) << "Local-Z wipe tower preplan" - << " print_z=" << layer_tools.print_z - << " start_tool=" << current_extruder_id - << " nominal_toolchanges=" << layer_tools.extruders.size() - << " local_z_toolchanges=" << local_z_toolchanges.size() - << " sequence=" << local_z_sequence.str(); - } - for (const LocalZWipeTowerToolchange &toolchange : local_z_toolchanges) { - wipe_tower.plan_local_z_toolchange((float) layer_tools.print_z, (float) layer_tools.wipe_tower_layer_height, - toolchange.old_tool, toolchange.new_tool, (float) m_config.prime_volume); - } - if (!local_z_toolchanges.empty()) - current_extruder_id = local_z_toolchanges.back().new_tool; - } - - const std::vector nominal_layer_extruders = - rotate_extruders_to_start_with(layer_tools.extruders, current_extruder_id); - wipe_tower.plan_toolchange((float) layer_tools.print_z, (float) layer_tools.wipe_tower_layer_height, current_extruder_id, current_extruder_id, false); - for (const auto extruder_id : nominal_layer_extruders) { + for (const auto extruder_id : layer_tools.extruders) { if ((first_layer && extruder_id == m_wipe_tower_data.tool_ordering.all_extruders().back()) || extruder_id != current_extruder_id) { float volume_to_wipe = m_config.prime_volume; @@ -3400,7 +2899,6 @@ void Print::_make_wipe_tower() current_extruder_id = extruder_id; } } - layer_tools.wiping_extrusions().ensure_perimeters_infills_order(*this); if (&layer_tools == &m_wipe_tower_data.tool_ordering.back() || (&layer_tools + 1)->wipe_tower_partitions == 0) break; @@ -3409,14 +2907,9 @@ void Print::_make_wipe_tower() // Generate the wipe tower layers. m_wipe_tower_data.tool_changes.reserve(m_wipe_tower_data.tool_ordering.layer_tools().size()); - m_wipe_tower_data.local_z_tool_changes.reserve(m_wipe_tower_data.tool_ordering.layer_tools().size()); - wipe_tower.generate(m_wipe_tower_data.tool_changes, m_wipe_tower_data.local_z_tool_changes); - BOOST_LOG_TRIVIAL(debug) << "Wipe tower generation completed" - << " nominal_layers=" << m_wipe_tower_data.tool_changes.size() - << " local_z_layers=" << m_wipe_tower_data.local_z_tool_changes.size(); + wipe_tower.generate(m_wipe_tower_data.tool_changes); m_wipe_tower_data.depth = wipe_tower.get_depth(); m_wipe_tower_data.z_and_depth_pairs = wipe_tower.get_z_and_depth_pairs(); - m_wipe_tower_data.local_z_reserve_boxes = wipe_tower.get_local_z_reserve_boxes(); m_wipe_tower_data.brim_width = wipe_tower.get_brim_width(); m_wipe_tower_data.height = wipe_tower.get_wipe_tower_height(); @@ -4882,10 +4375,7 @@ BoundingBoxf3 PrintInstance::get_bounding_box() { Polygon PrintInstance::get_convex_hull_2d() { Polygon poly = print_object->model_object()->convex_hull_2d(model_instance->get_matrix()); - // Change the distance threshold of the Douglas-Peucker algorithm to 0.5 millimeter and reduce the number of points - poly.douglas_peucker(scale_(0.5)); - // Round coordinates to 0.1mm grid to limit decimal places - poly.round_to_grid(scale_(0.1)); + poly.douglas_peucker(0.1); return poly; } diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index b80ab11e5ef..2579136b9aa 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -1,32 +1,15 @@ #include "Plater.hpp" -#include "MixedFilamentDialog.hpp" -#include "MixedGradientSelector.hpp" -#include "MixedColorMatchPanel.hpp" -#include "MixedFilamentBadge.hpp" -#include "MixedFilamentColorMapPanel.hpp" #include "libslic3r/Config.hpp" -#include "libslic3r/MixedFilament.hpp" -#include "libslic3r/filament_mixer.h" #include "common_func/common_func.hpp" #include -#include -#include -#include #include -#include #include -#include -#include -#include #include #include #include -#include #include #include -#include -#include #include #include #include @@ -52,23 +35,14 @@ #include #include #include -#include -#include #include #include -#include -#include -#include -#include -#include #ifdef _WIN32 #include #include #include #endif #include -#include -#include #include #include @@ -132,7 +106,6 @@ #include "ModelMall.hpp" #include "ConfigWizard.hpp" #include "../Utils/ASCIIFolding.hpp" -#include "../Utils/ColorSpaceConvert.hpp" #include "../Utils/FixModelByWin10.hpp" #include "../Utils/UndoRedo.hpp" #include "../Utils/PresetUpdater.hpp" @@ -235,7 +208,6 @@ wxDEFINE_EVENT(EVT_ADD_FILAMENT, SimpleEvent); wxDEFINE_EVENT(EVT_DEL_FILAMENT, SimpleEvent); wxDEFINE_EVENT(EVT_ADD_CUSTOM_FILAMENT, ColorEvent); - #define PRINTER_THUMBNAIL_SIZE (wxSize(FromDIP(48), FromDIP(48))) #define PRINTER_THUMBNAIL_SIZE_SMALL (wxSize(FromDIP(32), FromDIP(32))) #define PRINTER_PANEL_SIZE_SMALL (wxSize(FromDIP(98), FromDIP(68))) @@ -720,7 +692,6 @@ struct Sidebar::priv wxComboBox* m_comboBox_print_preset; wxStaticLine* m_staticline1; StaticBox* m_panel_filament_title; - ScalableButton* m_filament_config_icon = nullptr; wxStaticText* m_staticText_filament_settings; ScalableButton * m_bpButton_add_filament; ScalableButton * m_bpButton_del_filament; @@ -729,40 +700,6 @@ struct Sidebar::priv int m_menu_filament_id = -1; wxPanel* m_panel_filament_content; wxScrolledWindow* m_scrolledWindow_filament_content; - - // Mixed (virtual) filaments panel - collapsible like Printer/Filament sections - StaticBox* m_panel_mixed_filaments_title = nullptr; // Collapsible title bar - wxPanel* m_panel_mixed_filaments_content = nullptr; // Content panel - wxBoxSizer* m_sizer_mixed_filaments_content = nullptr; // Content sizer - ScalableButton* m_mixed_filaments_icon = nullptr; // Icon - wxStaticText* m_staticText_mixed_filaments = nullptr; // Title text - Button* m_btn_add_gradient = nullptr; // Add gradient button - Button* m_btn_add_pattern = nullptr; // Add pattern button - Button* m_btn_add_color = nullptr; // Add color-match button - Button* m_btn_toggle_mixed_filaments = nullptr; // Collapse/expand toggle button - bool m_mixed_filaments_collapsed = false; // Collapse state - bool m_skip_mixed_filament_sync_once = false; // Local edits already mutated manager in place. - std::unordered_set m_expanded_mixed_filament_rows; // Expanded row editors - struct MixedFilamentRowBinding { - size_t mixed_id = size_t(-1); - wxWindow *row = nullptr; - }; - std::vector m_mixed_filament_row_bindings; - std::vector m_mixed_filament_ui_order; - bool m_mixed_filament_drag_active = false; - size_t m_mixed_filament_drag_source_mixed_id = size_t(-1); - // Physical filament scrolled window - wxScrolledWindow* m_scrolled_filaments = nullptr; - wxPanel* m_panel_scrolled_filament_content = nullptr; - // Color mix panel - StaticBox* m_panel_physical_filaments_title = nullptr; - StaticBox* m_panel_color_mix_title = nullptr; - wxPanel* m_panel_color_mix_content = nullptr; - wxScrolledWindow* m_scrolled_color_mix = nullptr; - ScalableButton* m_color_mix_icon = nullptr; - ScalableButton* m_btn_add_color_mix = nullptr; - ScalableButton* m_btn_del_color_mix = nullptr; - wxStaticLine* m_staticline2; wxPanel* m_panel_project_title; ScalableButton* m_filament_icon = nullptr; @@ -962,7 +899,6 @@ std::vector get_min_flush_volumes(const DynamicPrintConfig& full_config) return extra_flush_volumes; } - // Sidebar / public struct DynamicFilamentList : DynamicList @@ -1060,528 +996,6 @@ struct DynamicFilamentList1Based : DynamicFilamentList }; -class MixedFilamentColorMatchDialog : public DPIDialog -{ -public: - MixedFilamentColorMatchDialog(wxWindow* parent, const std::vector& physical_colors, const wxColour& initial_color) - : DPIDialog(parent ? parent : static_cast(wxGetApp().mainframe), - wxID_ANY, - _L("Add Color"), - wxDefaultPosition, - wxDefaultSize, - wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER) - , m_physical_colors(physical_colors) - { - m_recipe_timer.SetOwner(this); - m_loading_timer.SetOwner(this); - m_display_context = build_mixed_filament_display_context(m_physical_colors); - - m_palette.reserve(m_physical_colors.size()); - for (const std::string& hex : m_physical_colors) - m_palette.emplace_back(parse_mixed_color(hex)); - - const wxColour safe_initial = initial_color.IsOk() ? - initial_color : - (m_palette.size() >= 2 ? blend_pair_filament_mixer(m_palette[0], m_palette[1], 0.5f) : - wxColour("#26A69A")); - std::vector initial_weights(m_palette.size(), 0); - if (!initial_weights.empty()) - initial_weights[0] = 100; - if (initial_weights.size() >= 2) { - initial_weights[0] = 50; - initial_weights[1] = 50; - } - - std::vector filament_ids; - filament_ids.reserve(m_palette.size()); - for (size_t idx = 0; idx < m_palette.size(); ++idx) - filament_ids.emplace_back(unsigned(idx + 1)); - - SetMinSize(wxSize(FromDIP(430), FromDIP(520))); - - auto* root = new wxBoxSizer(wxVERTICAL); - auto* description = new wxStaticText(this, wxID_ANY, - _L("Pick from the current filament gamut. The dialog previews the closest 2-color, 3-color, " - "or 4-color FilamentMixer recipe before it is added.")); - description->Wrap(FromDIP(390)); - root->Add(description, 0, wxEXPAND | wxALL, FromDIP(12)); - - m_color_map = new MixedFilamentColorMapPanel(this, filament_ids, m_palette, initial_weights, wxSize(FromDIP(260), FromDIP(260))); - root->Add(m_color_map, 1, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(12)); - - auto* hex_row = new wxBoxSizer(wxHORIZONTAL); - hex_row->Add(new wxStaticText(this, wxID_ANY, _L("Hex")), 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(8)); - m_hex_input = new wxTextCtrl(this, wxID_ANY, normalize_color_match_hex(safe_initial.GetAsString(wxC2S_HTML_SYNTAX)), - wxDefaultPosition, wxDefaultSize, wxTE_PROCESS_ENTER); - m_hex_input->SetToolTip(_L("Enter a hex color like #00FF88. The picker will snap to the closest supported FilamentMixer color.")); - hex_row->Add(m_hex_input, 1, wxALIGN_CENTER_VERTICAL); - hex_row->AddSpacer(FromDIP(8)); - m_classic_picker = new wxColourPickerCtrl(this, wxID_ANY, safe_initial); - m_classic_picker->SetToolTip(_L("Classic color picker. The result will snap to the closest supported FilamentMixer color.")); - hex_row->Add(m_classic_picker, 0, wxALIGN_CENTER_VERTICAL); - root->Add(hex_row, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, FromDIP(12)); - - auto* range_row = new wxBoxSizer(wxHORIZONTAL); - range_row->Add(new wxStaticText(this, wxID_ANY, _L("Range")), 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(8)); - m_range_slider = new wxSlider(this, wxID_ANY, m_min_component_percent, 0, 50); - m_range_slider->SetToolTip(_L("Minimum percent for each participating color. Higher values block highly skewed mixes.")); - range_row->Add(m_range_slider, 1, wxALIGN_CENTER_VERTICAL); - range_row->AddSpacer(FromDIP(8)); - m_range_value = new wxStaticText(this, wxID_ANY, wxEmptyString); - range_row->Add(m_range_value, 0, wxALIGN_CENTER_VERTICAL); - root->Add(range_row, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, FromDIP(12)); - - auto* summary_grid = new wxFlexGridSizer(2, FromDIP(8), FromDIP(8)); - summary_grid->AddGrowableCol(1, 1); - - summary_grid->Add(new wxStaticText(this, wxID_ANY, _L("Requested")), 0, wxALIGN_CENTER_VERTICAL); - auto* selected_row = new wxBoxSizer(wxHORIZONTAL); - m_selected_preview = new wxPanel(this, wxID_ANY, wxDefaultPosition, wxSize(FromDIP(72), FromDIP(24)), wxBORDER_SIMPLE); - selected_row->Add(m_selected_preview, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(8)); - m_selected_label = new wxStaticText(this, wxID_ANY, wxEmptyString); - selected_row->Add(m_selected_label, 1, wxALIGN_CENTER_VERTICAL); - summary_grid->Add(selected_row, 1, wxEXPAND); - - summary_grid->Add(new wxStaticText(this, wxID_ANY, _L("Creates")), 0, wxALIGN_CENTER_VERTICAL); - auto* recipe_row = new wxBoxSizer(wxHORIZONTAL); - m_recipe_preview = new wxPanel(this, wxID_ANY, wxDefaultPosition, wxSize(FromDIP(72), FromDIP(24)), wxBORDER_SIMPLE); - recipe_row->Add(m_recipe_preview, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(8)); - m_recipe_label = new wxStaticText(this, wxID_ANY, wxEmptyString); - m_recipe_label->Wrap(FromDIP(280)); - recipe_row->Add(m_recipe_label, 1, wxALIGN_CENTER_VERTICAL); - summary_grid->Add(recipe_row, 1, wxEXPAND); - - root->Add(summary_grid, 0, wxEXPAND | wxALL, FromDIP(12)); - - m_delta_label = new wxStaticText(this, wxID_ANY, wxEmptyString); - root->Add(m_delta_label, 0, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(12)); - - m_presets_label = new wxStaticText(this, wxID_ANY, _L("Exact preset mixes")); - root->Add(m_presets_label, 0, wxLEFT | wxRIGHT | wxTOP, FromDIP(12)); - m_presets_host = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxSize(-1, FromDIP(96)), wxVSCROLL | wxBORDER_SIMPLE); - m_presets_host->SetScrollRate(FromDIP(6), FromDIP(6)); - m_presets_sizer = new wxWrapSizer(wxHORIZONTAL, wxWRAPSIZER_DEFAULT_FLAGS); - m_presets_host->SetSizer(m_presets_sizer); - root->Add(m_presets_host, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, FromDIP(12)); - - m_error_label = new wxStaticText(this, wxID_ANY, wxEmptyString); - m_error_label->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#D32F2F"))); - root->Add(m_error_label, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, FromDIP(12)); - - if (wxSizer* button_sizer = CreateStdDialogButtonSizer(wxOK | wxCANCEL)) - root->Add(button_sizer, 0, wxEXPAND | wxALL, FromDIP(12)); - - m_loading_panel = new wxPanel(this, wxID_ANY); - m_loading_panel->SetMinSize(wxSize(-1, FromDIP(24))); - auto* loading_row = new wxBoxSizer(wxHORIZONTAL); - m_loading_label = new wxStaticText(m_loading_panel, wxID_ANY, " "); - loading_row->Add(m_loading_label, 1, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(8)); - m_loading_gauge = new wxGauge(m_loading_panel, wxID_ANY, 100, wxDefaultPosition, wxSize(FromDIP(150), FromDIP(8)), - wxGA_HORIZONTAL | wxGA_SMOOTH); - m_loading_gauge->SetValue(0); - m_loading_gauge->Enable(false); - loading_row->Add(m_loading_gauge, 0, wxALIGN_CENTER_VERTICAL); - m_loading_panel->SetSizer(loading_row); - root->Add(m_loading_panel, 0, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, FromDIP(12)); - - SetSizerAndFit(root); - - m_selected_target = safe_initial; - m_requested_target = safe_initial; - if (m_color_map) - m_color_map->set_min_component_percent(m_min_component_percent); - update_range_label(); - rebuild_presets_ui(); - sync_inputs_to_requested(); - update_dialog_state(); - - if (m_color_map) { - m_color_map->Bind(wxEVT_SLIDER, [this](wxCommandEvent&) { - if (!m_color_map) - return; - request_recipe_match(m_color_map->selected_color(), true, _L("Matching closest supported mix...")); - }); - } - - if (m_hex_input) { - m_hex_input->Bind(wxEVT_TEXT_ENTER, [this](wxCommandEvent&) { apply_hex_input(true); }); - m_hex_input->Bind(wxEVT_KILL_FOCUS, [this](wxFocusEvent& evt) { - apply_hex_input(false); - evt.Skip(); - }); - } - if (m_classic_picker) { - m_classic_picker->Bind(wxEVT_COLOURPICKER_CHANGED, [this](wxColourPickerEvent& evt) { - if (m_syncing_inputs) - return; - apply_requested_target(evt.GetColour()); - }); - } - if (m_range_slider) { - m_range_slider->Bind(wxEVT_SLIDER, [this](wxCommandEvent&) { - m_min_component_percent = m_range_slider ? std::clamp(m_range_slider->GetValue(), 0, 50) : m_min_component_percent; - update_range_label(); - if (m_color_map) - m_color_map->set_min_component_percent(m_min_component_percent); - rebuild_presets_ui(); - request_recipe_match(m_requested_target, true, _L("Matching closest supported mix...")); - }); - } - - Bind(wxEVT_TIMER, [this](wxTimerEvent&) { refresh_selected_recipe(); }, m_recipe_timer.GetId()); - Bind( - wxEVT_TIMER, - [this](wxTimerEvent&) { - if (m_loading_gauge && m_recipe_loading) - m_loading_gauge->Pulse(); - }, - m_loading_timer.GetId()); - if (wxWindow* ok_button = FindWindow(wxID_OK)) { - ok_button->Bind(wxEVT_BUTTON, [this](wxCommandEvent& evt) { - if (m_recipe_refresh_pending) - refresh_selected_recipe(); - if (m_recipe_loading || !m_selected_recipe.valid) - return; - evt.Skip(); - }); - } - - Bind(wxEVT_CLOSE_WINDOW, [this](wxCloseEvent&) { EndModal(wxID_CANCEL); }); - - CentreOnParent(); - wxGetApp().UpdateDlgDarkUI(this); - } - - ~MixedFilamentColorMatchDialog() override - { - if (m_recipe_timer.IsRunning()) - m_recipe_timer.Stop(); - if (m_loading_timer.IsRunning()) - m_loading_timer.Stop(); - } - - void begin_initial_recipe_load() { request_recipe_match(m_requested_target, false, _L("Calculating closest supported mix...")); } - - MixedColorMatchRecipeResult selected_recipe() const { return m_selected_recipe; } - - void on_dpi_changed(const wxRect& suggested_rect) override - { - wxUnusedVar(suggested_rect); - Layout(); - Fit(); - Refresh(); - } - -private: - void sync_recipe_preview(MixedColorMatchRecipeResult& recipe, const wxColour* requested_target = nullptr) - { - if (!recipe.valid) - return; - - recipe.preview_color = compute_color_match_recipe_display_color(recipe, m_display_context); - if (requested_target != nullptr && requested_target->IsOk()) - recipe.delta_e = color_delta_e00(*requested_target, recipe.preview_color); - } - - void update_range_label() - { - if (m_range_value) - m_range_value->SetLabel(wxString::Format(_L("%d%% min"), m_min_component_percent)); - } - - void rebuild_presets_ui() - { - if (!m_presets_host || !m_presets_sizer || !m_presets_label) - return; - - m_presets = build_color_match_presets(m_physical_colors, m_min_component_percent); - for (MixedColorMatchRecipeResult& preset : m_presets) - sync_recipe_preview(preset); - - m_presets_host->Freeze(); - while (m_presets_sizer->GetItemCount() > 0) { - wxSizerItem* item = m_presets_sizer->GetItem(size_t(0)); - wxWindow* window = item ? item->GetWindow() : nullptr; - m_presets_sizer->Remove(0); - if (window) - window->Destroy(); - } - - for (const MixedColorMatchRecipeResult& preset : m_presets) { - auto* button = new wxBitmapButton(m_presets_host, wxID_ANY, - make_color_match_swatch_bitmap(preset.preview_color, wxSize(FromDIP(30), FromDIP(20))), - wxDefaultPosition, wxDefaultSize, wxBU_EXACTFIT); - const wxString tooltip = from_u8(summarize_color_match_recipe(preset)) + "\n" + - normalize_color_match_hex(preset.preview_color.GetAsString(wxC2S_HTML_SYNTAX)); - button->SetToolTip(tooltip); - button->Bind(wxEVT_BUTTON, [this, preset](wxCommandEvent&) { apply_preset(preset); }); - m_presets_sizer->Add(button, 0, wxALL, FromDIP(2)); - } - - m_presets_host->FitInside(); - const bool show_presets = !m_presets.empty(); - m_presets_label->Show(show_presets); - m_presets_host->Show(show_presets); - m_presets_host->Thaw(); - } - - void set_recipe_loading(bool loading, const wxString& message) - { - m_recipe_loading = loading; - if (!message.empty()) - m_loading_message = message; - - if (m_loading_label) - m_loading_label->SetLabel(loading ? m_loading_message : wxString(" ")); - if (m_loading_gauge) { - if (loading) { - m_loading_gauge->Enable(true); - m_loading_gauge->Pulse(); - if (!m_loading_timer.IsRunning()) - m_loading_timer.Start(100); - } else { - if (m_loading_timer.IsRunning()) - m_loading_timer.Stop(); - m_loading_gauge->SetValue(0); - m_loading_gauge->Enable(false); - } - } - } - - void sync_inputs_to_requested() - { - m_syncing_inputs = true; - if (m_hex_input) - m_hex_input->ChangeValue(normalize_color_match_hex(m_requested_target.GetAsString(wxC2S_HTML_SYNTAX))); - if (m_classic_picker) - m_classic_picker->SetColour(m_requested_target); - m_syncing_inputs = false; - } - - bool apply_requested_target(const wxColour& requested_target) - { - request_recipe_match(requested_target, false, _L("Matching closest supported mix...")); - return true; - } - - bool apply_hex_input(bool show_invalid_error) - { - if (!m_hex_input || m_syncing_inputs) - return false; - - wxColour parsed; - if (!try_parse_color_match_hex(m_hex_input->GetValue(), parsed)) { - if (show_invalid_error && m_error_label) - m_error_label->SetLabel(_L("Use a valid hex color like #00FF88.")); - return false; - } - - return apply_requested_target(parsed); - } - - void request_recipe_match(const wxColour& requested_target, bool debounce, const wxString& loading_message) - { - m_requested_target = requested_target; - m_selected_target = requested_target; - sync_inputs_to_requested(); - - ++m_recipe_request_token; - set_recipe_loading(true, loading_message); - - if (m_recipe_timer.IsRunning()) - m_recipe_timer.Stop(); - m_recipe_refresh_pending = debounce; - update_dialog_state(); - - if (debounce) { - m_recipe_timer.StartOnce(120); - return; - } - - launch_recipe_match(m_recipe_request_token, requested_target); - } - - void refresh_selected_recipe() - { - m_recipe_refresh_pending = false; - launch_recipe_match(m_recipe_request_token, m_requested_target); - } - - void launch_recipe_match(size_t request_token, const wxColour& requested_target) - { - const std::vector physical_colors = m_physical_colors; - const int min_component_percent = m_min_component_percent; - wxWeakRef weak_self(this); - std::thread([weak_self, physical_colors, requested_target, request_token, min_component_percent]() { - MixedColorMatchRecipeResult recipe = build_best_color_match_recipe(physical_colors, requested_target, min_component_percent); - wxGetApp().CallAfter([weak_self, requested_target, recipe = std::move(recipe), request_token]() mutable { - if (!weak_self) - return; - auto* self = static_cast(weak_self.get()); - self->handle_recipe_result(request_token, requested_target, std::move(recipe)); - }); - }).detach(); - } - - void handle_recipe_result(size_t request_token, const wxColour& requested_target, MixedColorMatchRecipeResult recipe) - { - if (request_token != m_recipe_request_token) - return; - - m_has_recipe_result = true; - m_selected_recipe = std::move(recipe); - sync_recipe_preview(m_selected_recipe, &requested_target); - set_recipe_loading(false, wxEmptyString); - - if (m_selected_recipe.valid) { - m_selected_target = m_selected_recipe.preview_color; - if (m_color_map) - m_color_map->set_normalized_weights(expand_color_match_recipe_weights(m_selected_recipe, m_palette.size()), false); - sync_inputs_to_requested(); - } else { - m_selected_target = requested_target; - } - - update_dialog_state(); - } - - void apply_preset(MixedColorMatchRecipeResult preset) - { - preset.delta_e = 0.0; - sync_recipe_preview(preset); - ++m_recipe_request_token; - m_requested_target = preset.preview_color; - m_selected_target = preset.preview_color; - m_selected_recipe = std::move(preset); - m_has_recipe_result = true; - m_recipe_refresh_pending = false; - if (m_recipe_timer.IsRunning()) - m_recipe_timer.Stop(); - set_recipe_loading(false, wxEmptyString); - if (m_color_map) - m_color_map->set_normalized_weights(expand_color_match_recipe_weights(m_selected_recipe, m_palette.size()), false); - sync_inputs_to_requested(); - update_dialog_state(); - } - - void update_dialog_state() - { - const wxColour fallback = wxColour("#26A69A"); - if (m_selected_preview) { - m_selected_preview->SetBackgroundColour(m_requested_target.IsOk() ? m_requested_target : fallback); - m_selected_preview->Refresh(); - } - if (m_selected_label) - m_selected_label->SetLabel(m_requested_target.IsOk() ? - normalize_color_match_hex(m_requested_target.GetAsString(wxC2S_HTML_SYNTAX)) : - normalize_color_match_hex(fallback.GetAsString(wxC2S_HTML_SYNTAX))); - - const bool valid = m_selected_recipe.valid; - const wxColour recipe_color = (valid && m_selected_recipe.preview_color.IsOk()) ? - m_selected_recipe.preview_color : - (m_requested_target.IsOk() ? m_requested_target : fallback); - if (m_recipe_preview) { - m_recipe_preview->SetBackgroundColour(recipe_color); - m_recipe_preview->Refresh(); - } - if (m_recipe_label) { - if (m_recipe_loading) { - m_recipe_label->SetLabel(m_loading_message); - } else if (valid) { - const wxString recipe_summary = from_u8(summarize_color_match_recipe(m_selected_recipe)); - const wxString recipe_hex = normalize_color_match_hex(recipe_color.GetAsString(wxC2S_HTML_SYNTAX)); - m_recipe_label->SetLabel(recipe_summary + " " + recipe_hex); - } else if (m_has_recipe_result) { - m_recipe_label->SetLabel(_L("No supported 2-color, 3-color, or 4-color recipe found.")); - } else { - m_recipe_label->SetLabel(wxEmptyString); - } - } - if (m_delta_label) { - if (m_recipe_loading && m_requested_target.IsOk()) { - m_delta_label->SetLabel( - wxString::Format(_L("Matching %s..."), normalize_color_match_hex(m_requested_target.GetAsString(wxC2S_HTML_SYNTAX)))); - } else if (valid && m_requested_target.IsOk()) { - m_delta_label->SetLabel(wxString::Format(_L("Requested %s, closest recipe delta: %.2f"), - normalize_color_match_hex(m_requested_target.GetAsString(wxC2S_HTML_SYNTAX)), - m_selected_recipe.delta_e)); - } else { - m_delta_label->SetLabel(wxEmptyString); - } - } - if (m_error_label) { - if (m_recipe_loading) - m_error_label->SetLabel(wxEmptyString); - else if (!valid && m_has_recipe_result) - m_error_label->SetLabel( - _L("Unable to create a color mix from the current physical filament colors within the selected range.")); - else if (m_hex_input && !m_syncing_inputs) { - wxColour parsed; - if (!try_parse_color_match_hex(m_hex_input->GetValue(), parsed)) - m_error_label->SetLabel(_L("Use a valid hex color like #00FF88.")); - else - m_error_label->SetLabel(wxEmptyString); - } else { - m_error_label->SetLabel(wxEmptyString); - } - } - if (wxWindow* ok_button = FindWindow(wxID_OK)) - ok_button->Enable(valid && !m_recipe_loading && !m_recipe_refresh_pending); - - Layout(); - } - -private: - std::vector m_physical_colors; - MixedFilamentDisplayContext m_display_context; - std::vector m_palette; - std::vector m_presets; - MixedFilamentColorMapPanel* m_color_map = nullptr; - wxTextCtrl* m_hex_input = nullptr; - wxColourPickerCtrl* m_classic_picker = nullptr; - wxSlider* m_range_slider = nullptr; - wxStaticText* m_range_value = nullptr; - wxStaticText* m_presets_label = nullptr; - wxScrolledWindow* m_presets_host = nullptr; - wxWrapSizer* m_presets_sizer = nullptr; - wxPanel* m_loading_panel = nullptr; - wxStaticText* m_loading_label = nullptr; - wxGauge* m_loading_gauge = nullptr; - wxPanel* m_selected_preview = nullptr; - wxStaticText* m_selected_label = nullptr; - wxPanel* m_recipe_preview = nullptr; - wxStaticText* m_recipe_label = nullptr; - wxStaticText* m_delta_label = nullptr; - wxStaticText* m_error_label = nullptr; - wxColour m_requested_target{wxColour("#26A69A")}; - wxColour m_selected_target{wxColour("#26A69A")}; - MixedColorMatchRecipeResult m_selected_recipe; - wxTimer m_recipe_timer; - wxTimer m_loading_timer; - wxString m_loading_message; - size_t m_recipe_request_token{0}; - int m_min_component_percent{15}; - bool m_has_recipe_result{false}; - bool m_recipe_loading{false}; - bool m_recipe_refresh_pending{false}; - bool m_syncing_inputs{false}; -}; - - -MixedColorMatchRecipeResult prompt_best_color_match_recipe(wxWindow* parent, - const std::vector& physical_colors, - const wxColour& initial_color) -{ - MixedFilamentColorMatchDialog dlg(parent, physical_colors, initial_color); - dlg.begin_initial_recipe_load(); - if (dlg.ShowModal() != wxID_OK) { - MixedColorMatchRecipeResult cancelled; - cancelled.cancelled = true; - return cancelled; - } - - return dlg.selected_recipe(); -} static DynamicFilamentList dynamic_filament_list; static DynamicFilamentList1Based dynamic_filament_list_1_based; @@ -1610,6 +1024,11 @@ Sidebar::Sidebar(Plater *parent) Choice::register_dynamic_list("wipe_tower_filament", &dynamic_filament_list); p->scrolled = new wxPanel(this); + // p->scrolled->SetScrollbars(0, 100, 1, 2); // ys_DELETE_after_testing. pixelsPerUnitY = 100 + // but this cause the bad layout of the sidebar, when all infoboxes appear. + // As a result we can see the empty block at the bottom of the sidebar + // But if we set this value to 5, layout will be better + //p->scrolled->SetScrollRate(0, 5); p->scrolled->SetBackgroundColour(*wxWHITE); @@ -2013,7 +1432,7 @@ Sidebar::Sidebar(Plater *parent) p->m_panel_filament_title->SetBackgroundColor2(0xF1F1F1); p->m_panel_filament_title->Bind(wxEVT_LEFT_UP, [this](wxMouseEvent &e) { if (e.GetPosition().x > (p->m_flushing_volume_btn->IsShown() - ? p->m_flushing_volume_btn->GetPosition().x : (p->m_bpButton_ams_filament->GetPosition().x - FromDIP(30)))) + ? p->m_flushing_volume_btn->GetPosition().x : (p->m_bpButton_add_filament->GetPosition().x - FromDIP(30)))) // ORCA exclude area of del button from titlebar collapse/expand feature to fix undesired collapse when user spams del filament button return; if (p->m_panel_filament_content->GetMaxHeight() == 0) p->m_panel_filament_content->SetMaxSize({-1, -1}); @@ -2024,10 +1443,11 @@ Sidebar::Sidebar(Plater *parent) wxBoxSizer* bSizer39; bSizer39 = new wxBoxSizer( wxHORIZONTAL ); - p->m_filament_config_icon = new ScalableButton(p->m_panel_filament_title, wxID_ANY, "filament_group"); - bSizer39->Add(p->m_filament_config_icon, 0, wxALIGN_CENTER | wxLEFT, FromDIP(SidebarProps::TitlebarMargin())); - p->m_staticText_filament_settings = new Label(p->m_panel_filament_title, _L("Filament Management"), LB_PROPAGATE_MOUSE_EVENT); - bSizer39->Add( p->m_staticText_filament_settings, 0, wxALIGN_CENTER | wxLEFT, FromDIP(SidebarProps::TitlebarMargin())); + p->m_filament_icon = new ScalableButton(p->m_panel_filament_title, wxID_ANY, "filament"); + p->m_staticText_filament_settings = new Label(p->m_panel_filament_title, _L("Filament"), LB_PROPAGATE_MOUSE_EVENT); + bSizer39->Add(p->m_filament_icon, 0, wxALIGN_CENTER | wxLEFT, FromDIP(SidebarProps::TitlebarMargin())); + bSizer39->AddSpacer(FromDIP(SidebarProps::ElementSpacing())); + bSizer39->Add( p->m_staticText_filament_settings, 0, wxALIGN_CENTER ); bSizer39->Add(FromDIP(10), 0, 0, 0, 0); bSizer39->SetMinSize(-1, FromDIP(30)); @@ -2057,7 +1477,7 @@ Sidebar::Sidebar(Plater *parent) ConfigOptionFloat* flush_multi_opt = project_config.option("flush_multiplier"); float flush_multiplier = flush_multi_opt ? flush_multi_opt->getFloat() : 1.f; - const std::vector extruder_colours = wxGetApp().plater()->get_extruder_colors_from_plater_config(nullptr, false); + const std::vector extruder_colours = wxGetApp().plater()->get_extruder_colors_from_plater_config(); const auto& full_config = wxGetApp().preset_bundle->full_config(); const auto& extra_flush_volumes = get_min_flush_volumes(full_config); WipingDialog dlg(parent, cast(init_matrix), cast(init_extruders), extruder_colours, extra_flush_volumes, flush_multiplier); @@ -2078,6 +1498,57 @@ Sidebar::Sidebar(Plater *parent) bSizer39->Add(p->m_flushing_volume_btn, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(5)); bSizer39->Hide(p->m_flushing_volume_btn); + ScalableButton* add_btn = new ScalableButton(p->m_panel_filament_title, wxID_ANY, "add_filament"); + add_btn->SetToolTip(_L("Add one filament")); + add_btn->Bind(wxEVT_BUTTON, [this, scrolled_sizer](wxCommandEvent& e){ + // Orca: limit filament choices to MAXIMUM_EXTRUDER_NUMBER + if (p->combos_filament.size() >= MAXIMUM_EXTRUDER_NUMBER) + return; + + int filament_count = p->combos_filament.size() + 1; + wxColour new_col = Plater::get_next_color_for_filament(); + std::string new_color = new_col.GetAsString(wxC2S_HTML_SYNTAX).ToStdString(); + wxGetApp().preset_bundle->set_num_filaments(filament_count, new_color); + wxGetApp().plater()->on_filaments_change(filament_count); + wxGetApp().get_tab(Preset::TYPE_PRINT)->update(); + wxGetApp().preset_bundle->export_selections(*wxGetApp().app_config); + auto_calc_flushing_volumes(filament_count - 1); + }); + p->m_bpButton_add_filament = add_btn; + + // ORCA Moved add button after delete button to prevent add button position change when remove icon automatically hidden + + ScalableButton* del_btn = new ScalableButton(p->m_panel_filament_title, wxID_ANY, "delete_filament"); + del_btn->SetToolTip(_L("Remove last filament")); + del_btn->Bind(wxEVT_BUTTON, [this, scrolled_sizer](wxCommandEvent &e) { + if (p->combos_filament.size() <= 1) + return; + + size_t filament_count = p->combos_filament.size() - 1; + if (wxGetApp().preset_bundle->is_the_only_edited_filament(filament_count) || (filament_count == 1)) { + wxGetApp().get_tab(Preset::TYPE_FILAMENT)->select_preset(wxGetApp().preset_bundle->filament_presets[0], false, "", true); + } + + if (p->editing_filament >= filament_count) { + p->editing_filament = -1; + } + + wxGetApp().preset_bundle->set_num_filaments(filament_count); + wxGetApp().plater()->on_filaments_change(filament_count); + wxGetApp().get_tab(Preset::TYPE_PRINT)->update(); + wxGetApp().preset_bundle->export_selections(*wxGetApp().app_config); + }); + p->m_bpButton_del_filament = del_btn; + + bSizer39->Add(del_btn, 0, wxALIGN_CENTER | wxLEFT, FromDIP(SidebarProps::IconSpacing())); + bSizer39->Add(add_btn, 0, wxALIGN_CENTER | wxLEFT, FromDIP(SidebarProps::IconSpacing())); // ORCA Moved add button after delete button to prevent add button position change when remove icon automatically hidden + bSizer39->AddSpacer(FromDIP(20)); + + if (p->combos_filament.size() <= 1) { // ORCA Fix Flushing button and Delete filament button not hidden on launch while only 1 filament exist + bSizer39->Hide(p->m_flushing_volume_btn); + bSizer39->Hide(p->m_bpButton_del_filament); // ORCA: Hide delete filament button if there is only one filament + } + ams_btn = new ScalableButton(p->m_panel_filament_title, wxID_ANY, "ams_fila_sync", wxEmptyString, wxDefaultSize, wxDefaultPosition, wxBU_EXACTFIT | wxNO_BORDER, false, 16); // ORCA match icon size with other icons as 16x16 ams_btn->SetToolTip(_L("Synchronize filament list from AMS")); @@ -2109,27 +1580,17 @@ Sidebar::Sidebar(Plater *parent) //wxBoxSizer* bSizer_filament_content; //bSizer_filament_content = new wxBoxSizer( wxHORIZONTAL ); - // BBS: filament double columns, wrapped in scrolled window (max 3 rows) + // BBS: filament double columns p->sizer_filaments = new wxBoxSizer(wxHORIZONTAL); p->sizer_filaments->Add(new wxBoxSizer(wxVERTICAL), 1, wxEXPAND); p->sizer_filaments->Add(new wxBoxSizer(wxVERTICAL), 1, wxEXPAND); - p->m_scrolled_filaments = new wxScrolledWindow(p->m_panel_filament_content, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL); - p->m_scrolled_filaments->SetScrollRate(0, 5); - p->m_scrolled_filaments->SetBackgroundColour(*wxWHITE); - p->m_panel_scrolled_filament_content = new wxPanel(p->m_scrolled_filaments, wxID_ANY); - p->m_panel_scrolled_filament_content->SetBackgroundColour(*wxWHITE); - p->m_panel_scrolled_filament_content->SetSizer(p->sizer_filaments); - auto* scrolled_fila_sizer = new wxBoxSizer(wxVERTICAL); - scrolled_fila_sizer->Add(p->m_panel_scrolled_filament_content, 0, wxEXPAND); - p->m_scrolled_filaments->SetSizer(scrolled_fila_sizer); - p->combos_filament.push_back(nullptr); /* first filament item */ // init_filament_combo(&p->combos_filament[0], 0); - p->combos_filament[0] = new PlaterPresetComboBox(p->m_panel_scrolled_filament_content, Preset::TYPE_FILAMENT); + p->combos_filament[0] = new PlaterPresetComboBox(p->m_panel_filament_content, Preset::TYPE_FILAMENT); auto combo_and_btn_sizer = new wxBoxSizer(wxHORIZONTAL); // BBS: filament double columns combo_and_btn_sizer->AddSpacer(FromDIP(SidebarProps::ContentMargin())); @@ -2143,7 +1604,7 @@ Sidebar::Sidebar(Plater *parent) edit_btn->SetBackgroundColour(wxColour(255, 255, 255)); edit_btn->SetToolTip(_L("Click to edit preset"));*/ - ScalableButton* edit_btn = new ScalableButton(p->m_panel_scrolled_filament_content, wxID_ANY, "menu_filament"); + ScalableButton* edit_btn = new ScalableButton(p->m_panel_filament_content, wxID_ANY, "menu_filament"); edit_btn->SetToolTip(_L("Click to edit preset")); PlaterPresetComboBox* combobox = p->combos_filament[0]; @@ -2165,250 +1626,14 @@ Sidebar::Sidebar(Plater *parent) //bSizer_filament_content->Add(p->sizer_filaments, 1, wxALIGN_CENTER | wxALL); wxSizer *sizer_filaments2 = new wxBoxSizer(wxVERTICAL); - // --- Filaments title bar (same level as Color Mix title) --- - p->m_panel_physical_filaments_title = new StaticBox(p->m_panel_filament_content, wxID_ANY, wxDefaultPosition, - wxDefaultSize, wxTAB_TRAVERSAL | wxBORDER_NONE); - p->m_panel_physical_filaments_title->SetBackgroundColor(title_bg); - p->m_panel_physical_filaments_title->SetBackgroundColor2(0xF1F1F1); - p->m_panel_physical_filaments_title->SetMinSize(wxSize(-1, FromDIP(30))); - p->m_panel_physical_filaments_title->SetMaxSize(wxSize(-1, FromDIP(30))); - p->m_filament_icon = new ScalableButton(p->m_panel_physical_filaments_title, wxID_ANY, "filament"); - auto* physical_label = new Label(p->m_panel_physical_filaments_title, _L("Filaments"), LB_PROPAGATE_MOUSE_EVENT); - auto* h_physical_title = new wxBoxSizer(wxHORIZONTAL); - auto* white_left_f = new wxPanel(p->m_panel_physical_filaments_title, wxID_ANY, wxDefaultPosition, wxSize(FromDIP(SidebarProps::ContentMargin()), -1)); - white_left_f->SetBackgroundColour(*wxWHITE); - h_physical_title->Add(white_left_f, 0, wxEXPAND | wxTOP | wxBOTTOM, 0); - h_physical_title->Add(p->m_filament_icon, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(SidebarProps::TitlebarMargin())); - h_physical_title->AddSpacer(FromDIP(SidebarProps::ElementSpacing())); - h_physical_title->Add(physical_label, 0, wxALIGN_CENTER_VERTICAL); - h_physical_title->AddStretchSpacer(); - - // Delete filament button — delegates to delete_filament for consistent remap behavior - ScalableButton* del_btn = new ScalableButton(p->m_panel_physical_filaments_title, wxID_ANY, "delete_filament"); - del_btn->SetToolTip(_L("Remove last filament")); - del_btn->Bind(wxEVT_BUTTON, [this](wxCommandEvent &e) { - if (p->combos_filament.size() <= 1) - return; - delete_filament(size_t(-1), -1); - }); - p->m_bpButton_del_filament = del_btn; - - // Add filament button - ScalableButton* add_btn = new ScalableButton(p->m_panel_physical_filaments_title, wxID_ANY, "add_filament"); - add_btn->SetToolTip(_L("Add one filament")); - add_btn->Bind(wxEVT_BUTTON, [this](wxCommandEvent& e){ - if (p->combos_filament.size() >= MAXIMUM_EXTRUDER_NUMBER) - return; - PresetBundle* pb = wxGetApp().preset_bundle; - if (!pb || pb->mixed_filaments.total_filaments(p->combos_filament.size()) >= MAXIMUM_FILAMENT_NUMBER) - return; - int filament_count = p->combos_filament.size() + 1; - wxGetApp().plater()->confirm_auto_generated_gradients(filament_count); - wxColour new_col = Plater::get_next_color_for_filament(); - std::string new_color = new_col.GetAsString(wxC2S_HTML_SYNTAX).ToStdString(); - pb->set_num_filaments(filament_count, new_color); - wxGetApp().plater()->on_filaments_change(filament_count); - wxGetApp().get_tab(Preset::TYPE_PRINT)->update(); - pb->export_selections(*wxGetApp().app_config); - auto_calc_flushing_volumes(filament_count - 1); - }); - p->m_bpButton_add_filament = add_btn; - - h_physical_title->Add(del_btn, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(4)); - h_physical_title->Add(add_btn, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(8)); - auto* white_right_f = new wxPanel(p->m_panel_physical_filaments_title, wxID_ANY, wxDefaultPosition, wxSize(FromDIP(SidebarProps::ContentMargin()), -1)); - white_right_f->SetBackgroundColour(*wxWHITE); - h_physical_title->Add(white_right_f, 0, wxEXPAND | wxTOP | wxBOTTOM, 0); - p->m_panel_physical_filaments_title->SetSizer(h_physical_title); - p->m_panel_physical_filaments_title->Layout(); - - if (p->combos_filament.size() <= 1) - h_physical_title->Hide(p->m_bpButton_del_filament); - - sizer_filaments2->AddSpacer(FromDIP(8)); - sizer_filaments2->Add(p->m_panel_physical_filaments_title, 0, wxEXPAND, 0); - sizer_filaments2->AddSpacer(FromDIP(8)); - sizer_filaments2->Add(p->m_scrolled_filaments, 0, wxEXPAND, 0); - sizer_filaments2->AddSpacer(FromDIP(8)); - // --- Color Mix Panel (inside filament content, same level as filaments) --- - init_color_mix_panel(p->m_panel_filament_content, sizer_filaments2); + sizer_filaments2->AddSpacer(FromDIP(16)); + sizer_filaments2->Add(p->sizer_filaments, 0, wxEXPAND, 0); + sizer_filaments2->AddSpacer(FromDIP(16)); p->m_panel_filament_content->SetSizer(sizer_filaments2); p->m_panel_filament_content->Layout(); scrolled_sizer->Add(p->m_panel_filament_content, 0, wxEXPAND, 0); } - // --- Mixed Filaments Panel (Collapsible) --- - { - // Create title bar (StaticBox for collapsible header) - p->m_panel_mixed_filaments_title = new StaticBox(p->scrolled, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL | wxBORDER_NONE); - p->m_panel_mixed_filaments_title->SetBackgroundColor(title_bg); - p->m_panel_mixed_filaments_title->SetBackgroundColor2(0xF1F1F1); - - // Create icon - p->m_mixed_filaments_icon = new ScalableButton(p->m_panel_mixed_filaments_title, wxID_ANY, "filament"); - - // Create title text - p->m_staticText_mixed_filaments = new Label(p->m_panel_mixed_filaments_title, _L("Mixed Filaments"), LB_PROPAGATE_MOUSE_EVENT); - - // Create "Add Gradient" button - p->m_btn_add_gradient = new Button(p->m_panel_mixed_filaments_title, _L("Add Gradient")); - p->m_btn_add_gradient->SetStyle(ButtonStyle::Confirm, ButtonType::Compact); - p->m_btn_add_gradient->Bind(wxEVT_BUTTON, [this](wxCommandEvent& e) { - // Add gradient mixed filament - if (wxGetApp().preset_bundle) { - auto &mgr = wxGetApp().preset_bundle->mixed_filaments; - // Get physical filament colors - ConfigOptionStrings *co = wxGetApp().preset_bundle->project_config.option("filament_colour"); - std::vector colors = co ? co->values : std::vector(); - // Add a custom gradient (50% mix) - mgr.add_custom_filament(1, 2, 50, colors); - // Persist the custom entries so they survive the clear/load cycle in update_mixed_filament_panel - if (ConfigOptionString *opt = wxGetApp().preset_bundle->project_config.option("mixed_filament_definitions")) - opt->value = mgr.serialize_custom_entries(); - update_mixed_filament_panel(false); - m_scrolled_sizer->Layout(); - } - }); - - // Create "Add Pattern" button - p->m_btn_add_pattern = new Button(p->m_panel_mixed_filaments_title, _L("Add Pattern")); - p->m_btn_add_pattern->SetStyle(ButtonStyle::Confirm, ButtonType::Compact); - p->m_btn_add_pattern->Bind(wxEVT_BUTTON, [this](wxCommandEvent& e) { - // Add pattern mixed filament - if (wxGetApp().preset_bundle) { - auto &mgr = wxGetApp().preset_bundle->mixed_filaments; - // Get physical filament colors - ConfigOptionStrings *co = wxGetApp().preset_bundle->project_config.option("filament_colour"); - std::vector colors = co ? co->values : std::vector(); - // Add a custom pattern filament (will be configured by user) - mgr.add_custom_filament(1, 2, 50, colors); - // Set manual pattern for the newly added filament - auto &mfs = mgr.mixed_filaments(); - if (!mfs.empty()) { - mfs.back().manual_pattern = "12"; - mfs.back().custom = true; - } - // Persist the custom entries so they survive the clear/load cycle in update_mixed_filament_panel - if (ConfigOptionString *opt = wxGetApp().preset_bundle->project_config.option("mixed_filament_definitions")) - opt->value = mgr.serialize_custom_entries(); - update_mixed_filament_panel(false); - m_scrolled_sizer->Layout(); - } - }); - - // Create "Add Color" button - p->m_btn_add_color = new Button(p->m_panel_mixed_filaments_title, _L("Add Color")); - p->m_btn_add_color->SetStyle(ButtonStyle::Confirm, ButtonType::Compact); - p->m_btn_add_color->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { - if (wxGetApp().preset_bundle == nullptr) - return; - - ConfigOptionStrings *co = wxGetApp().preset_bundle->project_config.option("filament_colour"); - const std::vector colors = co ? co->values : std::vector(); - if (colors.size() < 2) - return; - - const MixedColorMatchRecipeResult recipe = - prompt_best_color_match_recipe(this, colors, Plater::get_next_color_for_filament()); - if (recipe.cancelled) - return; - if (!recipe.valid) { - show_error(this, _L("Unable to create a color match from the current physical filament colors.")); - return; - } - - const MixedFilamentDisplayContext display_context = build_mixed_filament_display_context(colors); - auto &mgr = wxGetApp().preset_bundle->mixed_filaments; - mgr.set_display_context(display_context); - mgr.add_custom_filament(recipe.component_a, recipe.component_b, recipe.mix_b_percent, colors); - auto &mfs = mgr.mixed_filaments(); - if (!mfs.empty()) { - MixedFilament &created = mfs.back(); - created.manual_pattern = recipe.manual_pattern; - created.mix_b_percent = recipe.mix_b_percent; - created.gradient_component_ids = recipe.gradient_component_ids; - created.gradient_component_weights = recipe.gradient_component_weights; - created.pointillism_all_filaments = false; - created.distribution_mode = recipe.gradient_component_ids.empty() ? int(MixedFilament::Simple) : int(MixedFilament::LayerCycle); - created.custom = true; - created.display_color = compute_color_match_recipe_display_color(recipe, display_context).GetAsString(wxC2S_HTML_SYNTAX).ToStdString(); - } - - if (ConfigOptionString *opt = wxGetApp().preset_bundle->project_config.option("mixed_filament_definitions")) - opt->value = mgr.serialize_custom_entries(); - update_mixed_filament_panel(false); - m_scrolled_sizer->Layout(); - }); - - // Create horizontal sizer for title bar - wxBoxSizer* h_sizer_mixed_title = new wxBoxSizer(wxHORIZONTAL); - h_sizer_mixed_title->Add(p->m_mixed_filaments_icon, 0, wxALIGN_CENTER | wxLEFT, FromDIP(SidebarProps::TitlebarMargin())); - h_sizer_mixed_title->AddSpacer(FromDIP(SidebarProps::ElementSpacing())); - h_sizer_mixed_title->Add(p->m_staticText_mixed_filaments, 0, wxALIGN_CENTER); - h_sizer_mixed_title->AddStretchSpacer(); - h_sizer_mixed_title->Add(p->m_btn_add_gradient, 0, wxALIGN_CENTER | wxRIGHT, FromDIP(SidebarProps::ElementSpacing())); - h_sizer_mixed_title->Add(p->m_btn_add_pattern, 0, wxALIGN_CENTER | wxRIGHT, FromDIP(SidebarProps::ElementSpacing())); - h_sizer_mixed_title->Add(p->m_btn_add_color, 0, wxALIGN_CENTER | wxRIGHT, FromDIP(SidebarProps::TitlebarMargin())); - h_sizer_mixed_title->SetMinSize(-1, FromDIP(30)); - - p->m_panel_mixed_filaments_title->SetSizer(h_sizer_mixed_title); - p->m_panel_mixed_filaments_title->Layout(); - - // Add splitter line before title - auto spliter_mixed_1 = new ::StaticLine(p->scrolled); - spliter_mixed_1->SetLineColour("#A6A9AA"); - scrolled_sizer->Add(spliter_mixed_1, 0, wxEXPAND); - - // Add title bar to scrolled sizer - scrolled_sizer->Add(p->m_panel_mixed_filaments_title, 0, wxEXPAND | wxALL, 0); - - // Add splitter line after title - auto spliter_mixed_2 = new ::StaticLine(p->scrolled); - spliter_mixed_2->SetLineColour("#CECECE"); - scrolled_sizer->Add(spliter_mixed_2, 0, wxEXPAND); - - // Create content panel (collapsible) - p->m_panel_mixed_filaments_content = new wxPanel(p->scrolled, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL); - { - const bool is_dark = wxGetApp().dark_mode(); - p->m_panel_mixed_filaments_content->SetBackgroundColour(is_dark ? wxColour(45, 45, 49) : wxColour(255, 255, 255)); - } - - // Content sizer - store in member variable for later use - p->m_sizer_mixed_filaments_content = new wxBoxSizer(wxVERTICAL); - p->m_sizer_mixed_filaments_content->AddSpacer(FromDIP(SidebarProps::ContentMargin())); - p->m_panel_mixed_filaments_content->SetSizer(p->m_sizer_mixed_filaments_content); - p->m_panel_mixed_filaments_content->Layout(); - - // Add content panel to scrolled sizer - scrolled_sizer->Add(p->m_panel_mixed_filaments_content, 0, wxEXPAND, 0); - - // Bind collapse/expand event to title bar - p->m_panel_mixed_filaments_title->Bind(wxEVT_LEFT_UP, [this](wxMouseEvent& e) { - // Exclude button areas from collapse/expand - int button_left = p->m_panel_mixed_filaments_title->GetClientSize().x; - auto consider_button = [&button_left](wxWindow *button) { - if (button && button->IsShown()) - button_left = std::min(button_left, button->GetPosition().x); - }; - consider_button(p->m_btn_add_gradient); - consider_button(p->m_btn_add_pattern); - consider_button(p->m_btn_add_color); - if (e.GetPosition().x > button_left - FromDIP(12)) - return; - - if (p->m_panel_mixed_filaments_content->GetMaxHeight() == 0) - p->m_panel_mixed_filaments_content->SetMaxSize({-1, -1}); - else - p->m_panel_mixed_filaments_content->SetMaxSize({-1, 0}); - m_scrolled_sizer->Layout(); - }); - - // Initially hidden until 2+ filaments - p->m_panel_mixed_filaments_title->Hide(); - p->m_panel_mixed_filaments_content->Hide(); - } - { //add project title auto params_panel = ((MainFrame*)parent->GetParent())->m_param_panel; @@ -2524,7 +1749,7 @@ void Sidebar::create_printer_preset() void Sidebar::init_filament_combo(PlaterPresetComboBox **combo, const int filament_idx) { - *combo = new PlaterPresetComboBox(p->m_panel_scrolled_filament_content, Slic3r::Preset::TYPE_FILAMENT); + *combo = new PlaterPresetComboBox(p->m_panel_filament_content, Slic3r::Preset::TYPE_FILAMENT); (*combo)->set_filament_idx(filament_idx); auto combo_and_btn_sizer = new wxBoxSizer(wxHORIZONTAL); @@ -2553,7 +1778,7 @@ void Sidebar::init_filament_combo(PlaterPresetComboBox **combo, const int filame combo_and_btn_sizer->Add(32 * em / 10, 0, 0, 0, 0); combo_and_btn_sizer->Add(del_btn, 0, wxALIGN_CENTER_VERTICAL, 5 * em / 10); */ - ScalableButton* edit_btn = new ScalableButton(p->m_panel_scrolled_filament_content, wxID_ANY, "menu_filament"); + ScalableButton* edit_btn = new ScalableButton(p->m_panel_filament_content, wxID_ANY, "menu_filament"); edit_btn->SetToolTip(_L("Click to edit preset")); PlaterPresetComboBox* combobox = (*combo); @@ -2716,13 +1941,11 @@ void Sidebar::update_all_preset_comboboxes(bool reload_printer_view) } if (cfg.opt_bool("pellet_modded_printer")) { - p->m_staticText_filament_settings->SetLabel(_L("Pellet Configuration")); + p->m_staticText_filament_settings->SetLabel(_L("Pellets")); p->m_filament_icon->SetBitmap_("pellets"); - p->m_filament_config_icon->SetBitmap_("filament_group"); } else { - p->m_staticText_filament_settings->SetLabel(_L("Filament Management")); + p->m_staticText_filament_settings->SetLabel(_L("Filament")); p->m_filament_icon->SetBitmap_("filament"); - p->m_filament_config_icon->SetBitmap_("filament_group"); } show_SEMM_buttons(/*cfg.opt_bool("single_extruder_multi_material")*/true); @@ -2868,6 +2091,10 @@ void Sidebar::update_presets(Preset::Type preset_type) p->combos_filament[i]->update(); update_dynamic_filament_list(); + + // Real-time high/low temperature filament mixing check after filament preset change + p->plater->check_filament_temp_mixing(); + break; } @@ -2996,7 +2223,6 @@ void Sidebar::msw_rescale() p->m_printerinfo_syncbtn->msw_rescale(); p->m_printer_setting->msw_rescale(); p->m_filament_icon->msw_rescale(); - p->m_color_mix_icon->msw_rescale(); p->m_bpButton_add_filament->msw_rescale(); p->m_bpButton_del_filament->msw_rescale(); p->m_bpButton_ams_filament->msw_rescale(); @@ -3069,7 +2295,6 @@ void Sidebar::sys_color_changed() p->m_printerinfo_syncbtn->msw_rescale(); p->m_printer_setting->msw_rescale(); p->m_filament_icon->msw_rescale(); - p->m_color_mix_icon->msw_rescale(); p->m_bpButton_add_filament->msw_rescale(); p->m_bpButton_del_filament->msw_rescale(); p->m_bpButton_ams_filament->msw_rescale(); @@ -3145,31 +2370,8 @@ void Sidebar::on_filaments_change(size_t num_filaments) { auto& choices = combos_filament(); - if (num_filaments == choices.size()) { - // Project load may keep the same physical filament count while mixed - // definitions changed. Refresh mixed panel even without count changes. - const bool sync_manager = !p->m_skip_mixed_filament_sync_once; - p->m_skip_mixed_filament_sync_once = false; - update_ui_from_settings(); - update_dynamic_filament_list(); - update_mixed_filament_panel(sync_manager); - - // Recalc scrolled filament window height (max 3 rows, matches color mix) - if (p->m_scrolled_filaments && p->m_panel_scrolled_filament_content) { - p->m_panel_scrolled_filament_content->Layout(); - const wxSize content_best = p->m_panel_scrolled_filament_content->GetBestSize(); - const int row_count = ((int)num_filaments + 1) / 2; // 2-column grid - const int desired_h = row_count > 3 - ? (content_best.GetHeight() * 3) / std::max(1, row_count) - : content_best.GetHeight(); - p->m_scrolled_filaments->SetMinSize({-1, desired_h}); - p->m_scrolled_filaments->SetMaxSize({-1, desired_h}); - } - Layout(); + if (num_filaments == choices.size()) return; - } - - p->m_skip_mixed_filament_sync_once = false; if (choices.size() == 1 || num_filaments == 1) choices[0]->GetDropDown().Invalidate(); @@ -3187,4010 +2389,31 @@ void Sidebar::on_filaments_change(size_t num_filaments) // initialize selection choice->update(); choice->SetSelection(last_selection); - ++i; - } - - // remove unused choices if any - remove_unused_filament_combos(num_filaments); - - auto sizer = p->m_panel_filament_title->GetSizer(); - if (p->m_flushing_volume_btn != nullptr && sizer != nullptr) { - if (num_filaments > 1) { - sizer->Show(p->m_flushing_volume_btn); - } else { - sizer->Hide(p->m_flushing_volume_btn); - } - } - if (p->m_bpButton_del_filament != nullptr && p->m_panel_physical_filaments_title != nullptr) { - auto* inner_sizer = p->m_panel_physical_filaments_title->GetSizer(); - if (inner_sizer) { - if (num_filaments > 1) - inner_sizer->Show(p->m_bpButton_del_filament); - else - inner_sizer->Hide(p->m_bpButton_del_filament); - } - } - - // Recalc scrolled filament window height (max 3 rows, matches color mix) - if (p->m_scrolled_filaments && p->m_panel_scrolled_filament_content) { - p->m_panel_scrolled_filament_content->Layout(); - const wxSize content_best = p->m_panel_scrolled_filament_content->GetBestSize(); - const int row_count = ((int)num_filaments + 1) / 2; // 2-column grid - const int desired_h = row_count > 3 - ? (content_best.GetHeight() / std::max(1, row_count)) * 3 - : content_best.GetHeight(); - p->m_scrolled_filaments->SetMinSize({-1, desired_h}); - p->m_scrolled_filaments->SetMaxSize({-1, desired_h}); - } - - Layout(); - wxWeakRef weak_this(this); - wxTheApp->CallAfter([weak_this]() { - Sidebar* sidebar = weak_this.get(); - if (sidebar && sidebar->p && sidebar->p->m_scrolled_filaments) { - int vh = sidebar->p->m_scrolled_filaments->GetVirtualSize().y; - int ch = sidebar->p->m_scrolled_filaments->GetClientSize().y; - sidebar->p->m_scrolled_filaments->Scroll(0, std::max(0, vh - ch)); - } - }); - p->m_panel_filament_title->Refresh(); - update_ui_from_settings(); - update_dynamic_filament_list(); - update_mixed_filament_panel(); - update_color_mix_panel(); - - // Disable add buttons when combined filament limit reached - if (PresetBundle *pb = wxGetApp().preset_bundle) { - const bool can_add = pb->mixed_filaments.total_filaments(combos_filament().size()) < MAXIMUM_FILAMENT_NUMBER; - if (p->m_bpButton_add_filament) - p->m_bpButton_add_filament->Enable(can_add); - if (p->m_btn_add_color_mix) - p->m_btn_add_color_mix->Enable(can_add); - } -} - - -class MixedGradientWeightsDialog : public wxDialog -{ -public: - MixedGradientWeightsDialog(wxWindow *parent, - const std::vector &filament_ids, - const std::vector &palette, - const std::vector &initial_weights) - : wxDialog(parent, wxID_ANY, _L("Gradient Mix Weights"), wxDefaultPosition, wxDefaultSize, - wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER) - { - m_colors.reserve(filament_ids.size()); - m_weights = normalize_color_match_weights(initial_weights, filament_ids.size()); - for (const unsigned int filament_id : filament_ids) { - if (filament_id >= 1 && filament_id <= palette.size()) - m_colors.emplace_back(palette[filament_id - 1]); - else - m_colors.emplace_back(wxColour("#26A69A")); - } - if (m_colors.empty()) - m_colors.emplace_back(wxColour("#26A69A")); - - auto *root = new wxBoxSizer(wxVERTICAL); - auto *hint = new wxStaticText(this, wxID_ANY, _L("Pick a point in the gradient map to control multi-filament mix.")); - root->Add(hint, 0, wxEXPAND | wxALL, FromDIP(10)); - - m_color_map = new MixedFilamentColorMapPanel(this, filament_ids, palette, initial_weights, - wxSize(FromDIP(240), FromDIP(240))); - root->Add(m_color_map, 1, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, FromDIP(10)); - - for (size_t i = 0; i < filament_ids.size(); ++i) { - auto *row = new wxBoxSizer(wxHORIZONTAL); - wxPanel *chip = new wxPanel(this, wxID_ANY, wxDefaultPosition, wxSize(FromDIP(18), FromDIP(18)), wxBORDER_SIMPLE); - chip->SetBackgroundColour(m_colors[i]); - row->Add(chip, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(6)); - row->Add(new wxStaticText(this, wxID_ANY, wxString::Format("F%d", int(filament_ids[i]))), - 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(8)); - auto *label = new wxStaticText(this, wxID_ANY, wxString::Format("%d%%", m_weights[i])); - label->SetFont(Label::Body_12); - row->Add(label, 0, wxALIGN_CENTER_VERTICAL); - root->Add(row, 0, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, FromDIP(8)); - m_weight_labels.emplace_back(label); - } - - root->Add(CreateSeparatedButtonSizer(wxOK | wxCANCEL), 0, wxEXPAND | wxALL, FromDIP(8)); - SetSizerAndFit(root); - SetMinSize(wxSize(FromDIP(380), std::max(GetSize().GetHeight(), FromDIP(460)))); - update_weight_labels(); - - Bind(wxEVT_CLOSE_WINDOW, [this](wxCloseEvent&) { EndModal(wxID_CANCEL); }); - - if (m_color_map) { - m_color_map->Bind(wxEVT_SLIDER, [this](wxCommandEvent &) { - m_weights = m_color_map ? m_color_map->normalized_weights() : m_weights; - update_weight_labels(); - }); - } - } - - std::vector normalized_weights() const - { - return m_color_map ? m_color_map->normalized_weights() : m_weights; - } - -private: - void update_weight_labels() - { - for (size_t i = 0; i < m_weight_labels.size() && i < m_weights.size(); ++i) { - if (m_weight_labels[i]) - m_weight_labels[i]->SetLabel(wxString::Format("%d%%", m_weights[i])); - } - Layout(); - } - -private: - MixedFilamentColorMapPanel *m_color_map { nullptr }; - std::vector m_colors; - std::vector m_weights; - std::vector m_weight_labels; -}; - -// Forward declaration for MixedMixPreview (defined below) -class MixedMixPreview; - -// Inline editor panel for configuring a single mixed filament -class MixedFilamentConfigPanel : public wxPanel -{ -public: - using OnChangeFn = std::function; - - MixedFilamentConfigPanel(wxWindow *parent, - size_t mixed_id, - const MixedFilament &mf, - size_t num_physical, - const std::vector &physical_colors, - const std::vector &nozzle_diameters, - const std::vector &palette, - const MixedFilamentPreviewSettings &preview_settings, - bool bias_mode_enabled, - OnChangeFn on_change = {}); - - // Get the updated mixed filament data - MixedFilament get_mixed_filament() const { return m_mf; } - bool has_changes() const { return m_has_changes; } - static int effective_local_z_preview_mix_b_percent(const MixedFilament &mf, - const MixedFilamentPreviewSettings &preview_settings); - -private: - void build_ui(); - void update_preview(); - void update_local_z_breakdown(); - void update_component_picker_visuals(); - - size_t m_mixed_id; - MixedFilament m_mf; - size_t m_num_physical; - std::vector m_physical_colors; - std::vector m_nozzle_diameters; - std::vector m_palette; - MixedFilamentPreviewSettings m_preview_settings; - bool m_bias_mode_enabled = false; - bool m_has_changes = false; - - wxChoice *m_choice_a = nullptr; - wxChoice *m_choice_b = nullptr; - wxChoice *m_choice_c = nullptr; - wxChoice *m_choice_d = nullptr; - wxPanel *m_picker_a_container = nullptr; - wxPanel *m_picker_b_container = nullptr; - wxPanel *m_picker_c_container = nullptr; - wxPanel *m_picker_d_container = nullptr; - wxPanel *m_picker_a_swatch = nullptr; - wxPanel *m_picker_b_swatch = nullptr; - wxPanel *m_picker_c_swatch = nullptr; - wxPanel *m_picker_d_swatch = nullptr; - wxStaticText *m_picker_a_label = nullptr; - wxStaticText *m_picker_b_label = nullptr; - wxStaticText *m_picker_c_label = nullptr; - wxStaticText *m_picker_d_label = nullptr; - wxPanel *m_surface_offset_target_container = nullptr; - wxPanel *m_surface_offset_target_swatch = nullptr; - wxStaticText *m_surface_offset_target_label = nullptr; - MixedGradientSelector *m_blend_selector = nullptr; - wxStaticText *m_blend_label = nullptr; - wxTextCtrl *m_pattern_ctrl = nullptr; - wxCheckBox *m_local_z_limit_checkbox = nullptr; - wxSpinCtrl *m_local_z_limit_spin = nullptr; - wxSpinCtrlDouble *m_surface_offset_spin = nullptr; - std::vector m_pattern_quick_buttons; - MixedMixPreview *m_mix_preview = nullptr; - wxStaticText *m_breakdown_label = nullptr; - wxPanel *m_swatch = nullptr; - std::shared_ptr> m_selected_weight_state; - OnChangeFn m_on_change; - - // Helper functions (copied from update_mixed_filament_panel) - static std::vector decode_gradient_ids(const std::string &s); - static std::string encode_gradient_ids(const std::vector &ids); - static std::vector decode_manual_pattern_ids(const std::string &pattern, - unsigned int a, - unsigned int b, - size_t num_physical, - size_t wall_loops = 0); - static std::vector decode_gradient_weights(const std::string &s, size_t n); - static std::vector normalize_gradient_weights(const std::vector &w, size_t n); - static std::string encode_gradient_weights(const std::vector &w); - static std::vector build_weighted_pair_sequence(unsigned int a, unsigned int b, int percent_b, bool limit_cycle = false); - static std::vector build_weighted_multi_sequence(const std::vector &ids, - const std::vector &weights, - size_t max_cycle_limit = 0); - static std::string summarize_sequence(const std::vector &seq); - static std::string summarize_local_z_breakdown(const MixedFilament &mf, - const std::vector &weights, - const MixedFilamentPreviewSettings &preview_settings); - static std::string blend_from_sequence(const std::vector &colors, const std::vector &seq, const std::string &fallback); - static std::vector build_local_z_preview_pass_heights(double nominal_layer_height, - double lower_bound, - double upper_bound, - double preferred_a_height, - double preferred_b_height, - int mix_b_percent, - int max_sublayers_limit); -}; - -class MixedMixPreview : public wxPanel -{ -public: - explicit MixedMixPreview(wxWindow *parent) - : wxPanel(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxBORDER_NONE) - { - SetBackgroundStyle(wxBG_STYLE_PAINT); - SetMinSize(wxSize(FromDIP(120), FromDIP(20))); - Bind(wxEVT_PAINT, &MixedMixPreview::on_paint, this); - } - - void set_data(const std::vector &palette, - const std::vector &sequence, - bool same_layer_mode, - const std::vector &surface_offsets_mm, - const wxColour &fallback, - const wxString &left_overlay, - const wxString &right_overlay) - { - m_palette = palette; - m_sequence = sequence; - m_same_layer = same_layer_mode; - m_surface_offsets_mm = surface_offsets_mm; - m_fallback = fallback; - m_left_overlay = left_overlay; - m_right_overlay = right_overlay; - Refresh(); - } - -private: - wxRect preview_rect() const - { - const int margin_x = FromDIP(1); - const int margin_y = FromDIP(1); - const wxSize sz = GetClientSize(); - return wxRect(margin_x, margin_y, std::max(1, sz.GetWidth() - margin_x * 2), std::max(1, sz.GetHeight() - margin_y * 2)); - } - - wxColour color_for_extruder(unsigned int extruder_id) const - { - if (extruder_id >= 1 && extruder_id <= m_palette.size()) - return m_palette[extruder_id - 1]; - return m_fallback; - } - - double max_active_surface_offset_mm() const - { - double max_offset = 0.0; - for (double offset_mm : m_surface_offsets_mm) - max_offset = std::max(max_offset, std::abs(offset_mm)); - return std::max(0.001, max_offset); - } - - int slot_inset_for_extruder(unsigned int extruder_id, int slot_extent) const - { - if (extruder_id == 0 || extruder_id >= m_surface_offsets_mm.size() || slot_extent <= 2) - return 0; - - const double offset_mm = m_surface_offsets_mm[extruder_id]; - if (std::abs(offset_mm) <= EPSILON) - return 0; - - const double normalized = std::clamp(std::abs(offset_mm) / max_active_surface_offset_mm(), 0.0, 1.0); - const int inset = int(std::round(normalized * slot_extent * 0.45)) * (offset_mm < 0.0 ? -1 : 1); - return std::clamp(inset, -std::max(0, slot_extent / 2), std::max(0, slot_extent / 2)); - } - - void on_paint(wxPaintEvent &) - { - wxAutoBufferedPaintDC dc(this); - dc.SetBackground(wxBrush(GetBackgroundColour())); - dc.Clear(); - - const wxRect rect = preview_rect(); - dc.SetPen(*wxTRANSPARENT_PEN); - dc.SetBrush(wxBrush(m_fallback)); - dc.DrawRectangle(rect); - - if (!m_sequence.empty()) { - if (m_same_layer) { - // Same-layer preview: full-height stripe lines. - const int stripes = 24; - const int stripe_w = std::max(1, rect.GetWidth() / stripes); - const size_t seq_len = m_sequence.size(); - for (int s = 0; s < stripes; ++s) { - const size_t idx = size_t(s % int(seq_len)); - const unsigned int extruder_id = m_sequence[idx]; - dc.SetBrush(wxBrush(color_for_extruder(m_sequence[idx]))); - const int x = rect.GetLeft() + s * stripe_w; - const int w = (s == stripes - 1) ? (rect.GetRight() - x + 1) : stripe_w; - const int inset = slot_inset_for_extruder(extruder_id, w); - wxRect draw_rect(x + inset / 2, rect.GetTop(), std::max(1, w - inset), rect.GetHeight()); - draw_rect.Intersect(rect); - if (draw_rect.GetWidth() > 0) - dc.DrawRectangle(draw_rect); - } - } else { - const int bars = 24; - const int bar_w = std::max(1, rect.GetWidth() / bars); - for (int i = 0; i < bars; ++i) { - size_t idx = 0; - if (m_sequence.size() > size_t(bars)) - idx = (size_t(i) * m_sequence.size()) / size_t(bars); - else - idx = size_t(i) % m_sequence.size(); - const unsigned int extruder_id = m_sequence[idx]; - dc.SetBrush(wxBrush(color_for_extruder(extruder_id))); - const int x = rect.GetLeft() + i * bar_w; - const int w = (i == bars - 1) ? (rect.GetRight() - x + 1) : bar_w; - const int inset = slot_inset_for_extruder(extruder_id, w); - wxRect draw_rect(x + inset / 2, rect.GetTop(), std::max(1, w - inset), rect.GetHeight()); - draw_rect.Intersect(rect); - if (draw_rect.GetWidth() > 0) - dc.DrawRectangle(draw_rect); - } - } - } - - auto draw_outlined_text = [this, &dc](const wxString &text, int x, int y) { - if (text.empty()) - return; - dc.SetTextForeground(wxColour(255, 255, 255)); - const int outline_radius = std::max(2, FromDIP(2)); - for (int ox = -outline_radius; ox <= outline_radius; ++ox) { - for (int oy = -outline_radius; oy <= outline_radius; ++oy) { - if (ox == 0 && oy == 0) - continue; - dc.DrawText(text, x + ox, y + oy); - } - } - dc.SetTextForeground(wxColour(22, 22, 22)); - dc.DrawText(text, x, y); - }; - - wxCoord left_w = 0, left_h = 0; - wxCoord right_w = 0, right_h = 0; - dc.GetTextExtent(m_left_overlay, &left_w, &left_h); - dc.GetTextExtent(m_right_overlay, &right_w, &right_h); - const int text_y = rect.GetTop() + std::max(0, (rect.GetHeight() - int(std::max(left_h, right_h))) / 2); - const int pad = FromDIP(6); - if (!m_left_overlay.empty()) - draw_outlined_text(m_left_overlay, rect.GetLeft() + pad, text_y); - if (!m_right_overlay.empty()) - draw_outlined_text(m_right_overlay, rect.GetRight() - pad - int(right_w), text_y); - - const bool is_dark = wxGetApp().dark_mode(); - dc.SetPen(wxPen(is_dark ? wxColour(110, 110, 110) : wxColour(170, 170, 170), 1)); - dc.SetBrush(*wxTRANSPARENT_BRUSH); - dc.DrawRectangle(rect); - } - -private: - std::vector m_palette; - std::vector m_sequence; - std::vector m_surface_offsets_mm; - bool m_same_layer { false }; - wxColour m_fallback { wxColour(38, 166, 154) }; - wxString m_left_overlay; - wxString m_right_overlay; -}; - -// Implementation of MixedFilamentConfigPanel helper functions -static std::vector build_grouped_manual_pattern_preview_sequence(const std::string &pattern, - unsigned int component_a, - unsigned int component_b, - size_t num_physical, - size_t wall_loops) -{ - std::vector sequence; - if (num_physical == 0) - return sequence; - - const std::string normalized = MixedFilamentManager::normalize_manual_pattern(pattern); - if (normalized.empty()) - return sequence; - - const std::vector groups = MixedFilamentManager::split_pattern_groups(normalized); - if (groups.empty()) - return sequence; - - MixedFilament dummy_mf; - dummy_mf.component_a = component_a; - dummy_mf.component_b = component_b; - - if (groups.size() == 1) { - const std::vector tokens = - MixedFilamentManager::split_pattern_group_to_tokens(groups[0], num_physical); - sequence.reserve(tokens.size()); - for (const std::string &token : tokens) { - const unsigned int extruder_id = - MixedFilamentManager::physical_filament_from_token(token, dummy_mf, num_physical); - if (extruder_id != 0) - sequence.emplace_back(extruder_id); - } - return sequence; - } - - std::vector> group_tokens; - group_tokens.reserve(groups.size()); - for (const std::string &group : groups) - group_tokens.push_back(MixedFilamentManager::split_pattern_group_to_tokens(group, num_physical)); - - constexpr size_t k_max_preview_cycle = 48; - size_t cycle = 1; - for (const auto &tokens : group_tokens) { - if (tokens.empty()) - continue; - cycle = std::lcm(cycle, tokens.size()); - if (cycle >= k_max_preview_cycle) { - cycle = k_max_preview_cycle; - break; - } - } - - const size_t preview_wall_loops = std::max(1, wall_loops == 0 ? groups.size() : wall_loops); - sequence.reserve(preview_wall_loops * cycle); - for (size_t layer_idx = 0; layer_idx < cycle; ++layer_idx) { - for (size_t wall_idx = 0; wall_idx < preview_wall_loops; ++wall_idx) { - const auto &tokens = group_tokens[std::min(wall_idx, group_tokens.size() - 1)]; - if (tokens.empty()) - continue; - const std::string &token = tokens[layer_idx % tokens.size()]; - const unsigned int extruder_id = - MixedFilamentManager::physical_filament_from_token(token, dummy_mf, num_physical); - if (extruder_id != 0) - sequence.emplace_back(extruder_id); - } - } - - return sequence; -} - -std::vector MixedFilamentConfigPanel::decode_gradient_ids(const std::string &s) -{ - return MixedFilamentManager::decode_gradient_component_ids(s, 0); -} - -std::string MixedFilamentConfigPanel::encode_gradient_ids(const std::vector &ids) -{ - return MixedFilamentManager::encode_gradient_component_ids(ids); -} - -std::vector MixedFilamentConfigPanel::decode_manual_pattern_ids(const std::string &pattern, - unsigned int a, - unsigned int b, - size_t num_physical, - size_t wall_loops) -{ - return build_grouped_manual_pattern_preview_sequence(pattern, a, b, num_physical, wall_loops); -} - -std::vector MixedFilamentConfigPanel::decode_gradient_weights(const std::string &s, size_t n) -{ - std::vector w; - if (s.empty() || n == 0) - return w; - - std::string token; - for (const char c : s) { - if (c >= '0' && c <= '9') { - token.push_back(c); - continue; - } - if (!token.empty()) { - w.emplace_back(std::max(0, std::atoi(token.c_str()))); - token.clear(); - } - } - if (!token.empty()) - w.emplace_back(std::max(0, std::atoi(token.c_str()))); - if (w.size() != n) - w.clear(); - return w; -} - -std::vector MixedFilamentConfigPanel::normalize_gradient_weights(const std::vector &w, size_t n) -{ - std::vector out = w; - if (out.size() != n) out.assign(n, n > 0 ? int(100 / n) : 0); - int sum = 0; - for (int &v : out) { v = std::max(0, v); sum += v; } - if (sum <= 0 && n > 0) { out.assign(n, 0); out[0] = 100; return out; } - std::vector rem(n, 0.); - int assigned = 0; - for (size_t i = 0; i < n; ++i) { - const double exact = 100.0 * double(out[i]) / double(sum); - out[i] = int(std::floor(exact)); - rem[i] = exact - double(out[i]); - assigned += out[i]; - } - int missing = std::max(0, 100 - assigned); - while (missing > 0) { - size_t best = 0; - double best_rem = -1.0; - for (size_t i = 0; i < rem.size(); ++i) { - if (rem[i] > best_rem) { best_rem = rem[i]; best = i; } - } - ++out[best]; - rem[best] = 0.0; - --missing; - } - return out; -} - -std::string MixedFilamentConfigPanel::encode_gradient_weights(const std::vector &w) -{ - std::ostringstream out; - for (size_t i = 0; i < w.size(); ++i) { - if (i > 0) - out << '/'; - out << std::max(0, w[i]); - } - return out.str(); -} - -namespace { - -std::pair effective_pair_preview_ratios(int percent_b) -{ - const int mix_b = std::clamp(percent_b, 0, 100); - int ratio_a = 1; - int ratio_b = 0; - - if (mix_b >= 100) { - ratio_a = 0; - ratio_b = 1; - } else if (mix_b > 0) { - const int pct_b = mix_b; - const int pct_a = 100 - pct_b; - const bool b_is_major = pct_b >= pct_a; - const int major_pct = b_is_major ? pct_b : pct_a; - const int minor_pct = b_is_major ? pct_a : pct_b; - const int major_layers = - std::max(1, int(std::lround(double(major_pct) / double(std::max(1, minor_pct))))); - ratio_a = b_is_major ? 1 : major_layers; - ratio_b = b_is_major ? major_layers : 1; - } - - if (ratio_a > 0 && ratio_b > 0) { - const int g = std::gcd(ratio_a, ratio_b); - if (g > 1) { - ratio_a /= g; - ratio_b /= g; - } - } - - return { std::max(0, ratio_a), std::max(0, ratio_b) }; -} - -std::vector build_effective_pair_preview_sequence(unsigned int component_a, - unsigned int component_b, - int percent_b, - bool limit_cycle) -{ - std::vector sequence; - if (component_a == 0 || component_b == 0 || component_a == component_b) - return sequence; - - auto [ratio_a, ratio_b] = effective_pair_preview_ratios(percent_b); - constexpr int k_max_cycle = 24; - if (limit_cycle && ratio_a > 0 && ratio_b > 0 && ratio_a + ratio_b > k_max_cycle) { - const double scale = double(k_max_cycle) / double(ratio_a + ratio_b); - ratio_a = std::max(1, int(std::round(double(ratio_a) * scale))); - ratio_b = std::max(1, int(std::round(double(ratio_b) * scale))); - } - if (ratio_a == 0 && ratio_b == 0) - ratio_a = 1; - - const int cycle = std::max(1, ratio_a + ratio_b); - sequence.reserve(size_t(cycle)); - for (int pos = 0; pos < cycle; ++pos) { - const int b_before = (pos * ratio_b) / cycle; - const int b_after = ((pos + 1) * ratio_b) / cycle; - sequence.emplace_back((b_after > b_before) ? component_b : component_a); - } - return sequence; -} - -std::string format_preview_sequence_percent(int count, int total) -{ - if (count <= 0 || total <= 0) - return ""; - - const double percent = 100.0 * double(count) / double(total); - const double rounded_tenths = std::round(percent * 10.0) / 10.0; - const double nearest_integer = std::round(rounded_tenths); - if (std::abs(rounded_tenths - nearest_integer) < 1e-6) - return wxString::Format("%d%%", int(nearest_integer)).ToStdString(); - return wxString::Format("%.1f%%", rounded_tenths).ToStdString(); -} - -} // namespace - -std::vector MixedFilamentConfigPanel::build_weighted_pair_sequence(unsigned int a, - unsigned int b, - int percent_b, - bool limit_cycle) -{ - return build_effective_pair_preview_sequence(a, b, percent_b, limit_cycle); -} - -static void reduce_weight_counts_to_cycle_limit(std::vector &counts, size_t cycle_limit) -{ - if (counts.empty() || cycle_limit == 0) - return; - - int total = std::accumulate(counts.begin(), counts.end(), 0); - if (total <= 0 || size_t(total) <= cycle_limit) - return; - - std::vector positive_indices; - positive_indices.reserve(counts.size()); - for (size_t i = 0; i < counts.size(); ++i) - if (counts[i] > 0) - positive_indices.emplace_back(i); - - if (positive_indices.empty()) { - counts.assign(counts.size(), 0); - return; - } - - std::vector reduced(counts.size(), 0); - if (cycle_limit < positive_indices.size()) { - std::sort(positive_indices.begin(), positive_indices.end(), [&counts](size_t lhs, size_t rhs) { - if (counts[lhs] != counts[rhs]) - return counts[lhs] > counts[rhs]; - return lhs < rhs; - }); - for (size_t i = 0; i < cycle_limit; ++i) - reduced[positive_indices[i]] = 1; - counts = std::move(reduced); - return; - } - - size_t remaining_slots = cycle_limit; - for (const size_t idx : positive_indices) { - reduced[idx] = 1; - --remaining_slots; - } - - int total_extras = 0; - std::vector extra_counts(counts.size(), 0); - for (const size_t idx : positive_indices) { - extra_counts[idx] = std::max(0, counts[idx] - 1); - total_extras += extra_counts[idx]; - } - if (remaining_slots == 0 || total_extras <= 0) { - counts = std::move(reduced); - return; - } - - std::vector remainders(counts.size(), -1.0); - size_t assigned_slots = 0; - for (const size_t idx : positive_indices) { - if (extra_counts[idx] == 0) - continue; - const double exact = double(remaining_slots) * double(extra_counts[idx]) / double(total_extras); - const int assigned = int(std::floor(exact)); - reduced[idx] += assigned; - assigned_slots += size_t(assigned); - remainders[idx] = exact - double(assigned); - } - - size_t missing_slots = remaining_slots > assigned_slots ? (remaining_slots - assigned_slots) : size_t(0); - while (missing_slots > 0) { - size_t best_idx = size_t(-1); - double best_remainder = -1.0; - int best_extra = -1; - for (const size_t idx : positive_indices) { - if (extra_counts[idx] == 0) - continue; - if (remainders[idx] > best_remainder || - (std::abs(remainders[idx] - best_remainder) <= 1e-9 && extra_counts[idx] > best_extra) || - (std::abs(remainders[idx] - best_remainder) <= 1e-9 && extra_counts[idx] == best_extra && idx < best_idx)) { - best_idx = idx; - best_remainder = remainders[idx]; - best_extra = extra_counts[idx]; - } - } - if (best_idx == size_t(-1)) - break; - ++reduced[best_idx]; - remainders[best_idx] = -1.0; - --missing_slots; - } - - counts = std::move(reduced); -} - -std::vector MixedFilamentConfigPanel::build_weighted_multi_sequence(const std::vector &ids, - const std::vector &weights, - size_t max_cycle_limit) -{ - std::vector seq; - if (ids.empty()) - return seq; - - std::vector filtered_ids; - std::vector counts; - filtered_ids.reserve(ids.size()); - counts.reserve(ids.size()); - - std::vector normalized = normalize_gradient_weights(weights, ids.size()); - for (size_t i = 0; i < ids.size(); ++i) { - const int weight = (i < normalized.size()) ? std::max(0, normalized[i]) : 0; - if (weight <= 0) - continue; - filtered_ids.emplace_back(ids[i]); - counts.emplace_back(weight); - } - if (filtered_ids.empty()) { - filtered_ids = ids; - counts.assign(ids.size(), 1); - } - - int g = 0; - for (const int c : counts) - g = std::gcd(g, std::max(1, c)); - if (g > 1) { - for (int &c : counts) - c = std::max(1, c / g); - } - - constexpr size_t k_max_cycle = 48; - const size_t effective_cycle_limit = - max_cycle_limit > 0 ? std::min(k_max_cycle, std::max(1, max_cycle_limit)) : k_max_cycle; - reduce_weight_counts_to_cycle_limit(counts, effective_cycle_limit); - - std::vector reduced_ids; - std::vector reduced_counts; - reduced_ids.reserve(filtered_ids.size()); - reduced_counts.reserve(counts.size()); - for (size_t i = 0; i < counts.size(); ++i) { - if (counts[i] <= 0) - continue; - reduced_ids.emplace_back(filtered_ids[i]); - reduced_counts.emplace_back(counts[i]); - } - if (reduced_ids.empty()) - return seq; - filtered_ids = std::move(reduced_ids); - counts = std::move(reduced_counts); - - const int total = std::accumulate(counts.begin(), counts.end(), 0); - if (total <= 0) - return seq; - - const size_t cycle = size_t(total); - - seq.reserve(cycle); - std::vector emitted(counts.size(), 0); - for (size_t pos = 0; pos < cycle; ++pos) { - size_t best_idx = 0; - double best_score = -1e9; - for (size_t i = 0; i < counts.size(); ++i) { - const double target = double(pos + 1) * double(counts[i]) / double(total); - const double score = target - double(emitted[i]); - if (score > best_score) { - best_score = score; - best_idx = i; - } - } - ++emitted[best_idx]; - seq.emplace_back(filtered_ids[best_idx]); - } - if (seq.empty()) - seq = filtered_ids; - return seq; -} - - -std::vector MixedFilamentConfigPanel::build_local_z_preview_pass_heights(double nominal_layer_height, - double lower_bound, - double upper_bound, - double preferred_a_height, - double preferred_b_height, - int mix_b_percent, - int max_sublayers_limit) -{ - if (nominal_layer_height <= EPSILON) - return {}; - - const double base_height = nominal_layer_height; - const double lo = std::max(0.01, lower_bound); - const double hi = std::max(lo, upper_bound); - const size_t max_passes_limit = max_sublayers_limit >= 2 ? size_t(max_sublayers_limit) : size_t(0); - - auto fit_pass_heights_to_interval = [](std::vector &passes, double total_height, double local_lo, double local_hi) { - if (passes.empty() || total_height <= EPSILON) - return false; - - const auto within = [local_lo, local_hi](double value) { - return value >= local_lo - 1e-6 && value <= local_hi + 1e-6; - }; - - double sum = 0.0; - for (const double h : passes) - sum += h; - - double delta = total_height - sum; - if (std::abs(delta) > 1e-6) { - if (delta > 0.0) { - for (double &h : passes) { - if (delta <= 1e-6) - break; - const double room = local_hi - h; - if (room <= 1e-6) - continue; - const double take = std::min(room, delta); - h += take; - delta -= take; - } - } else { - for (auto it = passes.rbegin(); it != passes.rend() && delta < -1e-6; ++it) { - const double room = *it - local_lo; - if (room <= 1e-6) - continue; - const double take = std::min(room, -delta); - *it -= take; - delta += take; - } - } - } - - if (std::abs(delta) > 1e-6) - return false; - return std::all_of(passes.begin(), passes.end(), within); - }; - - auto build_uniform = [&fit_pass_heights_to_interval, base_height, lo, hi, max_passes_limit]() { - std::vector out; - size_t min_passes = size_t(std::max(1.0, std::ceil((base_height - EPSILON) / hi))); - size_t max_passes = size_t(std::max(1.0, std::floor((base_height + EPSILON) / lo))); - size_t pass_count = min_passes; - - if (max_passes >= min_passes) { - const double target_step = 0.5 * (lo + hi); - const size_t target_passes = - size_t(std::max(1.0, std::llround(base_height / std::max(target_step, EPSILON)))); - pass_count = std::clamp(target_passes, min_passes, max_passes); - } - - if (max_passes_limit > 0 && pass_count > max_passes_limit) - pass_count = max_passes_limit; - - if (pass_count == 1 && base_height >= 2.0 * lo - EPSILON && max_passes >= 2) - pass_count = 2; - - if (pass_count <= 1) { - out.emplace_back(base_height); - return out; - } - - out.assign(pass_count, base_height / double(pass_count)); - double accumulated = 0.0; - for (size_t i = 0; i + 1 < out.size(); ++i) - accumulated += out[i]; - out.back() = std::max(EPSILON, base_height - accumulated); - if (!fit_pass_heights_to_interval(out, base_height, lo, hi) && max_passes_limit == 0) { - out.assign(pass_count, base_height / double(pass_count)); - accumulated = 0.0; - for (size_t i = 0; i + 1 < out.size(); ++i) - accumulated += out[i]; - out.back() = std::max(EPSILON, base_height - accumulated); - } - return out; - }; - - auto build_alternating = [&build_uniform, &fit_pass_heights_to_interval, base_height, lo, hi, max_passes_limit](double gradient_h_a, double gradient_h_b) { - if (base_height < 2.0 * lo - EPSILON) - return std::vector{ base_height }; - - const double cycle_h = std::max(EPSILON, gradient_h_a + gradient_h_b); - const double ratio_a = std::clamp(gradient_h_a / cycle_h, 0.0, 1.0); - - size_t min_passes = size_t(std::max(2.0, std::ceil((base_height - EPSILON) / hi))); - if ((min_passes % 2) != 0) - ++min_passes; - - size_t max_passes = size_t(std::max(2.0, std::floor((base_height + EPSILON) / lo))); - if ((max_passes % 2) != 0) - --max_passes; - if (max_passes_limit > 0) { - size_t capped_limit = std::max(2, max_passes_limit); - if ((capped_limit % 2) != 0) - --capped_limit; - if (capped_limit >= 2) - max_passes = std::min(max_passes, capped_limit); - } - if (max_passes < 2) - return build_uniform(); - if (min_passes > max_passes) - min_passes = max_passes; - if (min_passes < 2) - min_passes = 2; - if ((min_passes % 2) != 0) - ++min_passes; - if (min_passes > max_passes) - return build_uniform(); - - const double target_step = 0.5 * (lo + hi); - size_t target_passes = - size_t(std::max(2.0, std::llround(base_height / std::max(target_step, EPSILON)))); - if ((target_passes % 2) != 0) { - const size_t round_up = (target_passes < max_passes) ? (target_passes + 1) : max_passes; - const size_t round_down = (target_passes > min_passes) ? (target_passes - 1) : min_passes; - if (round_up > max_passes) - target_passes = round_down; - else if (round_down < min_passes) - target_passes = round_up; - else - target_passes = ((round_up - target_passes) <= (target_passes - round_down)) ? round_up : round_down; - } - target_passes = std::clamp(target_passes, min_passes, max_passes); - - bool has_best = false; - std::vector best_passes; - double best_ratio_error = 0.0; - size_t best_pass_distance = 0; - double best_max_height = 0.0; - size_t best_pass_count = 0; - - for (size_t pass_count = min_passes; pass_count <= max_passes; pass_count += 2) { - const size_t pair_count = pass_count / 2; - if (pair_count == 0) - continue; - const double pair_h = base_height / double(pair_count); - - const double h_a_min = std::max(lo, pair_h - hi); - const double h_a_max = std::min(hi, pair_h - lo); - if (h_a_min > h_a_max + EPSILON) - continue; - - const double h_a = std::clamp(pair_h * ratio_a, h_a_min, h_a_max); - const double h_b = pair_h - h_a; - - std::vector out; - out.reserve(pass_count); - for (size_t pair_idx = 0; pair_idx < pair_count; ++pair_idx) { - out.emplace_back(h_a); - out.emplace_back(h_b); - } - if (!fit_pass_heights_to_interval(out, base_height, lo, hi)) - continue; - - const double ratio_actual = (h_a + h_b > EPSILON) ? (h_a / (h_a + h_b)) : 0.5; - const double ratio_error = std::abs(ratio_actual - ratio_a); - const size_t pass_distance = - (pass_count > target_passes) ? (pass_count - target_passes) : (target_passes - pass_count); - const double max_height = std::max(h_a, h_b); - - const bool better_ratio = !has_best || (ratio_error + 1e-6 < best_ratio_error); - const bool similar_ratio = has_best && std::abs(ratio_error - best_ratio_error) <= 1e-6; - const bool better_distance = similar_ratio && (pass_distance < best_pass_distance); - const bool similar_distance = similar_ratio && (pass_distance == best_pass_distance); - const bool better_max_height = similar_distance && (max_height + 1e-6 < best_max_height); - const bool similar_max_height = similar_distance && std::abs(max_height - best_max_height) <= 1e-6; - const bool better_pass_count = similar_max_height && (pass_count > best_pass_count); - - if (better_ratio || better_distance || better_max_height || better_pass_count) { - has_best = true; - best_passes = std::move(out); - best_ratio_error = ratio_error; - best_pass_distance = pass_distance; - best_max_height = max_height; - best_pass_count = pass_count; - } - } - - return has_best ? best_passes : build_uniform(); - }; - - if (preferred_a_height > EPSILON || preferred_b_height > EPSILON) { - std::vector cadence_unit; - if (preferred_a_height > EPSILON) - cadence_unit.push_back(std::clamp(preferred_a_height, lo, hi)); - if (preferred_b_height > EPSILON) - cadence_unit.push_back(std::clamp(preferred_b_height, lo, hi)); - - if (!cadence_unit.empty()) { - std::vector out; - out.reserve(size_t(std::ceil(base_height / lo)) + 2); - - double z_used = 0.0; - size_t idx = 0; - size_t guard = 0; - while (z_used + cadence_unit[idx] < base_height - EPSILON && guard++ < 100000) { - out.push_back(cadence_unit[idx]); - z_used += cadence_unit[idx]; - idx = (idx + 1) % cadence_unit.size(); - } - - const double remainder = base_height - z_used; - if (remainder > EPSILON) - out.push_back(remainder); - - if (fit_pass_heights_to_interval(out, base_height, lo, hi) && - (max_passes_limit == 0 || out.size() <= max_passes_limit)) - return out; - } - - if (preferred_a_height > EPSILON && preferred_b_height > EPSILON) - return build_alternating(preferred_a_height, preferred_b_height); - return build_uniform(); - } - - const int mix_b = std::clamp(mix_b_percent, 0, 100); - const double pct_b = double(mix_b) / 100.0; - const double pct_a = 1.0 - pct_b; - const double gradient_h_a = lo + pct_a * (hi - lo); - const double gradient_h_b = lo + pct_b * (hi - lo); - return build_alternating(gradient_h_a, gradient_h_b); -} - -int MixedFilamentConfigPanel::effective_local_z_preview_mix_b_percent(const MixedFilament &mf, - const MixedFilamentPreviewSettings &preview_settings) -{ - return Slic3r::mixed_filament_effective_local_z_preview_mix_b_percent(mf, preview_settings); -} - -static double mixed_filament_reference_nozzle_mm(unsigned int component_a, - unsigned int component_b, - const std::vector &nozzle_diameters) -{ - std::vector samples; - samples.reserve(2); - - auto append_if_valid = [&samples, &nozzle_diameters](unsigned int component_id) { - if (component_id >= 1 && component_id <= nozzle_diameters.size()) - samples.emplace_back(std::max(0.05, nozzle_diameters[size_t(component_id - 1)])); - }; - - append_if_valid(component_a); - append_if_valid(component_b); - - if (samples.empty()) - return 0.4; - return std::accumulate(samples.begin(), samples.end(), 0.0) / double(samples.size()); -} - -static double mixed_filament_bias_limit_mm(const MixedFilament &mf, const std::vector &nozzle_diameters) -{ - const double reference_nozzle_mm = mixed_filament_reference_nozzle_mm(mf.component_a, mf.component_b, nozzle_diameters); - return MixedFilamentManager::max_pair_bias_mm(float(reference_nozzle_mm)); -} - -static float mixed_filament_single_surface_offset_value(const MixedFilament &mf, - const std::vector &nozzle_diameters) -{ - const double reference_nozzle_mm = mixed_filament_reference_nozzle_mm(mf.component_a, mf.component_b, nozzle_diameters); - return MixedFilamentManager::bias_ui_value_from_surface_offsets( - mf.component_a_surface_offset, - mf.component_b_surface_offset, - float(reference_nozzle_mm)); -} - -static std::pair mixed_filament_single_surface_offset_pair(const MixedFilament &mf, - float value, - const std::vector &nozzle_diameters) -{ - const double reference_nozzle_mm = mixed_filament_reference_nozzle_mm(mf.component_a, mf.component_b, nozzle_diameters); - return MixedFilamentManager::surface_offset_pair_from_signed_bias(value, float(reference_nozzle_mm)); -} - -static std::string mixed_filament_apparent_pair_summary(const MixedFilament &mf, - const MixedFilamentPreviewSettings &preview_settings, - const std::vector &nozzle_diameters, - bool bias_mode_enabled) -{ - if (!Slic3r::mixed_filament_supports_bias_apparent_color(mf, preview_settings, bias_mode_enabled)) - return {}; - - const int base_b = MixedFilamentConfigPanel::effective_local_z_preview_mix_b_percent(mf, preview_settings); - const int base_a = 100 - base_b; - const auto [apparent_a, apparent_b] = - Slic3r::mixed_filament_apparent_pair_percentages(mf, preview_settings, nozzle_diameters, bias_mode_enabled); - - if (std::abs(mf.component_a_surface_offset - mf.component_b_surface_offset) > 1e-4f && - (apparent_a != base_a || apparent_b != base_b)) { - std::ostringstream ss; - ss << '~' << apparent_a << '/' << apparent_b; - return ss.str(); - } - - std::ostringstream ss; - ss << apparent_a << "%/" << apparent_b << '%'; - return ss.str(); -} - - -static std::vector build_display_weighted_multi_sequence(const std::vector &ids, - const std::vector &weights, - size_t max_cycle_limit = 0) -{ - if (ids.empty()) - return {}; - - std::vector filtered_ids; - std::vector counts; - filtered_ids.reserve(ids.size()); - counts.reserve(ids.size()); - - const std::vector normalized = normalize_color_match_weights(weights, ids.size()); - for (size_t idx = 0; idx < ids.size(); ++idx) { - const int weight = idx < normalized.size() ? std::max(0, normalized[idx]) : 0; - if (weight <= 0) - continue; - filtered_ids.emplace_back(ids[idx]); - counts.emplace_back(weight); - } - if (filtered_ids.empty()) { - filtered_ids = ids; - counts.assign(ids.size(), 1); - } - - int g = 0; - for (const int count : counts) - g = std::gcd(g, std::max(1, count)); - if (g > 1) { - for (int &count : counts) - count = std::max(1, count / g); - } - - constexpr size_t k_max_cycle = 48; - const size_t effective_cycle_limit = - max_cycle_limit > 0 ? std::min(k_max_cycle, std::max(1, max_cycle_limit)) : k_max_cycle; - reduce_weight_counts_to_cycle_limit(counts, effective_cycle_limit); - - std::vector reduced_ids; - std::vector reduced_counts; - reduced_ids.reserve(filtered_ids.size()); - reduced_counts.reserve(counts.size()); - for (size_t idx = 0; idx < counts.size(); ++idx) { - if (counts[idx] <= 0) - continue; - reduced_ids.emplace_back(filtered_ids[idx]); - reduced_counts.emplace_back(counts[idx]); - } - if (reduced_ids.empty()) - return {}; - filtered_ids = std::move(reduced_ids); - counts = std::move(reduced_counts); - - const int total = std::accumulate(counts.begin(), counts.end(), 0); - if (total <= 0) - return std::vector(filtered_ids.begin(), filtered_ids.end()); - - const size_t cycle = size_t(total); - - std::vector sequence; - sequence.reserve(cycle); - std::vector emitted(counts.size(), 0); - for (size_t pos = 0; pos < cycle; ++pos) { - size_t best_idx = 0; - double best_score = -1e9; - for (size_t idx = 0; idx < counts.size(); ++idx) { - const double target = double(pos + 1) * double(counts[idx]) / double(total); - const double score = target - double(emitted[idx]); - if (score > best_score) { - best_score = score; - best_idx = idx; - } - } - ++emitted[best_idx]; - sequence.emplace_back(filtered_ids[best_idx]); - } - if (sequence.empty()) - sequence = filtered_ids; - return sequence; -} - -static std::string blend_display_color_from_sequence(const std::vector &colors, - size_t num_physical, - const std::vector &sequence, - const std::string &fallback) -{ - if (colors.empty() || sequence.empty() || num_physical == 0) - return fallback; - - std::vector counts(num_physical + 1, size_t(0)); - size_t total = 0; - for (const unsigned int id : sequence) { - if (id == 0 || id > num_physical) - continue; - ++counts[id]; - ++total; - } - if (total == 0) - return fallback; - - unsigned int first_id = 0; - for (size_t id = 1; id <= num_physical; ++id) { - if (counts[id] > 0) { - first_id = unsigned(id); - break; - } - } - if (first_id == 0 || first_id > colors.size()) - return fallback; - - std::string blended = colors[first_id - 1]; - int accumulated = int(counts[first_id]); - for (size_t id = size_t(first_id + 1); id <= num_physical; ++id) { - if (counts[id] == 0 || id > colors.size()) - continue; - blended = MixedFilamentManager::blend_color(blended, colors[id - 1], accumulated, int(counts[id])); - accumulated += int(counts[id]); - } - - return blended; -} - - -std::string MixedFilamentConfigPanel::summarize_sequence(const std::vector &seq) -{ - if (seq.empty()) return ""; - std::unordered_map counts; - for (unsigned int id : seq) counts[id]++; - std::vector> sorted; - for (auto &kv : counts) sorted.emplace_back(kv.second, kv.first); - std::sort(sorted.begin(), sorted.end(), std::greater<>()); - std::string out; - for (auto &p : sorted) { - if (!out.empty()) out += "/"; - out += format_preview_sequence_percent(p.first, int(seq.size())); - } - return out; -} - -std::string MixedFilamentConfigPanel::summarize_local_z_breakdown(const MixedFilament &mf, - const std::vector &weights, - const MixedFilamentPreviewSettings &preview_settings) -{ - const std::string normalized_pattern = MixedFilamentManager::normalize_manual_pattern(mf.manual_pattern); - if (!normalized_pattern.empty()) - return "Local-Z breakdown: manual pattern rows do not use pair decomposition."; - - if (mf.distribution_mode == int(MixedFilament::SameLayerPointillisme)) - return "Local-Z breakdown: same-layer mode does not use local-Z pair decomposition."; - - auto pair_name = [](unsigned int a, unsigned int b) { - std::ostringstream ss; - ss << 'F' << a << "+F" << b; - return ss.str(); - }; - auto pair_split = [](unsigned int a, unsigned int b, int weight_a, int weight_b) { - const int safe_a = std::max(0, weight_a); - const int safe_b = std::max(0, weight_b); - const int total = std::max(1, safe_a + safe_b); - const int pct_a = int(std::lround(100.0 * double(safe_a) / double(total))); - const int pct_b = std::max(0, 100 - pct_a); - - std::ostringstream ss; - ss << 'F' << a << "/F" << b << " " << safe_a << ':' << safe_b << " (" << pct_a << '/' << pct_b << ')'; - return ss.str(); - }; - auto cadence_entry = [&pair_name](unsigned int a, unsigned int b, int weight, int total) { - const int pct = int(std::lround(100.0 * double(std::max(0, weight)) / double(std::max(1, total)))); - std::ostringstream ss; - ss << pair_name(a, b) << ' ' << pct << '%'; - return ss.str(); - }; - - const std::vector ids = decode_gradient_ids(mf.gradient_component_ids); - if (preview_settings.local_z_mode && preview_settings.local_z_direct_multicolor && ids.size() >= 3) { - const std::vector normalized = normalize_gradient_weights(weights, ids.size()); - const size_t effective_sublayers = - mf.local_z_max_sublayers >= 2 ? size_t(std::max(2, mf.local_z_max_sublayers)) : ids.size(); - - std::ostringstream ss; - ss << "Local-Z direct multicolor solver: "; - for (size_t idx = 0; idx < ids.size(); ++idx) { - if (idx > 0) - ss << ", "; - const int pct = idx < normalized.size() ? normalized[idx] : 0; - ss << 'F' << ids[idx] << ' ' << pct << '%'; - } - ss << ".\nCarry-over error is distributed directly across all " << ids.size() - << " components instead of collapsing them into pair cadence."; - if (mf.local_z_max_sublayers >= 2) - ss << "\nEffective Local-Z cap: up to " << effective_sublayers << " sublayers per nominal layer."; - return ss.str(); - } - - if (ids.size() >= 4) { - const std::vector normalized = normalize_gradient_weights(weights, ids.size()); - const std::vector pair_tokens = { 1, 2 }; - const std::vector pair_weights = { - std::max(1, normalized[0] + normalized[1]), - std::max(1, normalized[2] + normalized[3]) - }; - const size_t max_pair_layers = - (preview_settings.local_z_mode && mf.local_z_max_sublayers >= 2) ? - std::max(1, size_t(mf.local_z_max_sublayers) / 2) : - size_t(0); - const std::vector uncapped_pair_sequence = build_weighted_multi_sequence(pair_tokens, pair_weights); - const std::vector effective_pair_sequence = - max_pair_layers > 0 ? build_weighted_multi_sequence(pair_tokens, pair_weights, max_pair_layers) : uncapped_pair_sequence; - const std::vector &pair_sequence = effective_pair_sequence.empty() ? uncapped_pair_sequence : effective_pair_sequence; - const int pair_ab_weight = int(std::count(pair_sequence.begin(), pair_sequence.end(), 1u)); - const int pair_cd_weight = int(std::count(pair_sequence.begin(), pair_sequence.end(), 2u)); - const int pair_total = std::max(1, int(pair_sequence.size())); - - std::ostringstream ss; - ss << "Local-Z layer cadence: " - << cadence_entry(ids[0], ids[1], pair_ab_weight, pair_total) - << ", " - << cadence_entry(ids[2], ids[3], pair_cd_weight, pair_total) - << ".\nPair splits: " - << pair_split(ids[0], ids[1], normalized[0], normalized[1]) - << ", " - << pair_split(ids[2], ids[3], normalized[2], normalized[3]) - << '.'; - if (!preview_settings.local_z_mode && mf.local_z_max_sublayers >= 2) - ss << "\nSaved row limit will apply when Local-Z dithering mode is enabled in print settings."; - if (preview_settings.local_z_mode && mf.local_z_max_sublayers >= 2) { - ss << "\nEffective Local-Z stack: " << (pair_total * 2) << " sublayers over " << pair_total << " pair layers"; - if (uncapped_pair_sequence.size() > pair_sequence.size()) - ss << " (uncapped " << (uncapped_pair_sequence.size() * 2) << ')'; - ss << '.'; - } - return ss.str(); - } - - if (ids.size() == 3) { - const std::vector normalized = normalize_gradient_weights(weights, ids.size()); - const std::vector pair_tokens = { 1, 2, 3 }; - const std::vector pair_weights = { - std::max(1, normalized[0] + normalized[1]), - std::max(1, normalized[0] + normalized[2]), - std::max(1, normalized[1] + normalized[2]) - }; - const size_t max_pair_layers = - (preview_settings.local_z_mode && mf.local_z_max_sublayers >= 2) ? - std::max(1, size_t(mf.local_z_max_sublayers) / 2) : - size_t(0); - const std::vector uncapped_pair_sequence = build_weighted_multi_sequence(pair_tokens, pair_weights); - const std::vector effective_pair_sequence = - max_pair_layers > 0 ? build_weighted_multi_sequence(pair_tokens, pair_weights, max_pair_layers) : uncapped_pair_sequence; - const std::vector &pair_sequence = effective_pair_sequence.empty() ? uncapped_pair_sequence : effective_pair_sequence; - const int pair_ab_weight = int(std::count(pair_sequence.begin(), pair_sequence.end(), 1u)); - const int pair_ac_weight = int(std::count(pair_sequence.begin(), pair_sequence.end(), 2u)); - const int pair_bc_weight = int(std::count(pair_sequence.begin(), pair_sequence.end(), 3u)); - const int pair_total = std::max(1, int(pair_sequence.size())); - - std::ostringstream ss; - ss << "Local-Z layer cadence: " - << cadence_entry(ids[0], ids[1], pair_ab_weight, pair_total) - << ", " - << cadence_entry(ids[0], ids[2], pair_ac_weight, pair_total) - << ", " - << cadence_entry(ids[1], ids[2], pair_bc_weight, pair_total) - << ".\nPair splits: " - << pair_split(ids[0], ids[1], normalized[0], normalized[1]) - << ", " - << pair_split(ids[0], ids[2], normalized[0], normalized[2]) - << ", " - << pair_split(ids[1], ids[2], normalized[1], normalized[2]) - << '.'; - if (!preview_settings.local_z_mode && mf.local_z_max_sublayers >= 2) - ss << "\nSaved row limit will apply when Local-Z dithering mode is enabled in print settings."; - if (preview_settings.local_z_mode && mf.local_z_max_sublayers >= 2) { - ss << "\nEffective Local-Z stack: " << (pair_total * 2) << " sublayers over " << pair_total << " pair layers"; - if (uncapped_pair_sequence.size() > pair_sequence.size()) - ss << " (uncapped " << (uncapped_pair_sequence.size() * 2) << ')'; - ss << '.'; - } - return ss.str(); - } - - if (mf.component_a >= 1 && mf.component_b >= 1 && mf.component_a != mf.component_b) { - const int pct_b = std::clamp(mf.mix_b_percent, 0, 100); - const int pct_a = 100 - pct_b; - std::ostringstream ss; - ss << "Local-Z pair split: requested F" << mf.component_a << "/F" << mf.component_b - << ' ' << pct_a << '/' << pct_b; - if (preview_settings.local_z_mode) { - const std::vector effective_passes = build_local_z_preview_pass_heights(preview_settings.nominal_layer_height, - preview_settings.mixed_lower_bound, - preview_settings.mixed_upper_bound, - preview_settings.preferred_a_height, - preview_settings.preferred_b_height, - mf.mix_b_percent, - 0); - if (!effective_passes.empty()) { - const int effective_pct_b = effective_local_z_preview_mix_b_percent(mf, preview_settings); - ss << ", effective " << (100 - effective_pct_b) << '/' << effective_pct_b - << " over " << effective_passes.size() << " sublayers"; - } - } - ss << '.'; - return ss.str(); - } - - return "Local-Z breakdown: unavailable."; -} - -std::string MixedFilamentConfigPanel::blend_from_sequence(const std::vector &colors, const std::vector &seq, const std::string &fallback) -{ - if (colors.empty() || seq.empty()) - return fallback; - - std::vector counts(colors.size() + 1, size_t(0)); - size_t total = 0; - for (const unsigned int id : seq) { - if (id == 0 || id > colors.size()) - continue; - ++counts[id]; - ++total; - } - if (total == 0) - return fallback; - - unsigned int first_id = 0; - for (size_t id = 1; id <= colors.size(); ++id) { - if (counts[id] > 0) { - first_id = unsigned(id); - break; - } - } - if (first_id == 0 || first_id > colors.size()) - return fallback; - - std::string blended = colors[first_id - 1]; - int acc = int(counts[first_id]); - for (size_t id = size_t(first_id + 1); id <= colors.size(); ++id) { - if (counts[id] == 0) - continue; - blended = MixedFilamentManager::blend_color(blended, colors[id - 1], acc, int(counts[id])); - acc += int(counts[id]); - } - - return blended; -} - -MixedFilamentConfigPanel::MixedFilamentConfigPanel(wxWindow *parent, - size_t mixed_id, - const MixedFilament &mf, - size_t num_physical, - const std::vector &physical_colors, - const std::vector &nozzle_diameters, - const std::vector &palette, - const MixedFilamentPreviewSettings &preview_settings, - bool bias_mode_enabled, - OnChangeFn on_change) - : wxPanel(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL | wxBORDER_NONE) - , m_mixed_id(mixed_id) - , m_mf(mf) - , m_num_physical(num_physical) - , m_physical_colors(physical_colors) - , m_nozzle_diameters(nozzle_diameters) - , m_palette(palette) - , m_preview_settings(preview_settings) - , m_bias_mode_enabled(bias_mode_enabled) - , m_selected_weight_state(std::make_shared>()) - , m_on_change(on_change) -{ - if (parent) - SetBackgroundColour(parent->GetBackgroundColour()); - else - SetBackgroundColour(wxGetApp().dark_mode() ? wxColour(52, 52, 56) : wxColour(255, 255, 255)); - build_ui(); -} - -void MixedFilamentConfigPanel::build_ui() -{ - const int gap = FromDIP(6); - const int compact_gap = std::max(FromDIP(2), gap / 3); - const bool is_dark = wxGetApp().dark_mode(); - const wxColour panel_bg = GetBackgroundColour().IsOk() ? GetBackgroundColour() : - (is_dark ? wxColour(52, 52, 56) : wxColour(255, 255, 255)); - SetBackgroundColour(panel_bg); - auto *root = new wxBoxSizer(wxVERTICAL); - - // Filament choices - wxArrayString filament_choices; - for (size_t i = 0; i < m_num_physical; ++i) - filament_choices.Add(wxString::Format("F%d", int(i + 1))); - wxArrayString optional_filament_choices; - optional_filament_choices.Add(_L("None")); - for (size_t i = 0; i < m_num_physical; ++i) - optional_filament_choices.Add(wxString::Format("F%d", int(i + 1))); - - const int component_a = std::clamp(int(m_mf.component_a), 1, int(m_num_physical)); - const int component_b = std::clamp(int(m_mf.component_b), 1, int(m_num_physical)); - - const std::vector initial_gradient_ids = decode_gradient_ids(m_mf.gradient_component_ids); - if (m_mf.distribution_mode == int(MixedFilament::SameLayerPointillisme)) { - m_mf.distribution_mode = initial_gradient_ids.size() >= 3 ? int(MixedFilament::LayerCycle) : int(MixedFilament::Simple); - m_mf.pointillism_all_filaments = false; - } - const int stored_distribution_mode = std::clamp(m_mf.distribution_mode, - int(MixedFilament::LayerCycle), - int(MixedFilament::Simple)); - const int row_distribution_mode = initial_gradient_ids.size() >= 3 ? - (stored_distribution_mode == int(MixedFilament::Simple) ? int(MixedFilament::LayerCycle) : stored_distribution_mode) : - int(MixedFilament::Simple); - m_mf.distribution_mode = row_distribution_mode; - const bool multi_gradient_row = row_distribution_mode != int(MixedFilament::Simple) && initial_gradient_ids.size() >= 3; - const int selection_c = initial_gradient_ids.size() >= 3 ? int(initial_gradient_ids[2]) : 0; - const int selection_d = initial_gradient_ids.size() >= 4 ? int(initial_gradient_ids[3]) : 0; - - // Hidden data controls used as backing state for swatch pickers. - m_choice_a = new wxChoice(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, filament_choices); - m_choice_b = new wxChoice(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, filament_choices); - m_choice_a->SetSelection(component_a - 1); - m_choice_b->SetSelection(component_b - 1); - m_choice_a->Hide(); - m_choice_b->Hide(); - if (multi_gradient_row) { - m_choice_c = new wxChoice(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, optional_filament_choices); - m_choice_c->SetSelection(std::clamp(selection_c, 0, int(m_num_physical))); - m_choice_c->Hide(); - if (initial_gradient_ids.size() >= 4) { - m_choice_d = new wxChoice(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, optional_filament_choices); - m_choice_d->SetSelection(std::clamp(selection_d, 0, int(m_num_physical))); - m_choice_d->Hide(); - } - } - - auto create_component_picker = [this, gap](wxPanel *&container_out, wxPanel *&swatch_out, wxStaticText *&label_out, const wxString &tooltip) { - const int inner_gap = std::max(FromDIP(1), gap / 4); - const bool local_is_dark = wxGetApp().dark_mode(); - const wxColour local_picker_bg = local_is_dark ? wxColour(64, 64, 70) : wxColour(255, 255, 255); - const wxColour local_picker_text = local_is_dark ? wxColour(230, 230, 230) : wxColour(32, 32, 32); - container_out = new wxPanel(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxBORDER_SIMPLE); - container_out->SetBackgroundColour(local_picker_bg); - const wxSize picker_size(FromDIP(38), FromDIP(22)); - container_out->SetMinSize(picker_size); - container_out->SetMaxSize(picker_size); - - auto *container_sizer = new wxBoxSizer(wxHORIZONTAL); - swatch_out = new wxPanel(container_out, wxID_ANY, wxDefaultPosition, wxSize(FromDIP(12), FromDIP(12)), wxBORDER_SIMPLE); - swatch_out->SetMinSize(wxSize(FromDIP(12), FromDIP(12))); - swatch_out->SetToolTip(tooltip); - label_out = new wxStaticText(container_out, wxID_ANY, wxEmptyString); - label_out->SetForegroundColour(local_picker_text); - label_out->SetToolTip(tooltip); - - auto *content_sizer = new wxBoxSizer(wxHORIZONTAL); - content_sizer->Add(swatch_out, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, inner_gap); - content_sizer->Add(label_out, 0, wxALIGN_CENTER_VERTICAL); - container_sizer->AddStretchSpacer(1); - container_sizer->Add(content_sizer, 0, wxALIGN_CENTER_VERTICAL); - container_sizer->AddStretchSpacer(1); - container_out->SetSizer(container_sizer); - container_out->SetToolTip(tooltip); - container_out->SetCursor(wxCursor(wxCURSOR_HAND)); - swatch_out->SetCursor(wxCursor(wxCURSOR_HAND)); - label_out->SetCursor(wxCursor(wxCURSOR_HAND)); - }; - - create_component_picker(m_picker_a_container, m_picker_a_swatch, m_picker_a_label, _L("Click to choose a physical filament color")); - create_component_picker(m_picker_b_container, m_picker_b_swatch, m_picker_b_label, _L("Click to choose a physical filament color")); - if (m_choice_c) - create_component_picker(m_picker_c_container, m_picker_c_swatch, m_picker_c_label, _L("Click to choose a physical filament color")); - if (m_choice_d) - create_component_picker(m_picker_d_container, m_picker_d_swatch, m_picker_d_label, _L("Click to choose a physical filament color")); - update_component_picker_visuals(); - - // Check for pattern mode - const std::string normalized_pattern = MixedFilamentManager::normalize_manual_pattern(m_mf.manual_pattern); - const bool pattern_row_mode = !normalized_pattern.empty(); - - auto *picker_row = new wxBoxSizer(wxHORIZONTAL); - if (!pattern_row_mode) { - auto add_picker = [this, picker_row, gap](wxPanel *container, bool &first_picker) { - if (!container) - return; - if (!first_picker) - picker_row->Add(new wxStaticText(this, wxID_ANY, "+"), 0, wxALIGN_CENTER_VERTICAL | wxLEFT | wxRIGHT, std::max(FromDIP(2), gap / 2)); - picker_row->Add(container, 0, wxALIGN_CENTER_VERTICAL); - first_picker = false; - }; - - bool first_picker = true; - add_picker(m_picker_a_container, first_picker); - add_picker(m_picker_b_container, first_picker); - add_picker(m_picker_c_container, first_picker); - add_picker(m_picker_d_container, first_picker); - } else { - if (m_picker_a_container) m_picker_a_container->Hide(); - if (m_picker_b_container) m_picker_b_container->Hide(); - if (m_picker_c_container) m_picker_c_container->Hide(); - if (m_picker_d_container) m_picker_d_container->Hide(); - } - root->Add(picker_row, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, gap); - - // Pattern controls (if pattern mode) - if (pattern_row_mode) { - auto *pattern_row = new wxBoxSizer(wxHORIZONTAL); - auto *pattern_label = new wxStaticText(this, wxID_ANY, _L("Pattern")); - pattern_label->SetForegroundColour(is_dark ? wxColour(236, 236, 236) : wxColour(20, 20, 20)); - pattern_row->Add(pattern_label, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, gap); - m_pattern_ctrl = new wxTextCtrl(this, wxID_ANY, from_u8(normalized_pattern), wxDefaultPosition, - wxSize(FromDIP(200), -1), wxTE_PROCESS_ENTER); - m_pattern_ctrl->SetToolTip(_L("Manual repeating pattern. Digits 1-9 for filament IDs 1-9. " - "Use [N] for IDs >= 10 (e.g. [12]). " - "Use commas to define per-perimeter groups, e.g. 12,21. " - "Example: 11112222, 12,21, or 1234.")); - pattern_row->Add(m_pattern_ctrl, 1, wxALIGN_CENTER_VERTICAL); - root->Add(pattern_row, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, gap); - - auto *quick_buttons = new wxBoxSizer(wxHORIZONTAL); - for (size_t fid = 0; fid < m_num_physical; ++fid) { - wxButton *btn = new wxButton(this, wxID_ANY, wxString::Format("%d", int(fid + 1)), - wxDefaultPosition, wxSize(FromDIP(24), FromDIP(22)), wxBU_EXACTFIT); - const wxColour chip_color = (fid < m_palette.size()) ? m_palette[fid] : wxColour("#26A69A"); - btn->SetBackgroundColour(chip_color); - btn->SetToolTip(wxString::Format(_L("Append filament %d to pattern"), int(fid + 1))); - quick_buttons->Add(btn, 0, wxRIGHT, FromDIP(4)); - m_pattern_quick_buttons.emplace_back(btn); - } - auto *filaments_label = new wxStaticText(this, wxID_ANY, _L("Filaments")); - filaments_label->SetForegroundColour(is_dark ? wxColour(236, 236, 236) : wxColour(20, 20, 20)); - picker_row->Add(filaments_label, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, std::max(FromDIP(3), gap / 2)); - picker_row->Add(quick_buttons, 0, wxALIGN_CENTER_VERTICAL); - } else { - // Blend selector for non-pattern mode - const bool simple_mode = row_distribution_mode == int(MixedFilament::Simple); - std::vector selected_gradient_ids = simple_mode ? std::vector() : initial_gradient_ids; - if (selected_gradient_ids.size() < 3) selected_gradient_ids.clear(); - if (selected_gradient_ids.empty()) { - selected_gradient_ids.emplace_back(unsigned(component_a)); - if (component_b != component_a) selected_gradient_ids.emplace_back(unsigned(component_b)); - } - const bool multi_gradient_mode = selected_gradient_ids.size() >= 3; - *m_selected_weight_state = normalize_gradient_weights( - decode_gradient_weights(m_mf.gradient_component_weights, selected_gradient_ids.size()), - selected_gradient_ids.size()); - - wxColour color_a = (component_a >= 1 && component_a <= int(m_palette.size())) ? m_palette[component_a - 1] : wxColour("#26A69A"); - wxColour color_b = (component_b >= 1 && component_b <= int(m_palette.size())) ? m_palette[component_b - 1] : wxColour("#26A69A"); - m_blend_selector = new MixedGradientSelector(this, color_a, color_b, std::clamp(m_mf.mix_b_percent, 0, 100)); - m_blend_selector->SetBackgroundColour(panel_bg); - m_blend_label = nullptr; - picker_row->AddSpacer(gap); - picker_row->Add(m_blend_selector, 1, wxEXPAND | wxALIGN_CENTER_VERTICAL | wxLEFT, gap); - - if (m_blend_selector) { - std::vector corner_colors; - corner_colors.reserve(selected_gradient_ids.size()); - for (const unsigned int id : selected_gradient_ids) { - if (id >= 1 && id <= m_palette.size()) - corner_colors.emplace_back(m_palette[id - 1]); - } - if (!simple_mode && corner_colors.size() >= 3) - m_blend_selector->set_multi_preview(corner_colors, *m_selected_weight_state); - } - } - - // Preview - auto *preview_row = new wxBoxSizer(wxHORIZONTAL); - m_mix_preview = new MixedMixPreview(this); - m_mix_preview->SetBackgroundColour(panel_bg); - preview_row->Add(m_mix_preview, 1, wxEXPAND | wxALIGN_CENTER_VERTICAL | wxRIGHT, compact_gap); - - auto *bias_controls = new wxBoxSizer(wxHORIZONTAL); - const float initial_surface_offset_value = mixed_filament_single_surface_offset_value(m_mf, m_nozzle_diameters); - const double initial_bias_limit = mixed_filament_bias_limit_mm(m_mf, m_nozzle_diameters); - const wxString bias_tooltip = - _L("Positive bias recesses the second filament in the pair; negative bias recesses the first filament.\n\n" - "The color chip shows which filament the current value affects.\n\n" - "Grouped wall patterns and Local-Z dithering ignore it."); - - auto *surface_offset_label = new wxStaticText(this, wxID_ANY, _L("Bias")); - surface_offset_label->SetForegroundColour(is_dark ? wxColour(236, 236, 236) : wxColour(20, 20, 20)); - surface_offset_label->SetToolTip(bias_tooltip); - bias_controls->Add(surface_offset_label, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, compact_gap); - - create_component_picker(m_surface_offset_target_container, - m_surface_offset_target_swatch, - m_surface_offset_target_label, - bias_tooltip); - if (m_surface_offset_target_container) - m_surface_offset_target_container->SetCursor(wxCursor(wxCURSOR_ARROW)); - if (m_surface_offset_target_swatch) - m_surface_offset_target_swatch->SetCursor(wxCursor(wxCURSOR_ARROW)); - if (m_surface_offset_target_label) - m_surface_offset_target_label->SetCursor(wxCursor(wxCURSOR_ARROW)); - bias_controls->Add(m_surface_offset_target_container, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, compact_gap); - - m_surface_offset_spin = new wxSpinCtrlDouble(this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize(FromDIP(58), -1), - wxSP_ARROW_KEYS | wxALIGN_RIGHT | wxTE_PROCESS_ENTER, - -initial_bias_limit, initial_bias_limit, - std::clamp(double(initial_surface_offset_value), -initial_bias_limit, initial_bias_limit), 0.001); - m_surface_offset_spin->SetDigits(3); - m_surface_offset_spin->SetToolTip(bias_tooltip); - bias_controls->Add(m_surface_offset_spin, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, compact_gap); - - auto *surface_offset_units = new wxStaticText(this, wxID_ANY, _L("mm")); - surface_offset_units->SetForegroundColour(is_dark ? wxColour(210, 210, 210) : wxColour(72, 72, 72)); - surface_offset_units->SetToolTip(bias_tooltip); - bias_controls->Add(surface_offset_units, 0, wxALIGN_CENTER_VERTICAL); - if (m_bias_mode_enabled) - preview_row->Add(bias_controls, 0, wxALIGN_CENTER_VERTICAL); - else { - surface_offset_label->Hide(); - if (m_surface_offset_target_container) - m_surface_offset_target_container->Hide(); - if (m_surface_offset_spin) - m_surface_offset_spin->Hide(); - surface_offset_units->Hide(); - } - root->Add(preview_row, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, gap); - - if (m_bias_mode_enabled) { - const auto initial_surface_offset_pair = - mixed_filament_single_surface_offset_pair(m_mf, initial_surface_offset_value, m_nozzle_diameters); - m_mf.component_a_surface_offset = initial_surface_offset_pair.first; - m_mf.component_b_surface_offset = initial_surface_offset_pair.second; - } - - const bool initial_component_surface_offsets_supported = m_bias_mode_enabled && - !pattern_row_mode && - row_distribution_mode != int(MixedFilament::SameLayerPointillisme) && - !m_preview_settings.local_z_mode; - if (m_surface_offset_spin) - m_surface_offset_spin->Enable(initial_component_surface_offsets_supported); - - const bool local_z_limit_supported = multi_gradient_row && - row_distribution_mode != int(MixedFilament::SameLayerPointillisme); - if (local_z_limit_supported) { - auto *local_z_limit_row = new wxBoxSizer(wxHORIZONTAL); - m_local_z_limit_checkbox = new wxCheckBox(this, wxID_ANY, _L("Limit Local-Z")); - m_local_z_limit_checkbox->SetValue(m_mf.local_z_max_sublayers >= 2); - m_local_z_limit_checkbox->SetForegroundColour(is_dark ? wxColour(236, 236, 236) : wxColour(20, 20, 20)); - m_local_z_limit_checkbox->SetToolTip( - _L("Store a per-color Local-Z cadence cap. It applies when Local-Z dithering mode is enabled in print settings.")); - local_z_limit_row->Add(m_local_z_limit_checkbox, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, gap); - - auto *local_z_limit_label = new wxStaticText(this, wxID_ANY, _L("Max sublayers")); - local_z_limit_label->SetForegroundColour(is_dark ? wxColour(236, 236, 236) : wxColour(20, 20, 20)); - local_z_limit_row->Add(local_z_limit_label, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, std::max(FromDIP(3), gap / 2)); - - const int initial_local_z_limit = std::max(2, m_mf.local_z_max_sublayers > 0 ? m_mf.local_z_max_sublayers : 6); - m_local_z_limit_spin = new wxSpinCtrl(this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize(FromDIP(72), -1), - wxSP_ARROW_KEYS | wxALIGN_RIGHT | wxTE_PROCESS_ENTER, 2, 999, initial_local_z_limit); - m_local_z_limit_spin->SetToolTip( - _L("Maximum number of Local-Z sublayers this color may use before its cadence repeats.")); - local_z_limit_row->Add(m_local_z_limit_spin, 0, wxALIGN_CENTER_VERTICAL); - - const bool enable_local_z_limit_controls = m_local_z_limit_checkbox->GetValue(); - m_local_z_limit_spin->Enable(enable_local_z_limit_controls); - root->Add(local_z_limit_row, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, gap); - } - - m_breakdown_label = new wxStaticText(this, wxID_ANY, wxEmptyString); - m_breakdown_label->SetForegroundColour(is_dark ? wxColour(210, 210, 210) : wxColour(72, 72, 72)); - m_breakdown_label->Wrap(FromDIP(360)); - root->Add(m_breakdown_label, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, gap); - - // Bind events - auto apply_changes = [this]() { - m_has_changes = true; - - double surface_offset_value = 0.0; - if (m_surface_offset_spin) { - surface_offset_value = m_surface_offset_spin->GetValue(); -#if !defined(wxHAS_NATIVE_SPINCTRLDOUBLE) - if (wxTextCtrl *text = m_surface_offset_spin->GetText()) { - double parsed_value = 0.0; - if (text->GetValue().ToDouble(&parsed_value)) - surface_offset_value = parsed_value; - } -#endif - } - - int a = std::clamp(m_choice_a->GetSelection() + 1, 1, int(m_num_physical)); - int b = std::clamp(m_choice_b->GetSelection() + 1, 1, int(m_num_physical)); - if (a == b && m_num_physical > 1) { - b = (a == int(m_num_physical)) ? 1 : a + 1; - m_choice_b->SetSelection(b - 1); - } - update_component_picker_visuals(); - - if (m_local_z_limit_spin) - m_local_z_limit_spin->Enable(m_local_z_limit_checkbox != nullptr && - m_local_z_limit_checkbox->GetValue()); - - m_mf.component_a = unsigned(a); - m_mf.component_b = unsigned(b); - if (m_bias_mode_enabled) { - const double bias_limit = mixed_filament_bias_limit_mm(m_mf, m_nozzle_diameters); - const float clamped_surface_offset_value = std::clamp(float(surface_offset_value), -float(bias_limit), float(bias_limit)); - const auto surface_offset_pair = - mixed_filament_single_surface_offset_pair(m_mf, clamped_surface_offset_value, m_nozzle_diameters); - m_mf.component_a_surface_offset = surface_offset_pair.first; - m_mf.component_b_surface_offset = surface_offset_pair.second; - if (m_surface_offset_spin) - m_surface_offset_spin->SetValue(clamped_surface_offset_value); - } - m_mf.local_z_max_sublayers = - (m_local_z_limit_checkbox != nullptr && m_local_z_limit_checkbox->GetValue() && m_local_z_limit_spin != nullptr) ? - std::max(2, m_local_z_limit_spin->GetValue()) : - 0; - - bool simple_mode = true; - bool same_layer_mode = false; - int preview_mix_b_percent = std::clamp(m_mf.mix_b_percent, 0, 100); - std::vector preview_sequence; - - if (m_pattern_ctrl) { - m_mf.distribution_mode = int(MixedFilament::Simple); - std::string normalized = MixedFilamentManager::normalize_manual_pattern(into_u8(m_pattern_ctrl->GetValue())); - if (normalized.empty()) normalized = "12"; - if (into_u8(m_pattern_ctrl->GetValue()) != normalized) - m_pattern_ctrl->ChangeValue(from_u8(normalized)); - m_mf.manual_pattern = normalized; - m_mf.mix_b_percent = MixedFilamentManager::mix_percent_from_manual_pattern(normalized); - m_mf.pointillism_all_filaments = false; - m_mf.gradient_component_ids.clear(); - m_mf.gradient_component_weights.clear(); - preview_sequence = decode_manual_pattern_ids(m_mf.manual_pattern, - m_mf.component_a, - m_mf.component_b, - m_num_physical, - m_preview_settings.wall_loops); - } else { - std::vector selected_ids; - selected_ids.reserve(4); - auto add_unique = [&selected_ids](unsigned int id) { - if (id == 0) return; - if (std::find(selected_ids.begin(), selected_ids.end(), id) == selected_ids.end()) - selected_ids.emplace_back(id); - }; - add_unique(unsigned(a)); - add_unique(unsigned(b)); - if (m_choice_c && m_choice_c->GetSelection() > 0) - add_unique(unsigned(m_choice_c->GetSelection())); - if (m_choice_d && m_choice_d->GetSelection() > 0) - add_unique(unsigned(m_choice_d->GetSelection())); - const bool multi_gradient_mode = selected_ids.size() >= 3; - m_mf.distribution_mode = multi_gradient_mode ? int(MixedFilament::LayerCycle) : int(MixedFilament::Simple); - simple_mode = m_mf.distribution_mode == int(MixedFilament::Simple); - m_mf.mix_b_percent = std::clamp(m_blend_selector ? m_blend_selector->value() : 50, 0, 100); - m_mf.manual_pattern.clear(); - m_mf.pointillism_all_filaments = false; - - const wxColour color_a = (a >= 1 && a <= int(m_palette.size())) ? m_palette[size_t(a - 1)] : wxColour("#26A69A"); - const wxColour color_b = (b >= 1 && b <= int(m_palette.size())) ? m_palette[size_t(b - 1)] : wxColour("#26A69A"); - if (m_blend_selector) { - if (!simple_mode && multi_gradient_mode) { - std::vector corner_colors; - corner_colors.reserve(selected_ids.size()); - for (const unsigned int id : selected_ids) { - if (id >= 1 && id <= m_palette.size()) - corner_colors.emplace_back(m_palette[id - 1]); - } - if (corner_colors.size() >= 3) - m_blend_selector->set_multi_preview(corner_colors, *m_selected_weight_state); - else - m_blend_selector->set_colors(color_a, color_b); - } else { - m_blend_selector->set_colors(color_a, color_b); - } - } - - if (multi_gradient_mode) { - const std::vector decoded_weights = - decode_gradient_weights(m_mf.gradient_component_weights, selected_ids.size()); - if (m_selected_weight_state->size() != selected_ids.size()) - *m_selected_weight_state = decoded_weights; - *m_selected_weight_state = normalize_gradient_weights(*m_selected_weight_state, selected_ids.size()); - m_mf.gradient_component_ids = encode_gradient_ids(selected_ids); - m_mf.gradient_component_weights = encode_gradient_weights(*m_selected_weight_state); - preview_sequence = build_weighted_multi_sequence(selected_ids, *m_selected_weight_state); - } else { - m_mf.gradient_component_ids.clear(); - m_mf.gradient_component_weights.clear(); - preview_mix_b_percent = effective_local_z_preview_mix_b_percent(m_mf, m_preview_settings); - preview_sequence = build_weighted_pair_sequence(m_mf.component_a, m_mf.component_b, preview_mix_b_percent, same_layer_mode); - } - } - m_mf.custom = true; - - const std::vector selected_gradient_ids = decode_gradient_ids(m_mf.gradient_component_ids); - const bool component_surface_offsets_supported = m_bias_mode_enabled && - (m_pattern_ctrl == nullptr) && - !same_layer_mode && - !m_preview_settings.local_z_mode; - if (m_surface_offset_spin) - m_surface_offset_spin->Enable(component_surface_offsets_supported); - if (preview_sequence.empty()) - preview_sequence = build_weighted_pair_sequence(m_mf.component_a, m_mf.component_b, preview_mix_b_percent, same_layer_mode); - - if (m_blend_selector && selected_gradient_ids.size() >= 3) { - std::vector corner_colors; - corner_colors.reserve(selected_gradient_ids.size()); - for (const unsigned int id : selected_gradient_ids) { - if (id >= 1 && id <= m_palette.size()) - corner_colors.emplace_back(m_palette[id - 1]); - } - if (corner_colors.size() >= 3) - m_blend_selector->set_multi_preview(corner_colors, *m_selected_weight_state); - } - - if (Slic3r::mixed_filament_supports_bias_apparent_color(m_mf, m_preview_settings, m_bias_mode_enabled) && - m_mf.component_a >= 1 && m_mf.component_b >= 1 && - m_mf.component_a <= m_physical_colors.size() && m_mf.component_b <= m_physical_colors.size()) { - const auto [apparent_pct_a, apparent_pct_b] = - Slic3r::mixed_filament_apparent_pair_percentages(m_mf, m_preview_settings, m_nozzle_diameters, m_bias_mode_enabled); - m_mf.display_color = MixedFilamentManager::blend_color( - m_physical_colors[size_t(m_mf.component_a - 1)], - m_physical_colors[size_t(m_mf.component_b - 1)], - apparent_pct_a, - apparent_pct_b); - } else if (selected_gradient_ids.size() >= 3 || !preview_sequence.empty()) { - m_mf.display_color = blend_from_sequence(m_physical_colors, preview_sequence, "#26A69A"); - if (m_blend_label) { - if (selected_gradient_ids.size() >= 3) { - m_blend_label->SetLabel(wxString::Format(_L("%d-color layer cycle"), int(selected_gradient_ids.size()))); - } else { - m_blend_label->SetLabel(wxString::Format(simple_mode ? _L("Simple %d%%/%d%%") : _L("%d%%/%d%%"), - 100 - preview_mix_b_percent, preview_mix_b_percent)); - } - } - } else { - m_mf.display_color = MixedFilamentManager::blend_color( - m_physical_colors[size_t(a - 1)], m_physical_colors[size_t(b - 1)], - 100 - preview_mix_b_percent, preview_mix_b_percent); - if (m_blend_label) - m_blend_label->SetLabel(wxString::Format(simple_mode ? _L("Simple %d%%/%d%%") : _L("%d%%/%d%%"), - 100 - preview_mix_b_percent, preview_mix_b_percent)); - } - - if (m_mix_preview) { - const std::string bias_summary = - mixed_filament_apparent_pair_summary(m_mf, m_preview_settings, m_nozzle_diameters, m_bias_mode_enabled); - const std::string summary = bias_summary.empty() ? summarize_sequence(preview_sequence) : bias_summary; - std::vector preview_surface_offsets(m_palette.size() + 1, 0.0); - if (m_bias_mode_enabled && m_mf.component_a >= 1 && m_mf.component_a < preview_surface_offsets.size()) - preview_surface_offsets[m_mf.component_a] = double(m_mf.component_a_surface_offset); - if (m_bias_mode_enabled && m_mf.component_b >= 1 && m_mf.component_b < preview_surface_offsets.size()) - preview_surface_offsets[m_mf.component_b] = double(m_mf.component_b_surface_offset); - m_mix_preview->set_data(m_palette, preview_sequence, same_layer_mode, preview_surface_offsets, wxColour(m_mf.display_color), - _L("Preview"), summary.empty() ? wxString() : from_u8(summary)); - } - update_local_z_breakdown(); - if (m_swatch) { - m_swatch->SetBackgroundColour(wxColour(m_mf.display_color)); - m_swatch->Refresh(); - } - if (m_on_change) - m_on_change(m_mf); - }; - - auto make_color_chip_bitmap = [this](const wxColour &color) { - const int chip_size = FromDIP(14); - wxBitmap bmp(chip_size, chip_size); - wxMemoryDC dc(bmp); - dc.SetBackground(wxBrush(wxColour(255, 255, 255))); - dc.Clear(); - dc.SetPen(wxPen(wxColour(120, 120, 120))); - dc.SetBrush(wxBrush(color)); - dc.DrawRectangle(0, 0, chip_size, chip_size); - dc.SelectObject(wxNullBitmap); - return bmp; - }; - - auto bind_component_picker_popup = [this, apply_changes, make_color_chip_bitmap](wxWindow *target, wxChoice *backing_choice) { - if (!target || !backing_choice) - return; - - target->Bind(wxEVT_LEFT_UP, [this, apply_changes, make_color_chip_bitmap, backing_choice](wxMouseEvent &) { - if (m_num_physical == 0) - return; - - const bool allow_none = backing_choice->GetCount() == unsigned(m_num_physical + 1); - wxMenu menu; - std::vector item_ids; - item_ids.reserve(m_num_physical + (allow_none ? 1 : 0)); - if (allow_none) { - const int item_id = wxWindow::NewControlId(); - item_ids.emplace_back(item_id); - menu.Append(item_id, backing_choice->GetSelection() == 0 ? _L("None (Selected)") : _L("None")); - } - for (size_t i = 0; i < m_num_physical; ++i) { - const int item_id = wxWindow::NewControlId(); - item_ids.emplace_back(item_id); - const int selection_index = allow_none ? int(i + 1) : int(i); - const bool is_selected = selection_index == backing_choice->GetSelection(); - const wxString item_label = wxString::Format("F%d%s", int(i + 1), is_selected ? " (Selected)" : ""); - auto *menu_item = new wxMenuItem(&menu, item_id, item_label, wxEmptyString, wxITEM_NORMAL); - const wxColour item_color = (i < m_palette.size()) ? m_palette[i] : wxColour("#26A69A"); - menu_item->SetBitmap(make_color_chip_bitmap(item_color)); - menu.Append(menu_item); - } - - menu.Bind(wxEVT_COMMAND_MENU_SELECTED, [apply_changes, backing_choice, item_ids](wxCommandEvent &evt) { - const auto it = std::find(item_ids.begin(), item_ids.end(), evt.GetId()); - if (it == item_ids.end()) - return; - const int selection = int(std::distance(item_ids.begin(), it)); - backing_choice->SetSelection(selection); - apply_changes(); - }); - PopupMenu(&menu); - }); - }; - - bind_component_picker_popup(m_picker_a_container, m_choice_a); - bind_component_picker_popup(m_picker_a_swatch, m_choice_a); - bind_component_picker_popup(m_picker_a_label, m_choice_a); - bind_component_picker_popup(m_picker_b_container, m_choice_b); - bind_component_picker_popup(m_picker_b_swatch, m_choice_b); - bind_component_picker_popup(m_picker_b_label, m_choice_b); - bind_component_picker_popup(m_picker_c_container, m_choice_c); - bind_component_picker_popup(m_picker_c_swatch, m_choice_c); - bind_component_picker_popup(m_picker_c_label, m_choice_c); - bind_component_picker_popup(m_picker_d_container, m_choice_d); - bind_component_picker_popup(m_picker_d_swatch, m_choice_d); - bind_component_picker_popup(m_picker_d_label, m_choice_d); - - m_choice_a->Bind(wxEVT_CHOICE, [apply_changes](wxCommandEvent&) { apply_changes(); }); - m_choice_b->Bind(wxEVT_CHOICE, [apply_changes](wxCommandEvent&) { apply_changes(); }); - if (m_choice_c) - m_choice_c->Bind(wxEVT_CHOICE, [apply_changes](wxCommandEvent&) { apply_changes(); }); - if (m_choice_d) - m_choice_d->Bind(wxEVT_CHOICE, [apply_changes](wxCommandEvent&) { apply_changes(); }); - if (m_blend_selector) - m_blend_selector->Bind(wxEVT_SLIDER, [apply_changes](wxCommandEvent&) { apply_changes(); }); - if (m_local_z_limit_checkbox) - m_local_z_limit_checkbox->Bind(wxEVT_CHECKBOX, [apply_changes](wxCommandEvent &) { apply_changes(); }); - if (m_local_z_limit_spin) { - m_local_z_limit_spin->Bind(wxEVT_SPINCTRL, [apply_changes](wxCommandEvent &) { apply_changes(); }); - m_local_z_limit_spin->Bind(wxEVT_TEXT_ENTER, [apply_changes](wxCommandEvent &) { apply_changes(); }); - m_local_z_limit_spin->Bind(wxEVT_KILL_FOCUS, [apply_changes](wxFocusEvent &evt) { - apply_changes(); - evt.Skip(); - }); - } - if (m_surface_offset_spin) { - m_surface_offset_spin->Bind(wxEVT_SPINCTRLDOUBLE, [apply_changes](wxSpinDoubleEvent &) { apply_changes(); }); - m_surface_offset_spin->Bind(wxEVT_TEXT_ENTER, [apply_changes](wxCommandEvent &) { apply_changes(); }); - m_surface_offset_spin->Bind(wxEVT_KILL_FOCUS, [apply_changes](wxFocusEvent &evt) { - apply_changes(); - evt.Skip(); - }); - } - - if (m_blend_selector) { - m_blend_selector->Bind(wxEVT_BUTTON, [this, apply_changes](wxCommandEvent&) { - if (!m_blend_selector->is_multi_mode()) return; - std::vector selected_ids; - auto add_unique = [&selected_ids](unsigned int id) { if (id > 0 && std::find(selected_ids.begin(), selected_ids.end(), id) == selected_ids.end()) selected_ids.emplace_back(id); }; - add_unique(unsigned(std::clamp(m_choice_a ? (m_choice_a->GetSelection() + 1) : 0, 1, int(m_num_physical)))); - add_unique(unsigned(std::clamp(m_choice_b ? (m_choice_b->GetSelection() + 1) : 0, 1, int(m_num_physical)))); - if (m_choice_c && m_choice_c->GetSelection() > 0) add_unique(unsigned(m_choice_c->GetSelection())); - if (m_choice_d && m_choice_d->GetSelection() > 0) add_unique(unsigned(m_choice_d->GetSelection())); - if (selected_ids.size() < 3) return; - const std::vector initial_weights = normalize_gradient_weights(*m_selected_weight_state, selected_ids.size()); - MixedGradientWeightsDialog dlg(this, selected_ids, m_palette, initial_weights); - if (dlg.ShowModal() != wxID_OK) return; - *m_selected_weight_state = dlg.normalized_weights(); - apply_changes(); - }); - } - - if (m_pattern_ctrl) { - auto append_pattern_token = [this](int filament_id) { - if (!m_pattern_ctrl || filament_id <= 0) return; - if (filament_id >= 10) - m_pattern_ctrl->AppendText(wxString::Format("[%d]", filament_id)); - else - m_pattern_ctrl->AppendText(wxString::Format("%d", filament_id)); - }; - m_pattern_ctrl->Bind(wxEVT_TEXT_ENTER, [apply_changes](wxCommandEvent&) { apply_changes(); }); - m_pattern_ctrl->Bind(wxEVT_KILL_FOCUS, [apply_changes](wxFocusEvent &evt) { apply_changes(); evt.Skip(); }); - for (size_t fid = 0; fid < m_pattern_quick_buttons.size(); ++fid) { - wxButton *btn = m_pattern_quick_buttons[fid]; - if (btn) { - const int filament_id = int(fid + 1); - btn->Bind(wxEVT_BUTTON, [apply_changes, append_pattern_token, filament_id](wxCommandEvent&) { - append_pattern_token(filament_id); - apply_changes(); - }); - } - } - } - - update_component_picker_visuals(); - SetSizer(root); - Layout(); - SetMinSize(wxSize(-1, GetBestSize().GetHeight())); - update_preview(); -} - -void MixedFilamentConfigPanel::update_component_picker_visuals() -{ - auto update_one = [this](wxChoice *choice, wxPanel *container, wxPanel *swatch, wxStaticText *label) { - if (!choice) - return; - int sel = choice->GetSelection(); - const bool allow_none = choice->GetCount() == unsigned(m_num_physical + 1); - if (sel < 0 && m_num_physical > 0) { - sel = 0; - choice->SetSelection(sel); - } - if (sel < 0) - return; - - if (allow_none && sel == 0) { - const wxColour none_color = wxGetApp().dark_mode() ? wxColour(86, 86, 92) : wxColour(224, 224, 224); - if (swatch) { - swatch->SetBackgroundColour(none_color); - swatch->Refresh(); - } - if (label) - label->SetLabel(_L("None")); - if (container) { - container->Layout(); - container->Refresh(); - } - return; - } - - const int color_idx = allow_none ? sel - 1 : sel; - const wxColour color = (color_idx >= 0 && size_t(color_idx) < m_palette.size()) ? m_palette[size_t(color_idx)] : wxColour("#26A69A"); - if (swatch) { - swatch->SetBackgroundColour(color); - swatch->Refresh(); - } - if (label) - label->SetLabel(wxString::Format("F%d", color_idx + 1)); - if (container) { - container->Layout(); - container->Refresh(); - } - }; - - update_one(m_choice_a, m_picker_a_container, m_picker_a_swatch, m_picker_a_label); - update_one(m_choice_b, m_picker_b_container, m_picker_b_swatch, m_picker_b_label); - update_one(m_choice_c, m_picker_c_container, m_picker_c_swatch, m_picker_c_label); - update_one(m_choice_d, m_picker_d_container, m_picker_d_swatch, m_picker_d_label); - - if (m_surface_offset_target_container || m_surface_offset_target_swatch || m_surface_offset_target_label || m_surface_offset_spin) { - const int a_filament = std::clamp(m_choice_a ? (m_choice_a->GetSelection() + 1) : int(m_mf.component_a), 1, int(std::max(1, m_num_physical))); - const int b_filament = std::clamp(m_choice_b ? (m_choice_b->GetSelection() + 1) : int(m_mf.component_b), 1, int(std::max(1, m_num_physical))); - MixedFilament active_pair = m_mf; - active_pair.component_a = unsigned(a_filament); - active_pair.component_b = unsigned(b_filament); - double signed_bias_value = mixed_filament_single_surface_offset_value(active_pair, m_nozzle_diameters); - - if (m_surface_offset_spin && m_bias_mode_enabled) { - const double bias_limit = mixed_filament_bias_limit_mm(active_pair, m_nozzle_diameters); - m_surface_offset_spin->SetRange(-bias_limit, bias_limit); - signed_bias_value = m_surface_offset_spin->GetValue(); - } - - const int active_filament = signed_bias_value < -EPSILON ? a_filament : b_filament; - const int color_idx = active_filament - 1; - const wxColour color = (color_idx >= 0 && size_t(color_idx) < m_palette.size()) ? m_palette[size_t(color_idx)] : wxColour("#26A69A"); - if (m_surface_offset_target_swatch) { - m_surface_offset_target_swatch->SetBackgroundColour(color); - m_surface_offset_target_swatch->Refresh(); - } - if (m_surface_offset_target_label) - m_surface_offset_target_label->SetLabel(wxString::Format("F%d", active_filament)); - if (m_surface_offset_target_container) { - m_surface_offset_target_container->Layout(); - m_surface_offset_target_container->Refresh(); - } - } -} - -void MixedFilamentConfigPanel::update_preview() -{ - const bool simple_mode = m_mf.distribution_mode == int(MixedFilament::Simple); - const bool same_layer_mode = m_mf.distribution_mode == int(MixedFilament::SameLayerPointillisme); - const std::string normalized_pattern = MixedFilamentManager::normalize_manual_pattern(m_mf.manual_pattern); - const bool pattern_row_mode = !normalized_pattern.empty(); - - std::vector initial_sequence; - if (pattern_row_mode) { - initial_sequence = decode_manual_pattern_ids(normalized_pattern, - m_mf.component_a, - m_mf.component_b, - m_num_physical, - m_preview_settings.wall_loops); - } else { - std::vector initial_gradient_ids = simple_mode ? std::vector() : decode_gradient_ids(m_mf.gradient_component_ids); - if (initial_gradient_ids.size() >= 3) - initial_sequence = build_weighted_multi_sequence(initial_gradient_ids, *m_selected_weight_state); - else - initial_sequence = build_weighted_pair_sequence(m_mf.component_a, - m_mf.component_b, - effective_local_z_preview_mix_b_percent(m_mf, m_preview_settings), - same_layer_mode); - - if (m_blend_selector && initial_gradient_ids.size() >= 3) { - std::vector corner_colors; - corner_colors.reserve(initial_gradient_ids.size()); - for (const unsigned int id : initial_gradient_ids) { - if (id >= 1 && id <= m_palette.size()) - corner_colors.emplace_back(m_palette[id - 1]); - } - if (corner_colors.size() >= 3) - m_blend_selector->set_multi_preview(corner_colors, *m_selected_weight_state); - } - } - - if (m_mix_preview) { - if (Slic3r::mixed_filament_supports_bias_apparent_color(m_mf, m_preview_settings, m_bias_mode_enabled) && - m_mf.component_a >= 1 && m_mf.component_b >= 1 && - m_mf.component_a <= m_physical_colors.size() && m_mf.component_b <= m_physical_colors.size()) { - const auto [apparent_pct_a, apparent_pct_b] = - Slic3r::mixed_filament_apparent_pair_percentages(m_mf, m_preview_settings, m_nozzle_diameters, m_bias_mode_enabled); - m_mf.display_color = MixedFilamentManager::blend_color( - m_physical_colors[size_t(m_mf.component_a - 1)], - m_physical_colors[size_t(m_mf.component_b - 1)], - apparent_pct_a, - apparent_pct_b); - } - - const std::string bias_summary = - mixed_filament_apparent_pair_summary(m_mf, m_preview_settings, m_nozzle_diameters, m_bias_mode_enabled); - const std::string summary = bias_summary.empty() ? summarize_sequence(initial_sequence) : bias_summary; - std::vector preview_surface_offsets(m_palette.size() + 1, 0.0); - if (m_bias_mode_enabled && m_mf.component_a >= 1 && m_mf.component_a < preview_surface_offsets.size()) - preview_surface_offsets[m_mf.component_a] = double(m_mf.component_a_surface_offset); - if (m_bias_mode_enabled && m_mf.component_b >= 1 && m_mf.component_b < preview_surface_offsets.size()) - preview_surface_offsets[m_mf.component_b] = double(m_mf.component_b_surface_offset); - m_mix_preview->set_data(m_palette, initial_sequence, same_layer_mode, preview_surface_offsets, wxColour(m_mf.display_color), - _L("Preview"), summary.empty() ? wxString() : from_u8(summary)); - } - update_local_z_breakdown(); -} - -void MixedFilamentConfigPanel::update_local_z_breakdown() -{ - if (!m_breakdown_label) - return; - - std::vector weights = *m_selected_weight_state; - const std::vector ids = decode_gradient_ids(m_mf.gradient_component_ids); - if (!ids.empty()) - weights = normalize_gradient_weights(weights, ids.size()); - - const std::string breakdown = summarize_local_z_breakdown(m_mf, weights, m_preview_settings); - m_breakdown_label->SetLabel(from_u8(breakdown)); - m_breakdown_label->Wrap(FromDIP(360)); - m_breakdown_label->Show(!breakdown.empty()); - Layout(); -} - -class MixedFilamentDragHandle : public wxPanel -{ -public: - MixedFilamentDragHandle(wxWindow *parent, const wxColour &dot_color, const wxColour &bg_color) - : wxPanel(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxBORDER_NONE) - , m_dot_color(dot_color) - { - const wxSize handle_size = parent ? parent->FromDIP(wxSize(14, 18)) : wxSize(14, 18); - SetMinSize(handle_size); - SetMaxSize(handle_size); - SetInitialSize(handle_size); - SetBackgroundStyle(wxBG_STYLE_PAINT); - SetBackgroundColour(bg_color); - SetCursor(wxCursor(wxCURSOR_SIZING)); - Bind(wxEVT_PAINT, &MixedFilamentDragHandle::on_paint, this); - } - - void set_colors(const wxColour &dot_color, const wxColour &bg_color) - { - m_dot_color = dot_color; - SetBackgroundColour(bg_color); - Refresh(); - } - -private: - void on_paint(wxPaintEvent &) - { - wxAutoBufferedPaintDC dc(this); - dc.SetBackground(wxBrush(GetBackgroundColour())); - dc.Clear(); - dc.SetPen(*wxTRANSPARENT_PEN); - dc.SetBrush(wxBrush(m_dot_color)); - - const wxSize size = GetClientSize(); - const int radius = std::max(1, FromDIP(1)); - const int left_x = std::max(radius, size.x / 2 - FromDIP(2)); - const int right_x = std::min(size.x - radius - 1, size.x / 2 + FromDIP(2)); - const int top_y = std::max(radius + 1, size.y / 2 - FromDIP(5)); - const int gap_y = FromDIP(4); - - for (int row = 0; row < 3; ++row) { - const int y = top_y + row * gap_y; - dc.DrawCircle(wxPoint(left_x, y), radius); - dc.DrawCircle(wxPoint(right_x, y), radius); - } - } - - wxColour m_dot_color; -}; - -static std::vector build_mixed_filament_ui_indices(const std::vector &mixed, - const std::vector &preferred_order) -{ - std::vector ordered_indices; - std::vector used(mixed.size(), false); - - for (const uint64_t stable_id : preferred_order) { - for (size_t idx = 0; idx < mixed.size(); ++idx) { - const MixedFilament &entry = mixed[idx]; - if (used[idx] || entry.deleted || entry.stable_id != stable_id) - continue; - used[idx] = true; - ordered_indices.emplace_back(idx); - break; - } - } - - for (size_t idx = 0; idx < mixed.size(); ++idx) { - if (used[idx] || mixed[idx].deleted) - continue; - ordered_indices.emplace_back(idx); - } - - return ordered_indices; -} - -void Sidebar::init_color_mix_panel(wxWindow* parent, wxSizer* sizer) -{ - // Title bar - p->m_panel_color_mix_title = new StaticBox(parent, wxID_ANY, wxDefaultPosition, - wxDefaultSize, wxTAB_TRAVERSAL | wxBORDER_NONE); - p->m_panel_color_mix_title->SetBackgroundColor(wxColour(248, 248, 248)); - p->m_panel_color_mix_title->SetBackgroundColor2(0xF1F1F1); - p->m_panel_color_mix_title->SetMinSize(wxSize(-1, FromDIP(30))); - p->m_panel_color_mix_title->SetMaxSize(wxSize(-1, FromDIP(30))); - - p->m_color_mix_icon = new ScalableButton(p->m_panel_color_mix_title, wxID_ANY, "color_palette"); - auto* label = new Label(p->m_panel_color_mix_title, _L("Color Mixing"), LB_PROPAGATE_MOUSE_EVENT); - - p->m_btn_del_color_mix = new ScalableButton(p->m_panel_color_mix_title, wxID_ANY, "delete_filament"); - p->m_btn_add_color_mix = new ScalableButton(p->m_panel_color_mix_title, wxID_ANY, "add_filament"); - - auto* h_title = new wxBoxSizer(wxHORIZONTAL); - auto* white_left_c = new wxPanel(p->m_panel_color_mix_title, wxID_ANY, wxDefaultPosition, wxSize(FromDIP(SidebarProps::ContentMargin()), -1)); - white_left_c->SetBackgroundColour(StateColor::darkModeColorFor(*wxWHITE)); - h_title->Add(white_left_c, 0, wxEXPAND | wxTOP | wxBOTTOM, 0); - h_title->Add(p->m_color_mix_icon, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(SidebarProps::TitlebarMargin())); - h_title->AddSpacer(FromDIP(SidebarProps::ElementSpacing())); - h_title->Add(label, 0, wxALIGN_CENTER_VERTICAL); - h_title->AddStretchSpacer(); - h_title->Add(p->m_btn_del_color_mix, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(4)); - h_title->Add(p->m_btn_add_color_mix, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(8)); - auto* white_right_c = new wxPanel(p->m_panel_color_mix_title, wxID_ANY, wxDefaultPosition, wxSize(FromDIP(SidebarProps::ContentMargin()), -1)); - white_right_c->SetBackgroundColour(StateColor::darkModeColorFor(*wxWHITE)); - h_title->Add(white_right_c, 0, wxEXPAND | wxTOP | wxBOTTOM, 0); - p->m_panel_color_mix_title->SetSizer(h_title); - p->m_panel_color_mix_title->Layout(); - - // Scrolled window for content with max height of 3 rows - p->m_scrolled_color_mix = new wxScrolledWindow(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL); - p->m_scrolled_color_mix->SetScrollRate(0, 5); - p->m_scrolled_color_mix->SetBackgroundColour(StateColor::darkModeColorFor(*wxWHITE)); - - // Content panel — match physical filament content panel (sizer set dynamically in update) - p->m_panel_color_mix_content = new wxPanel(p->m_scrolled_color_mix, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL); - p->m_panel_color_mix_content->SetBackgroundColour(StateColor::darkModeColorFor(*wxWHITE)); - - // Add content panel to scrolled window - auto* scrolled_sizer = new wxBoxSizer(wxVERTICAL); - scrolled_sizer->Add(p->m_panel_color_mix_content, 0, wxEXPAND); - p->m_scrolled_color_mix->SetSizer(scrolled_sizer); - - sizer->Add(p->m_panel_color_mix_title, 0, wxEXPAND, 0); - sizer->Add(p->m_scrolled_color_mix, 0, wxEXPAND, 0); - - // Add button: open dialog to create new mix - p->m_btn_add_color_mix->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { - PresetBundle* pb = wxGetApp().preset_bundle; - if (!pb) return; - - auto* co = pb->project_config.option("filament_colour"); - const std::vector colors = co ? co->values : std::vector{}; - if (colors.size() < 2) return; - - MixedFilamentDialog dlg(wxGetApp().mainframe, colors); - if (dlg.ShowModal() != wxID_OK) return; - - auto& mgr = pb->mixed_filaments; - if (mgr.total_filaments(colors.size()) >= MAXIMUM_FILAMENT_NUMBER) return; - const MixedFilament& r = dlg.GetResult(); - mgr.add_custom_filament(r.component_a, r.component_b, r.mix_b_percent, colors); - auto& mfs = mgr.mixed_filaments(); - if (!mfs.empty()) { - mfs.back().distribution_mode = r.distribution_mode; - mfs.back().manual_pattern = r.manual_pattern; - mfs.back().gradient_component_ids = r.gradient_component_ids; - mfs.back().gradient_component_weights = r.gradient_component_weights; - mfs.back().ratio_a = r.ratio_a; - mfs.back().ratio_b = r.ratio_b; - mfs.back().local_z_max_sublayers = r.local_z_max_sublayers; - mfs.back().gradient_enabled = r.gradient_enabled; - mfs.back().gradient_start = r.gradient_start; - mfs.back().gradient_end = r.gradient_end; - mfs.back().display_color = r.display_color; - mfs.back().ui_mode = r.ui_mode; - mfs.back().custom = true; - } - if (auto* opt = pb->project_config.option("mixed_filament_definitions")) - opt->value = mgr.serialize_custom_entries(); - wxGetApp().plater()->post_slice_state_change_update(); - wxGetApp().plater()->on_filaments_change(p->combos_filament.size()); - update_color_mix_panel(); - m_scrolled_sizer->Layout(); - }); - - // Delete button: remove last custom entry - p->m_btn_del_color_mix->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { - PresetBundle* pb = wxGetApp().preset_bundle; - if (!pb) return; - - auto& mgr = pb->mixed_filaments; - auto& mfs = mgr.mixed_filaments(); - for (int i = static_cast(mfs.size()) - 1; i >= 0; --i) { - if (mfs[i].custom && !mfs[i].deleted) { - mfs[i].deleted = true; - break; - } - } - if (auto* opt = pb->project_config.option("mixed_filament_definitions")) - opt->value = mgr.serialize_custom_entries(); - wxGetApp().plater()->post_slice_state_change_update(); - wxGetApp().plater()->on_filaments_change(p->combos_filament.size()); - update_color_mix_panel(); - m_scrolled_sizer->Layout(); - }); - - // Initial visibility: hide if fewer than 2 physical filaments - update_color_mix_panel(); -} - -void Sidebar::update_color_mix_panel() -{ - if (!p->m_panel_color_mix_content) return; - - auto* co = wxGetApp().preset_bundle - ? wxGetApp().preset_bundle->project_config.option("filament_colour") - : nullptr; - const int n_physical = co ? static_cast(co->values.size()) : 0; - const bool show = (n_physical >= 2); - p->m_panel_color_mix_title->Show(show); - p->m_scrolled_color_mix->Show(show); - if (!show) { - return; - } - - wxWindowUpdateLocker no_updates(p->m_panel_color_mix_content); - p->m_panel_color_mix_content->DestroyChildren(); - - auto* preset_bundle = wxGetApp().preset_bundle; - const size_t num_physical = p->combos_filament.size(); - - std::vector physical_colors = co->values; - physical_colors.resize(num_physical, "#26A69A"); - - std::vector nozzle_diameters(num_physical, 0.4); - if (const ConfigOptionFloats* opt = preset_bundle->printers.get_edited_preset().config.option("nozzle_diameter")) { - const size_t opt_count = opt->values.size(); - if (opt_count > 0) - for (size_t i = 0; i < num_physical; ++i) - nozzle_diameters[i] = std::max(0.05, opt->get_at(unsigned(std::min(i, opt_count - 1)))); - } - - float lower_bound = 0.04f, upper_bound = 0.16f; - if (preset_bundle->project_config.has("mixed_filament_height_lower_bound")) - lower_bound = std::max(0.01f, float(preset_bundle->project_config.opt_float("mixed_filament_height_lower_bound"))); - if (preset_bundle->project_config.has("mixed_filament_height_upper_bound")) - upper_bound = std::max(lower_bound, float(preset_bundle->project_config.opt_float("mixed_filament_height_upper_bound"))); - - bool local_z_mode = false; - if (const ConfigOptionBool* opt = preset_bundle->project_config.option("dithering_local_z_mode")) - local_z_mode = opt->value; - - bool component_bias_enabled = false; - if (const ConfigOptionBool* opt = preset_bundle->project_config.option("mixed_filament_component_bias_enabled")) - component_bias_enabled = opt->value; - - const MixedFilamentPreviewSettings preview_settings { - 0.2f, lower_bound, upper_bound, 0.f, 0.f, local_z_mode, false, 1 - }; - const MixedFilamentDisplayContext display_context { - num_physical, physical_colors, nozzle_diameters, preview_settings, component_bias_enabled - }; - preset_bundle->mixed_filaments.set_display_context(display_context); - - auto& mfs = preset_bundle->mixed_filaments.mixed_filaments(); - bool any_visible = false; - for (const MixedFilament& mf : mfs) - if (!mf.deleted) { any_visible = true; break; } - - if (!any_visible) { - p->m_scrolled_color_mix->Hide(); - p->m_btn_del_color_mix->Hide(); - p->m_btn_add_color_mix->SetBitmap_("icon_add_circle"); - m_scrolled_sizer->Layout(); - return; - } - p->m_scrolled_color_mix->Show(); - p->m_btn_del_color_mix->Show(); - p->m_btn_add_color_mix->SetBitmap_("add_filament"); - - // 2-column grid matching the physical filament panel layout - auto* grid_sizer = new wxBoxSizer(wxHORIZONTAL); - auto* col0 = new wxBoxSizer(wxVERTICAL); - auto* col1 = new wxBoxSizer(wxVERTICAL); - grid_sizer->Add(col0, 1, wxEXPAND); - grid_sizer->Add(col1, 1, wxEXPAND); - - int visible_idx = 0; - for (size_t i = 0; i < mfs.size(); ++i) { - MixedFilament& mf = mfs[i]; - if (mf.deleted) continue; - - const std::string synced_color = compute_mixed_filament_display_color(mf, display_context); - if (mf.display_color != synced_color) - mf.display_color = synced_color; - - const int virtual_id = static_cast(num_physical) + visible_idx + 1; - - // Badge button: colored background + virtual filament number with gradient support - auto* badge = new MixedFilamentBadge(p->m_panel_color_mix_content, wxID_ANY, - virtual_id, mf, display_context); - - const std::string normalized_pattern_cm = MixedFilamentManager::normalize_manual_pattern(mf.manual_pattern); - std::vector gradient_ids = MixedFilamentManager::decode_gradient_component_ids(mf.gradient_component_ids, 0); - const bool z_gradient_tile = mf.gradient_enabled && mf.component_a != mf.component_b - && normalized_pattern_cm.empty() && gradient_ids.size() < 3; - wxString lbl; - if (!normalized_pattern_cm.empty()) - lbl = wxString(summarize_cycle_pattern_text(normalized_pattern_cm, mf, int(num_physical))); - else if (gradient_ids.size() >= 3) { - // parse weights - const size_t n = gradient_ids.size(); - std::vector weights; - { - std::string token; - for (const char c : mf.gradient_component_weights) { - if (c >= '0' && c <= '9') { token.push_back(c); continue; } - if (!token.empty()) { weights.emplace_back(std::max(0, std::atoi(token.c_str()))); token.clear(); } - } - if (!token.empty()) weights.emplace_back(std::max(0, std::atoi(token.c_str()))); - if (weights.size() != n) weights.assign(n, int(100 / n)); - } - // normalize to 100 - int sum = 0; for (int v : weights) sum += v; - if (sum <= 0) { weights.assign(n, 0); weights[0] = 100; sum = 100; } - for (size_t k = 0; k < n; ++k) { - const unsigned int fid = gradient_ids[k]; - const int pct = int(std::round(100.0 * weights[k] / sum)); - if (k > 0) lbl += "+"; - lbl += wxString::Format("F%u %d%%", fid, pct); - } - } else if (z_gradient_tile) { - const unsigned from_id = - mf.gradient_start >= mf.gradient_end ? mf.component_a : mf.component_b; - const unsigned to_id = - mf.gradient_start >= mf.gradient_end ? mf.component_b : mf.component_a; - lbl = wxString::Format("F%u->F%u", from_id, to_id); - } else { - const int pct_b = std::clamp(mf.mix_b_percent, 0, 100); - const int pct_a = 100 - pct_b; - lbl = wxString::Format("F%u %d%%+F%u %d%%", mf.component_a, pct_a, mf.component_b, pct_b); - if (mf.distribution_mode != int(MixedFilament::Simple)) - for (unsigned int fid : gradient_ids) - lbl += wxString::Format("+F%u", fid); - } - - bool has_error = !is_filament_compatible(mf); - - // Create a panel with border for the text - auto* name_panel = new wxPanel(p->m_panel_color_mix_content, wxID_ANY); - name_panel->SetBackgroundColour(StateColor::darkModeColorFor(*wxWHITE)); - - auto* name_sizer = new wxBoxSizer(wxHORIZONTAL); - - // Add error icon if there's an error - if (has_error) { - name_sizer->AddSpacer(FromDIP(8)); - ScalableBitmap error_bmp(name_panel, "error_icon_red_exclamation", 14); - auto* error_icon = new wxStaticBitmap(name_panel, wxID_ANY, error_bmp.bmp()); - name_sizer->Add(error_icon, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(4)); - } - - auto* name_btn = new wxStaticText(name_panel, wxID_ANY, lbl, wxDefaultPosition, wxDefaultSize, 0); - name_btn->SetBackgroundColour(StateColor::darkModeColorFor(*wxWHITE)); - name_btn->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#262E30"))); - name_btn->SetCursor(wxCursor(wxCURSOR_HAND)); - name_btn->SetMinSize(wxSize(0, -1)); // allow sizer to shrink below text width - - int name_flags = wxEXPAND | (has_error ? (wxTOP | wxBOTTOM | wxRIGHT) : wxALL); - name_sizer->Add(name_btn, 1, name_flags, FromDIP(8)); - name_panel->SetSizer(name_sizer); - name_panel->SetMinSize(wxSize(name_panel->FromDIP(100), name_panel->FromDIP(30))); - name_panel->SetMaxSize(wxSize(-1, name_panel->FromDIP(30))); - - // Use wxControl::Ellipsize on resize to match combo truncation behavior - name_panel->Bind(wxEVT_SIZE, [lbl, name_btn, name_panel](wxSizeEvent& evt) { - name_panel->Layout(); // force sizer layout so name_btn has its current size - int avail = name_btn->GetSize().x; - if (avail > 0) { - wxClientDC dc(name_btn); - dc.SetFont(name_btn->GetFont()); - wxString ellipsized = wxControl::Ellipsize(lbl, dc, wxELLIPSIZE_END, avail); - name_btn->SetLabel(ellipsized); - } - evt.Skip(); - }); - - // Add border to the panel - name_panel->Bind(wxEVT_PAINT, [](wxPaintEvent& evt) { - wxPanel* panel = dynamic_cast(evt.GetEventObject()); - if (!panel) return; - - wxPaintDC dc(panel); - wxRect rect = panel->GetClientRect(); - - // Draw border matching combo (#dbdbdb, 1px) - dc.SetPen(wxPen(StateColor::darkModeColorFor(wxColour(0xdb, 0xdb, 0xdb)), 1)); - dc.SetBrush(*wxTRANSPARENT_BRUSH); - dc.DrawRectangle(rect); - }); - - name_btn->SetToolTip(lbl); - name_btn->Bind(wxEVT_LEFT_DOWN, [this, i](wxMouseEvent&) { - auto* co = wxGetApp().preset_bundle->project_config.option("filament_colour"); - const std::vector colors = co ? co->values : std::vector{}; - if (colors.size() < 2) return; - auto& mgr = wxGetApp().preset_bundle->mixed_filaments; - auto& mfs2 = mgr.mixed_filaments(); - if (i >= mfs2.size()) return; - MixedFilamentDialog dlg(wxGetApp().mainframe, colors, mfs2[i]); - if (dlg.ShowModal() != wxID_OK) return; - const MixedFilament& r = dlg.GetResult(); - mfs2[i].component_a = r.component_a; - mfs2[i].component_b = r.component_b; - mfs2[i].mix_b_percent = r.mix_b_percent; - mfs2[i].distribution_mode = r.distribution_mode; - mfs2[i].manual_pattern = r.manual_pattern; - mfs2[i].gradient_component_ids = r.gradient_component_ids; - mfs2[i].gradient_component_weights = r.gradient_component_weights; - mfs2[i].ratio_a = r.ratio_a; - mfs2[i].ratio_b = r.ratio_b; - mfs2[i].local_z_max_sublayers = r.local_z_max_sublayers; - mfs2[i].gradient_enabled = r.gradient_enabled; - mfs2[i].gradient_start = r.gradient_start; - mfs2[i].gradient_end = r.gradient_end; - mfs2[i].display_color = r.display_color; - mfs2[i].ui_mode = r.ui_mode; - mfs2[i].custom = true; - if (auto* opt = wxGetApp().preset_bundle->project_config.option("mixed_filament_definitions")) - opt->value = mgr.serialize_custom_entries(); - wxGetApp().plater()->post_slice_state_change_update(); - wxGetApp().plater()->on_filaments_change(p->combos_filament.size()); - wxWeakRef weak_this(this); - wxTheApp->CallAfter([weak_this]() { - Sidebar* sidebar = weak_this.get(); - if (sidebar) { - sidebar->update_color_mix_panel(); - sidebar->m_scrolled_sizer->Layout(); - } - }); - }); - - auto* menu_btn = new ScalableButton(p->m_panel_color_mix_content, wxID_ANY, "menu_filament"); - menu_btn->SetToolTip(_L("Options")); - menu_btn->Bind(wxEVT_BUTTON, [this, i, visible_idx, num_physical, menu_btn](wxCommandEvent&) { - wxMenu menu; - const int edit_id = wxWindow::NewControlId(); - const int del_id = wxWindow::NewControlId(); - const int merge_to_id = wxWindow::NewControlId(); - - menu.Append(edit_id, _L("Edit")); - menu.Bind(wxEVT_MENU, [this, i](wxCommandEvent&) { - auto* co = wxGetApp().preset_bundle->project_config.option("filament_colour"); - const std::vector colors = co ? co->values : std::vector{}; - if (colors.size() < 2) return; - auto& mgr = wxGetApp().preset_bundle->mixed_filaments; - auto& mfs2 = mgr.mixed_filaments(); - if (i >= mfs2.size()) return; - MixedFilamentDialog dlg(wxGetApp().mainframe, colors, mfs2[i]); - if (dlg.ShowModal() != wxID_OK) return; - const MixedFilament& r = dlg.GetResult(); - mfs2[i].component_a = r.component_a; - mfs2[i].component_b = r.component_b; - mfs2[i].mix_b_percent = r.mix_b_percent; - mfs2[i].distribution_mode = r.distribution_mode; - mfs2[i].manual_pattern = r.manual_pattern; - mfs2[i].gradient_component_ids = r.gradient_component_ids; - mfs2[i].gradient_component_weights = r.gradient_component_weights; - mfs2[i].ratio_a = r.ratio_a; - mfs2[i].ratio_b = r.ratio_b; - mfs2[i].local_z_max_sublayers = r.local_z_max_sublayers; - mfs2[i].gradient_enabled = r.gradient_enabled; - mfs2[i].gradient_start = r.gradient_start; - mfs2[i].gradient_end = r.gradient_end; - mfs2[i].ui_mode = r.ui_mode; - mfs2[i].custom = true; - if (auto* opt = wxGetApp().preset_bundle->project_config.option("mixed_filament_definitions")) - opt->value = mgr.serialize_custom_entries(); - wxGetApp().plater()->post_slice_state_change_update(); - wxGetApp().plater()->on_filaments_change(p->combos_filament.size()); - wxWeakRef weak_this(this); - wxTheApp->CallAfter([weak_this]() { - Sidebar* sidebar = weak_this.get(); - if (sidebar) { - sidebar->update_color_mix_panel(); - sidebar->m_scrolled_sizer->Layout(); - } - }); - }, edit_id); - - // Add "Merge with" submenu - allows merging to any other filament (physical or mixed) - // Build list of all available target filaments - wxMenu* merge_submenu = new wxMenu(); - - // Get physical filament icons - std::vector icons = get_extruder_color_icons(true); - - // Add physical filaments as targets - for (size_t phys_idx = 0; phys_idx < num_physical; ++phys_idx) { - const int target_id = wxWindow::NewControlId(); - auto preset = wxGetApp().preset_bundle->filaments.find_preset(wxGetApp().preset_bundle->filament_presets[phys_idx]); - wxString target_label = preset ? from_u8(preset->label(false)) : wxString::Format(_L("Filament %d"), phys_idx + 1); - - // Use icon if available - wxMenuItem* item = nullptr; - if (phys_idx < icons.size() && icons[phys_idx]) { - item = new wxMenuItem(merge_submenu, target_id, target_label); - item->SetBitmap(*icons[phys_idx]); - merge_submenu->Append(item); - } else { - merge_submenu->Append(target_id, target_label); - } - - merge_submenu->Bind(wxEVT_MENU, [this, visible_idx, phys_idx, num_physical](wxCommandEvent&) { - // Source: mixed filament with visible_idx (0-based in visible list) - // Target: physical filament phys_idx (0-based) - // Mixed filament virtual ID = num_physical + visible_idx + 1 (1-based), convert to 0-based - size_t source_virtual_id = num_physical + visible_idx; - change_filament(source_virtual_id, phys_idx); - }, target_id); - } - - // Add other mixed filaments as targets - auto& mgr = wxGetApp().preset_bundle->mixed_filaments; - auto& mfs_for_menu = mgr.mixed_filaments(); - const size_t total_mixed = mfs_for_menu.size(); - - // Get icon dimensions for mixed filaments - const double em = Slic3r::GUI::wxGetApp().em_unit(); - const int icon_width = lround(2 * em); - const int icon_height = lround(2 * em); - - size_t target_visible_idx = 0; - for (size_t j = 0; j < total_mixed; ++j) { - if (mfs_for_menu[j].deleted) continue; - - // Skip self (compare by visible index) - if (target_visible_idx == visible_idx) { - target_visible_idx++; - continue; - } - - const int target_virtual_id = static_cast(num_physical) + target_visible_idx + 1; - const wxString target_label = wxString::Format(_L("Mixed Filament %d"), target_virtual_id); - const int target_id = wxWindow::NewControlId(); - - // Create colored bitmap for mixed filament — gradient filaments get a gradient icon - MixedFilamentDisplayContext menu_ctx; - { - auto* co2 = wxGetApp().preset_bundle->project_config.option("filament_colour"); - menu_ctx.physical_colors = co2 ? co2->values : std::vector{}; - menu_ctx.num_physical = num_physical; - } - wxBitmap* mixed_bmp = create_mixed_filament_menu_bitmap( - mfs_for_menu[j], menu_ctx, icon_width, icon_height, - wxString::Format("%d", target_virtual_id)); - - wxMenuItem* item = new wxMenuItem(merge_submenu, target_id, target_label); - item->SetBitmap(*mixed_bmp); - merge_submenu->Append(item); - - merge_submenu->Bind(wxEVT_MENU, [this, visible_idx, target_visible_idx, num_physical](wxCommandEvent&) { - // Source: mixed filament with visible_idx (0-based in visible list) - // Target: mixed filament with target_visible_idx (0-based in visible list) - // Virtual ID (0-based) = num_physical + visible_idx - size_t source_virtual_id = num_physical + visible_idx; - size_t target_virtual_id = num_physical + target_visible_idx; - change_filament(source_virtual_id, target_virtual_id); - }, target_id); - - target_visible_idx++; - } - - menu.AppendSubMenu(merge_submenu, _L("Merge with")); - - menu.Append(del_id, _L("Delete")); - menu.Bind(wxEVT_MENU, [this, i, num_physical](wxCommandEvent&) { - auto& mgr2 = wxGetApp().preset_bundle->mixed_filaments; - auto& mfs2 = mgr2.mixed_filaments(); - const std::vector old_mixed = mfs2; - if (i < mfs2.size()) { mfs2[i].deleted = true; mfs2[i].enabled = false; } - if (auto* opt = wxGetApp().preset_bundle->project_config.option("mixed_filament_definitions")) - opt->value = mgr2.serialize_custom_entries(); - wxGetApp().preset_bundle->update_mixed_filament_id_remap(old_mixed, num_physical, num_physical, i); - wxGetApp().plater()->post_slice_state_change_update(); - wxGetApp().plater()->on_filaments_change(num_physical); - wxWeakRef weak_this(this); - wxTheApp->CallAfter([weak_this]() { - Sidebar* sidebar = weak_this.get(); - if (sidebar) { - sidebar->update_color_mix_panel(); - sidebar->m_scrolled_sizer->Layout(); - } - }); - }, del_id); - - wxPoint pt{0, menu_btn->GetSize().GetHeight()}; - pt = menu_btn->ClientToScreen(pt); - pt = wxGetApp().mainframe->ScreenToClient(pt); - wxGetApp().mainframe->PopupMenu(&menu, pt); - }); - - auto* cell = new wxBoxSizer(wxHORIZONTAL); - // Match physical filament asymmetric layout: left column gets left spacer, right doesn't - if (visible_idx % 2 == 0) - cell->AddSpacer(FromDIP(SidebarProps::ContentMargin())); - cell->Add(badge, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(SidebarProps::ElementSpacing()) - FromDIP(2)); - cell->Add(name_panel, 1, wxEXPAND | wxALL, FromDIP(2)); - cell->Add(menu_btn, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(SidebarProps::ElementSpacing()) - FromDIP(2)); - cell->AddSpacer(FromDIP(SidebarProps::ContentMargin())); - - (visible_idx % 2 == 0 ? col0 : col1)->Add(cell, 0, wxEXPAND | wxBOTTOM, FromDIP(4)); - - ++visible_idx; - } - - // If odd count, pad right column so left column doesn't stretch - if (visible_idx % 2 == 1) - col1->AddStretchSpacer(1); - - // Wrap grid in vertical sizer for padding, then set as panel sizer (matches physical structure) - auto* wrapper = new wxBoxSizer(wxVERTICAL); - wrapper->Add(grid_sizer, 0, wxEXPAND | wxTOP | wxBOTTOM, FromDIP(8)); - p->m_panel_color_mix_content->SetSizer(wrapper); - p->m_panel_color_mix_content->Layout(); - - // Dynamic height: grow with rows up to 3, only cap when > 3 rows - const wxSize content_best = p->m_panel_color_mix_content->GetBestSize(); - const int row_count = (visible_idx + 1) / 2; // rows (2 columns) - const int desired_h = row_count > 3 - ? (content_best.GetHeight() / row_count) * 3 - : content_best.GetHeight(); - p->m_scrolled_color_mix->SetMinSize({-1, desired_h}); - p->m_scrolled_color_mix->SetMaxSize({-1, desired_h}); - - m_scrolled_sizer->Layout(); - wxWeakRef weak_this(this); - wxTheApp->CallAfter([weak_this]() { - Sidebar* sidebar = weak_this.get(); - if (sidebar && sidebar->p && sidebar->p->m_scrolled_color_mix) { - int vh = sidebar->p->m_scrolled_color_mix->GetVirtualSize().y; - int ch = sidebar->p->m_scrolled_color_mix->GetClientSize().y; - sidebar->p->m_scrolled_color_mix->Scroll(0, std::max(0, vh - ch)); - } - }); - p->m_panel_color_mix_content->Refresh(); - - // Disable add buttons when combined filament limit reached - if (preset_bundle) { - const bool can_add = preset_bundle->mixed_filaments.total_filaments(num_physical) < MAXIMUM_FILAMENT_NUMBER; - if (p->m_bpButton_add_filament) - p->m_bpButton_add_filament->Enable(can_add); - if (p->m_btn_add_color_mix) - p->m_btn_add_color_mix->Enable(can_add); - } -} - -void Sidebar::update_mixed_filament_panel(bool sync_manager) -{ - // Check for new collapsible structure - if (!p->m_panel_mixed_filaments_title || !p->m_panel_mixed_filaments_content) - return; - - wxWindowUpdateLocker noUpdates_sidebar(this); - wxWindowUpdateLocker noUpdates_mixed_panel(p->m_panel_mixed_filaments_content); - - auto refresh_model_canvas_colors = []() { - Plater *plater = wxGetApp().plater(); - if (plater == nullptr) - return; - - auto refresh_canvas = [](GLCanvas3D *canvas) { - if (canvas == nullptr || !canvas->is_initialized()) - return; - canvas->update_volumes_colors_by_extruder(); - canvas->render(); - }; - - refresh_canvas(plater->get_view3D_canvas3D()); - refresh_canvas(plater->get_assmeble_canvas3D()); - }; - - int prev_rows_view_y = 0; - for (wxWindow *child : p->m_panel_mixed_filaments_content->GetChildren()) { - if (auto *scrolled = dynamic_cast(child)) { - int tmp_x = 0; - scrolled->GetViewStart(&tmp_x, &prev_rows_view_y); - break; - } - } - - auto *preset_bundle = wxGetApp().preset_bundle; - if (!preset_bundle) - return; - DynamicPrintConfig *print_cfg = &preset_bundle->prints.get_edited_preset().config; - - const size_t num_physical = p->combos_filament.size(); - ConfigOptionStrings *color_opt = preset_bundle->project_config.option("filament_colour"); - std::vector physical_colors = color_opt ? color_opt->values : std::vector(); - physical_colors.resize(num_physical, "#26A69A"); - std::vector nozzle_diameters(num_physical, 0.4); - if (const ConfigOptionFloats *opt = preset_bundle->printers.get_edited_preset().config.option("nozzle_diameter")) { - const size_t opt_count = opt->values.size(); - if (opt_count > 0) { - for (size_t i = 0; i < num_physical; ++i) - nozzle_diameters[i] = std::max(0.05, opt->get_at(unsigned(std::min(i, opt_count - 1)))); - } - } - - auto get_mixed_bool = [preset_bundle, print_cfg](const std::string &key, bool fallback) { - if (const ConfigOptionBool *opt = preset_bundle->project_config.option(key)) - return opt->value; - if (const ConfigOptionInt *opt = preset_bundle->project_config.option(key)) - return opt->value != 0; - if (print_cfg) { - if (const ConfigOptionBool *opt = print_cfg->option(key)) - return opt->value; - if (const ConfigOptionInt *opt = print_cfg->option(key)) - return opt->value != 0; - } - return fallback; - }; - auto get_mixed_mode = [preset_bundle, print_cfg](bool fallback) { - if (const ConfigOptionBool *opt = preset_bundle->project_config.option("mixed_filament_gradient_mode")) - return opt->value; - if (const ConfigOptionInt *opt = preset_bundle->project_config.option("mixed_filament_gradient_mode")) - return opt->value != 0; - if (print_cfg) { - if (const ConfigOptionBool *opt = print_cfg->option("mixed_filament_gradient_mode")) - return opt->value; - if (const ConfigOptionInt *opt = print_cfg->option("mixed_filament_gradient_mode")) - return opt->value != 0; - } - return fallback; - }; - auto get_mixed_float = [preset_bundle, print_cfg](const std::string &key, float fallback) { - if (preset_bundle->project_config.has(key)) - return float(preset_bundle->project_config.opt_float(key)); - if (print_cfg && print_cfg->has(key)) - return float(print_cfg->opt_float(key)); - return fallback; - }; - auto get_mixed_string = [preset_bundle, print_cfg](const std::string &key, const std::string &fallback = std::string()) { - std::string project_value; - if (preset_bundle->project_config.has(key)) - project_value = preset_bundle->project_config.opt_string(key); - if (!project_value.empty()) - return project_value; - if (print_cfg && print_cfg->has(key)) { - const std::string print_value = print_cfg->opt_string(key); - if (!print_value.empty()) - return print_value; - } - return project_value.empty() ? fallback : project_value; - }; - auto set_mixed_float = [preset_bundle, print_cfg](const std::string &key, float value) { - if (print_cfg) { - if (ConfigOptionFloat *opt = print_cfg->option(key)) - opt->value = value; - } - if (ConfigOptionFloat *opt = preset_bundle->project_config.option(key)) - opt->value = value; - else - preset_bundle->project_config.set_key_value(key, new ConfigOptionFloat(value)); - }; - auto set_mixed_string = [preset_bundle, print_cfg](const std::string &key, const std::string &value) { - if (print_cfg) { - if (ConfigOptionString *opt = print_cfg->option(key)) - opt->value = value; - } - if (ConfigOptionString *opt = preset_bundle->project_config.option(key)) - opt->value = value; - else - preset_bundle->project_config.set_key_value(key, new ConfigOptionString(value)); - }; - auto set_mixed_bool = [preset_bundle, print_cfg](const std::string &key, bool value) { - if (print_cfg) { - if (ConfigOptionBool *opt = print_cfg->option(key)) - opt->value = value; - else if (ConfigOptionInt *opt = print_cfg->option(key)) - opt->value = value ? 1 : 0; - } - if (ConfigOptionBool *opt = preset_bundle->project_config.option(key)) - opt->value = value; - else if (ConfigOptionInt *opt = preset_bundle->project_config.option(key)) - opt->value = value ? 1 : 0; - else - preset_bundle->project_config.set_key_value(key, new ConfigOptionBool(value)); - }; - auto set_mixed_mode = [preset_bundle, print_cfg](bool enabled) { - if (print_cfg) { - if (ConfigOptionBool *opt = print_cfg->option("mixed_filament_gradient_mode")) - opt->value = enabled; - else if (ConfigOptionInt *opt = print_cfg->option("mixed_filament_gradient_mode")) - opt->value = enabled ? 1 : 0; - } - if (ConfigOptionBool *opt = preset_bundle->project_config.option("mixed_filament_gradient_mode")) - opt->value = enabled; - else if (ConfigOptionInt *opt = preset_bundle->project_config.option("mixed_filament_gradient_mode")) - opt->value = enabled ? 1 : 0; - else - preset_bundle->project_config.set_key_value("mixed_filament_gradient_mode", new ConfigOptionBool(enabled)); - }; - auto notify_mixed_change = [print_cfg]() { - if (!print_cfg) - return; - if (auto *print_tab = wxGetApp().get_tab(Preset::TYPE_PRINT)) - print_tab->update_dirty(); - if (wxGetApp().mainframe) - wxGetApp().mainframe->on_config_changed(print_cfg); - }; - auto decode_gradient_ids = [](const std::string &encoded) { - return MixedFilamentManager::decode_gradient_component_ids(encoded, 0); - }; - auto encode_gradient_ids = [](const std::vector &ids) { - return MixedFilamentManager::encode_gradient_component_ids(ids); - }; - auto decode_gradient_weights = [](const std::string &encoded, size_t expected_count) { - std::vector out; - if (encoded.empty() || expected_count == 0) - return out; - std::string token; - for (const char c : encoded) { - if (c >= '0' && c <= '9') { - token.push_back(c); - continue; - } - if (!token.empty()) { - out.emplace_back(std::max(0, std::atoi(token.c_str()))); - token.clear(); - } - } - if (!token.empty()) - out.emplace_back(std::max(0, std::atoi(token.c_str()))); - if (out.size() != expected_count) - out.clear(); - return out; - }; - auto normalize_gradient_weights = [](const std::vector &weights, size_t n) { - std::vector out = weights; - if (out.size() != n) - out.assign(n, (n > 0) ? int(100 / n) : 0); - int sum = 0; - for (int &v : out) { - v = std::max(0, v); - sum += v; - } - if (sum <= 0 && n > 0) { - out.assign(n, 0); - out[0] = 100; - return out; - } - std::vector rem(n, 0.); - int assigned = 0; - for (size_t i = 0; i < n; ++i) { - const double exact = 100.0 * double(out[i]) / double(sum); - out[i] = int(std::floor(exact)); - rem[i] = exact - double(out[i]); - assigned += out[i]; - } - int missing = std::max(0, 100 - assigned); - while (missing > 0) { - size_t best_idx = 0; - double best_rem = -1.0; - for (size_t i = 0; i < rem.size(); ++i) { - if (rem[i] > best_rem) { - best_rem = rem[i]; - best_idx = i; - } - } - ++out[best_idx]; - rem[best_idx] = 0.0; - --missing; - } - return out; - }; - auto encode_gradient_weights = [](const std::vector &weights) { - std::ostringstream ss; - for (size_t i = 0; i < weights.size(); ++i) { - if (i > 0) - ss << '/'; - ss << std::max(0, weights[i]); - } - return ss.str(); - }; - auto build_weighted_multi_sequence = [normalize_gradient_weights](const std::vector &ids, - const std::vector &weights, - size_t max_cycle_limit) { - if (ids.empty()) - return std::vector(); - - std::vector filtered_ids; - std::vector counts; - filtered_ids.reserve(ids.size()); - counts.reserve(ids.size()); - - std::vector normalized = normalize_gradient_weights(weights, ids.size()); - for (size_t i = 0; i < ids.size(); ++i) { - const int weight = (i < normalized.size()) ? std::max(0, normalized[i]) : 0; - if (weight <= 0) - continue; - filtered_ids.emplace_back(ids[i]); - counts.emplace_back(weight); - } - if (filtered_ids.empty()) { - filtered_ids = ids; - counts.assign(ids.size(), 1); - } - - int g = 0; - for (const int c : counts) - g = std::gcd(g, std::max(1, c)); - if (g > 1) { - for (int &c : counts) - c = std::max(1, c / g); - } - - constexpr size_t k_max_cycle = 48; - const size_t effective_cycle_limit = - max_cycle_limit > 0 ? std::min(k_max_cycle, std::max(1, max_cycle_limit)) : k_max_cycle; - reduce_weight_counts_to_cycle_limit(counts, effective_cycle_limit); - - std::vector reduced_ids; - std::vector reduced_counts; - reduced_ids.reserve(filtered_ids.size()); - reduced_counts.reserve(counts.size()); - for (size_t i = 0; i < counts.size(); ++i) { - if (counts[i] <= 0) - continue; - reduced_ids.emplace_back(filtered_ids[i]); - reduced_counts.emplace_back(counts[i]); - } - if (reduced_ids.empty()) - return std::vector(); - filtered_ids = std::move(reduced_ids); - counts = std::move(reduced_counts); - - const int total = std::accumulate(counts.begin(), counts.end(), 0); - if (total <= 0) - return std::vector(filtered_ids.begin(), filtered_ids.end()); - - const size_t cycle = size_t(total); - - std::vector sequence; - sequence.reserve(cycle); - std::vector emitted(counts.size(), 0); - for (size_t pos = 0; pos < cycle; ++pos) { - size_t best_idx = 0; - double best_score = -1e9; - for (size_t i = 0; i < counts.size(); ++i) { - const double target = double(pos + 1) * double(counts[i]) / double(total); - const double score = target - double(emitted[i]); - if (score > best_score) { - best_score = score; - best_idx = i; - } - } - ++emitted[best_idx]; - sequence.emplace_back(filtered_ids[best_idx]); - } - if (sequence.empty()) - sequence = filtered_ids; - return sequence; - }; - auto decode_manual_pattern_ids = [num_physical](const std::string &pattern, - unsigned int component_a, - unsigned int component_b, - size_t wall_loops) { - return build_grouped_manual_pattern_preview_sequence(pattern, component_a, component_b, num_physical, wall_loops); - }; - const bool height_weighted_mode = get_mixed_mode(false); - int gradient_mode = height_weighted_mode ? 1 : 0; - float lower_bound = std::max(0.01f, get_mixed_float("mixed_filament_height_lower_bound", 0.04f)); - float upper_bound = std::max(lower_bound, get_mixed_float("mixed_filament_height_upper_bound", 0.16f)); - float preferred_local_z_a = std::max(0.f, get_mixed_float("mixed_color_layer_height_a", 0.f)); - float preferred_local_z_b = std::max(0.f, get_mixed_float("mixed_color_layer_height_b", 0.f)); - float nominal_layer_height = 0.2f; - if (print_cfg && print_cfg->has("layer_height")) - nominal_layer_height = float(print_cfg->opt_float("layer_height")); - nominal_layer_height = std::max(0.01f, nominal_layer_height); - size_t wall_loops = 1; - if (print_cfg && print_cfg->has("wall_loops")) - wall_loops = std::max(1, size_t(std::max(1, print_cfg->opt_int("wall_loops")))); - const bool local_z_mode = get_mixed_bool("dithering_local_z_mode", false); - const bool local_z_direct_multicolor = - get_mixed_bool("dithering_local_z_direct_multicolor", false) && - preferred_local_z_a <= EPSILON && - preferred_local_z_b <= EPSILON; - const bool component_bias_enabled = get_mixed_bool("mixed_filament_component_bias_enabled", false); - float pointillism_pixel_size = std::max(0.f, get_mixed_float("mixed_filament_pointillism_pixel_size", 0.f)); - float pointillism_line_gap = std::max(0.f, get_mixed_float("mixed_filament_pointillism_line_gap", 0.f)); - float mixed_surface_indentation = std::clamp(get_mixed_float("mixed_filament_surface_indentation", 0.f), -2.f, 2.f); - bool advanced_dithering = get_mixed_bool("mixed_filament_advanced_dithering", false); - const std::string mixed_definitions = get_mixed_string("mixed_filament_definitions"); - const MixedFilamentPreviewSettings preview_settings { - nominal_layer_height, - lower_bound, - upper_bound, - preferred_local_z_a, - preferred_local_z_b, - local_z_mode, - local_z_direct_multicolor, - wall_loops - }; - const MixedFilamentDisplayContext display_context { - num_physical, - physical_colors, - nozzle_diameters, - preview_settings, - component_bias_enabled - }; - auto summarize_sequence = [num_physical](const std::vector &sequence) { - if (sequence.empty() || num_physical == 0) - return std::string(); - std::vector counts(num_physical + 1, size_t(0)); - size_t total = 0; - for (const unsigned int id : sequence) { - if (id == 0 || id > num_physical) - continue; - ++counts[id]; - ++total; - } - if (total == 0) - return std::string(); - std::ostringstream ss; - bool first = true; - for (size_t id = 1; id <= num_physical; ++id) { - if (counts[id] == 0) - continue; - const int pct = int(std::lround(100.0 * double(counts[id]) / double(total))); - if (!first) - ss << " "; - first = false; - ss << "F" << id << ":" << pct << "%"; - } - return ss.str(); - }; - auto compute_entry_display_color = [display_context](const MixedFilament &entry) { - return compute_mixed_filament_display_color(entry, display_context); - }; - - auto &mixed_mgr = preset_bundle->mixed_filaments; - mixed_mgr.set_display_context(display_context); - if (sync_manager) { - mixed_mgr.auto_generate(physical_colors); - mixed_mgr.clear_custom_entries(); - mixed_mgr.load_custom_entries(mixed_definitions, physical_colors); - mixed_mgr.apply_gradient_settings(gradient_mode, lower_bound, upper_bound, advanced_dithering); - } - - if (component_bias_enabled) { - for (MixedFilament &entry : mixed_mgr.mixed_filaments()) { - const float bias_value = mixed_filament_single_surface_offset_value(entry, nozzle_diameters); - const auto balanced_pair = mixed_filament_single_surface_offset_pair(entry, bias_value, nozzle_diameters); - entry.component_a_surface_offset = balanced_pair.first; - entry.component_b_surface_offset = balanced_pair.second; - } - } - - // During project load, sidebar may refresh before physical filament combos - // finish syncing. Avoid overwriting persisted mixed definitions while the - // physical filament set is incomplete. - if (num_physical >= 2) { - set_mixed_mode(height_weighted_mode); - set_mixed_bool("mixed_filament_component_bias_enabled", component_bias_enabled); - set_mixed_float("mixed_filament_height_lower_bound", lower_bound); - set_mixed_float("mixed_filament_height_upper_bound", upper_bound); - set_mixed_float("mixed_color_layer_height_a", preferred_local_z_a); - set_mixed_float("mixed_color_layer_height_b", preferred_local_z_b); - set_mixed_float("mixed_filament_pointillism_pixel_size", pointillism_pixel_size); - set_mixed_float("mixed_filament_pointillism_line_gap", pointillism_line_gap); - set_mixed_float("mixed_filament_surface_indentation", mixed_surface_indentation); - set_mixed_string("mixed_filament_definitions", mixed_mgr.serialize_custom_entries()); - } - - auto &mixed = mixed_mgr.mixed_filaments(); - const std::vector ordered_mixed_indices = build_mixed_filament_ui_indices(mixed, p->m_mixed_filament_ui_order); - std::vector sanitized_mixed_ui_order_ids; - sanitized_mixed_ui_order_ids.reserve(ordered_mixed_indices.size()); - for (const size_t mixed_id : ordered_mixed_indices) { - if (mixed_id < mixed.size() && mixed[mixed_id].stable_id != 0) - sanitized_mixed_ui_order_ids.emplace_back(mixed[mixed_id].stable_id); - } - p->m_mixed_filament_ui_order = std::move(sanitized_mixed_ui_order_ids); - - p->m_mixed_filament_drag_active = false; - p->m_mixed_filament_drag_source_mixed_id = size_t(-1); - p->m_mixed_filament_row_bindings.clear(); - - const int compact_gap_x = FromDIP(6); - const int compact_gap_y = FromDIP(4); - const int compact_row_pad = FromDIP(6); - const bool is_dark = wxGetApp().dark_mode(); - const wxColour mixed_rows_bg = is_dark ? wxColour(45, 45, 49) : wxColour(246, 248, 251); - const wxColour mixed_row_bg = is_dark ? wxColour(52, 52, 56) : wxColour(255, 255, 255); - const wxColour mixed_row_hover_bg = is_dark ? wxColour(62, 62, 68) : wxColour(241, 247, 255); - const wxColour mixed_text_fg = is_dark ? wxColour(232, 232, 232) : wxColour(20, 20, 20); - const wxColour mixed_summary_fg = is_dark ? wxColour(182, 182, 182) : wxColour(96, 96, 96); - p->m_panel_mixed_filaments_content->SetBackgroundColour(mixed_rows_bg); - - // Get the content sizer and clear it - wxSizer *content_sizer = p->m_panel_mixed_filaments_content->GetSizer(); - if (content_sizer) - content_sizer->Clear(true); - - // Re-add the top margin spacer that was added in constructor but cleared above - if (content_sizer) - content_sizer->AddSpacer(FromDIP(SidebarProps::ContentMargin())); - - // Update button states (buttons are now in title bar, created in constructor) - if (p->m_btn_add_gradient) - p->m_btn_add_gradient->Enable(num_physical >= 2); - if (p->m_btn_add_pattern) - p->m_btn_add_pattern->Enable(num_physical >= 2); - if (p->m_btn_add_color) - p->m_btn_add_color->Enable(num_physical >= 2); - - // Mixed Filaments panel is hidden - p->m_panel_mixed_filaments_title->Hide(); - p->m_panel_mixed_filaments_content->Hide(); - Layout(); - refresh_model_canvas_colors(); - wxWeakRef weakSelf(this); - wxTheApp->CallAfter([weakSelf]() { - if (weakSelf) weakSelf->update_color_mix_panel(); - }); - return; - -#if 0 // Mixed Filaments panel UI — hidden, preserved for potential future re-enablement - - // Reset the max size in case it was collapsed - p->m_panel_mixed_filaments_content->SetMaxSize({-1, -1}); - - auto *rows_scroller = new wxScrolledWindow(p->m_panel_mixed_filaments_content, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL | wxTAB_TRAVERSAL); - rows_scroller->SetScrollRate(0, FromDIP(6)); - rows_scroller->ShowScrollbars(wxSHOW_SB_NEVER, wxSHOW_SB_DEFAULT); - rows_scroller->SetBackgroundColour(mixed_rows_bg); - auto *rows_sizer = new wxBoxSizer(wxVERTICAL); - rows_scroller->SetSizer(rows_sizer); - - if (mixed.empty()) { - auto *empty_label = new wxStaticText(rows_scroller, wxID_ANY, - _L("No mixed filaments yet. Use Add Gradient, Add Pattern, or Add Color to create one.")); - empty_label->SetForegroundColour(mixed_summary_fg); - empty_label->SetFont(::Label::Body_13); - empty_label->Wrap(FromDIP(360)); - rows_sizer->Add(empty_label, 0, wxALL | wxEXPAND, FromDIP(12)); - rows_scroller->Layout(); - rows_scroller->FitInside(); - const int empty_content_h = empty_label->GetBestSize().GetHeight() + FromDIP(28); - const int empty_rows_h = std::max(FromDIP(86), empty_content_h); - rows_scroller->SetMinSize(wxSize(-1, empty_rows_h)); - rows_scroller->SetMaxSize(wxSize(-1, empty_rows_h)); - if (content_sizer) - content_sizer->Add(rows_scroller, 0, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, FromDIP(SidebarProps::ContentMargin())); - p->m_panel_mixed_filaments_content->Layout(); - Layout(); - refresh_model_canvas_colors(); - update_color_mix_panel(); - return; - } - - auto adjust_rows_scroller_height = [this, rows_scroller]() { - if (!rows_scroller) - return; - const int min_h = FromDIP(68); - const int collapsed_max_h = FromDIP(220); - int two_rows_cap_h = collapsed_max_h; - const auto &children = rows_scroller->GetChildren(); - if (!children.empty()) { - std::vector heights; - heights.reserve(children.GetCount()); - for (wxWindowList::compatibility_iterator it = children.GetFirst(); it; it = it->GetNext()) { - wxWindow *child = it->GetData(); - wxPanel *panel = dynamic_cast(child); - if (!panel) - continue; - heights.emplace_back(std::max(panel->GetSize().GetHeight(), panel->GetBestSize().GetHeight())); - } - if (!heights.empty()) { - std::sort(heights.begin(), heights.end(), std::greater()); - const size_t keep = std::min(2, heights.size()); - int rows_h = 0; - for (size_t i = 0; i < keep; ++i) - rows_h += heights[i]; - if (keep > 1) - rows_h += int(keep - 1) * FromDIP(2); - rows_h += FromDIP(8); - two_rows_cap_h = std::max(collapsed_max_h, rows_h); - } - } - - const int max_h = p->m_expanded_mixed_filament_rows.empty() ? collapsed_max_h : two_rows_cap_h; - const int content_h = std::max(0, rows_scroller->GetVirtualSize().GetHeight()); - const int desired_h = std::clamp(content_h, min_h, max_h); - rows_scroller->SetMinSize(wxSize(-1, desired_h)); - rows_scroller->SetMaxSize(wxSize(-1, desired_h)); - }; - - for (auto it = p->m_expanded_mixed_filament_rows.begin(); it != p->m_expanded_mixed_filament_rows.end();) { - if (*it >= mixed.size() || mixed[*it].deleted) - it = p->m_expanded_mixed_filament_rows.erase(it); - else - ++it; - } - - std::vector palette; - palette.reserve(physical_colors.size()); - for (const std::string &hex : physical_colors) - palette.emplace_back(parse_mixed_color(hex)); - - auto mixed_summary_text = [decode_gradient_ids](const MixedFilament &entry) { - const std::string normalized_pattern = MixedFilamentManager::normalize_manual_pattern(entry.manual_pattern); - if (!entry.custom) - return wxString::Format("(Filament %u + Filament %u)", unsigned(entry.component_a), unsigned(entry.component_b)); - if (!normalized_pattern.empty()) - return _L("(Pattern)"); - if (decode_gradient_ids(entry.gradient_component_ids).size() >= 3) - return _L("(Color)"); - return wxString::Format("(F%u + F%u)", unsigned(entry.component_a), unsigned(entry.component_b)); - }; - - auto apply_mixed_entry_changes = [this, preset_bundle, print_cfg, num_physical](size_t mixed_id, - const MixedFilament &updated_mf, - bool preserve_enabled = false, - bool rebuild_virtual_id_remap = false) { - if (!preset_bundle) - return; - - auto &mgr = preset_bundle->mixed_filaments; - auto &mfs = mgr.mixed_filaments(); - if (mixed_id >= mfs.size()) - return; - - const std::vector old_mixed = rebuild_virtual_id_remap ? mfs : std::vector(); - MixedFilament merged = updated_mf; - if (preserve_enabled) - merged.enabled = mfs[mixed_id].enabled; - mfs[mixed_id] = merged; - - const std::string serialized = mgr.serialize_custom_entries(); - if (print_cfg) { - if (ConfigOptionString *opt = print_cfg->option("mixed_filament_definitions")) - opt->value = serialized; - else - print_cfg->set_key_value("mixed_filament_definitions", new ConfigOptionString(serialized)); - } - if (ConfigOptionString *opt = preset_bundle->project_config.option("mixed_filament_definitions")) - opt->value = serialized; - else - preset_bundle->project_config.set_key_value("mixed_filament_definitions", new ConfigOptionString(serialized)); - - if (print_cfg) { - if (auto *print_tab = wxGetApp().get_tab(Preset::TYPE_PRINT)) - print_tab->update_dirty(); - if (wxGetApp().mainframe) - wxGetApp().mainframe->on_config_changed(print_cfg); - } - if (wxGetApp().plater()) - wxGetApp().plater()->update_project_dirty_from_presets(); - - if (rebuild_virtual_id_remap) - preset_bundle->update_mixed_filament_id_remap(old_mixed, num_physical, num_physical); - - int mode = 0; - if (const ConfigOptionBool *opt = preset_bundle->project_config.option("mixed_filament_gradient_mode")) - mode = opt->value ? 1 : 0; - else if (const ConfigOptionInt *opt = preset_bundle->project_config.option("mixed_filament_gradient_mode")) - mode = opt->value != 0 ? 1 : 0; - float lo = preset_bundle->project_config.has("mixed_filament_height_lower_bound") ? - float(preset_bundle->project_config.opt_float("mixed_filament_height_lower_bound")) : 0.04f; - float hi = preset_bundle->project_config.has("mixed_filament_height_upper_bound") ? - float(preset_bundle->project_config.opt_float("mixed_filament_height_upper_bound")) : 0.16f; - bool advanced = false; - if (const ConfigOptionBool *opt = preset_bundle->project_config.option("mixed_filament_advanced_dithering")) - advanced = opt->value; - mode = std::clamp(mode, 0, 1); - lo = std::max(0.01f, lo); - hi = std::max(lo, hi); - mgr.apply_gradient_settings(mode, lo, hi, advanced); - update_dynamic_filament_list(); - - if (rebuild_virtual_id_remap && wxGetApp().plater()) { - p->m_skip_mixed_filament_sync_once = true; - wxGetApp().plater()->on_filaments_change(num_physical); - } - }; - - auto current_mixed_filament_ui_order = [this, &mixed]() { - std::vector ordered_ids; - ordered_ids.reserve(p->m_mixed_filament_row_bindings.size()); - for (const auto &binding : p->m_mixed_filament_row_bindings) { - if (binding.mixed_id < mixed.size() && mixed[binding.mixed_id].stable_id != 0) - ordered_ids.emplace_back(mixed[binding.mixed_id].stable_id); - } - return ordered_ids; - }; - - auto drop_insert_position = [this]() { - const wxPoint mouse_pos = wxGetMousePosition(); - size_t visible_idx = 0; - for (const auto &binding : p->m_mixed_filament_row_bindings) { - if (binding.row == nullptr || !binding.row->IsShown()) - continue; - - const wxPoint top_left = binding.row->ClientToScreen(wxPoint(0, 0)); - const int row_h = std::max(binding.row->GetSize().GetHeight(), binding.row->GetBestSize().GetHeight()); - const int center_y = top_left.y + row_h / 2; - if (mouse_pos.y < center_y) - return visible_idx; - - ++visible_idx; - } - return visible_idx; - }; - - for (size_t display_mixed_idx = 0; display_mixed_idx < ordered_mixed_indices.size(); ++display_mixed_idx) { - const size_t mixed_id = ordered_mixed_indices[display_mixed_idx]; - MixedFilament &mf = mixed[mixed_id]; - const bool auto_row = !mf.custom; - - auto *row = new wxPanel(rows_scroller, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxBORDER_NONE); - row->SetBackgroundColour(mixed_row_bg); - auto *row_sizer = new wxBoxSizer(wxVERTICAL); - p->m_mixed_filament_row_bindings.push_back({mixed_id, row}); - - auto *header_panel = new wxPanel(row, wxID_ANY); - header_panel->SetBackgroundColour(mixed_row_bg); - auto *header_sizer = new wxBoxSizer(wxHORIZONTAL); - - const std::string synced_color = compute_entry_display_color(mf); - if (mf.display_color != synced_color) - mf.display_color = synced_color; - auto *drag_handle = new MixedFilamentDragHandle(header_panel, mixed_summary_fg, mixed_row_bg); - drag_handle->SetToolTip(_L("Drag to reorder mixed filaments in this panel.")); - header_sizer->Add(drag_handle, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, compact_gap_x); - - wxColour swatch_color = parse_mixed_color(mf.display_color); - auto *swatch = new wxPanel(header_panel, wxID_ANY, wxDefaultPosition, wxSize(FromDIP(12), FromDIP(12))); - swatch->SetBackgroundColour(swatch_color); - swatch->SetMinSize(wxSize(FromDIP(12), FromDIP(12))); - header_sizer->Add(swatch, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, compact_gap_x); - - const int virtual_filament_id = int(num_physical + display_mixed_idx + 1); - auto *name_label = new wxStaticText(header_panel, wxID_ANY, wxString::Format("Mixed Filament %d", virtual_filament_id)); - name_label->SetForegroundColour(mixed_text_fg); - header_sizer->Add(name_label, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, compact_gap_x); - - auto *summary_label = new wxStaticText(header_panel, wxID_ANY, mixed_summary_text(mf)); - summary_label->SetForegroundColour(mixed_summary_fg); - header_sizer->Add(summary_label, 1, wxALIGN_CENTER_VERTICAL | wxLEFT, compact_gap_x); - - header_sizer->AddStretchSpacer(1); - - auto *enabled_chk = new wxCheckBox(header_panel, wxID_ANY, _L("Enabled")); - enabled_chk->SetValue(mf.enabled); - enabled_chk->SetForegroundColour(mixed_text_fg); - header_sizer->Add(enabled_chk, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, compact_gap_x); - enabled_chk->Bind(wxEVT_LEFT_UP, [](wxMouseEvent &evt) { - evt.StopPropagation(); - evt.Skip(); - }); - enabled_chk->Bind(wxEVT_CHECKBOX, [mixed_id, enabled_chk, apply_mixed_entry_changes, preset_bundle](wxCommandEvent &) { - if (!preset_bundle || !enabled_chk) - return; - auto &mgr = preset_bundle->mixed_filaments; - auto &mfs = mgr.mixed_filaments(); - if (mixed_id >= mfs.size()) - return; - MixedFilament updated = mfs[mixed_id]; - updated.enabled = enabled_chk->GetValue(); - apply_mixed_entry_changes(mixed_id, updated, false, true); - }); - - auto *del_btn = new ScalableButton(header_panel, wxID_ANY, "cross"); - del_btn->SetToolTip(_L("Delete mixed filament")); - header_sizer->Add(del_btn, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, compact_gap_x); - - del_btn->Bind(wxEVT_BUTTON, [this, mixed_id, num_physical, set_mixed_string, notify_mixed_change](wxCommandEvent&) { - if (wxGetApp().preset_bundle) { - auto &mgr = wxGetApp().preset_bundle->mixed_filaments; - auto &mfs = mgr.mixed_filaments(); - if (mixed_id < mfs.size()) { - const std::vector old_mixed = mfs; - auto canonical_pair = [](unsigned int a, unsigned int b) { - return std::make_pair(std::min(a, b), std::max(a, b)); - }; - MixedFilament &target = mfs[mixed_id]; - const auto target_pair = canonical_pair(target.component_a, target.component_b); - const bool valid_auto_pair = target_pair.first >= 1 && - target_pair.second >= 1 && - target_pair.first <= num_physical && - target_pair.second <= num_physical && - target_pair.first != target_pair.second; - if (target.custom && target.origin_auto && valid_auto_pair) { - bool tombstoned_existing_auto = false; - for (size_t idx = 0; idx < mfs.size(); ++idx) { - if (idx == mixed_id) - continue; - MixedFilament &candidate = mfs[idx]; - if (candidate.custom) - continue; - if (canonical_pair(candidate.component_a, candidate.component_b) != target_pair) - continue; - candidate.deleted = true; - candidate.enabled = false; - tombstoned_existing_auto = true; - break; - } - - if (tombstoned_existing_auto) { - mfs.erase(mfs.begin() + mixed_id); - } else { - target.component_a = target_pair.first; - target.component_b = target_pair.second; - target.mix_b_percent = 50; - target.ratio_a = 1; - target.ratio_b = 1; - target.manual_pattern.clear(); - target.gradient_component_ids.clear(); - target.gradient_component_weights.clear(); - target.pointillism_all_filaments = false; - target.distribution_mode = int(MixedFilament::Simple); - target.custom = false; - target.origin_auto = true; - target.deleted = true; - target.enabled = false; - } - } else if (target.custom) { - mfs.erase(mfs.begin() + mixed_id); - } else { - target.deleted = true; - target.enabled = false; - } - p->m_expanded_mixed_filament_rows.clear(); - set_mixed_string("mixed_filament_definitions", mgr.serialize_custom_entries()); - wxGetApp().preset_bundle->update_mixed_filament_id_remap(old_mixed, num_physical, num_physical, mixed_id); - notify_mixed_change(); - if (wxGetApp().plater()) - wxGetApp().plater()->update_project_dirty_from_presets(); - if (wxGetApp().plater()) { - p->m_skip_mixed_filament_sync_once = true; - wxGetApp().plater()->on_filaments_change(num_physical); - } - } - } - }); - - header_panel->SetSizer(header_sizer); - row_sizer->Add(header_panel, 0, wxEXPAND | wxALL, 0); - - auto *editor_host = new wxPanel(row, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxBORDER_NONE); - editor_host->SetBackgroundColour(mixed_row_bg); - auto *editor_sizer = new wxBoxSizer(wxVERTICAL); - editor_host->SetSizer(editor_sizer); - editor_host->Hide(); - row_sizer->Add(editor_host, 0, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, compact_row_pad); - - auto set_row_hover = [row, header_panel, editor_host, drag_handle, mixed_summary_fg, mixed_row_bg, mixed_row_hover_bg](bool hovered) { - const wxColour bg = hovered ? mixed_row_hover_bg : mixed_row_bg; - if (row) row->SetBackgroundColour(bg); - if (header_panel) header_panel->SetBackgroundColour(bg); - if (editor_host) editor_host->SetBackgroundColour(bg); - if (drag_handle) drag_handle->set_colors(mixed_summary_fg, bg); - if (row) row->Refresh(); - if (header_panel) header_panel->Refresh(); - if (editor_host) editor_host->Refresh(); - }; - - auto row_contains_mouse = [row]() { - if (!row) - return false; - const wxPoint mouse_pos = wxGetMousePosition(); - const wxPoint local = row->ScreenToClient(mouse_pos); - return row->GetClientRect().Contains(local); - }; - - auto ensure_editor = [this, mixed_id, num_physical, physical_colors, nozzle_diameters, palette, preview_settings, component_bias_enabled, preset_bundle, - editor_host, editor_sizer, swatch, summary_label, header_panel, row, - rows_scroller, mixed_summary_text, apply_mixed_entry_changes]() { - if (!preset_bundle || !editor_sizer || editor_sizer->GetItemCount() > 0) - return; - - auto &mgr = preset_bundle->mixed_filaments; - auto &mfs = mgr.mixed_filaments(); - if (mixed_id >= mfs.size()) - return; - - auto *editor = new MixedFilamentConfigPanel(editor_host, mixed_id, mfs[mixed_id], num_physical, physical_colors, nozzle_diameters, palette, preview_settings, - component_bias_enabled, - [this, mixed_id, swatch, summary_label, header_panel, row, rows_scroller, mixed_summary_text, apply_mixed_entry_changes](const MixedFilament &updated_mf) { - apply_mixed_entry_changes(mixed_id, updated_mf, true); - - if (swatch) { - swatch->SetBackgroundColour(parse_mixed_color(updated_mf.display_color)); - swatch->Refresh(); - } - if (summary_label) { - summary_label->SetLabel(mixed_summary_text(updated_mf)); - } - if (header_panel) - header_panel->Layout(); - if (row) - row->Layout(); - if (rows_scroller) { - rows_scroller->Layout(); - rows_scroller->FitInside(); - } - }); - - editor_sizer->Add(editor, 0, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, FromDIP(4)); - editor_host->Layout(); - }; - - auto toggle_editor = [this, mixed_id, editor_host, ensure_editor, rows_scroller, adjust_rows_scroller_height]() { - if (!editor_host || !rows_scroller) - return; - - if (editor_host->IsShown()) { - editor_host->Hide(); - p->m_expanded_mixed_filament_rows.erase(mixed_id); - } else { - ensure_editor(); - editor_host->Show(); - p->m_expanded_mixed_filament_rows.insert(mixed_id); - } - - rows_scroller->Layout(); - rows_scroller->FitInside(); - adjust_rows_scroller_height(); - p->m_panel_mixed_filaments_content->Layout(); - m_scrolled_sizer->Layout(); - Layout(); - }; - - auto bind_toggle_target = [&toggle_editor](wxWindow *target) { - if (!target) - return; - target->SetCursor(wxCursor(wxCURSOR_HAND)); - target->Bind(wxEVT_LEFT_UP, [toggle_editor](wxMouseEvent &) { - toggle_editor(); - }); - }; - - auto bind_hover_target = [set_row_hover, row_contains_mouse](wxWindow *target) { - if (!target) - return; - target->Bind(wxEVT_ENTER_WINDOW, [set_row_hover](wxMouseEvent &evt) { - set_row_hover(true); - evt.Skip(); - }); - target->Bind(wxEVT_LEAVE_WINDOW, [set_row_hover, row_contains_mouse](wxMouseEvent &evt) { - set_row_hover(row_contains_mouse()); - evt.Skip(); - }); - }; - - auto release_drag_capture = [this]() { - p->m_mixed_filament_drag_active = false; - p->m_mixed_filament_drag_source_mixed_id = size_t(-1); - }; - - auto bind_drag_target = [this, - mixed_id, - &mixed, - drop_insert_position, - current_mixed_filament_ui_order, - release_drag_capture](wxWindow *target) { - if (!target) - return; - - target->Bind(wxEVT_LEFT_DOWN, [this, mixed_id, target](wxMouseEvent &evt) { - if (!target) - return; - p->m_mixed_filament_drag_active = true; - p->m_mixed_filament_drag_source_mixed_id = mixed_id; - if (!target->HasCapture()) - target->CaptureMouse(); - evt.StopPropagation(); - }); - - target->Bind(wxEVT_MOTION, [this](wxMouseEvent &evt) { - if (p->m_mixed_filament_drag_active) - evt.StopPropagation(); - }); - - target->Bind(wxEVT_LEFT_UP, [this, &mixed, target, drop_insert_position, current_mixed_filament_ui_order](wxMouseEvent &evt) { - if (target && target->HasCapture()) - target->ReleaseMouse(); - - if (!p->m_mixed_filament_drag_active || p->m_mixed_filament_drag_source_mixed_id >= mixed.size()) { - p->m_mixed_filament_drag_active = false; - p->m_mixed_filament_drag_source_mixed_id = size_t(-1); - evt.StopPropagation(); - return; - } - - const size_t source_mixed_id = p->m_mixed_filament_drag_source_mixed_id; - p->m_mixed_filament_drag_active = false; - p->m_mixed_filament_drag_source_mixed_id = size_t(-1); - - std::vector current_mixed_ids; - current_mixed_ids.reserve(p->m_mixed_filament_row_bindings.size()); - for (const auto &binding : p->m_mixed_filament_row_bindings) { - if (binding.mixed_id < mixed.size() && !mixed[binding.mixed_id].deleted) - current_mixed_ids.emplace_back(binding.mixed_id); - } - - const auto source_it = std::find(current_mixed_ids.begin(), current_mixed_ids.end(), source_mixed_id); - if (source_it == current_mixed_ids.end()) { - evt.StopPropagation(); - return; - } - - const size_t source_pos = size_t(std::distance(current_mixed_ids.begin(), source_it)); - size_t insert_pos = drop_insert_position(); - insert_pos = std::min(insert_pos, current_mixed_ids.size()); - - current_mixed_ids.erase(source_it); - if (insert_pos > source_pos) - --insert_pos; - insert_pos = std::min(insert_pos, current_mixed_ids.size()); - current_mixed_ids.insert(current_mixed_ids.begin() + ptrdiff_t(insert_pos), source_mixed_id); - - std::vector reordered_stable_ids; - reordered_stable_ids.reserve(current_mixed_ids.size()); - for (const size_t row_mixed_id : current_mixed_ids) { - if (row_mixed_id < mixed.size() && mixed[row_mixed_id].stable_id != 0) - reordered_stable_ids.emplace_back(mixed[row_mixed_id].stable_id); - } - - if (reordered_stable_ids != current_mixed_filament_ui_order()) { - p->m_mixed_filament_ui_order = std::move(reordered_stable_ids); - update_mixed_filament_panel(false); - } - - evt.StopPropagation(); - }); - - target->Bind(wxEVT_MOUSE_CAPTURE_LOST, [release_drag_capture](wxMouseCaptureLostEvent &) { - release_drag_capture(); - }); - }; - - header_panel->SetToolTip(auto_row ? - _L("Click to edit automatic mixed filament settings (saved as custom).") : - _L("Click to expand/retract mixed filament settings")); - bind_toggle_target(row); - bind_toggle_target(header_panel); - bind_toggle_target(name_label); - bind_toggle_target(summary_label); - bind_toggle_target(swatch); - bind_hover_target(row); - bind_hover_target(header_panel); - bind_hover_target(name_label); - bind_hover_target(summary_label); - bind_hover_target(swatch); - bind_hover_target(drag_handle); - bind_drag_target(drag_handle); - - del_btn->Bind(wxEVT_LEFT_UP, [](wxMouseEvent &evt) { - evt.StopPropagation(); - evt.Skip(); - }); - - if (p->m_expanded_mixed_filament_rows.count(mixed_id) != 0) { - ensure_editor(); - editor_host->Show(); - } - - row->SetSizer(row_sizer); - rows_sizer->Add(row, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, FromDIP(2)); - rows_sizer->AddSpacer(FromDIP(2)); - } - - rows_sizer->AddSpacer(FromDIP(2)); - rows_scroller->Layout(); - rows_scroller->FitInside(); - adjust_rows_scroller_height(); - if (prev_rows_view_y > 0) - rows_scroller->Scroll(0, prev_rows_view_y); - - content_sizer->Add(rows_scroller, 0, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(2)); - content_sizer->AddSpacer(FromDIP(2)); - p->m_panel_mixed_filaments_content->Layout(); - m_scrolled_sizer->Layout(); - Layout(); - refresh_model_canvas_colors(); - update_color_mix_panel(); -#endif // Mixed Filaments panel UI -} - -std::vector Sidebar::get_ui_ordered_filament_ids() const -{ - const size_t num_physical = static_cast(std::max(wxGetApp().filaments_cnt(), 0)); - std::vector ordered_filament_ids; - ordered_filament_ids.reserve(num_physical); - for (size_t idx = 0; idx < num_physical; ++idx) - ordered_filament_ids.emplace_back(unsigned(idx + 1)); - - if (wxGetApp().preset_bundle == nullptr) - return ordered_filament_ids; - - const auto &mixed = wxGetApp().preset_bundle->mixed_filaments.mixed_filaments(); - if (mixed.empty()) - return ordered_filament_ids; - - const std::vector ordered_mixed_indices = build_mixed_filament_ui_indices(mixed, p->m_mixed_filament_ui_order); - std::vector actual_filament_id_by_mixed_idx(mixed.size(), 0); - unsigned int next_filament_id = unsigned(num_physical + 1); - for (size_t mixed_idx = 0; mixed_idx < mixed.size(); ++mixed_idx) { - if (!mixed[mixed_idx].enabled || mixed[mixed_idx].deleted) - continue; - actual_filament_id_by_mixed_idx[mixed_idx] = next_filament_id++; + ++i; } - ordered_filament_ids.reserve(size_t(next_filament_id - 1)); - for (const size_t mixed_idx : ordered_mixed_indices) { - if (mixed_idx >= actual_filament_id_by_mixed_idx.size()) - continue; - const unsigned int actual_filament_id = actual_filament_id_by_mixed_idx[mixed_idx]; - if (actual_filament_id != 0) - ordered_filament_ids.emplace_back(actual_filament_id); + // remove unused choices if any + remove_unused_filament_combos(num_filaments); + + auto sizer = p->m_panel_filament_title->GetSizer(); + if (p->m_flushing_volume_btn != nullptr && sizer != nullptr) { + if (num_filaments > 1) { + sizer->Show(p->m_flushing_volume_btn); + sizer->Show(p->m_bpButton_del_filament); // ORCA: Show delete filament button if multiple filaments + } else { + sizer->Hide(p->m_flushing_volume_btn); + sizer->Hide(p->m_bpButton_del_filament); // ORCA: Hide delete filament button if there is only one filament + } } - return ordered_filament_ids; + Layout(); + p->m_panel_filament_title->Refresh(); + update_ui_from_settings(); + update_dynamic_filament_list(); } void Sidebar::add_filament() { if (p->combos_filament.size() >= MAXIMUM_EXTRUDER_NUMBER) return; - PresetBundle* pb = wxGetApp().preset_bundle; - if (!pb || pb->mixed_filaments.total_filaments(p->combos_filament.size()) >= MAXIMUM_FILAMENT_NUMBER) return; wxColour new_col = Plater::get_next_color_for_filament(); add_custom_filament(new_col); } @@ -7199,12 +2422,10 @@ void Sidebar::on_filaments_delete(size_t filament_id) { auto& choices = combos_filament(); - p->m_skip_mixed_filament_sync_once = false; - if (filament_id >= choices.size()) return; - if (choices.size() <= 2) + if (choices.size() == 1) choices[0]->GetDropDown().Invalidate(); wxWindowUpdateLocker noUpdates_scrolled_panel(this); @@ -7242,55 +2463,14 @@ void Sidebar::on_filaments_delete(size_t filament_id) sizer->Hide(p->m_flushing_volume_btn); } - if (p->m_bpButton_del_filament != nullptr && p->m_panel_physical_filaments_title != nullptr) { - auto* inner_sizer = p->m_panel_physical_filaments_title->GetSizer(); - if (inner_sizer) { - if (p->combos_filament.size() > 1) - inner_sizer->Show(p->m_bpButton_del_filament); - else - inner_sizer->Hide(p->m_bpButton_del_filament); - } - } - for (size_t idx = filament_id; idx < p->combos_filament.size(); ++idx) { p->combos_filament[idx]->update(); } - // Recalc scrolled filament window height (max 3 rows) - if (p->m_scrolled_filaments && p->m_panel_scrolled_filament_content) { - p->m_panel_scrolled_filament_content->Layout(); - const wxSize content_best = p->m_panel_scrolled_filament_content->GetBestSize(); - const int row_count = ((int)p->combos_filament.size() + 1) / 2; - const int desired_h = row_count > 3 - ? (content_best.GetHeight() / std::max(1, row_count)) * 3 - : content_best.GetHeight(); - p->m_scrolled_filaments->SetMinSize({-1, desired_h}); - p->m_scrolled_filaments->SetMaxSize({-1, desired_h}); - } - Layout(); - wxWeakRef weak_this(this); - wxTheApp->CallAfter([weak_this]() { - Sidebar* sidebar = weak_this.get(); - if (sidebar && sidebar->p && sidebar->p->m_scrolled_filaments) { - int vh = sidebar->p->m_scrolled_filaments->GetVirtualSize().y; - int ch = sidebar->p->m_scrolled_filaments->GetClientSize().y; - sidebar->p->m_scrolled_filaments->Scroll(0, std::max(0, vh - ch)); - } - }); p->m_panel_filament_title->Refresh(); update_ui_from_settings(); - update_dynamic_filament_list(); - update_mixed_filament_panel(); - update_color_mix_panel(); - - if (PresetBundle *pb = wxGetApp().preset_bundle) { - const bool can_add = pb->mixed_filaments.total_filaments(p->combos_filament.size()) < MAXIMUM_FILAMENT_NUMBER; - if (p->m_bpButton_add_filament) - p->m_bpButton_add_filament->Enable(can_add); - if (p->m_btn_add_color_mix) - p->m_btn_add_color_mix->Enable(can_add); - } + dynamic_filament_list.update(); } void Sidebar::edit_filament() { @@ -7300,198 +2480,11 @@ void Sidebar::edit_filament() { p->editing_filament = p->m_menu_filament_id; // sync with TabPresetComboxBox's m_filament_idx } -// Helper function: Check if target mixed filament depends on source physical filament -static bool mixed_filament_uses_physical(const MixedFilament* target_mf, unsigned int source_physical_1based) -{ - if (!target_mf) - return false; - - // Check manual_pattern tokens (resolve order #1) - const std::string norm = MixedFilamentManager::normalize_manual_pattern(target_mf->manual_pattern); - if (!norm.empty()) { - const auto groups = MixedFilamentManager::split_pattern_groups(norm); - for (const std::string &group : groups) { - const auto tokens = MixedFilamentManager::split_pattern_group_to_tokens(group, 0); - for (const std::string &token : tokens) { - if (MixedFilamentManager::physical_filament_from_token(token, *target_mf, MixedFilamentManager::kMaxPhysicalFilaments) == source_physical_1based) - return true; - } - } - } - - // Check gradient components (resolve order #2). - // Only check when there is no manual_pattern; a pattern already resolves - // every token, so gradient IDs would be a false positive at worst. - if (norm.empty()) { - const std::vector ids = MixedFilamentManager::decode_gradient_component_ids(target_mf->gradient_component_ids, 0); - for (unsigned int id : ids) { - if (id == source_physical_1based) - return true; - } - } - - // Check if target mixed filament uses source physical filament as component - // (resolve order #3). Only reached when the mixed filament has no manual_pattern, - // because in cycle mode pattern tokens "1"/"2" already cover component_a/b. - if (norm.empty() && (target_mf->component_a == source_physical_1based || target_mf->component_b == source_physical_1based)) { - return true; - } - return false; -} void Sidebar::change_filament(size_t from_id, size_t to_id) { - // 1. Parameter preprocessing - if (from_id == size_t(-2)) - from_id = p->m_menu_filament_id; - if (from_id == size_t(-1)) - from_id = p->combos_filament.size() - 1; - if (from_id == to_id) - return; - - auto& pb = *wxGetApp().preset_bundle; - const size_t num_physical = pb.filament_presets.size(); - - // 2. Determine source and target types - // Note: filament IDs here are 0-based, but is_mixed expects 1-based - bool from_is_mixed = pb.mixed_filaments.is_mixed((unsigned int)(from_id + 1), num_physical); - bool to_is_mixed = pb.mixed_filaments.is_mixed((unsigned int)(to_id + 1), num_physical); - - // 3. Dependency check: physical → mixed - if (!from_is_mixed && to_is_mixed) { - const MixedFilament* target_mf = pb.mixed_filaments.mixed_filament_from_id((unsigned int)(to_id + 1), num_physical); - unsigned int from_1based = (unsigned int)(from_id + 1); - - if (mixed_filament_uses_physical(target_mf, from_1based)) { - MessageDialog dlg(wxGetApp().plater(), - _L("The target mixed filament uses this physical filament as a component. " - "Merging will remove this physical filament and may invalidate the mixed filament. Continue?"), - _L("Warning"), wxOK | wxCANCEL | wxICON_WARNING); - int ret = dlg.ShowModal(); - if (ret != wxID_OK) - return; - } - } - - // 3b. Check for mixed filaments that depend on the source physical filament - if (!from_is_mixed) { - unsigned int from_1based = (unsigned int)(from_id + 1); - std::vector dependent_mixed_indices = pb.mixed_filaments.mixed_filaments_using_physical(from_1based); - - // If there are dependent mixed filaments, show warning dialog - if (!dependent_mixed_indices.empty()) { - wxString msg = _L("This filament is used in the following mixed filament configurations:\n\n"); - - const auto& mfs = pb.mixed_filaments.mixed_filaments(); - size_t visible_idx = 0; - for (size_t j = 0; j < mfs.size(); ++j) { - if (mfs[j].deleted) continue; - - // Check if this is one of the dependent mixed filaments - bool is_dependent = std::find(dependent_mixed_indices.begin(), dependent_mixed_indices.end(), j) - != dependent_mixed_indices.end(); - - if (is_dependent) { - const int virtual_id = static_cast(num_physical) + visible_idx + 1; - msg += wxString::Format(_L("• Mixed Filament %d\n"), virtual_id); - } - - visible_idx++; - } - - msg += _L("\nMerging this filament will invalidate these mixed filament configurations. Continue?"); - - MessageDialog dlg(wxGetApp().plater(), msg, _L("Warning"), wxOK | wxCANCEL | wxICON_WARNING); - int ret = dlg.ShowModal(); - if (ret != wxID_OK) - return; - } - } - - // 4. Execute merge based on scenario - if (from_is_mixed) { - // Mixed → Physical or Mixed → Mixed - merge_mixed_filament(from_id, to_id); - } else { - // Physical → Physical or Physical → Mixed - delete_filament(from_id, int(to_id)); - } -} - -void Sidebar::merge_mixed_filament(size_t from_id, size_t to_id) -{ - // Merge a mixed filament into another filament (physical or mixed) - // This marks the source as deleted and remaps all objects using it - - PresetBundle* preset_bundle = wxGetApp().preset_bundle; - if (!preset_bundle) return; - auto& pb = *preset_bundle; - const size_t num_physical = pb.filament_presets.size(); - - // Validate parameters - size_t total_filaments = pb.mixed_filaments.total_filaments(num_physical); - if (from_id >= total_filaments || to_id >= total_filaments) { - BOOST_LOG_TRIVIAL(error) << "merge_mixed_filament: Invalid filament ID. from_id=" - << from_id << " to_id=" << to_id << " total=" << total_filaments; - return; - } - - // Prevent self-merge - if (from_id == to_id) { - BOOST_LOG_TRIVIAL(warning) << "merge_mixed_filament: Cannot merge filament into itself: " << from_id; - return; - } - - // Verify source is actually a mixed filament - if (!pb.mixed_filaments.is_mixed((unsigned int)(from_id + 1), num_physical)) { - BOOST_LOG_TRIVIAL(error) << "merge_mixed_filament: Source filament " << from_id - << " is not a mixed filament"; - return; - } - - // Get source mixed filament index - int source_mixed_idx = pb.mixed_filaments.mixed_index_from_filament_id((unsigned int)(from_id + 1), num_physical); - if (source_mixed_idx < 0) { - BOOST_LOG_TRIVIAL(error) << "merge_mixed_filament: Cannot find mixed filament index for ID " << from_id; - return; - } - - auto& mfs = pb.mixed_filaments.mixed_filaments(); - if ((size_t)source_mixed_idx >= mfs.size()) { - BOOST_LOG_TRIVIAL(error) << "merge_mixed_filament: Mixed filament index " << source_mixed_idx - << " out of range (size=" << mfs.size() << ")"; - return; - } - - BOOST_LOG_TRIVIAL(info) << "Merging mixed filament " << from_id << " into filament " << to_id; - - // Build remap table using PresetBundle method - pb.build_merge_filament_remap(from_id, to_id, total_filaments); - - // Mark source mixed filament as deleted - mfs[source_mixed_idx].deleted = true; - mfs[source_mixed_idx].enabled = false; - - // Persist changes - if (auto* opt = pb.project_config.option("mixed_filament_definitions")) - opt->value = pb.mixed_filaments.serialize_custom_entries(); - - // Save mixed snapshot - std::vector is_mixed_snapshot; - if (auto* opt = pb.project_config.option("filament_is_mixed")) - is_mixed_snapshot = opt->values; - - // Update objects to use new filament IDs - size_t total_after = pb.mixed_filaments.total_filaments(num_physical); - wxGetApp().plater()->on_filaments_delete(total_after, from_id, -1, is_mixed_snapshot); - - BOOST_LOG_TRIVIAL(info) << "Mixed filament merge completed. Total filaments after: " << total_after; - - // Update UI - update_color_mix_panel(); - m_scrolled_sizer->Layout(); - wxGetApp().plater()->update(); + delete_filament(from_id, int(to_id)); } void Sidebar::delete_filament(size_t filament_id, int replace_filament_id) @@ -7507,149 +2500,25 @@ void Sidebar::delete_filament(size_t filament_id, int replace_filament_id) filament_id = filament_count; } - size_t total_filaments = wxGetApp().preset_bundle->filament_presets.size(); - if (filament_id > filament_count && filament_id >= total_filaments) + if (filament_id > filament_count) return; - bool is_mixed = (filament_id >= p->combos_filament.size()); - - // Check for mixed filaments that depend on the source physical filament (before deletion) - // Skip this dialog when called from change_filament() (merge path), since change_filament() - // already showed its own confirmation dialog for the same dependent mixed filaments. - if (!is_mixed && replace_filament_id < 0) { - auto& pb = *wxGetApp().preset_bundle; - const size_t num_physical = pb.filament_presets.size(); - unsigned int filament_1based = (unsigned int)(filament_id + 1); - std::vector dependent_mixed_indices = pb.mixed_filaments.mixed_filaments_using_physical(filament_1based); - - // If there are dependent mixed filaments, show warning dialog - if (!dependent_mixed_indices.empty()) { - wxString msg = _L("This filament is used in the following mixed filament configurations:\n\n"); - - const auto& mfs = pb.mixed_filaments.mixed_filaments(); - size_t visible_idx = 0; - for (size_t j = 0; j < mfs.size(); ++j) { - if (mfs[j].deleted) continue; - - // Check if this is one of the dependent mixed filaments - bool is_dependent = std::find(dependent_mixed_indices.begin(), dependent_mixed_indices.end(), j) - != dependent_mixed_indices.end(); - - if (is_dependent) { - const int virtual_id = static_cast(num_physical) + visible_idx + 1; - msg += wxString::Format(_L("• Mixed Filament %d\n"), virtual_id); - } - - visible_idx++; - } - - msg += _L("\nDeleting this filament will invalidate these mixed filament configurations. Continue?"); - - MessageDialog dlg(wxGetApp().plater(), msg, _L("Warning"), wxOK | wxCANCEL | wxICON_WARNING); - int ret = dlg.ShowModal(); - if (ret != wxID_OK) - return; - } - } - - PresetBundle* preset_bundle = wxGetApp().preset_bundle; - if (!preset_bundle) return; - - if (!is_mixed) { - if (preset_bundle->is_the_only_edited_filament(filament_id) || (filament_id == 0)) { - wxGetApp().get_tab(Preset::TYPE_FILAMENT)->select_preset(preset_bundle->filament_presets[0], false, "", true); - } - if (p->editing_filament == filament_id || p->editing_filament >= filament_count) { - p->editing_filament = -1; - } - } - - std::vector is_mixed_snapshot; - if (auto* opt = preset_bundle->project_config.option("filament_is_mixed")) - is_mixed_snapshot = opt->values; - - auto& pb = *preset_bundle; - size_t old_num_physical = pb.filament_presets.size(); - size_t old_total_filaments = pb.mixed_filaments.total_filaments(old_num_physical); - - // Check if this is a physical filament being merged into a mixed filament - bool is_physical_to_mixed_merge = !is_mixed && replace_filament_id >= (int)old_num_physical; - - // Check if target mixed filament depends on source physical filament - bool target_depends_on_source = false; - - if (is_physical_to_mixed_merge) { - // Get target mixed filament - const MixedFilament* target_mf = pb.mixed_filaments.mixed_filament_from_id( - (unsigned int)(replace_filament_id + 1), old_num_physical); - unsigned int source_1based = (unsigned int)(filament_id + 1); - - // Use helper function to check dependency - target_depends_on_source = mixed_filament_uses_physical(target_mf, source_1based); - - BOOST_LOG_TRIVIAL(info) << "Physical to mixed merge: source=" << filament_id - << " target=" << replace_filament_id - << " target_depends_on_source=" << target_depends_on_source; - } - - // Case 2: If target mixed doesn't depend on source physical, use remap mechanism - // This is similar to merge_mixed_filament() - we use a remap table to redirect objects - // to the target mixed filament, then delete the physical filament - if (is_physical_to_mixed_merge && !target_depends_on_source) { - // Build custom remap table that accounts for physical filament deletion - // When a physical filament is deleted, mixed filament virtual IDs change - pb.build_merge_filament_remap(filament_id, replace_filament_id, old_total_filaments, old_num_physical); - - BOOST_LOG_TRIVIAL(info) << "Built custom remap for physical to mixed merge (accounts for virtual ID changes)"; - - // Call on_filaments_delete with -1 to trigger remap usage - // This updates object colors using the remap table - wxGetApp().plater()->on_filaments_delete(old_total_filaments, filament_id, -1, is_mixed_snapshot); - - // Now delete the physical filament - pb.update_num_filaments(filament_id); - pb.consume_last_filament_id_remap(); // discard the remap built by update_num_filaments - wxGetApp().plater()->get_partplate_list().on_filament_deleted( - pb.filament_presets.size(), filament_id); - - BOOST_LOG_TRIVIAL(info) << "Physical to mixed merge completed using custom remap mechanism"; - - // Update UI - wxGetApp().get_tab(Preset::TYPE_PRINT)->update(); - wxGetApp().preset_bundle->export_selections(*wxGetApp().app_config); - wxGetApp().plater()->update(); - return; + if (wxGetApp().preset_bundle->is_the_only_edited_filament(filament_id) || (filament_id == 0)) { + wxGetApp().get_tab(Preset::TYPE_FILAMENT)->select_preset(wxGetApp().preset_bundle->filament_presets[0], false, "", true); } - // Case 1: If target mixed depends on source physical, delete the physical filament - // Let on_filaments_delete use default behavior (reset to default) - else if (is_physical_to_mixed_merge && target_depends_on_source) { - BOOST_LOG_TRIVIAL(info) << "Physical to mixed merge: target depends on source, will delete physical and reset to default"; - } - - // Delete the physical filament (this also removes mixed filaments that depend on it) - pb.update_num_filaments(filament_id); - size_t new_num_physical = pb.filament_presets.size(); - size_t total_after_delete = pb.mixed_filaments.total_filaments(new_num_physical); - wxGetApp().plater()->get_partplate_list().on_filament_deleted(total_after_delete, filament_id); - - int final_replace_id; - if (is_physical_to_mixed_merge && target_depends_on_source) { - // Case 1: target mixed was also deleted by remove_physical_filament - final_replace_id = -1; - } else { - int adjusted = replace_filament_id; - if (replace_filament_id >= (int)old_num_physical) - adjusted = int(new_num_physical + (size_t(replace_filament_id) - old_num_physical)); - else if (replace_filament_id > (int)filament_id) - adjusted = replace_filament_id - 1; - final_replace_id = adjusted; + if (p->editing_filament == filament_id || p->editing_filament >= filament_count) { + p->editing_filament = -1; } - wxGetApp().plater()->on_filaments_delete(total_after_delete, filament_id, - final_replace_id, - is_mixed_snapshot); + wxGetApp().preset_bundle->update_num_filaments(filament_id); + wxGetApp().plater()->get_partplate_list().on_filament_deleted(filament_count, filament_id); + + // wxGetApp().plater()->on_filaments_change(filament_count); + wxGetApp().plater()->on_filaments_delete(filament_count, filament_id, + replace_filament_id > (int) filament_id ? (replace_filament_id - 1) : replace_filament_id); + // wxGetApp().plater()->sidebar().on_filaments_delete(filament_id); wxGetApp().get_tab(Preset::TYPE_PRINT)->update(); wxGetApp().preset_bundle->export_selections(*wxGetApp().app_config); @@ -7658,16 +2527,13 @@ void Sidebar::delete_filament(size_t filament_id, int replace_filament_id) void Sidebar::add_custom_filament(wxColour new_col) { if (p->combos_filament.size() >= MAXIMUM_EXTRUDER_NUMBER) return; - PresetBundle* pb = wxGetApp().preset_bundle; - if (!pb || pb->mixed_filaments.total_filaments(p->combos_filament.size()) >= MAXIMUM_FILAMENT_NUMBER) return; int filament_count = p->combos_filament.size() + 1; - wxGetApp().plater()->confirm_auto_generated_gradients(filament_count); std::string new_color = new_col.GetAsString(wxC2S_HTML_SYNTAX).ToStdString(); - pb->set_num_filaments(filament_count, new_color); + wxGetApp().preset_bundle->set_num_filaments(filament_count, new_color); wxGetApp().plater()->on_filaments_change(filament_count); wxGetApp().get_tab(Preset::TYPE_PRINT)->update(); - pb->export_selections(*wxGetApp().app_config); + wxGetApp().preset_bundle->export_selections(*wxGetApp().app_config); auto_calc_flushing_volumes(filament_count - 1); } @@ -8240,7 +3106,7 @@ void Sidebar::auto_calc_flushing_volumes(const int modify_id) int m_max_flush_volume = Slic3r::g_max_flush_volume; unsigned int m_number_of_extruders = (int)(sqrt(init_matrix.size()) + 0.001); - const std::vector extruder_colours = wxGetApp().plater()->get_extruder_colors_from_plater_config(nullptr, false); + const std::vector extruder_colours = wxGetApp().plater()->get_extruder_colors_from_plater_config(); std::vector> multi_colours; // Support for multi-color filament @@ -8448,6 +3314,7 @@ struct Plater::priv bool m_slice_all_only_has_gcode{ false }; bool m_need_update{false}; + int m_filament_temp_check_serial{0}; // cancels stale CallAfter in check_filament_temp_mixing() //BBS: add popup object table logic //ObjectTableDialog* m_popup_table{ nullptr }; @@ -8508,13 +3375,9 @@ struct Plater::priv static const std::regex pattern_prusa; bool m_is_dark = false; - size_t m_last_auto_gradient_prompt_physical_count = 0; - bool m_last_auto_gradient_prompt_accepted = false; priv(Plater *q, MainFrame *main_frame); ~priv(); - bool confirm_auto_generated_gradients(wxWindow *parent, size_t num_physical); - void set_auto_generated_gradient_decision(size_t num_physical, bool create_auto_gradients); bool need_update() const { return m_need_update; } @@ -8988,7 +3851,7 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame) "brim_width", "wall_loops", "wall_filament", "sparse_infill_density", "sparse_infill_filament", "top_shell_layers", "enable_support", "support_filament", "support_interface_filament", "support_top_z_distance", "support_bottom_z_distance", "raft_layers", - "wipe_tower_cone_angle", "wipe_tower_extra_spacing", "wipe_tower_extra_flow", "local_z_wipe_tower_purge_lines", "wipe_tower_max_purge_speed", + "wipe_tower_rotation_angle", "wipe_tower_cone_angle", "wipe_tower_extra_spacing", "wipe_tower_extra_flow", "local_z_wipe_tower_purge_lines", "wipe_tower_max_purge_speed", "wipe_tower_wall_type", "wipe_tower_extra_rib_length","wipe_tower_rib_width","wipe_tower_fillet_wall", "wipe_tower_filament", "best_object_pos" @@ -9967,32 +4830,6 @@ std::vector Plater::priv::load_files(const std::vector& input_ << boost::format(", plate_data.size %1%, project_preset.size %2%, is_bbs_3mf %3%, file_version %4% \n") % plate_data.size() % project_presets.size() % (en_3mf_file_type == En3mfType::From_BBS) % file_version.to_string(); - auto imported_string_count = [&config_loaded](const char *key) -> size_t { - if (const auto *opt = config_loaded.option(key)) - return opt->values.size(); - return 0; - }; - auto imported_float_count = [&config_loaded](const char *key) -> size_t { - if (const auto *opt = config_loaded.option(key)) - return opt->values.size(); - return 0; - }; - - std::vector imported_filament_colors; - size_t imported_physical_filaments = 0; - if (const auto *filament_colors_opt = config_loaded.option("filament_colour")) { - imported_filament_colors = filament_colors_opt->values; - imported_physical_filaments = imported_filament_colors.size(); - } - if (imported_physical_filaments == 0) - imported_physical_filaments = imported_string_count("filament_settings_id"); - if (imported_physical_filaments == 0) - imported_physical_filaments = imported_string_count("filament_ids"); - if (imported_physical_filaments == 0) - imported_physical_filaments = imported_string_count("default_filament_colour"); - if (imported_physical_filaments == 0) - imported_physical_filaments = imported_float_count("nozzle_diameter"); - // 1. add extruder for prusa model if the number of existing extruders is not enough // 2. add extruder for BBS or Other model if only import geometry if (en_3mf_file_type == En3mfType::From_Prusa || (load_model && !load_config)) { @@ -10005,66 +4842,10 @@ std::vector Plater::priv::load_files(const std::vector& input_ } } int size = extruderIds.size() == 0 ? 0 : *(extruderIds.rbegin()); - const bool geometry_only_project_import = load_model && !load_config && imported_physical_filaments > 0; - const size_t desired_physical_filaments = geometry_only_project_import ? - std::min({imported_physical_filaments, size_t(MAXIMUM_EXTRUDER_NUMBER), size_t(MAXIMUM_FILAMENT_NUMBER)}) : 0; - BOOST_LOG_TRIVIAL(info) << "3MF geometry import filament detection" - << " imported_physical=" << imported_physical_filaments - << " imported_colors=" << imported_filament_colors.size() - << " config_filament_settings_id=" << imported_string_count("filament_settings_id") - << " config_filament_ids=" << imported_string_count("filament_ids") - << " config_default_filament_colour=" << imported_string_count("default_filament_colour") - << " config_nozzle_diameter=" << imported_float_count("nozzle_diameter") - << " model_max_extruder=" << size - << " geometry_only_project_import=" << (geometry_only_project_import ? 1 : 0); - if (geometry_only_project_import) - size = int(desired_physical_filaments); - - PresetBundle *preset_bundle = wxGetApp().preset_bundle; - if (geometry_only_project_import && preset_bundle != nullptr) { - const size_t current_num_filaments = preset_bundle->filament_presets.size(); - const bool current_project_empty = this->model.objects.empty(); - if (current_project_empty) { - static const t_config_option_keys imported_project_option_keys = { - "filament_colour", - "mixed_filament_definitions", - "mixed_filament_gradient_mode", - "mixed_filament_height_lower_bound", - "mixed_filament_height_upper_bound", - "mixed_filament_advanced_dithering", - "mixed_filament_pointillism_pixel_size", - "mixed_filament_pointillism_line_gap", - "mixed_filament_component_bias_enabled", - "mixed_filament_surface_indentation" - }; - preset_bundle->project_config.apply_only(config_loaded, imported_project_option_keys, true); - if (current_num_filaments != desired_physical_filaments) { - q->confirm_auto_generated_gradients(desired_physical_filaments); - preset_bundle->set_num_filaments(unsigned(desired_physical_filaments)); - } else - preset_bundle->update_multi_material_filament_presets(); - BOOST_LOG_TRIVIAL(info) << "3MF geometry import applied imported project config" - << " current_num_filaments=" << current_num_filaments - << " desired_physical_filaments=" << desired_physical_filaments - << " mixed_enabled=" << preset_bundle->mixed_filaments.enabled_count(); - wxGetApp().plater()->on_filaments_change(desired_physical_filaments); - } else if (current_num_filaments < desired_physical_filaments) { - std::vector new_colors; - if (imported_filament_colors.size() > current_num_filaments) { - new_colors.assign(imported_filament_colors.begin() + current_num_filaments, - imported_filament_colors.begin() + desired_physical_filaments); - } - q->confirm_auto_generated_gradients(desired_physical_filaments); - preset_bundle->set_num_filaments(unsigned(desired_physical_filaments), new_colors); - wxGetApp().plater()->on_filaments_change(desired_physical_filaments); - } - } int filament_size = sidebar->combos_filament().size(); - const int filament_limit = std::min({size, int(MAXIMUM_EXTRUDER_NUMBER), int(MAXIMUM_FILAMENT_NUMBER)}); - while (filament_size < filament_limit) { + while (filament_size < MAXIMUM_EXTRUDER_NUMBER && filament_size < size) { int filament_count = filament_size + 1; - wxGetApp().plater()->confirm_auto_generated_gradients(filament_count); wxColour new_col = Plater::get_next_color_for_filament(); std::string new_color = new_col.GetAsString(wxC2S_HTML_SYNTAX).ToStdString(); wxGetApp().preset_bundle->set_num_filaments(filament_count, new_color); @@ -10404,10 +5185,6 @@ std::vector Plater::priv::load_files(const std::vector& input_ // BBS: add preset combo box re-active logic // currently found only needs re-active here wxGetApp().load_current_presets(false, false); - // Some preset-tab refresh paths rebuild printer/filament UI from the - // active presets but do not preserve the mixed manager instance. - // Rebuild it explicitly from project_config before clamping object IDs. - preset_bundle->update_multi_material_filament_presets(); // Update filament colors for the MM-printer profile in the full config // to avoid black (default) colors for Extruders in the ObjectList, // when for extruder colors are used filament colors @@ -11725,9 +6502,17 @@ unsigned int Plater::priv::update_background_process(bool force_validation, bool BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": validate err=%1%, warning=%2%")%err.string%warning.string; if (err.string.empty()) { - this->partplate_list.get_curr_plate()->update_apply_result_invalid(false); - notification_manager->set_all_slicing_errors_gray(true); - notification_manager->close_notification_of_type(NotificationType::ValidateError); + // Validate passed, but also check filament temp mixing as a final + // guard. The Print::validate() extruders() check may miss some + // cases (e.g. wall_filament changes that haven't propagated to + // PrintRegions yet). check_filament_temp_mixing() reads directly + // from full_config() and is always current. + bool filament_ok = q->check_filament_temp_mixing(); + if (filament_ok) { + this->partplate_list.get_curr_plate()->update_apply_result_invalid(false); + notification_manager->set_all_slicing_errors_gray(true); + notification_manager->close_notification_of_type(NotificationType::ValidateError); + } if (invalidated != Print::APPLY_STATUS_UNCHANGED && background_processing_enabled()) return_state |= UPDATE_BACKGROUND_PROCESS_RESTART; @@ -12643,8 +7428,7 @@ void Plater::priv::set_current_panel(wxPanel* panel, bool no_slice) BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": from set_current_panel, no_slice %1%, export_in_progress %2%, model_fits %3%, m_is_slicing %4%")%no_slice%export_in_progress%model_fits%m_is_slicing; - if (!no_slice && !this->model.objects.empty() && !export_in_progress && model_fits && current_has_print_instances - && !current_plate->is_slice_result_valid()) + if (!no_slice && !this->model.objects.empty() && !export_in_progress && model_fits && current_has_print_instances) { //if already running in background, not relice here //BBS: add more judge for slicing @@ -13050,7 +7834,6 @@ void Plater::priv::on_select_preset(wxCommandEvent &evt) wxGetApp().plater()->update_project_dirty_from_presets(); wxGetApp().preset_bundle->export_selections(*wxGetApp().app_config); sidebar->update_dynamic_filament_list(); - sidebar->update_color_mix_panel(); bool flag_is_change = is_support_filament(idx); if (flag != flag_is_change) { sidebar->auto_calc_flushing_volumes(idx); @@ -13924,76 +8707,6 @@ void Plater::priv::on_repair_model(wxCommandEvent &event) wxGetApp().obj_list()->fix_through_netfabb(); } -bool Plater::priv::confirm_auto_generated_gradients(wxWindow *parent, size_t num_physical) -{ - auto *app_config = wxGetApp().app_config; - if (app_config == nullptr) - return MixedFilamentManager::auto_generate_enabled(); - - const bool pref_enabled = app_config->get_bool("auto_generate_gradients"); - if (!pref_enabled) { - m_last_auto_gradient_prompt_physical_count = 0; - m_last_auto_gradient_prompt_accepted = false; - MixedFilamentManager::set_auto_generate_enabled(false); - return false; - } - - if (num_physical <= 4) { - m_last_auto_gradient_prompt_physical_count = 0; - m_last_auto_gradient_prompt_accepted = false; - MixedFilamentManager::set_auto_generate_enabled(true); - return true; - } - - if (parent == nullptr || !parent->IsShownOnScreen()) { - m_last_auto_gradient_prompt_physical_count = 0; - m_last_auto_gradient_prompt_accepted = false; - MixedFilamentManager::set_auto_generate_enabled(true); - return true; - } - - if (m_last_auto_gradient_prompt_physical_count == num_physical) { - MixedFilamentManager::set_auto_generate_enabled(m_last_auto_gradient_prompt_accepted); - return m_last_auto_gradient_prompt_accepted; - } - - const size_t auto_gradient_count = num_physical * (num_physical - 1) / 2; - const wxString message = wxString::Format( - _L("Using %d physical filaments will create %d auto-generated gradients.\nDo you want to create them now?"), - int(num_physical), - int(auto_gradient_count)); - const int result = MessageDialog(parent, - message, - wxString(SLIC3R_APP_FULL_NAME) + " - " + _L("Auto gradients"), - wxYES_NO | wxYES_DEFAULT | wxCENTRE | wxICON_QUESTION) - .ShowModal(); - const bool accepted = result == wxID_YES; - m_last_auto_gradient_prompt_physical_count = num_physical; - m_last_auto_gradient_prompt_accepted = accepted; - MixedFilamentManager::set_auto_generate_enabled(accepted); - return accepted; -} - -void Plater::priv::set_auto_generated_gradient_decision(size_t num_physical, bool create_auto_gradients) -{ - m_last_auto_gradient_prompt_physical_count = num_physical; - m_last_auto_gradient_prompt_accepted = create_auto_gradients; - MixedFilamentManager::set_auto_generate_enabled(create_auto_gradients); -} - -bool Plater::confirm_auto_generated_gradients(size_t num_physical) -{ - return p != nullptr ? p->confirm_auto_generated_gradients(this, num_physical) : MixedFilamentManager::auto_generate_enabled(); -} - -void Plater::set_auto_generated_gradient_decision(size_t num_physical, bool create_auto_gradients) -{ - if (p != nullptr) - p->set_auto_generated_gradient_decision(num_physical, create_auto_gradients); - else - MixedFilamentManager::set_auto_generate_enabled(create_auto_gradients); -} - void Plater::priv::on_filament_color_changed(wxCommandEvent &event) { //q->update_all_plate_thumbnails(true); @@ -14007,13 +8720,6 @@ void Plater::priv::on_filament_color_changed(wxCommandEvent &event) if (wxGetApp().app_config->get("auto_calculate") == "true") { sidebar->auto_calc_flushing_volumes(modify_id); } - - // Regenerate mixed filaments and refresh the mixed panel only. Color - // changes do not alter filament IDs, so the full on_filaments_change() - // path is unnecessary and can re-enter UI rebuilds mid-update. - wxGetApp().preset_bundle->update_multi_material_filament_presets(); - sidebar->update_mixed_filament_panel(); - sidebar->update_color_mix_panel(); } void Plater::priv::install_network_plugin(wxCommandEvent &event) @@ -14085,9 +8791,6 @@ void Plater::priv::apply_color_mode() m_aui_mgr.GetArtProvider()->SetColour(wxAUI_DOCKART_INACTIVE_CAPTION_TEXT_COLOUR, *wxWHITE); m_aui_mgr.GetArtProvider()->SetColour(wxAUI_DOCKART_SASH_COLOUR, sash_color); m_aui_mgr.GetArtProvider()->SetColour(wxAUI_DOCKART_BORDER_COLOUR, is_dark ? *wxBLACK : wxColour(165, 165, 165)); - - if (sidebar) - sidebar->update_color_mix_panel(); } static void get_position(wxWindowBase* child, wxWindowBase* until_parent, int& x, int& y) { @@ -19355,38 +14058,6 @@ void Plater::send_gcode_legacy(int plate_idx, Export3mfProgressFn proFn, bool us // if physical_printer is selected, send gcode for this printer // DynamicPrintConfig* physical_printer_config = wxGetApp().preset_bundle->physical_printers.get_selected_printer_config(); - auto prepare_upload_filename_for_dialog = [this, use_3mf](fs::path output_file) { - output_file = fs::path(Slic3r::fold_utf8_to_ascii(output_file.string())); - if (use_3mf) - output_file.replace_extension("3mf"); - - PartPlate *current_plate = this->get_partplate_list().get_curr_plate(); - if (current_plate != nullptr) { - const Print *current_print = current_plate->fff_print(); - if (current_print != nullptr && !current_print->print_statistics().estimated_normal_print_time.empty()) - return fs::path(current_print->print_statistics().finalize_output_path(output_file.string())); - } - - if (current_plate != nullptr && current_plate->is_slice_result_valid() && current_plate->get_slice_result() != nullptr) { - const auto &estimated_stats = current_plate->get_slice_result()->print_statistics; - const float normal_time = estimated_stats.modes[static_cast(PrintEstimatedStatistics::ETimeMode::Normal)].time; - if (normal_time > 0.0f) { - std::string filename = output_file.string(); - const std::string normal_time_str = short_time(get_time_dhms(normal_time)); - boost::replace_all(filename, "{print_time}", normal_time_str); - boost::replace_all(filename, "{normal_print_time}", normal_time_str); - - const float silent_time = estimated_stats.modes[static_cast(PrintEstimatedStatistics::ETimeMode::Stealth)].time; - if (silent_time > 0.0f) - boost::replace_all(filename, "{silent_print_time}", short_time(get_time_dhms(silent_time))); - - output_file = fs::path(filename); - } - } - - return output_file; - }; - // 校验机型 auto devices = wxGetApp().app_config->get_devices(); std::string connect_preset = ""; @@ -19451,7 +14122,10 @@ void Plater::send_gcode_legacy(int plate_idx, Export3mfProgressFn proFn, bool us show_error(this, ex.what(), false); return; } - default_output_file = prepare_upload_filename_for_dialog(std::move(default_output_file)); + default_output_file = fs::path(Slic3r::fold_utf8_to_ascii(default_output_file.string())); + if (use_3mf) { + default_output_file.replace_extension("3mf"); + } // get file path auto file_path = get_partplate_list().get_curr_plate()->get_tmp_gcode_path(); @@ -19522,7 +14196,10 @@ void Plater::send_gcode_legacy(int plate_idx, Export3mfProgressFn proFn, bool us show_error(this, ex.what(), false); return; } - default_output_file = prepare_upload_filename_for_dialog(std::move(default_output_file)); + default_output_file = fs::path(Slic3r::fold_utf8_to_ascii(default_output_file.string())); + if (use_3mf) { + default_output_file.replace_extension("3mf"); + } // Repetier specific: Query the server for the list of file groups. wxArrayString groups; @@ -19787,7 +14464,7 @@ bool Plater::search_string_getter(int idx, const char** label, const char** tool return false; } -void Plater::on_filaments_delete(size_t num_filaments, size_t filament_id, int replace_filament_id, const std::vector& is_mixed_snapshot) +void Plater::on_filaments_delete(size_t num_filaments, size_t filament_id, int replace_filament_id) { // only update elements in plater update_filament_colors_in_full_config(); @@ -19800,58 +14477,19 @@ void Plater::on_filaments_delete(size_t num_filaments, size_t filament_id, int r part_plate->update_first_layer_print_sequence_when_delete_filament(filament_id); }*/ - // Consume remap before updating volumes - // This is used when merging mixed filaments to properly remap object filament IDs - PresetBundle *preset_bundle = wxGetApp().preset_bundle; - std::vector id_remap; - if (preset_bundle != nullptr) - id_remap = preset_bundle->consume_last_filament_id_remap(); - - // Build state map for remap if available. - // Use the remap for both pure-delete and merge paths so that mixed - // filaments deleted by remove_physical_filament are correctly mapped - // to NONE instead of being shifted onto wrong IDs. - EnforcerBlockerStateMap state_map; - bool should_remap_states = false; - if (!id_remap.empty()) { - should_remap_states = true; - if (replace_filament_id >= 0) { - // Merge: inject the merge target into the remap so the deleted - // physical filament maps to the target instead of 0. - size_t old_1based = filament_id + 1; - size_t new_1based = replace_filament_id + 1; - if (old_1based < id_remap.size()) - id_remap[old_1based] = (unsigned int)new_1based; - } - for (size_t i = 0; i < state_map.size(); ++i) - state_map[i] = EnforcerBlockerType(i); - for (size_t i = 1; i < state_map.size(); ++i) { - const unsigned int mapped = i < id_remap.size() ? id_remap[i] : 0; - if (mapped == 0 || mapped >= state_map.size() || mapped > num_filaments) - state_map[i] = EnforcerBlockerType::NONE; - else - state_map[i] = EnforcerBlockerType(mapped); - } - } - - // update mmu paint data + // update mmu info for (ModelObject* mo : wxGetApp().model().objects) { for (ModelVolume* mv : mo->volumes) { - if (should_remap_states) { - mv->remap_extruder_ids(num_filaments, state_map); - } else { - mv->update_extruder_count_when_delete_filament(num_filaments, filament_id + 1, - replace_filament_id + 1); // this function is 1 base - } + mv->update_extruder_count_when_delete_filament(num_filaments, filament_id + 1, + replace_filament_id + 1); // this function is 1 base } } // update UI sidebar().on_filaments_delete(filament_id); - // update global feature filament selections - static const char* keys[] = {"wall_filament", "sparse_infill_filament", "solid_infill_filament", - "support_filament", "support_interface_filament"}; + // update global support filament + static const char* keys[] = {"support_filament", "support_interface_filament"}; for (auto key : keys) if (p->config->has(key)) { if (p->config->opt_int(key) == filament_id + 1) @@ -19889,132 +14527,6 @@ void Plater::on_filaments_change(size_t num_filaments) { // only update elements in plater update_filament_colors_in_full_config(); - - const size_t old_num_filaments = sidebar().combos_filament().size(); - const bool auto_generate_before = MixedFilamentManager::auto_generate_enabled(); - const bool allow_auto_gradients = p->confirm_auto_generated_gradients(this, num_filaments); - auto summarize_uint_vector = [](const std::vector &values, size_t max_items = 24) { - std::string out = "["; - const size_t n = std::min(values.size(), max_items); - for (size_t i = 0; i < n; ++i) { - if (i > 0) - out += ","; - out += std::to_string(values[i]); - } - if (values.size() > n) - out += ",..."; - out += "]"; - return out; - }; - auto summarize_used_states = [](const std::vector &used, size_t max_items = 24) { - std::string out = "["; - size_t total = 0; - size_t emitted = 0; - for (size_t i = 1; i < used.size(); ++i) { - if (!used[i]) - continue; - ++total; - if (emitted < max_items) { - if (emitted > 0) - out += ","; - out += std::to_string(i); - ++emitted; - } - } - if (total > emitted) - out += ",..."; - out += "] total=" + std::to_string(total); - return out; - }; - PresetBundle *preset_bundle = wxGetApp().preset_bundle; - if (preset_bundle != nullptr && auto_generate_before && !allow_auto_gradients) - preset_bundle->update_multi_material_filament_presets(size_t(-1), old_num_filaments); - // Consume remap before sidebar refresh, which may trigger config sync - // paths that regenerate mixed filaments and clear this remap buffer. - std::vector id_remap; - if (preset_bundle != nullptr) - id_remap = preset_bundle->consume_last_filament_id_remap(); - - size_t total_filaments = num_filaments; - if (preset_bundle != nullptr) - total_filaments = preset_bundle->mixed_filaments.total_filaments(num_filaments); - - EnforcerBlockerStateMap state_map; - for (size_t i = 0; i < state_map.size(); ++i) - state_map[i] = EnforcerBlockerType(i); - - bool have_explicit_remap = false; - bool should_remap_states = false; - if (!id_remap.empty()) { - have_explicit_remap = true; - should_remap_states = true; - for (size_t i = 1; i < state_map.size(); ++i) { - const unsigned int mapped = i < id_remap.size() ? id_remap[i] : 0; - if (mapped == 0 || mapped >= state_map.size() || mapped > total_filaments) - state_map[i] = EnforcerBlockerType::NONE; - else - state_map[i] = EnforcerBlockerType(mapped); - } - } - - size_t changed_entries = 0; - std::string changed_map_preview = "["; - for (size_t i = 1; i < state_map.size(); ++i) { - const unsigned int mapped = unsigned(state_map[i]); - if (mapped == i) - continue; - ++changed_entries; - if (changed_entries <= 24) { - if (changed_entries > 1) - changed_map_preview += ","; - changed_map_preview += std::to_string(i) + "->" + std::to_string(mapped); - } - } - if (changed_entries > 24) - changed_map_preview += ",..."; - changed_map_preview += "]"; - BOOST_LOG_TRIVIAL(warning) << "MF_REMAP on_filaments_change" - << " old_physical=" << old_num_filaments - << " new_physical=" << num_filaments - << " total_filaments=" << total_filaments - << " id_remap_size=" << id_remap.size() - << " id_remap=" << summarize_uint_vector(id_remap) - << " explicit_remap=" << (have_explicit_remap ? 1 : 0) - << " should_remap_states=" << (should_remap_states ? 1 : 0) - << " changed_entries=" << changed_entries - << " changed_map=" << changed_map_preview; - - size_t obj_idx = 0; - for (ModelObject* mo : wxGetApp().model().objects) { - size_t vol_idx = 0; - for (ModelVolume* mv : mo->volumes) { - std::string used_before; - const bool has_mmu_paint = (mv != nullptr && !mv->mmu_segmentation_facets.empty()); - if (has_mmu_paint) - used_before = summarize_used_states(mv->mmu_segmentation_facets.get_data().used_states); - - if (should_remap_states) - mv->remap_extruder_ids(total_filaments, state_map); - else - mv->update_extruder_count(total_filaments); - - if (has_mmu_paint) { - const std::string used_after = summarize_used_states(mv->mmu_segmentation_facets.get_data().used_states); - BOOST_LOG_TRIVIAL(warning) << "MF_REMAP volume" - << " obj_idx=" << obj_idx - << " vol_idx=" << vol_idx - << " obj_name=" << mo->name - << " vol_name=" << mv->name - << " before=" << used_before - << " after=" << used_after; - } - ++vol_idx; - } - ++obj_idx; - } - - // Keep UI refresh after model remap. Some UI update paths may trigger - // scene/model sync that assumes already-remapped MMU state. sidebar().on_filaments_change(num_filaments); sidebar().obj_list()->update_objects_list_filament_column(num_filaments); @@ -20023,6 +14535,12 @@ void Plater::on_filaments_change(size_t num_filaments) PartPlate* part_plate = plate_list.get_plate(i); part_plate->update_first_layer_print_sequence(num_filaments); } + + for (ModelObject* mo : wxGetApp().model().objects) { + for (ModelVolume* mv : mo->volumes) { + mv->update_extruder_count(num_filaments); + } + } } void Plater::on_bed_type_change(BedType bed_type) @@ -20057,6 +14575,95 @@ void Plater::config_change_notification(const DynamicPrintConfig &config, const // notification for more options } +bool Plater::check_filament_temp_mixing() +{ + // Use full_config() from preset_bundle — it always contains all keys including + // filament_type. p->config is a plate-level config that may not have it. + const DynamicPrintConfig& cfg = wxGetApp().preset_bundle->full_config(); + auto* ft_opt = cfg.option("filament_type"); + if (!ft_opt || ft_opt->values.empty()) + return true; + + int num_filaments = (int)ft_opt->values.size(); + std::set used_slots; + + // ---- 1-based keys: wall / sparse infill / solid infill ---- + static const char* keys_1based[] = { + "wall_filament", "sparse_infill_filament", "solid_infill_filament" + }; + for (auto key : keys_1based) { + auto* opt = cfg.option(key); + if (!opt) opt = p->config->option(key); + if (opt && opt->value >= 1 && opt->value <= num_filaments) + used_slots.insert(opt->value - 1); + } + + // ---- 0-based keys: support / support interface / wipe tower ---- + // Value 0 means "use current filament", does not introduce a new material type. + static const char* keys_0based[] = { + "support_filament", "support_interface_filament", "wipe_tower_filament" + }; + for (auto key : keys_0based) { + auto* opt = cfg.option(key); + if (!opt) opt = p->config->option(key); + if (opt && opt->value >= 1 && opt->value <= num_filaments) + used_slots.insert(opt->value - 1); + } + + // ---- ModelVolume painting extruders (multi-material coloring) ---- + for (const ModelObject* mo : wxGetApp().model().objects) { + for (const ModelVolume* mv : mo->volumes) { + for (int eid : mv->get_extruders()) { + if (eid >= 1 && eid <= num_filaments) + used_slots.insert(eid - 1); + } + } + } + + if (used_slots.empty()) + return true; + + // Use filament_is_high_temperature from each filament preset rather than + // the outdated filament_info.json. If any used slot is high-temp and any + // other is not, high/low mixing is detected. + auto* is_high_opt = cfg.option("filament_is_high_temperature"); + bool has_high = false, has_low = false; + for (int slot : used_slots) { + if (is_high_opt && slot < (int)is_high_opt->values.size()) { + if (is_high_opt->get_at(slot)) + has_high = true; + else + has_low = true; + } + } + bool compatible = !(has_high && has_low); + + // Defer notification update to the next event loop iteration so that all + // config changes from the current event have settled. Without this, rapid + // successive calls from on_config_change + Sidebar::update_presets can + // race: one pushes an error, the next reads stale config → thinks it's + // compatible → immediately closes the notification. + // The serial counter cancels all but the last deferred update. + int serial = ++p->m_filament_temp_check_serial; + wxTheApp->CallAfter([this, compatible, serial] { + if (serial != p->m_filament_temp_check_serial) return; // stale + if (!compatible) { + StringObjectException err; + err.type = STRING_EXCEPT_FILAMENTS_DIFFERENT_TEMP; + err.string = _u8L("Cannot print multiple filaments which have large difference of " + "temperature together. Otherwise, the extruder and nozzle may " + "be blocked or damaged during printing."); + p->notification_manager->push_validate_error_notification(err); + p->partplate_list.get_curr_plate()->update_apply_result_invalid(true); + } else { + p->notification_manager->close_notification_of_type(NotificationType::ValidateError); + p->partplate_list.get_curr_plate()->update_apply_result_invalid(false); + } + }); + + return compatible; +} + void Plater::on_config_change(const DynamicPrintConfig &config) { bool update_scheduled = false; @@ -20145,6 +14752,9 @@ void Plater::on_config_change(const DynamicPrintConfig &config) this->p->schedule_background_process(); update_title_dirty_status(); } + + // Real-time high/low temperature filament mixing check + check_filament_temp_mixing(); } void Plater::set_bed_shape() const @@ -20222,30 +14832,17 @@ void Plater::on_activate() } // Get vector of extruder colors considering filament color, if extruder color is undefined. -std::vector Plater::get_extruder_colors_from_plater_config(const GCodeProcessorResult* const result, bool include_mixed) const +std::vector Plater::get_extruder_colors_from_plater_config(const GCodeProcessorResult* const result) const { if (wxGetApp().is_gcode_viewer() && result != nullptr) return result->extruder_colors; else { - if (wxGetApp().preset_bundle == nullptr) - return {}; - const Slic3r::DynamicPrintConfig* config = &wxGetApp().preset_bundle->project_config; std::vector filament_colors; if (!config->has("filament_colour")) // in case of a SLA print return filament_colors; filament_colors = (config->option("filament_colour"))->values; - const size_t num_physical = static_cast(std::max(wxGetApp().filaments_cnt(), 0)); - filament_colors.resize(num_physical, "#26A69A"); - - if (include_mixed) { - // Append display colours for enabled mixed (virtual) filaments. - const auto &mixed_mgr = wxGetApp().preset_bundle->mixed_filaments; - for (const auto &dc : mixed_mgr.display_colors()) - filament_colors.push_back(dc); - } - return filament_colors; } } @@ -21499,20 +16096,6 @@ void Plater::reset_gcode_toolpaths() p->reset_gcode_toolpaths(); } -void Plater::post_slice_state_change_update() -{ - p->partplate_list.invalid_all_slice_result(); - reset_gcode_toolpaths(); - wxGetApp().mainframe->update_slice_print_status(MainFrame::SlicePrintEventType::eEventSliceUpdate, true, false); - CallAfter([this]() { - GLCanvas3D* canvas = get_current_canvas3D(); - if (canvas) { - canvas->set_as_dirty(); - canvas->request_extra_frame(); - } - }); -} - const Mouse3DController& Plater::get_mouse3d_controller() const { return p->mouse3d_controller; From a2cbbaffe80bb442ffb388e78253f90bb676c05a Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Mon, 25 May 2026 16:33:43 +0800 Subject: [PATCH 073/135] fix: remove CallAfter races in filament temp mixing detection Make check_filament_temp_mixing() a pure detection function with no side effects. Notification push/close is now owned exclusively by Plater::priv::update()'s three-layer guard, eliminating races where CallAfter serial cancellation would accidentally drop the error notification. Also scan all plates' configs (not just the active plate) so that filament changes on any plate are detected immediately. --- src/slic3r/GUI/Plater.cpp | 100 +++++++++++++++++--------------------- 1 file changed, 45 insertions(+), 55 deletions(-) diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 2579136b9aa..b39ea3ecb42 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -2093,7 +2093,9 @@ void Sidebar::update_presets(Preset::Type preset_type) update_dynamic_filament_list(); // Real-time high/low temperature filament mixing check after filament preset change - p->plater->check_filament_temp_mixing(); + if (!p->plater->check_filament_temp_mixing()) { + p->plater->get_partplate_list().get_curr_plate()->update_apply_result_invalid(true); + } break; } @@ -3314,7 +3316,6 @@ struct Plater::priv bool m_slice_all_only_has_gcode{ false }; bool m_need_update{false}; - int m_filament_temp_check_serial{0}; // cancels stale CallAfter in check_filament_temp_mixing() //BBS: add popup object table logic //ObjectTableDialog* m_popup_table{ nullptr }; @@ -6512,6 +6513,15 @@ unsigned int Plater::priv::update_background_process(bool force_validation, bool this->partplate_list.get_curr_plate()->update_apply_result_invalid(false); notification_manager->set_all_slicing_errors_gray(true); notification_manager->close_notification_of_type(NotificationType::ValidateError); + } else { + // Filament mixing detected — show error and keep plate invalid + StringObjectException err; + err.type = STRING_EXCEPT_FILAMENTS_DIFFERENT_TEMP; + err.string = _u8L("Cannot print multiple filaments which have large difference of " + "temperature together. Otherwise, the extruder and nozzle may " + "be blocked or damaged during printing."); + notification_manager->push_validate_error_notification(err); + this->partplate_list.get_curr_plate()->update_apply_result_invalid(true); } if (invalidated != Print::APPLY_STATUS_UNCHANGED && background_processing_enabled()) return_state |= UPDATE_BACKGROUND_PROCESS_RESTART; @@ -14577,40 +14587,41 @@ void Plater::config_change_notification(const DynamicPrintConfig &config, const bool Plater::check_filament_temp_mixing() { - // Use full_config() from preset_bundle — it always contains all keys including - // filament_type. p->config is a plate-level config that may not have it. - const DynamicPrintConfig& cfg = wxGetApp().preset_bundle->full_config(); - auto* ft_opt = cfg.option("filament_type"); + const DynamicPrintConfig& full_cfg = wxGetApp().preset_bundle->full_config(); + auto* ft_opt = full_cfg.option("filament_type"); if (!ft_opt || ft_opt->values.empty()) return true; int num_filaments = (int)ft_opt->values.size(); std::set used_slots; - // ---- 1-based keys: wall / sparse infill / solid infill ---- - static const char* keys_1based[] = { - "wall_filament", "sparse_infill_filament", "solid_infill_filament" + // Helper: collect used filament slots from a single config + auto collect_from_cfg = [&](const DynamicPrintConfig& cfg) { + static const char* keys_1based[] = {"wall_filament", "sparse_infill_filament", "solid_infill_filament"}; + for (auto key : keys_1based) { + auto* opt = cfg.option(key); + if (opt && opt->value >= 1 && opt->value <= num_filaments) + used_slots.insert(opt->value - 1); + } + static const char* keys_0based[] = {"support_filament", "support_interface_filament", "wipe_tower_filament"}; + for (auto key : keys_0based) { + auto* opt = cfg.option(key); + if (opt && opt->value >= 1 && opt->value <= num_filaments) + used_slots.insert(opt->value - 1); + } }; - for (auto key : keys_1based) { - auto* opt = cfg.option(key); - if (!opt) opt = p->config->option(key); - if (opt && opt->value >= 1 && opt->value <= num_filaments) - used_slots.insert(opt->value - 1); - } - // ---- 0-based keys: support / support interface / wipe tower ---- - // Value 0 means "use current filament", does not introduce a new material type. - static const char* keys_0based[] = { - "support_filament", "support_interface_filament", "wipe_tower_filament" - }; - for (auto key : keys_0based) { - auto* opt = cfg.option(key); - if (!opt) opt = p->config->option(key); - if (opt && opt->value >= 1 && opt->value <= num_filaments) - used_slots.insert(opt->value - 1); + // Collect from full_config first (global defaults for wall/infill/etc.) + collect_from_cfg(full_cfg); + + // Collect from every plate's config (per-plate overrides) + for (int i = 0; i < p->partplate_list.get_plate_count(); ++i) { + PartPlate* plate = p->partplate_list.get_plate(i); + if (plate) + collect_from_cfg(*plate->config()); } - // ---- ModelVolume painting extruders (multi-material coloring) ---- + // Collect from ModelVolume painting extruders (multi-material coloring) for (const ModelObject* mo : wxGetApp().model().objects) { for (const ModelVolume* mv : mo->volumes) { for (int eid : mv->get_extruders()) { @@ -14623,10 +14634,8 @@ bool Plater::check_filament_temp_mixing() if (used_slots.empty()) return true; - // Use filament_is_high_temperature from each filament preset rather than - // the outdated filament_info.json. If any used slot is high-temp and any - // other is not, high/low mixing is detected. - auto* is_high_opt = cfg.option("filament_is_high_temperature"); + // Use filament_is_high_temperature from each filament preset + auto* is_high_opt = full_cfg.option("filament_is_high_temperature"); bool has_high = false, has_low = false; for (int slot : used_slots) { if (is_high_opt && slot < (int)is_high_opt->values.size()) { @@ -14638,29 +14647,6 @@ bool Plater::check_filament_temp_mixing() } bool compatible = !(has_high && has_low); - // Defer notification update to the next event loop iteration so that all - // config changes from the current event have settled. Without this, rapid - // successive calls from on_config_change + Sidebar::update_presets can - // race: one pushes an error, the next reads stale config → thinks it's - // compatible → immediately closes the notification. - // The serial counter cancels all but the last deferred update. - int serial = ++p->m_filament_temp_check_serial; - wxTheApp->CallAfter([this, compatible, serial] { - if (serial != p->m_filament_temp_check_serial) return; // stale - if (!compatible) { - StringObjectException err; - err.type = STRING_EXCEPT_FILAMENTS_DIFFERENT_TEMP; - err.string = _u8L("Cannot print multiple filaments which have large difference of " - "temperature together. Otherwise, the extruder and nozzle may " - "be blocked or damaged during printing."); - p->notification_manager->push_validate_error_notification(err); - p->partplate_list.get_curr_plate()->update_apply_result_invalid(true); - } else { - p->notification_manager->close_notification_of_type(NotificationType::ValidateError); - p->partplate_list.get_curr_plate()->update_apply_result_invalid(false); - } - }); - return compatible; } @@ -14753,8 +14739,12 @@ void Plater::on_config_change(const DynamicPrintConfig &config) update_title_dirty_status(); } - // Real-time high/low temperature filament mixing check - check_filament_temp_mixing(); + // Real-time high/low temperature filament mixing check. + // Notification push/close is handled by priv::update()'s three-layer guard; + // here we only mark the plate invalid to disable the slice button early. + if (!check_filament_temp_mixing()) { + p->partplate_list.get_curr_plate()->update_apply_result_invalid(true); + } } void Plater::set_bed_shape() const From ae41a2ff8496fc8274381e19ff63f7793bad229b Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Mon, 25 May 2026 16:44:15 +0800 Subject: [PATCH 074/135] fix: push notification immediately in both filament mixing hooks Previously the hooks only marked the plate invalid (greyed out slice button) but relied on priv::update() to push the error notification. Since priv::update() only runs after schedule_background_process(), sidebar filament preset changes that don't trigger a background process cycle would never show the red error bar. Now all three sites push/close notification synchronously: - Sidebar::update_presets() - Plater::on_config_change() - Plater::priv::update() (three-layer guard) --- src/slic3r/GUI/Plater.cpp | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index b39ea3ecb42..690e3a32ee0 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -2094,7 +2094,16 @@ void Sidebar::update_presets(Preset::Type preset_type) // Real-time high/low temperature filament mixing check after filament preset change if (!p->plater->check_filament_temp_mixing()) { + StringObjectException err; + err.type = STRING_EXCEPT_FILAMENTS_DIFFERENT_TEMP; + err.string = _u8L("Cannot print multiple filaments which have large difference of " + "temperature together. Otherwise, the extruder and nozzle may " + "be blocked or damaged during printing."); + p->plater->get_notification_manager()->push_validate_error_notification(err); p->plater->get_partplate_list().get_curr_plate()->update_apply_result_invalid(true); + } else { + p->plater->get_notification_manager()->close_notification_of_type(NotificationType::ValidateError); + p->plater->get_partplate_list().get_curr_plate()->update_apply_result_invalid(false); } break; @@ -14739,11 +14748,18 @@ void Plater::on_config_change(const DynamicPrintConfig &config) update_title_dirty_status(); } - // Real-time high/low temperature filament mixing check. - // Notification push/close is handled by priv::update()'s three-layer guard; - // here we only mark the plate invalid to disable the slice button early. + // Real-time high/low temperature filament mixing check if (!check_filament_temp_mixing()) { + StringObjectException err; + err.type = STRING_EXCEPT_FILAMENTS_DIFFERENT_TEMP; + err.string = _u8L("Cannot print multiple filaments which have large difference of " + "temperature together. Otherwise, the extruder and nozzle may " + "be blocked or damaged during printing."); + p->notification_manager->push_validate_error_notification(err); p->partplate_list.get_curr_plate()->update_apply_result_invalid(true); + } else { + p->notification_manager->close_notification_of_type(NotificationType::ValidateError); + p->partplate_list.get_curr_plate()->update_apply_result_invalid(false); } } From e48233e30c11da48ac321ff253de98f7228e377e Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Mon, 25 May 2026 16:54:38 +0800 Subject: [PATCH 075/135] fix: read filament_is_high_temperature directly from preset configs full_config() builds a merged snapshot that may lag when called from Sidebar hooks because the edited preset config hasn't been committed yet. Read filament_is_high_temperature directly from each filament preset's individual config via find_preset(name, true) instead. --- src/slic3r/GUI/Plater.cpp | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 690e3a32ee0..2df76d3597f 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -14643,15 +14643,23 @@ bool Plater::check_filament_temp_mixing() if (used_slots.empty()) return true; - // Use filament_is_high_temperature from each filament preset - auto* is_high_opt = full_cfg.option("filament_is_high_temperature"); + // Read filament_is_high_temperature directly from each filament preset's + // own config rather than through full_config(). full_config() builds a + // merged snapshot that may lag behind when called from Sidebar hooks + // (the edited preset config hasn't been committed yet). + PresetBundle* bundle = wxGetApp().preset_bundle; bool has_high = false, has_low = false; for (int slot : used_slots) { - if (is_high_opt && slot < (int)is_high_opt->values.size()) { - if (is_high_opt->get_at(slot)) - has_high = true; - else - has_low = true; + if (slot < (int)bundle->filament_presets.size()) { + const Preset* preset = bundle->filaments.find_preset( + bundle->filament_presets[slot], true); + if (preset) { + bool is_high = preset->config.opt_bool("filament_is_high_temperature", 0); + if (is_high) + has_high = true; + else + has_low = true; + } } } bool compatible = !(has_high && has_low); From 3f190fe008312da0cdb7cd305ecd37a021ab5d43 Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Tue, 26 May 2026 15:32:09 +0800 Subject: [PATCH 076/135] Add preference option to allow high/low temperature filament mixing --- src/slic3r/GUI/Preferences.cpp | 25 +++---------------------- 1 file changed, 3 insertions(+), 22 deletions(-) diff --git a/src/slic3r/GUI/Preferences.cpp b/src/slic3r/GUI/Preferences.cpp index a194749e11b..93cc328ae83 100644 --- a/src/slic3r/GUI/Preferences.cpp +++ b/src/slic3r/GUI/Preferences.cpp @@ -6,7 +6,6 @@ #include "MsgDialog.hpp" #include "I18N.hpp" #include "libslic3r/AppConfig.hpp" -#include "libslic3r/MixedFilament.hpp" #include #include #include "Notebook.hpp" @@ -779,16 +778,6 @@ wxBoxSizer *PreferencesDialog::create_item_checkbox(wxString title, wxWindow *pa BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " sync_user_preset: " << (sync ? "true" : "false"); } - if (param == "auto_generate_gradients") { - MixedFilamentManager::set_auto_generate_enabled(checkbox->GetValue()); - if (wxGetApp().preset_bundle != nullptr && wxGetApp().plater() != nullptr) { - const size_t num_physical = wxGetApp().preset_bundle->filament_presets.size(); - wxGetApp().plater()->set_auto_generated_gradient_decision(num_physical, checkbox->GetValue()); - wxGetApp().preset_bundle->update_multi_material_filament_presets(); - wxGetApp().plater()->on_filaments_change(num_physical); - } - } - #ifdef __WXMSW__ if (param == "associate_3mf") { bool pbool = app_config->get("associate_3mf") == "true" ? true : false; @@ -1052,8 +1041,6 @@ PreferencesDialog::PreferencesDialog(wxWindow *parent, wxWindowID id, const wxSt j["auto_flushing"] = value; value = wxGetApp().app_config->get("auto_calculate_when_filament_change"); j["auto_calculate_when_filament_change"] = value; - value = wxGetApp().app_config->get("auto_generate_gradients"); - j["auto_generate_gradients"] = value; agent->track_event("preferences_changed", j.dump()); } } catch(...) {} @@ -1234,6 +1221,7 @@ wxWindow* PreferencesDialog::create_general_page() auto item_use_free_camera_settings = create_item_checkbox(_L("Use free camera"), page, _L("If enabled, use free camera. If not enabled, use constrained camera."), 50, "use_free_camera"); auto swap_pan_rotate = create_item_checkbox(_L("Swap pan and rotate mouse buttons"), page, _L("If enabled, swaps the left and right mouse buttons pan and rotate functions."), 50, "swap_mouse_buttons"); auto reverse_mouse_zoom = create_item_checkbox(_L("Reverse mouse zoom"), page, _L("If enabled, reverses the direction of zoom with mouse wheel."), 50, "reverse_mouse_wheel_zoom"); + auto allow_filament_temp_mixing = create_item_checkbox(_L("Allow high/low temperature filament mixing"), page, _L("If enabled, allows printing with both high-temperature and low-temperature filaments simultaneously."), 50, "allow_filament_temp_mixing"); auto camera_orbit_mult = create_camera_orbit_mult_input(_L("Orbit speed multiplier"), page, _L("Multiplies the orbit speed for finer or coarser camera movement.")); auto item_show_splash_screen = create_item_checkbox(_L("Show splash screen"), page, _L("Show the splash screen during startup."), 50, "show_splash_screen"); @@ -1241,10 +1229,6 @@ wxWindow* PreferencesDialog::create_general_page() auto item_calc_mode = create_item_checkbox(_L("Flushing volumes: Auto-calculate every time the color changed."), page, _L("If enabled, auto-calculate every time the color changed."), 50, "auto_calculate"); auto item_calc_in_long_retract = create_item_checkbox(_L("Flushing volumes: Auto-calculate every time when the filament is changed."), page, _L("If enabled, auto-calculate every time when filament is changed"), 50, "auto_calculate_when_filament_change"); -#if 0 // Developer section and auto-generate gradients — hidden, preserved for potential future re-enablement - auto item_auto_generate_gradients = create_item_checkbox(_L("Mixed filaments: Auto-generate gradients."), page, _L("If enabled, Snapmaker Orca automatically creates gradient mixed filaments from physical filament pairs."), 50, "auto_generate_gradients"); - auto title_full_spectrum = create_item_title(_devL("Developer"), page, _devL("Developer")); -#endif // Developer section and auto-generate gradients auto item_remember_printer_config = create_item_checkbox(_L("Remember printer configuration"), page, _L("If enabled, Orca will remember and switch filament/process configuration for each printer automatically."), 50, "remember_printer_config"); auto item_step_mesh_setting = create_item_checkbox(_L("Show the step mesh parameter setting dialog."), page, _L("If enabled,a parameter settings dialog will appear during STEP file import."), 50, "enable_step_mesh_setting"); auto item_multi_machine = create_item_checkbox(_L("Multi-device Management (Take effect after restarting Snapmaker Orca)."), page, _L("With this option enabled, you can send a task to multiple devices at the same time and manage multiple devices."), 50, "enable_multi_machine"); @@ -1342,23 +1326,20 @@ wxWindow* PreferencesDialog::create_general_page() sizer_page->Add(item_use_free_camera_settings, 0, wxTOP, FromDIP(3)); sizer_page->Add(swap_pan_rotate, 0, wxTOP, FromDIP(3)); sizer_page->Add(reverse_mouse_zoom, 0, wxTOP, FromDIP(3)); + sizer_page->Add(allow_filament_temp_mixing, 0, wxTOP, FromDIP(3)); sizer_page->Add(camera_orbit_mult, 0, wxTOP, FromDIP(3)); sizer_page->Add(item_show_splash_screen, 0, wxTOP, FromDIP(3)); sizer_page->Add(item_hints, 0, wxTOP, FromDIP(3)); + sizer_page->Add(item_calc_in_long_retract, 0, wxTOP, FromDIP(3)); sizer_page->Add(item_multi_machine, 0, wxTOP, FromDIP(3)); sizer_page->Add(item_step_mesh_setting, 0, wxTOP, FromDIP(3)); sizer_page->Add(item_auto_arrange, 0, wxTOP, FromDIP(3)); sizer_page->Add(title_presets, 0, wxTOP | wxEXPAND, FromDIP(20)); sizer_page->Add(item_calc_mode, 0, wxTOP, FromDIP(3)); - sizer_page->Add(item_calc_in_long_retract, 0, wxTOP, FromDIP(3)); sizer_page->Add(item_user_sync, 0, wxTOP, FromDIP(3)); sizer_page->Add(item_system_sync, 0, wxTOP, FromDIP(3)); sizer_page->Add(item_remember_printer_config, 0, wxTOP, FromDIP(3)); sizer_page->Add(item_save_presets, 0, wxTOP, FromDIP(3)); -#if 0 // Developer section and auto-generate gradients — hidden, preserved for potential future re-enablement - sizer_page->Add(title_full_spectrum, 0, wxTOP | wxEXPAND, FromDIP(20)); - sizer_page->Add(item_auto_generate_gradients, 0, wxTOP, FromDIP(3)); -#endif // Developer section and auto-generate gradients //sizer_page->Add(title_network, 0, wxTOP | wxEXPAND, FromDIP(20)); //sizer_page->Add(item_check_stable_version_only, 0, wxTOP, FromDIP(3)); From 86aa2369f964d91e83251df87bdc3b88f77e5737 Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Tue, 26 May 2026 16:45:53 +0800 Subject: [PATCH 077/135] Add confirmation dialog with guard for allow_filament_temp_mixing preference toggle --- src/slic3r/GUI/Preferences.cpp | 58 ++++++++++++++++++++++++++++++++-- 1 file changed, 55 insertions(+), 3 deletions(-) diff --git a/src/slic3r/GUI/Preferences.cpp b/src/slic3r/GUI/Preferences.cpp index 93cc328ae83..40f11f7ff62 100644 --- a/src/slic3r/GUI/Preferences.cpp +++ b/src/slic3r/GUI/Preferences.cpp @@ -18,6 +18,7 @@ #include #include #include +#include #include "sentry_wrapper/SentryWrapper.hpp" @@ -721,7 +722,8 @@ void PreferencesDialog::set_dark_mode() #endif } -wxBoxSizer *PreferencesDialog::create_item_checkbox(wxString title, wxWindow *parent, wxString tooltip, int padding_left, std::string param) +wxBoxSizer *PreferencesDialog::create_item_checkbox(wxString title, wxWindow *parent, wxString tooltip, int padding_left, std::string param, + std::function confirm_cb) { wxBoxSizer *m_sizer_checkbox = new wxBoxSizer(wxHORIZONTAL); @@ -744,7 +746,19 @@ wxBoxSizer *PreferencesDialog::create_item_checkbox(wxString title, wxWindow *pa //// save config - checkbox->Bind(wxEVT_TOGGLEBUTTON, [this, checkbox, param](wxCommandEvent &e) { + auto in_handler = std::make_shared(false); + checkbox->Bind(wxEVT_TOGGLEBUTTON, [this, checkbox, param, confirm_cb, in_handler](wxCommandEvent &e) { + if (*in_handler) { e.Skip(); return; } + if (confirm_cb) { + bool old_val = !checkbox->GetValue(); + bool new_val = checkbox->GetValue(); + bool final_val = confirm_cb(new_val, old_val); + if (final_val != new_val) { + *in_handler = true; + checkbox->SetValue(final_val); + *in_handler = false; + } + } app_config->set_bool(param, checkbox->GetValue()); app_config->save(); @@ -1221,7 +1235,45 @@ wxWindow* PreferencesDialog::create_general_page() auto item_use_free_camera_settings = create_item_checkbox(_L("Use free camera"), page, _L("If enabled, use free camera. If not enabled, use constrained camera."), 50, "use_free_camera"); auto swap_pan_rotate = create_item_checkbox(_L("Swap pan and rotate mouse buttons"), page, _L("If enabled, swaps the left and right mouse buttons pan and rotate functions."), 50, "swap_mouse_buttons"); auto reverse_mouse_zoom = create_item_checkbox(_L("Reverse mouse zoom"), page, _L("If enabled, reverses the direction of zoom with mouse wheel."), 50, "reverse_mouse_wheel_zoom"); - auto allow_filament_temp_mixing = create_item_checkbox(_L("Allow high/low temperature filament mixing"), page, _L("If enabled, allows printing with both high-temperature and low-temperature filaments simultaneously."), 50, "allow_filament_temp_mixing"); + auto allow_filament_temp_mixing = create_item_checkbox(_L("Allow high/low temperature filament mixing"), page, _L("If enabled, allows printing with both high-temperature and low-temperature filaments simultaneously."), 50, "allow_filament_temp_mixing", + [this](bool new_val, bool old_val) -> bool { + // Only confirm when turning ON; allow turning OFF without dialog. + if (!new_val) + return false; + + wxDialog dlg(this, wxID_ANY, _L("高/低温材料混打风险"), wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE); + dlg.SetBackgroundColour(*wxWHITE); + + auto* main_sizer = new wxBoxSizer(wxVERTICAL); + + wxString msg = _L("混合使用打印温度差异较大的耗材,可能导致:\n" + "· 喷嘴阻塞\n" + "· 喷嘴损坏\n" + "· 层间粘附问题\n\n" + "仍要启用高/低温耗材混合打印吗?"); + auto* text = new wxStaticText(&dlg, wxID_ANY, msg); + text->Wrap(FromDIP(400)); + main_sizer->Add(text, 0, wxALL, FromDIP(20)); + + auto* btn_sizer = new wxBoxSizer(wxHORIZONTAL); + auto* btn_yes = new wxButton(&dlg, wxID_YES, _L("是")); + btn_yes->SetBackgroundColour(wxColour(0, 255, 0)); + auto* btn_no = new wxButton(&dlg, wxID_NO, _L("否")); + + btn_yes->Bind(wxEVT_BUTTON, [&dlg](wxCommandEvent&) { dlg.EndModal(wxID_YES); }); + btn_no->Bind(wxEVT_BUTTON, [&dlg](wxCommandEvent&) { dlg.EndModal(wxID_NO); }); + + btn_sizer->AddStretchSpacer(); + btn_sizer->Add(btn_yes, 0, wxALL, FromDIP(5)); + btn_sizer->Add(btn_no, 0, wxALL, FromDIP(5)); + + main_sizer->Add(btn_sizer, 0, wxEXPAND | wxALL, FromDIP(10)); + dlg.SetSizer(main_sizer); + dlg.Fit(); + dlg.CenterOnParent(); + + return dlg.ShowModal() == wxID_YES; + }); auto camera_orbit_mult = create_camera_orbit_mult_input(_L("Orbit speed multiplier"), page, _L("Multiplies the orbit speed for finer or coarser camera movement.")); auto item_show_splash_screen = create_item_checkbox(_L("Show splash screen"), page, _L("Show the splash screen during startup."), 50, "show_splash_screen"); From 175d2c13bcc397fb9b63b28f9d263f10293e0b85 Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Tue, 26 May 2026 17:50:18 +0800 Subject: [PATCH 078/135] Switch filament temp mixing to warning level and add pre-slice confirmation when allowed --- src/libslic3r/Print.cpp | 12 -------- src/slic3r/GUI/Plater.cpp | 62 ++++++++++++++++++++++++++++++++------- 2 files changed, 52 insertions(+), 22 deletions(-) diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index 57e74558c90..16d74616716 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -1098,18 +1098,6 @@ StringObjectException Print::validate(StringObjectException *warning, Polygons* if (extruders.empty()) return { L("No extrusions under current settings.") }; - // High/low temperature filament mixing check — enforced at both the - // real-time UI level (Plater::check_filament_temp_mixing) and here in - // validate() so that the background process does not clear the error - // notification (see Plater::priv::update line ~6507). - if (extruders.size() > 1) { - auto ret = check_multi_filament_valid(*this); - if (!ret.string.empty()) { - ret.type = STRING_EXCEPT_FILAMENTS_DIFFERENT_TEMP; - return ret; - } - } - if (m_config.print_sequence == PrintSequence::ByObject) { if (m_config.timelapse_type == TimelapseType::tlSmooth) return {L("Smooth mode of timelapse is not supported when \"by object\" sequence is enabled.")}; diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 2df76d3597f..16d2fca85f4 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -2099,10 +2099,18 @@ void Sidebar::update_presets(Preset::Type preset_type) err.string = _u8L("Cannot print multiple filaments which have large difference of " "temperature together. Otherwise, the extruder and nozzle may " "be blocked or damaged during printing."); - p->plater->get_notification_manager()->push_validate_error_notification(err); - p->plater->get_partplate_list().get_curr_plate()->update_apply_result_invalid(true); + if (wxGetApp().app_config->get_bool("allow_filament_temp_mixing")) { + p->plater->get_notification_manager()->push_notification( + NotificationType::ValidateWarning, + NotificationManager::NotificationLevel::WarningNotificationLevel, + _u8L("WARNING:") + "\n" + err.string); + } else { + p->plater->get_notification_manager()->push_validate_error_notification(err); + p->plater->get_partplate_list().get_curr_plate()->update_apply_result_invalid(true); + } } else { p->plater->get_notification_manager()->close_notification_of_type(NotificationType::ValidateError); + p->plater->get_notification_manager()->close_notification_of_type(NotificationType::ValidateWarning); p->plater->get_partplate_list().get_curr_plate()->update_apply_result_invalid(false); } @@ -6522,22 +6530,30 @@ unsigned int Plater::priv::update_background_process(bool force_validation, bool this->partplate_list.get_curr_plate()->update_apply_result_invalid(false); notification_manager->set_all_slicing_errors_gray(true); notification_manager->close_notification_of_type(NotificationType::ValidateError); + notification_manager->close_notification_of_type(NotificationType::ValidateWarning); + // Pass a warning from validation and either show a notification, + // or hide the old one. + process_validation_warning(warning); } else { - // Filament mixing detected — show error and keep plate invalid + // Filament mixing detected StringObjectException err; err.type = STRING_EXCEPT_FILAMENTS_DIFFERENT_TEMP; err.string = _u8L("Cannot print multiple filaments which have large difference of " "temperature together. Otherwise, the extruder and nozzle may " "be blocked or damaged during printing."); - notification_manager->push_validate_error_notification(err); - this->partplate_list.get_curr_plate()->update_apply_result_invalid(true); + if (wxGetApp().app_config->get_bool("allow_filament_temp_mixing")) { + notification_manager->push_notification( + NotificationType::ValidateWarning, + NotificationManager::NotificationLevel::WarningNotificationLevel, + _u8L("WARNING:") + "\n" + err.string); + } else { + notification_manager->push_validate_error_notification(err); + this->partplate_list.get_curr_plate()->update_apply_result_invalid(true); + } } if (invalidated != Print::APPLY_STATUS_UNCHANGED && background_processing_enabled()) return_state |= UPDATE_BACKGROUND_PROCESS_RESTART; - // Pass a warning from validation and either show a notification, - // or hide the old one. - process_validation_warning(warning); if (printer_technology == ptFFF) { view3D->get_canvas3d()->reset_sequential_print_clearance(); view3D->get_canvas3d()->set_as_dirty(); @@ -8434,6 +8450,15 @@ void Plater::priv::on_action_open_project(SimpleEvent&) void Plater::priv::on_action_slice_plate(SimpleEvent&) { if (q != nullptr) { + // Pre-slice confirmation for high/low temp filament mixing + if (wxGetApp().app_config->get_bool("allow_filament_temp_mixing") + && !q->check_filament_temp_mixing()) { + wxMessageDialog dlg(q, _L("该组合可能导致风险,是否继续"), + _L("确认"), wxYES_NO | wxICON_WARNING); + if (dlg.ShowModal() != wxID_YES) + return; + } + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << ":received slice plate event\n" ; //BBS update extruder params and speed table before slicing const Slic3r::DynamicPrintConfig& config = wxGetApp().preset_bundle->full_config(); @@ -8453,6 +8478,15 @@ void Plater::priv::on_action_slice_plate(SimpleEvent&) void Plater::priv::on_action_slice_all(SimpleEvent&) { if (q != nullptr) { + // Pre-slice confirmation for high/low temp filament mixing + if (wxGetApp().app_config->get_bool("allow_filament_temp_mixing") + && !q->check_filament_temp_mixing()) { + wxMessageDialog dlg(q, _L("该组合可能导致风险,是否继续"), + _L("确认"), wxYES_NO | wxICON_WARNING); + if (dlg.ShowModal() != wxID_YES) + return; + } + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << ":received slice project event\n" ; //BBS update extruder params and speed table before slicing const Slic3r::DynamicPrintConfig& config = wxGetApp().preset_bundle->full_config(); @@ -14763,10 +14797,18 @@ void Plater::on_config_change(const DynamicPrintConfig &config) err.string = _u8L("Cannot print multiple filaments which have large difference of " "temperature together. Otherwise, the extruder and nozzle may " "be blocked or damaged during printing."); - p->notification_manager->push_validate_error_notification(err); - p->partplate_list.get_curr_plate()->update_apply_result_invalid(true); + if (wxGetApp().app_config->get_bool("allow_filament_temp_mixing")) { + p->notification_manager->push_notification( + NotificationType::ValidateWarning, + NotificationManager::NotificationLevel::WarningNotificationLevel, + _u8L("WARNING:") + "\n" + err.string); + } else { + p->notification_manager->push_validate_error_notification(err); + p->partplate_list.get_curr_plate()->update_apply_result_invalid(true); + } } else { p->notification_manager->close_notification_of_type(NotificationType::ValidateError); + p->notification_manager->close_notification_of_type(NotificationType::ValidateWarning); p->partplate_list.get_curr_plate()->update_apply_result_invalid(false); } } From 7787cd6a307d919c5b688606b6f2a71f6106ffc6 Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Thu, 4 Jun 2026 10:34:43 +0800 Subject: [PATCH 079/135] =?UTF-8?q?Fix=EF=BC=9Amodify=20machine=20start=20?= =?UTF-8?q?gcode?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../profiles/Snapmaker/machine/Snapmaker U1 (0.2 nozzle).json | 2 +- .../profiles/Snapmaker/machine/Snapmaker U1 (0.4 nozzle).json | 2 +- .../profiles/Snapmaker/machine/Snapmaker U1 (0.6 nozzle).json | 2 +- .../profiles/Snapmaker/machine/Snapmaker U1 (0.8 nozzle).json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.2 nozzle).json b/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.2 nozzle).json index c3ef510ca5e..12b3bb62e5b 100644 --- a/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.2 nozzle).json +++ b/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.2 nozzle).json @@ -48,7 +48,7 @@ "20", "12" ], - "machine_start_gcode": ";===== date: 20260128 =====================\\n\\nPRINT_START\\nDEFECT_DETECTION_START\\nSET_PRINT_STATS_INFO TOTAL_LAYER={total_layer_count} CURRENT_LAYER=0\\nTIMELAPSE_START\\nM140 S{bed_temperature_initial_layer_single}\\nM104 T{initial_extruder} S140\\nM204 S10000\\n{if chamber_cooling_mode==0}\\n; 保温\\nSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=180\\n{elsif chamber_cooling_mode==2}\\n; 强冷\\nSET_PURIFIER_MODE MODE=1 DESIRE_TEMP=42 ALARM_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=0\\n{else}\\n; 弱冷\\nSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=0 FAN_SPEED=0.6 DELAY_OFF=180\t\t\\n{endif}\\nG28 X Y\\n;===== 床面异物检测 ========\\nT{initial_extruder}\\nG90\\nDEFECT_DETECTION_DETECT_BED\\n;===== 取放头检测 =================\\nSM_PRINT_CHECK_SWITCH_EXTRUDER\\n\\n;===== 自动进料 & 挤出流量 & 预挤出 ======================\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=1 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=0\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=0\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=2 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=1\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=1\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=3 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=2\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=2\\nSM_PRINT_AUTO_FEED EXTRUDER=3\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=3\\nM104 S0 T0 A0\\nM104 S0 T1 A0\\nM104 S0 T2 A0\\nM104 S0 T3 A0\\nM104 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\n\\n;===== 粗回零 =================\\nT{initial_extruder}\\nM106 S255\\nM106 P2 S0\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\nROUGHLY_CLEAN_NOZZLE_WITH_DISCARD\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nG28 Z I140 J140\\n\\n;===== 检测钢板 =================\\nDETECT_BED_PLATE\\n\\n;===== 深度清洁喷嘴 =================\\nG90\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 S{nozzle_temperature[initial_extruder] - 50}\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_1\\nM104 S{nozzle_temperature[initial_extruder] - 90}\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_2\\n\\n;===== 精回零 =================\\nM106 S255\\nM109 S{nozzle_temperature[initial_extruder] - 90}\\nM190 S{bed_temperature_initial_layer_single}\\nM107 P2\\nG90\\nG0 Z5 F10000\\nWAIT_CHAMBER_TEMP TIMEOUT=180\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{else} \\nG28 Z \\n{endif} \\n\\n\\n;===== 热床调平 =================\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{else} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 \\n{endif} \\n\\n\\n;===== 画起始线 =================\\nG90\\nG1 Z1.5\\nG0 X85 Y1 Z2 F18000\\nM109 S{nozzle_temperature_initial_layer[initial_extruder]}\\nG1 Z0.2\\nM83\\nG1 X185 E15 F360\\nG1 Z1.5\\n\\nG90\\nM106 S0", + "machine_start_gcode": ";===== date: 20260128 =====================\\n\\nPRINT_START\\nDEFECT_DETECTION_START\\nSET_PRINT_STATS_INFO TOTAL_LAYER={total_layer_count} CURRENT_LAYER=0\\nTIMELAPSE_START\\nM140 S{bed_temperature_initial_layer_single}\\nM104 T{initial_extruder} S140\\nM204 S10000\\n{if chamber_cooling_mode==0}\\n; 保温\\nSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=180\\n{elsif chamber_cooling_mode==2}\\n; 强冷\\nSET_PURIFIER_MODE MODE=1 DESIRE_TEMP=42 ALARM_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=0\\n{else}\\n; 弱冷\\nSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=0 FAN_SPEED=0.6 DELAY_OFF=180\t\t\\n{endif}\\nG28 X Y\\nDEFECT_DETECT_NOODLE_FIRST\\n;===== 床面异物检测 ========\\nT{initial_extruder}\\nG90\\nDEFECT_DETECTION_DETECT_BED\\n;===== 取放头检测 =================\\nSM_PRINT_CHECK_SWITCH_EXTRUDER\\n\\n;===== 自动进料 & 挤出流量 & 预挤出 ======================\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=1 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=0\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=0\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=2 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=1\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=1\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=3 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=2\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=2\\nSM_PRINT_AUTO_FEED EXTRUDER=3\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=3\\nM104 S0 T0 A0\\nM104 S0 T1 A0\\nM104 S0 T2 A0\\nM104 S0 T3 A0\\nM104 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\n\\n;===== 粗回零 =================\\nT{initial_extruder}\\nM106 S255\\nM106 P2 S0\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\nROUGHLY_CLEAN_NOZZLE_WITH_DISCARD\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nG28 Z I140 J140\\n\\n;===== 检测钢板 =================\\nDETECT_BED_PLATE\\n\\n;===== 深度清洁喷嘴 =================\\nG90\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 S{nozzle_temperature[initial_extruder] - 50}\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_1\\nM104 S{nozzle_temperature[initial_extruder] - 90}\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_2\\n\\n;===== 精回零 =================\\nM106 S255\\nM109 S{nozzle_temperature[initial_extruder] - 90}\\nM190 S{bed_temperature_initial_layer_single}\\nM107 P2\\nG90\\nG0 Z5 F10000\\nWAIT_CHAMBER_TEMP TIMEOUT=180\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{else} \\nG28 Z \\n{endif} \\n\\n\\n;===== 热床调平 =================\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{else} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 \\n{endif} \\n\\n\\n;===== 画起始线 =================\\nG90\\nG1 Z1.5\\nG0 X85 Y1 Z2 F18000\\nM109 S{nozzle_temperature_initial_layer[initial_extruder]}\\nG1 Z0.2\\nM83\\nG1 X185 E15 F360\\nG1 Z1.5\\n\\nG90\\nM106 S0", "machine_tool_change_time": "5", "max_layer_height": [ "0.14", diff --git a/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.4 nozzle).json b/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.4 nozzle).json index c9d896e1303..597c9684008 100644 --- a/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.4 nozzle).json +++ b/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.4 nozzle).json @@ -48,7 +48,7 @@ "20", "12" ], - "machine_start_gcode": ";===== date: 20260128 =====================\\n\\nPRINT_START\\nDEFECT_DETECTION_START\\nSET_PRINT_STATS_INFO TOTAL_LAYER={total_layer_count} CURRENT_LAYER=0\\nTIMELAPSE_START\\nM140 S{bed_temperature_initial_layer_single}\\nM104 T{initial_extruder} S140\\nM204 S10000\\n{if chamber_cooling_mode==0}\\n; 保温\\nSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=180\\n{elsif chamber_cooling_mode==2}\\n; 强冷\\nSET_PURIFIER_MODE MODE=1 DESIRE_TEMP=42 ALARM_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=0\\n{else}\\n; 弱冷\\nSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=0 FAN_SPEED=0.6 DELAY_OFF=180\t\t\\n{endif}\\nG28 X Y\\n;===== 床面异物检测 ========\\nT{initial_extruder}\\nG90\\nDEFECT_DETECTION_DETECT_BED\\n;===== 取放头检测 =================\\nSM_PRINT_CHECK_SWITCH_EXTRUDER\\n\\n;===== 自动进料 & 挤出流量 & 预挤出 ======================\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=1 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=0\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=0\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=2 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=1\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=1\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=3 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=2\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=2\\nSM_PRINT_AUTO_FEED EXTRUDER=3\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=3\\nM104 S0 T0 A0\\nM104 S0 T1 A0\\nM104 S0 T2 A0\\nM104 S0 T3 A0\\nM104 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\n\\n;===== 粗回零 =================\\nT{initial_extruder}\\nM106 S255\\nM106 P2 S0\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\nROUGHLY_CLEAN_NOZZLE_WITH_DISCARD\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nG28 Z I140 J140\\n\\n;===== 检测钢板 =================\\nDETECT_BED_PLATE\\n\\n;===== 深度清洁喷嘴 =================\\nG90\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 S{nozzle_temperature[initial_extruder] - 50}\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_1\\nM104 S{nozzle_temperature[initial_extruder] - 90}\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_2\\n\\n;===== 精回零 =================\\nM106 S255\\nM109 S{nozzle_temperature[initial_extruder] - 90}\\nM190 S{bed_temperature_initial_layer_single}\\nM107 P2\\nG90\\nG0 Z5 F10000\\nWAIT_CHAMBER_TEMP TIMEOUT=180\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{else} \\nG28 Z \\n{endif} \\n\\n\\n;===== 热床调平 =================\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{else} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 \\n{endif} \\n\\n\\n;===== 画起始线 =================\\nG90\\nG1 Z1.5\\nG0 X85 Y1 Z2 F18000\\nM109 S{nozzle_temperature_initial_layer[initial_extruder]}\\nG1 Z0.2\\nM83\\nG1 X185 E15 F360\\nG1 Z1.5\\n\\nG90\\nM106 S0", + "machine_start_gcode": ";===== date: 20260128 =====================\\n\\nPRINT_START\\nDEFECT_DETECTION_START\\nSET_PRINT_STATS_INFO TOTAL_LAYER={total_layer_count} CURRENT_LAYER=0\\nTIMELAPSE_START\\nM140 S{bed_temperature_initial_layer_single}\\nM104 T{initial_extruder} S140\\nM204 S10000\\n{if chamber_cooling_mode==0}\\n; 保温\\nSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=180\\n{elsif chamber_cooling_mode==2}\\n; 强冷\\nSET_PURIFIER_MODE MODE=1 DESIRE_TEMP=42 ALARM_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=0\\n{else}\\n; 弱冷\\nSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=0 FAN_SPEED=0.6 DELAY_OFF=180\t\t\\n{endif}\\nG28 X Y\\nDEFECT_DETECT_NOODLE_FIRST\\n;===== 床面异物检测 ========\\nT{initial_extruder}\\nG90\\nDEFECT_DETECTION_DETECT_BED\\n;===== 取放头检测 =================\\nSM_PRINT_CHECK_SWITCH_EXTRUDER\\n\\n;===== 自动进料 & 挤出流量 & 预挤出 ======================\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=1 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=0\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=0\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=2 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=1\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=1\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=3 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=2\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=2\\nSM_PRINT_AUTO_FEED EXTRUDER=3\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=3\\nM104 S0 T0 A0\\nM104 S0 T1 A0\\nM104 S0 T2 A0\\nM104 S0 T3 A0\\nM104 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\n\\n;===== 粗回零 =================\\nT{initial_extruder}\\nM106 S255\\nM106 P2 S0\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\nROUGHLY_CLEAN_NOZZLE_WITH_DISCARD\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nG28 Z I140 J140\\n\\n;===== 检测钢板 =================\\nDETECT_BED_PLATE\\n\\n;===== 深度清洁喷嘴 =================\\nG90\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 S{nozzle_temperature[initial_extruder] - 50}\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_1\\nM104 S{nozzle_temperature[initial_extruder] - 90}\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_2\\n\\n;===== 精回零 =================\\nM106 S255\\nM109 S{nozzle_temperature[initial_extruder] - 90}\\nM190 S{bed_temperature_initial_layer_single}\\nM107 P2\\nG90\\nG0 Z5 F10000\\nWAIT_CHAMBER_TEMP TIMEOUT=180\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{else} \\nG28 Z \\n{endif} \\n\\n\\n;===== 热床调平 =================\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{else} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 \\n{endif} \\n\\n\\n;===== 画起始线 =================\\nG90\\nG1 Z1.5\\nG0 X85 Y1 Z2 F18000\\nM109 S{nozzle_temperature_initial_layer[initial_extruder]}\\nG1 Z0.2\\nM83\\nG1 X185 E15 F360\\nG1 Z1.5\\n\\nG90\\nM106 S0", "machine_tool_change_time": "5", "max_layer_height": [ "0.32", diff --git a/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.6 nozzle).json b/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.6 nozzle).json index 441e975af86..9e6b751fa3c 100644 --- a/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.6 nozzle).json +++ b/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.6 nozzle).json @@ -48,7 +48,7 @@ "20", "12" ], - "machine_start_gcode": ";===== date: 20260128 =====================\\n\\nPRINT_START\\nDEFECT_DETECTION_START\\nSET_PRINT_STATS_INFO TOTAL_LAYER={total_layer_count} CURRENT_LAYER=0\\nTIMELAPSE_START\\nM140 S{bed_temperature_initial_layer_single}\\nM104 T{initial_extruder} S140\\nM204 S10000\\n{if chamber_cooling_mode==0}\\n; 保温\\nSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=180\\n{elsif chamber_cooling_mode==2}\\n; 强冷\\nSET_PURIFIER_MODE MODE=1 DESIRE_TEMP=42 ALARM_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=0\\n{else}\\n; 弱冷\\nSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=0 FAN_SPEED=0.6 DELAY_OFF=180\t\t\\n{endif}\\nG28 X Y\\n;===== 床面异物检测 ========\\nT{initial_extruder}\\nG90\\nDEFECT_DETECTION_DETECT_BED\\n;===== 取放头检测 =================\\nSM_PRINT_CHECK_SWITCH_EXTRUDER\\n\\n;===== 自动进料 & 挤出流量 & 预挤出 ======================\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=1 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=0\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=0\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=2 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=1\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=1\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=3 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=2\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=2\\nSM_PRINT_AUTO_FEED EXTRUDER=3\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=3\\nM104 S0 T0 A0\\nM104 S0 T1 A0\\nM104 S0 T2 A0\\nM104 S0 T3 A0\\nM104 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\n\\n;===== 粗回零 =================\\nT{initial_extruder}\\nM106 S255\\nM106 P2 S0\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\nROUGHLY_CLEAN_NOZZLE_WITH_DISCARD\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nG28 Z I140 J140\\n\\n;===== 检测钢板 =================\\nDETECT_BED_PLATE\\n\\n;===== 深度清洁喷嘴 =================\\nG90\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 S{nozzle_temperature[initial_extruder] - 50}\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_1\\nM104 S{nozzle_temperature[initial_extruder] - 90}\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_2\\n\\n;===== 精回零 =================\\nM106 S255\\nM109 S{nozzle_temperature[initial_extruder] - 90}\\nM190 S{bed_temperature_initial_layer_single}\\nM107 P2\\nG90\\nG0 Z5 F10000\\nWAIT_CHAMBER_TEMP TIMEOUT=180\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{else} \\nG28 Z \\n{endif} \\n\\n\\n;===== 热床调平 =================\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{else} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 \\n{endif} \\n\\n\\n;===== 画起始线 =================\\nG90\\nG1 Z1.5\\nG0 X85 Y1 Z2 F18000\\nM109 S{nozzle_temperature_initial_layer[initial_extruder]}\\nG1 Z0.2\\nM83\\nG1 X185 E15 F360\\nG1 Z1.5\\n\\nG90\\nM106 S0", + "machine_start_gcode": ";===== date: 20260128 =====================\\n\\nPRINT_START\\nDEFECT_DETECTION_START\\nSET_PRINT_STATS_INFO TOTAL_LAYER={total_layer_count} CURRENT_LAYER=0\\nTIMELAPSE_START\\nM140 S{bed_temperature_initial_layer_single}\\nM104 T{initial_extruder} S140\\nM204 S10000\\n{if chamber_cooling_mode==0}\\n; 保温\\nSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=180\\n{elsif chamber_cooling_mode==2}\\n; 强冷\\nSET_PURIFIER_MODE MODE=1 DESIRE_TEMP=42 ALARM_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=0\\n{else}\\n; 弱冷\\nSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=0 FAN_SPEED=0.6 DELAY_OFF=180\t\t\\n{endif}\\nG28 X Y\\nDEFECT_DETECT_NOODLE_FIRST\\n;===== 床面异物检测 ========\\nT{initial_extruder}\\nG90\\nDEFECT_DETECTION_DETECT_BED\\n;===== 取放头检测 =================\\nSM_PRINT_CHECK_SWITCH_EXTRUDER\\n\\n;===== 自动进料 & 挤出流量 & 预挤出 ======================\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=1 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=0\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=0\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=2 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=1\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=1\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=3 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=2\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=2\\nSM_PRINT_AUTO_FEED EXTRUDER=3\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=3\\nM104 S0 T0 A0\\nM104 S0 T1 A0\\nM104 S0 T2 A0\\nM104 S0 T3 A0\\nM104 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\n\\n;===== 粗回零 =================\\nT{initial_extruder}\\nM106 S255\\nM106 P2 S0\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\nROUGHLY_CLEAN_NOZZLE_WITH_DISCARD\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nG28 Z I140 J140\\n\\n;===== 检测钢板 =================\\nDETECT_BED_PLATE\\n\\n;===== 深度清洁喷嘴 =================\\nG90\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 S{nozzle_temperature[initial_extruder] - 50}\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_1\\nM104 S{nozzle_temperature[initial_extruder] - 90}\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_2\\n\\n;===== 精回零 =================\\nM106 S255\\nM109 S{nozzle_temperature[initial_extruder] - 90}\\nM190 S{bed_temperature_initial_layer_single}\\nM107 P2\\nG90\\nG0 Z5 F10000\\nWAIT_CHAMBER_TEMP TIMEOUT=180\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{else} \\nG28 Z \\n{endif} \\n\\n\\n;===== 热床调平 =================\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{else} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 \\n{endif} \\n\\n\\n;===== 画起始线 =================\\nG90\\nG1 Z1.5\\nG0 X85 Y1 Z2 F18000\\nM109 S{nozzle_temperature_initial_layer[initial_extruder]}\\nG1 Z0.2\\nM83\\nG1 X185 E15 F360\\nG1 Z1.5\\n\\nG90\\nM106 S0", "machine_tool_change_time": "5", "max_layer_height": [ "0.42", diff --git a/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.8 nozzle).json b/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.8 nozzle).json index 1c8255b1505..7c69a864f55 100644 --- a/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.8 nozzle).json +++ b/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.8 nozzle).json @@ -48,7 +48,7 @@ "20", "12" ], - "machine_start_gcode": ";===== date: 20260128 =====================\\n\\nPRINT_START\\nDEFECT_DETECTION_START\\nSET_PRINT_STATS_INFO TOTAL_LAYER={total_layer_count} CURRENT_LAYER=0\\nTIMELAPSE_START\\nM140 S{bed_temperature_initial_layer_single}\\nM104 T{initial_extruder} S140\\nM204 S10000\\n{if chamber_cooling_mode==0}\\n; 保温\\nSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=180\\n{elsif chamber_cooling_mode==2}\\n; 强冷\\nSET_PURIFIER_MODE MODE=1 DESIRE_TEMP=42 ALARM_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=0\\n{else}\\n; 弱冷\\nSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=0 FAN_SPEED=0.6 DELAY_OFF=180\t\t\\n{endif}\\nG28 X Y\\n;===== 床面异物检测 ========\\nT{initial_extruder}\\nG90\\nDEFECT_DETECTION_DETECT_BED\\n;===== 取放头检测 =================\\nSM_PRINT_CHECK_SWITCH_EXTRUDER\\n\\n;===== 自动进料 & 挤出流量 & 预挤出 ======================\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=1 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=0\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=0\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=2 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=1\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=1\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=3 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=2\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=2\\nSM_PRINT_AUTO_FEED EXTRUDER=3\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=3\\nM104 S0 T0 A0\\nM104 S0 T1 A0\\nM104 S0 T2 A0\\nM104 S0 T3 A0\\nM104 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\n\\n;===== 粗回零 =================\\nT{initial_extruder}\\nM106 S255\\nM106 P2 S0\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\nROUGHLY_CLEAN_NOZZLE_WITH_DISCARD\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nG28 Z I140 J140\\n\\n;===== 检测钢板 =================\\nDETECT_BED_PLATE\\n\\n;===== 深度清洁喷嘴 =================\\nG90\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 S{nozzle_temperature[initial_extruder] - 50}\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_1\\nM104 S{nozzle_temperature[initial_extruder] - 90}\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_2\\n\\n;===== 精回零 =================\\nM106 S255\\nM109 S{nozzle_temperature[initial_extruder] - 90}\\nM190 S{bed_temperature_initial_layer_single}\\nM107 P2\\nG90\\nG0 Z5 F10000\\nWAIT_CHAMBER_TEMP TIMEOUT=180\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{else} \\nG28 Z \\n{endif} \\n\\n\\n;===== 热床调平 =================\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{else} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 \\n{endif} \\n\\n\\n;===== 画起始线 =================\\nG90\\nG1 Z1.5\\nG0 X85 Y1 Z2 F18000\\nM109 S{nozzle_temperature_initial_layer[initial_extruder]}\\nG1 Z0.2\\nM83\\nG1 X185 E15 F360\\nG1 Z1.5\\n\\nG90\\nM106 S0", + "machine_start_gcode": ";===== date: 20260128 =====================\\n\\nPRINT_START\\nDEFECT_DETECTION_START\\nSET_PRINT_STATS_INFO TOTAL_LAYER={total_layer_count} CURRENT_LAYER=0\\nTIMELAPSE_START\\nM140 S{bed_temperature_initial_layer_single}\\nM104 T{initial_extruder} S140\\nM204 S10000\\n{if chamber_cooling_mode==0}\\n; 保温\\nSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=180\\n{elsif chamber_cooling_mode==2}\\n; 强冷\\nSET_PURIFIER_MODE MODE=1 DESIRE_TEMP=42 ALARM_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=0\\n{else}\\n; 弱冷\\nSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=0 FAN_SPEED=0.6 DELAY_OFF=180\t\t\\n{endif}\\nG28 X Y\\nDEFECT_DETECT_NOODLE_FIRST\\n;===== 床面异物检测 ========\\nT{initial_extruder}\\nG90\\nDEFECT_DETECTION_DETECT_BED\\n;===== 取放头检测 =================\\nSM_PRINT_CHECK_SWITCH_EXTRUDER\\n\\n;===== 自动进料 & 挤出流量 & 预挤出 ======================\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=1 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=0\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=0\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=2 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=1\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=1\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=3 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=2\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=2\\nSM_PRINT_AUTO_FEED EXTRUDER=3\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=3\\nM104 S0 T0 A0\\nM104 S0 T1 A0\\nM104 S0 T2 A0\\nM104 S0 T3 A0\\nM104 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\n\\n;===== 粗回零 =================\\nT{initial_extruder}\\nM106 S255\\nM106 P2 S0\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\nROUGHLY_CLEAN_NOZZLE_WITH_DISCARD\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nG28 Z I140 J140\\n\\n;===== 检测钢板 =================\\nDETECT_BED_PLATE\\n\\n;===== 深度清洁喷嘴 =================\\nG90\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 S{nozzle_temperature[initial_extruder] - 50}\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_1\\nM104 S{nozzle_temperature[initial_extruder] - 90}\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_2\\n\\n;===== 精回零 =================\\nM106 S255\\nM109 S{nozzle_temperature[initial_extruder] - 90}\\nM190 S{bed_temperature_initial_layer_single}\\nM107 P2\\nG90\\nG0 Z5 F10000\\nWAIT_CHAMBER_TEMP TIMEOUT=180\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{else} \\nG28 Z \\n{endif} \\n\\n\\n;===== 热床调平 =================\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{else} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 \\n{endif} \\n\\n\\n;===== 画起始线 =================\\nG90\\nG1 Z1.5\\nG0 X85 Y1 Z2 F18000\\nM109 S{nozzle_temperature_initial_layer[initial_extruder]}\\nG1 Z0.2\\nM83\\nG1 X185 E15 F360\\nG1 Z1.5\\n\\nG90\\nM106 S0", "machine_tool_change_time": "5", "max_layer_height": [ "0.56", From 4e5892d40e0b4b5e94c4fe3636cd3a98094284fb Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Thu, 4 Jun 2026 11:08:17 +0800 Subject: [PATCH 080/135] Remove .hermes/ from git tracking --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index f2f47fb245a..5a985196ab3 100644 --- a/.gitignore +++ b/.gitignore @@ -40,4 +40,5 @@ resources/profiles/user/default *.code-workspace deps_src/build/ .claude/ -CLAUDE.md \ No newline at end of file +CLAUDE.md +.hermes/ From f885f57ffc4e07dd018631dd4ee2c2d9352424e9 Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Thu, 4 Jun 2026 11:42:07 +0800 Subject: [PATCH 081/135] Fix build errors: WipeTower2::generate signature, Plater method mismatches, and missing Sidebar stubs --- src/libslic3r/Print.cpp | 3 ++- src/slic3r/GUI/Plater.cpp | 24 ++++++++++++++++++++++-- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index 16d74616716..efde90ad054 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -2895,7 +2895,8 @@ void Print::_make_wipe_tower() // Generate the wipe tower layers. m_wipe_tower_data.tool_changes.reserve(m_wipe_tower_data.tool_ordering.layer_tools().size()); - wipe_tower.generate(m_wipe_tower_data.tool_changes); + m_wipe_tower_data.local_z_tool_changes.reserve(m_wipe_tower_data.tool_ordering.layer_tools().size()); + wipe_tower.generate(m_wipe_tower_data.tool_changes, m_wipe_tower_data.local_z_tool_changes); m_wipe_tower_data.depth = wipe_tower.get_depth(); m_wipe_tower_data.z_and_depth_pairs = wipe_tower.get_z_and_depth_pairs(); m_wipe_tower_data.brim_width = wipe_tower.get_brim_width(); diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 16d2fca85f4..b75b148aa12 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -2499,6 +2499,26 @@ void Sidebar::edit_filament() { p->editing_filament = p->m_menu_filament_id; // sync with TabPresetComboxBox's m_filament_idx } +void Sidebar::update_color_mix_panel() +{ + // Stub: mixed filament color panel not yet implemented in this version. +} + +void Sidebar::update_mixed_filament_panel(bool sync_manager) +{ + // Stub: mixed filament panel not yet implemented in this version. +} + +std::vector Sidebar::get_ui_ordered_filament_ids() const +{ + const size_t num_physical = static_cast(std::max(wxGetApp().filaments_cnt(), 0)); + std::vector ordered_filament_ids; + ordered_filament_ids.reserve(num_physical); + for (size_t idx = 0; idx < num_physical; ++idx) + ordered_filament_ids.emplace_back(unsigned(idx + 1)); + return ordered_filament_ids; +} + void Sidebar::change_filament(size_t from_id, size_t to_id) @@ -14517,7 +14537,7 @@ bool Plater::search_string_getter(int idx, const char** label, const char** tool return false; } -void Plater::on_filaments_delete(size_t num_filaments, size_t filament_id, int replace_filament_id) +void Plater::on_filaments_delete(size_t num_filaments, size_t filament_id, int replace_filament_id, const std::vector& is_mixed_snapshot) { // only update elements in plater update_filament_colors_in_full_config(); @@ -14888,7 +14908,7 @@ void Plater::on_activate() } // Get vector of extruder colors considering filament color, if extruder color is undefined. -std::vector Plater::get_extruder_colors_from_plater_config(const GCodeProcessorResult* const result) const +std::vector Plater::get_extruder_colors_from_plater_config(const GCodeProcessorResult* const result, bool include_mixed) const { if (wxGetApp().is_gcode_viewer() && result != nullptr) return result->extruder_colors; From 5a333c77f9421f52534c89dafbac220983f26f2a Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Thu, 4 Jun 2026 17:29:36 +0800 Subject: [PATCH 082/135] Replace chamber cooling magic numbers with named constants Co-Authored-By: Claude Opus 4.7 --- src/libslic3r/GCode.cpp | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index 946b4d85ec4..fca61264939 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -2700,17 +2700,21 @@ void GCode::_do_export(Print& print, GCodeOutputStream& file, ThumbnailsGenerato } // Compute chamber cooling mode based on all filaments used on this plate. - // 0 = keep warm (all high-temp), 1 = weak cooling, 2 = strong cooling. + constexpr int kChamberCoolingKeepWarm = 0; + constexpr int kChamberCoolingWeak = 1; + constexpr int kChamberCoolingStrong = 2; + constexpr int kVitrificationStrongCool = 50; // <= 50: trigger strong cooling + constexpr int kVitrificationWeakCool = 70; // <= 70: trigger weak cooling (and > 50) { - int chamber_cooling_mode = 0; + int chamber_cooling_mode = kChamberCoolingKeepWarm; for (unsigned int extruder : tool_ordering.all_extruders()) { if (!m_config.filament_is_high_temperature.get_at(extruder)) { int vitrification = m_config.temperature_vitrification.get_at(extruder); - if (vitrification <= 50) { - chamber_cooling_mode = 2; + if (vitrification <= kVitrificationStrongCool) { + chamber_cooling_mode = kChamberCoolingStrong; break; - } else if (vitrification <= 70) { - chamber_cooling_mode = 1; + } else if (vitrification <= kVitrificationWeakCool) { + chamber_cooling_mode = kChamberCoolingWeak; } } } From b6b812c3c2f4362f21a2997383c9e1a77b61cb3c Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Thu, 4 Jun 2026 17:32:53 +0800 Subject: [PATCH 083/135] Rename in_handler to reentry_guard for clarity Co-Authored-By: Claude Opus 4.7 --- src/slic3r/GUI/Preferences.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/slic3r/GUI/Preferences.cpp b/src/slic3r/GUI/Preferences.cpp index 40f11f7ff62..613e064f698 100644 --- a/src/slic3r/GUI/Preferences.cpp +++ b/src/slic3r/GUI/Preferences.cpp @@ -746,17 +746,17 @@ wxBoxSizer *PreferencesDialog::create_item_checkbox(wxString title, wxWindow *pa //// save config - auto in_handler = std::make_shared(false); - checkbox->Bind(wxEVT_TOGGLEBUTTON, [this, checkbox, param, confirm_cb, in_handler](wxCommandEvent &e) { - if (*in_handler) { e.Skip(); return; } + auto reentry_guard = std::make_shared(false); + checkbox->Bind(wxEVT_TOGGLEBUTTON, [this, checkbox, param, confirm_cb, reentry_guard](wxCommandEvent &e) { + if (*reentry_guard) { e.Skip(); return; } if (confirm_cb) { bool old_val = !checkbox->GetValue(); bool new_val = checkbox->GetValue(); bool final_val = confirm_cb(new_val, old_val); if (final_val != new_val) { - *in_handler = true; + *reentry_guard = true; checkbox->SetValue(final_val); - *in_handler = false; + *reentry_guard = false; } } app_config->set_bool(param, checkbox->GetValue()); From 3f931969a62ef5a6d08aa90fd8a6f071c2fd5413 Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Thu, 4 Jun 2026 19:08:54 +0800 Subject: [PATCH 084/135] Add missing trigger points for filament temp mixing detection Consolidate duplicated notification logic into sync_filament_temp_mixing_notification() and hook it into model add/remove (object_list_changed), filament assignment changes (set_extruder_for_selected_items, update_filament_in_config), and MMU painting (update_model_object) so the mixing warning updates immediately when plate composition changes. Co-Authored-By: Claude Opus 4.7 --- src/slic3r/GUI/GUI_ObjectList.cpp | 2 + .../GUI/Gizmos/GLGizmoMmuSegmentation.cpp | 1 + src/slic3r/GUI/Plater.cpp | 94 +++++++------------ src/slic3r/GUI/Plater.hpp | 6 ++ 4 files changed, 41 insertions(+), 62 deletions(-) diff --git a/src/slic3r/GUI/GUI_ObjectList.cpp b/src/slic3r/GUI/GUI_ObjectList.cpp index 5ec57c04513..6072ebeca1a 100644 --- a/src/slic3r/GUI/GUI_ObjectList.cpp +++ b/src/slic3r/GUI/GUI_ObjectList.cpp @@ -1099,6 +1099,7 @@ void ObjectList::update_filament_in_config(const wxDataViewItem& item) // update scene wxGetApp().plater()->update(); + wxGetApp().plater()->sync_filament_temp_mixing_notification(); } void ObjectList::update_name_in_model(const wxDataViewItem& item) const @@ -5921,6 +5922,7 @@ void ObjectList::set_extruder_for_selected_items(const int extruder) // update scene wxGetApp().plater()->update(); + wxGetApp().plater()->sync_filament_temp_mixing_notification(); // BBS: update extruder/filament column Refresh(); diff --git a/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp b/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp index 4f2704be870..0435434a4e9 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp @@ -1000,6 +1000,7 @@ void GLGizmoMmuSegmentation::update_model_object() wxGetApp().obj_list()->update_info_items(obj_idx); wxGetApp().plater()->get_partplate_list().notify_instance_update(obj_idx, 0); m_parent.post_event(SimpleEvent(EVT_GLCANVAS_SCHEDULE_BACKGROUND_PROCESS)); + wxGetApp().plater()->sync_filament_temp_mixing_notification(); } } diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index b75b148aa12..588a88d9261 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -2093,26 +2093,7 @@ void Sidebar::update_presets(Preset::Type preset_type) update_dynamic_filament_list(); // Real-time high/low temperature filament mixing check after filament preset change - if (!p->plater->check_filament_temp_mixing()) { - StringObjectException err; - err.type = STRING_EXCEPT_FILAMENTS_DIFFERENT_TEMP; - err.string = _u8L("Cannot print multiple filaments which have large difference of " - "temperature together. Otherwise, the extruder and nozzle may " - "be blocked or damaged during printing."); - if (wxGetApp().app_config->get_bool("allow_filament_temp_mixing")) { - p->plater->get_notification_manager()->push_notification( - NotificationType::ValidateWarning, - NotificationManager::NotificationLevel::WarningNotificationLevel, - _u8L("WARNING:") + "\n" + err.string); - } else { - p->plater->get_notification_manager()->push_validate_error_notification(err); - p->plater->get_partplate_list().get_curr_plate()->update_apply_result_invalid(true); - } - } else { - p->plater->get_notification_manager()->close_notification_of_type(NotificationType::ValidateError); - p->plater->get_notification_manager()->close_notification_of_type(NotificationType::ValidateWarning); - p->plater->get_partplate_list().get_curr_plate()->update_apply_result_invalid(false); - } + p->plater->sync_filament_temp_mixing_notification(); break; } @@ -6047,6 +6028,8 @@ void Plater::priv::object_list_changed() main_frame->update_slice_print_status(MainFrame::eEventObjectUpdate, can_slice); wxGetApp().params_panel()->notify_object_config_changed(); + + q->sync_filament_temp_mixing_notification(); } void Plater::priv::select_curr_plate_all() @@ -6545,31 +6528,10 @@ unsigned int Plater::priv::update_background_process(bool force_validation, bool // cases (e.g. wall_filament changes that haven't propagated to // PrintRegions yet). check_filament_temp_mixing() reads directly // from full_config() and is always current. - bool filament_ok = q->check_filament_temp_mixing(); + bool filament_ok = q->sync_filament_temp_mixing_notification(); if (filament_ok) { - this->partplate_list.get_curr_plate()->update_apply_result_invalid(false); notification_manager->set_all_slicing_errors_gray(true); - notification_manager->close_notification_of_type(NotificationType::ValidateError); - notification_manager->close_notification_of_type(NotificationType::ValidateWarning); - // Pass a warning from validation and either show a notification, - // or hide the old one. process_validation_warning(warning); - } else { - // Filament mixing detected - StringObjectException err; - err.type = STRING_EXCEPT_FILAMENTS_DIFFERENT_TEMP; - err.string = _u8L("Cannot print multiple filaments which have large difference of " - "temperature together. Otherwise, the extruder and nozzle may " - "be blocked or damaged during printing."); - if (wxGetApp().app_config->get_bool("allow_filament_temp_mixing")) { - notification_manager->push_notification( - NotificationType::ValidateWarning, - NotificationManager::NotificationLevel::WarningNotificationLevel, - _u8L("WARNING:") + "\n" + err.string); - } else { - notification_manager->push_validate_error_notification(err); - this->partplate_list.get_curr_plate()->update_apply_result_invalid(true); - } } if (invalidated != Print::APPLY_STATUS_UNCHANGED && background_processing_enabled()) return_state |= UPDATE_BACKGROUND_PROCESS_RESTART; @@ -14721,6 +14683,33 @@ bool Plater::check_filament_temp_mixing() return compatible; } +bool Plater::sync_filament_temp_mixing_notification() +{ + if (check_filament_temp_mixing()) { + get_notification_manager()->close_notification_of_type(NotificationType::ValidateError); + get_notification_manager()->close_notification_of_type(NotificationType::ValidateWarning); + get_partplate_list().get_curr_plate()->update_apply_result_invalid(false); + return true; + } + + StringObjectException err; + err.type = STRING_EXCEPT_FILAMENTS_DIFFERENT_TEMP; + err.string = _u8L("Cannot print multiple filaments which have large difference of " + "temperature together. Otherwise, the extruder and nozzle may " + "be blocked or damaged during printing."); + + if (wxGetApp().app_config->get_bool("allow_filament_temp_mixing")) { + get_notification_manager()->push_notification( + NotificationType::ValidateWarning, + NotificationManager::NotificationLevel::WarningNotificationLevel, + _u8L("WARNING:") + "\n" + err.string); + } else { + get_notification_manager()->push_validate_error_notification(err); + get_partplate_list().get_curr_plate()->update_apply_result_invalid(true); + } + return false; +} + void Plater::on_config_change(const DynamicPrintConfig &config) { bool update_scheduled = false; @@ -14811,26 +14800,7 @@ void Plater::on_config_change(const DynamicPrintConfig &config) } // Real-time high/low temperature filament mixing check - if (!check_filament_temp_mixing()) { - StringObjectException err; - err.type = STRING_EXCEPT_FILAMENTS_DIFFERENT_TEMP; - err.string = _u8L("Cannot print multiple filaments which have large difference of " - "temperature together. Otherwise, the extruder and nozzle may " - "be blocked or damaged during printing."); - if (wxGetApp().app_config->get_bool("allow_filament_temp_mixing")) { - p->notification_manager->push_notification( - NotificationType::ValidateWarning, - NotificationManager::NotificationLevel::WarningNotificationLevel, - _u8L("WARNING:") + "\n" + err.string); - } else { - p->notification_manager->push_validate_error_notification(err); - p->partplate_list.get_curr_plate()->update_apply_result_invalid(true); - } - } else { - p->notification_manager->close_notification_of_type(NotificationType::ValidateError); - p->notification_manager->close_notification_of_type(NotificationType::ValidateWarning); - p->partplate_list.get_curr_plate()->update_apply_result_invalid(false); - } + sync_filament_temp_mixing_notification(); } void Plater::set_bed_shape() const diff --git a/src/slic3r/GUI/Plater.hpp b/src/slic3r/GUI/Plater.hpp index def70264758..a11ef680bef 100644 --- a/src/slic3r/GUI/Plater.hpp +++ b/src/slic3r/GUI/Plater.hpp @@ -507,6 +507,12 @@ class Plater: public wxPanel bool update_filament_colors_in_full_config(); void config_change_notification(const DynamicPrintConfig &config, const std::string& key); void on_config_change(const DynamicPrintConfig &config); + /// Check whether high-temperature and low-temperature filaments are mixed on the current plate. + /// Returns true if compatible (no mixing), false if mixing is detected and error notification is shown. + bool check_filament_temp_mixing(); + /// Sync notification state with current filament temp mixing status. + /// Returns true if compatible (no mixing), false if mixing detected. + bool sync_filament_temp_mixing_notification(); void force_filament_colors_update(); void force_print_bed_update(); // On activating the parent window. From a3032b146961060bc810debf565a199dbe4a18dc Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Thu, 4 Jun 2026 19:19:47 +0800 Subject: [PATCH 085/135] Add filament temp mixing check triggers for object move-off-plate Hook sync_filament_temp_mixing_notification() into Selection::notify_instance_update (3D view drag) and ObjectList::reload_all_plates (arrange, file load, plate operations) so the mixing warning updates immediately when objects are moved on/off plates. Co-Authored-By: Claude Opus 4.7 --- src/slic3r/GUI/GUI_ObjectList.cpp | 1 + src/slic3r/GUI/Selection.cpp | 1 + 2 files changed, 2 insertions(+) diff --git a/src/slic3r/GUI/GUI_ObjectList.cpp b/src/slic3r/GUI/GUI_ObjectList.cpp index 6072ebeca1a..537da98594a 100644 --- a/src/slic3r/GUI/GUI_ObjectList.cpp +++ b/src/slic3r/GUI/GUI_ObjectList.cpp @@ -5982,6 +5982,7 @@ void ObjectList::reload_all_plates(bool notify_partplate) wxGetApp().plater()->update(); // update printable states on canvas wxGetApp().plater()->get_view3D_canvas3D()->update_instance_printable_state_for_objects(obj_idxs); + wxGetApp().plater()->sync_filament_temp_mixing_notification(); } void ObjectList::on_plate_selected(int plate_index) diff --git a/src/slic3r/GUI/Selection.cpp b/src/slic3r/GUI/Selection.cpp index 074d33850fb..9342e5025a2 100644 --- a/src/slic3r/GUI/Selection.cpp +++ b/src/slic3r/GUI/Selection.cpp @@ -1805,6 +1805,7 @@ void Selection::notify_instance_update(int object_idx, int instance_idx) else plate_list.notify_instance_update(object_idx, instance_idx); } + wxGetApp().plater()->sync_filament_temp_mixing_notification(); } void Selection::erase() From bbff036023edf99dcb51f3b8e2afe81059cf3511 Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Fri, 5 Jun 2026 10:16:21 +0800 Subject: [PATCH 086/135] Fix sync_filament_temp_mixing_notification placement for object move Move the sync call from Selection::notify_instance_update to the end of GLCanvas3D::do_move, so it runs only once after all plate membership updates are complete, avoiding flip-flopping between valid/invalid states. Co-Authored-By: Claude Opus 4.7 --- src/slic3r/GUI/GLCanvas3D.cpp | 2 ++ src/slic3r/GUI/Selection.cpp | 1 - 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp index 371d4f6b20e..846eb49611b 100644 --- a/src/slic3r/GUI/GLCanvas3D.cpp +++ b/src/slic3r/GUI/GLCanvas3D.cpp @@ -4826,6 +4826,8 @@ void GLCanvas3D::do_move(const std::string& snapshot_type) reset_sequential_print_clearance(); + wxGetApp().plater()->sync_filament_temp_mixing_notification(); + m_dirty = true; } diff --git a/src/slic3r/GUI/Selection.cpp b/src/slic3r/GUI/Selection.cpp index 9342e5025a2..074d33850fb 100644 --- a/src/slic3r/GUI/Selection.cpp +++ b/src/slic3r/GUI/Selection.cpp @@ -1805,7 +1805,6 @@ void Selection::notify_instance_update(int object_idx, int instance_idx) else plate_list.notify_instance_update(object_idx, instance_idx); } - wxGetApp().plater()->sync_filament_temp_mixing_notification(); } void Selection::erase() From 1ccf137ea7814b755b5daed0f56e3ed91bfb2645 Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Fri, 5 Jun 2026 10:25:42 +0800 Subject: [PATCH 087/135] Make check_filament_temp_mixing per-plate instead of global Limit plate config collection and ModelVolume scanning to objects on the current plate, so removing an object from the plate correctly clears the high/low temperature mixing error notification. Co-Authored-By: Claude Opus 4.7 --- src/slic3r/GUI/Plater.cpp | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 588a88d9261..1bfef28c1b5 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -14639,15 +14639,25 @@ bool Plater::check_filament_temp_mixing() // Collect from full_config first (global defaults for wall/infill/etc.) collect_from_cfg(full_cfg); - // Collect from every plate's config (per-plate overrides) - for (int i = 0; i < p->partplate_list.get_plate_count(); ++i) { - PartPlate* plate = p->partplate_list.get_plate(i); - if (plate) - collect_from_cfg(*plate->config()); - } - - // Collect from ModelVolume painting extruders (multi-material coloring) - for (const ModelObject* mo : wxGetApp().model().objects) { + // Collect from current plate's config only (per-plate overrides) + PartPlate* curr_plate = p->partplate_list.get_curr_plate(); + if (curr_plate) + collect_from_cfg(*curr_plate->config()); + + // Collect from ModelVolume painting extruders for objects on the current plate + for (size_t obj_idx = 0; obj_idx < wxGetApp().model().objects.size(); ++obj_idx) { + const ModelObject* mo = wxGetApp().model().objects[obj_idx]; + bool on_curr_plate = false; + if (curr_plate) { + for (int inst_idx = 0; inst_idx < (int)mo->instances.size(); ++inst_idx) { + if (curr_plate->contain_instance((int)obj_idx, inst_idx)) { + on_curr_plate = true; + break; + } + } + } + if (!on_curr_plate) + continue; for (const ModelVolume* mv : mo->volumes) { for (int eid : mv->get_extruders()) { if (eid >= 1 && eid <= num_filaments) From 80b5248078c7f0becac6d9275305dff406625461 Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Fri, 5 Jun 2026 10:33:08 +0800 Subject: [PATCH 088/135] Remove full_config collection from per-plate filament mixing check The global config's wall_filament etc. always reference the default filament slot, causing false mixing detection when only high-temp models remain on the plate. Only collect config from current plate. Co-Authored-By: Claude Opus 4.7 --- src/slic3r/GUI/Plater.cpp | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 1bfef28c1b5..b747189e3e4 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -14636,10 +14636,7 @@ bool Plater::check_filament_temp_mixing() } }; - // Collect from full_config first (global defaults for wall/infill/etc.) - collect_from_cfg(full_cfg); - - // Collect from current plate's config only (per-plate overrides) + // Collect from current plate's config only PartPlate* curr_plate = p->partplate_list.get_curr_plate(); if (curr_plate) collect_from_cfg(*curr_plate->config()); From c3536053dacd8ab94d42fb0df31b82e063292c37 Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Fri, 5 Jun 2026 11:23:31 +0800 Subject: [PATCH 089/135] Trim trailing whitespace Co-Authored-By: Claude Opus 4.7 --- src/slic3r/GUI/Preferences.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/slic3r/GUI/Preferences.cpp b/src/slic3r/GUI/Preferences.cpp index 613e064f698..d33efac592a 100644 --- a/src/slic3r/GUI/Preferences.cpp +++ b/src/slic3r/GUI/Preferences.cpp @@ -745,7 +745,7 @@ wxBoxSizer *PreferencesDialog::create_item_checkbox(wxString title, wxWindow *pa m_sizer_checkbox->Add(checkbox_title, 0, wxALIGN_CENTER | wxALL, 3); - //// save config + //// save config auto reentry_guard = std::make_shared(false); checkbox->Bind(wxEVT_TOGGLEBUTTON, [this, checkbox, param, confirm_cb, reentry_guard](wxCommandEvent &e) { if (*reentry_guard) { e.Skip(); return; } From 75a7f77e12ff918ceaba2993d61a2d7e4f661711 Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Fri, 5 Jun 2026 11:29:12 +0800 Subject: [PATCH 090/135] Replace custom dialog with Orca-styled MessageDialog for filament mixing warning MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use MessageDialog with SetButtonLabel for proper OrcaSlicer button styling instead of manual wxButton with hardcoded green background. Buttons now read "确认"/"取消" with auto-applied Confirm/Regular ButtonStyle. Co-Authored-By: Claude Opus 4.7 --- src/slic3r/GUI/Preferences.cpp | 31 ++++--------------------------- 1 file changed, 4 insertions(+), 27 deletions(-) diff --git a/src/slic3r/GUI/Preferences.cpp b/src/slic3r/GUI/Preferences.cpp index d33efac592a..6433ca8a475 100644 --- a/src/slic3r/GUI/Preferences.cpp +++ b/src/slic3r/GUI/Preferences.cpp @@ -1241,38 +1241,15 @@ wxWindow* PreferencesDialog::create_general_page() if (!new_val) return false; - wxDialog dlg(this, wxID_ANY, _L("高/低温材料混打风险"), wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE); - dlg.SetBackgroundColour(*wxWHITE); - - auto* main_sizer = new wxBoxSizer(wxVERTICAL); - wxString msg = _L("混合使用打印温度差异较大的耗材,可能导致:\n" "· 喷嘴阻塞\n" "· 喷嘴损坏\n" "· 层间粘附问题\n\n" "仍要启用高/低温耗材混合打印吗?"); - auto* text = new wxStaticText(&dlg, wxID_ANY, msg); - text->Wrap(FromDIP(400)); - main_sizer->Add(text, 0, wxALL, FromDIP(20)); - - auto* btn_sizer = new wxBoxSizer(wxHORIZONTAL); - auto* btn_yes = new wxButton(&dlg, wxID_YES, _L("是")); - btn_yes->SetBackgroundColour(wxColour(0, 255, 0)); - auto* btn_no = new wxButton(&dlg, wxID_NO, _L("否")); - - btn_yes->Bind(wxEVT_BUTTON, [&dlg](wxCommandEvent&) { dlg.EndModal(wxID_YES); }); - btn_no->Bind(wxEVT_BUTTON, [&dlg](wxCommandEvent&) { dlg.EndModal(wxID_NO); }); - - btn_sizer->AddStretchSpacer(); - btn_sizer->Add(btn_yes, 0, wxALL, FromDIP(5)); - btn_sizer->Add(btn_no, 0, wxALL, FromDIP(5)); - - main_sizer->Add(btn_sizer, 0, wxEXPAND | wxALL, FromDIP(10)); - dlg.SetSizer(main_sizer); - dlg.Fit(); - dlg.CenterOnParent(); - - return dlg.ShowModal() == wxID_YES; + MessageDialog dlg(this, msg, _L("高/低温材料混打风险"), wxICON_WARNING | wxOK | wxCANCEL); + dlg.SetButtonLabel(wxID_OK, _L("确认")); + dlg.SetButtonLabel(wxID_CANCEL, _L("取消")); + return dlg.ShowModal() == wxID_OK; }); auto camera_orbit_mult = create_camera_orbit_mult_input(_L("Orbit speed multiplier"), page, _L("Multiplies the orbit speed for finer or coarser camera movement.")); From 5318fdfa01f891115da143edb65d2386fc256382 Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Fri, 5 Jun 2026 11:39:00 +0800 Subject: [PATCH 091/135] Localize filament temp mixing UI strings from Chinese to English Replace hardcoded Chinese text with _L()/_u8L() wrapped English strings: - Preferences dialog title, body, and button labels - Error notification (allow=false): includes guidance to enable in Preferences - Warning notification (allow=true): concise message without guidance - Calibration wizard validation error Co-Authored-By: Claude Opus 4.7 --- src/slic3r/GUI/CalibrationWizardPresetPage.cpp | 2 +- src/slic3r/GUI/Plater.cpp | 11 +++++++---- src/slic3r/GUI/Preferences.cpp | 16 ++++++++-------- 3 files changed, 16 insertions(+), 13 deletions(-) diff --git a/src/slic3r/GUI/CalibrationWizardPresetPage.cpp b/src/slic3r/GUI/CalibrationWizardPresetPage.cpp index 581b9153bb2..c56de4139a2 100644 --- a/src/slic3r/GUI/CalibrationWizardPresetPage.cpp +++ b/src/slic3r/GUI/CalibrationWizardPresetPage.cpp @@ -1073,7 +1073,7 @@ bool CalibrationPresetPage::is_filaments_compatiable(const std::vector } if (!Print::check_multi_filaments_compatibility(filament_types)) { - error_tips = _u8L("Cannot print multiple filaments which have large difference of temperature together. Otherwise, the extruder and nozzle may be blocked or damaged during printing"); + error_tips = _u8L("Detected both high and low temperature materials. Mixed printing may result in extruder clogging, nozzle damage, or layer adhesion issues."); return false; } diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index b747189e3e4..a407ae363c2 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -14701,16 +14701,19 @@ bool Plater::sync_filament_temp_mixing_notification() StringObjectException err; err.type = STRING_EXCEPT_FILAMENTS_DIFFERENT_TEMP; - err.string = _u8L("Cannot print multiple filaments which have large difference of " - "temperature together. Otherwise, the extruder and nozzle may " - "be blocked or damaged during printing."); if (wxGetApp().app_config->get_bool("allow_filament_temp_mixing")) { get_notification_manager()->push_notification( NotificationType::ValidateWarning, NotificationManager::NotificationLevel::WarningNotificationLevel, - _u8L("WARNING:") + "\n" + err.string); + _u8L("WARNING:") + "\n" + + _u8L("Detected both high and low temperature materials. Mixed printing may " + "result in extruder clogging, nozzle damage, or layer adhesion issues.")); } else { + err.string = _u8L("Detected both high and low temperature materials. Mixed printing may " + "result in extruder clogging, nozzle damage, or layer adhesion issues. " + "To continue printing, enable \"Allow mixed printing of high and low " + "temperature materials\" in Preferences."); get_notification_manager()->push_validate_error_notification(err); get_partplate_list().get_curr_plate()->update_apply_result_invalid(true); } diff --git a/src/slic3r/GUI/Preferences.cpp b/src/slic3r/GUI/Preferences.cpp index 6433ca8a475..82291ebe2ca 100644 --- a/src/slic3r/GUI/Preferences.cpp +++ b/src/slic3r/GUI/Preferences.cpp @@ -1241,14 +1241,14 @@ wxWindow* PreferencesDialog::create_general_page() if (!new_val) return false; - wxString msg = _L("混合使用打印温度差异较大的耗材,可能导致:\n" - "· 喷嘴阻塞\n" - "· 喷嘴损坏\n" - "· 层间粘附问题\n\n" - "仍要启用高/低温耗材混合打印吗?"); - MessageDialog dlg(this, msg, _L("高/低温材料混打风险"), wxICON_WARNING | wxOK | wxCANCEL); - dlg.SetButtonLabel(wxID_OK, _L("确认")); - dlg.SetButtonLabel(wxID_CANCEL, _L("取消")); + wxString msg = _L("Mixing materials with significantly different printing temperatures may result in:\n" + "· Extruder clogging\n" + "· Nozzle damage\n" + "· Layer adhesion issues\n\n" + "Do you want to enable this feature?"); + MessageDialog dlg(this, msg, _L("High and Low Temperature Material Mixing Risk"), wxICON_WARNING | wxOK | wxCANCEL); + dlg.SetButtonLabel(wxID_OK, _L("Confirm")); + dlg.SetButtonLabel(wxID_CANCEL, _L("Cancel")); return dlg.ShowModal() == wxID_OK; }); auto camera_orbit_mult = create_camera_orbit_mult_input(_L("Orbit speed multiplier"), page, _L("Multiplies the orbit speed for finer or coarser camera movement.")); From 6134d375e8484ae2dcbc6dd69bfc1d6c335ce232 Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Fri, 5 Jun 2026 11:59:54 +0800 Subject: [PATCH 092/135] Add Chinese translations for filament temp mixing UI strings Co-Authored-By: Claude Opus 4.7 --- .../i18n/zh_CN/Snapmaker_Orca_zh_CN.po | 40 ++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/localization/i18n/zh_CN/Snapmaker_Orca_zh_CN.po b/localization/i18n/zh_CN/Snapmaker_Orca_zh_CN.po index 38532da3efc..ac70bd675ef 100644 --- a/localization/i18n/zh_CN/Snapmaker_Orca_zh_CN.po +++ b/localization/i18n/zh_CN/Snapmaker_Orca_zh_CN.po @@ -15236,4 +15236,42 @@ msgid "Tower ironing area" msgstr "擦拭塔熨烫面积" msgid "Ironing area for prime tower interface layer (where different materials meet)." -msgstr "擦拭塔的起始点熨烫面积" \ No newline at end of file +msgstr "擦拭塔的起始点熨烫面积" + +msgid "High and Low Temperature Material Mixing Risk" +msgstr "高/低温材料混打风险" + +msgid "" +"Mixing materials with significantly different printing temperatures may result in:\n" +"· Extruder clogging\n" +"· Nozzle damage\n" +"· Layer adhesion issues\n" +"\n" +"Do you want to enable this feature?" +msgstr "" +"混合使用打印温度差异较大的材料,可能导致:\n" +"· 挤出机堵塞\n" +"· 喷嘴损坏\n" +"· 层间粘附问题\n" +"\n" +"确认启用此功能吗?" + +msgid "Confirm" +msgstr "确认" + +msgid "Cancel" +msgstr "取消" + +msgid "" +"Detected both high and low temperature materials. Mixed printing may " +"result in extruder clogging, nozzle damage, or layer adhesion issues." +msgstr "检测到高温材料与低温材料同时使用。混合打印可能导致挤出机堵塞、喷嘴损坏或层间粘附问题。" + +msgid "" +"Detected both high and low temperature materials. Mixed printing may " +"result in extruder clogging, nozzle damage, or layer adhesion issues. " +"To continue printing, enable \"Allow mixed printing of high and low " +"temperature materials\" in Preferences." +msgstr "" +"检测到高温材料与低温材料同时使用。混合打印可能导致挤出机堵塞、喷嘴损坏或层间粘附问题。" +"如需继续打印,请在「偏好设置」中开启 \"允许高/低温材料混合打印\"。" From 6668fa684fd5dc687bae6705a095973fe37853b0 Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Fri, 5 Jun 2026 12:00:56 +0800 Subject: [PATCH 093/135] Remove duplicate Cancel msgid from zh_CN translation Co-Authored-By: Claude Opus 4.7 --- localization/i18n/zh_CN/Snapmaker_Orca_zh_CN.po | 3 --- 1 file changed, 3 deletions(-) diff --git a/localization/i18n/zh_CN/Snapmaker_Orca_zh_CN.po b/localization/i18n/zh_CN/Snapmaker_Orca_zh_CN.po index ac70bd675ef..aa0b47a5710 100644 --- a/localization/i18n/zh_CN/Snapmaker_Orca_zh_CN.po +++ b/localization/i18n/zh_CN/Snapmaker_Orca_zh_CN.po @@ -15259,9 +15259,6 @@ msgstr "" msgid "Confirm" msgstr "确认" -msgid "Cancel" -msgstr "取消" - msgid "" "Detected both high and low temperature materials. Mixed printing may " "result in extruder clogging, nozzle damage, or layer adhesion issues." From f5b33a62fe29184414236457e28003dee1d4c13d Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Fri, 5 Jun 2026 12:07:31 +0800 Subject: [PATCH 094/135] Use single-line gettext strings for reliable translation matching Multi-line string literals in _u8L() may not match multi-line msgid entries in .po files during gettext lookup. Consolidate to single-line strings in both C++ source and .po msgid for the filament temp mixing messages. Co-Authored-By: Claude Opus 4.7 --- localization/i18n/zh_CN/Snapmaker_Orca_zh_CN.po | 14 +++----------- src/slic3r/GUI/Plater.cpp | 8 ++------ 2 files changed, 5 insertions(+), 17 deletions(-) diff --git a/localization/i18n/zh_CN/Snapmaker_Orca_zh_CN.po b/localization/i18n/zh_CN/Snapmaker_Orca_zh_CN.po index aa0b47a5710..6292b0dd598 100644 --- a/localization/i18n/zh_CN/Snapmaker_Orca_zh_CN.po +++ b/localization/i18n/zh_CN/Snapmaker_Orca_zh_CN.po @@ -15259,16 +15259,8 @@ msgstr "" msgid "Confirm" msgstr "确认" -msgid "" -"Detected both high and low temperature materials. Mixed printing may " -"result in extruder clogging, nozzle damage, or layer adhesion issues." +msgid "Detected both high and low temperature materials. Mixed printing may result in extruder clogging, nozzle damage, or layer adhesion issues." msgstr "检测到高温材料与低温材料同时使用。混合打印可能导致挤出机堵塞、喷嘴损坏或层间粘附问题。" -msgid "" -"Detected both high and low temperature materials. Mixed printing may " -"result in extruder clogging, nozzle damage, or layer adhesion issues. " -"To continue printing, enable \"Allow mixed printing of high and low " -"temperature materials\" in Preferences." -msgstr "" -"检测到高温材料与低温材料同时使用。混合打印可能导致挤出机堵塞、喷嘴损坏或层间粘附问题。" -"如需继续打印,请在「偏好设置」中开启 \"允许高/低温材料混合打印\"。" +msgid "Detected both high and low temperature materials. Mixed printing may result in extruder clogging, nozzle damage, or layer adhesion issues. To continue printing, enable \"Allow mixed printing of high and low temperature materials\" in Preferences." +msgstr "检测到高温材料与低温材料同时使用。混合打印可能导致挤出机堵塞、喷嘴损坏或层间粘附问题。如需继续打印,请在「偏好设置」中开启 \"允许高/低温材料混合打印\"。" diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index a407ae363c2..fe6adf4d9bd 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -14707,13 +14707,9 @@ bool Plater::sync_filament_temp_mixing_notification() NotificationType::ValidateWarning, NotificationManager::NotificationLevel::WarningNotificationLevel, _u8L("WARNING:") + "\n" + - _u8L("Detected both high and low temperature materials. Mixed printing may " - "result in extruder clogging, nozzle damage, or layer adhesion issues.")); + _u8L("Detected both high and low temperature materials. Mixed printing may result in extruder clogging, nozzle damage, or layer adhesion issues.")); } else { - err.string = _u8L("Detected both high and low temperature materials. Mixed printing may " - "result in extruder clogging, nozzle damage, or layer adhesion issues. " - "To continue printing, enable \"Allow mixed printing of high and low " - "temperature materials\" in Preferences."); + err.string = _u8L("Detected both high and low temperature materials. Mixed printing may result in extruder clogging, nozzle damage, or layer adhesion issues. To continue printing, enable \"Allow mixed printing of high and low temperature materials\" in Preferences."); get_notification_manager()->push_validate_error_notification(err); get_partplate_list().get_curr_plate()->update_apply_result_invalid(true); } From 52771ee2ef6ccafb3971e45d656925656afc2246 Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Fri, 5 Jun 2026 14:39:06 +0800 Subject: [PATCH 095/135] Remove duplicate Confirm msgid from zh_CN .po file Co-Authored-By: Claude Opus 4.7 --- localization/i18n/zh_CN/Snapmaker_Orca_zh_CN.po | 3 --- 1 file changed, 3 deletions(-) diff --git a/localization/i18n/zh_CN/Snapmaker_Orca_zh_CN.po b/localization/i18n/zh_CN/Snapmaker_Orca_zh_CN.po index 6292b0dd598..3991aba5181 100644 --- a/localization/i18n/zh_CN/Snapmaker_Orca_zh_CN.po +++ b/localization/i18n/zh_CN/Snapmaker_Orca_zh_CN.po @@ -15256,9 +15256,6 @@ msgstr "" "\n" "确认启用此功能吗?" -msgid "Confirm" -msgstr "确认" - msgid "Detected both high and low temperature materials. Mixed printing may result in extruder clogging, nozzle damage, or layer adhesion issues." msgstr "检测到高温材料与低温材料同时使用。混合打印可能导致挤出机堵塞、喷嘴损坏或层间粘附问题。" From 47b1ed5fac5c873e2184cdf02cec4c1551a74198 Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Fri, 5 Jun 2026 14:57:00 +0800 Subject: [PATCH 096/135] Add missing Chinese translations for filament mixing checkbox Co-Authored-By: Claude Opus 4.7 --- localization/i18n/zh_CN/Snapmaker_Orca_zh_CN.po | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/localization/i18n/zh_CN/Snapmaker_Orca_zh_CN.po b/localization/i18n/zh_CN/Snapmaker_Orca_zh_CN.po index 3991aba5181..353d1f9b3f1 100644 --- a/localization/i18n/zh_CN/Snapmaker_Orca_zh_CN.po +++ b/localization/i18n/zh_CN/Snapmaker_Orca_zh_CN.po @@ -15261,3 +15261,9 @@ msgstr "检测到高温材料与低温材料同时使用。混合打印可能导 msgid "Detected both high and low temperature materials. Mixed printing may result in extruder clogging, nozzle damage, or layer adhesion issues. To continue printing, enable \"Allow mixed printing of high and low temperature materials\" in Preferences." msgstr "检测到高温材料与低温材料同时使用。混合打印可能导致挤出机堵塞、喷嘴损坏或层间粘附问题。如需继续打印,请在「偏好设置」中开启 \"允许高/低温材料混合打印\"。" + +msgid "Allow high/low temperature filament mixing" +msgstr "允许高/低温材料混合打印" + +msgid "If enabled, allows printing with both high-temperature and low-temperature filaments simultaneously." +msgstr "启用后允许高温材料与低温材料同时打印" From 484ce5b58e4efae3d991fd1c8ee3bf0680579fb9 Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Fri, 5 Jun 2026 19:37:19 +0800 Subject: [PATCH 097/135] fix: improve filament temp mixing detection coverage - Detect global Process filament assignments (wall_filament, solid_infill_filament, support_filament, support_interface_filament, wipe_tower_filament) in addition to plate-level overrides. Uses p->config which is updated in real-time by on_config_change(). - Remove sparse_infill_filament from checked keys: it is not user-configurable in the UI but has a hardcoded default of 1 in some process presets, causing false positives. - Trigger sync after object-mode config changes in TabPrintModel::on_value_change. - Trigger sync after moving instances between plates (PartPlateList::add_to_plate). - Trigger sync after clearing all objects from the current plate (remove_curr_plate_all). - Update Preferences::create_item_checkbox declaration to match the cherry-picked implementation which adds an optional confirm_cb parameter. Co-Authored-By: Claude Opus 4.7 --- src/slic3r/GUI/PartPlate.cpp | 2 ++ src/slic3r/GUI/Plater.cpp | 11 +++++++++-- src/slic3r/GUI/Preferences.hpp | 3 ++- src/slic3r/GUI/Tab.cpp | 3 +++ 4 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/slic3r/GUI/PartPlate.cpp b/src/slic3r/GUI/PartPlate.cpp index f80f8349f0d..736943da928 100644 --- a/src/slic3r/GUI/PartPlate.cpp +++ b/src/slic3r/GUI/PartPlate.cpp @@ -4527,6 +4527,8 @@ int PartPlateList::add_to_plate(int obj_id, int instance_id, int plate_id) } ret = plate->add_instance(obj_id, instance_id, true); + wxGetApp().plater()->sync_filament_temp_mixing_notification(); + return ret; } diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index fe6adf4d9bd..0d76b54c89e 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -6043,6 +6043,8 @@ void Plater::priv::remove_curr_plate_all() SingleSnapshot ss(q); view3D->remove_curr_plate_all(); this->sidebar->obj_list()->update_selections(); + + q->sync_filament_temp_mixing_notification(); } void Plater::priv::select_all() @@ -14622,7 +14624,7 @@ bool Plater::check_filament_temp_mixing() // Helper: collect used filament slots from a single config auto collect_from_cfg = [&](const DynamicPrintConfig& cfg) { - static const char* keys_1based[] = {"wall_filament", "sparse_infill_filament", "solid_infill_filament"}; + static const char* keys_1based[] = {"wall_filament", "solid_infill_filament"}; for (auto key : keys_1based) { auto* opt = cfg.option(key); if (opt && opt->value >= 1 && opt->value <= num_filaments) @@ -14636,7 +14638,11 @@ bool Plater::check_filament_temp_mixing() } }; - // Collect from current plate's config only + // Collect from the Plater's working config for global Process settings. + // Defaults are 0 (skipped by the >= 1 check). + collect_from_cfg(*this->config()); + + // Also collect from current plate's config for any plate-level overrides PartPlate* curr_plate = p->partplate_list.get_curr_plate(); if (curr_plate) collect_from_cfg(*curr_plate->config()); @@ -14672,6 +14678,7 @@ bool Plater::check_filament_temp_mixing() // (the edited preset config hasn't been committed yet). PresetBundle* bundle = wxGetApp().preset_bundle; bool has_high = false, has_low = false; + for (int slot : used_slots) { if (slot < (int)bundle->filament_presets.size()) { const Preset* preset = bundle->filaments.find_preset( diff --git a/src/slic3r/GUI/Preferences.hpp b/src/slic3r/GUI/Preferences.hpp index e65ae52d35a..6fbf5d19ce9 100644 --- a/src/slic3r/GUI/Preferences.hpp +++ b/src/slic3r/GUI/Preferences.hpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include "Widgets/ComboBox.hpp" @@ -110,7 +111,7 @@ class PreferencesDialog : public DPIDialog wxBoxSizer *create_item_region_combobox(wxString title, wxWindow *parent, wxString tooltip, std::vector vlist); wxBoxSizer *create_item_language_combobox(wxString title, wxWindow *parent, wxString tooltip, int padding_left, std::string param, std::vector vlist); wxBoxSizer *create_item_loglevel_combobox(wxString title, wxWindow *parent, wxString tooltip, std::vector vlist); - wxBoxSizer *create_item_checkbox(wxString title, wxWindow *parent, wxString tooltip, int padding_left, std::string param); + wxBoxSizer *create_item_checkbox(wxString title, wxWindow *parent, wxString tooltip, int padding_left, std::string param, std::function confirm_cb = nullptr); wxBoxSizer *create_item_darkmode_checkbox(wxString title, wxWindow *parent, wxString tooltip, int padding_left, std::string param); void set_dark_mode(); wxBoxSizer *create_item_button(wxString title, wxString title2, wxWindow *parent, wxString tooltip, wxString tooltip2, std::function onclick, bool button_on_left = false); diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index e69f7b475e8..f142647e322 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -3110,6 +3110,9 @@ void TabPrintModel::on_value_change(const std::string& opt_key, const boost::any notify_changed(config.first); } wxGetApp().params_panel()->notify_object_config_changed(); + + // Real-time high/low temperature filament mixing check after object-level config change + wxGetApp().plater()->sync_filament_temp_mixing_notification(); } void TabPrintModel::reload_config() From bd16de4906ddfd46466ffa384b0addbcec138e83 Mon Sep 17 00:00:00 2001 From: Joyx-SnapMaker <1906956558@qq.com> Date: Fri, 8 May 2026 12:15:17 +0800 Subject: [PATCH 098/135] =?UTF-8?q?fix=EF=BC=9ATemporarily=20enable=20the?= =?UTF-8?q?=20ability=20to=20export=20gcode.3mf?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/slic3r/GUI/MainFrame.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index 662210a3483..84a010aa361 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -1736,8 +1736,8 @@ wxBoxSizer* MainFrame::create_side_tools() { SidePopup* p = new SidePopup(this); - if (wxGetApp().preset_bundle && !wxGetApp().preset_bundle->is_bbl_vendor()) - //if (0) + //if (wxGetApp().preset_bundle && !wxGetApp().preset_bundle->is_bbl_vendor()) + if (0) { // ThirdParty Buttons SideButton* export_gcode_btn = new SideButton(p, _L("Export G-code file"), ""); From 2cc411853547ada09d459f8bb3cc236116f8486f Mon Sep 17 00:00:00 2001 From: Joyx-SnapMaker <1906956558@qq.com> Date: Fri, 8 May 2026 15:17:13 +0800 Subject: [PATCH 099/135] =?UTF-8?q?Revert=20"fix=EF=BC=9ATemporarily=20ena?= =?UTF-8?q?ble=20the=20ability=20to=20export=20gcode.3mf"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit a3c3e5e5400e0ca68847d7b6817aa85fa835eabb. --- src/slic3r/GUI/MainFrame.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index 84a010aa361..662210a3483 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -1736,8 +1736,8 @@ wxBoxSizer* MainFrame::create_side_tools() { SidePopup* p = new SidePopup(this); - //if (wxGetApp().preset_bundle && !wxGetApp().preset_bundle->is_bbl_vendor()) - if (0) + if (wxGetApp().preset_bundle && !wxGetApp().preset_bundle->is_bbl_vendor()) + //if (0) { // ThirdParty Buttons SideButton* export_gcode_btn = new SideButton(p, _L("Export G-code file"), ""); From c8aa45bc14d6498d5f085fc34a1d6cb6d7e40f86 Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Mon, 8 Jun 2026 10:30:38 +0800 Subject: [PATCH 100/135] fix: rebuild filament mixing feature on current main base MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The original cherry-pick fca2f3d2c4 used --theirs conflict resolution which overwrote Plater.cpp and Print.cpp with older versions, accidentally dropping 6194 lines of code from main. This commit properly re-applies only the filament mixing additions: Plater.cpp: - Add check_filament_temp_mixing() — detects high/low temp mixing across wall, solid infill, support, support interface, and wipe tower filaments - Add sync_filament_temp_mixing_notification() — updates notification state - Add sync calls in: Sidebar::update_presets, on_config_change, update_background_process, remove_curr_plate_all - Add pre-slice confirmation dialog when allow_filament_temp_mixing is on Print.cpp: - Replace nozzles<2 restriction with simpler extruders>1 guard so the filament mixing check covers multi-nozzle printers too Co-Authored-By: Claude Opus 4.7 --- src/libslic3r/Print.cpp | 553 +++- src/slic3r/GUI/Plater.cpp | 5842 +++++++++++++++++++++++++++++++++++-- 2 files changed, 6212 insertions(+), 183 deletions(-) diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index efde90ad054..c0ee6f1ace3 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -8,6 +8,7 @@ #include "Flow.hpp" #include "Geometry/ConvexHull.hpp" #include "I18N.hpp" +#include "LocalZOrderOptimizer.hpp" #include "ShortestPath.hpp" #include "Thread.hpp" #include "Time.hpp" @@ -53,6 +54,453 @@ template class PrintState; PrintRegion::PrintRegion(const PrintRegionConfig &config) : PrintRegion(config, config.hash()) {} PrintRegion::PrintRegion(PrintRegionConfig &&config) : PrintRegion(std::move(config), config.hash()) {} +namespace { + +constexpr double LOCAL_Z_PERIMETER_MASK_EXPAND_MM = 0.10; + +struct LocalZWipeTowerToolchange +{ + unsigned int old_tool { 0 }; + unsigned int new_tool { 0 }; +}; + +struct LocalZWipeTowerPassRef +{ + size_t layer_to_print_idx { 0 }; + const SubLayerPlan *plan { nullptr }; + std::vector extruders; +}; + +static inline ExPolygons local_z_compensate_masks_for_wipe_tower(const ExPolygons &src_masks, + const float delta_scaled, + const bool fallback_to_source) +{ + if (src_masks.empty() || std::abs(delta_scaled) <= EPSILON) + return src_masks; + + ExPolygons compensated = offset_ex(src_masks, delta_scaled); + if (!compensated.empty() && compensated.size() > 1) + compensated = union_ex(compensated); + + if (compensated.empty() && fallback_to_source) + return src_masks; + return compensated; +} + +static bool local_z_segments_exist(Polylines segments) +{ + for (Polyline &segment : segments) { + if (segment.is_valid()) + return true; + } + return false; +} + +static bool extrusion_collection_has_local_z_segment(const ExtrusionEntityCollection &source, + const ExPolygons &include_masks) +{ + if (source.entities.empty() || include_masks.empty()) + return false; + + ExtrusionEntityCollection flattened = source.flatten(false); + for (const ExtrusionEntity *entity : flattened.entities) { + if (const auto *path = dynamic_cast(entity)) { + if (local_z_segments_exist(intersection_pl(Polylines{path->polyline}, include_masks))) + return true; + } else if (const auto *multipath = dynamic_cast(entity)) { + for (const ExtrusionPath &path : multipath->paths) { + if (local_z_segments_exist(intersection_pl(Polylines{path.polyline}, include_masks))) + return true; + } + } else if (const auto *loop = dynamic_cast(entity)) { + for (const ExtrusionPath &path : loop->paths) { + if (local_z_segments_exist(intersection_pl(Polylines{path.polyline}, include_masks))) + return true; + } + } + } + + return false; +} + +static bool layer_has_local_z_extrusions(const Layer &layer, const ExPolygons &pass_masks, bool include_infill) +{ + if (pass_masks.empty()) + return false; + + for (const LayerRegion *layer_region : layer.regions()) { + for (const ExtrusionEntity *entity : layer_region->perimeters.entities) { + const auto *extrusions = dynamic_cast(entity); + if (extrusions == nullptr) + continue; + if (extrusion_collection_has_local_z_segment(*extrusions, pass_masks)) + return true; + } + if (!include_infill) + continue; + for (const ExtrusionEntity *entity : layer_region->fills.entities) { + const auto *extrusions = dynamic_cast(entity); + if (extrusions == nullptr) + continue; + if (extrusion_collection_has_local_z_segment(*extrusions, pass_masks)) + return true; + } + } + + return false; +} + +static inline int shared_local_z_extruder_for_wipe_tower(const std::vector &lhs, + const std::vector &rhs) +{ + for (unsigned int extruder_id : lhs) { + if (std::find(rhs.begin(), rhs.end(), extruder_id) != rhs.end()) + return static_cast(extruder_id); + } + return -1; +} + +static std::vector rotate_extruders_to_start_with(const std::vector &extruders, + unsigned int start_extruder) +{ + std::vector rotated = extruders; + auto it = std::find(rotated.begin(), rotated.end(), start_extruder); + if (it != rotated.end()) + std::rotate(rotated.begin(), it, rotated.end()); + return rotated; +} + +static std::vector collect_local_z_wipe_tower_toolchanges( + const Print &print, + const std::vector &layers, + int start_extruder) +{ + std::vector pass_refs; + const bool local_z_whole_objects_enabled = print.full_print_config().opt_bool("dithering_local_z_whole_objects"); + const bool local_z_infill_enabled = print.full_print_config().opt_bool("dithering_local_z_infill"); + const float local_z_perimeter_mask_expand = float(scale_(LOCAL_Z_PERIMETER_MASK_EXPAND_MM)); + + for (size_t layer_to_print_idx = 0; layer_to_print_idx < layers.size(); ++layer_to_print_idx) { + const GCode::LayerToPrint &layer_to_print = layers[layer_to_print_idx]; + if (layer_to_print.object_layer == nullptr) + continue; + + const PrintObject *print_object = + layer_to_print.original_object != nullptr ? layer_to_print.original_object : layer_to_print.object(); + if (print_object == nullptr) + continue; + + const size_t layer_id = size_t(layer_to_print.object_layer->id()); + const auto &intervals = print_object->local_z_intervals(); + const auto &plans = print_object->local_z_sublayer_plan(); + const auto interval_it = std::find_if(intervals.begin(), intervals.end(), [layer_id](const LocalZInterval &interval) { + return interval.layer_id == layer_id; + }); + if (interval_it == intervals.end() || !interval_it->has_mixed_paint || interval_it->sublayer_count <= 1 || + interval_it->first_sublayer_idx >= plans.size()) { + continue; + } + + const size_t first_idx = interval_it->first_sublayer_idx; + const size_t end_idx = std::min(plans.size(), first_idx + interval_it->sublayer_count); + for (size_t plan_idx = first_idx; plan_idx < end_idx; ++plan_idx) { + const SubLayerPlan &plan = plans[plan_idx]; + if (!plan.split_interval) + continue; + + const size_t plan_mask_slots = + std::max(plan.painted_masks_by_extruder.size(), plan.fixed_painted_masks_by_extruder.size()); + std::vector compensated_masks_by_extruder(plan_mask_slots, ExPolygons()); + + ExPolygons fixed_raw_masks_union; + for (const ExPolygons &fixed_masks : plan.fixed_painted_masks_by_extruder) { + if (!fixed_masks.empty()) + append(fixed_raw_masks_union, fixed_masks); + } + if (!fixed_raw_masks_union.empty() && fixed_raw_masks_union.size() > 1) + fixed_raw_masks_union = union_ex(fixed_raw_masks_union); + + const ExPolygons fixed_compensated_guard = + fixed_raw_masks_union.empty() ? + ExPolygons() : + local_z_compensate_masks_for_wipe_tower(fixed_raw_masks_union, local_z_perimeter_mask_expand, true); + + for (size_t extruder_id = 0; extruder_id < plan_mask_slots; ++extruder_id) { + const ExPolygons mixed_raw_masks = + extruder_id < plan.painted_masks_by_extruder.size() ? plan.painted_masks_by_extruder[extruder_id] : ExPolygons(); + const ExPolygons fixed_raw_masks = + extruder_id < plan.fixed_painted_masks_by_extruder.size() ? plan.fixed_painted_masks_by_extruder[extruder_id] : + ExPolygons(); + if (mixed_raw_masks.empty() && fixed_raw_masks.empty()) + continue; + + ExPolygons compensated; + if (!mixed_raw_masks.empty()) { + ExPolygons compensated_mixed = + local_z_compensate_masks_for_wipe_tower(mixed_raw_masks, local_z_perimeter_mask_expand, true); + if (local_z_whole_objects_enabled && !fixed_compensated_guard.empty()) + compensated_mixed = diff_ex(compensated_mixed, fixed_compensated_guard); + if (!compensated_mixed.empty()) + append(compensated, compensated_mixed); + } + if (!fixed_raw_masks.empty()) + append(compensated, fixed_raw_masks); + if (!compensated.empty() && compensated.size() > 1) + compensated = union_ex(compensated); + compensated_masks_by_extruder[extruder_id] = std::move(compensated); + } + + LocalZWipeTowerPassRef pass_ref; + pass_ref.layer_to_print_idx = layer_to_print_idx; + pass_ref.plan = &plan; + for (size_t extruder_id = 0; extruder_id < plan.painted_masks_by_extruder.size(); ++extruder_id) { + if (extruder_id >= compensated_masks_by_extruder.size()) + continue; + const ExPolygons &pass_masks = compensated_masks_by_extruder[extruder_id]; + if (pass_masks.empty()) + continue; + if (layer_has_local_z_extrusions(*layer_to_print.object_layer, pass_masks, local_z_infill_enabled)) + pass_ref.extruders.push_back(unsigned(extruder_id)); + } + + if (!pass_ref.extruders.empty()) + pass_refs.emplace_back(std::move(pass_ref)); + } + } + + std::sort(pass_refs.begin(), pass_refs.end(), [](const LocalZWipeTowerPassRef &lhs, const LocalZWipeTowerPassRef &rhs) { + assert(lhs.plan != nullptr && rhs.plan != nullptr); + if (lhs.plan->print_z != rhs.plan->print_z) + return lhs.plan->print_z < rhs.plan->print_z; + if (lhs.layer_to_print_idx != rhs.layer_to_print_idx) + return lhs.layer_to_print_idx < rhs.layer_to_print_idx; + return lhs.plan->pass_index < rhs.plan->pass_index; + }); + + auto collect_toolchanges_legacy = [&](int start_tool) { + std::vector legacy_toolchanges; + int active_extruder = start_tool; + size_t pass_ref_idx = 0; + while (pass_ref_idx < pass_refs.size()) { + size_t pass_group_end = pass_ref_idx + 1; + while (pass_group_end < pass_refs.size() && + std::abs(pass_refs[pass_ref_idx].plan->print_z - pass_refs[pass_group_end].plan->print_z) <= EPSILON) { + ++pass_group_end; + } + + std::vector pass_group_extruders; + for (size_t group_idx = pass_ref_idx; group_idx < pass_group_end; ++group_idx) + for (unsigned int extruder_id : pass_refs[group_idx].extruders) + if (std::find(pass_group_extruders.begin(), pass_group_extruders.end(), extruder_id) == pass_group_extruders.end()) + pass_group_extruders.push_back(extruder_id); + + std::vector next_group_extruders; + if (pass_group_end < pass_refs.size()) { + size_t next_group_end = pass_group_end + 1; + while (next_group_end < pass_refs.size() && + std::abs(pass_refs[pass_group_end].plan->print_z - pass_refs[next_group_end].plan->print_z) <= EPSILON) { + ++next_group_end; + } + for (size_t group_idx = pass_group_end; group_idx < next_group_end; ++group_idx) + for (unsigned int extruder_id : pass_refs[group_idx].extruders) + if (std::find(next_group_extruders.begin(), next_group_extruders.end(), extruder_id) == next_group_extruders.end()) + next_group_extruders.push_back(extruder_id); + } + + const int preferred_last_extruder = + shared_local_z_extruder_for_wipe_tower(pass_group_extruders, next_group_extruders); + const std::vector ordered_group_extruders = + LocalZOrderOptimizer::order_bucket_extruders(pass_group_extruders, active_extruder, preferred_last_extruder); + + for (unsigned int extruder_id : ordered_group_extruders) { + if (active_extruder >= 0 && active_extruder != int(extruder_id)) + legacy_toolchanges.push_back(LocalZWipeTowerToolchange{unsigned(active_extruder), extruder_id}); + active_extruder = int(extruder_id); + } + + pass_ref_idx = pass_group_end; + } + + return legacy_toolchanges; + }; + + const bool dependency_chain_mode = + !pass_refs.empty() && + std::all_of(pass_refs.begin(), pass_refs.end(), [](const LocalZWipeTowerPassRef &pass_ref) { + return pass_ref.plan != nullptr && pass_ref.plan->dependency_group != 0; + }); + if (!dependency_chain_mode) + return collect_toolchanges_legacy(start_extruder); + + struct ChainKey { + size_t layer_to_print_idx { 0 }; + size_t dependency_group { 0 }; + + bool operator<(const ChainKey &rhs) const + { + if (layer_to_print_idx != rhs.layer_to_print_idx) + return layer_to_print_idx < rhs.layer_to_print_idx; + return dependency_group < rhs.dependency_group; + } + }; + struct PassState { + const LocalZWipeTowerPassRef *pass_ref { nullptr }; + std::vector remaining_extruders; + size_t chain_idx { 0 }; + size_t chain_pos { 0 }; + bool ready { false }; + bool completed { false }; + }; + + std::map chain_index_by_key; + std::vector> chains; + std::vector pass_states; + pass_states.reserve(pass_refs.size()); + for (const LocalZWipeTowerPassRef &pass_ref : pass_refs) { + ChainKey chain_key { pass_ref.layer_to_print_idx, pass_ref.plan->dependency_group }; + auto [it_chain, inserted] = chain_index_by_key.emplace(chain_key, chains.size()); + if (inserted) + chains.emplace_back(); + + const size_t chain_idx = it_chain->second; + const size_t pass_state_idx = pass_states.size(); + pass_states.push_back(PassState{ &pass_ref, pass_ref.extruders, chain_idx, 0, false, false }); + chains[chain_idx].push_back(pass_state_idx); + } + + for (std::vector &chain : chains) { + std::sort(chain.begin(), chain.end(), [&pass_states](size_t lhs_idx, size_t rhs_idx) { + const SubLayerPlan &lhs = *pass_states[lhs_idx].pass_ref->plan; + const SubLayerPlan &rhs = *pass_states[rhs_idx].pass_ref->plan; + if (lhs.dependency_order != rhs.dependency_order) + return lhs.dependency_order < rhs.dependency_order; + if (std::abs(lhs.print_z - rhs.print_z) > EPSILON) + return lhs.print_z < rhs.print_z; + return lhs.pass_index < rhs.pass_index; + }); + for (size_t chain_pos = 0; chain_pos < chain.size(); ++chain_pos) + pass_states[chain[chain_pos]].chain_pos = chain_pos; + if (!chain.empty()) + pass_states[chain.front()].ready = true; + } + + auto pass_contains_extruder = [](const PassState &pass_state, unsigned int extruder_id) { + return std::find(pass_state.remaining_extruders.begin(), pass_state.remaining_extruders.end(), extruder_id) != + pass_state.remaining_extruders.end(); + }; + + auto choose_ready_extruder = [&](int active_extruder) -> int { + std::vector ready_extruders; + for (const PassState &pass_state : pass_states) { + if (!pass_state.ready || pass_state.completed) + continue; + for (unsigned int extruder_id : pass_state.remaining_extruders) + if (std::find(ready_extruders.begin(), ready_extruders.end(), extruder_id) == ready_extruders.end()) + ready_extruders.push_back(extruder_id); + } + if (ready_extruders.empty()) + return -1; + if (active_extruder >= 0 && + std::find(ready_extruders.begin(), ready_extruders.end(), unsigned(active_extruder)) != ready_extruders.end()) { + return active_extruder; + } + + int best_extruder = -1; + size_t best_ready_count = 0; + size_t best_future_count = 0; + for (unsigned int extruder_id : ready_extruders) { + size_t ready_count = 0; + size_t future_count = 0; + for (const PassState &pass_state : pass_states) { + if (pass_state.completed || !pass_contains_extruder(pass_state, extruder_id)) + continue; + ++future_count; + if (pass_state.ready) + ++ready_count; + } + + if (best_extruder < 0 || + ready_count > best_ready_count || + (ready_count == best_ready_count && future_count > best_future_count) || + (ready_count == best_ready_count && future_count == best_future_count && extruder_id < unsigned(best_extruder))) { + best_extruder = int(extruder_id); + best_ready_count = ready_count; + best_future_count = future_count; + } + } + return best_extruder; + }; + + std::vector toolchanges; + int active_extruder = start_extruder; + size_t completed_passes = 0; + while (completed_passes < pass_states.size()) { + const int chosen_extruder = choose_ready_extruder(active_extruder); + if (chosen_extruder < 0) { + BOOST_LOG_TRIVIAL(warning) << "Local-Z wipe tower dependency scheduler deadlocked, falling back" + << " start_extruder=" << start_extruder + << " pass_count=" << pass_refs.size(); + return collect_toolchanges_legacy(start_extruder); + } + + if (active_extruder >= 0 && active_extruder != chosen_extruder) + toolchanges.push_back(LocalZWipeTowerToolchange{unsigned(active_extruder), unsigned(chosen_extruder)}); + active_extruder = chosen_extruder; + + bool completed_any = false; + std::vector newly_completed; + for (size_t pass_state_idx = 0; pass_state_idx < pass_states.size(); ++pass_state_idx) { + PassState &pass_state = pass_states[pass_state_idx]; + if (!pass_state.ready || pass_state.completed) + continue; + + auto it_extruder = std::find(pass_state.remaining_extruders.begin(), + pass_state.remaining_extruders.end(), + unsigned(chosen_extruder)); + if (it_extruder == pass_state.remaining_extruders.end()) + continue; + + pass_state.remaining_extruders.erase(it_extruder); + completed_any = true; + if (pass_state.remaining_extruders.empty()) + newly_completed.push_back(pass_state_idx); + } + + if (!completed_any) { + BOOST_LOG_TRIVIAL(warning) << "Local-Z wipe tower dependency scheduler made no progress, falling back" + << " start_extruder=" << start_extruder + << " active_extruder=" << active_extruder + << " chosen_extruder=" << chosen_extruder + << " pass_count=" << pass_refs.size(); + return collect_toolchanges_legacy(start_extruder); + } + + for (size_t pass_state_idx : newly_completed) { + PassState &pass_state = pass_states[pass_state_idx]; + if (pass_state.completed) + continue; + + pass_state.ready = false; + pass_state.completed = true; + ++completed_passes; + + const std::vector &chain = chains[pass_state.chain_idx]; + const size_t next_chain_pos = pass_state.chain_pos + 1; + if (next_chain_pos < chain.size()) + pass_states[chain[next_chain_pos]].ready = true; + } + } + + BOOST_LOG_TRIVIAL(info) << "Local-Z wipe tower dependency scheduler" + << " start_extruder=" << start_extruder + << " pass_count=" << pass_refs.size() + << " chain_count=" << chains.size() + << " toolchanges=" << toolchanges.size(); + return toolchanges; +} + +} // namespace + //BBS // ORCA: Now this is a parameter //float Print::min_skirt_length = 0; @@ -247,6 +695,19 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n || opt_key == "filament_shrinkage_compensation_z" || opt_key == "resolution" || opt_key == "precise_z_height" + || opt_key == "dithering_z_step_size" + || opt_key == "dithering_local_z_mode" + || opt_key == "dithering_local_z_whole_objects" + || opt_key == "dithering_local_z_direct_multicolor" + || opt_key == "dithering_step_painted_zones_only" + || opt_key == "mixed_filament_gradient_mode" + || opt_key == "mixed_filament_height_lower_bound" + || opt_key == "mixed_filament_height_upper_bound" + || opt_key == "mixed_filament_advanced_dithering" + || opt_key == "mixed_filament_component_bias_enabled" + || opt_key == "mixed_filament_surface_indentation" + || opt_key == "mixed_filament_region_collapse" + || opt_key == "mixed_filament_definitions" // Spiral Vase forces different kind of slicing than the normal model: // In Spiral Vase mode, holes are closed and only the largest area contour is kept at each layer. // Therefore toggling the Spiral Vase on / off requires complete reslicing. @@ -318,6 +779,7 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n || opt_key == "wipe_tower_wall_gap" || opt_key == "wipe_tower_filament" || opt_key == "wiping_volumes_extruders" + || opt_key == "dithering_local_z_infill" || opt_key == "enable_filament_ramming" || opt_key == "purge_in_prime_tower" || opt_key == "z_offset" @@ -480,10 +942,11 @@ std::vector Print::extruders(bool conside_custom_gcode) const if (conside_custom_gcode) { //BBS - int num_extruders = m_config.filament_colour.size(); + const size_t num_physical = m_config.filament_colour.size(); + const size_t num_filaments = m_mixed_filament_mgr.total_filaments(num_physical); if (m_model.plates_custom_gcodes.find(m_model.curr_plate_index) != m_model.plates_custom_gcodes.end()) { for (auto item : m_model.plates_custom_gcodes.at(m_model.curr_plate_index).gcodes) { - if (item.type == CustomGCode::Type::ToolChange && item.extruder <= num_extruders) + if (item.type == CustomGCode::Type::ToolChange && item.extruder <= int(num_filaments)) extruders.push_back((unsigned int)(item.extruder - 1)); } } @@ -1066,17 +1529,13 @@ StringObjectException Print::check_multi_filament_valid(const Print& print) { auto print_config = print.config(); std::vector extruders = print.extruders(); + std::vector filament_types; + filament_types.reserve(extruders.size()); - bool has_high = false, has_low = false; - for (const auto& extruder_idx : extruders) { - if (extruder_idx < print_config.filament_is_high_temperature.size()) { - if (print_config.filament_is_high_temperature.get_at(extruder_idx)) - has_high = true; - else - has_low = true; - } - } - if (has_high && has_low) + for (const auto& extruder_idx : extruders) + filament_types.push_back(print_config.filament_type.get_at(extruder_idx)); + + if (!check_multi_filaments_compatibility(filament_types)) return {L("Cannot print multiple filaments which have large difference of temperature together. Otherwise, the extruder and nozzle may be blocked or damaged during printing.")}; return {std::string()}; @@ -1091,6 +1550,7 @@ boost::regex regex_g92e0 { "^[ \\t]*[gG]92[ \\t]*[eE](0(\\.0*)?|\\.0+)[ \\t]*(;. StringObjectException Print::validate(StringObjectException *warning, Polygons* collison_polygons, std::vector>* height_polygons) const { std::vector extruders = this->extruders(); + unsigned int nozzles = m_config.nozzle_diameter.size(); if (m_objects.empty()) return {std::string()}; @@ -1098,6 +1558,17 @@ StringObjectException Print::validate(StringObjectException *warning, Polygons* if (extruders.empty()) return { L("No extrusions under current settings.") }; + // Check for high/low temperature filament mixing. The check uses + // filament_is_high_temperature from each filament preset. This guard + // works for both single-nozzle and multi-nozzle printers. + if (extruders.size() > 1) { + auto ret = check_multi_filament_valid(*this); + if (!ret.string.empty()) { + ret.type = STRING_EXCEPT_FILAMENTS_DIFFERENT_TEMP; + return ret; + } + } + if (m_config.print_sequence == PrintSequence::ByObject) { if (m_config.timelapse_type == TimelapseType::tlSmooth) return {L("Smooth mode of timelapse is not supported when \"by object\" sequence is enabled.")}; @@ -1148,7 +1619,7 @@ StringObjectException Print::validate(StringObjectException *warning, Polygons* layer_height_profiles.assign(m_objects.size(), std::vector()); std::vector &profile = layer_height_profiles[print_object_idx]; if (profile.empty()) - PrintObject::update_layer_height_profile(*print_object.model_object(), print_object.slicing_parameters(), profile); + PrintObject::update_layer_height_profile(*print_object.model_object(), print_object.slicing_parameters(), profile, &print_object); return profile; }; @@ -2842,6 +3313,8 @@ void Print::_make_wipe_tower() // Initialize the wipe tower. WipeTower2 wipe_tower(m_config, m_default_region_config, m_plate_index, m_origin, wipe_volumes, m_wipe_tower_data.tool_ordering.first_extruder()); + const std::vector>> layers_to_print = GCode::collect_layers_to_print(*this); + size_t layers_to_print_idx = 0; // wipe_tower.set_retract(); // wipe_tower.set_zhop(); @@ -2860,10 +3333,52 @@ void Print::_make_wipe_tower() for (auto &layer_tools : m_wipe_tower_data.tool_ordering.layer_tools()) { // for all layers if (!layer_tools.has_wipe_tower) continue; + while (layers_to_print_idx + 1 < layers_to_print.size() && + layers_to_print[layers_to_print_idx].first + EPSILON < layer_tools.print_z) { + ++layers_to_print_idx; + } + + const std::vector *layers_with_same_print_z = nullptr; + if (layers_to_print_idx < layers_to_print.size() && + std::abs(layers_to_print[layers_to_print_idx].first - layer_tools.print_z) <= EPSILON) { + layers_with_same_print_z = &layers_to_print[layers_to_print_idx].second; + } + bool first_layer = &layer_tools == &m_wipe_tower_data.tool_ordering.front(); + + if (layers_with_same_print_z != nullptr) { + const std::vector local_z_toolchanges = + collect_local_z_wipe_tower_toolchanges(*this, *layers_with_same_print_z, int(current_extruder_id)); + if (!local_z_toolchanges.empty()) { + std::ostringstream local_z_sequence; + for (size_t toolchange_idx = 0; toolchange_idx < local_z_toolchanges.size(); ++toolchange_idx) { + if (toolchange_idx != 0) + local_z_sequence << ","; + local_z_sequence << local_z_toolchanges[toolchange_idx].old_tool << "->" + << local_z_toolchanges[toolchange_idx].new_tool; + } + + BOOST_LOG_TRIVIAL(debug) << "Local-Z wipe tower preplan" + << " print_z=" << layer_tools.print_z + << " start_tool=" << current_extruder_id + << " nominal_toolchanges=" << layer_tools.extruders.size() + << " local_z_toolchanges=" << local_z_toolchanges.size() + << " sequence=" << local_z_sequence.str(); + } + for (const LocalZWipeTowerToolchange &toolchange : local_z_toolchanges) { + wipe_tower.plan_local_z_toolchange((float) layer_tools.print_z, (float) layer_tools.wipe_tower_layer_height, + toolchange.old_tool, toolchange.new_tool, (float) m_config.prime_volume); + } + if (!local_z_toolchanges.empty()) + current_extruder_id = local_z_toolchanges.back().new_tool; + } + + const std::vector nominal_layer_extruders = + rotate_extruders_to_start_with(layer_tools.extruders, current_extruder_id); + wipe_tower.plan_toolchange((float) layer_tools.print_z, (float) layer_tools.wipe_tower_layer_height, current_extruder_id, current_extruder_id, false); - for (const auto extruder_id : layer_tools.extruders) { + for (const auto extruder_id : nominal_layer_extruders) { if ((first_layer && extruder_id == m_wipe_tower_data.tool_ordering.all_extruders().back()) || extruder_id != current_extruder_id) { float volume_to_wipe = m_config.prime_volume; @@ -2887,6 +3402,7 @@ void Print::_make_wipe_tower() current_extruder_id = extruder_id; } } + layer_tools.wiping_extrusions().ensure_perimeters_infills_order(*this); if (&layer_tools == &m_wipe_tower_data.tool_ordering.back() || (&layer_tools + 1)->wipe_tower_partitions == 0) break; @@ -2897,8 +3413,12 @@ void Print::_make_wipe_tower() m_wipe_tower_data.tool_changes.reserve(m_wipe_tower_data.tool_ordering.layer_tools().size()); m_wipe_tower_data.local_z_tool_changes.reserve(m_wipe_tower_data.tool_ordering.layer_tools().size()); wipe_tower.generate(m_wipe_tower_data.tool_changes, m_wipe_tower_data.local_z_tool_changes); + BOOST_LOG_TRIVIAL(debug) << "Wipe tower generation completed" + << " nominal_layers=" << m_wipe_tower_data.tool_changes.size() + << " local_z_layers=" << m_wipe_tower_data.local_z_tool_changes.size(); m_wipe_tower_data.depth = wipe_tower.get_depth(); m_wipe_tower_data.z_and_depth_pairs = wipe_tower.get_z_and_depth_pairs(); + m_wipe_tower_data.local_z_reserve_boxes = wipe_tower.get_local_z_reserve_boxes(); m_wipe_tower_data.brim_width = wipe_tower.get_brim_width(); m_wipe_tower_data.height = wipe_tower.get_wipe_tower_height(); @@ -4364,7 +4884,10 @@ BoundingBoxf3 PrintInstance::get_bounding_box() { Polygon PrintInstance::get_convex_hull_2d() { Polygon poly = print_object->model_object()->convex_hull_2d(model_instance->get_matrix()); - poly.douglas_peucker(0.1); + // Change the distance threshold of the Douglas-Peucker algorithm to 0.5 millimeter and reduce the number of points + poly.douglas_peucker(scale_(0.5)); + // Round coordinates to 0.1mm grid to limit decimal places + poly.round_to_grid(scale_(0.1)); return poly; } diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 0d76b54c89e..df3926ed971 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -1,15 +1,32 @@ #include "Plater.hpp" +#include "MixedFilamentDialog.hpp" +#include "MixedGradientSelector.hpp" +#include "MixedColorMatchPanel.hpp" +#include "MixedFilamentBadge.hpp" +#include "MixedFilamentColorMapPanel.hpp" #include "libslic3r/Config.hpp" +#include "libslic3r/MixedFilament.hpp" +#include "libslic3r/filament_mixer.h" #include "common_func/common_func.hpp" #include +#include +#include +#include #include +#include #include +#include +#include +#include #include #include #include +#include #include #include +#include +#include #include #include #include @@ -35,14 +52,23 @@ #include #include #include +#include +#include #include #include +#include +#include +#include +#include +#include #ifdef _WIN32 #include #include #include #endif #include +#include +#include #include #include @@ -106,6 +132,7 @@ #include "ModelMall.hpp" #include "ConfigWizard.hpp" #include "../Utils/ASCIIFolding.hpp" +#include "../Utils/ColorSpaceConvert.hpp" #include "../Utils/FixModelByWin10.hpp" #include "../Utils/UndoRedo.hpp" #include "../Utils/PresetUpdater.hpp" @@ -208,6 +235,7 @@ wxDEFINE_EVENT(EVT_ADD_FILAMENT, SimpleEvent); wxDEFINE_EVENT(EVT_DEL_FILAMENT, SimpleEvent); wxDEFINE_EVENT(EVT_ADD_CUSTOM_FILAMENT, ColorEvent); + #define PRINTER_THUMBNAIL_SIZE (wxSize(FromDIP(48), FromDIP(48))) #define PRINTER_THUMBNAIL_SIZE_SMALL (wxSize(FromDIP(32), FromDIP(32))) #define PRINTER_PANEL_SIZE_SMALL (wxSize(FromDIP(98), FromDIP(68))) @@ -692,6 +720,7 @@ struct Sidebar::priv wxComboBox* m_comboBox_print_preset; wxStaticLine* m_staticline1; StaticBox* m_panel_filament_title; + ScalableButton* m_filament_config_icon = nullptr; wxStaticText* m_staticText_filament_settings; ScalableButton * m_bpButton_add_filament; ScalableButton * m_bpButton_del_filament; @@ -700,6 +729,40 @@ struct Sidebar::priv int m_menu_filament_id = -1; wxPanel* m_panel_filament_content; wxScrolledWindow* m_scrolledWindow_filament_content; + + // Mixed (virtual) filaments panel - collapsible like Printer/Filament sections + StaticBox* m_panel_mixed_filaments_title = nullptr; // Collapsible title bar + wxPanel* m_panel_mixed_filaments_content = nullptr; // Content panel + wxBoxSizer* m_sizer_mixed_filaments_content = nullptr; // Content sizer + ScalableButton* m_mixed_filaments_icon = nullptr; // Icon + wxStaticText* m_staticText_mixed_filaments = nullptr; // Title text + Button* m_btn_add_gradient = nullptr; // Add gradient button + Button* m_btn_add_pattern = nullptr; // Add pattern button + Button* m_btn_add_color = nullptr; // Add color-match button + Button* m_btn_toggle_mixed_filaments = nullptr; // Collapse/expand toggle button + bool m_mixed_filaments_collapsed = false; // Collapse state + bool m_skip_mixed_filament_sync_once = false; // Local edits already mutated manager in place. + std::unordered_set m_expanded_mixed_filament_rows; // Expanded row editors + struct MixedFilamentRowBinding { + size_t mixed_id = size_t(-1); + wxWindow *row = nullptr; + }; + std::vector m_mixed_filament_row_bindings; + std::vector m_mixed_filament_ui_order; + bool m_mixed_filament_drag_active = false; + size_t m_mixed_filament_drag_source_mixed_id = size_t(-1); + // Physical filament scrolled window + wxScrolledWindow* m_scrolled_filaments = nullptr; + wxPanel* m_panel_scrolled_filament_content = nullptr; + // Color mix panel + StaticBox* m_panel_physical_filaments_title = nullptr; + StaticBox* m_panel_color_mix_title = nullptr; + wxPanel* m_panel_color_mix_content = nullptr; + wxScrolledWindow* m_scrolled_color_mix = nullptr; + ScalableButton* m_color_mix_icon = nullptr; + ScalableButton* m_btn_add_color_mix = nullptr; + ScalableButton* m_btn_del_color_mix = nullptr; + wxStaticLine* m_staticline2; wxPanel* m_panel_project_title; ScalableButton* m_filament_icon = nullptr; @@ -899,6 +962,7 @@ std::vector get_min_flush_volumes(const DynamicPrintConfig& full_config) return extra_flush_volumes; } + // Sidebar / public struct DynamicFilamentList : DynamicList @@ -996,6 +1060,528 @@ struct DynamicFilamentList1Based : DynamicFilamentList }; +class MixedFilamentColorMatchDialog : public DPIDialog +{ +public: + MixedFilamentColorMatchDialog(wxWindow* parent, const std::vector& physical_colors, const wxColour& initial_color) + : DPIDialog(parent ? parent : static_cast(wxGetApp().mainframe), + wxID_ANY, + _L("Add Color"), + wxDefaultPosition, + wxDefaultSize, + wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER) + , m_physical_colors(physical_colors) + { + m_recipe_timer.SetOwner(this); + m_loading_timer.SetOwner(this); + m_display_context = build_mixed_filament_display_context(m_physical_colors); + + m_palette.reserve(m_physical_colors.size()); + for (const std::string& hex : m_physical_colors) + m_palette.emplace_back(parse_mixed_color(hex)); + + const wxColour safe_initial = initial_color.IsOk() ? + initial_color : + (m_palette.size() >= 2 ? blend_pair_filament_mixer(m_palette[0], m_palette[1], 0.5f) : + wxColour("#26A69A")); + std::vector initial_weights(m_palette.size(), 0); + if (!initial_weights.empty()) + initial_weights[0] = 100; + if (initial_weights.size() >= 2) { + initial_weights[0] = 50; + initial_weights[1] = 50; + } + + std::vector filament_ids; + filament_ids.reserve(m_palette.size()); + for (size_t idx = 0; idx < m_palette.size(); ++idx) + filament_ids.emplace_back(unsigned(idx + 1)); + + SetMinSize(wxSize(FromDIP(430), FromDIP(520))); + + auto* root = new wxBoxSizer(wxVERTICAL); + auto* description = new wxStaticText(this, wxID_ANY, + _L("Pick from the current filament gamut. The dialog previews the closest 2-color, 3-color, " + "or 4-color FilamentMixer recipe before it is added.")); + description->Wrap(FromDIP(390)); + root->Add(description, 0, wxEXPAND | wxALL, FromDIP(12)); + + m_color_map = new MixedFilamentColorMapPanel(this, filament_ids, m_palette, initial_weights, wxSize(FromDIP(260), FromDIP(260))); + root->Add(m_color_map, 1, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(12)); + + auto* hex_row = new wxBoxSizer(wxHORIZONTAL); + hex_row->Add(new wxStaticText(this, wxID_ANY, _L("Hex")), 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(8)); + m_hex_input = new wxTextCtrl(this, wxID_ANY, normalize_color_match_hex(safe_initial.GetAsString(wxC2S_HTML_SYNTAX)), + wxDefaultPosition, wxDefaultSize, wxTE_PROCESS_ENTER); + m_hex_input->SetToolTip(_L("Enter a hex color like #00FF88. The picker will snap to the closest supported FilamentMixer color.")); + hex_row->Add(m_hex_input, 1, wxALIGN_CENTER_VERTICAL); + hex_row->AddSpacer(FromDIP(8)); + m_classic_picker = new wxColourPickerCtrl(this, wxID_ANY, safe_initial); + m_classic_picker->SetToolTip(_L("Classic color picker. The result will snap to the closest supported FilamentMixer color.")); + hex_row->Add(m_classic_picker, 0, wxALIGN_CENTER_VERTICAL); + root->Add(hex_row, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, FromDIP(12)); + + auto* range_row = new wxBoxSizer(wxHORIZONTAL); + range_row->Add(new wxStaticText(this, wxID_ANY, _L("Range")), 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(8)); + m_range_slider = new wxSlider(this, wxID_ANY, m_min_component_percent, 0, 50); + m_range_slider->SetToolTip(_L("Minimum percent for each participating color. Higher values block highly skewed mixes.")); + range_row->Add(m_range_slider, 1, wxALIGN_CENTER_VERTICAL); + range_row->AddSpacer(FromDIP(8)); + m_range_value = new wxStaticText(this, wxID_ANY, wxEmptyString); + range_row->Add(m_range_value, 0, wxALIGN_CENTER_VERTICAL); + root->Add(range_row, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, FromDIP(12)); + + auto* summary_grid = new wxFlexGridSizer(2, FromDIP(8), FromDIP(8)); + summary_grid->AddGrowableCol(1, 1); + + summary_grid->Add(new wxStaticText(this, wxID_ANY, _L("Requested")), 0, wxALIGN_CENTER_VERTICAL); + auto* selected_row = new wxBoxSizer(wxHORIZONTAL); + m_selected_preview = new wxPanel(this, wxID_ANY, wxDefaultPosition, wxSize(FromDIP(72), FromDIP(24)), wxBORDER_SIMPLE); + selected_row->Add(m_selected_preview, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(8)); + m_selected_label = new wxStaticText(this, wxID_ANY, wxEmptyString); + selected_row->Add(m_selected_label, 1, wxALIGN_CENTER_VERTICAL); + summary_grid->Add(selected_row, 1, wxEXPAND); + + summary_grid->Add(new wxStaticText(this, wxID_ANY, _L("Creates")), 0, wxALIGN_CENTER_VERTICAL); + auto* recipe_row = new wxBoxSizer(wxHORIZONTAL); + m_recipe_preview = new wxPanel(this, wxID_ANY, wxDefaultPosition, wxSize(FromDIP(72), FromDIP(24)), wxBORDER_SIMPLE); + recipe_row->Add(m_recipe_preview, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(8)); + m_recipe_label = new wxStaticText(this, wxID_ANY, wxEmptyString); + m_recipe_label->Wrap(FromDIP(280)); + recipe_row->Add(m_recipe_label, 1, wxALIGN_CENTER_VERTICAL); + summary_grid->Add(recipe_row, 1, wxEXPAND); + + root->Add(summary_grid, 0, wxEXPAND | wxALL, FromDIP(12)); + + m_delta_label = new wxStaticText(this, wxID_ANY, wxEmptyString); + root->Add(m_delta_label, 0, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(12)); + + m_presets_label = new wxStaticText(this, wxID_ANY, _L("Exact preset mixes")); + root->Add(m_presets_label, 0, wxLEFT | wxRIGHT | wxTOP, FromDIP(12)); + m_presets_host = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxSize(-1, FromDIP(96)), wxVSCROLL | wxBORDER_SIMPLE); + m_presets_host->SetScrollRate(FromDIP(6), FromDIP(6)); + m_presets_sizer = new wxWrapSizer(wxHORIZONTAL, wxWRAPSIZER_DEFAULT_FLAGS); + m_presets_host->SetSizer(m_presets_sizer); + root->Add(m_presets_host, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, FromDIP(12)); + + m_error_label = new wxStaticText(this, wxID_ANY, wxEmptyString); + m_error_label->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#D32F2F"))); + root->Add(m_error_label, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, FromDIP(12)); + + if (wxSizer* button_sizer = CreateStdDialogButtonSizer(wxOK | wxCANCEL)) + root->Add(button_sizer, 0, wxEXPAND | wxALL, FromDIP(12)); + + m_loading_panel = new wxPanel(this, wxID_ANY); + m_loading_panel->SetMinSize(wxSize(-1, FromDIP(24))); + auto* loading_row = new wxBoxSizer(wxHORIZONTAL); + m_loading_label = new wxStaticText(m_loading_panel, wxID_ANY, " "); + loading_row->Add(m_loading_label, 1, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(8)); + m_loading_gauge = new wxGauge(m_loading_panel, wxID_ANY, 100, wxDefaultPosition, wxSize(FromDIP(150), FromDIP(8)), + wxGA_HORIZONTAL | wxGA_SMOOTH); + m_loading_gauge->SetValue(0); + m_loading_gauge->Enable(false); + loading_row->Add(m_loading_gauge, 0, wxALIGN_CENTER_VERTICAL); + m_loading_panel->SetSizer(loading_row); + root->Add(m_loading_panel, 0, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, FromDIP(12)); + + SetSizerAndFit(root); + + m_selected_target = safe_initial; + m_requested_target = safe_initial; + if (m_color_map) + m_color_map->set_min_component_percent(m_min_component_percent); + update_range_label(); + rebuild_presets_ui(); + sync_inputs_to_requested(); + update_dialog_state(); + + if (m_color_map) { + m_color_map->Bind(wxEVT_SLIDER, [this](wxCommandEvent&) { + if (!m_color_map) + return; + request_recipe_match(m_color_map->selected_color(), true, _L("Matching closest supported mix...")); + }); + } + + if (m_hex_input) { + m_hex_input->Bind(wxEVT_TEXT_ENTER, [this](wxCommandEvent&) { apply_hex_input(true); }); + m_hex_input->Bind(wxEVT_KILL_FOCUS, [this](wxFocusEvent& evt) { + apply_hex_input(false); + evt.Skip(); + }); + } + if (m_classic_picker) { + m_classic_picker->Bind(wxEVT_COLOURPICKER_CHANGED, [this](wxColourPickerEvent& evt) { + if (m_syncing_inputs) + return; + apply_requested_target(evt.GetColour()); + }); + } + if (m_range_slider) { + m_range_slider->Bind(wxEVT_SLIDER, [this](wxCommandEvent&) { + m_min_component_percent = m_range_slider ? std::clamp(m_range_slider->GetValue(), 0, 50) : m_min_component_percent; + update_range_label(); + if (m_color_map) + m_color_map->set_min_component_percent(m_min_component_percent); + rebuild_presets_ui(); + request_recipe_match(m_requested_target, true, _L("Matching closest supported mix...")); + }); + } + + Bind(wxEVT_TIMER, [this](wxTimerEvent&) { refresh_selected_recipe(); }, m_recipe_timer.GetId()); + Bind( + wxEVT_TIMER, + [this](wxTimerEvent&) { + if (m_loading_gauge && m_recipe_loading) + m_loading_gauge->Pulse(); + }, + m_loading_timer.GetId()); + if (wxWindow* ok_button = FindWindow(wxID_OK)) { + ok_button->Bind(wxEVT_BUTTON, [this](wxCommandEvent& evt) { + if (m_recipe_refresh_pending) + refresh_selected_recipe(); + if (m_recipe_loading || !m_selected_recipe.valid) + return; + evt.Skip(); + }); + } + + Bind(wxEVT_CLOSE_WINDOW, [this](wxCloseEvent&) { EndModal(wxID_CANCEL); }); + + CentreOnParent(); + wxGetApp().UpdateDlgDarkUI(this); + } + + ~MixedFilamentColorMatchDialog() override + { + if (m_recipe_timer.IsRunning()) + m_recipe_timer.Stop(); + if (m_loading_timer.IsRunning()) + m_loading_timer.Stop(); + } + + void begin_initial_recipe_load() { request_recipe_match(m_requested_target, false, _L("Calculating closest supported mix...")); } + + MixedColorMatchRecipeResult selected_recipe() const { return m_selected_recipe; } + + void on_dpi_changed(const wxRect& suggested_rect) override + { + wxUnusedVar(suggested_rect); + Layout(); + Fit(); + Refresh(); + } + +private: + void sync_recipe_preview(MixedColorMatchRecipeResult& recipe, const wxColour* requested_target = nullptr) + { + if (!recipe.valid) + return; + + recipe.preview_color = compute_color_match_recipe_display_color(recipe, m_display_context); + if (requested_target != nullptr && requested_target->IsOk()) + recipe.delta_e = color_delta_e00(*requested_target, recipe.preview_color); + } + + void update_range_label() + { + if (m_range_value) + m_range_value->SetLabel(wxString::Format(_L("%d%% min"), m_min_component_percent)); + } + + void rebuild_presets_ui() + { + if (!m_presets_host || !m_presets_sizer || !m_presets_label) + return; + + m_presets = build_color_match_presets(m_physical_colors, m_min_component_percent); + for (MixedColorMatchRecipeResult& preset : m_presets) + sync_recipe_preview(preset); + + m_presets_host->Freeze(); + while (m_presets_sizer->GetItemCount() > 0) { + wxSizerItem* item = m_presets_sizer->GetItem(size_t(0)); + wxWindow* window = item ? item->GetWindow() : nullptr; + m_presets_sizer->Remove(0); + if (window) + window->Destroy(); + } + + for (const MixedColorMatchRecipeResult& preset : m_presets) { + auto* button = new wxBitmapButton(m_presets_host, wxID_ANY, + make_color_match_swatch_bitmap(preset.preview_color, wxSize(FromDIP(30), FromDIP(20))), + wxDefaultPosition, wxDefaultSize, wxBU_EXACTFIT); + const wxString tooltip = from_u8(summarize_color_match_recipe(preset)) + "\n" + + normalize_color_match_hex(preset.preview_color.GetAsString(wxC2S_HTML_SYNTAX)); + button->SetToolTip(tooltip); + button->Bind(wxEVT_BUTTON, [this, preset](wxCommandEvent&) { apply_preset(preset); }); + m_presets_sizer->Add(button, 0, wxALL, FromDIP(2)); + } + + m_presets_host->FitInside(); + const bool show_presets = !m_presets.empty(); + m_presets_label->Show(show_presets); + m_presets_host->Show(show_presets); + m_presets_host->Thaw(); + } + + void set_recipe_loading(bool loading, const wxString& message) + { + m_recipe_loading = loading; + if (!message.empty()) + m_loading_message = message; + + if (m_loading_label) + m_loading_label->SetLabel(loading ? m_loading_message : wxString(" ")); + if (m_loading_gauge) { + if (loading) { + m_loading_gauge->Enable(true); + m_loading_gauge->Pulse(); + if (!m_loading_timer.IsRunning()) + m_loading_timer.Start(100); + } else { + if (m_loading_timer.IsRunning()) + m_loading_timer.Stop(); + m_loading_gauge->SetValue(0); + m_loading_gauge->Enable(false); + } + } + } + + void sync_inputs_to_requested() + { + m_syncing_inputs = true; + if (m_hex_input) + m_hex_input->ChangeValue(normalize_color_match_hex(m_requested_target.GetAsString(wxC2S_HTML_SYNTAX))); + if (m_classic_picker) + m_classic_picker->SetColour(m_requested_target); + m_syncing_inputs = false; + } + + bool apply_requested_target(const wxColour& requested_target) + { + request_recipe_match(requested_target, false, _L("Matching closest supported mix...")); + return true; + } + + bool apply_hex_input(bool show_invalid_error) + { + if (!m_hex_input || m_syncing_inputs) + return false; + + wxColour parsed; + if (!try_parse_color_match_hex(m_hex_input->GetValue(), parsed)) { + if (show_invalid_error && m_error_label) + m_error_label->SetLabel(_L("Use a valid hex color like #00FF88.")); + return false; + } + + return apply_requested_target(parsed); + } + + void request_recipe_match(const wxColour& requested_target, bool debounce, const wxString& loading_message) + { + m_requested_target = requested_target; + m_selected_target = requested_target; + sync_inputs_to_requested(); + + ++m_recipe_request_token; + set_recipe_loading(true, loading_message); + + if (m_recipe_timer.IsRunning()) + m_recipe_timer.Stop(); + m_recipe_refresh_pending = debounce; + update_dialog_state(); + + if (debounce) { + m_recipe_timer.StartOnce(120); + return; + } + + launch_recipe_match(m_recipe_request_token, requested_target); + } + + void refresh_selected_recipe() + { + m_recipe_refresh_pending = false; + launch_recipe_match(m_recipe_request_token, m_requested_target); + } + + void launch_recipe_match(size_t request_token, const wxColour& requested_target) + { + const std::vector physical_colors = m_physical_colors; + const int min_component_percent = m_min_component_percent; + wxWeakRef weak_self(this); + std::thread([weak_self, physical_colors, requested_target, request_token, min_component_percent]() { + MixedColorMatchRecipeResult recipe = build_best_color_match_recipe(physical_colors, requested_target, min_component_percent); + wxGetApp().CallAfter([weak_self, requested_target, recipe = std::move(recipe), request_token]() mutable { + if (!weak_self) + return; + auto* self = static_cast(weak_self.get()); + self->handle_recipe_result(request_token, requested_target, std::move(recipe)); + }); + }).detach(); + } + + void handle_recipe_result(size_t request_token, const wxColour& requested_target, MixedColorMatchRecipeResult recipe) + { + if (request_token != m_recipe_request_token) + return; + + m_has_recipe_result = true; + m_selected_recipe = std::move(recipe); + sync_recipe_preview(m_selected_recipe, &requested_target); + set_recipe_loading(false, wxEmptyString); + + if (m_selected_recipe.valid) { + m_selected_target = m_selected_recipe.preview_color; + if (m_color_map) + m_color_map->set_normalized_weights(expand_color_match_recipe_weights(m_selected_recipe, m_palette.size()), false); + sync_inputs_to_requested(); + } else { + m_selected_target = requested_target; + } + + update_dialog_state(); + } + + void apply_preset(MixedColorMatchRecipeResult preset) + { + preset.delta_e = 0.0; + sync_recipe_preview(preset); + ++m_recipe_request_token; + m_requested_target = preset.preview_color; + m_selected_target = preset.preview_color; + m_selected_recipe = std::move(preset); + m_has_recipe_result = true; + m_recipe_refresh_pending = false; + if (m_recipe_timer.IsRunning()) + m_recipe_timer.Stop(); + set_recipe_loading(false, wxEmptyString); + if (m_color_map) + m_color_map->set_normalized_weights(expand_color_match_recipe_weights(m_selected_recipe, m_palette.size()), false); + sync_inputs_to_requested(); + update_dialog_state(); + } + + void update_dialog_state() + { + const wxColour fallback = wxColour("#26A69A"); + if (m_selected_preview) { + m_selected_preview->SetBackgroundColour(m_requested_target.IsOk() ? m_requested_target : fallback); + m_selected_preview->Refresh(); + } + if (m_selected_label) + m_selected_label->SetLabel(m_requested_target.IsOk() ? + normalize_color_match_hex(m_requested_target.GetAsString(wxC2S_HTML_SYNTAX)) : + normalize_color_match_hex(fallback.GetAsString(wxC2S_HTML_SYNTAX))); + + const bool valid = m_selected_recipe.valid; + const wxColour recipe_color = (valid && m_selected_recipe.preview_color.IsOk()) ? + m_selected_recipe.preview_color : + (m_requested_target.IsOk() ? m_requested_target : fallback); + if (m_recipe_preview) { + m_recipe_preview->SetBackgroundColour(recipe_color); + m_recipe_preview->Refresh(); + } + if (m_recipe_label) { + if (m_recipe_loading) { + m_recipe_label->SetLabel(m_loading_message); + } else if (valid) { + const wxString recipe_summary = from_u8(summarize_color_match_recipe(m_selected_recipe)); + const wxString recipe_hex = normalize_color_match_hex(recipe_color.GetAsString(wxC2S_HTML_SYNTAX)); + m_recipe_label->SetLabel(recipe_summary + " " + recipe_hex); + } else if (m_has_recipe_result) { + m_recipe_label->SetLabel(_L("No supported 2-color, 3-color, or 4-color recipe found.")); + } else { + m_recipe_label->SetLabel(wxEmptyString); + } + } + if (m_delta_label) { + if (m_recipe_loading && m_requested_target.IsOk()) { + m_delta_label->SetLabel( + wxString::Format(_L("Matching %s..."), normalize_color_match_hex(m_requested_target.GetAsString(wxC2S_HTML_SYNTAX)))); + } else if (valid && m_requested_target.IsOk()) { + m_delta_label->SetLabel(wxString::Format(_L("Requested %s, closest recipe delta: %.2f"), + normalize_color_match_hex(m_requested_target.GetAsString(wxC2S_HTML_SYNTAX)), + m_selected_recipe.delta_e)); + } else { + m_delta_label->SetLabel(wxEmptyString); + } + } + if (m_error_label) { + if (m_recipe_loading) + m_error_label->SetLabel(wxEmptyString); + else if (!valid && m_has_recipe_result) + m_error_label->SetLabel( + _L("Unable to create a color mix from the current physical filament colors within the selected range.")); + else if (m_hex_input && !m_syncing_inputs) { + wxColour parsed; + if (!try_parse_color_match_hex(m_hex_input->GetValue(), parsed)) + m_error_label->SetLabel(_L("Use a valid hex color like #00FF88.")); + else + m_error_label->SetLabel(wxEmptyString); + } else { + m_error_label->SetLabel(wxEmptyString); + } + } + if (wxWindow* ok_button = FindWindow(wxID_OK)) + ok_button->Enable(valid && !m_recipe_loading && !m_recipe_refresh_pending); + + Layout(); + } + +private: + std::vector m_physical_colors; + MixedFilamentDisplayContext m_display_context; + std::vector m_palette; + std::vector m_presets; + MixedFilamentColorMapPanel* m_color_map = nullptr; + wxTextCtrl* m_hex_input = nullptr; + wxColourPickerCtrl* m_classic_picker = nullptr; + wxSlider* m_range_slider = nullptr; + wxStaticText* m_range_value = nullptr; + wxStaticText* m_presets_label = nullptr; + wxScrolledWindow* m_presets_host = nullptr; + wxWrapSizer* m_presets_sizer = nullptr; + wxPanel* m_loading_panel = nullptr; + wxStaticText* m_loading_label = nullptr; + wxGauge* m_loading_gauge = nullptr; + wxPanel* m_selected_preview = nullptr; + wxStaticText* m_selected_label = nullptr; + wxPanel* m_recipe_preview = nullptr; + wxStaticText* m_recipe_label = nullptr; + wxStaticText* m_delta_label = nullptr; + wxStaticText* m_error_label = nullptr; + wxColour m_requested_target{wxColour("#26A69A")}; + wxColour m_selected_target{wxColour("#26A69A")}; + MixedColorMatchRecipeResult m_selected_recipe; + wxTimer m_recipe_timer; + wxTimer m_loading_timer; + wxString m_loading_message; + size_t m_recipe_request_token{0}; + int m_min_component_percent{15}; + bool m_has_recipe_result{false}; + bool m_recipe_loading{false}; + bool m_recipe_refresh_pending{false}; + bool m_syncing_inputs{false}; +}; + + +MixedColorMatchRecipeResult prompt_best_color_match_recipe(wxWindow* parent, + const std::vector& physical_colors, + const wxColour& initial_color) +{ + MixedFilamentColorMatchDialog dlg(parent, physical_colors, initial_color); + dlg.begin_initial_recipe_load(); + if (dlg.ShowModal() != wxID_OK) { + MixedColorMatchRecipeResult cancelled; + cancelled.cancelled = true; + return cancelled; + } + + return dlg.selected_recipe(); +} static DynamicFilamentList dynamic_filament_list; static DynamicFilamentList1Based dynamic_filament_list_1_based; @@ -1024,11 +1610,6 @@ Sidebar::Sidebar(Plater *parent) Choice::register_dynamic_list("wipe_tower_filament", &dynamic_filament_list); p->scrolled = new wxPanel(this); - // p->scrolled->SetScrollbars(0, 100, 1, 2); // ys_DELETE_after_testing. pixelsPerUnitY = 100 - // but this cause the bad layout of the sidebar, when all infoboxes appear. - // As a result we can see the empty block at the bottom of the sidebar - // But if we set this value to 5, layout will be better - //p->scrolled->SetScrollRate(0, 5); p->scrolled->SetBackgroundColour(*wxWHITE); @@ -1432,7 +2013,7 @@ Sidebar::Sidebar(Plater *parent) p->m_panel_filament_title->SetBackgroundColor2(0xF1F1F1); p->m_panel_filament_title->Bind(wxEVT_LEFT_UP, [this](wxMouseEvent &e) { if (e.GetPosition().x > (p->m_flushing_volume_btn->IsShown() - ? p->m_flushing_volume_btn->GetPosition().x : (p->m_bpButton_add_filament->GetPosition().x - FromDIP(30)))) // ORCA exclude area of del button from titlebar collapse/expand feature to fix undesired collapse when user spams del filament button + ? p->m_flushing_volume_btn->GetPosition().x : (p->m_bpButton_ams_filament->GetPosition().x - FromDIP(30)))) return; if (p->m_panel_filament_content->GetMaxHeight() == 0) p->m_panel_filament_content->SetMaxSize({-1, -1}); @@ -1443,11 +2024,10 @@ Sidebar::Sidebar(Plater *parent) wxBoxSizer* bSizer39; bSizer39 = new wxBoxSizer( wxHORIZONTAL ); - p->m_filament_icon = new ScalableButton(p->m_panel_filament_title, wxID_ANY, "filament"); - p->m_staticText_filament_settings = new Label(p->m_panel_filament_title, _L("Filament"), LB_PROPAGATE_MOUSE_EVENT); - bSizer39->Add(p->m_filament_icon, 0, wxALIGN_CENTER | wxLEFT, FromDIP(SidebarProps::TitlebarMargin())); - bSizer39->AddSpacer(FromDIP(SidebarProps::ElementSpacing())); - bSizer39->Add( p->m_staticText_filament_settings, 0, wxALIGN_CENTER ); + p->m_filament_config_icon = new ScalableButton(p->m_panel_filament_title, wxID_ANY, "filament_group"); + bSizer39->Add(p->m_filament_config_icon, 0, wxALIGN_CENTER | wxLEFT, FromDIP(SidebarProps::TitlebarMargin())); + p->m_staticText_filament_settings = new Label(p->m_panel_filament_title, _L("Filament Management"), LB_PROPAGATE_MOUSE_EVENT); + bSizer39->Add( p->m_staticText_filament_settings, 0, wxALIGN_CENTER | wxLEFT, FromDIP(SidebarProps::TitlebarMargin())); bSizer39->Add(FromDIP(10), 0, 0, 0, 0); bSizer39->SetMinSize(-1, FromDIP(30)); @@ -1477,7 +2057,7 @@ Sidebar::Sidebar(Plater *parent) ConfigOptionFloat* flush_multi_opt = project_config.option("flush_multiplier"); float flush_multiplier = flush_multi_opt ? flush_multi_opt->getFloat() : 1.f; - const std::vector extruder_colours = wxGetApp().plater()->get_extruder_colors_from_plater_config(); + const std::vector extruder_colours = wxGetApp().plater()->get_extruder_colors_from_plater_config(nullptr, false); const auto& full_config = wxGetApp().preset_bundle->full_config(); const auto& extra_flush_volumes = get_min_flush_volumes(full_config); WipingDialog dlg(parent, cast(init_matrix), cast(init_extruders), extruder_colours, extra_flush_volumes, flush_multiplier); @@ -1498,57 +2078,6 @@ Sidebar::Sidebar(Plater *parent) bSizer39->Add(p->m_flushing_volume_btn, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(5)); bSizer39->Hide(p->m_flushing_volume_btn); - ScalableButton* add_btn = new ScalableButton(p->m_panel_filament_title, wxID_ANY, "add_filament"); - add_btn->SetToolTip(_L("Add one filament")); - add_btn->Bind(wxEVT_BUTTON, [this, scrolled_sizer](wxCommandEvent& e){ - // Orca: limit filament choices to MAXIMUM_EXTRUDER_NUMBER - if (p->combos_filament.size() >= MAXIMUM_EXTRUDER_NUMBER) - return; - - int filament_count = p->combos_filament.size() + 1; - wxColour new_col = Plater::get_next_color_for_filament(); - std::string new_color = new_col.GetAsString(wxC2S_HTML_SYNTAX).ToStdString(); - wxGetApp().preset_bundle->set_num_filaments(filament_count, new_color); - wxGetApp().plater()->on_filaments_change(filament_count); - wxGetApp().get_tab(Preset::TYPE_PRINT)->update(); - wxGetApp().preset_bundle->export_selections(*wxGetApp().app_config); - auto_calc_flushing_volumes(filament_count - 1); - }); - p->m_bpButton_add_filament = add_btn; - - // ORCA Moved add button after delete button to prevent add button position change when remove icon automatically hidden - - ScalableButton* del_btn = new ScalableButton(p->m_panel_filament_title, wxID_ANY, "delete_filament"); - del_btn->SetToolTip(_L("Remove last filament")); - del_btn->Bind(wxEVT_BUTTON, [this, scrolled_sizer](wxCommandEvent &e) { - if (p->combos_filament.size() <= 1) - return; - - size_t filament_count = p->combos_filament.size() - 1; - if (wxGetApp().preset_bundle->is_the_only_edited_filament(filament_count) || (filament_count == 1)) { - wxGetApp().get_tab(Preset::TYPE_FILAMENT)->select_preset(wxGetApp().preset_bundle->filament_presets[0], false, "", true); - } - - if (p->editing_filament >= filament_count) { - p->editing_filament = -1; - } - - wxGetApp().preset_bundle->set_num_filaments(filament_count); - wxGetApp().plater()->on_filaments_change(filament_count); - wxGetApp().get_tab(Preset::TYPE_PRINT)->update(); - wxGetApp().preset_bundle->export_selections(*wxGetApp().app_config); - }); - p->m_bpButton_del_filament = del_btn; - - bSizer39->Add(del_btn, 0, wxALIGN_CENTER | wxLEFT, FromDIP(SidebarProps::IconSpacing())); - bSizer39->Add(add_btn, 0, wxALIGN_CENTER | wxLEFT, FromDIP(SidebarProps::IconSpacing())); // ORCA Moved add button after delete button to prevent add button position change when remove icon automatically hidden - bSizer39->AddSpacer(FromDIP(20)); - - if (p->combos_filament.size() <= 1) { // ORCA Fix Flushing button and Delete filament button not hidden on launch while only 1 filament exist - bSizer39->Hide(p->m_flushing_volume_btn); - bSizer39->Hide(p->m_bpButton_del_filament); // ORCA: Hide delete filament button if there is only one filament - } - ams_btn = new ScalableButton(p->m_panel_filament_title, wxID_ANY, "ams_fila_sync", wxEmptyString, wxDefaultSize, wxDefaultPosition, wxBU_EXACTFIT | wxNO_BORDER, false, 16); // ORCA match icon size with other icons as 16x16 ams_btn->SetToolTip(_L("Synchronize filament list from AMS")); @@ -1580,17 +2109,27 @@ Sidebar::Sidebar(Plater *parent) //wxBoxSizer* bSizer_filament_content; //bSizer_filament_content = new wxBoxSizer( wxHORIZONTAL ); - // BBS: filament double columns + // BBS: filament double columns, wrapped in scrolled window (max 3 rows) p->sizer_filaments = new wxBoxSizer(wxHORIZONTAL); p->sizer_filaments->Add(new wxBoxSizer(wxVERTICAL), 1, wxEXPAND); p->sizer_filaments->Add(new wxBoxSizer(wxVERTICAL), 1, wxEXPAND); + p->m_scrolled_filaments = new wxScrolledWindow(p->m_panel_filament_content, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL); + p->m_scrolled_filaments->SetScrollRate(0, 5); + p->m_scrolled_filaments->SetBackgroundColour(*wxWHITE); + p->m_panel_scrolled_filament_content = new wxPanel(p->m_scrolled_filaments, wxID_ANY); + p->m_panel_scrolled_filament_content->SetBackgroundColour(*wxWHITE); + p->m_panel_scrolled_filament_content->SetSizer(p->sizer_filaments); + auto* scrolled_fila_sizer = new wxBoxSizer(wxVERTICAL); + scrolled_fila_sizer->Add(p->m_panel_scrolled_filament_content, 0, wxEXPAND); + p->m_scrolled_filaments->SetSizer(scrolled_fila_sizer); + p->combos_filament.push_back(nullptr); /* first filament item */ // init_filament_combo(&p->combos_filament[0], 0); - p->combos_filament[0] = new PlaterPresetComboBox(p->m_panel_filament_content, Preset::TYPE_FILAMENT); + p->combos_filament[0] = new PlaterPresetComboBox(p->m_panel_scrolled_filament_content, Preset::TYPE_FILAMENT); auto combo_and_btn_sizer = new wxBoxSizer(wxHORIZONTAL); // BBS: filament double columns combo_and_btn_sizer->AddSpacer(FromDIP(SidebarProps::ContentMargin())); @@ -1604,7 +2143,7 @@ Sidebar::Sidebar(Plater *parent) edit_btn->SetBackgroundColour(wxColour(255, 255, 255)); edit_btn->SetToolTip(_L("Click to edit preset"));*/ - ScalableButton* edit_btn = new ScalableButton(p->m_panel_filament_content, wxID_ANY, "menu_filament"); + ScalableButton* edit_btn = new ScalableButton(p->m_panel_scrolled_filament_content, wxID_ANY, "menu_filament"); edit_btn->SetToolTip(_L("Click to edit preset")); PlaterPresetComboBox* combobox = p->combos_filament[0]; @@ -1626,14 +2165,250 @@ Sidebar::Sidebar(Plater *parent) //bSizer_filament_content->Add(p->sizer_filaments, 1, wxALIGN_CENTER | wxALL); wxSizer *sizer_filaments2 = new wxBoxSizer(wxVERTICAL); - sizer_filaments2->AddSpacer(FromDIP(16)); - sizer_filaments2->Add(p->sizer_filaments, 0, wxEXPAND, 0); - sizer_filaments2->AddSpacer(FromDIP(16)); + // --- Filaments title bar (same level as Color Mix title) --- + p->m_panel_physical_filaments_title = new StaticBox(p->m_panel_filament_content, wxID_ANY, wxDefaultPosition, + wxDefaultSize, wxTAB_TRAVERSAL | wxBORDER_NONE); + p->m_panel_physical_filaments_title->SetBackgroundColor(title_bg); + p->m_panel_physical_filaments_title->SetBackgroundColor2(0xF1F1F1); + p->m_panel_physical_filaments_title->SetMinSize(wxSize(-1, FromDIP(30))); + p->m_panel_physical_filaments_title->SetMaxSize(wxSize(-1, FromDIP(30))); + p->m_filament_icon = new ScalableButton(p->m_panel_physical_filaments_title, wxID_ANY, "filament"); + auto* physical_label = new Label(p->m_panel_physical_filaments_title, _L("Filaments"), LB_PROPAGATE_MOUSE_EVENT); + auto* h_physical_title = new wxBoxSizer(wxHORIZONTAL); + auto* white_left_f = new wxPanel(p->m_panel_physical_filaments_title, wxID_ANY, wxDefaultPosition, wxSize(FromDIP(SidebarProps::ContentMargin()), -1)); + white_left_f->SetBackgroundColour(*wxWHITE); + h_physical_title->Add(white_left_f, 0, wxEXPAND | wxTOP | wxBOTTOM, 0); + h_physical_title->Add(p->m_filament_icon, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(SidebarProps::TitlebarMargin())); + h_physical_title->AddSpacer(FromDIP(SidebarProps::ElementSpacing())); + h_physical_title->Add(physical_label, 0, wxALIGN_CENTER_VERTICAL); + h_physical_title->AddStretchSpacer(); + + // Delete filament button — delegates to delete_filament for consistent remap behavior + ScalableButton* del_btn = new ScalableButton(p->m_panel_physical_filaments_title, wxID_ANY, "delete_filament"); + del_btn->SetToolTip(_L("Remove last filament")); + del_btn->Bind(wxEVT_BUTTON, [this](wxCommandEvent &e) { + if (p->combos_filament.size() <= 1) + return; + delete_filament(size_t(-1), -1); + }); + p->m_bpButton_del_filament = del_btn; + + // Add filament button + ScalableButton* add_btn = new ScalableButton(p->m_panel_physical_filaments_title, wxID_ANY, "add_filament"); + add_btn->SetToolTip(_L("Add one filament")); + add_btn->Bind(wxEVT_BUTTON, [this](wxCommandEvent& e){ + if (p->combos_filament.size() >= MAXIMUM_EXTRUDER_NUMBER) + return; + PresetBundle* pb = wxGetApp().preset_bundle; + if (!pb || pb->mixed_filaments.total_filaments(p->combos_filament.size()) >= MAXIMUM_FILAMENT_NUMBER) + return; + int filament_count = p->combos_filament.size() + 1; + wxGetApp().plater()->confirm_auto_generated_gradients(filament_count); + wxColour new_col = Plater::get_next_color_for_filament(); + std::string new_color = new_col.GetAsString(wxC2S_HTML_SYNTAX).ToStdString(); + pb->set_num_filaments(filament_count, new_color); + wxGetApp().plater()->on_filaments_change(filament_count); + wxGetApp().get_tab(Preset::TYPE_PRINT)->update(); + pb->export_selections(*wxGetApp().app_config); + auto_calc_flushing_volumes(filament_count - 1); + }); + p->m_bpButton_add_filament = add_btn; + + h_physical_title->Add(del_btn, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(4)); + h_physical_title->Add(add_btn, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(8)); + auto* white_right_f = new wxPanel(p->m_panel_physical_filaments_title, wxID_ANY, wxDefaultPosition, wxSize(FromDIP(SidebarProps::ContentMargin()), -1)); + white_right_f->SetBackgroundColour(*wxWHITE); + h_physical_title->Add(white_right_f, 0, wxEXPAND | wxTOP | wxBOTTOM, 0); + p->m_panel_physical_filaments_title->SetSizer(h_physical_title); + p->m_panel_physical_filaments_title->Layout(); + + if (p->combos_filament.size() <= 1) + h_physical_title->Hide(p->m_bpButton_del_filament); + + sizer_filaments2->AddSpacer(FromDIP(8)); + sizer_filaments2->Add(p->m_panel_physical_filaments_title, 0, wxEXPAND, 0); + sizer_filaments2->AddSpacer(FromDIP(8)); + sizer_filaments2->Add(p->m_scrolled_filaments, 0, wxEXPAND, 0); + sizer_filaments2->AddSpacer(FromDIP(8)); + // --- Color Mix Panel (inside filament content, same level as filaments) --- + init_color_mix_panel(p->m_panel_filament_content, sizer_filaments2); p->m_panel_filament_content->SetSizer(sizer_filaments2); p->m_panel_filament_content->Layout(); scrolled_sizer->Add(p->m_panel_filament_content, 0, wxEXPAND, 0); } + // --- Mixed Filaments Panel (Collapsible) --- + { + // Create title bar (StaticBox for collapsible header) + p->m_panel_mixed_filaments_title = new StaticBox(p->scrolled, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL | wxBORDER_NONE); + p->m_panel_mixed_filaments_title->SetBackgroundColor(title_bg); + p->m_panel_mixed_filaments_title->SetBackgroundColor2(0xF1F1F1); + + // Create icon + p->m_mixed_filaments_icon = new ScalableButton(p->m_panel_mixed_filaments_title, wxID_ANY, "filament"); + + // Create title text + p->m_staticText_mixed_filaments = new Label(p->m_panel_mixed_filaments_title, _L("Mixed Filaments"), LB_PROPAGATE_MOUSE_EVENT); + + // Create "Add Gradient" button + p->m_btn_add_gradient = new Button(p->m_panel_mixed_filaments_title, _L("Add Gradient")); + p->m_btn_add_gradient->SetStyle(ButtonStyle::Confirm, ButtonType::Compact); + p->m_btn_add_gradient->Bind(wxEVT_BUTTON, [this](wxCommandEvent& e) { + // Add gradient mixed filament + if (wxGetApp().preset_bundle) { + auto &mgr = wxGetApp().preset_bundle->mixed_filaments; + // Get physical filament colors + ConfigOptionStrings *co = wxGetApp().preset_bundle->project_config.option("filament_colour"); + std::vector colors = co ? co->values : std::vector(); + // Add a custom gradient (50% mix) + mgr.add_custom_filament(1, 2, 50, colors); + // Persist the custom entries so they survive the clear/load cycle in update_mixed_filament_panel + if (ConfigOptionString *opt = wxGetApp().preset_bundle->project_config.option("mixed_filament_definitions")) + opt->value = mgr.serialize_custom_entries(); + update_mixed_filament_panel(false); + m_scrolled_sizer->Layout(); + } + }); + + // Create "Add Pattern" button + p->m_btn_add_pattern = new Button(p->m_panel_mixed_filaments_title, _L("Add Pattern")); + p->m_btn_add_pattern->SetStyle(ButtonStyle::Confirm, ButtonType::Compact); + p->m_btn_add_pattern->Bind(wxEVT_BUTTON, [this](wxCommandEvent& e) { + // Add pattern mixed filament + if (wxGetApp().preset_bundle) { + auto &mgr = wxGetApp().preset_bundle->mixed_filaments; + // Get physical filament colors + ConfigOptionStrings *co = wxGetApp().preset_bundle->project_config.option("filament_colour"); + std::vector colors = co ? co->values : std::vector(); + // Add a custom pattern filament (will be configured by user) + mgr.add_custom_filament(1, 2, 50, colors); + // Set manual pattern for the newly added filament + auto &mfs = mgr.mixed_filaments(); + if (!mfs.empty()) { + mfs.back().manual_pattern = "12"; + mfs.back().custom = true; + } + // Persist the custom entries so they survive the clear/load cycle in update_mixed_filament_panel + if (ConfigOptionString *opt = wxGetApp().preset_bundle->project_config.option("mixed_filament_definitions")) + opt->value = mgr.serialize_custom_entries(); + update_mixed_filament_panel(false); + m_scrolled_sizer->Layout(); + } + }); + + // Create "Add Color" button + p->m_btn_add_color = new Button(p->m_panel_mixed_filaments_title, _L("Add Color")); + p->m_btn_add_color->SetStyle(ButtonStyle::Confirm, ButtonType::Compact); + p->m_btn_add_color->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { + if (wxGetApp().preset_bundle == nullptr) + return; + + ConfigOptionStrings *co = wxGetApp().preset_bundle->project_config.option("filament_colour"); + const std::vector colors = co ? co->values : std::vector(); + if (colors.size() < 2) + return; + + const MixedColorMatchRecipeResult recipe = + prompt_best_color_match_recipe(this, colors, Plater::get_next_color_for_filament()); + if (recipe.cancelled) + return; + if (!recipe.valid) { + show_error(this, _L("Unable to create a color match from the current physical filament colors.")); + return; + } + + const MixedFilamentDisplayContext display_context = build_mixed_filament_display_context(colors); + auto &mgr = wxGetApp().preset_bundle->mixed_filaments; + mgr.set_display_context(display_context); + mgr.add_custom_filament(recipe.component_a, recipe.component_b, recipe.mix_b_percent, colors); + auto &mfs = mgr.mixed_filaments(); + if (!mfs.empty()) { + MixedFilament &created = mfs.back(); + created.manual_pattern = recipe.manual_pattern; + created.mix_b_percent = recipe.mix_b_percent; + created.gradient_component_ids = recipe.gradient_component_ids; + created.gradient_component_weights = recipe.gradient_component_weights; + created.pointillism_all_filaments = false; + created.distribution_mode = recipe.gradient_component_ids.empty() ? int(MixedFilament::Simple) : int(MixedFilament::LayerCycle); + created.custom = true; + created.display_color = compute_color_match_recipe_display_color(recipe, display_context).GetAsString(wxC2S_HTML_SYNTAX).ToStdString(); + } + + if (ConfigOptionString *opt = wxGetApp().preset_bundle->project_config.option("mixed_filament_definitions")) + opt->value = mgr.serialize_custom_entries(); + update_mixed_filament_panel(false); + m_scrolled_sizer->Layout(); + }); + + // Create horizontal sizer for title bar + wxBoxSizer* h_sizer_mixed_title = new wxBoxSizer(wxHORIZONTAL); + h_sizer_mixed_title->Add(p->m_mixed_filaments_icon, 0, wxALIGN_CENTER | wxLEFT, FromDIP(SidebarProps::TitlebarMargin())); + h_sizer_mixed_title->AddSpacer(FromDIP(SidebarProps::ElementSpacing())); + h_sizer_mixed_title->Add(p->m_staticText_mixed_filaments, 0, wxALIGN_CENTER); + h_sizer_mixed_title->AddStretchSpacer(); + h_sizer_mixed_title->Add(p->m_btn_add_gradient, 0, wxALIGN_CENTER | wxRIGHT, FromDIP(SidebarProps::ElementSpacing())); + h_sizer_mixed_title->Add(p->m_btn_add_pattern, 0, wxALIGN_CENTER | wxRIGHT, FromDIP(SidebarProps::ElementSpacing())); + h_sizer_mixed_title->Add(p->m_btn_add_color, 0, wxALIGN_CENTER | wxRIGHT, FromDIP(SidebarProps::TitlebarMargin())); + h_sizer_mixed_title->SetMinSize(-1, FromDIP(30)); + + p->m_panel_mixed_filaments_title->SetSizer(h_sizer_mixed_title); + p->m_panel_mixed_filaments_title->Layout(); + + // Add splitter line before title + auto spliter_mixed_1 = new ::StaticLine(p->scrolled); + spliter_mixed_1->SetLineColour("#A6A9AA"); + scrolled_sizer->Add(spliter_mixed_1, 0, wxEXPAND); + + // Add title bar to scrolled sizer + scrolled_sizer->Add(p->m_panel_mixed_filaments_title, 0, wxEXPAND | wxALL, 0); + + // Add splitter line after title + auto spliter_mixed_2 = new ::StaticLine(p->scrolled); + spliter_mixed_2->SetLineColour("#CECECE"); + scrolled_sizer->Add(spliter_mixed_2, 0, wxEXPAND); + + // Create content panel (collapsible) + p->m_panel_mixed_filaments_content = new wxPanel(p->scrolled, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL); + { + const bool is_dark = wxGetApp().dark_mode(); + p->m_panel_mixed_filaments_content->SetBackgroundColour(is_dark ? wxColour(45, 45, 49) : wxColour(255, 255, 255)); + } + + // Content sizer - store in member variable for later use + p->m_sizer_mixed_filaments_content = new wxBoxSizer(wxVERTICAL); + p->m_sizer_mixed_filaments_content->AddSpacer(FromDIP(SidebarProps::ContentMargin())); + p->m_panel_mixed_filaments_content->SetSizer(p->m_sizer_mixed_filaments_content); + p->m_panel_mixed_filaments_content->Layout(); + + // Add content panel to scrolled sizer + scrolled_sizer->Add(p->m_panel_mixed_filaments_content, 0, wxEXPAND, 0); + + // Bind collapse/expand event to title bar + p->m_panel_mixed_filaments_title->Bind(wxEVT_LEFT_UP, [this](wxMouseEvent& e) { + // Exclude button areas from collapse/expand + int button_left = p->m_panel_mixed_filaments_title->GetClientSize().x; + auto consider_button = [&button_left](wxWindow *button) { + if (button && button->IsShown()) + button_left = std::min(button_left, button->GetPosition().x); + }; + consider_button(p->m_btn_add_gradient); + consider_button(p->m_btn_add_pattern); + consider_button(p->m_btn_add_color); + if (e.GetPosition().x > button_left - FromDIP(12)) + return; + + if (p->m_panel_mixed_filaments_content->GetMaxHeight() == 0) + p->m_panel_mixed_filaments_content->SetMaxSize({-1, -1}); + else + p->m_panel_mixed_filaments_content->SetMaxSize({-1, 0}); + m_scrolled_sizer->Layout(); + }); + + // Initially hidden until 2+ filaments + p->m_panel_mixed_filaments_title->Hide(); + p->m_panel_mixed_filaments_content->Hide(); + } + { //add project title auto params_panel = ((MainFrame*)parent->GetParent())->m_param_panel; @@ -1749,7 +2524,7 @@ void Sidebar::create_printer_preset() void Sidebar::init_filament_combo(PlaterPresetComboBox **combo, const int filament_idx) { - *combo = new PlaterPresetComboBox(p->m_panel_filament_content, Slic3r::Preset::TYPE_FILAMENT); + *combo = new PlaterPresetComboBox(p->m_panel_scrolled_filament_content, Slic3r::Preset::TYPE_FILAMENT); (*combo)->set_filament_idx(filament_idx); auto combo_and_btn_sizer = new wxBoxSizer(wxHORIZONTAL); @@ -1778,7 +2553,7 @@ void Sidebar::init_filament_combo(PlaterPresetComboBox **combo, const int filame combo_and_btn_sizer->Add(32 * em / 10, 0, 0, 0, 0); combo_and_btn_sizer->Add(del_btn, 0, wxALIGN_CENTER_VERTICAL, 5 * em / 10); */ - ScalableButton* edit_btn = new ScalableButton(p->m_panel_filament_content, wxID_ANY, "menu_filament"); + ScalableButton* edit_btn = new ScalableButton(p->m_panel_scrolled_filament_content, wxID_ANY, "menu_filament"); edit_btn->SetToolTip(_L("Click to edit preset")); PlaterPresetComboBox* combobox = (*combo); @@ -1941,11 +2716,13 @@ void Sidebar::update_all_preset_comboboxes(bool reload_printer_view) } if (cfg.opt_bool("pellet_modded_printer")) { - p->m_staticText_filament_settings->SetLabel(_L("Pellets")); + p->m_staticText_filament_settings->SetLabel(_L("Pellet Configuration")); p->m_filament_icon->SetBitmap_("pellets"); + p->m_filament_config_icon->SetBitmap_("filament_group"); } else { - p->m_staticText_filament_settings->SetLabel(_L("Filament")); + p->m_staticText_filament_settings->SetLabel(_L("Filament Management")); p->m_filament_icon->SetBitmap_("filament"); + p->m_filament_config_icon->SetBitmap_("filament_group"); } show_SEMM_buttons(/*cfg.opt_bool("single_extruder_multi_material")*/true); @@ -2223,6 +3000,7 @@ void Sidebar::msw_rescale() p->m_printerinfo_syncbtn->msw_rescale(); p->m_printer_setting->msw_rescale(); p->m_filament_icon->msw_rescale(); + p->m_color_mix_icon->msw_rescale(); p->m_bpButton_add_filament->msw_rescale(); p->m_bpButton_del_filament->msw_rescale(); p->m_bpButton_ams_filament->msw_rescale(); @@ -2295,6 +3073,7 @@ void Sidebar::sys_color_changed() p->m_printerinfo_syncbtn->msw_rescale(); p->m_printer_setting->msw_rescale(); p->m_filament_icon->msw_rescale(); + p->m_color_mix_icon->msw_rescale(); p->m_bpButton_add_filament->msw_rescale(); p->m_bpButton_del_filament->msw_rescale(); p->m_bpButton_ams_filament->msw_rescale(); @@ -2370,8 +3149,31 @@ void Sidebar::on_filaments_change(size_t num_filaments) { auto& choices = combos_filament(); - if (num_filaments == choices.size()) + if (num_filaments == choices.size()) { + // Project load may keep the same physical filament count while mixed + // definitions changed. Refresh mixed panel even without count changes. + const bool sync_manager = !p->m_skip_mixed_filament_sync_once; + p->m_skip_mixed_filament_sync_once = false; + update_ui_from_settings(); + update_dynamic_filament_list(); + update_mixed_filament_panel(sync_manager); + + // Recalc scrolled filament window height (max 3 rows, matches color mix) + if (p->m_scrolled_filaments && p->m_panel_scrolled_filament_content) { + p->m_panel_scrolled_filament_content->Layout(); + const wxSize content_best = p->m_panel_scrolled_filament_content->GetBestSize(); + const int row_count = ((int)num_filaments + 1) / 2; // 2-column grid + const int desired_h = row_count > 3 + ? (content_best.GetHeight() * 3) / std::max(1, row_count) + : content_best.GetHeight(); + p->m_scrolled_filaments->SetMinSize({-1, desired_h}); + p->m_scrolled_filaments->SetMaxSize({-1, desired_h}); + } + Layout(); return; + } + + p->m_skip_mixed_filament_sync_once = false; if (choices.size() == 1 || num_filaments == 1) choices[0]->GetDropDown().Invalidate(); @@ -2399,36 +3201,4017 @@ void Sidebar::on_filaments_change(size_t num_filaments) if (p->m_flushing_volume_btn != nullptr && sizer != nullptr) { if (num_filaments > 1) { sizer->Show(p->m_flushing_volume_btn); - sizer->Show(p->m_bpButton_del_filament); // ORCA: Show delete filament button if multiple filaments } else { sizer->Hide(p->m_flushing_volume_btn); - sizer->Hide(p->m_bpButton_del_filament); // ORCA: Hide delete filament button if there is only one filament + } + } + if (p->m_bpButton_del_filament != nullptr && p->m_panel_physical_filaments_title != nullptr) { + auto* inner_sizer = p->m_panel_physical_filaments_title->GetSizer(); + if (inner_sizer) { + if (num_filaments > 1) + inner_sizer->Show(p->m_bpButton_del_filament); + else + inner_sizer->Hide(p->m_bpButton_del_filament); } } + // Recalc scrolled filament window height (max 3 rows, matches color mix) + if (p->m_scrolled_filaments && p->m_panel_scrolled_filament_content) { + p->m_panel_scrolled_filament_content->Layout(); + const wxSize content_best = p->m_panel_scrolled_filament_content->GetBestSize(); + const int row_count = ((int)num_filaments + 1) / 2; // 2-column grid + const int desired_h = row_count > 3 + ? (content_best.GetHeight() / std::max(1, row_count)) * 3 + : content_best.GetHeight(); + p->m_scrolled_filaments->SetMinSize({-1, desired_h}); + p->m_scrolled_filaments->SetMaxSize({-1, desired_h}); + } + Layout(); + wxWeakRef weak_this(this); + wxTheApp->CallAfter([weak_this]() { + Sidebar* sidebar = weak_this.get(); + if (sidebar && sidebar->p && sidebar->p->m_scrolled_filaments) { + int vh = sidebar->p->m_scrolled_filaments->GetVirtualSize().y; + int ch = sidebar->p->m_scrolled_filaments->GetClientSize().y; + sidebar->p->m_scrolled_filaments->Scroll(0, std::max(0, vh - ch)); + } + }); p->m_panel_filament_title->Refresh(); update_ui_from_settings(); update_dynamic_filament_list(); -} + update_mixed_filament_panel(); + update_color_mix_panel(); -void Sidebar::add_filament() { - if (p->combos_filament.size() >= MAXIMUM_EXTRUDER_NUMBER) return; - wxColour new_col = Plater::get_next_color_for_filament(); - add_custom_filament(new_col); + // Disable add buttons when combined filament limit reached + if (PresetBundle *pb = wxGetApp().preset_bundle) { + const bool can_add = pb->mixed_filaments.total_filaments(combos_filament().size()) < MAXIMUM_FILAMENT_NUMBER; + if (p->m_bpButton_add_filament) + p->m_bpButton_add_filament->Enable(can_add); + if (p->m_btn_add_color_mix) + p->m_btn_add_color_mix->Enable(can_add); + } } -void Sidebar::on_filaments_delete(size_t filament_id) + +class MixedGradientWeightsDialog : public wxDialog { - auto& choices = combos_filament(); +public: + MixedGradientWeightsDialog(wxWindow *parent, + const std::vector &filament_ids, + const std::vector &palette, + const std::vector &initial_weights) + : wxDialog(parent, wxID_ANY, _L("Gradient Mix Weights"), wxDefaultPosition, wxDefaultSize, + wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER) + { + m_colors.reserve(filament_ids.size()); + m_weights = normalize_color_match_weights(initial_weights, filament_ids.size()); + for (const unsigned int filament_id : filament_ids) { + if (filament_id >= 1 && filament_id <= palette.size()) + m_colors.emplace_back(palette[filament_id - 1]); + else + m_colors.emplace_back(wxColour("#26A69A")); + } + if (m_colors.empty()) + m_colors.emplace_back(wxColour("#26A69A")); + + auto *root = new wxBoxSizer(wxVERTICAL); + auto *hint = new wxStaticText(this, wxID_ANY, _L("Pick a point in the gradient map to control multi-filament mix.")); + root->Add(hint, 0, wxEXPAND | wxALL, FromDIP(10)); + + m_color_map = new MixedFilamentColorMapPanel(this, filament_ids, palette, initial_weights, + wxSize(FromDIP(240), FromDIP(240))); + root->Add(m_color_map, 1, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, FromDIP(10)); + + for (size_t i = 0; i < filament_ids.size(); ++i) { + auto *row = new wxBoxSizer(wxHORIZONTAL); + wxPanel *chip = new wxPanel(this, wxID_ANY, wxDefaultPosition, wxSize(FromDIP(18), FromDIP(18)), wxBORDER_SIMPLE); + chip->SetBackgroundColour(m_colors[i]); + row->Add(chip, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(6)); + row->Add(new wxStaticText(this, wxID_ANY, wxString::Format("F%d", int(filament_ids[i]))), + 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(8)); + auto *label = new wxStaticText(this, wxID_ANY, wxString::Format("%d%%", m_weights[i])); + label->SetFont(Label::Body_12); + row->Add(label, 0, wxALIGN_CENTER_VERTICAL); + root->Add(row, 0, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, FromDIP(8)); + m_weight_labels.emplace_back(label); + } + + root->Add(CreateSeparatedButtonSizer(wxOK | wxCANCEL), 0, wxEXPAND | wxALL, FromDIP(8)); + SetSizerAndFit(root); + SetMinSize(wxSize(FromDIP(380), std::max(GetSize().GetHeight(), FromDIP(460)))); + update_weight_labels(); + + Bind(wxEVT_CLOSE_WINDOW, [this](wxCloseEvent&) { EndModal(wxID_CANCEL); }); + + if (m_color_map) { + m_color_map->Bind(wxEVT_SLIDER, [this](wxCommandEvent &) { + m_weights = m_color_map ? m_color_map->normalized_weights() : m_weights; + update_weight_labels(); + }); + } + } - if (filament_id >= choices.size()) - return; + std::vector normalized_weights() const + { + return m_color_map ? m_color_map->normalized_weights() : m_weights; + } - if (choices.size() == 1) - choices[0]->GetDropDown().Invalidate(); +private: + void update_weight_labels() + { + for (size_t i = 0; i < m_weight_labels.size() && i < m_weights.size(); ++i) { + if (m_weight_labels[i]) + m_weight_labels[i]->SetLabel(wxString::Format("%d%%", m_weights[i])); + } + Layout(); + } - wxWindowUpdateLocker noUpdates_scrolled_panel(this); +private: + MixedFilamentColorMapPanel *m_color_map { nullptr }; + std::vector m_colors; + std::vector m_weights; + std::vector m_weight_labels; +}; + +// Forward declaration for MixedMixPreview (defined below) +class MixedMixPreview; + +// Inline editor panel for configuring a single mixed filament +class MixedFilamentConfigPanel : public wxPanel +{ +public: + using OnChangeFn = std::function; + + MixedFilamentConfigPanel(wxWindow *parent, + size_t mixed_id, + const MixedFilament &mf, + size_t num_physical, + const std::vector &physical_colors, + const std::vector &nozzle_diameters, + const std::vector &palette, + const MixedFilamentPreviewSettings &preview_settings, + bool bias_mode_enabled, + OnChangeFn on_change = {}); + + // Get the updated mixed filament data + MixedFilament get_mixed_filament() const { return m_mf; } + bool has_changes() const { return m_has_changes; } + static int effective_local_z_preview_mix_b_percent(const MixedFilament &mf, + const MixedFilamentPreviewSettings &preview_settings); + +private: + void build_ui(); + void update_preview(); + void update_local_z_breakdown(); + void update_component_picker_visuals(); + + size_t m_mixed_id; + MixedFilament m_mf; + size_t m_num_physical; + std::vector m_physical_colors; + std::vector m_nozzle_diameters; + std::vector m_palette; + MixedFilamentPreviewSettings m_preview_settings; + bool m_bias_mode_enabled = false; + bool m_has_changes = false; + + wxChoice *m_choice_a = nullptr; + wxChoice *m_choice_b = nullptr; + wxChoice *m_choice_c = nullptr; + wxChoice *m_choice_d = nullptr; + wxPanel *m_picker_a_container = nullptr; + wxPanel *m_picker_b_container = nullptr; + wxPanel *m_picker_c_container = nullptr; + wxPanel *m_picker_d_container = nullptr; + wxPanel *m_picker_a_swatch = nullptr; + wxPanel *m_picker_b_swatch = nullptr; + wxPanel *m_picker_c_swatch = nullptr; + wxPanel *m_picker_d_swatch = nullptr; + wxStaticText *m_picker_a_label = nullptr; + wxStaticText *m_picker_b_label = nullptr; + wxStaticText *m_picker_c_label = nullptr; + wxStaticText *m_picker_d_label = nullptr; + wxPanel *m_surface_offset_target_container = nullptr; + wxPanel *m_surface_offset_target_swatch = nullptr; + wxStaticText *m_surface_offset_target_label = nullptr; + MixedGradientSelector *m_blend_selector = nullptr; + wxStaticText *m_blend_label = nullptr; + wxTextCtrl *m_pattern_ctrl = nullptr; + wxCheckBox *m_local_z_limit_checkbox = nullptr; + wxSpinCtrl *m_local_z_limit_spin = nullptr; + wxSpinCtrlDouble *m_surface_offset_spin = nullptr; + std::vector m_pattern_quick_buttons; + MixedMixPreview *m_mix_preview = nullptr; + wxStaticText *m_breakdown_label = nullptr; + wxPanel *m_swatch = nullptr; + std::shared_ptr> m_selected_weight_state; + OnChangeFn m_on_change; + + // Helper functions (copied from update_mixed_filament_panel) + static std::vector decode_gradient_ids(const std::string &s); + static std::string encode_gradient_ids(const std::vector &ids); + static std::vector decode_manual_pattern_ids(const std::string &pattern, + unsigned int a, + unsigned int b, + size_t num_physical, + size_t wall_loops = 0); + static std::vector decode_gradient_weights(const std::string &s, size_t n); + static std::vector normalize_gradient_weights(const std::vector &w, size_t n); + static std::string encode_gradient_weights(const std::vector &w); + static std::vector build_weighted_pair_sequence(unsigned int a, unsigned int b, int percent_b, bool limit_cycle = false); + static std::vector build_weighted_multi_sequence(const std::vector &ids, + const std::vector &weights, + size_t max_cycle_limit = 0); + static std::string summarize_sequence(const std::vector &seq); + static std::string summarize_local_z_breakdown(const MixedFilament &mf, + const std::vector &weights, + const MixedFilamentPreviewSettings &preview_settings); + static std::string blend_from_sequence(const std::vector &colors, const std::vector &seq, const std::string &fallback); + static std::vector build_local_z_preview_pass_heights(double nominal_layer_height, + double lower_bound, + double upper_bound, + double preferred_a_height, + double preferred_b_height, + int mix_b_percent, + int max_sublayers_limit); +}; + +class MixedMixPreview : public wxPanel +{ +public: + explicit MixedMixPreview(wxWindow *parent) + : wxPanel(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxBORDER_NONE) + { + SetBackgroundStyle(wxBG_STYLE_PAINT); + SetMinSize(wxSize(FromDIP(120), FromDIP(20))); + Bind(wxEVT_PAINT, &MixedMixPreview::on_paint, this); + } + + void set_data(const std::vector &palette, + const std::vector &sequence, + bool same_layer_mode, + const std::vector &surface_offsets_mm, + const wxColour &fallback, + const wxString &left_overlay, + const wxString &right_overlay) + { + m_palette = palette; + m_sequence = sequence; + m_same_layer = same_layer_mode; + m_surface_offsets_mm = surface_offsets_mm; + m_fallback = fallback; + m_left_overlay = left_overlay; + m_right_overlay = right_overlay; + Refresh(); + } + +private: + wxRect preview_rect() const + { + const int margin_x = FromDIP(1); + const int margin_y = FromDIP(1); + const wxSize sz = GetClientSize(); + return wxRect(margin_x, margin_y, std::max(1, sz.GetWidth() - margin_x * 2), std::max(1, sz.GetHeight() - margin_y * 2)); + } + + wxColour color_for_extruder(unsigned int extruder_id) const + { + if (extruder_id >= 1 && extruder_id <= m_palette.size()) + return m_palette[extruder_id - 1]; + return m_fallback; + } + + double max_active_surface_offset_mm() const + { + double max_offset = 0.0; + for (double offset_mm : m_surface_offsets_mm) + max_offset = std::max(max_offset, std::abs(offset_mm)); + return std::max(0.001, max_offset); + } + + int slot_inset_for_extruder(unsigned int extruder_id, int slot_extent) const + { + if (extruder_id == 0 || extruder_id >= m_surface_offsets_mm.size() || slot_extent <= 2) + return 0; + + const double offset_mm = m_surface_offsets_mm[extruder_id]; + if (std::abs(offset_mm) <= EPSILON) + return 0; + + const double normalized = std::clamp(std::abs(offset_mm) / max_active_surface_offset_mm(), 0.0, 1.0); + const int inset = int(std::round(normalized * slot_extent * 0.45)) * (offset_mm < 0.0 ? -1 : 1); + return std::clamp(inset, -std::max(0, slot_extent / 2), std::max(0, slot_extent / 2)); + } + + void on_paint(wxPaintEvent &) + { + wxAutoBufferedPaintDC dc(this); + dc.SetBackground(wxBrush(GetBackgroundColour())); + dc.Clear(); + + const wxRect rect = preview_rect(); + dc.SetPen(*wxTRANSPARENT_PEN); + dc.SetBrush(wxBrush(m_fallback)); + dc.DrawRectangle(rect); + + if (!m_sequence.empty()) { + if (m_same_layer) { + // Same-layer preview: full-height stripe lines. + const int stripes = 24; + const int stripe_w = std::max(1, rect.GetWidth() / stripes); + const size_t seq_len = m_sequence.size(); + for (int s = 0; s < stripes; ++s) { + const size_t idx = size_t(s % int(seq_len)); + const unsigned int extruder_id = m_sequence[idx]; + dc.SetBrush(wxBrush(color_for_extruder(m_sequence[idx]))); + const int x = rect.GetLeft() + s * stripe_w; + const int w = (s == stripes - 1) ? (rect.GetRight() - x + 1) : stripe_w; + const int inset = slot_inset_for_extruder(extruder_id, w); + wxRect draw_rect(x + inset / 2, rect.GetTop(), std::max(1, w - inset), rect.GetHeight()); + draw_rect.Intersect(rect); + if (draw_rect.GetWidth() > 0) + dc.DrawRectangle(draw_rect); + } + } else { + const int bars = 24; + const int bar_w = std::max(1, rect.GetWidth() / bars); + for (int i = 0; i < bars; ++i) { + size_t idx = 0; + if (m_sequence.size() > size_t(bars)) + idx = (size_t(i) * m_sequence.size()) / size_t(bars); + else + idx = size_t(i) % m_sequence.size(); + const unsigned int extruder_id = m_sequence[idx]; + dc.SetBrush(wxBrush(color_for_extruder(extruder_id))); + const int x = rect.GetLeft() + i * bar_w; + const int w = (i == bars - 1) ? (rect.GetRight() - x + 1) : bar_w; + const int inset = slot_inset_for_extruder(extruder_id, w); + wxRect draw_rect(x + inset / 2, rect.GetTop(), std::max(1, w - inset), rect.GetHeight()); + draw_rect.Intersect(rect); + if (draw_rect.GetWidth() > 0) + dc.DrawRectangle(draw_rect); + } + } + } + + auto draw_outlined_text = [this, &dc](const wxString &text, int x, int y) { + if (text.empty()) + return; + dc.SetTextForeground(wxColour(255, 255, 255)); + const int outline_radius = std::max(2, FromDIP(2)); + for (int ox = -outline_radius; ox <= outline_radius; ++ox) { + for (int oy = -outline_radius; oy <= outline_radius; ++oy) { + if (ox == 0 && oy == 0) + continue; + dc.DrawText(text, x + ox, y + oy); + } + } + dc.SetTextForeground(wxColour(22, 22, 22)); + dc.DrawText(text, x, y); + }; + + wxCoord left_w = 0, left_h = 0; + wxCoord right_w = 0, right_h = 0; + dc.GetTextExtent(m_left_overlay, &left_w, &left_h); + dc.GetTextExtent(m_right_overlay, &right_w, &right_h); + const int text_y = rect.GetTop() + std::max(0, (rect.GetHeight() - int(std::max(left_h, right_h))) / 2); + const int pad = FromDIP(6); + if (!m_left_overlay.empty()) + draw_outlined_text(m_left_overlay, rect.GetLeft() + pad, text_y); + if (!m_right_overlay.empty()) + draw_outlined_text(m_right_overlay, rect.GetRight() - pad - int(right_w), text_y); + + const bool is_dark = wxGetApp().dark_mode(); + dc.SetPen(wxPen(is_dark ? wxColour(110, 110, 110) : wxColour(170, 170, 170), 1)); + dc.SetBrush(*wxTRANSPARENT_BRUSH); + dc.DrawRectangle(rect); + } + +private: + std::vector m_palette; + std::vector m_sequence; + std::vector m_surface_offsets_mm; + bool m_same_layer { false }; + wxColour m_fallback { wxColour(38, 166, 154) }; + wxString m_left_overlay; + wxString m_right_overlay; +}; + +// Implementation of MixedFilamentConfigPanel helper functions +static std::vector build_grouped_manual_pattern_preview_sequence(const std::string &pattern, + unsigned int component_a, + unsigned int component_b, + size_t num_physical, + size_t wall_loops) +{ + std::vector sequence; + if (num_physical == 0) + return sequence; + + const std::string normalized = MixedFilamentManager::normalize_manual_pattern(pattern); + if (normalized.empty()) + return sequence; + + const std::vector groups = MixedFilamentManager::split_pattern_groups(normalized); + if (groups.empty()) + return sequence; + + MixedFilament dummy_mf; + dummy_mf.component_a = component_a; + dummy_mf.component_b = component_b; + + if (groups.size() == 1) { + const std::vector tokens = + MixedFilamentManager::split_pattern_group_to_tokens(groups[0], num_physical); + sequence.reserve(tokens.size()); + for (const std::string &token : tokens) { + const unsigned int extruder_id = + MixedFilamentManager::physical_filament_from_token(token, dummy_mf, num_physical); + if (extruder_id != 0) + sequence.emplace_back(extruder_id); + } + return sequence; + } + + std::vector> group_tokens; + group_tokens.reserve(groups.size()); + for (const std::string &group : groups) + group_tokens.push_back(MixedFilamentManager::split_pattern_group_to_tokens(group, num_physical)); + + constexpr size_t k_max_preview_cycle = 48; + size_t cycle = 1; + for (const auto &tokens : group_tokens) { + if (tokens.empty()) + continue; + cycle = std::lcm(cycle, tokens.size()); + if (cycle >= k_max_preview_cycle) { + cycle = k_max_preview_cycle; + break; + } + } + + const size_t preview_wall_loops = std::max(1, wall_loops == 0 ? groups.size() : wall_loops); + sequence.reserve(preview_wall_loops * cycle); + for (size_t layer_idx = 0; layer_idx < cycle; ++layer_idx) { + for (size_t wall_idx = 0; wall_idx < preview_wall_loops; ++wall_idx) { + const auto &tokens = group_tokens[std::min(wall_idx, group_tokens.size() - 1)]; + if (tokens.empty()) + continue; + const std::string &token = tokens[layer_idx % tokens.size()]; + const unsigned int extruder_id = + MixedFilamentManager::physical_filament_from_token(token, dummy_mf, num_physical); + if (extruder_id != 0) + sequence.emplace_back(extruder_id); + } + } + + return sequence; +} + +std::vector MixedFilamentConfigPanel::decode_gradient_ids(const std::string &s) +{ + return MixedFilamentManager::decode_gradient_component_ids(s, 0); +} + +std::string MixedFilamentConfigPanel::encode_gradient_ids(const std::vector &ids) +{ + return MixedFilamentManager::encode_gradient_component_ids(ids); +} + +std::vector MixedFilamentConfigPanel::decode_manual_pattern_ids(const std::string &pattern, + unsigned int a, + unsigned int b, + size_t num_physical, + size_t wall_loops) +{ + return build_grouped_manual_pattern_preview_sequence(pattern, a, b, num_physical, wall_loops); +} + +std::vector MixedFilamentConfigPanel::decode_gradient_weights(const std::string &s, size_t n) +{ + std::vector w; + if (s.empty() || n == 0) + return w; + + std::string token; + for (const char c : s) { + if (c >= '0' && c <= '9') { + token.push_back(c); + continue; + } + if (!token.empty()) { + w.emplace_back(std::max(0, std::atoi(token.c_str()))); + token.clear(); + } + } + if (!token.empty()) + w.emplace_back(std::max(0, std::atoi(token.c_str()))); + if (w.size() != n) + w.clear(); + return w; +} + +std::vector MixedFilamentConfigPanel::normalize_gradient_weights(const std::vector &w, size_t n) +{ + std::vector out = w; + if (out.size() != n) out.assign(n, n > 0 ? int(100 / n) : 0); + int sum = 0; + for (int &v : out) { v = std::max(0, v); sum += v; } + if (sum <= 0 && n > 0) { out.assign(n, 0); out[0] = 100; return out; } + std::vector rem(n, 0.); + int assigned = 0; + for (size_t i = 0; i < n; ++i) { + const double exact = 100.0 * double(out[i]) / double(sum); + out[i] = int(std::floor(exact)); + rem[i] = exact - double(out[i]); + assigned += out[i]; + } + int missing = std::max(0, 100 - assigned); + while (missing > 0) { + size_t best = 0; + double best_rem = -1.0; + for (size_t i = 0; i < rem.size(); ++i) { + if (rem[i] > best_rem) { best_rem = rem[i]; best = i; } + } + ++out[best]; + rem[best] = 0.0; + --missing; + } + return out; +} + +std::string MixedFilamentConfigPanel::encode_gradient_weights(const std::vector &w) +{ + std::ostringstream out; + for (size_t i = 0; i < w.size(); ++i) { + if (i > 0) + out << '/'; + out << std::max(0, w[i]); + } + return out.str(); +} + +namespace { + +std::pair effective_pair_preview_ratios(int percent_b) +{ + const int mix_b = std::clamp(percent_b, 0, 100); + int ratio_a = 1; + int ratio_b = 0; + + if (mix_b >= 100) { + ratio_a = 0; + ratio_b = 1; + } else if (mix_b > 0) { + const int pct_b = mix_b; + const int pct_a = 100 - pct_b; + const bool b_is_major = pct_b >= pct_a; + const int major_pct = b_is_major ? pct_b : pct_a; + const int minor_pct = b_is_major ? pct_a : pct_b; + const int major_layers = + std::max(1, int(std::lround(double(major_pct) / double(std::max(1, minor_pct))))); + ratio_a = b_is_major ? 1 : major_layers; + ratio_b = b_is_major ? major_layers : 1; + } + + if (ratio_a > 0 && ratio_b > 0) { + const int g = std::gcd(ratio_a, ratio_b); + if (g > 1) { + ratio_a /= g; + ratio_b /= g; + } + } + + return { std::max(0, ratio_a), std::max(0, ratio_b) }; +} + +std::vector build_effective_pair_preview_sequence(unsigned int component_a, + unsigned int component_b, + int percent_b, + bool limit_cycle) +{ + std::vector sequence; + if (component_a == 0 || component_b == 0 || component_a == component_b) + return sequence; + + auto [ratio_a, ratio_b] = effective_pair_preview_ratios(percent_b); + constexpr int k_max_cycle = 24; + if (limit_cycle && ratio_a > 0 && ratio_b > 0 && ratio_a + ratio_b > k_max_cycle) { + const double scale = double(k_max_cycle) / double(ratio_a + ratio_b); + ratio_a = std::max(1, int(std::round(double(ratio_a) * scale))); + ratio_b = std::max(1, int(std::round(double(ratio_b) * scale))); + } + if (ratio_a == 0 && ratio_b == 0) + ratio_a = 1; + + const int cycle = std::max(1, ratio_a + ratio_b); + sequence.reserve(size_t(cycle)); + for (int pos = 0; pos < cycle; ++pos) { + const int b_before = (pos * ratio_b) / cycle; + const int b_after = ((pos + 1) * ratio_b) / cycle; + sequence.emplace_back((b_after > b_before) ? component_b : component_a); + } + return sequence; +} + +std::string format_preview_sequence_percent(int count, int total) +{ + if (count <= 0 || total <= 0) + return ""; + + const double percent = 100.0 * double(count) / double(total); + const double rounded_tenths = std::round(percent * 10.0) / 10.0; + const double nearest_integer = std::round(rounded_tenths); + if (std::abs(rounded_tenths - nearest_integer) < 1e-6) + return wxString::Format("%d%%", int(nearest_integer)).ToStdString(); + return wxString::Format("%.1f%%", rounded_tenths).ToStdString(); +} + +} // namespace + +std::vector MixedFilamentConfigPanel::build_weighted_pair_sequence(unsigned int a, + unsigned int b, + int percent_b, + bool limit_cycle) +{ + return build_effective_pair_preview_sequence(a, b, percent_b, limit_cycle); +} + +static void reduce_weight_counts_to_cycle_limit(std::vector &counts, size_t cycle_limit) +{ + if (counts.empty() || cycle_limit == 0) + return; + + int total = std::accumulate(counts.begin(), counts.end(), 0); + if (total <= 0 || size_t(total) <= cycle_limit) + return; + + std::vector positive_indices; + positive_indices.reserve(counts.size()); + for (size_t i = 0; i < counts.size(); ++i) + if (counts[i] > 0) + positive_indices.emplace_back(i); + + if (positive_indices.empty()) { + counts.assign(counts.size(), 0); + return; + } + + std::vector reduced(counts.size(), 0); + if (cycle_limit < positive_indices.size()) { + std::sort(positive_indices.begin(), positive_indices.end(), [&counts](size_t lhs, size_t rhs) { + if (counts[lhs] != counts[rhs]) + return counts[lhs] > counts[rhs]; + return lhs < rhs; + }); + for (size_t i = 0; i < cycle_limit; ++i) + reduced[positive_indices[i]] = 1; + counts = std::move(reduced); + return; + } + + size_t remaining_slots = cycle_limit; + for (const size_t idx : positive_indices) { + reduced[idx] = 1; + --remaining_slots; + } + + int total_extras = 0; + std::vector extra_counts(counts.size(), 0); + for (const size_t idx : positive_indices) { + extra_counts[idx] = std::max(0, counts[idx] - 1); + total_extras += extra_counts[idx]; + } + if (remaining_slots == 0 || total_extras <= 0) { + counts = std::move(reduced); + return; + } + + std::vector remainders(counts.size(), -1.0); + size_t assigned_slots = 0; + for (const size_t idx : positive_indices) { + if (extra_counts[idx] == 0) + continue; + const double exact = double(remaining_slots) * double(extra_counts[idx]) / double(total_extras); + const int assigned = int(std::floor(exact)); + reduced[idx] += assigned; + assigned_slots += size_t(assigned); + remainders[idx] = exact - double(assigned); + } + + size_t missing_slots = remaining_slots > assigned_slots ? (remaining_slots - assigned_slots) : size_t(0); + while (missing_slots > 0) { + size_t best_idx = size_t(-1); + double best_remainder = -1.0; + int best_extra = -1; + for (const size_t idx : positive_indices) { + if (extra_counts[idx] == 0) + continue; + if (remainders[idx] > best_remainder || + (std::abs(remainders[idx] - best_remainder) <= 1e-9 && extra_counts[idx] > best_extra) || + (std::abs(remainders[idx] - best_remainder) <= 1e-9 && extra_counts[idx] == best_extra && idx < best_idx)) { + best_idx = idx; + best_remainder = remainders[idx]; + best_extra = extra_counts[idx]; + } + } + if (best_idx == size_t(-1)) + break; + ++reduced[best_idx]; + remainders[best_idx] = -1.0; + --missing_slots; + } + + counts = std::move(reduced); +} + +std::vector MixedFilamentConfigPanel::build_weighted_multi_sequence(const std::vector &ids, + const std::vector &weights, + size_t max_cycle_limit) +{ + std::vector seq; + if (ids.empty()) + return seq; + + std::vector filtered_ids; + std::vector counts; + filtered_ids.reserve(ids.size()); + counts.reserve(ids.size()); + + std::vector normalized = normalize_gradient_weights(weights, ids.size()); + for (size_t i = 0; i < ids.size(); ++i) { + const int weight = (i < normalized.size()) ? std::max(0, normalized[i]) : 0; + if (weight <= 0) + continue; + filtered_ids.emplace_back(ids[i]); + counts.emplace_back(weight); + } + if (filtered_ids.empty()) { + filtered_ids = ids; + counts.assign(ids.size(), 1); + } + + int g = 0; + for (const int c : counts) + g = std::gcd(g, std::max(1, c)); + if (g > 1) { + for (int &c : counts) + c = std::max(1, c / g); + } + + constexpr size_t k_max_cycle = 48; + const size_t effective_cycle_limit = + max_cycle_limit > 0 ? std::min(k_max_cycle, std::max(1, max_cycle_limit)) : k_max_cycle; + reduce_weight_counts_to_cycle_limit(counts, effective_cycle_limit); + + std::vector reduced_ids; + std::vector reduced_counts; + reduced_ids.reserve(filtered_ids.size()); + reduced_counts.reserve(counts.size()); + for (size_t i = 0; i < counts.size(); ++i) { + if (counts[i] <= 0) + continue; + reduced_ids.emplace_back(filtered_ids[i]); + reduced_counts.emplace_back(counts[i]); + } + if (reduced_ids.empty()) + return seq; + filtered_ids = std::move(reduced_ids); + counts = std::move(reduced_counts); + + const int total = std::accumulate(counts.begin(), counts.end(), 0); + if (total <= 0) + return seq; + + const size_t cycle = size_t(total); + + seq.reserve(cycle); + std::vector emitted(counts.size(), 0); + for (size_t pos = 0; pos < cycle; ++pos) { + size_t best_idx = 0; + double best_score = -1e9; + for (size_t i = 0; i < counts.size(); ++i) { + const double target = double(pos + 1) * double(counts[i]) / double(total); + const double score = target - double(emitted[i]); + if (score > best_score) { + best_score = score; + best_idx = i; + } + } + ++emitted[best_idx]; + seq.emplace_back(filtered_ids[best_idx]); + } + if (seq.empty()) + seq = filtered_ids; + return seq; +} + + +std::vector MixedFilamentConfigPanel::build_local_z_preview_pass_heights(double nominal_layer_height, + double lower_bound, + double upper_bound, + double preferred_a_height, + double preferred_b_height, + int mix_b_percent, + int max_sublayers_limit) +{ + if (nominal_layer_height <= EPSILON) + return {}; + + const double base_height = nominal_layer_height; + const double lo = std::max(0.01, lower_bound); + const double hi = std::max(lo, upper_bound); + const size_t max_passes_limit = max_sublayers_limit >= 2 ? size_t(max_sublayers_limit) : size_t(0); + + auto fit_pass_heights_to_interval = [](std::vector &passes, double total_height, double local_lo, double local_hi) { + if (passes.empty() || total_height <= EPSILON) + return false; + + const auto within = [local_lo, local_hi](double value) { + return value >= local_lo - 1e-6 && value <= local_hi + 1e-6; + }; + + double sum = 0.0; + for (const double h : passes) + sum += h; + + double delta = total_height - sum; + if (std::abs(delta) > 1e-6) { + if (delta > 0.0) { + for (double &h : passes) { + if (delta <= 1e-6) + break; + const double room = local_hi - h; + if (room <= 1e-6) + continue; + const double take = std::min(room, delta); + h += take; + delta -= take; + } + } else { + for (auto it = passes.rbegin(); it != passes.rend() && delta < -1e-6; ++it) { + const double room = *it - local_lo; + if (room <= 1e-6) + continue; + const double take = std::min(room, -delta); + *it -= take; + delta += take; + } + } + } + + if (std::abs(delta) > 1e-6) + return false; + return std::all_of(passes.begin(), passes.end(), within); + }; + + auto build_uniform = [&fit_pass_heights_to_interval, base_height, lo, hi, max_passes_limit]() { + std::vector out; + size_t min_passes = size_t(std::max(1.0, std::ceil((base_height - EPSILON) / hi))); + size_t max_passes = size_t(std::max(1.0, std::floor((base_height + EPSILON) / lo))); + size_t pass_count = min_passes; + + if (max_passes >= min_passes) { + const double target_step = 0.5 * (lo + hi); + const size_t target_passes = + size_t(std::max(1.0, std::llround(base_height / std::max(target_step, EPSILON)))); + pass_count = std::clamp(target_passes, min_passes, max_passes); + } + + if (max_passes_limit > 0 && pass_count > max_passes_limit) + pass_count = max_passes_limit; + + if (pass_count == 1 && base_height >= 2.0 * lo - EPSILON && max_passes >= 2) + pass_count = 2; + + if (pass_count <= 1) { + out.emplace_back(base_height); + return out; + } + + out.assign(pass_count, base_height / double(pass_count)); + double accumulated = 0.0; + for (size_t i = 0; i + 1 < out.size(); ++i) + accumulated += out[i]; + out.back() = std::max(EPSILON, base_height - accumulated); + if (!fit_pass_heights_to_interval(out, base_height, lo, hi) && max_passes_limit == 0) { + out.assign(pass_count, base_height / double(pass_count)); + accumulated = 0.0; + for (size_t i = 0; i + 1 < out.size(); ++i) + accumulated += out[i]; + out.back() = std::max(EPSILON, base_height - accumulated); + } + return out; + }; + + auto build_alternating = [&build_uniform, &fit_pass_heights_to_interval, base_height, lo, hi, max_passes_limit](double gradient_h_a, double gradient_h_b) { + if (base_height < 2.0 * lo - EPSILON) + return std::vector{ base_height }; + + const double cycle_h = std::max(EPSILON, gradient_h_a + gradient_h_b); + const double ratio_a = std::clamp(gradient_h_a / cycle_h, 0.0, 1.0); + + size_t min_passes = size_t(std::max(2.0, std::ceil((base_height - EPSILON) / hi))); + if ((min_passes % 2) != 0) + ++min_passes; + + size_t max_passes = size_t(std::max(2.0, std::floor((base_height + EPSILON) / lo))); + if ((max_passes % 2) != 0) + --max_passes; + if (max_passes_limit > 0) { + size_t capped_limit = std::max(2, max_passes_limit); + if ((capped_limit % 2) != 0) + --capped_limit; + if (capped_limit >= 2) + max_passes = std::min(max_passes, capped_limit); + } + if (max_passes < 2) + return build_uniform(); + if (min_passes > max_passes) + min_passes = max_passes; + if (min_passes < 2) + min_passes = 2; + if ((min_passes % 2) != 0) + ++min_passes; + if (min_passes > max_passes) + return build_uniform(); + + const double target_step = 0.5 * (lo + hi); + size_t target_passes = + size_t(std::max(2.0, std::llround(base_height / std::max(target_step, EPSILON)))); + if ((target_passes % 2) != 0) { + const size_t round_up = (target_passes < max_passes) ? (target_passes + 1) : max_passes; + const size_t round_down = (target_passes > min_passes) ? (target_passes - 1) : min_passes; + if (round_up > max_passes) + target_passes = round_down; + else if (round_down < min_passes) + target_passes = round_up; + else + target_passes = ((round_up - target_passes) <= (target_passes - round_down)) ? round_up : round_down; + } + target_passes = std::clamp(target_passes, min_passes, max_passes); + + bool has_best = false; + std::vector best_passes; + double best_ratio_error = 0.0; + size_t best_pass_distance = 0; + double best_max_height = 0.0; + size_t best_pass_count = 0; + + for (size_t pass_count = min_passes; pass_count <= max_passes; pass_count += 2) { + const size_t pair_count = pass_count / 2; + if (pair_count == 0) + continue; + const double pair_h = base_height / double(pair_count); + + const double h_a_min = std::max(lo, pair_h - hi); + const double h_a_max = std::min(hi, pair_h - lo); + if (h_a_min > h_a_max + EPSILON) + continue; + + const double h_a = std::clamp(pair_h * ratio_a, h_a_min, h_a_max); + const double h_b = pair_h - h_a; + + std::vector out; + out.reserve(pass_count); + for (size_t pair_idx = 0; pair_idx < pair_count; ++pair_idx) { + out.emplace_back(h_a); + out.emplace_back(h_b); + } + if (!fit_pass_heights_to_interval(out, base_height, lo, hi)) + continue; + + const double ratio_actual = (h_a + h_b > EPSILON) ? (h_a / (h_a + h_b)) : 0.5; + const double ratio_error = std::abs(ratio_actual - ratio_a); + const size_t pass_distance = + (pass_count > target_passes) ? (pass_count - target_passes) : (target_passes - pass_count); + const double max_height = std::max(h_a, h_b); + + const bool better_ratio = !has_best || (ratio_error + 1e-6 < best_ratio_error); + const bool similar_ratio = has_best && std::abs(ratio_error - best_ratio_error) <= 1e-6; + const bool better_distance = similar_ratio && (pass_distance < best_pass_distance); + const bool similar_distance = similar_ratio && (pass_distance == best_pass_distance); + const bool better_max_height = similar_distance && (max_height + 1e-6 < best_max_height); + const bool similar_max_height = similar_distance && std::abs(max_height - best_max_height) <= 1e-6; + const bool better_pass_count = similar_max_height && (pass_count > best_pass_count); + + if (better_ratio || better_distance || better_max_height || better_pass_count) { + has_best = true; + best_passes = std::move(out); + best_ratio_error = ratio_error; + best_pass_distance = pass_distance; + best_max_height = max_height; + best_pass_count = pass_count; + } + } + + return has_best ? best_passes : build_uniform(); + }; + + if (preferred_a_height > EPSILON || preferred_b_height > EPSILON) { + std::vector cadence_unit; + if (preferred_a_height > EPSILON) + cadence_unit.push_back(std::clamp(preferred_a_height, lo, hi)); + if (preferred_b_height > EPSILON) + cadence_unit.push_back(std::clamp(preferred_b_height, lo, hi)); + + if (!cadence_unit.empty()) { + std::vector out; + out.reserve(size_t(std::ceil(base_height / lo)) + 2); + + double z_used = 0.0; + size_t idx = 0; + size_t guard = 0; + while (z_used + cadence_unit[idx] < base_height - EPSILON && guard++ < 100000) { + out.push_back(cadence_unit[idx]); + z_used += cadence_unit[idx]; + idx = (idx + 1) % cadence_unit.size(); + } + + const double remainder = base_height - z_used; + if (remainder > EPSILON) + out.push_back(remainder); + + if (fit_pass_heights_to_interval(out, base_height, lo, hi) && + (max_passes_limit == 0 || out.size() <= max_passes_limit)) + return out; + } + + if (preferred_a_height > EPSILON && preferred_b_height > EPSILON) + return build_alternating(preferred_a_height, preferred_b_height); + return build_uniform(); + } + + const int mix_b = std::clamp(mix_b_percent, 0, 100); + const double pct_b = double(mix_b) / 100.0; + const double pct_a = 1.0 - pct_b; + const double gradient_h_a = lo + pct_a * (hi - lo); + const double gradient_h_b = lo + pct_b * (hi - lo); + return build_alternating(gradient_h_a, gradient_h_b); +} + +int MixedFilamentConfigPanel::effective_local_z_preview_mix_b_percent(const MixedFilament &mf, + const MixedFilamentPreviewSettings &preview_settings) +{ + return Slic3r::mixed_filament_effective_local_z_preview_mix_b_percent(mf, preview_settings); +} + +static double mixed_filament_reference_nozzle_mm(unsigned int component_a, + unsigned int component_b, + const std::vector &nozzle_diameters) +{ + std::vector samples; + samples.reserve(2); + + auto append_if_valid = [&samples, &nozzle_diameters](unsigned int component_id) { + if (component_id >= 1 && component_id <= nozzle_diameters.size()) + samples.emplace_back(std::max(0.05, nozzle_diameters[size_t(component_id - 1)])); + }; + + append_if_valid(component_a); + append_if_valid(component_b); + + if (samples.empty()) + return 0.4; + return std::accumulate(samples.begin(), samples.end(), 0.0) / double(samples.size()); +} + +static double mixed_filament_bias_limit_mm(const MixedFilament &mf, const std::vector &nozzle_diameters) +{ + const double reference_nozzle_mm = mixed_filament_reference_nozzle_mm(mf.component_a, mf.component_b, nozzle_diameters); + return MixedFilamentManager::max_pair_bias_mm(float(reference_nozzle_mm)); +} + +static float mixed_filament_single_surface_offset_value(const MixedFilament &mf, + const std::vector &nozzle_diameters) +{ + const double reference_nozzle_mm = mixed_filament_reference_nozzle_mm(mf.component_a, mf.component_b, nozzle_diameters); + return MixedFilamentManager::bias_ui_value_from_surface_offsets( + mf.component_a_surface_offset, + mf.component_b_surface_offset, + float(reference_nozzle_mm)); +} + +static std::pair mixed_filament_single_surface_offset_pair(const MixedFilament &mf, + float value, + const std::vector &nozzle_diameters) +{ + const double reference_nozzle_mm = mixed_filament_reference_nozzle_mm(mf.component_a, mf.component_b, nozzle_diameters); + return MixedFilamentManager::surface_offset_pair_from_signed_bias(value, float(reference_nozzle_mm)); +} + +static std::string mixed_filament_apparent_pair_summary(const MixedFilament &mf, + const MixedFilamentPreviewSettings &preview_settings, + const std::vector &nozzle_diameters, + bool bias_mode_enabled) +{ + if (!Slic3r::mixed_filament_supports_bias_apparent_color(mf, preview_settings, bias_mode_enabled)) + return {}; + + const int base_b = MixedFilamentConfigPanel::effective_local_z_preview_mix_b_percent(mf, preview_settings); + const int base_a = 100 - base_b; + const auto [apparent_a, apparent_b] = + Slic3r::mixed_filament_apparent_pair_percentages(mf, preview_settings, nozzle_diameters, bias_mode_enabled); + + if (std::abs(mf.component_a_surface_offset - mf.component_b_surface_offset) > 1e-4f && + (apparent_a != base_a || apparent_b != base_b)) { + std::ostringstream ss; + ss << '~' << apparent_a << '/' << apparent_b; + return ss.str(); + } + + std::ostringstream ss; + ss << apparent_a << "%/" << apparent_b << '%'; + return ss.str(); +} + + +static std::vector build_display_weighted_multi_sequence(const std::vector &ids, + const std::vector &weights, + size_t max_cycle_limit = 0) +{ + if (ids.empty()) + return {}; + + std::vector filtered_ids; + std::vector counts; + filtered_ids.reserve(ids.size()); + counts.reserve(ids.size()); + + const std::vector normalized = normalize_color_match_weights(weights, ids.size()); + for (size_t idx = 0; idx < ids.size(); ++idx) { + const int weight = idx < normalized.size() ? std::max(0, normalized[idx]) : 0; + if (weight <= 0) + continue; + filtered_ids.emplace_back(ids[idx]); + counts.emplace_back(weight); + } + if (filtered_ids.empty()) { + filtered_ids = ids; + counts.assign(ids.size(), 1); + } + + int g = 0; + for (const int count : counts) + g = std::gcd(g, std::max(1, count)); + if (g > 1) { + for (int &count : counts) + count = std::max(1, count / g); + } + + constexpr size_t k_max_cycle = 48; + const size_t effective_cycle_limit = + max_cycle_limit > 0 ? std::min(k_max_cycle, std::max(1, max_cycle_limit)) : k_max_cycle; + reduce_weight_counts_to_cycle_limit(counts, effective_cycle_limit); + + std::vector reduced_ids; + std::vector reduced_counts; + reduced_ids.reserve(filtered_ids.size()); + reduced_counts.reserve(counts.size()); + for (size_t idx = 0; idx < counts.size(); ++idx) { + if (counts[idx] <= 0) + continue; + reduced_ids.emplace_back(filtered_ids[idx]); + reduced_counts.emplace_back(counts[idx]); + } + if (reduced_ids.empty()) + return {}; + filtered_ids = std::move(reduced_ids); + counts = std::move(reduced_counts); + + const int total = std::accumulate(counts.begin(), counts.end(), 0); + if (total <= 0) + return std::vector(filtered_ids.begin(), filtered_ids.end()); + + const size_t cycle = size_t(total); + + std::vector sequence; + sequence.reserve(cycle); + std::vector emitted(counts.size(), 0); + for (size_t pos = 0; pos < cycle; ++pos) { + size_t best_idx = 0; + double best_score = -1e9; + for (size_t idx = 0; idx < counts.size(); ++idx) { + const double target = double(pos + 1) * double(counts[idx]) / double(total); + const double score = target - double(emitted[idx]); + if (score > best_score) { + best_score = score; + best_idx = idx; + } + } + ++emitted[best_idx]; + sequence.emplace_back(filtered_ids[best_idx]); + } + if (sequence.empty()) + sequence = filtered_ids; + return sequence; +} + +static std::string blend_display_color_from_sequence(const std::vector &colors, + size_t num_physical, + const std::vector &sequence, + const std::string &fallback) +{ + if (colors.empty() || sequence.empty() || num_physical == 0) + return fallback; + + std::vector counts(num_physical + 1, size_t(0)); + size_t total = 0; + for (const unsigned int id : sequence) { + if (id == 0 || id > num_physical) + continue; + ++counts[id]; + ++total; + } + if (total == 0) + return fallback; + + unsigned int first_id = 0; + for (size_t id = 1; id <= num_physical; ++id) { + if (counts[id] > 0) { + first_id = unsigned(id); + break; + } + } + if (first_id == 0 || first_id > colors.size()) + return fallback; + + std::string blended = colors[first_id - 1]; + int accumulated = int(counts[first_id]); + for (size_t id = size_t(first_id + 1); id <= num_physical; ++id) { + if (counts[id] == 0 || id > colors.size()) + continue; + blended = MixedFilamentManager::blend_color(blended, colors[id - 1], accumulated, int(counts[id])); + accumulated += int(counts[id]); + } + + return blended; +} + + +std::string MixedFilamentConfigPanel::summarize_sequence(const std::vector &seq) +{ + if (seq.empty()) return ""; + std::unordered_map counts; + for (unsigned int id : seq) counts[id]++; + std::vector> sorted; + for (auto &kv : counts) sorted.emplace_back(kv.second, kv.first); + std::sort(sorted.begin(), sorted.end(), std::greater<>()); + std::string out; + for (auto &p : sorted) { + if (!out.empty()) out += "/"; + out += format_preview_sequence_percent(p.first, int(seq.size())); + } + return out; +} + +std::string MixedFilamentConfigPanel::summarize_local_z_breakdown(const MixedFilament &mf, + const std::vector &weights, + const MixedFilamentPreviewSettings &preview_settings) +{ + const std::string normalized_pattern = MixedFilamentManager::normalize_manual_pattern(mf.manual_pattern); + if (!normalized_pattern.empty()) + return "Local-Z breakdown: manual pattern rows do not use pair decomposition."; + + if (mf.distribution_mode == int(MixedFilament::SameLayerPointillisme)) + return "Local-Z breakdown: same-layer mode does not use local-Z pair decomposition."; + + auto pair_name = [](unsigned int a, unsigned int b) { + std::ostringstream ss; + ss << 'F' << a << "+F" << b; + return ss.str(); + }; + auto pair_split = [](unsigned int a, unsigned int b, int weight_a, int weight_b) { + const int safe_a = std::max(0, weight_a); + const int safe_b = std::max(0, weight_b); + const int total = std::max(1, safe_a + safe_b); + const int pct_a = int(std::lround(100.0 * double(safe_a) / double(total))); + const int pct_b = std::max(0, 100 - pct_a); + + std::ostringstream ss; + ss << 'F' << a << "/F" << b << " " << safe_a << ':' << safe_b << " (" << pct_a << '/' << pct_b << ')'; + return ss.str(); + }; + auto cadence_entry = [&pair_name](unsigned int a, unsigned int b, int weight, int total) { + const int pct = int(std::lround(100.0 * double(std::max(0, weight)) / double(std::max(1, total)))); + std::ostringstream ss; + ss << pair_name(a, b) << ' ' << pct << '%'; + return ss.str(); + }; + + const std::vector ids = decode_gradient_ids(mf.gradient_component_ids); + if (preview_settings.local_z_mode && preview_settings.local_z_direct_multicolor && ids.size() >= 3) { + const std::vector normalized = normalize_gradient_weights(weights, ids.size()); + const size_t effective_sublayers = + mf.local_z_max_sublayers >= 2 ? size_t(std::max(2, mf.local_z_max_sublayers)) : ids.size(); + + std::ostringstream ss; + ss << "Local-Z direct multicolor solver: "; + for (size_t idx = 0; idx < ids.size(); ++idx) { + if (idx > 0) + ss << ", "; + const int pct = idx < normalized.size() ? normalized[idx] : 0; + ss << 'F' << ids[idx] << ' ' << pct << '%'; + } + ss << ".\nCarry-over error is distributed directly across all " << ids.size() + << " components instead of collapsing them into pair cadence."; + if (mf.local_z_max_sublayers >= 2) + ss << "\nEffective Local-Z cap: up to " << effective_sublayers << " sublayers per nominal layer."; + return ss.str(); + } + + if (ids.size() >= 4) { + const std::vector normalized = normalize_gradient_weights(weights, ids.size()); + const std::vector pair_tokens = { 1, 2 }; + const std::vector pair_weights = { + std::max(1, normalized[0] + normalized[1]), + std::max(1, normalized[2] + normalized[3]) + }; + const size_t max_pair_layers = + (preview_settings.local_z_mode && mf.local_z_max_sublayers >= 2) ? + std::max(1, size_t(mf.local_z_max_sublayers) / 2) : + size_t(0); + const std::vector uncapped_pair_sequence = build_weighted_multi_sequence(pair_tokens, pair_weights); + const std::vector effective_pair_sequence = + max_pair_layers > 0 ? build_weighted_multi_sequence(pair_tokens, pair_weights, max_pair_layers) : uncapped_pair_sequence; + const std::vector &pair_sequence = effective_pair_sequence.empty() ? uncapped_pair_sequence : effective_pair_sequence; + const int pair_ab_weight = int(std::count(pair_sequence.begin(), pair_sequence.end(), 1u)); + const int pair_cd_weight = int(std::count(pair_sequence.begin(), pair_sequence.end(), 2u)); + const int pair_total = std::max(1, int(pair_sequence.size())); + + std::ostringstream ss; + ss << "Local-Z layer cadence: " + << cadence_entry(ids[0], ids[1], pair_ab_weight, pair_total) + << ", " + << cadence_entry(ids[2], ids[3], pair_cd_weight, pair_total) + << ".\nPair splits: " + << pair_split(ids[0], ids[1], normalized[0], normalized[1]) + << ", " + << pair_split(ids[2], ids[3], normalized[2], normalized[3]) + << '.'; + if (!preview_settings.local_z_mode && mf.local_z_max_sublayers >= 2) + ss << "\nSaved row limit will apply when Local-Z dithering mode is enabled in print settings."; + if (preview_settings.local_z_mode && mf.local_z_max_sublayers >= 2) { + ss << "\nEffective Local-Z stack: " << (pair_total * 2) << " sublayers over " << pair_total << " pair layers"; + if (uncapped_pair_sequence.size() > pair_sequence.size()) + ss << " (uncapped " << (uncapped_pair_sequence.size() * 2) << ')'; + ss << '.'; + } + return ss.str(); + } + + if (ids.size() == 3) { + const std::vector normalized = normalize_gradient_weights(weights, ids.size()); + const std::vector pair_tokens = { 1, 2, 3 }; + const std::vector pair_weights = { + std::max(1, normalized[0] + normalized[1]), + std::max(1, normalized[0] + normalized[2]), + std::max(1, normalized[1] + normalized[2]) + }; + const size_t max_pair_layers = + (preview_settings.local_z_mode && mf.local_z_max_sublayers >= 2) ? + std::max(1, size_t(mf.local_z_max_sublayers) / 2) : + size_t(0); + const std::vector uncapped_pair_sequence = build_weighted_multi_sequence(pair_tokens, pair_weights); + const std::vector effective_pair_sequence = + max_pair_layers > 0 ? build_weighted_multi_sequence(pair_tokens, pair_weights, max_pair_layers) : uncapped_pair_sequence; + const std::vector &pair_sequence = effective_pair_sequence.empty() ? uncapped_pair_sequence : effective_pair_sequence; + const int pair_ab_weight = int(std::count(pair_sequence.begin(), pair_sequence.end(), 1u)); + const int pair_ac_weight = int(std::count(pair_sequence.begin(), pair_sequence.end(), 2u)); + const int pair_bc_weight = int(std::count(pair_sequence.begin(), pair_sequence.end(), 3u)); + const int pair_total = std::max(1, int(pair_sequence.size())); + + std::ostringstream ss; + ss << "Local-Z layer cadence: " + << cadence_entry(ids[0], ids[1], pair_ab_weight, pair_total) + << ", " + << cadence_entry(ids[0], ids[2], pair_ac_weight, pair_total) + << ", " + << cadence_entry(ids[1], ids[2], pair_bc_weight, pair_total) + << ".\nPair splits: " + << pair_split(ids[0], ids[1], normalized[0], normalized[1]) + << ", " + << pair_split(ids[0], ids[2], normalized[0], normalized[2]) + << ", " + << pair_split(ids[1], ids[2], normalized[1], normalized[2]) + << '.'; + if (!preview_settings.local_z_mode && mf.local_z_max_sublayers >= 2) + ss << "\nSaved row limit will apply when Local-Z dithering mode is enabled in print settings."; + if (preview_settings.local_z_mode && mf.local_z_max_sublayers >= 2) { + ss << "\nEffective Local-Z stack: " << (pair_total * 2) << " sublayers over " << pair_total << " pair layers"; + if (uncapped_pair_sequence.size() > pair_sequence.size()) + ss << " (uncapped " << (uncapped_pair_sequence.size() * 2) << ')'; + ss << '.'; + } + return ss.str(); + } + + if (mf.component_a >= 1 && mf.component_b >= 1 && mf.component_a != mf.component_b) { + const int pct_b = std::clamp(mf.mix_b_percent, 0, 100); + const int pct_a = 100 - pct_b; + std::ostringstream ss; + ss << "Local-Z pair split: requested F" << mf.component_a << "/F" << mf.component_b + << ' ' << pct_a << '/' << pct_b; + if (preview_settings.local_z_mode) { + const std::vector effective_passes = build_local_z_preview_pass_heights(preview_settings.nominal_layer_height, + preview_settings.mixed_lower_bound, + preview_settings.mixed_upper_bound, + preview_settings.preferred_a_height, + preview_settings.preferred_b_height, + mf.mix_b_percent, + 0); + if (!effective_passes.empty()) { + const int effective_pct_b = effective_local_z_preview_mix_b_percent(mf, preview_settings); + ss << ", effective " << (100 - effective_pct_b) << '/' << effective_pct_b + << " over " << effective_passes.size() << " sublayers"; + } + } + ss << '.'; + return ss.str(); + } + + return "Local-Z breakdown: unavailable."; +} + +std::string MixedFilamentConfigPanel::blend_from_sequence(const std::vector &colors, const std::vector &seq, const std::string &fallback) +{ + if (colors.empty() || seq.empty()) + return fallback; + + std::vector counts(colors.size() + 1, size_t(0)); + size_t total = 0; + for (const unsigned int id : seq) { + if (id == 0 || id > colors.size()) + continue; + ++counts[id]; + ++total; + } + if (total == 0) + return fallback; + + unsigned int first_id = 0; + for (size_t id = 1; id <= colors.size(); ++id) { + if (counts[id] > 0) { + first_id = unsigned(id); + break; + } + } + if (first_id == 0 || first_id > colors.size()) + return fallback; + + std::string blended = colors[first_id - 1]; + int acc = int(counts[first_id]); + for (size_t id = size_t(first_id + 1); id <= colors.size(); ++id) { + if (counts[id] == 0) + continue; + blended = MixedFilamentManager::blend_color(blended, colors[id - 1], acc, int(counts[id])); + acc += int(counts[id]); + } + + return blended; +} + +MixedFilamentConfigPanel::MixedFilamentConfigPanel(wxWindow *parent, + size_t mixed_id, + const MixedFilament &mf, + size_t num_physical, + const std::vector &physical_colors, + const std::vector &nozzle_diameters, + const std::vector &palette, + const MixedFilamentPreviewSettings &preview_settings, + bool bias_mode_enabled, + OnChangeFn on_change) + : wxPanel(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL | wxBORDER_NONE) + , m_mixed_id(mixed_id) + , m_mf(mf) + , m_num_physical(num_physical) + , m_physical_colors(physical_colors) + , m_nozzle_diameters(nozzle_diameters) + , m_palette(palette) + , m_preview_settings(preview_settings) + , m_bias_mode_enabled(bias_mode_enabled) + , m_selected_weight_state(std::make_shared>()) + , m_on_change(on_change) +{ + if (parent) + SetBackgroundColour(parent->GetBackgroundColour()); + else + SetBackgroundColour(wxGetApp().dark_mode() ? wxColour(52, 52, 56) : wxColour(255, 255, 255)); + build_ui(); +} + +void MixedFilamentConfigPanel::build_ui() +{ + const int gap = FromDIP(6); + const int compact_gap = std::max(FromDIP(2), gap / 3); + const bool is_dark = wxGetApp().dark_mode(); + const wxColour panel_bg = GetBackgroundColour().IsOk() ? GetBackgroundColour() : + (is_dark ? wxColour(52, 52, 56) : wxColour(255, 255, 255)); + SetBackgroundColour(panel_bg); + auto *root = new wxBoxSizer(wxVERTICAL); + + // Filament choices + wxArrayString filament_choices; + for (size_t i = 0; i < m_num_physical; ++i) + filament_choices.Add(wxString::Format("F%d", int(i + 1))); + wxArrayString optional_filament_choices; + optional_filament_choices.Add(_L("None")); + for (size_t i = 0; i < m_num_physical; ++i) + optional_filament_choices.Add(wxString::Format("F%d", int(i + 1))); + + const int component_a = std::clamp(int(m_mf.component_a), 1, int(m_num_physical)); + const int component_b = std::clamp(int(m_mf.component_b), 1, int(m_num_physical)); + + const std::vector initial_gradient_ids = decode_gradient_ids(m_mf.gradient_component_ids); + if (m_mf.distribution_mode == int(MixedFilament::SameLayerPointillisme)) { + m_mf.distribution_mode = initial_gradient_ids.size() >= 3 ? int(MixedFilament::LayerCycle) : int(MixedFilament::Simple); + m_mf.pointillism_all_filaments = false; + } + const int stored_distribution_mode = std::clamp(m_mf.distribution_mode, + int(MixedFilament::LayerCycle), + int(MixedFilament::Simple)); + const int row_distribution_mode = initial_gradient_ids.size() >= 3 ? + (stored_distribution_mode == int(MixedFilament::Simple) ? int(MixedFilament::LayerCycle) : stored_distribution_mode) : + int(MixedFilament::Simple); + m_mf.distribution_mode = row_distribution_mode; + const bool multi_gradient_row = row_distribution_mode != int(MixedFilament::Simple) && initial_gradient_ids.size() >= 3; + const int selection_c = initial_gradient_ids.size() >= 3 ? int(initial_gradient_ids[2]) : 0; + const int selection_d = initial_gradient_ids.size() >= 4 ? int(initial_gradient_ids[3]) : 0; + + // Hidden data controls used as backing state for swatch pickers. + m_choice_a = new wxChoice(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, filament_choices); + m_choice_b = new wxChoice(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, filament_choices); + m_choice_a->SetSelection(component_a - 1); + m_choice_b->SetSelection(component_b - 1); + m_choice_a->Hide(); + m_choice_b->Hide(); + if (multi_gradient_row) { + m_choice_c = new wxChoice(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, optional_filament_choices); + m_choice_c->SetSelection(std::clamp(selection_c, 0, int(m_num_physical))); + m_choice_c->Hide(); + if (initial_gradient_ids.size() >= 4) { + m_choice_d = new wxChoice(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, optional_filament_choices); + m_choice_d->SetSelection(std::clamp(selection_d, 0, int(m_num_physical))); + m_choice_d->Hide(); + } + } + + auto create_component_picker = [this, gap](wxPanel *&container_out, wxPanel *&swatch_out, wxStaticText *&label_out, const wxString &tooltip) { + const int inner_gap = std::max(FromDIP(1), gap / 4); + const bool local_is_dark = wxGetApp().dark_mode(); + const wxColour local_picker_bg = local_is_dark ? wxColour(64, 64, 70) : wxColour(255, 255, 255); + const wxColour local_picker_text = local_is_dark ? wxColour(230, 230, 230) : wxColour(32, 32, 32); + container_out = new wxPanel(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxBORDER_SIMPLE); + container_out->SetBackgroundColour(local_picker_bg); + const wxSize picker_size(FromDIP(38), FromDIP(22)); + container_out->SetMinSize(picker_size); + container_out->SetMaxSize(picker_size); + + auto *container_sizer = new wxBoxSizer(wxHORIZONTAL); + swatch_out = new wxPanel(container_out, wxID_ANY, wxDefaultPosition, wxSize(FromDIP(12), FromDIP(12)), wxBORDER_SIMPLE); + swatch_out->SetMinSize(wxSize(FromDIP(12), FromDIP(12))); + swatch_out->SetToolTip(tooltip); + label_out = new wxStaticText(container_out, wxID_ANY, wxEmptyString); + label_out->SetForegroundColour(local_picker_text); + label_out->SetToolTip(tooltip); + + auto *content_sizer = new wxBoxSizer(wxHORIZONTAL); + content_sizer->Add(swatch_out, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, inner_gap); + content_sizer->Add(label_out, 0, wxALIGN_CENTER_VERTICAL); + container_sizer->AddStretchSpacer(1); + container_sizer->Add(content_sizer, 0, wxALIGN_CENTER_VERTICAL); + container_sizer->AddStretchSpacer(1); + container_out->SetSizer(container_sizer); + container_out->SetToolTip(tooltip); + container_out->SetCursor(wxCursor(wxCURSOR_HAND)); + swatch_out->SetCursor(wxCursor(wxCURSOR_HAND)); + label_out->SetCursor(wxCursor(wxCURSOR_HAND)); + }; + + create_component_picker(m_picker_a_container, m_picker_a_swatch, m_picker_a_label, _L("Click to choose a physical filament color")); + create_component_picker(m_picker_b_container, m_picker_b_swatch, m_picker_b_label, _L("Click to choose a physical filament color")); + if (m_choice_c) + create_component_picker(m_picker_c_container, m_picker_c_swatch, m_picker_c_label, _L("Click to choose a physical filament color")); + if (m_choice_d) + create_component_picker(m_picker_d_container, m_picker_d_swatch, m_picker_d_label, _L("Click to choose a physical filament color")); + update_component_picker_visuals(); + + // Check for pattern mode + const std::string normalized_pattern = MixedFilamentManager::normalize_manual_pattern(m_mf.manual_pattern); + const bool pattern_row_mode = !normalized_pattern.empty(); + + auto *picker_row = new wxBoxSizer(wxHORIZONTAL); + if (!pattern_row_mode) { + auto add_picker = [this, picker_row, gap](wxPanel *container, bool &first_picker) { + if (!container) + return; + if (!first_picker) + picker_row->Add(new wxStaticText(this, wxID_ANY, "+"), 0, wxALIGN_CENTER_VERTICAL | wxLEFT | wxRIGHT, std::max(FromDIP(2), gap / 2)); + picker_row->Add(container, 0, wxALIGN_CENTER_VERTICAL); + first_picker = false; + }; + + bool first_picker = true; + add_picker(m_picker_a_container, first_picker); + add_picker(m_picker_b_container, first_picker); + add_picker(m_picker_c_container, first_picker); + add_picker(m_picker_d_container, first_picker); + } else { + if (m_picker_a_container) m_picker_a_container->Hide(); + if (m_picker_b_container) m_picker_b_container->Hide(); + if (m_picker_c_container) m_picker_c_container->Hide(); + if (m_picker_d_container) m_picker_d_container->Hide(); + } + root->Add(picker_row, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, gap); + + // Pattern controls (if pattern mode) + if (pattern_row_mode) { + auto *pattern_row = new wxBoxSizer(wxHORIZONTAL); + auto *pattern_label = new wxStaticText(this, wxID_ANY, _L("Pattern")); + pattern_label->SetForegroundColour(is_dark ? wxColour(236, 236, 236) : wxColour(20, 20, 20)); + pattern_row->Add(pattern_label, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, gap); + m_pattern_ctrl = new wxTextCtrl(this, wxID_ANY, from_u8(normalized_pattern), wxDefaultPosition, + wxSize(FromDIP(200), -1), wxTE_PROCESS_ENTER); + m_pattern_ctrl->SetToolTip(_L("Manual repeating pattern. Digits 1-9 for filament IDs 1-9. " + "Use [N] for IDs >= 10 (e.g. [12]). " + "Use commas to define per-perimeter groups, e.g. 12,21. " + "Example: 11112222, 12,21, or 1234.")); + pattern_row->Add(m_pattern_ctrl, 1, wxALIGN_CENTER_VERTICAL); + root->Add(pattern_row, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, gap); + + auto *quick_buttons = new wxBoxSizer(wxHORIZONTAL); + for (size_t fid = 0; fid < m_num_physical; ++fid) { + wxButton *btn = new wxButton(this, wxID_ANY, wxString::Format("%d", int(fid + 1)), + wxDefaultPosition, wxSize(FromDIP(24), FromDIP(22)), wxBU_EXACTFIT); + const wxColour chip_color = (fid < m_palette.size()) ? m_palette[fid] : wxColour("#26A69A"); + btn->SetBackgroundColour(chip_color); + btn->SetToolTip(wxString::Format(_L("Append filament %d to pattern"), int(fid + 1))); + quick_buttons->Add(btn, 0, wxRIGHT, FromDIP(4)); + m_pattern_quick_buttons.emplace_back(btn); + } + auto *filaments_label = new wxStaticText(this, wxID_ANY, _L("Filaments")); + filaments_label->SetForegroundColour(is_dark ? wxColour(236, 236, 236) : wxColour(20, 20, 20)); + picker_row->Add(filaments_label, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, std::max(FromDIP(3), gap / 2)); + picker_row->Add(quick_buttons, 0, wxALIGN_CENTER_VERTICAL); + } else { + // Blend selector for non-pattern mode + const bool simple_mode = row_distribution_mode == int(MixedFilament::Simple); + std::vector selected_gradient_ids = simple_mode ? std::vector() : initial_gradient_ids; + if (selected_gradient_ids.size() < 3) selected_gradient_ids.clear(); + if (selected_gradient_ids.empty()) { + selected_gradient_ids.emplace_back(unsigned(component_a)); + if (component_b != component_a) selected_gradient_ids.emplace_back(unsigned(component_b)); + } + const bool multi_gradient_mode = selected_gradient_ids.size() >= 3; + *m_selected_weight_state = normalize_gradient_weights( + decode_gradient_weights(m_mf.gradient_component_weights, selected_gradient_ids.size()), + selected_gradient_ids.size()); + + wxColour color_a = (component_a >= 1 && component_a <= int(m_palette.size())) ? m_palette[component_a - 1] : wxColour("#26A69A"); + wxColour color_b = (component_b >= 1 && component_b <= int(m_palette.size())) ? m_palette[component_b - 1] : wxColour("#26A69A"); + m_blend_selector = new MixedGradientSelector(this, color_a, color_b, std::clamp(m_mf.mix_b_percent, 0, 100)); + m_blend_selector->SetBackgroundColour(panel_bg); + m_blend_label = nullptr; + picker_row->AddSpacer(gap); + picker_row->Add(m_blend_selector, 1, wxEXPAND | wxALIGN_CENTER_VERTICAL | wxLEFT, gap); + + if (m_blend_selector) { + std::vector corner_colors; + corner_colors.reserve(selected_gradient_ids.size()); + for (const unsigned int id : selected_gradient_ids) { + if (id >= 1 && id <= m_palette.size()) + corner_colors.emplace_back(m_palette[id - 1]); + } + if (!simple_mode && corner_colors.size() >= 3) + m_blend_selector->set_multi_preview(corner_colors, *m_selected_weight_state); + } + } + + // Preview + auto *preview_row = new wxBoxSizer(wxHORIZONTAL); + m_mix_preview = new MixedMixPreview(this); + m_mix_preview->SetBackgroundColour(panel_bg); + preview_row->Add(m_mix_preview, 1, wxEXPAND | wxALIGN_CENTER_VERTICAL | wxRIGHT, compact_gap); + + auto *bias_controls = new wxBoxSizer(wxHORIZONTAL); + const float initial_surface_offset_value = mixed_filament_single_surface_offset_value(m_mf, m_nozzle_diameters); + const double initial_bias_limit = mixed_filament_bias_limit_mm(m_mf, m_nozzle_diameters); + const wxString bias_tooltip = + _L("Positive bias recesses the second filament in the pair; negative bias recesses the first filament.\n\n" + "The color chip shows which filament the current value affects.\n\n" + "Grouped wall patterns and Local-Z dithering ignore it."); + + auto *surface_offset_label = new wxStaticText(this, wxID_ANY, _L("Bias")); + surface_offset_label->SetForegroundColour(is_dark ? wxColour(236, 236, 236) : wxColour(20, 20, 20)); + surface_offset_label->SetToolTip(bias_tooltip); + bias_controls->Add(surface_offset_label, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, compact_gap); + + create_component_picker(m_surface_offset_target_container, + m_surface_offset_target_swatch, + m_surface_offset_target_label, + bias_tooltip); + if (m_surface_offset_target_container) + m_surface_offset_target_container->SetCursor(wxCursor(wxCURSOR_ARROW)); + if (m_surface_offset_target_swatch) + m_surface_offset_target_swatch->SetCursor(wxCursor(wxCURSOR_ARROW)); + if (m_surface_offset_target_label) + m_surface_offset_target_label->SetCursor(wxCursor(wxCURSOR_ARROW)); + bias_controls->Add(m_surface_offset_target_container, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, compact_gap); + + m_surface_offset_spin = new wxSpinCtrlDouble(this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize(FromDIP(58), -1), + wxSP_ARROW_KEYS | wxALIGN_RIGHT | wxTE_PROCESS_ENTER, + -initial_bias_limit, initial_bias_limit, + std::clamp(double(initial_surface_offset_value), -initial_bias_limit, initial_bias_limit), 0.001); + m_surface_offset_spin->SetDigits(3); + m_surface_offset_spin->SetToolTip(bias_tooltip); + bias_controls->Add(m_surface_offset_spin, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, compact_gap); + + auto *surface_offset_units = new wxStaticText(this, wxID_ANY, _L("mm")); + surface_offset_units->SetForegroundColour(is_dark ? wxColour(210, 210, 210) : wxColour(72, 72, 72)); + surface_offset_units->SetToolTip(bias_tooltip); + bias_controls->Add(surface_offset_units, 0, wxALIGN_CENTER_VERTICAL); + if (m_bias_mode_enabled) + preview_row->Add(bias_controls, 0, wxALIGN_CENTER_VERTICAL); + else { + surface_offset_label->Hide(); + if (m_surface_offset_target_container) + m_surface_offset_target_container->Hide(); + if (m_surface_offset_spin) + m_surface_offset_spin->Hide(); + surface_offset_units->Hide(); + } + root->Add(preview_row, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, gap); + + if (m_bias_mode_enabled) { + const auto initial_surface_offset_pair = + mixed_filament_single_surface_offset_pair(m_mf, initial_surface_offset_value, m_nozzle_diameters); + m_mf.component_a_surface_offset = initial_surface_offset_pair.first; + m_mf.component_b_surface_offset = initial_surface_offset_pair.second; + } + + const bool initial_component_surface_offsets_supported = m_bias_mode_enabled && + !pattern_row_mode && + row_distribution_mode != int(MixedFilament::SameLayerPointillisme) && + !m_preview_settings.local_z_mode; + if (m_surface_offset_spin) + m_surface_offset_spin->Enable(initial_component_surface_offsets_supported); + + const bool local_z_limit_supported = multi_gradient_row && + row_distribution_mode != int(MixedFilament::SameLayerPointillisme); + if (local_z_limit_supported) { + auto *local_z_limit_row = new wxBoxSizer(wxHORIZONTAL); + m_local_z_limit_checkbox = new wxCheckBox(this, wxID_ANY, _L("Limit Local-Z")); + m_local_z_limit_checkbox->SetValue(m_mf.local_z_max_sublayers >= 2); + m_local_z_limit_checkbox->SetForegroundColour(is_dark ? wxColour(236, 236, 236) : wxColour(20, 20, 20)); + m_local_z_limit_checkbox->SetToolTip( + _L("Store a per-color Local-Z cadence cap. It applies when Local-Z dithering mode is enabled in print settings.")); + local_z_limit_row->Add(m_local_z_limit_checkbox, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, gap); + + auto *local_z_limit_label = new wxStaticText(this, wxID_ANY, _L("Max sublayers")); + local_z_limit_label->SetForegroundColour(is_dark ? wxColour(236, 236, 236) : wxColour(20, 20, 20)); + local_z_limit_row->Add(local_z_limit_label, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, std::max(FromDIP(3), gap / 2)); + + const int initial_local_z_limit = std::max(2, m_mf.local_z_max_sublayers > 0 ? m_mf.local_z_max_sublayers : 6); + m_local_z_limit_spin = new wxSpinCtrl(this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize(FromDIP(72), -1), + wxSP_ARROW_KEYS | wxALIGN_RIGHT | wxTE_PROCESS_ENTER, 2, 999, initial_local_z_limit); + m_local_z_limit_spin->SetToolTip( + _L("Maximum number of Local-Z sublayers this color may use before its cadence repeats.")); + local_z_limit_row->Add(m_local_z_limit_spin, 0, wxALIGN_CENTER_VERTICAL); + + const bool enable_local_z_limit_controls = m_local_z_limit_checkbox->GetValue(); + m_local_z_limit_spin->Enable(enable_local_z_limit_controls); + root->Add(local_z_limit_row, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, gap); + } + + m_breakdown_label = new wxStaticText(this, wxID_ANY, wxEmptyString); + m_breakdown_label->SetForegroundColour(is_dark ? wxColour(210, 210, 210) : wxColour(72, 72, 72)); + m_breakdown_label->Wrap(FromDIP(360)); + root->Add(m_breakdown_label, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, gap); + + // Bind events + auto apply_changes = [this]() { + m_has_changes = true; + + double surface_offset_value = 0.0; + if (m_surface_offset_spin) { + surface_offset_value = m_surface_offset_spin->GetValue(); +#if !defined(wxHAS_NATIVE_SPINCTRLDOUBLE) + if (wxTextCtrl *text = m_surface_offset_spin->GetText()) { + double parsed_value = 0.0; + if (text->GetValue().ToDouble(&parsed_value)) + surface_offset_value = parsed_value; + } +#endif + } + + int a = std::clamp(m_choice_a->GetSelection() + 1, 1, int(m_num_physical)); + int b = std::clamp(m_choice_b->GetSelection() + 1, 1, int(m_num_physical)); + if (a == b && m_num_physical > 1) { + b = (a == int(m_num_physical)) ? 1 : a + 1; + m_choice_b->SetSelection(b - 1); + } + update_component_picker_visuals(); + + if (m_local_z_limit_spin) + m_local_z_limit_spin->Enable(m_local_z_limit_checkbox != nullptr && + m_local_z_limit_checkbox->GetValue()); + + m_mf.component_a = unsigned(a); + m_mf.component_b = unsigned(b); + if (m_bias_mode_enabled) { + const double bias_limit = mixed_filament_bias_limit_mm(m_mf, m_nozzle_diameters); + const float clamped_surface_offset_value = std::clamp(float(surface_offset_value), -float(bias_limit), float(bias_limit)); + const auto surface_offset_pair = + mixed_filament_single_surface_offset_pair(m_mf, clamped_surface_offset_value, m_nozzle_diameters); + m_mf.component_a_surface_offset = surface_offset_pair.first; + m_mf.component_b_surface_offset = surface_offset_pair.second; + if (m_surface_offset_spin) + m_surface_offset_spin->SetValue(clamped_surface_offset_value); + } + m_mf.local_z_max_sublayers = + (m_local_z_limit_checkbox != nullptr && m_local_z_limit_checkbox->GetValue() && m_local_z_limit_spin != nullptr) ? + std::max(2, m_local_z_limit_spin->GetValue()) : + 0; + + bool simple_mode = true; + bool same_layer_mode = false; + int preview_mix_b_percent = std::clamp(m_mf.mix_b_percent, 0, 100); + std::vector preview_sequence; + + if (m_pattern_ctrl) { + m_mf.distribution_mode = int(MixedFilament::Simple); + std::string normalized = MixedFilamentManager::normalize_manual_pattern(into_u8(m_pattern_ctrl->GetValue())); + if (normalized.empty()) normalized = "12"; + if (into_u8(m_pattern_ctrl->GetValue()) != normalized) + m_pattern_ctrl->ChangeValue(from_u8(normalized)); + m_mf.manual_pattern = normalized; + m_mf.mix_b_percent = MixedFilamentManager::mix_percent_from_manual_pattern(normalized); + m_mf.pointillism_all_filaments = false; + m_mf.gradient_component_ids.clear(); + m_mf.gradient_component_weights.clear(); + preview_sequence = decode_manual_pattern_ids(m_mf.manual_pattern, + m_mf.component_a, + m_mf.component_b, + m_num_physical, + m_preview_settings.wall_loops); + } else { + std::vector selected_ids; + selected_ids.reserve(4); + auto add_unique = [&selected_ids](unsigned int id) { + if (id == 0) return; + if (std::find(selected_ids.begin(), selected_ids.end(), id) == selected_ids.end()) + selected_ids.emplace_back(id); + }; + add_unique(unsigned(a)); + add_unique(unsigned(b)); + if (m_choice_c && m_choice_c->GetSelection() > 0) + add_unique(unsigned(m_choice_c->GetSelection())); + if (m_choice_d && m_choice_d->GetSelection() > 0) + add_unique(unsigned(m_choice_d->GetSelection())); + const bool multi_gradient_mode = selected_ids.size() >= 3; + m_mf.distribution_mode = multi_gradient_mode ? int(MixedFilament::LayerCycle) : int(MixedFilament::Simple); + simple_mode = m_mf.distribution_mode == int(MixedFilament::Simple); + m_mf.mix_b_percent = std::clamp(m_blend_selector ? m_blend_selector->value() : 50, 0, 100); + m_mf.manual_pattern.clear(); + m_mf.pointillism_all_filaments = false; + + const wxColour color_a = (a >= 1 && a <= int(m_palette.size())) ? m_palette[size_t(a - 1)] : wxColour("#26A69A"); + const wxColour color_b = (b >= 1 && b <= int(m_palette.size())) ? m_palette[size_t(b - 1)] : wxColour("#26A69A"); + if (m_blend_selector) { + if (!simple_mode && multi_gradient_mode) { + std::vector corner_colors; + corner_colors.reserve(selected_ids.size()); + for (const unsigned int id : selected_ids) { + if (id >= 1 && id <= m_palette.size()) + corner_colors.emplace_back(m_palette[id - 1]); + } + if (corner_colors.size() >= 3) + m_blend_selector->set_multi_preview(corner_colors, *m_selected_weight_state); + else + m_blend_selector->set_colors(color_a, color_b); + } else { + m_blend_selector->set_colors(color_a, color_b); + } + } + + if (multi_gradient_mode) { + const std::vector decoded_weights = + decode_gradient_weights(m_mf.gradient_component_weights, selected_ids.size()); + if (m_selected_weight_state->size() != selected_ids.size()) + *m_selected_weight_state = decoded_weights; + *m_selected_weight_state = normalize_gradient_weights(*m_selected_weight_state, selected_ids.size()); + m_mf.gradient_component_ids = encode_gradient_ids(selected_ids); + m_mf.gradient_component_weights = encode_gradient_weights(*m_selected_weight_state); + preview_sequence = build_weighted_multi_sequence(selected_ids, *m_selected_weight_state); + } else { + m_mf.gradient_component_ids.clear(); + m_mf.gradient_component_weights.clear(); + preview_mix_b_percent = effective_local_z_preview_mix_b_percent(m_mf, m_preview_settings); + preview_sequence = build_weighted_pair_sequence(m_mf.component_a, m_mf.component_b, preview_mix_b_percent, same_layer_mode); + } + } + m_mf.custom = true; + + const std::vector selected_gradient_ids = decode_gradient_ids(m_mf.gradient_component_ids); + const bool component_surface_offsets_supported = m_bias_mode_enabled && + (m_pattern_ctrl == nullptr) && + !same_layer_mode && + !m_preview_settings.local_z_mode; + if (m_surface_offset_spin) + m_surface_offset_spin->Enable(component_surface_offsets_supported); + if (preview_sequence.empty()) + preview_sequence = build_weighted_pair_sequence(m_mf.component_a, m_mf.component_b, preview_mix_b_percent, same_layer_mode); + + if (m_blend_selector && selected_gradient_ids.size() >= 3) { + std::vector corner_colors; + corner_colors.reserve(selected_gradient_ids.size()); + for (const unsigned int id : selected_gradient_ids) { + if (id >= 1 && id <= m_palette.size()) + corner_colors.emplace_back(m_palette[id - 1]); + } + if (corner_colors.size() >= 3) + m_blend_selector->set_multi_preview(corner_colors, *m_selected_weight_state); + } + + if (Slic3r::mixed_filament_supports_bias_apparent_color(m_mf, m_preview_settings, m_bias_mode_enabled) && + m_mf.component_a >= 1 && m_mf.component_b >= 1 && + m_mf.component_a <= m_physical_colors.size() && m_mf.component_b <= m_physical_colors.size()) { + const auto [apparent_pct_a, apparent_pct_b] = + Slic3r::mixed_filament_apparent_pair_percentages(m_mf, m_preview_settings, m_nozzle_diameters, m_bias_mode_enabled); + m_mf.display_color = MixedFilamentManager::blend_color( + m_physical_colors[size_t(m_mf.component_a - 1)], + m_physical_colors[size_t(m_mf.component_b - 1)], + apparent_pct_a, + apparent_pct_b); + } else if (selected_gradient_ids.size() >= 3 || !preview_sequence.empty()) { + m_mf.display_color = blend_from_sequence(m_physical_colors, preview_sequence, "#26A69A"); + if (m_blend_label) { + if (selected_gradient_ids.size() >= 3) { + m_blend_label->SetLabel(wxString::Format(_L("%d-color layer cycle"), int(selected_gradient_ids.size()))); + } else { + m_blend_label->SetLabel(wxString::Format(simple_mode ? _L("Simple %d%%/%d%%") : _L("%d%%/%d%%"), + 100 - preview_mix_b_percent, preview_mix_b_percent)); + } + } + } else { + m_mf.display_color = MixedFilamentManager::blend_color( + m_physical_colors[size_t(a - 1)], m_physical_colors[size_t(b - 1)], + 100 - preview_mix_b_percent, preview_mix_b_percent); + if (m_blend_label) + m_blend_label->SetLabel(wxString::Format(simple_mode ? _L("Simple %d%%/%d%%") : _L("%d%%/%d%%"), + 100 - preview_mix_b_percent, preview_mix_b_percent)); + } + + if (m_mix_preview) { + const std::string bias_summary = + mixed_filament_apparent_pair_summary(m_mf, m_preview_settings, m_nozzle_diameters, m_bias_mode_enabled); + const std::string summary = bias_summary.empty() ? summarize_sequence(preview_sequence) : bias_summary; + std::vector preview_surface_offsets(m_palette.size() + 1, 0.0); + if (m_bias_mode_enabled && m_mf.component_a >= 1 && m_mf.component_a < preview_surface_offsets.size()) + preview_surface_offsets[m_mf.component_a] = double(m_mf.component_a_surface_offset); + if (m_bias_mode_enabled && m_mf.component_b >= 1 && m_mf.component_b < preview_surface_offsets.size()) + preview_surface_offsets[m_mf.component_b] = double(m_mf.component_b_surface_offset); + m_mix_preview->set_data(m_palette, preview_sequence, same_layer_mode, preview_surface_offsets, wxColour(m_mf.display_color), + _L("Preview"), summary.empty() ? wxString() : from_u8(summary)); + } + update_local_z_breakdown(); + if (m_swatch) { + m_swatch->SetBackgroundColour(wxColour(m_mf.display_color)); + m_swatch->Refresh(); + } + if (m_on_change) + m_on_change(m_mf); + }; + + auto make_color_chip_bitmap = [this](const wxColour &color) { + const int chip_size = FromDIP(14); + wxBitmap bmp(chip_size, chip_size); + wxMemoryDC dc(bmp); + dc.SetBackground(wxBrush(wxColour(255, 255, 255))); + dc.Clear(); + dc.SetPen(wxPen(wxColour(120, 120, 120))); + dc.SetBrush(wxBrush(color)); + dc.DrawRectangle(0, 0, chip_size, chip_size); + dc.SelectObject(wxNullBitmap); + return bmp; + }; + + auto bind_component_picker_popup = [this, apply_changes, make_color_chip_bitmap](wxWindow *target, wxChoice *backing_choice) { + if (!target || !backing_choice) + return; + + target->Bind(wxEVT_LEFT_UP, [this, apply_changes, make_color_chip_bitmap, backing_choice](wxMouseEvent &) { + if (m_num_physical == 0) + return; + + const bool allow_none = backing_choice->GetCount() == unsigned(m_num_physical + 1); + wxMenu menu; + std::vector item_ids; + item_ids.reserve(m_num_physical + (allow_none ? 1 : 0)); + if (allow_none) { + const int item_id = wxWindow::NewControlId(); + item_ids.emplace_back(item_id); + menu.Append(item_id, backing_choice->GetSelection() == 0 ? _L("None (Selected)") : _L("None")); + } + for (size_t i = 0; i < m_num_physical; ++i) { + const int item_id = wxWindow::NewControlId(); + item_ids.emplace_back(item_id); + const int selection_index = allow_none ? int(i + 1) : int(i); + const bool is_selected = selection_index == backing_choice->GetSelection(); + const wxString item_label = wxString::Format("F%d%s", int(i + 1), is_selected ? " (Selected)" : ""); + auto *menu_item = new wxMenuItem(&menu, item_id, item_label, wxEmptyString, wxITEM_NORMAL); + const wxColour item_color = (i < m_palette.size()) ? m_palette[i] : wxColour("#26A69A"); + menu_item->SetBitmap(make_color_chip_bitmap(item_color)); + menu.Append(menu_item); + } + + menu.Bind(wxEVT_COMMAND_MENU_SELECTED, [apply_changes, backing_choice, item_ids](wxCommandEvent &evt) { + const auto it = std::find(item_ids.begin(), item_ids.end(), evt.GetId()); + if (it == item_ids.end()) + return; + const int selection = int(std::distance(item_ids.begin(), it)); + backing_choice->SetSelection(selection); + apply_changes(); + }); + PopupMenu(&menu); + }); + }; + + bind_component_picker_popup(m_picker_a_container, m_choice_a); + bind_component_picker_popup(m_picker_a_swatch, m_choice_a); + bind_component_picker_popup(m_picker_a_label, m_choice_a); + bind_component_picker_popup(m_picker_b_container, m_choice_b); + bind_component_picker_popup(m_picker_b_swatch, m_choice_b); + bind_component_picker_popup(m_picker_b_label, m_choice_b); + bind_component_picker_popup(m_picker_c_container, m_choice_c); + bind_component_picker_popup(m_picker_c_swatch, m_choice_c); + bind_component_picker_popup(m_picker_c_label, m_choice_c); + bind_component_picker_popup(m_picker_d_container, m_choice_d); + bind_component_picker_popup(m_picker_d_swatch, m_choice_d); + bind_component_picker_popup(m_picker_d_label, m_choice_d); + + m_choice_a->Bind(wxEVT_CHOICE, [apply_changes](wxCommandEvent&) { apply_changes(); }); + m_choice_b->Bind(wxEVT_CHOICE, [apply_changes](wxCommandEvent&) { apply_changes(); }); + if (m_choice_c) + m_choice_c->Bind(wxEVT_CHOICE, [apply_changes](wxCommandEvent&) { apply_changes(); }); + if (m_choice_d) + m_choice_d->Bind(wxEVT_CHOICE, [apply_changes](wxCommandEvent&) { apply_changes(); }); + if (m_blend_selector) + m_blend_selector->Bind(wxEVT_SLIDER, [apply_changes](wxCommandEvent&) { apply_changes(); }); + if (m_local_z_limit_checkbox) + m_local_z_limit_checkbox->Bind(wxEVT_CHECKBOX, [apply_changes](wxCommandEvent &) { apply_changes(); }); + if (m_local_z_limit_spin) { + m_local_z_limit_spin->Bind(wxEVT_SPINCTRL, [apply_changes](wxCommandEvent &) { apply_changes(); }); + m_local_z_limit_spin->Bind(wxEVT_TEXT_ENTER, [apply_changes](wxCommandEvent &) { apply_changes(); }); + m_local_z_limit_spin->Bind(wxEVT_KILL_FOCUS, [apply_changes](wxFocusEvent &evt) { + apply_changes(); + evt.Skip(); + }); + } + if (m_surface_offset_spin) { + m_surface_offset_spin->Bind(wxEVT_SPINCTRLDOUBLE, [apply_changes](wxSpinDoubleEvent &) { apply_changes(); }); + m_surface_offset_spin->Bind(wxEVT_TEXT_ENTER, [apply_changes](wxCommandEvent &) { apply_changes(); }); + m_surface_offset_spin->Bind(wxEVT_KILL_FOCUS, [apply_changes](wxFocusEvent &evt) { + apply_changes(); + evt.Skip(); + }); + } + + if (m_blend_selector) { + m_blend_selector->Bind(wxEVT_BUTTON, [this, apply_changes](wxCommandEvent&) { + if (!m_blend_selector->is_multi_mode()) return; + std::vector selected_ids; + auto add_unique = [&selected_ids](unsigned int id) { if (id > 0 && std::find(selected_ids.begin(), selected_ids.end(), id) == selected_ids.end()) selected_ids.emplace_back(id); }; + add_unique(unsigned(std::clamp(m_choice_a ? (m_choice_a->GetSelection() + 1) : 0, 1, int(m_num_physical)))); + add_unique(unsigned(std::clamp(m_choice_b ? (m_choice_b->GetSelection() + 1) : 0, 1, int(m_num_physical)))); + if (m_choice_c && m_choice_c->GetSelection() > 0) add_unique(unsigned(m_choice_c->GetSelection())); + if (m_choice_d && m_choice_d->GetSelection() > 0) add_unique(unsigned(m_choice_d->GetSelection())); + if (selected_ids.size() < 3) return; + const std::vector initial_weights = normalize_gradient_weights(*m_selected_weight_state, selected_ids.size()); + MixedGradientWeightsDialog dlg(this, selected_ids, m_palette, initial_weights); + if (dlg.ShowModal() != wxID_OK) return; + *m_selected_weight_state = dlg.normalized_weights(); + apply_changes(); + }); + } + + if (m_pattern_ctrl) { + auto append_pattern_token = [this](int filament_id) { + if (!m_pattern_ctrl || filament_id <= 0) return; + if (filament_id >= 10) + m_pattern_ctrl->AppendText(wxString::Format("[%d]", filament_id)); + else + m_pattern_ctrl->AppendText(wxString::Format("%d", filament_id)); + }; + m_pattern_ctrl->Bind(wxEVT_TEXT_ENTER, [apply_changes](wxCommandEvent&) { apply_changes(); }); + m_pattern_ctrl->Bind(wxEVT_KILL_FOCUS, [apply_changes](wxFocusEvent &evt) { apply_changes(); evt.Skip(); }); + for (size_t fid = 0; fid < m_pattern_quick_buttons.size(); ++fid) { + wxButton *btn = m_pattern_quick_buttons[fid]; + if (btn) { + const int filament_id = int(fid + 1); + btn->Bind(wxEVT_BUTTON, [apply_changes, append_pattern_token, filament_id](wxCommandEvent&) { + append_pattern_token(filament_id); + apply_changes(); + }); + } + } + } + + update_component_picker_visuals(); + SetSizer(root); + Layout(); + SetMinSize(wxSize(-1, GetBestSize().GetHeight())); + update_preview(); +} + +void MixedFilamentConfigPanel::update_component_picker_visuals() +{ + auto update_one = [this](wxChoice *choice, wxPanel *container, wxPanel *swatch, wxStaticText *label) { + if (!choice) + return; + int sel = choice->GetSelection(); + const bool allow_none = choice->GetCount() == unsigned(m_num_physical + 1); + if (sel < 0 && m_num_physical > 0) { + sel = 0; + choice->SetSelection(sel); + } + if (sel < 0) + return; + + if (allow_none && sel == 0) { + const wxColour none_color = wxGetApp().dark_mode() ? wxColour(86, 86, 92) : wxColour(224, 224, 224); + if (swatch) { + swatch->SetBackgroundColour(none_color); + swatch->Refresh(); + } + if (label) + label->SetLabel(_L("None")); + if (container) { + container->Layout(); + container->Refresh(); + } + return; + } + + const int color_idx = allow_none ? sel - 1 : sel; + const wxColour color = (color_idx >= 0 && size_t(color_idx) < m_palette.size()) ? m_palette[size_t(color_idx)] : wxColour("#26A69A"); + if (swatch) { + swatch->SetBackgroundColour(color); + swatch->Refresh(); + } + if (label) + label->SetLabel(wxString::Format("F%d", color_idx + 1)); + if (container) { + container->Layout(); + container->Refresh(); + } + }; + + update_one(m_choice_a, m_picker_a_container, m_picker_a_swatch, m_picker_a_label); + update_one(m_choice_b, m_picker_b_container, m_picker_b_swatch, m_picker_b_label); + update_one(m_choice_c, m_picker_c_container, m_picker_c_swatch, m_picker_c_label); + update_one(m_choice_d, m_picker_d_container, m_picker_d_swatch, m_picker_d_label); + + if (m_surface_offset_target_container || m_surface_offset_target_swatch || m_surface_offset_target_label || m_surface_offset_spin) { + const int a_filament = std::clamp(m_choice_a ? (m_choice_a->GetSelection() + 1) : int(m_mf.component_a), 1, int(std::max(1, m_num_physical))); + const int b_filament = std::clamp(m_choice_b ? (m_choice_b->GetSelection() + 1) : int(m_mf.component_b), 1, int(std::max(1, m_num_physical))); + MixedFilament active_pair = m_mf; + active_pair.component_a = unsigned(a_filament); + active_pair.component_b = unsigned(b_filament); + double signed_bias_value = mixed_filament_single_surface_offset_value(active_pair, m_nozzle_diameters); + + if (m_surface_offset_spin && m_bias_mode_enabled) { + const double bias_limit = mixed_filament_bias_limit_mm(active_pair, m_nozzle_diameters); + m_surface_offset_spin->SetRange(-bias_limit, bias_limit); + signed_bias_value = m_surface_offset_spin->GetValue(); + } + + const int active_filament = signed_bias_value < -EPSILON ? a_filament : b_filament; + const int color_idx = active_filament - 1; + const wxColour color = (color_idx >= 0 && size_t(color_idx) < m_palette.size()) ? m_palette[size_t(color_idx)] : wxColour("#26A69A"); + if (m_surface_offset_target_swatch) { + m_surface_offset_target_swatch->SetBackgroundColour(color); + m_surface_offset_target_swatch->Refresh(); + } + if (m_surface_offset_target_label) + m_surface_offset_target_label->SetLabel(wxString::Format("F%d", active_filament)); + if (m_surface_offset_target_container) { + m_surface_offset_target_container->Layout(); + m_surface_offset_target_container->Refresh(); + } + } +} + +void MixedFilamentConfigPanel::update_preview() +{ + const bool simple_mode = m_mf.distribution_mode == int(MixedFilament::Simple); + const bool same_layer_mode = m_mf.distribution_mode == int(MixedFilament::SameLayerPointillisme); + const std::string normalized_pattern = MixedFilamentManager::normalize_manual_pattern(m_mf.manual_pattern); + const bool pattern_row_mode = !normalized_pattern.empty(); + + std::vector initial_sequence; + if (pattern_row_mode) { + initial_sequence = decode_manual_pattern_ids(normalized_pattern, + m_mf.component_a, + m_mf.component_b, + m_num_physical, + m_preview_settings.wall_loops); + } else { + std::vector initial_gradient_ids = simple_mode ? std::vector() : decode_gradient_ids(m_mf.gradient_component_ids); + if (initial_gradient_ids.size() >= 3) + initial_sequence = build_weighted_multi_sequence(initial_gradient_ids, *m_selected_weight_state); + else + initial_sequence = build_weighted_pair_sequence(m_mf.component_a, + m_mf.component_b, + effective_local_z_preview_mix_b_percent(m_mf, m_preview_settings), + same_layer_mode); + + if (m_blend_selector && initial_gradient_ids.size() >= 3) { + std::vector corner_colors; + corner_colors.reserve(initial_gradient_ids.size()); + for (const unsigned int id : initial_gradient_ids) { + if (id >= 1 && id <= m_palette.size()) + corner_colors.emplace_back(m_palette[id - 1]); + } + if (corner_colors.size() >= 3) + m_blend_selector->set_multi_preview(corner_colors, *m_selected_weight_state); + } + } + + if (m_mix_preview) { + if (Slic3r::mixed_filament_supports_bias_apparent_color(m_mf, m_preview_settings, m_bias_mode_enabled) && + m_mf.component_a >= 1 && m_mf.component_b >= 1 && + m_mf.component_a <= m_physical_colors.size() && m_mf.component_b <= m_physical_colors.size()) { + const auto [apparent_pct_a, apparent_pct_b] = + Slic3r::mixed_filament_apparent_pair_percentages(m_mf, m_preview_settings, m_nozzle_diameters, m_bias_mode_enabled); + m_mf.display_color = MixedFilamentManager::blend_color( + m_physical_colors[size_t(m_mf.component_a - 1)], + m_physical_colors[size_t(m_mf.component_b - 1)], + apparent_pct_a, + apparent_pct_b); + } + + const std::string bias_summary = + mixed_filament_apparent_pair_summary(m_mf, m_preview_settings, m_nozzle_diameters, m_bias_mode_enabled); + const std::string summary = bias_summary.empty() ? summarize_sequence(initial_sequence) : bias_summary; + std::vector preview_surface_offsets(m_palette.size() + 1, 0.0); + if (m_bias_mode_enabled && m_mf.component_a >= 1 && m_mf.component_a < preview_surface_offsets.size()) + preview_surface_offsets[m_mf.component_a] = double(m_mf.component_a_surface_offset); + if (m_bias_mode_enabled && m_mf.component_b >= 1 && m_mf.component_b < preview_surface_offsets.size()) + preview_surface_offsets[m_mf.component_b] = double(m_mf.component_b_surface_offset); + m_mix_preview->set_data(m_palette, initial_sequence, same_layer_mode, preview_surface_offsets, wxColour(m_mf.display_color), + _L("Preview"), summary.empty() ? wxString() : from_u8(summary)); + } + update_local_z_breakdown(); +} + +void MixedFilamentConfigPanel::update_local_z_breakdown() +{ + if (!m_breakdown_label) + return; + + std::vector weights = *m_selected_weight_state; + const std::vector ids = decode_gradient_ids(m_mf.gradient_component_ids); + if (!ids.empty()) + weights = normalize_gradient_weights(weights, ids.size()); + + const std::string breakdown = summarize_local_z_breakdown(m_mf, weights, m_preview_settings); + m_breakdown_label->SetLabel(from_u8(breakdown)); + m_breakdown_label->Wrap(FromDIP(360)); + m_breakdown_label->Show(!breakdown.empty()); + Layout(); +} + +class MixedFilamentDragHandle : public wxPanel +{ +public: + MixedFilamentDragHandle(wxWindow *parent, const wxColour &dot_color, const wxColour &bg_color) + : wxPanel(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxBORDER_NONE) + , m_dot_color(dot_color) + { + const wxSize handle_size = parent ? parent->FromDIP(wxSize(14, 18)) : wxSize(14, 18); + SetMinSize(handle_size); + SetMaxSize(handle_size); + SetInitialSize(handle_size); + SetBackgroundStyle(wxBG_STYLE_PAINT); + SetBackgroundColour(bg_color); + SetCursor(wxCursor(wxCURSOR_SIZING)); + Bind(wxEVT_PAINT, &MixedFilamentDragHandle::on_paint, this); + } + + void set_colors(const wxColour &dot_color, const wxColour &bg_color) + { + m_dot_color = dot_color; + SetBackgroundColour(bg_color); + Refresh(); + } + +private: + void on_paint(wxPaintEvent &) + { + wxAutoBufferedPaintDC dc(this); + dc.SetBackground(wxBrush(GetBackgroundColour())); + dc.Clear(); + dc.SetPen(*wxTRANSPARENT_PEN); + dc.SetBrush(wxBrush(m_dot_color)); + + const wxSize size = GetClientSize(); + const int radius = std::max(1, FromDIP(1)); + const int left_x = std::max(radius, size.x / 2 - FromDIP(2)); + const int right_x = std::min(size.x - radius - 1, size.x / 2 + FromDIP(2)); + const int top_y = std::max(radius + 1, size.y / 2 - FromDIP(5)); + const int gap_y = FromDIP(4); + + for (int row = 0; row < 3; ++row) { + const int y = top_y + row * gap_y; + dc.DrawCircle(wxPoint(left_x, y), radius); + dc.DrawCircle(wxPoint(right_x, y), radius); + } + } + + wxColour m_dot_color; +}; + +static std::vector build_mixed_filament_ui_indices(const std::vector &mixed, + const std::vector &preferred_order) +{ + std::vector ordered_indices; + std::vector used(mixed.size(), false); + + for (const uint64_t stable_id : preferred_order) { + for (size_t idx = 0; idx < mixed.size(); ++idx) { + const MixedFilament &entry = mixed[idx]; + if (used[idx] || entry.deleted || entry.stable_id != stable_id) + continue; + used[idx] = true; + ordered_indices.emplace_back(idx); + break; + } + } + + for (size_t idx = 0; idx < mixed.size(); ++idx) { + if (used[idx] || mixed[idx].deleted) + continue; + ordered_indices.emplace_back(idx); + } + + return ordered_indices; +} + +void Sidebar::init_color_mix_panel(wxWindow* parent, wxSizer* sizer) +{ + // Title bar + p->m_panel_color_mix_title = new StaticBox(parent, wxID_ANY, wxDefaultPosition, + wxDefaultSize, wxTAB_TRAVERSAL | wxBORDER_NONE); + p->m_panel_color_mix_title->SetBackgroundColor(wxColour(248, 248, 248)); + p->m_panel_color_mix_title->SetBackgroundColor2(0xF1F1F1); + p->m_panel_color_mix_title->SetMinSize(wxSize(-1, FromDIP(30))); + p->m_panel_color_mix_title->SetMaxSize(wxSize(-1, FromDIP(30))); + + p->m_color_mix_icon = new ScalableButton(p->m_panel_color_mix_title, wxID_ANY, "color_palette"); + auto* label = new Label(p->m_panel_color_mix_title, _L("Color Mixing"), LB_PROPAGATE_MOUSE_EVENT); + + p->m_btn_del_color_mix = new ScalableButton(p->m_panel_color_mix_title, wxID_ANY, "delete_filament"); + p->m_btn_add_color_mix = new ScalableButton(p->m_panel_color_mix_title, wxID_ANY, "add_filament"); + + auto* h_title = new wxBoxSizer(wxHORIZONTAL); + auto* white_left_c = new wxPanel(p->m_panel_color_mix_title, wxID_ANY, wxDefaultPosition, wxSize(FromDIP(SidebarProps::ContentMargin()), -1)); + white_left_c->SetBackgroundColour(StateColor::darkModeColorFor(*wxWHITE)); + h_title->Add(white_left_c, 0, wxEXPAND | wxTOP | wxBOTTOM, 0); + h_title->Add(p->m_color_mix_icon, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(SidebarProps::TitlebarMargin())); + h_title->AddSpacer(FromDIP(SidebarProps::ElementSpacing())); + h_title->Add(label, 0, wxALIGN_CENTER_VERTICAL); + h_title->AddStretchSpacer(); + h_title->Add(p->m_btn_del_color_mix, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(4)); + h_title->Add(p->m_btn_add_color_mix, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(8)); + auto* white_right_c = new wxPanel(p->m_panel_color_mix_title, wxID_ANY, wxDefaultPosition, wxSize(FromDIP(SidebarProps::ContentMargin()), -1)); + white_right_c->SetBackgroundColour(StateColor::darkModeColorFor(*wxWHITE)); + h_title->Add(white_right_c, 0, wxEXPAND | wxTOP | wxBOTTOM, 0); + p->m_panel_color_mix_title->SetSizer(h_title); + p->m_panel_color_mix_title->Layout(); + + // Scrolled window for content with max height of 3 rows + p->m_scrolled_color_mix = new wxScrolledWindow(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL); + p->m_scrolled_color_mix->SetScrollRate(0, 5); + p->m_scrolled_color_mix->SetBackgroundColour(StateColor::darkModeColorFor(*wxWHITE)); + + // Content panel — match physical filament content panel (sizer set dynamically in update) + p->m_panel_color_mix_content = new wxPanel(p->m_scrolled_color_mix, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL); + p->m_panel_color_mix_content->SetBackgroundColour(StateColor::darkModeColorFor(*wxWHITE)); + + // Add content panel to scrolled window + auto* scrolled_sizer = new wxBoxSizer(wxVERTICAL); + scrolled_sizer->Add(p->m_panel_color_mix_content, 0, wxEXPAND); + p->m_scrolled_color_mix->SetSizer(scrolled_sizer); + + sizer->Add(p->m_panel_color_mix_title, 0, wxEXPAND, 0); + sizer->Add(p->m_scrolled_color_mix, 0, wxEXPAND, 0); + + // Add button: open dialog to create new mix + p->m_btn_add_color_mix->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { + PresetBundle* pb = wxGetApp().preset_bundle; + if (!pb) return; + + auto* co = pb->project_config.option("filament_colour"); + const std::vector colors = co ? co->values : std::vector{}; + if (colors.size() < 2) return; + + MixedFilamentDialog dlg(wxGetApp().mainframe, colors); + if (dlg.ShowModal() != wxID_OK) return; + + auto& mgr = pb->mixed_filaments; + if (mgr.total_filaments(colors.size()) >= MAXIMUM_FILAMENT_NUMBER) return; + const MixedFilament& r = dlg.GetResult(); + mgr.add_custom_filament(r.component_a, r.component_b, r.mix_b_percent, colors); + auto& mfs = mgr.mixed_filaments(); + if (!mfs.empty()) { + mfs.back().distribution_mode = r.distribution_mode; + mfs.back().manual_pattern = r.manual_pattern; + mfs.back().gradient_component_ids = r.gradient_component_ids; + mfs.back().gradient_component_weights = r.gradient_component_weights; + mfs.back().ratio_a = r.ratio_a; + mfs.back().ratio_b = r.ratio_b; + mfs.back().local_z_max_sublayers = r.local_z_max_sublayers; + mfs.back().gradient_enabled = r.gradient_enabled; + mfs.back().gradient_start = r.gradient_start; + mfs.back().gradient_end = r.gradient_end; + mfs.back().display_color = r.display_color; + mfs.back().ui_mode = r.ui_mode; + mfs.back().custom = true; + } + if (auto* opt = pb->project_config.option("mixed_filament_definitions")) + opt->value = mgr.serialize_custom_entries(); + wxGetApp().plater()->post_slice_state_change_update(); + wxGetApp().plater()->on_filaments_change(p->combos_filament.size()); + update_color_mix_panel(); + m_scrolled_sizer->Layout(); + }); + + // Delete button: remove last custom entry + p->m_btn_del_color_mix->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { + PresetBundle* pb = wxGetApp().preset_bundle; + if (!pb) return; + + auto& mgr = pb->mixed_filaments; + auto& mfs = mgr.mixed_filaments(); + for (int i = static_cast(mfs.size()) - 1; i >= 0; --i) { + if (mfs[i].custom && !mfs[i].deleted) { + mfs[i].deleted = true; + break; + } + } + if (auto* opt = pb->project_config.option("mixed_filament_definitions")) + opt->value = mgr.serialize_custom_entries(); + wxGetApp().plater()->post_slice_state_change_update(); + wxGetApp().plater()->on_filaments_change(p->combos_filament.size()); + update_color_mix_panel(); + m_scrolled_sizer->Layout(); + }); + + // Initial visibility: hide if fewer than 2 physical filaments + update_color_mix_panel(); +} + +void Sidebar::update_color_mix_panel() +{ + if (!p->m_panel_color_mix_content) return; + + auto* co = wxGetApp().preset_bundle + ? wxGetApp().preset_bundle->project_config.option("filament_colour") + : nullptr; + const int n_physical = co ? static_cast(co->values.size()) : 0; + const bool show = (n_physical >= 2); + p->m_panel_color_mix_title->Show(show); + p->m_scrolled_color_mix->Show(show); + if (!show) { + return; + } + + wxWindowUpdateLocker no_updates(p->m_panel_color_mix_content); + p->m_panel_color_mix_content->DestroyChildren(); + + auto* preset_bundle = wxGetApp().preset_bundle; + const size_t num_physical = p->combos_filament.size(); + + std::vector physical_colors = co->values; + physical_colors.resize(num_physical, "#26A69A"); + + std::vector nozzle_diameters(num_physical, 0.4); + if (const ConfigOptionFloats* opt = preset_bundle->printers.get_edited_preset().config.option("nozzle_diameter")) { + const size_t opt_count = opt->values.size(); + if (opt_count > 0) + for (size_t i = 0; i < num_physical; ++i) + nozzle_diameters[i] = std::max(0.05, opt->get_at(unsigned(std::min(i, opt_count - 1)))); + } + + float lower_bound = 0.04f, upper_bound = 0.16f; + if (preset_bundle->project_config.has("mixed_filament_height_lower_bound")) + lower_bound = std::max(0.01f, float(preset_bundle->project_config.opt_float("mixed_filament_height_lower_bound"))); + if (preset_bundle->project_config.has("mixed_filament_height_upper_bound")) + upper_bound = std::max(lower_bound, float(preset_bundle->project_config.opt_float("mixed_filament_height_upper_bound"))); + + bool local_z_mode = false; + if (const ConfigOptionBool* opt = preset_bundle->project_config.option("dithering_local_z_mode")) + local_z_mode = opt->value; + + bool component_bias_enabled = false; + if (const ConfigOptionBool* opt = preset_bundle->project_config.option("mixed_filament_component_bias_enabled")) + component_bias_enabled = opt->value; + + const MixedFilamentPreviewSettings preview_settings { + 0.2f, lower_bound, upper_bound, 0.f, 0.f, local_z_mode, false, 1 + }; + const MixedFilamentDisplayContext display_context { + num_physical, physical_colors, nozzle_diameters, preview_settings, component_bias_enabled + }; + preset_bundle->mixed_filaments.set_display_context(display_context); + + auto& mfs = preset_bundle->mixed_filaments.mixed_filaments(); + bool any_visible = false; + for (const MixedFilament& mf : mfs) + if (!mf.deleted) { any_visible = true; break; } + + if (!any_visible) { + p->m_scrolled_color_mix->Hide(); + p->m_btn_del_color_mix->Hide(); + p->m_btn_add_color_mix->SetBitmap_("icon_add_circle"); + m_scrolled_sizer->Layout(); + return; + } + p->m_scrolled_color_mix->Show(); + p->m_btn_del_color_mix->Show(); + p->m_btn_add_color_mix->SetBitmap_("add_filament"); + + // 2-column grid matching the physical filament panel layout + auto* grid_sizer = new wxBoxSizer(wxHORIZONTAL); + auto* col0 = new wxBoxSizer(wxVERTICAL); + auto* col1 = new wxBoxSizer(wxVERTICAL); + grid_sizer->Add(col0, 1, wxEXPAND); + grid_sizer->Add(col1, 1, wxEXPAND); + + int visible_idx = 0; + for (size_t i = 0; i < mfs.size(); ++i) { + MixedFilament& mf = mfs[i]; + if (mf.deleted) continue; + + const std::string synced_color = compute_mixed_filament_display_color(mf, display_context); + if (mf.display_color != synced_color) + mf.display_color = synced_color; + + const int virtual_id = static_cast(num_physical) + visible_idx + 1; + + // Badge button: colored background + virtual filament number with gradient support + auto* badge = new MixedFilamentBadge(p->m_panel_color_mix_content, wxID_ANY, + virtual_id, mf, display_context); + + const std::string normalized_pattern_cm = MixedFilamentManager::normalize_manual_pattern(mf.manual_pattern); + std::vector gradient_ids = MixedFilamentManager::decode_gradient_component_ids(mf.gradient_component_ids, 0); + const bool z_gradient_tile = mf.gradient_enabled && mf.component_a != mf.component_b + && normalized_pattern_cm.empty() && gradient_ids.size() < 3; + wxString lbl; + if (!normalized_pattern_cm.empty()) + lbl = wxString(summarize_cycle_pattern_text(normalized_pattern_cm, mf, int(num_physical))); + else if (gradient_ids.size() >= 3) { + // parse weights + const size_t n = gradient_ids.size(); + std::vector weights; + { + std::string token; + for (const char c : mf.gradient_component_weights) { + if (c >= '0' && c <= '9') { token.push_back(c); continue; } + if (!token.empty()) { weights.emplace_back(std::max(0, std::atoi(token.c_str()))); token.clear(); } + } + if (!token.empty()) weights.emplace_back(std::max(0, std::atoi(token.c_str()))); + if (weights.size() != n) weights.assign(n, int(100 / n)); + } + // normalize to 100 + int sum = 0; for (int v : weights) sum += v; + if (sum <= 0) { weights.assign(n, 0); weights[0] = 100; sum = 100; } + for (size_t k = 0; k < n; ++k) { + const unsigned int fid = gradient_ids[k]; + const int pct = int(std::round(100.0 * weights[k] / sum)); + if (k > 0) lbl += "+"; + lbl += wxString::Format("F%u %d%%", fid, pct); + } + } else if (z_gradient_tile) { + const unsigned from_id = + mf.gradient_start >= mf.gradient_end ? mf.component_a : mf.component_b; + const unsigned to_id = + mf.gradient_start >= mf.gradient_end ? mf.component_b : mf.component_a; + lbl = wxString::Format("F%u->F%u", from_id, to_id); + } else { + const int pct_b = std::clamp(mf.mix_b_percent, 0, 100); + const int pct_a = 100 - pct_b; + lbl = wxString::Format("F%u %d%%+F%u %d%%", mf.component_a, pct_a, mf.component_b, pct_b); + if (mf.distribution_mode != int(MixedFilament::Simple)) + for (unsigned int fid : gradient_ids) + lbl += wxString::Format("+F%u", fid); + } + + bool has_error = !is_filament_compatible(mf); + + // Create a panel with border for the text + auto* name_panel = new wxPanel(p->m_panel_color_mix_content, wxID_ANY); + name_panel->SetBackgroundColour(StateColor::darkModeColorFor(*wxWHITE)); + + auto* name_sizer = new wxBoxSizer(wxHORIZONTAL); + + // Add error icon if there's an error + if (has_error) { + name_sizer->AddSpacer(FromDIP(8)); + ScalableBitmap error_bmp(name_panel, "error_icon_red_exclamation", 14); + auto* error_icon = new wxStaticBitmap(name_panel, wxID_ANY, error_bmp.bmp()); + name_sizer->Add(error_icon, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(4)); + } + + auto* name_btn = new wxStaticText(name_panel, wxID_ANY, lbl, wxDefaultPosition, wxDefaultSize, 0); + name_btn->SetBackgroundColour(StateColor::darkModeColorFor(*wxWHITE)); + name_btn->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#262E30"))); + name_btn->SetCursor(wxCursor(wxCURSOR_HAND)); + name_btn->SetMinSize(wxSize(0, -1)); // allow sizer to shrink below text width + + int name_flags = wxEXPAND | (has_error ? (wxTOP | wxBOTTOM | wxRIGHT) : wxALL); + name_sizer->Add(name_btn, 1, name_flags, FromDIP(8)); + name_panel->SetSizer(name_sizer); + name_panel->SetMinSize(wxSize(name_panel->FromDIP(100), name_panel->FromDIP(30))); + name_panel->SetMaxSize(wxSize(-1, name_panel->FromDIP(30))); + + // Use wxControl::Ellipsize on resize to match combo truncation behavior + name_panel->Bind(wxEVT_SIZE, [lbl, name_btn, name_panel](wxSizeEvent& evt) { + name_panel->Layout(); // force sizer layout so name_btn has its current size + int avail = name_btn->GetSize().x; + if (avail > 0) { + wxClientDC dc(name_btn); + dc.SetFont(name_btn->GetFont()); + wxString ellipsized = wxControl::Ellipsize(lbl, dc, wxELLIPSIZE_END, avail); + name_btn->SetLabel(ellipsized); + } + evt.Skip(); + }); + + // Add border to the panel + name_panel->Bind(wxEVT_PAINT, [](wxPaintEvent& evt) { + wxPanel* panel = dynamic_cast(evt.GetEventObject()); + if (!panel) return; + + wxPaintDC dc(panel); + wxRect rect = panel->GetClientRect(); + + // Draw border matching combo (#dbdbdb, 1px) + dc.SetPen(wxPen(StateColor::darkModeColorFor(wxColour(0xdb, 0xdb, 0xdb)), 1)); + dc.SetBrush(*wxTRANSPARENT_BRUSH); + dc.DrawRectangle(rect); + }); + + name_btn->SetToolTip(lbl); + name_btn->Bind(wxEVT_LEFT_DOWN, [this, i](wxMouseEvent&) { + auto* co = wxGetApp().preset_bundle->project_config.option("filament_colour"); + const std::vector colors = co ? co->values : std::vector{}; + if (colors.size() < 2) return; + auto& mgr = wxGetApp().preset_bundle->mixed_filaments; + auto& mfs2 = mgr.mixed_filaments(); + if (i >= mfs2.size()) return; + MixedFilamentDialog dlg(wxGetApp().mainframe, colors, mfs2[i]); + if (dlg.ShowModal() != wxID_OK) return; + const MixedFilament& r = dlg.GetResult(); + mfs2[i].component_a = r.component_a; + mfs2[i].component_b = r.component_b; + mfs2[i].mix_b_percent = r.mix_b_percent; + mfs2[i].distribution_mode = r.distribution_mode; + mfs2[i].manual_pattern = r.manual_pattern; + mfs2[i].gradient_component_ids = r.gradient_component_ids; + mfs2[i].gradient_component_weights = r.gradient_component_weights; + mfs2[i].ratio_a = r.ratio_a; + mfs2[i].ratio_b = r.ratio_b; + mfs2[i].local_z_max_sublayers = r.local_z_max_sublayers; + mfs2[i].gradient_enabled = r.gradient_enabled; + mfs2[i].gradient_start = r.gradient_start; + mfs2[i].gradient_end = r.gradient_end; + mfs2[i].display_color = r.display_color; + mfs2[i].ui_mode = r.ui_mode; + mfs2[i].custom = true; + if (auto* opt = wxGetApp().preset_bundle->project_config.option("mixed_filament_definitions")) + opt->value = mgr.serialize_custom_entries(); + wxGetApp().plater()->post_slice_state_change_update(); + wxGetApp().plater()->on_filaments_change(p->combos_filament.size()); + wxWeakRef weak_this(this); + wxTheApp->CallAfter([weak_this]() { + Sidebar* sidebar = weak_this.get(); + if (sidebar) { + sidebar->update_color_mix_panel(); + sidebar->m_scrolled_sizer->Layout(); + } + }); + }); + + auto* menu_btn = new ScalableButton(p->m_panel_color_mix_content, wxID_ANY, "menu_filament"); + menu_btn->SetToolTip(_L("Options")); + menu_btn->Bind(wxEVT_BUTTON, [this, i, visible_idx, num_physical, menu_btn](wxCommandEvent&) { + wxMenu menu; + const int edit_id = wxWindow::NewControlId(); + const int del_id = wxWindow::NewControlId(); + const int merge_to_id = wxWindow::NewControlId(); + + menu.Append(edit_id, _L("Edit")); + menu.Bind(wxEVT_MENU, [this, i](wxCommandEvent&) { + auto* co = wxGetApp().preset_bundle->project_config.option("filament_colour"); + const std::vector colors = co ? co->values : std::vector{}; + if (colors.size() < 2) return; + auto& mgr = wxGetApp().preset_bundle->mixed_filaments; + auto& mfs2 = mgr.mixed_filaments(); + if (i >= mfs2.size()) return; + MixedFilamentDialog dlg(wxGetApp().mainframe, colors, mfs2[i]); + if (dlg.ShowModal() != wxID_OK) return; + const MixedFilament& r = dlg.GetResult(); + mfs2[i].component_a = r.component_a; + mfs2[i].component_b = r.component_b; + mfs2[i].mix_b_percent = r.mix_b_percent; + mfs2[i].distribution_mode = r.distribution_mode; + mfs2[i].manual_pattern = r.manual_pattern; + mfs2[i].gradient_component_ids = r.gradient_component_ids; + mfs2[i].gradient_component_weights = r.gradient_component_weights; + mfs2[i].ratio_a = r.ratio_a; + mfs2[i].ratio_b = r.ratio_b; + mfs2[i].local_z_max_sublayers = r.local_z_max_sublayers; + mfs2[i].gradient_enabled = r.gradient_enabled; + mfs2[i].gradient_start = r.gradient_start; + mfs2[i].gradient_end = r.gradient_end; + mfs2[i].ui_mode = r.ui_mode; + mfs2[i].custom = true; + if (auto* opt = wxGetApp().preset_bundle->project_config.option("mixed_filament_definitions")) + opt->value = mgr.serialize_custom_entries(); + wxGetApp().plater()->post_slice_state_change_update(); + wxGetApp().plater()->on_filaments_change(p->combos_filament.size()); + wxWeakRef weak_this(this); + wxTheApp->CallAfter([weak_this]() { + Sidebar* sidebar = weak_this.get(); + if (sidebar) { + sidebar->update_color_mix_panel(); + sidebar->m_scrolled_sizer->Layout(); + } + }); + }, edit_id); + + // Add "Merge with" submenu - allows merging to any other filament (physical or mixed) + // Build list of all available target filaments + wxMenu* merge_submenu = new wxMenu(); + + // Get physical filament icons + std::vector icons = get_extruder_color_icons(true); + + // Add physical filaments as targets + for (size_t phys_idx = 0; phys_idx < num_physical; ++phys_idx) { + const int target_id = wxWindow::NewControlId(); + auto preset = wxGetApp().preset_bundle->filaments.find_preset(wxGetApp().preset_bundle->filament_presets[phys_idx]); + wxString target_label = preset ? from_u8(preset->label(false)) : wxString::Format(_L("Filament %d"), phys_idx + 1); + + // Use icon if available + wxMenuItem* item = nullptr; + if (phys_idx < icons.size() && icons[phys_idx]) { + item = new wxMenuItem(merge_submenu, target_id, target_label); + item->SetBitmap(*icons[phys_idx]); + merge_submenu->Append(item); + } else { + merge_submenu->Append(target_id, target_label); + } + + merge_submenu->Bind(wxEVT_MENU, [this, visible_idx, phys_idx, num_physical](wxCommandEvent&) { + // Source: mixed filament with visible_idx (0-based in visible list) + // Target: physical filament phys_idx (0-based) + // Mixed filament virtual ID = num_physical + visible_idx + 1 (1-based), convert to 0-based + size_t source_virtual_id = num_physical + visible_idx; + change_filament(source_virtual_id, phys_idx); + }, target_id); + } + + // Add other mixed filaments as targets + auto& mgr = wxGetApp().preset_bundle->mixed_filaments; + auto& mfs_for_menu = mgr.mixed_filaments(); + const size_t total_mixed = mfs_for_menu.size(); + + // Get icon dimensions for mixed filaments + const double em = Slic3r::GUI::wxGetApp().em_unit(); + const int icon_width = lround(2 * em); + const int icon_height = lround(2 * em); + + size_t target_visible_idx = 0; + for (size_t j = 0; j < total_mixed; ++j) { + if (mfs_for_menu[j].deleted) continue; + + // Skip self (compare by visible index) + if (target_visible_idx == visible_idx) { + target_visible_idx++; + continue; + } + + const int target_virtual_id = static_cast(num_physical) + target_visible_idx + 1; + const wxString target_label = wxString::Format(_L("Mixed Filament %d"), target_virtual_id); + const int target_id = wxWindow::NewControlId(); + + // Create colored bitmap for mixed filament — gradient filaments get a gradient icon + MixedFilamentDisplayContext menu_ctx; + { + auto* co2 = wxGetApp().preset_bundle->project_config.option("filament_colour"); + menu_ctx.physical_colors = co2 ? co2->values : std::vector{}; + menu_ctx.num_physical = num_physical; + } + wxBitmap* mixed_bmp = create_mixed_filament_menu_bitmap( + mfs_for_menu[j], menu_ctx, icon_width, icon_height, + wxString::Format("%d", target_virtual_id)); + + wxMenuItem* item = new wxMenuItem(merge_submenu, target_id, target_label); + item->SetBitmap(*mixed_bmp); + merge_submenu->Append(item); + + merge_submenu->Bind(wxEVT_MENU, [this, visible_idx, target_visible_idx, num_physical](wxCommandEvent&) { + // Source: mixed filament with visible_idx (0-based in visible list) + // Target: mixed filament with target_visible_idx (0-based in visible list) + // Virtual ID (0-based) = num_physical + visible_idx + size_t source_virtual_id = num_physical + visible_idx; + size_t target_virtual_id = num_physical + target_visible_idx; + change_filament(source_virtual_id, target_virtual_id); + }, target_id); + + target_visible_idx++; + } + + menu.AppendSubMenu(merge_submenu, _L("Merge with")); + + menu.Append(del_id, _L("Delete")); + menu.Bind(wxEVT_MENU, [this, i, num_physical](wxCommandEvent&) { + auto& mgr2 = wxGetApp().preset_bundle->mixed_filaments; + auto& mfs2 = mgr2.mixed_filaments(); + const std::vector old_mixed = mfs2; + if (i < mfs2.size()) { mfs2[i].deleted = true; mfs2[i].enabled = false; } + if (auto* opt = wxGetApp().preset_bundle->project_config.option("mixed_filament_definitions")) + opt->value = mgr2.serialize_custom_entries(); + wxGetApp().preset_bundle->update_mixed_filament_id_remap(old_mixed, num_physical, num_physical, i); + wxGetApp().plater()->post_slice_state_change_update(); + wxGetApp().plater()->on_filaments_change(num_physical); + wxWeakRef weak_this(this); + wxTheApp->CallAfter([weak_this]() { + Sidebar* sidebar = weak_this.get(); + if (sidebar) { + sidebar->update_color_mix_panel(); + sidebar->m_scrolled_sizer->Layout(); + } + }); + }, del_id); + + wxPoint pt{0, menu_btn->GetSize().GetHeight()}; + pt = menu_btn->ClientToScreen(pt); + pt = wxGetApp().mainframe->ScreenToClient(pt); + wxGetApp().mainframe->PopupMenu(&menu, pt); + }); + + auto* cell = new wxBoxSizer(wxHORIZONTAL); + // Match physical filament asymmetric layout: left column gets left spacer, right doesn't + if (visible_idx % 2 == 0) + cell->AddSpacer(FromDIP(SidebarProps::ContentMargin())); + cell->Add(badge, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(SidebarProps::ElementSpacing()) - FromDIP(2)); + cell->Add(name_panel, 1, wxEXPAND | wxALL, FromDIP(2)); + cell->Add(menu_btn, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(SidebarProps::ElementSpacing()) - FromDIP(2)); + cell->AddSpacer(FromDIP(SidebarProps::ContentMargin())); + + (visible_idx % 2 == 0 ? col0 : col1)->Add(cell, 0, wxEXPAND | wxBOTTOM, FromDIP(4)); + + ++visible_idx; + } + + // If odd count, pad right column so left column doesn't stretch + if (visible_idx % 2 == 1) + col1->AddStretchSpacer(1); + + // Wrap grid in vertical sizer for padding, then set as panel sizer (matches physical structure) + auto* wrapper = new wxBoxSizer(wxVERTICAL); + wrapper->Add(grid_sizer, 0, wxEXPAND | wxTOP | wxBOTTOM, FromDIP(8)); + p->m_panel_color_mix_content->SetSizer(wrapper); + p->m_panel_color_mix_content->Layout(); + + // Dynamic height: grow with rows up to 3, only cap when > 3 rows + const wxSize content_best = p->m_panel_color_mix_content->GetBestSize(); + const int row_count = (visible_idx + 1) / 2; // rows (2 columns) + const int desired_h = row_count > 3 + ? (content_best.GetHeight() / row_count) * 3 + : content_best.GetHeight(); + p->m_scrolled_color_mix->SetMinSize({-1, desired_h}); + p->m_scrolled_color_mix->SetMaxSize({-1, desired_h}); + + m_scrolled_sizer->Layout(); + wxWeakRef weak_this(this); + wxTheApp->CallAfter([weak_this]() { + Sidebar* sidebar = weak_this.get(); + if (sidebar && sidebar->p && sidebar->p->m_scrolled_color_mix) { + int vh = sidebar->p->m_scrolled_color_mix->GetVirtualSize().y; + int ch = sidebar->p->m_scrolled_color_mix->GetClientSize().y; + sidebar->p->m_scrolled_color_mix->Scroll(0, std::max(0, vh - ch)); + } + }); + p->m_panel_color_mix_content->Refresh(); + + // Disable add buttons when combined filament limit reached + if (preset_bundle) { + const bool can_add = preset_bundle->mixed_filaments.total_filaments(num_physical) < MAXIMUM_FILAMENT_NUMBER; + if (p->m_bpButton_add_filament) + p->m_bpButton_add_filament->Enable(can_add); + if (p->m_btn_add_color_mix) + p->m_btn_add_color_mix->Enable(can_add); + } +} + +void Sidebar::update_mixed_filament_panel(bool sync_manager) +{ + // Check for new collapsible structure + if (!p->m_panel_mixed_filaments_title || !p->m_panel_mixed_filaments_content) + return; + + wxWindowUpdateLocker noUpdates_sidebar(this); + wxWindowUpdateLocker noUpdates_mixed_panel(p->m_panel_mixed_filaments_content); + + auto refresh_model_canvas_colors = []() { + Plater *plater = wxGetApp().plater(); + if (plater == nullptr) + return; + + auto refresh_canvas = [](GLCanvas3D *canvas) { + if (canvas == nullptr || !canvas->is_initialized()) + return; + canvas->update_volumes_colors_by_extruder(); + canvas->render(); + }; + + refresh_canvas(plater->get_view3D_canvas3D()); + refresh_canvas(plater->get_assmeble_canvas3D()); + }; + + int prev_rows_view_y = 0; + for (wxWindow *child : p->m_panel_mixed_filaments_content->GetChildren()) { + if (auto *scrolled = dynamic_cast(child)) { + int tmp_x = 0; + scrolled->GetViewStart(&tmp_x, &prev_rows_view_y); + break; + } + } + + auto *preset_bundle = wxGetApp().preset_bundle; + if (!preset_bundle) + return; + DynamicPrintConfig *print_cfg = &preset_bundle->prints.get_edited_preset().config; + + const size_t num_physical = p->combos_filament.size(); + ConfigOptionStrings *color_opt = preset_bundle->project_config.option("filament_colour"); + std::vector physical_colors = color_opt ? color_opt->values : std::vector(); + physical_colors.resize(num_physical, "#26A69A"); + std::vector nozzle_diameters(num_physical, 0.4); + if (const ConfigOptionFloats *opt = preset_bundle->printers.get_edited_preset().config.option("nozzle_diameter")) { + const size_t opt_count = opt->values.size(); + if (opt_count > 0) { + for (size_t i = 0; i < num_physical; ++i) + nozzle_diameters[i] = std::max(0.05, opt->get_at(unsigned(std::min(i, opt_count - 1)))); + } + } + + auto get_mixed_bool = [preset_bundle, print_cfg](const std::string &key, bool fallback) { + if (const ConfigOptionBool *opt = preset_bundle->project_config.option(key)) + return opt->value; + if (const ConfigOptionInt *opt = preset_bundle->project_config.option(key)) + return opt->value != 0; + if (print_cfg) { + if (const ConfigOptionBool *opt = print_cfg->option(key)) + return opt->value; + if (const ConfigOptionInt *opt = print_cfg->option(key)) + return opt->value != 0; + } + return fallback; + }; + auto get_mixed_mode = [preset_bundle, print_cfg](bool fallback) { + if (const ConfigOptionBool *opt = preset_bundle->project_config.option("mixed_filament_gradient_mode")) + return opt->value; + if (const ConfigOptionInt *opt = preset_bundle->project_config.option("mixed_filament_gradient_mode")) + return opt->value != 0; + if (print_cfg) { + if (const ConfigOptionBool *opt = print_cfg->option("mixed_filament_gradient_mode")) + return opt->value; + if (const ConfigOptionInt *opt = print_cfg->option("mixed_filament_gradient_mode")) + return opt->value != 0; + } + return fallback; + }; + auto get_mixed_float = [preset_bundle, print_cfg](const std::string &key, float fallback) { + if (preset_bundle->project_config.has(key)) + return float(preset_bundle->project_config.opt_float(key)); + if (print_cfg && print_cfg->has(key)) + return float(print_cfg->opt_float(key)); + return fallback; + }; + auto get_mixed_string = [preset_bundle, print_cfg](const std::string &key, const std::string &fallback = std::string()) { + std::string project_value; + if (preset_bundle->project_config.has(key)) + project_value = preset_bundle->project_config.opt_string(key); + if (!project_value.empty()) + return project_value; + if (print_cfg && print_cfg->has(key)) { + const std::string print_value = print_cfg->opt_string(key); + if (!print_value.empty()) + return print_value; + } + return project_value.empty() ? fallback : project_value; + }; + auto set_mixed_float = [preset_bundle, print_cfg](const std::string &key, float value) { + if (print_cfg) { + if (ConfigOptionFloat *opt = print_cfg->option(key)) + opt->value = value; + } + if (ConfigOptionFloat *opt = preset_bundle->project_config.option(key)) + opt->value = value; + else + preset_bundle->project_config.set_key_value(key, new ConfigOptionFloat(value)); + }; + auto set_mixed_string = [preset_bundle, print_cfg](const std::string &key, const std::string &value) { + if (print_cfg) { + if (ConfigOptionString *opt = print_cfg->option(key)) + opt->value = value; + } + if (ConfigOptionString *opt = preset_bundle->project_config.option(key)) + opt->value = value; + else + preset_bundle->project_config.set_key_value(key, new ConfigOptionString(value)); + }; + auto set_mixed_bool = [preset_bundle, print_cfg](const std::string &key, bool value) { + if (print_cfg) { + if (ConfigOptionBool *opt = print_cfg->option(key)) + opt->value = value; + else if (ConfigOptionInt *opt = print_cfg->option(key)) + opt->value = value ? 1 : 0; + } + if (ConfigOptionBool *opt = preset_bundle->project_config.option(key)) + opt->value = value; + else if (ConfigOptionInt *opt = preset_bundle->project_config.option(key)) + opt->value = value ? 1 : 0; + else + preset_bundle->project_config.set_key_value(key, new ConfigOptionBool(value)); + }; + auto set_mixed_mode = [preset_bundle, print_cfg](bool enabled) { + if (print_cfg) { + if (ConfigOptionBool *opt = print_cfg->option("mixed_filament_gradient_mode")) + opt->value = enabled; + else if (ConfigOptionInt *opt = print_cfg->option("mixed_filament_gradient_mode")) + opt->value = enabled ? 1 : 0; + } + if (ConfigOptionBool *opt = preset_bundle->project_config.option("mixed_filament_gradient_mode")) + opt->value = enabled; + else if (ConfigOptionInt *opt = preset_bundle->project_config.option("mixed_filament_gradient_mode")) + opt->value = enabled ? 1 : 0; + else + preset_bundle->project_config.set_key_value("mixed_filament_gradient_mode", new ConfigOptionBool(enabled)); + }; + auto notify_mixed_change = [print_cfg]() { + if (!print_cfg) + return; + if (auto *print_tab = wxGetApp().get_tab(Preset::TYPE_PRINT)) + print_tab->update_dirty(); + if (wxGetApp().mainframe) + wxGetApp().mainframe->on_config_changed(print_cfg); + }; + auto decode_gradient_ids = [](const std::string &encoded) { + return MixedFilamentManager::decode_gradient_component_ids(encoded, 0); + }; + auto encode_gradient_ids = [](const std::vector &ids) { + return MixedFilamentManager::encode_gradient_component_ids(ids); + }; + auto decode_gradient_weights = [](const std::string &encoded, size_t expected_count) { + std::vector out; + if (encoded.empty() || expected_count == 0) + return out; + std::string token; + for (const char c : encoded) { + if (c >= '0' && c <= '9') { + token.push_back(c); + continue; + } + if (!token.empty()) { + out.emplace_back(std::max(0, std::atoi(token.c_str()))); + token.clear(); + } + } + if (!token.empty()) + out.emplace_back(std::max(0, std::atoi(token.c_str()))); + if (out.size() != expected_count) + out.clear(); + return out; + }; + auto normalize_gradient_weights = [](const std::vector &weights, size_t n) { + std::vector out = weights; + if (out.size() != n) + out.assign(n, (n > 0) ? int(100 / n) : 0); + int sum = 0; + for (int &v : out) { + v = std::max(0, v); + sum += v; + } + if (sum <= 0 && n > 0) { + out.assign(n, 0); + out[0] = 100; + return out; + } + std::vector rem(n, 0.); + int assigned = 0; + for (size_t i = 0; i < n; ++i) { + const double exact = 100.0 * double(out[i]) / double(sum); + out[i] = int(std::floor(exact)); + rem[i] = exact - double(out[i]); + assigned += out[i]; + } + int missing = std::max(0, 100 - assigned); + while (missing > 0) { + size_t best_idx = 0; + double best_rem = -1.0; + for (size_t i = 0; i < rem.size(); ++i) { + if (rem[i] > best_rem) { + best_rem = rem[i]; + best_idx = i; + } + } + ++out[best_idx]; + rem[best_idx] = 0.0; + --missing; + } + return out; + }; + auto encode_gradient_weights = [](const std::vector &weights) { + std::ostringstream ss; + for (size_t i = 0; i < weights.size(); ++i) { + if (i > 0) + ss << '/'; + ss << std::max(0, weights[i]); + } + return ss.str(); + }; + auto build_weighted_multi_sequence = [normalize_gradient_weights](const std::vector &ids, + const std::vector &weights, + size_t max_cycle_limit) { + if (ids.empty()) + return std::vector(); + + std::vector filtered_ids; + std::vector counts; + filtered_ids.reserve(ids.size()); + counts.reserve(ids.size()); + + std::vector normalized = normalize_gradient_weights(weights, ids.size()); + for (size_t i = 0; i < ids.size(); ++i) { + const int weight = (i < normalized.size()) ? std::max(0, normalized[i]) : 0; + if (weight <= 0) + continue; + filtered_ids.emplace_back(ids[i]); + counts.emplace_back(weight); + } + if (filtered_ids.empty()) { + filtered_ids = ids; + counts.assign(ids.size(), 1); + } + + int g = 0; + for (const int c : counts) + g = std::gcd(g, std::max(1, c)); + if (g > 1) { + for (int &c : counts) + c = std::max(1, c / g); + } + + constexpr size_t k_max_cycle = 48; + const size_t effective_cycle_limit = + max_cycle_limit > 0 ? std::min(k_max_cycle, std::max(1, max_cycle_limit)) : k_max_cycle; + reduce_weight_counts_to_cycle_limit(counts, effective_cycle_limit); + + std::vector reduced_ids; + std::vector reduced_counts; + reduced_ids.reserve(filtered_ids.size()); + reduced_counts.reserve(counts.size()); + for (size_t i = 0; i < counts.size(); ++i) { + if (counts[i] <= 0) + continue; + reduced_ids.emplace_back(filtered_ids[i]); + reduced_counts.emplace_back(counts[i]); + } + if (reduced_ids.empty()) + return std::vector(); + filtered_ids = std::move(reduced_ids); + counts = std::move(reduced_counts); + + const int total = std::accumulate(counts.begin(), counts.end(), 0); + if (total <= 0) + return std::vector(filtered_ids.begin(), filtered_ids.end()); + + const size_t cycle = size_t(total); + + std::vector sequence; + sequence.reserve(cycle); + std::vector emitted(counts.size(), 0); + for (size_t pos = 0; pos < cycle; ++pos) { + size_t best_idx = 0; + double best_score = -1e9; + for (size_t i = 0; i < counts.size(); ++i) { + const double target = double(pos + 1) * double(counts[i]) / double(total); + const double score = target - double(emitted[i]); + if (score > best_score) { + best_score = score; + best_idx = i; + } + } + ++emitted[best_idx]; + sequence.emplace_back(filtered_ids[best_idx]); + } + if (sequence.empty()) + sequence = filtered_ids; + return sequence; + }; + auto decode_manual_pattern_ids = [num_physical](const std::string &pattern, + unsigned int component_a, + unsigned int component_b, + size_t wall_loops) { + return build_grouped_manual_pattern_preview_sequence(pattern, component_a, component_b, num_physical, wall_loops); + }; + const bool height_weighted_mode = get_mixed_mode(false); + int gradient_mode = height_weighted_mode ? 1 : 0; + float lower_bound = std::max(0.01f, get_mixed_float("mixed_filament_height_lower_bound", 0.04f)); + float upper_bound = std::max(lower_bound, get_mixed_float("mixed_filament_height_upper_bound", 0.16f)); + float preferred_local_z_a = std::max(0.f, get_mixed_float("mixed_color_layer_height_a", 0.f)); + float preferred_local_z_b = std::max(0.f, get_mixed_float("mixed_color_layer_height_b", 0.f)); + float nominal_layer_height = 0.2f; + if (print_cfg && print_cfg->has("layer_height")) + nominal_layer_height = float(print_cfg->opt_float("layer_height")); + nominal_layer_height = std::max(0.01f, nominal_layer_height); + size_t wall_loops = 1; + if (print_cfg && print_cfg->has("wall_loops")) + wall_loops = std::max(1, size_t(std::max(1, print_cfg->opt_int("wall_loops")))); + const bool local_z_mode = get_mixed_bool("dithering_local_z_mode", false); + const bool local_z_direct_multicolor = + get_mixed_bool("dithering_local_z_direct_multicolor", false) && + preferred_local_z_a <= EPSILON && + preferred_local_z_b <= EPSILON; + const bool component_bias_enabled = get_mixed_bool("mixed_filament_component_bias_enabled", false); + float pointillism_pixel_size = std::max(0.f, get_mixed_float("mixed_filament_pointillism_pixel_size", 0.f)); + float pointillism_line_gap = std::max(0.f, get_mixed_float("mixed_filament_pointillism_line_gap", 0.f)); + float mixed_surface_indentation = std::clamp(get_mixed_float("mixed_filament_surface_indentation", 0.f), -2.f, 2.f); + bool advanced_dithering = get_mixed_bool("mixed_filament_advanced_dithering", false); + const std::string mixed_definitions = get_mixed_string("mixed_filament_definitions"); + const MixedFilamentPreviewSettings preview_settings { + nominal_layer_height, + lower_bound, + upper_bound, + preferred_local_z_a, + preferred_local_z_b, + local_z_mode, + local_z_direct_multicolor, + wall_loops + }; + const MixedFilamentDisplayContext display_context { + num_physical, + physical_colors, + nozzle_diameters, + preview_settings, + component_bias_enabled + }; + auto summarize_sequence = [num_physical](const std::vector &sequence) { + if (sequence.empty() || num_physical == 0) + return std::string(); + std::vector counts(num_physical + 1, size_t(0)); + size_t total = 0; + for (const unsigned int id : sequence) { + if (id == 0 || id > num_physical) + continue; + ++counts[id]; + ++total; + } + if (total == 0) + return std::string(); + std::ostringstream ss; + bool first = true; + for (size_t id = 1; id <= num_physical; ++id) { + if (counts[id] == 0) + continue; + const int pct = int(std::lround(100.0 * double(counts[id]) / double(total))); + if (!first) + ss << " "; + first = false; + ss << "F" << id << ":" << pct << "%"; + } + return ss.str(); + }; + auto compute_entry_display_color = [display_context](const MixedFilament &entry) { + return compute_mixed_filament_display_color(entry, display_context); + }; + + auto &mixed_mgr = preset_bundle->mixed_filaments; + mixed_mgr.set_display_context(display_context); + if (sync_manager) { + mixed_mgr.auto_generate(physical_colors); + mixed_mgr.clear_custom_entries(); + mixed_mgr.load_custom_entries(mixed_definitions, physical_colors); + mixed_mgr.apply_gradient_settings(gradient_mode, lower_bound, upper_bound, advanced_dithering); + } + + if (component_bias_enabled) { + for (MixedFilament &entry : mixed_mgr.mixed_filaments()) { + const float bias_value = mixed_filament_single_surface_offset_value(entry, nozzle_diameters); + const auto balanced_pair = mixed_filament_single_surface_offset_pair(entry, bias_value, nozzle_diameters); + entry.component_a_surface_offset = balanced_pair.first; + entry.component_b_surface_offset = balanced_pair.second; + } + } + + // During project load, sidebar may refresh before physical filament combos + // finish syncing. Avoid overwriting persisted mixed definitions while the + // physical filament set is incomplete. + if (num_physical >= 2) { + set_mixed_mode(height_weighted_mode); + set_mixed_bool("mixed_filament_component_bias_enabled", component_bias_enabled); + set_mixed_float("mixed_filament_height_lower_bound", lower_bound); + set_mixed_float("mixed_filament_height_upper_bound", upper_bound); + set_mixed_float("mixed_color_layer_height_a", preferred_local_z_a); + set_mixed_float("mixed_color_layer_height_b", preferred_local_z_b); + set_mixed_float("mixed_filament_pointillism_pixel_size", pointillism_pixel_size); + set_mixed_float("mixed_filament_pointillism_line_gap", pointillism_line_gap); + set_mixed_float("mixed_filament_surface_indentation", mixed_surface_indentation); + set_mixed_string("mixed_filament_definitions", mixed_mgr.serialize_custom_entries()); + } + + auto &mixed = mixed_mgr.mixed_filaments(); + const std::vector ordered_mixed_indices = build_mixed_filament_ui_indices(mixed, p->m_mixed_filament_ui_order); + std::vector sanitized_mixed_ui_order_ids; + sanitized_mixed_ui_order_ids.reserve(ordered_mixed_indices.size()); + for (const size_t mixed_id : ordered_mixed_indices) { + if (mixed_id < mixed.size() && mixed[mixed_id].stable_id != 0) + sanitized_mixed_ui_order_ids.emplace_back(mixed[mixed_id].stable_id); + } + p->m_mixed_filament_ui_order = std::move(sanitized_mixed_ui_order_ids); + + p->m_mixed_filament_drag_active = false; + p->m_mixed_filament_drag_source_mixed_id = size_t(-1); + p->m_mixed_filament_row_bindings.clear(); + + const int compact_gap_x = FromDIP(6); + const int compact_gap_y = FromDIP(4); + const int compact_row_pad = FromDIP(6); + const bool is_dark = wxGetApp().dark_mode(); + const wxColour mixed_rows_bg = is_dark ? wxColour(45, 45, 49) : wxColour(246, 248, 251); + const wxColour mixed_row_bg = is_dark ? wxColour(52, 52, 56) : wxColour(255, 255, 255); + const wxColour mixed_row_hover_bg = is_dark ? wxColour(62, 62, 68) : wxColour(241, 247, 255); + const wxColour mixed_text_fg = is_dark ? wxColour(232, 232, 232) : wxColour(20, 20, 20); + const wxColour mixed_summary_fg = is_dark ? wxColour(182, 182, 182) : wxColour(96, 96, 96); + p->m_panel_mixed_filaments_content->SetBackgroundColour(mixed_rows_bg); + + // Get the content sizer and clear it + wxSizer *content_sizer = p->m_panel_mixed_filaments_content->GetSizer(); + if (content_sizer) + content_sizer->Clear(true); + + // Re-add the top margin spacer that was added in constructor but cleared above + if (content_sizer) + content_sizer->AddSpacer(FromDIP(SidebarProps::ContentMargin())); + + // Update button states (buttons are now in title bar, created in constructor) + if (p->m_btn_add_gradient) + p->m_btn_add_gradient->Enable(num_physical >= 2); + if (p->m_btn_add_pattern) + p->m_btn_add_pattern->Enable(num_physical >= 2); + if (p->m_btn_add_color) + p->m_btn_add_color->Enable(num_physical >= 2); + + // Mixed Filaments panel is hidden + p->m_panel_mixed_filaments_title->Hide(); + p->m_panel_mixed_filaments_content->Hide(); + Layout(); + refresh_model_canvas_colors(); + wxWeakRef weakSelf(this); + wxTheApp->CallAfter([weakSelf]() { + if (weakSelf) weakSelf->update_color_mix_panel(); + }); + return; + +#if 0 // Mixed Filaments panel UI — hidden, preserved for potential future re-enablement + + // Reset the max size in case it was collapsed + p->m_panel_mixed_filaments_content->SetMaxSize({-1, -1}); + + auto *rows_scroller = new wxScrolledWindow(p->m_panel_mixed_filaments_content, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL | wxTAB_TRAVERSAL); + rows_scroller->SetScrollRate(0, FromDIP(6)); + rows_scroller->ShowScrollbars(wxSHOW_SB_NEVER, wxSHOW_SB_DEFAULT); + rows_scroller->SetBackgroundColour(mixed_rows_bg); + auto *rows_sizer = new wxBoxSizer(wxVERTICAL); + rows_scroller->SetSizer(rows_sizer); + + if (mixed.empty()) { + auto *empty_label = new wxStaticText(rows_scroller, wxID_ANY, + _L("No mixed filaments yet. Use Add Gradient, Add Pattern, or Add Color to create one.")); + empty_label->SetForegroundColour(mixed_summary_fg); + empty_label->SetFont(::Label::Body_13); + empty_label->Wrap(FromDIP(360)); + rows_sizer->Add(empty_label, 0, wxALL | wxEXPAND, FromDIP(12)); + rows_scroller->Layout(); + rows_scroller->FitInside(); + const int empty_content_h = empty_label->GetBestSize().GetHeight() + FromDIP(28); + const int empty_rows_h = std::max(FromDIP(86), empty_content_h); + rows_scroller->SetMinSize(wxSize(-1, empty_rows_h)); + rows_scroller->SetMaxSize(wxSize(-1, empty_rows_h)); + if (content_sizer) + content_sizer->Add(rows_scroller, 0, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, FromDIP(SidebarProps::ContentMargin())); + p->m_panel_mixed_filaments_content->Layout(); + Layout(); + refresh_model_canvas_colors(); + update_color_mix_panel(); + return; + } + + auto adjust_rows_scroller_height = [this, rows_scroller]() { + if (!rows_scroller) + return; + const int min_h = FromDIP(68); + const int collapsed_max_h = FromDIP(220); + int two_rows_cap_h = collapsed_max_h; + const auto &children = rows_scroller->GetChildren(); + if (!children.empty()) { + std::vector heights; + heights.reserve(children.GetCount()); + for (wxWindowList::compatibility_iterator it = children.GetFirst(); it; it = it->GetNext()) { + wxWindow *child = it->GetData(); + wxPanel *panel = dynamic_cast(child); + if (!panel) + continue; + heights.emplace_back(std::max(panel->GetSize().GetHeight(), panel->GetBestSize().GetHeight())); + } + if (!heights.empty()) { + std::sort(heights.begin(), heights.end(), std::greater()); + const size_t keep = std::min(2, heights.size()); + int rows_h = 0; + for (size_t i = 0; i < keep; ++i) + rows_h += heights[i]; + if (keep > 1) + rows_h += int(keep - 1) * FromDIP(2); + rows_h += FromDIP(8); + two_rows_cap_h = std::max(collapsed_max_h, rows_h); + } + } + + const int max_h = p->m_expanded_mixed_filament_rows.empty() ? collapsed_max_h : two_rows_cap_h; + const int content_h = std::max(0, rows_scroller->GetVirtualSize().GetHeight()); + const int desired_h = std::clamp(content_h, min_h, max_h); + rows_scroller->SetMinSize(wxSize(-1, desired_h)); + rows_scroller->SetMaxSize(wxSize(-1, desired_h)); + }; + + for (auto it = p->m_expanded_mixed_filament_rows.begin(); it != p->m_expanded_mixed_filament_rows.end();) { + if (*it >= mixed.size() || mixed[*it].deleted) + it = p->m_expanded_mixed_filament_rows.erase(it); + else + ++it; + } + + std::vector palette; + palette.reserve(physical_colors.size()); + for (const std::string &hex : physical_colors) + palette.emplace_back(parse_mixed_color(hex)); + + auto mixed_summary_text = [decode_gradient_ids](const MixedFilament &entry) { + const std::string normalized_pattern = MixedFilamentManager::normalize_manual_pattern(entry.manual_pattern); + if (!entry.custom) + return wxString::Format("(Filament %u + Filament %u)", unsigned(entry.component_a), unsigned(entry.component_b)); + if (!normalized_pattern.empty()) + return _L("(Pattern)"); + if (decode_gradient_ids(entry.gradient_component_ids).size() >= 3) + return _L("(Color)"); + return wxString::Format("(F%u + F%u)", unsigned(entry.component_a), unsigned(entry.component_b)); + }; + + auto apply_mixed_entry_changes = [this, preset_bundle, print_cfg, num_physical](size_t mixed_id, + const MixedFilament &updated_mf, + bool preserve_enabled = false, + bool rebuild_virtual_id_remap = false) { + if (!preset_bundle) + return; + + auto &mgr = preset_bundle->mixed_filaments; + auto &mfs = mgr.mixed_filaments(); + if (mixed_id >= mfs.size()) + return; + + const std::vector old_mixed = rebuild_virtual_id_remap ? mfs : std::vector(); + MixedFilament merged = updated_mf; + if (preserve_enabled) + merged.enabled = mfs[mixed_id].enabled; + mfs[mixed_id] = merged; + + const std::string serialized = mgr.serialize_custom_entries(); + if (print_cfg) { + if (ConfigOptionString *opt = print_cfg->option("mixed_filament_definitions")) + opt->value = serialized; + else + print_cfg->set_key_value("mixed_filament_definitions", new ConfigOptionString(serialized)); + } + if (ConfigOptionString *opt = preset_bundle->project_config.option("mixed_filament_definitions")) + opt->value = serialized; + else + preset_bundle->project_config.set_key_value("mixed_filament_definitions", new ConfigOptionString(serialized)); + + if (print_cfg) { + if (auto *print_tab = wxGetApp().get_tab(Preset::TYPE_PRINT)) + print_tab->update_dirty(); + if (wxGetApp().mainframe) + wxGetApp().mainframe->on_config_changed(print_cfg); + } + if (wxGetApp().plater()) + wxGetApp().plater()->update_project_dirty_from_presets(); + + if (rebuild_virtual_id_remap) + preset_bundle->update_mixed_filament_id_remap(old_mixed, num_physical, num_physical); + + int mode = 0; + if (const ConfigOptionBool *opt = preset_bundle->project_config.option("mixed_filament_gradient_mode")) + mode = opt->value ? 1 : 0; + else if (const ConfigOptionInt *opt = preset_bundle->project_config.option("mixed_filament_gradient_mode")) + mode = opt->value != 0 ? 1 : 0; + float lo = preset_bundle->project_config.has("mixed_filament_height_lower_bound") ? + float(preset_bundle->project_config.opt_float("mixed_filament_height_lower_bound")) : 0.04f; + float hi = preset_bundle->project_config.has("mixed_filament_height_upper_bound") ? + float(preset_bundle->project_config.opt_float("mixed_filament_height_upper_bound")) : 0.16f; + bool advanced = false; + if (const ConfigOptionBool *opt = preset_bundle->project_config.option("mixed_filament_advanced_dithering")) + advanced = opt->value; + mode = std::clamp(mode, 0, 1); + lo = std::max(0.01f, lo); + hi = std::max(lo, hi); + mgr.apply_gradient_settings(mode, lo, hi, advanced); + update_dynamic_filament_list(); + + if (rebuild_virtual_id_remap && wxGetApp().plater()) { + p->m_skip_mixed_filament_sync_once = true; + wxGetApp().plater()->on_filaments_change(num_physical); + } + }; + + auto current_mixed_filament_ui_order = [this, &mixed]() { + std::vector ordered_ids; + ordered_ids.reserve(p->m_mixed_filament_row_bindings.size()); + for (const auto &binding : p->m_mixed_filament_row_bindings) { + if (binding.mixed_id < mixed.size() && mixed[binding.mixed_id].stable_id != 0) + ordered_ids.emplace_back(mixed[binding.mixed_id].stable_id); + } + return ordered_ids; + }; + + auto drop_insert_position = [this]() { + const wxPoint mouse_pos = wxGetMousePosition(); + size_t visible_idx = 0; + for (const auto &binding : p->m_mixed_filament_row_bindings) { + if (binding.row == nullptr || !binding.row->IsShown()) + continue; + + const wxPoint top_left = binding.row->ClientToScreen(wxPoint(0, 0)); + const int row_h = std::max(binding.row->GetSize().GetHeight(), binding.row->GetBestSize().GetHeight()); + const int center_y = top_left.y + row_h / 2; + if (mouse_pos.y < center_y) + return visible_idx; + + ++visible_idx; + } + return visible_idx; + }; + + for (size_t display_mixed_idx = 0; display_mixed_idx < ordered_mixed_indices.size(); ++display_mixed_idx) { + const size_t mixed_id = ordered_mixed_indices[display_mixed_idx]; + MixedFilament &mf = mixed[mixed_id]; + const bool auto_row = !mf.custom; + + auto *row = new wxPanel(rows_scroller, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxBORDER_NONE); + row->SetBackgroundColour(mixed_row_bg); + auto *row_sizer = new wxBoxSizer(wxVERTICAL); + p->m_mixed_filament_row_bindings.push_back({mixed_id, row}); + + auto *header_panel = new wxPanel(row, wxID_ANY); + header_panel->SetBackgroundColour(mixed_row_bg); + auto *header_sizer = new wxBoxSizer(wxHORIZONTAL); + + const std::string synced_color = compute_entry_display_color(mf); + if (mf.display_color != synced_color) + mf.display_color = synced_color; + auto *drag_handle = new MixedFilamentDragHandle(header_panel, mixed_summary_fg, mixed_row_bg); + drag_handle->SetToolTip(_L("Drag to reorder mixed filaments in this panel.")); + header_sizer->Add(drag_handle, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, compact_gap_x); + + wxColour swatch_color = parse_mixed_color(mf.display_color); + auto *swatch = new wxPanel(header_panel, wxID_ANY, wxDefaultPosition, wxSize(FromDIP(12), FromDIP(12))); + swatch->SetBackgroundColour(swatch_color); + swatch->SetMinSize(wxSize(FromDIP(12), FromDIP(12))); + header_sizer->Add(swatch, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, compact_gap_x); + + const int virtual_filament_id = int(num_physical + display_mixed_idx + 1); + auto *name_label = new wxStaticText(header_panel, wxID_ANY, wxString::Format("Mixed Filament %d", virtual_filament_id)); + name_label->SetForegroundColour(mixed_text_fg); + header_sizer->Add(name_label, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, compact_gap_x); + + auto *summary_label = new wxStaticText(header_panel, wxID_ANY, mixed_summary_text(mf)); + summary_label->SetForegroundColour(mixed_summary_fg); + header_sizer->Add(summary_label, 1, wxALIGN_CENTER_VERTICAL | wxLEFT, compact_gap_x); + + header_sizer->AddStretchSpacer(1); + + auto *enabled_chk = new wxCheckBox(header_panel, wxID_ANY, _L("Enabled")); + enabled_chk->SetValue(mf.enabled); + enabled_chk->SetForegroundColour(mixed_text_fg); + header_sizer->Add(enabled_chk, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, compact_gap_x); + enabled_chk->Bind(wxEVT_LEFT_UP, [](wxMouseEvent &evt) { + evt.StopPropagation(); + evt.Skip(); + }); + enabled_chk->Bind(wxEVT_CHECKBOX, [mixed_id, enabled_chk, apply_mixed_entry_changes, preset_bundle](wxCommandEvent &) { + if (!preset_bundle || !enabled_chk) + return; + auto &mgr = preset_bundle->mixed_filaments; + auto &mfs = mgr.mixed_filaments(); + if (mixed_id >= mfs.size()) + return; + MixedFilament updated = mfs[mixed_id]; + updated.enabled = enabled_chk->GetValue(); + apply_mixed_entry_changes(mixed_id, updated, false, true); + }); + + auto *del_btn = new ScalableButton(header_panel, wxID_ANY, "cross"); + del_btn->SetToolTip(_L("Delete mixed filament")); + header_sizer->Add(del_btn, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, compact_gap_x); + + del_btn->Bind(wxEVT_BUTTON, [this, mixed_id, num_physical, set_mixed_string, notify_mixed_change](wxCommandEvent&) { + if (wxGetApp().preset_bundle) { + auto &mgr = wxGetApp().preset_bundle->mixed_filaments; + auto &mfs = mgr.mixed_filaments(); + if (mixed_id < mfs.size()) { + const std::vector old_mixed = mfs; + auto canonical_pair = [](unsigned int a, unsigned int b) { + return std::make_pair(std::min(a, b), std::max(a, b)); + }; + MixedFilament &target = mfs[mixed_id]; + const auto target_pair = canonical_pair(target.component_a, target.component_b); + const bool valid_auto_pair = target_pair.first >= 1 && + target_pair.second >= 1 && + target_pair.first <= num_physical && + target_pair.second <= num_physical && + target_pair.first != target_pair.second; + if (target.custom && target.origin_auto && valid_auto_pair) { + bool tombstoned_existing_auto = false; + for (size_t idx = 0; idx < mfs.size(); ++idx) { + if (idx == mixed_id) + continue; + MixedFilament &candidate = mfs[idx]; + if (candidate.custom) + continue; + if (canonical_pair(candidate.component_a, candidate.component_b) != target_pair) + continue; + candidate.deleted = true; + candidate.enabled = false; + tombstoned_existing_auto = true; + break; + } + + if (tombstoned_existing_auto) { + mfs.erase(mfs.begin() + mixed_id); + } else { + target.component_a = target_pair.first; + target.component_b = target_pair.second; + target.mix_b_percent = 50; + target.ratio_a = 1; + target.ratio_b = 1; + target.manual_pattern.clear(); + target.gradient_component_ids.clear(); + target.gradient_component_weights.clear(); + target.pointillism_all_filaments = false; + target.distribution_mode = int(MixedFilament::Simple); + target.custom = false; + target.origin_auto = true; + target.deleted = true; + target.enabled = false; + } + } else if (target.custom) { + mfs.erase(mfs.begin() + mixed_id); + } else { + target.deleted = true; + target.enabled = false; + } + p->m_expanded_mixed_filament_rows.clear(); + set_mixed_string("mixed_filament_definitions", mgr.serialize_custom_entries()); + wxGetApp().preset_bundle->update_mixed_filament_id_remap(old_mixed, num_physical, num_physical, mixed_id); + notify_mixed_change(); + if (wxGetApp().plater()) + wxGetApp().plater()->update_project_dirty_from_presets(); + if (wxGetApp().plater()) { + p->m_skip_mixed_filament_sync_once = true; + wxGetApp().plater()->on_filaments_change(num_physical); + } + } + } + }); + + header_panel->SetSizer(header_sizer); + row_sizer->Add(header_panel, 0, wxEXPAND | wxALL, 0); + + auto *editor_host = new wxPanel(row, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxBORDER_NONE); + editor_host->SetBackgroundColour(mixed_row_bg); + auto *editor_sizer = new wxBoxSizer(wxVERTICAL); + editor_host->SetSizer(editor_sizer); + editor_host->Hide(); + row_sizer->Add(editor_host, 0, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, compact_row_pad); + + auto set_row_hover = [row, header_panel, editor_host, drag_handle, mixed_summary_fg, mixed_row_bg, mixed_row_hover_bg](bool hovered) { + const wxColour bg = hovered ? mixed_row_hover_bg : mixed_row_bg; + if (row) row->SetBackgroundColour(bg); + if (header_panel) header_panel->SetBackgroundColour(bg); + if (editor_host) editor_host->SetBackgroundColour(bg); + if (drag_handle) drag_handle->set_colors(mixed_summary_fg, bg); + if (row) row->Refresh(); + if (header_panel) header_panel->Refresh(); + if (editor_host) editor_host->Refresh(); + }; + + auto row_contains_mouse = [row]() { + if (!row) + return false; + const wxPoint mouse_pos = wxGetMousePosition(); + const wxPoint local = row->ScreenToClient(mouse_pos); + return row->GetClientRect().Contains(local); + }; + + auto ensure_editor = [this, mixed_id, num_physical, physical_colors, nozzle_diameters, palette, preview_settings, component_bias_enabled, preset_bundle, + editor_host, editor_sizer, swatch, summary_label, header_panel, row, + rows_scroller, mixed_summary_text, apply_mixed_entry_changes]() { + if (!preset_bundle || !editor_sizer || editor_sizer->GetItemCount() > 0) + return; + + auto &mgr = preset_bundle->mixed_filaments; + auto &mfs = mgr.mixed_filaments(); + if (mixed_id >= mfs.size()) + return; + + auto *editor = new MixedFilamentConfigPanel(editor_host, mixed_id, mfs[mixed_id], num_physical, physical_colors, nozzle_diameters, palette, preview_settings, + component_bias_enabled, + [this, mixed_id, swatch, summary_label, header_panel, row, rows_scroller, mixed_summary_text, apply_mixed_entry_changes](const MixedFilament &updated_mf) { + apply_mixed_entry_changes(mixed_id, updated_mf, true); + + if (swatch) { + swatch->SetBackgroundColour(parse_mixed_color(updated_mf.display_color)); + swatch->Refresh(); + } + if (summary_label) { + summary_label->SetLabel(mixed_summary_text(updated_mf)); + } + if (header_panel) + header_panel->Layout(); + if (row) + row->Layout(); + if (rows_scroller) { + rows_scroller->Layout(); + rows_scroller->FitInside(); + } + }); + + editor_sizer->Add(editor, 0, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, FromDIP(4)); + editor_host->Layout(); + }; + + auto toggle_editor = [this, mixed_id, editor_host, ensure_editor, rows_scroller, adjust_rows_scroller_height]() { + if (!editor_host || !rows_scroller) + return; + + if (editor_host->IsShown()) { + editor_host->Hide(); + p->m_expanded_mixed_filament_rows.erase(mixed_id); + } else { + ensure_editor(); + editor_host->Show(); + p->m_expanded_mixed_filament_rows.insert(mixed_id); + } + + rows_scroller->Layout(); + rows_scroller->FitInside(); + adjust_rows_scroller_height(); + p->m_panel_mixed_filaments_content->Layout(); + m_scrolled_sizer->Layout(); + Layout(); + }; + + auto bind_toggle_target = [&toggle_editor](wxWindow *target) { + if (!target) + return; + target->SetCursor(wxCursor(wxCURSOR_HAND)); + target->Bind(wxEVT_LEFT_UP, [toggle_editor](wxMouseEvent &) { + toggle_editor(); + }); + }; + + auto bind_hover_target = [set_row_hover, row_contains_mouse](wxWindow *target) { + if (!target) + return; + target->Bind(wxEVT_ENTER_WINDOW, [set_row_hover](wxMouseEvent &evt) { + set_row_hover(true); + evt.Skip(); + }); + target->Bind(wxEVT_LEAVE_WINDOW, [set_row_hover, row_contains_mouse](wxMouseEvent &evt) { + set_row_hover(row_contains_mouse()); + evt.Skip(); + }); + }; + + auto release_drag_capture = [this]() { + p->m_mixed_filament_drag_active = false; + p->m_mixed_filament_drag_source_mixed_id = size_t(-1); + }; + + auto bind_drag_target = [this, + mixed_id, + &mixed, + drop_insert_position, + current_mixed_filament_ui_order, + release_drag_capture](wxWindow *target) { + if (!target) + return; + + target->Bind(wxEVT_LEFT_DOWN, [this, mixed_id, target](wxMouseEvent &evt) { + if (!target) + return; + p->m_mixed_filament_drag_active = true; + p->m_mixed_filament_drag_source_mixed_id = mixed_id; + if (!target->HasCapture()) + target->CaptureMouse(); + evt.StopPropagation(); + }); + + target->Bind(wxEVT_MOTION, [this](wxMouseEvent &evt) { + if (p->m_mixed_filament_drag_active) + evt.StopPropagation(); + }); + + target->Bind(wxEVT_LEFT_UP, [this, &mixed, target, drop_insert_position, current_mixed_filament_ui_order](wxMouseEvent &evt) { + if (target && target->HasCapture()) + target->ReleaseMouse(); + + if (!p->m_mixed_filament_drag_active || p->m_mixed_filament_drag_source_mixed_id >= mixed.size()) { + p->m_mixed_filament_drag_active = false; + p->m_mixed_filament_drag_source_mixed_id = size_t(-1); + evt.StopPropagation(); + return; + } + + const size_t source_mixed_id = p->m_mixed_filament_drag_source_mixed_id; + p->m_mixed_filament_drag_active = false; + p->m_mixed_filament_drag_source_mixed_id = size_t(-1); + + std::vector current_mixed_ids; + current_mixed_ids.reserve(p->m_mixed_filament_row_bindings.size()); + for (const auto &binding : p->m_mixed_filament_row_bindings) { + if (binding.mixed_id < mixed.size() && !mixed[binding.mixed_id].deleted) + current_mixed_ids.emplace_back(binding.mixed_id); + } + + const auto source_it = std::find(current_mixed_ids.begin(), current_mixed_ids.end(), source_mixed_id); + if (source_it == current_mixed_ids.end()) { + evt.StopPropagation(); + return; + } + + const size_t source_pos = size_t(std::distance(current_mixed_ids.begin(), source_it)); + size_t insert_pos = drop_insert_position(); + insert_pos = std::min(insert_pos, current_mixed_ids.size()); + + current_mixed_ids.erase(source_it); + if (insert_pos > source_pos) + --insert_pos; + insert_pos = std::min(insert_pos, current_mixed_ids.size()); + current_mixed_ids.insert(current_mixed_ids.begin() + ptrdiff_t(insert_pos), source_mixed_id); + + std::vector reordered_stable_ids; + reordered_stable_ids.reserve(current_mixed_ids.size()); + for (const size_t row_mixed_id : current_mixed_ids) { + if (row_mixed_id < mixed.size() && mixed[row_mixed_id].stable_id != 0) + reordered_stable_ids.emplace_back(mixed[row_mixed_id].stable_id); + } + + if (reordered_stable_ids != current_mixed_filament_ui_order()) { + p->m_mixed_filament_ui_order = std::move(reordered_stable_ids); + update_mixed_filament_panel(false); + } + + evt.StopPropagation(); + }); + + target->Bind(wxEVT_MOUSE_CAPTURE_LOST, [release_drag_capture](wxMouseCaptureLostEvent &) { + release_drag_capture(); + }); + }; + + header_panel->SetToolTip(auto_row ? + _L("Click to edit automatic mixed filament settings (saved as custom).") : + _L("Click to expand/retract mixed filament settings")); + bind_toggle_target(row); + bind_toggle_target(header_panel); + bind_toggle_target(name_label); + bind_toggle_target(summary_label); + bind_toggle_target(swatch); + bind_hover_target(row); + bind_hover_target(header_panel); + bind_hover_target(name_label); + bind_hover_target(summary_label); + bind_hover_target(swatch); + bind_hover_target(drag_handle); + bind_drag_target(drag_handle); + + del_btn->Bind(wxEVT_LEFT_UP, [](wxMouseEvent &evt) { + evt.StopPropagation(); + evt.Skip(); + }); + + if (p->m_expanded_mixed_filament_rows.count(mixed_id) != 0) { + ensure_editor(); + editor_host->Show(); + } + + row->SetSizer(row_sizer); + rows_sizer->Add(row, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, FromDIP(2)); + rows_sizer->AddSpacer(FromDIP(2)); + } + + rows_sizer->AddSpacer(FromDIP(2)); + rows_scroller->Layout(); + rows_scroller->FitInside(); + adjust_rows_scroller_height(); + if (prev_rows_view_y > 0) + rows_scroller->Scroll(0, prev_rows_view_y); + + content_sizer->Add(rows_scroller, 0, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(2)); + content_sizer->AddSpacer(FromDIP(2)); + p->m_panel_mixed_filaments_content->Layout(); + m_scrolled_sizer->Layout(); + Layout(); + refresh_model_canvas_colors(); + update_color_mix_panel(); +#endif // Mixed Filaments panel UI +} + +std::vector Sidebar::get_ui_ordered_filament_ids() const +{ + const size_t num_physical = static_cast(std::max(wxGetApp().filaments_cnt(), 0)); + std::vector ordered_filament_ids; + ordered_filament_ids.reserve(num_physical); + for (size_t idx = 0; idx < num_physical; ++idx) + ordered_filament_ids.emplace_back(unsigned(idx + 1)); + + if (wxGetApp().preset_bundle == nullptr) + return ordered_filament_ids; + + const auto &mixed = wxGetApp().preset_bundle->mixed_filaments.mixed_filaments(); + if (mixed.empty()) + return ordered_filament_ids; + + const std::vector ordered_mixed_indices = build_mixed_filament_ui_indices(mixed, p->m_mixed_filament_ui_order); + std::vector actual_filament_id_by_mixed_idx(mixed.size(), 0); + unsigned int next_filament_id = unsigned(num_physical + 1); + for (size_t mixed_idx = 0; mixed_idx < mixed.size(); ++mixed_idx) { + if (!mixed[mixed_idx].enabled || mixed[mixed_idx].deleted) + continue; + actual_filament_id_by_mixed_idx[mixed_idx] = next_filament_id++; + } + + ordered_filament_ids.reserve(size_t(next_filament_id - 1)); + for (const size_t mixed_idx : ordered_mixed_indices) { + if (mixed_idx >= actual_filament_id_by_mixed_idx.size()) + continue; + const unsigned int actual_filament_id = actual_filament_id_by_mixed_idx[mixed_idx]; + if (actual_filament_id != 0) + ordered_filament_ids.emplace_back(actual_filament_id); + } + + return ordered_filament_ids; +} + +void Sidebar::add_filament() { + if (p->combos_filament.size() >= MAXIMUM_EXTRUDER_NUMBER) return; + PresetBundle* pb = wxGetApp().preset_bundle; + if (!pb || pb->mixed_filaments.total_filaments(p->combos_filament.size()) >= MAXIMUM_FILAMENT_NUMBER) return; + wxColour new_col = Plater::get_next_color_for_filament(); + add_custom_filament(new_col); +} + +void Sidebar::on_filaments_delete(size_t filament_id) +{ + auto& choices = combos_filament(); + + p->m_skip_mixed_filament_sync_once = false; + + if (filament_id >= choices.size()) + return; + + if (choices.size() <= 2) + choices[0]->GetDropDown().Invalidate(); + + wxWindowUpdateLocker noUpdates_scrolled_panel(this); // delete UI item if (filament_id < p->combos_filament.size()) { @@ -2463,14 +7246,55 @@ void Sidebar::on_filaments_delete(size_t filament_id) sizer->Hide(p->m_flushing_volume_btn); } + if (p->m_bpButton_del_filament != nullptr && p->m_panel_physical_filaments_title != nullptr) { + auto* inner_sizer = p->m_panel_physical_filaments_title->GetSizer(); + if (inner_sizer) { + if (p->combos_filament.size() > 1) + inner_sizer->Show(p->m_bpButton_del_filament); + else + inner_sizer->Hide(p->m_bpButton_del_filament); + } + } + for (size_t idx = filament_id; idx < p->combos_filament.size(); ++idx) { p->combos_filament[idx]->update(); } + // Recalc scrolled filament window height (max 3 rows) + if (p->m_scrolled_filaments && p->m_panel_scrolled_filament_content) { + p->m_panel_scrolled_filament_content->Layout(); + const wxSize content_best = p->m_panel_scrolled_filament_content->GetBestSize(); + const int row_count = ((int)p->combos_filament.size() + 1) / 2; + const int desired_h = row_count > 3 + ? (content_best.GetHeight() / std::max(1, row_count)) * 3 + : content_best.GetHeight(); + p->m_scrolled_filaments->SetMinSize({-1, desired_h}); + p->m_scrolled_filaments->SetMaxSize({-1, desired_h}); + } + Layout(); + wxWeakRef weak_this(this); + wxTheApp->CallAfter([weak_this]() { + Sidebar* sidebar = weak_this.get(); + if (sidebar && sidebar->p && sidebar->p->m_scrolled_filaments) { + int vh = sidebar->p->m_scrolled_filaments->GetVirtualSize().y; + int ch = sidebar->p->m_scrolled_filaments->GetClientSize().y; + sidebar->p->m_scrolled_filaments->Scroll(0, std::max(0, vh - ch)); + } + }); p->m_panel_filament_title->Refresh(); update_ui_from_settings(); - dynamic_filament_list.update(); + update_dynamic_filament_list(); + update_mixed_filament_panel(); + update_color_mix_panel(); + + if (PresetBundle *pb = wxGetApp().preset_bundle) { + const bool can_add = pb->mixed_filaments.total_filaments(p->combos_filament.size()) < MAXIMUM_FILAMENT_NUMBER; + if (p->m_bpButton_add_filament) + p->m_bpButton_add_filament->Enable(can_add); + if (p->m_btn_add_color_mix) + p->m_btn_add_color_mix->Enable(can_add); + } } void Sidebar::edit_filament() { @@ -2480,31 +7304,198 @@ void Sidebar::edit_filament() { p->editing_filament = p->m_menu_filament_id; // sync with TabPresetComboxBox's m_filament_idx } -void Sidebar::update_color_mix_panel() +// Helper function: Check if target mixed filament depends on source physical filament +static bool mixed_filament_uses_physical(const MixedFilament* target_mf, unsigned int source_physical_1based) { - // Stub: mixed filament color panel not yet implemented in this version. -} + if (!target_mf) + return false; -void Sidebar::update_mixed_filament_panel(bool sync_manager) -{ - // Stub: mixed filament panel not yet implemented in this version. + // Check manual_pattern tokens (resolve order #1) + const std::string norm = MixedFilamentManager::normalize_manual_pattern(target_mf->manual_pattern); + if (!norm.empty()) { + const auto groups = MixedFilamentManager::split_pattern_groups(norm); + for (const std::string &group : groups) { + const auto tokens = MixedFilamentManager::split_pattern_group_to_tokens(group, 0); + for (const std::string &token : tokens) { + if (MixedFilamentManager::physical_filament_from_token(token, *target_mf, MixedFilamentManager::kMaxPhysicalFilaments) == source_physical_1based) + return true; + } + } + } + + // Check gradient components (resolve order #2). + // Only check when there is no manual_pattern; a pattern already resolves + // every token, so gradient IDs would be a false positive at worst. + if (norm.empty()) { + const std::vector ids = MixedFilamentManager::decode_gradient_component_ids(target_mf->gradient_component_ids, 0); + for (unsigned int id : ids) { + if (id == source_physical_1based) + return true; + } + } + + // Check if target mixed filament uses source physical filament as component + // (resolve order #3). Only reached when the mixed filament has no manual_pattern, + // because in cycle mode pattern tokens "1"/"2" already cover component_a/b. + if (norm.empty() && (target_mf->component_a == source_physical_1based || target_mf->component_b == source_physical_1based)) { + return true; + } + + return false; } -std::vector Sidebar::get_ui_ordered_filament_ids() const +void Sidebar::change_filament(size_t from_id, size_t to_id) { - const size_t num_physical = static_cast(std::max(wxGetApp().filaments_cnt(), 0)); - std::vector ordered_filament_ids; - ordered_filament_ids.reserve(num_physical); - for (size_t idx = 0; idx < num_physical; ++idx) - ordered_filament_ids.emplace_back(unsigned(idx + 1)); - return ordered_filament_ids; -} + // 1. Parameter preprocessing + if (from_id == size_t(-2)) + from_id = p->m_menu_filament_id; + if (from_id == size_t(-1)) + from_id = p->combos_filament.size() - 1; + if (from_id == to_id) + return; + auto& pb = *wxGetApp().preset_bundle; + const size_t num_physical = pb.filament_presets.size(); + // 2. Determine source and target types + // Note: filament IDs here are 0-based, but is_mixed expects 1-based + bool from_is_mixed = pb.mixed_filaments.is_mixed((unsigned int)(from_id + 1), num_physical); + bool to_is_mixed = pb.mixed_filaments.is_mixed((unsigned int)(to_id + 1), num_physical); -void Sidebar::change_filament(size_t from_id, size_t to_id) + // 3. Dependency check: physical → mixed + if (!from_is_mixed && to_is_mixed) { + const MixedFilament* target_mf = pb.mixed_filaments.mixed_filament_from_id((unsigned int)(to_id + 1), num_physical); + unsigned int from_1based = (unsigned int)(from_id + 1); + + if (mixed_filament_uses_physical(target_mf, from_1based)) { + MessageDialog dlg(wxGetApp().plater(), + _L("The target mixed filament uses this physical filament as a component. " + "Merging will remove this physical filament and may invalidate the mixed filament. Continue?"), + _L("Warning"), wxOK | wxCANCEL | wxICON_WARNING); + int ret = dlg.ShowModal(); + if (ret != wxID_OK) + return; + } + } + + // 3b. Check for mixed filaments that depend on the source physical filament + if (!from_is_mixed) { + unsigned int from_1based = (unsigned int)(from_id + 1); + std::vector dependent_mixed_indices = pb.mixed_filaments.mixed_filaments_using_physical(from_1based); + + // If there are dependent mixed filaments, show warning dialog + if (!dependent_mixed_indices.empty()) { + wxString msg = _L("This filament is used in the following mixed filament configurations:\n\n"); + + const auto& mfs = pb.mixed_filaments.mixed_filaments(); + size_t visible_idx = 0; + for (size_t j = 0; j < mfs.size(); ++j) { + if (mfs[j].deleted) continue; + + // Check if this is one of the dependent mixed filaments + bool is_dependent = std::find(dependent_mixed_indices.begin(), dependent_mixed_indices.end(), j) + != dependent_mixed_indices.end(); + + if (is_dependent) { + const int virtual_id = static_cast(num_physical) + visible_idx + 1; + msg += wxString::Format(_L("• Mixed Filament %d\n"), virtual_id); + } + + visible_idx++; + } + + msg += _L("\nMerging this filament will invalidate these mixed filament configurations. Continue?"); + + MessageDialog dlg(wxGetApp().plater(), msg, _L("Warning"), wxOK | wxCANCEL | wxICON_WARNING); + int ret = dlg.ShowModal(); + if (ret != wxID_OK) + return; + } + } + + // 4. Execute merge based on scenario + if (from_is_mixed) { + // Mixed → Physical or Mixed → Mixed + merge_mixed_filament(from_id, to_id); + } else { + // Physical → Physical or Physical → Mixed + delete_filament(from_id, int(to_id)); + } +} + +void Sidebar::merge_mixed_filament(size_t from_id, size_t to_id) { - delete_filament(from_id, int(to_id)); + // Merge a mixed filament into another filament (physical or mixed) + // This marks the source as deleted and remaps all objects using it + + PresetBundle* preset_bundle = wxGetApp().preset_bundle; + if (!preset_bundle) return; + auto& pb = *preset_bundle; + const size_t num_physical = pb.filament_presets.size(); + + // Validate parameters + size_t total_filaments = pb.mixed_filaments.total_filaments(num_physical); + if (from_id >= total_filaments || to_id >= total_filaments) { + BOOST_LOG_TRIVIAL(error) << "merge_mixed_filament: Invalid filament ID. from_id=" + << from_id << " to_id=" << to_id << " total=" << total_filaments; + return; + } + + // Prevent self-merge + if (from_id == to_id) { + BOOST_LOG_TRIVIAL(warning) << "merge_mixed_filament: Cannot merge filament into itself: " << from_id; + return; + } + + // Verify source is actually a mixed filament + if (!pb.mixed_filaments.is_mixed((unsigned int)(from_id + 1), num_physical)) { + BOOST_LOG_TRIVIAL(error) << "merge_mixed_filament: Source filament " << from_id + << " is not a mixed filament"; + return; + } + + // Get source mixed filament index + int source_mixed_idx = pb.mixed_filaments.mixed_index_from_filament_id((unsigned int)(from_id + 1), num_physical); + if (source_mixed_idx < 0) { + BOOST_LOG_TRIVIAL(error) << "merge_mixed_filament: Cannot find mixed filament index for ID " << from_id; + return; + } + + auto& mfs = pb.mixed_filaments.mixed_filaments(); + if ((size_t)source_mixed_idx >= mfs.size()) { + BOOST_LOG_TRIVIAL(error) << "merge_mixed_filament: Mixed filament index " << source_mixed_idx + << " out of range (size=" << mfs.size() << ")"; + return; + } + + BOOST_LOG_TRIVIAL(info) << "Merging mixed filament " << from_id << " into filament " << to_id; + + // Build remap table using PresetBundle method + pb.build_merge_filament_remap(from_id, to_id, total_filaments); + + // Mark source mixed filament as deleted + mfs[source_mixed_idx].deleted = true; + mfs[source_mixed_idx].enabled = false; + + // Persist changes + if (auto* opt = pb.project_config.option("mixed_filament_definitions")) + opt->value = pb.mixed_filaments.serialize_custom_entries(); + + // Save mixed snapshot + std::vector is_mixed_snapshot; + if (auto* opt = pb.project_config.option("filament_is_mixed")) + is_mixed_snapshot = opt->values; + + // Update objects to use new filament IDs + size_t total_after = pb.mixed_filaments.total_filaments(num_physical); + wxGetApp().plater()->on_filaments_delete(total_after, from_id, -1, is_mixed_snapshot); + + BOOST_LOG_TRIVIAL(info) << "Mixed filament merge completed. Total filaments after: " << total_after; + + // Update UI + update_color_mix_panel(); + m_scrolled_sizer->Layout(); + wxGetApp().plater()->update(); } void Sidebar::delete_filament(size_t filament_id, int replace_filament_id) @@ -2520,25 +7511,149 @@ void Sidebar::delete_filament(size_t filament_id, int replace_filament_id) filament_id = filament_count; } - if (filament_id > filament_count) + size_t total_filaments = wxGetApp().preset_bundle->filament_presets.size(); + if (filament_id > filament_count && filament_id >= total_filaments) return; - if (wxGetApp().preset_bundle->is_the_only_edited_filament(filament_id) || (filament_id == 0)) { - wxGetApp().get_tab(Preset::TYPE_FILAMENT)->select_preset(wxGetApp().preset_bundle->filament_presets[0], false, "", true); + bool is_mixed = (filament_id >= p->combos_filament.size()); + + // Check for mixed filaments that depend on the source physical filament (before deletion) + // Skip this dialog when called from change_filament() (merge path), since change_filament() + // already showed its own confirmation dialog for the same dependent mixed filaments. + if (!is_mixed && replace_filament_id < 0) { + auto& pb = *wxGetApp().preset_bundle; + const size_t num_physical = pb.filament_presets.size(); + unsigned int filament_1based = (unsigned int)(filament_id + 1); + std::vector dependent_mixed_indices = pb.mixed_filaments.mixed_filaments_using_physical(filament_1based); + + // If there are dependent mixed filaments, show warning dialog + if (!dependent_mixed_indices.empty()) { + wxString msg = _L("This filament is used in the following mixed filament configurations:\n\n"); + + const auto& mfs = pb.mixed_filaments.mixed_filaments(); + size_t visible_idx = 0; + for (size_t j = 0; j < mfs.size(); ++j) { + if (mfs[j].deleted) continue; + + // Check if this is one of the dependent mixed filaments + bool is_dependent = std::find(dependent_mixed_indices.begin(), dependent_mixed_indices.end(), j) + != dependent_mixed_indices.end(); + + if (is_dependent) { + const int virtual_id = static_cast(num_physical) + visible_idx + 1; + msg += wxString::Format(_L("• Mixed Filament %d\n"), virtual_id); + } + + visible_idx++; + } + + msg += _L("\nDeleting this filament will invalidate these mixed filament configurations. Continue?"); + + MessageDialog dlg(wxGetApp().plater(), msg, _L("Warning"), wxOK | wxCANCEL | wxICON_WARNING); + int ret = dlg.ShowModal(); + if (ret != wxID_OK) + return; + } } - if (p->editing_filament == filament_id || p->editing_filament >= filament_count) { - p->editing_filament = -1; + PresetBundle* preset_bundle = wxGetApp().preset_bundle; + if (!preset_bundle) return; + + if (!is_mixed) { + if (preset_bundle->is_the_only_edited_filament(filament_id) || (filament_id == 0)) { + wxGetApp().get_tab(Preset::TYPE_FILAMENT)->select_preset(preset_bundle->filament_presets[0], false, "", true); + } + if (p->editing_filament == filament_id || p->editing_filament >= filament_count) { + p->editing_filament = -1; + } } - wxGetApp().preset_bundle->update_num_filaments(filament_id); - wxGetApp().plater()->get_partplate_list().on_filament_deleted(filament_count, filament_id); - + std::vector is_mixed_snapshot; + if (auto* opt = preset_bundle->project_config.option("filament_is_mixed")) + is_mixed_snapshot = opt->values; + + auto& pb = *preset_bundle; + size_t old_num_physical = pb.filament_presets.size(); + size_t old_total_filaments = pb.mixed_filaments.total_filaments(old_num_physical); + + // Check if this is a physical filament being merged into a mixed filament + bool is_physical_to_mixed_merge = !is_mixed && replace_filament_id >= (int)old_num_physical; + + // Check if target mixed filament depends on source physical filament + bool target_depends_on_source = false; + + if (is_physical_to_mixed_merge) { + // Get target mixed filament + const MixedFilament* target_mf = pb.mixed_filaments.mixed_filament_from_id( + (unsigned int)(replace_filament_id + 1), old_num_physical); + unsigned int source_1based = (unsigned int)(filament_id + 1); + + // Use helper function to check dependency + target_depends_on_source = mixed_filament_uses_physical(target_mf, source_1based); + + BOOST_LOG_TRIVIAL(info) << "Physical to mixed merge: source=" << filament_id + << " target=" << replace_filament_id + << " target_depends_on_source=" << target_depends_on_source; + } + + // Case 2: If target mixed doesn't depend on source physical, use remap mechanism + // This is similar to merge_mixed_filament() - we use a remap table to redirect objects + // to the target mixed filament, then delete the physical filament + if (is_physical_to_mixed_merge && !target_depends_on_source) { + // Build custom remap table that accounts for physical filament deletion + // When a physical filament is deleted, mixed filament virtual IDs change + pb.build_merge_filament_remap(filament_id, replace_filament_id, old_total_filaments, old_num_physical); + + BOOST_LOG_TRIVIAL(info) << "Built custom remap for physical to mixed merge (accounts for virtual ID changes)"; + + // Call on_filaments_delete with -1 to trigger remap usage + // This updates object colors using the remap table + wxGetApp().plater()->on_filaments_delete(old_total_filaments, filament_id, -1, is_mixed_snapshot); + + // Now delete the physical filament + pb.update_num_filaments(filament_id); + pb.consume_last_filament_id_remap(); // discard the remap built by update_num_filaments + wxGetApp().plater()->get_partplate_list().on_filament_deleted( + pb.filament_presets.size(), filament_id); + + BOOST_LOG_TRIVIAL(info) << "Physical to mixed merge completed using custom remap mechanism"; + + // Update UI + wxGetApp().get_tab(Preset::TYPE_PRINT)->update(); + wxGetApp().preset_bundle->export_selections(*wxGetApp().app_config); + wxGetApp().plater()->update(); + return; + } + // Case 1: If target mixed depends on source physical, delete the physical filament + // Let on_filaments_delete use default behavior (reset to default) + else if (is_physical_to_mixed_merge && target_depends_on_source) { + BOOST_LOG_TRIVIAL(info) << "Physical to mixed merge: target depends on source, will delete physical and reset to default"; + } + + // Delete the physical filament (this also removes mixed filaments that depend on it) + pb.update_num_filaments(filament_id); + size_t new_num_physical = pb.filament_presets.size(); + + size_t total_after_delete = pb.mixed_filaments.total_filaments(new_num_physical); + wxGetApp().plater()->get_partplate_list().on_filament_deleted(total_after_delete, filament_id); + + int final_replace_id; + if (is_physical_to_mixed_merge && target_depends_on_source) { + // Case 1: target mixed was also deleted by remove_physical_filament + final_replace_id = -1; + } else { + int adjusted = replace_filament_id; + if (replace_filament_id >= (int)old_num_physical) + adjusted = int(new_num_physical + (size_t(replace_filament_id) - old_num_physical)); + else if (replace_filament_id > (int)filament_id) + adjusted = replace_filament_id - 1; + final_replace_id = adjusted; + } + + wxGetApp().plater()->on_filaments_delete(total_after_delete, filament_id, + final_replace_id, + is_mixed_snapshot); - // wxGetApp().plater()->on_filaments_change(filament_count); - wxGetApp().plater()->on_filaments_delete(filament_count, filament_id, - replace_filament_id > (int) filament_id ? (replace_filament_id - 1) : replace_filament_id); - // wxGetApp().plater()->sidebar().on_filaments_delete(filament_id); wxGetApp().get_tab(Preset::TYPE_PRINT)->update(); wxGetApp().preset_bundle->export_selections(*wxGetApp().app_config); @@ -2547,13 +7662,16 @@ void Sidebar::delete_filament(size_t filament_id, int replace_filament_id) void Sidebar::add_custom_filament(wxColour new_col) { if (p->combos_filament.size() >= MAXIMUM_EXTRUDER_NUMBER) return; + PresetBundle* pb = wxGetApp().preset_bundle; + if (!pb || pb->mixed_filaments.total_filaments(p->combos_filament.size()) >= MAXIMUM_FILAMENT_NUMBER) return; int filament_count = p->combos_filament.size() + 1; + wxGetApp().plater()->confirm_auto_generated_gradients(filament_count); std::string new_color = new_col.GetAsString(wxC2S_HTML_SYNTAX).ToStdString(); - wxGetApp().preset_bundle->set_num_filaments(filament_count, new_color); + pb->set_num_filaments(filament_count, new_color); wxGetApp().plater()->on_filaments_change(filament_count); wxGetApp().get_tab(Preset::TYPE_PRINT)->update(); - wxGetApp().preset_bundle->export_selections(*wxGetApp().app_config); + pb->export_selections(*wxGetApp().app_config); auto_calc_flushing_volumes(filament_count - 1); } @@ -3126,7 +8244,7 @@ void Sidebar::auto_calc_flushing_volumes(const int modify_id) int m_max_flush_volume = Slic3r::g_max_flush_volume; unsigned int m_number_of_extruders = (int)(sqrt(init_matrix.size()) + 0.001); - const std::vector extruder_colours = wxGetApp().plater()->get_extruder_colors_from_plater_config(); + const std::vector extruder_colours = wxGetApp().plater()->get_extruder_colors_from_plater_config(nullptr, false); std::vector> multi_colours; // Support for multi-color filament @@ -3394,9 +8512,13 @@ struct Plater::priv static const std::regex pattern_prusa; bool m_is_dark = false; + size_t m_last_auto_gradient_prompt_physical_count = 0; + bool m_last_auto_gradient_prompt_accepted = false; priv(Plater *q, MainFrame *main_frame); ~priv(); + bool confirm_auto_generated_gradients(wxWindow *parent, size_t num_physical); + void set_auto_generated_gradient_decision(size_t num_physical, bool create_auto_gradients); bool need_update() const { return m_need_update; } @@ -3867,7 +8989,7 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame) "extruder_colour", "filament_colour", "material_colour", "printable_height", "printer_model", "printer_technology", // These values are necessary to construct SlicingParameters by the Canvas3D variable layer height editor. "layer_height", "initial_layer_print_height", "min_layer_height", "max_layer_height", - "brim_width", "wall_loops", "wall_filament", "sparse_infill_density", "sparse_infill_filament", "top_shell_layers", + "brim_width", "wall_loops", "wall_filament", "sparse_infill_density", "enable_infill_filament_override", "infill_filament_use_base_first_layers", "infill_filament_use_base_last_layers", "sparse_infill_filament", "top_shell_layers", "enable_support", "support_filament", "support_interface_filament", "support_top_z_distance", "support_bottom_z_distance", "raft_layers", "wipe_tower_rotation_angle", "wipe_tower_cone_angle", "wipe_tower_extra_spacing", "wipe_tower_extra_flow", "local_z_wipe_tower_purge_lines", "wipe_tower_max_purge_speed", @@ -4849,6 +9971,32 @@ std::vector Plater::priv::load_files(const std::vector& input_ << boost::format(", plate_data.size %1%, project_preset.size %2%, is_bbs_3mf %3%, file_version %4% \n") % plate_data.size() % project_presets.size() % (en_3mf_file_type == En3mfType::From_BBS) % file_version.to_string(); + auto imported_string_count = [&config_loaded](const char *key) -> size_t { + if (const auto *opt = config_loaded.option(key)) + return opt->values.size(); + return 0; + }; + auto imported_float_count = [&config_loaded](const char *key) -> size_t { + if (const auto *opt = config_loaded.option(key)) + return opt->values.size(); + return 0; + }; + + std::vector imported_filament_colors; + size_t imported_physical_filaments = 0; + if (const auto *filament_colors_opt = config_loaded.option("filament_colour")) { + imported_filament_colors = filament_colors_opt->values; + imported_physical_filaments = imported_filament_colors.size(); + } + if (imported_physical_filaments == 0) + imported_physical_filaments = imported_string_count("filament_settings_id"); + if (imported_physical_filaments == 0) + imported_physical_filaments = imported_string_count("filament_ids"); + if (imported_physical_filaments == 0) + imported_physical_filaments = imported_string_count("default_filament_colour"); + if (imported_physical_filaments == 0) + imported_physical_filaments = imported_float_count("nozzle_diameter"); + // 1. add extruder for prusa model if the number of existing extruders is not enough // 2. add extruder for BBS or Other model if only import geometry if (en_3mf_file_type == En3mfType::From_Prusa || (load_model && !load_config)) { @@ -4861,10 +10009,66 @@ std::vector Plater::priv::load_files(const std::vector& input_ } } int size = extruderIds.size() == 0 ? 0 : *(extruderIds.rbegin()); + const bool geometry_only_project_import = load_model && !load_config && imported_physical_filaments > 0; + const size_t desired_physical_filaments = geometry_only_project_import ? + std::min({imported_physical_filaments, size_t(MAXIMUM_EXTRUDER_NUMBER), size_t(MAXIMUM_FILAMENT_NUMBER)}) : 0; + BOOST_LOG_TRIVIAL(info) << "3MF geometry import filament detection" + << " imported_physical=" << imported_physical_filaments + << " imported_colors=" << imported_filament_colors.size() + << " config_filament_settings_id=" << imported_string_count("filament_settings_id") + << " config_filament_ids=" << imported_string_count("filament_ids") + << " config_default_filament_colour=" << imported_string_count("default_filament_colour") + << " config_nozzle_diameter=" << imported_float_count("nozzle_diameter") + << " model_max_extruder=" << size + << " geometry_only_project_import=" << (geometry_only_project_import ? 1 : 0); + if (geometry_only_project_import) + size = int(desired_physical_filaments); + + PresetBundle *preset_bundle = wxGetApp().preset_bundle; + if (geometry_only_project_import && preset_bundle != nullptr) { + const size_t current_num_filaments = preset_bundle->filament_presets.size(); + const bool current_project_empty = this->model.objects.empty(); + if (current_project_empty) { + static const t_config_option_keys imported_project_option_keys = { + "filament_colour", + "mixed_filament_definitions", + "mixed_filament_gradient_mode", + "mixed_filament_height_lower_bound", + "mixed_filament_height_upper_bound", + "mixed_filament_advanced_dithering", + "mixed_filament_pointillism_pixel_size", + "mixed_filament_pointillism_line_gap", + "mixed_filament_component_bias_enabled", + "mixed_filament_surface_indentation" + }; + preset_bundle->project_config.apply_only(config_loaded, imported_project_option_keys, true); + if (current_num_filaments != desired_physical_filaments) { + q->confirm_auto_generated_gradients(desired_physical_filaments); + preset_bundle->set_num_filaments(unsigned(desired_physical_filaments)); + } else + preset_bundle->update_multi_material_filament_presets(); + BOOST_LOG_TRIVIAL(info) << "3MF geometry import applied imported project config" + << " current_num_filaments=" << current_num_filaments + << " desired_physical_filaments=" << desired_physical_filaments + << " mixed_enabled=" << preset_bundle->mixed_filaments.enabled_count(); + wxGetApp().plater()->on_filaments_change(desired_physical_filaments); + } else if (current_num_filaments < desired_physical_filaments) { + std::vector new_colors; + if (imported_filament_colors.size() > current_num_filaments) { + new_colors.assign(imported_filament_colors.begin() + current_num_filaments, + imported_filament_colors.begin() + desired_physical_filaments); + } + q->confirm_auto_generated_gradients(desired_physical_filaments); + preset_bundle->set_num_filaments(unsigned(desired_physical_filaments), new_colors); + wxGetApp().plater()->on_filaments_change(desired_physical_filaments); + } + } int filament_size = sidebar->combos_filament().size(); - while (filament_size < MAXIMUM_EXTRUDER_NUMBER && filament_size < size) { + const int filament_limit = std::min({size, int(MAXIMUM_EXTRUDER_NUMBER), int(MAXIMUM_FILAMENT_NUMBER)}); + while (filament_size < filament_limit) { int filament_count = filament_size + 1; + wxGetApp().plater()->confirm_auto_generated_gradients(filament_count); wxColour new_col = Plater::get_next_color_for_filament(); std::string new_color = new_col.GetAsString(wxC2S_HTML_SYNTAX).ToStdString(); wxGetApp().preset_bundle->set_num_filaments(filament_count, new_color); @@ -5204,6 +10408,10 @@ std::vector Plater::priv::load_files(const std::vector& input_ // BBS: add preset combo box re-active logic // currently found only needs re-active here wxGetApp().load_current_presets(false, false); + // Some preset-tab refresh paths rebuild printer/filament UI from the + // active presets but do not preserve the mixed manager instance. + // Rebuild it explicitly from project_config before clamping object IDs. + preset_bundle->update_multi_material_filament_presets(); // Update filament colors for the MM-printer profile in the full config // to avoid black (default) colors for Extruders in the ObjectList, // when for extruder colors are used filament colors @@ -6028,8 +11236,6 @@ void Plater::priv::object_list_changed() main_frame->update_slice_print_status(MainFrame::eEventObjectUpdate, can_slice); wxGetApp().params_panel()->notify_object_config_changed(); - - q->sync_filament_temp_mixing_notification(); } void Plater::priv::select_curr_plate_all() @@ -6526,18 +11732,21 @@ unsigned int Plater::priv::update_background_process(bool force_validation, bool if (err.string.empty()) { // Validate passed, but also check filament temp mixing as a final - // guard. The Print::validate() extruders() check may miss some - // cases (e.g. wall_filament changes that haven't propagated to - // PrintRegions yet). check_filament_temp_mixing() reads directly - // from full_config() and is always current. + // guard. check_filament_temp_mixing() reads directly from preset + // configs and catches cases that Print::validate() may miss (e.g. + // wall_filament changes that haven't propagated to PrintRegions yet). bool filament_ok = q->sync_filament_temp_mixing_notification(); if (filament_ok) { + this->partplate_list.get_curr_plate()->update_apply_result_invalid(false); notification_manager->set_all_slicing_errors_gray(true); - process_validation_warning(warning); + notification_manager->close_notification_of_type(NotificationType::ValidateError); } if (invalidated != Print::APPLY_STATUS_UNCHANGED && background_processing_enabled()) return_state |= UPDATE_BACKGROUND_PROCESS_RESTART; + // Pass a warning from validation and either show a notification, + // or hide the old one. + process_validation_warning(warning); if (printer_technology == ptFFF) { view3D->get_canvas3d()->reset_sequential_print_clearance(); view3D->get_canvas3d()->set_as_dirty(); @@ -7447,7 +12656,8 @@ void Plater::priv::set_current_panel(wxPanel* panel, bool no_slice) BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": from set_current_panel, no_slice %1%, export_in_progress %2%, model_fits %3%, m_is_slicing %4%")%no_slice%export_in_progress%model_fits%m_is_slicing; - if (!no_slice && !this->model.objects.empty() && !export_in_progress && model_fits && current_has_print_instances) + if (!no_slice && !this->model.objects.empty() && !export_in_progress && model_fits && current_has_print_instances + && !current_plate->is_slice_result_valid()) { //if already running in background, not relice here //BBS: add more judge for slicing @@ -7853,6 +13063,7 @@ void Plater::priv::on_select_preset(wxCommandEvent &evt) wxGetApp().plater()->update_project_dirty_from_presets(); wxGetApp().preset_bundle->export_selections(*wxGetApp().app_config); sidebar->update_dynamic_filament_list(); + sidebar->update_color_mix_panel(); bool flag_is_change = is_support_filament(idx); if (flag != flag_is_change) { sidebar->auto_calc_flushing_volumes(idx); @@ -8434,15 +13645,6 @@ void Plater::priv::on_action_open_project(SimpleEvent&) void Plater::priv::on_action_slice_plate(SimpleEvent&) { if (q != nullptr) { - // Pre-slice confirmation for high/low temp filament mixing - if (wxGetApp().app_config->get_bool("allow_filament_temp_mixing") - && !q->check_filament_temp_mixing()) { - wxMessageDialog dlg(q, _L("该组合可能导致风险,是否继续"), - _L("确认"), wxYES_NO | wxICON_WARNING); - if (dlg.ShowModal() != wxID_YES) - return; - } - BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << ":received slice plate event\n" ; //BBS update extruder params and speed table before slicing const Slic3r::DynamicPrintConfig& config = wxGetApp().preset_bundle->full_config(); @@ -8453,15 +13655,7 @@ void Plater::priv::on_action_slice_plate(SimpleEvent&) Model::setExtruderParams(config, numExtruders); Model::setPrintSpeedTable(config, print_config); m_slice_all = false; - q->reslice(); - q->select_view_3D("Preview"); - } -} -//BBS: GUI refactor: slice all -void Plater::priv::on_action_slice_all(SimpleEvent&) -{ - if (q != nullptr) { // Pre-slice confirmation for high/low temp filament mixing if (wxGetApp().app_config->get_bool("allow_filament_temp_mixing") && !q->check_filament_temp_mixing()) { @@ -8471,6 +13665,15 @@ void Plater::priv::on_action_slice_all(SimpleEvent&) return; } + q->reslice(); + q->select_view_3D("Preview"); + } +} + +//BBS: GUI refactor: slice all +void Plater::priv::on_action_slice_all(SimpleEvent&) +{ + if (q != nullptr) { BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << ":received slice project event\n" ; //BBS update extruder params and speed table before slicing const Slic3r::DynamicPrintConfig& config = wxGetApp().preset_bundle->full_config(); @@ -8480,6 +13683,16 @@ void Plater::priv::on_action_slice_all(SimpleEvent&) Model::setExtruderParams(config, numExtruders); Model::setPrintSpeedTable(config, print_config); + + // Pre-slice confirmation for high/low temp filament mixing + if (wxGetApp().app_config->get_bool("allow_filament_temp_mixing") + && !q->check_filament_temp_mixing()) { + wxMessageDialog dlg(q, _L("该组合可能导致风险,是否继续"), + _L("确认"), wxYES_NO | wxICON_WARNING); + if (dlg.ShowModal() != wxID_YES) + return; + } + m_slice_all = true; m_slice_all_only_has_gcode = true; m_cur_slice_plate = 0; @@ -8744,6 +13957,76 @@ void Plater::priv::on_repair_model(wxCommandEvent &event) wxGetApp().obj_list()->fix_through_netfabb(); } +bool Plater::priv::confirm_auto_generated_gradients(wxWindow *parent, size_t num_physical) +{ + auto *app_config = wxGetApp().app_config; + if (app_config == nullptr) + return MixedFilamentManager::auto_generate_enabled(); + + const bool pref_enabled = app_config->get_bool("auto_generate_gradients"); + if (!pref_enabled) { + m_last_auto_gradient_prompt_physical_count = 0; + m_last_auto_gradient_prompt_accepted = false; + MixedFilamentManager::set_auto_generate_enabled(false); + return false; + } + + if (num_physical <= 4) { + m_last_auto_gradient_prompt_physical_count = 0; + m_last_auto_gradient_prompt_accepted = false; + MixedFilamentManager::set_auto_generate_enabled(true); + return true; + } + + if (parent == nullptr || !parent->IsShownOnScreen()) { + m_last_auto_gradient_prompt_physical_count = 0; + m_last_auto_gradient_prompt_accepted = false; + MixedFilamentManager::set_auto_generate_enabled(true); + return true; + } + + if (m_last_auto_gradient_prompt_physical_count == num_physical) { + MixedFilamentManager::set_auto_generate_enabled(m_last_auto_gradient_prompt_accepted); + return m_last_auto_gradient_prompt_accepted; + } + + const size_t auto_gradient_count = num_physical * (num_physical - 1) / 2; + const wxString message = wxString::Format( + _L("Using %d physical filaments will create %d auto-generated gradients.\nDo you want to create them now?"), + int(num_physical), + int(auto_gradient_count)); + const int result = MessageDialog(parent, + message, + wxString(SLIC3R_APP_FULL_NAME) + " - " + _L("Auto gradients"), + wxYES_NO | wxYES_DEFAULT | wxCENTRE | wxICON_QUESTION) + .ShowModal(); + const bool accepted = result == wxID_YES; + m_last_auto_gradient_prompt_physical_count = num_physical; + m_last_auto_gradient_prompt_accepted = accepted; + MixedFilamentManager::set_auto_generate_enabled(accepted); + return accepted; +} + +void Plater::priv::set_auto_generated_gradient_decision(size_t num_physical, bool create_auto_gradients) +{ + m_last_auto_gradient_prompt_physical_count = num_physical; + m_last_auto_gradient_prompt_accepted = create_auto_gradients; + MixedFilamentManager::set_auto_generate_enabled(create_auto_gradients); +} + +bool Plater::confirm_auto_generated_gradients(size_t num_physical) +{ + return p != nullptr ? p->confirm_auto_generated_gradients(this, num_physical) : MixedFilamentManager::auto_generate_enabled(); +} + +void Plater::set_auto_generated_gradient_decision(size_t num_physical, bool create_auto_gradients) +{ + if (p != nullptr) + p->set_auto_generated_gradient_decision(num_physical, create_auto_gradients); + else + MixedFilamentManager::set_auto_generate_enabled(create_auto_gradients); +} + void Plater::priv::on_filament_color_changed(wxCommandEvent &event) { //q->update_all_plate_thumbnails(true); @@ -8757,6 +14040,13 @@ void Plater::priv::on_filament_color_changed(wxCommandEvent &event) if (wxGetApp().app_config->get("auto_calculate") == "true") { sidebar->auto_calc_flushing_volumes(modify_id); } + + // Regenerate mixed filaments and refresh the mixed panel only. Color + // changes do not alter filament IDs, so the full on_filaments_change() + // path is unnecessary and can re-enter UI rebuilds mid-update. + wxGetApp().preset_bundle->update_multi_material_filament_presets(); + sidebar->update_mixed_filament_panel(); + sidebar->update_color_mix_panel(); } void Plater::priv::install_network_plugin(wxCommandEvent &event) @@ -8828,6 +14118,9 @@ void Plater::priv::apply_color_mode() m_aui_mgr.GetArtProvider()->SetColour(wxAUI_DOCKART_INACTIVE_CAPTION_TEXT_COLOUR, *wxWHITE); m_aui_mgr.GetArtProvider()->SetColour(wxAUI_DOCKART_SASH_COLOUR, sash_color); m_aui_mgr.GetArtProvider()->SetColour(wxAUI_DOCKART_BORDER_COLOUR, is_dark ? *wxBLACK : wxColour(165, 165, 165)); + + if (sidebar) + sidebar->update_color_mix_panel(); } static void get_position(wxWindowBase* child, wxWindowBase* until_parent, int& x, int& y) { @@ -14095,6 +19388,38 @@ void Plater::send_gcode_legacy(int plate_idx, Export3mfProgressFn proFn, bool us // if physical_printer is selected, send gcode for this printer // DynamicPrintConfig* physical_printer_config = wxGetApp().preset_bundle->physical_printers.get_selected_printer_config(); + auto prepare_upload_filename_for_dialog = [this, use_3mf](fs::path output_file) { + output_file = fs::path(Slic3r::fold_utf8_to_ascii(output_file.string())); + if (use_3mf) + output_file.replace_extension("3mf"); + + PartPlate *current_plate = this->get_partplate_list().get_curr_plate(); + if (current_plate != nullptr) { + const Print *current_print = current_plate->fff_print(); + if (current_print != nullptr && !current_print->print_statistics().estimated_normal_print_time.empty()) + return fs::path(current_print->print_statistics().finalize_output_path(output_file.string())); + } + + if (current_plate != nullptr && current_plate->is_slice_result_valid() && current_plate->get_slice_result() != nullptr) { + const auto &estimated_stats = current_plate->get_slice_result()->print_statistics; + const float normal_time = estimated_stats.modes[static_cast(PrintEstimatedStatistics::ETimeMode::Normal)].time; + if (normal_time > 0.0f) { + std::string filename = output_file.string(); + const std::string normal_time_str = short_time(get_time_dhms(normal_time)); + boost::replace_all(filename, "{print_time}", normal_time_str); + boost::replace_all(filename, "{normal_print_time}", normal_time_str); + + const float silent_time = estimated_stats.modes[static_cast(PrintEstimatedStatistics::ETimeMode::Stealth)].time; + if (silent_time > 0.0f) + boost::replace_all(filename, "{silent_print_time}", short_time(get_time_dhms(silent_time))); + + output_file = fs::path(filename); + } + } + + return output_file; + }; + // 校验机型 auto devices = wxGetApp().app_config->get_devices(); std::string connect_preset = ""; @@ -14159,10 +19484,7 @@ void Plater::send_gcode_legacy(int plate_idx, Export3mfProgressFn proFn, bool us show_error(this, ex.what(), false); return; } - default_output_file = fs::path(Slic3r::fold_utf8_to_ascii(default_output_file.string())); - if (use_3mf) { - default_output_file.replace_extension("3mf"); - } + default_output_file = prepare_upload_filename_for_dialog(std::move(default_output_file)); // get file path auto file_path = get_partplate_list().get_curr_plate()->get_tmp_gcode_path(); @@ -14233,10 +19555,7 @@ void Plater::send_gcode_legacy(int plate_idx, Export3mfProgressFn proFn, bool us show_error(this, ex.what(), false); return; } - default_output_file = fs::path(Slic3r::fold_utf8_to_ascii(default_output_file.string())); - if (use_3mf) { - default_output_file.replace_extension("3mf"); - } + default_output_file = prepare_upload_filename_for_dialog(std::move(default_output_file)); // Repetier specific: Query the server for the list of file groups. wxArrayString groups; @@ -14514,19 +19833,58 @@ void Plater::on_filaments_delete(size_t num_filaments, size_t filament_id, int r part_plate->update_first_layer_print_sequence_when_delete_filament(filament_id); }*/ - // update mmu info + // Consume remap before updating volumes + // This is used when merging mixed filaments to properly remap object filament IDs + PresetBundle *preset_bundle = wxGetApp().preset_bundle; + std::vector id_remap; + if (preset_bundle != nullptr) + id_remap = preset_bundle->consume_last_filament_id_remap(); + + // Build state map for remap if available. + // Use the remap for both pure-delete and merge paths so that mixed + // filaments deleted by remove_physical_filament are correctly mapped + // to NONE instead of being shifted onto wrong IDs. + EnforcerBlockerStateMap state_map; + bool should_remap_states = false; + if (!id_remap.empty()) { + should_remap_states = true; + if (replace_filament_id >= 0) { + // Merge: inject the merge target into the remap so the deleted + // physical filament maps to the target instead of 0. + size_t old_1based = filament_id + 1; + size_t new_1based = replace_filament_id + 1; + if (old_1based < id_remap.size()) + id_remap[old_1based] = (unsigned int)new_1based; + } + for (size_t i = 0; i < state_map.size(); ++i) + state_map[i] = EnforcerBlockerType(i); + for (size_t i = 1; i < state_map.size(); ++i) { + const unsigned int mapped = i < id_remap.size() ? id_remap[i] : 0; + if (mapped == 0 || mapped >= state_map.size() || mapped > num_filaments) + state_map[i] = EnforcerBlockerType::NONE; + else + state_map[i] = EnforcerBlockerType(mapped); + } + } + + // update mmu paint data for (ModelObject* mo : wxGetApp().model().objects) { for (ModelVolume* mv : mo->volumes) { - mv->update_extruder_count_when_delete_filament(num_filaments, filament_id + 1, - replace_filament_id + 1); // this function is 1 base + if (should_remap_states) { + mv->remap_extruder_ids(num_filaments, state_map); + } else { + mv->update_extruder_count_when_delete_filament(num_filaments, filament_id + 1, + replace_filament_id + 1); // this function is 1 base + } } } // update UI sidebar().on_filaments_delete(filament_id); - // update global support filament - static const char* keys[] = {"support_filament", "support_interface_filament"}; + // update global feature filament selections + static const char* keys[] = {"wall_filament", "sparse_infill_filament", "solid_infill_filament", + "support_filament", "support_interface_filament"}; for (auto key : keys) if (p->config->has(key)) { if (p->config->opt_int(key) == filament_id + 1) @@ -14564,6 +19922,132 @@ void Plater::on_filaments_change(size_t num_filaments) { // only update elements in plater update_filament_colors_in_full_config(); + + const size_t old_num_filaments = sidebar().combos_filament().size(); + const bool auto_generate_before = MixedFilamentManager::auto_generate_enabled(); + const bool allow_auto_gradients = p->confirm_auto_generated_gradients(this, num_filaments); + auto summarize_uint_vector = [](const std::vector &values, size_t max_items = 24) { + std::string out = "["; + const size_t n = std::min(values.size(), max_items); + for (size_t i = 0; i < n; ++i) { + if (i > 0) + out += ","; + out += std::to_string(values[i]); + } + if (values.size() > n) + out += ",..."; + out += "]"; + return out; + }; + auto summarize_used_states = [](const std::vector &used, size_t max_items = 24) { + std::string out = "["; + size_t total = 0; + size_t emitted = 0; + for (size_t i = 1; i < used.size(); ++i) { + if (!used[i]) + continue; + ++total; + if (emitted < max_items) { + if (emitted > 0) + out += ","; + out += std::to_string(i); + ++emitted; + } + } + if (total > emitted) + out += ",..."; + out += "] total=" + std::to_string(total); + return out; + }; + PresetBundle *preset_bundle = wxGetApp().preset_bundle; + if (preset_bundle != nullptr && auto_generate_before && !allow_auto_gradients) + preset_bundle->update_multi_material_filament_presets(size_t(-1), old_num_filaments); + // Consume remap before sidebar refresh, which may trigger config sync + // paths that regenerate mixed filaments and clear this remap buffer. + std::vector id_remap; + if (preset_bundle != nullptr) + id_remap = preset_bundle->consume_last_filament_id_remap(); + + size_t total_filaments = num_filaments; + if (preset_bundle != nullptr) + total_filaments = preset_bundle->mixed_filaments.total_filaments(num_filaments); + + EnforcerBlockerStateMap state_map; + for (size_t i = 0; i < state_map.size(); ++i) + state_map[i] = EnforcerBlockerType(i); + + bool have_explicit_remap = false; + bool should_remap_states = false; + if (!id_remap.empty()) { + have_explicit_remap = true; + should_remap_states = true; + for (size_t i = 1; i < state_map.size(); ++i) { + const unsigned int mapped = i < id_remap.size() ? id_remap[i] : 0; + if (mapped == 0 || mapped >= state_map.size() || mapped > total_filaments) + state_map[i] = EnforcerBlockerType::NONE; + else + state_map[i] = EnforcerBlockerType(mapped); + } + } + + size_t changed_entries = 0; + std::string changed_map_preview = "["; + for (size_t i = 1; i < state_map.size(); ++i) { + const unsigned int mapped = unsigned(state_map[i]); + if (mapped == i) + continue; + ++changed_entries; + if (changed_entries <= 24) { + if (changed_entries > 1) + changed_map_preview += ","; + changed_map_preview += std::to_string(i) + "->" + std::to_string(mapped); + } + } + if (changed_entries > 24) + changed_map_preview += ",..."; + changed_map_preview += "]"; + BOOST_LOG_TRIVIAL(warning) << "MF_REMAP on_filaments_change" + << " old_physical=" << old_num_filaments + << " new_physical=" << num_filaments + << " total_filaments=" << total_filaments + << " id_remap_size=" << id_remap.size() + << " id_remap=" << summarize_uint_vector(id_remap) + << " explicit_remap=" << (have_explicit_remap ? 1 : 0) + << " should_remap_states=" << (should_remap_states ? 1 : 0) + << " changed_entries=" << changed_entries + << " changed_map=" << changed_map_preview; + + size_t obj_idx = 0; + for (ModelObject* mo : wxGetApp().model().objects) { + size_t vol_idx = 0; + for (ModelVolume* mv : mo->volumes) { + std::string used_before; + const bool has_mmu_paint = (mv != nullptr && !mv->mmu_segmentation_facets.empty()); + if (has_mmu_paint) + used_before = summarize_used_states(mv->mmu_segmentation_facets.get_data().used_states); + + if (should_remap_states) + mv->remap_extruder_ids(total_filaments, state_map); + else + mv->update_extruder_count(total_filaments); + + if (has_mmu_paint) { + const std::string used_after = summarize_used_states(mv->mmu_segmentation_facets.get_data().used_states); + BOOST_LOG_TRIVIAL(warning) << "MF_REMAP volume" + << " obj_idx=" << obj_idx + << " vol_idx=" << vol_idx + << " obj_name=" << mo->name + << " vol_name=" << mv->name + << " before=" << used_before + << " after=" << used_after; + } + ++vol_idx; + } + ++obj_idx; + } + + // Keep UI refresh after model remap. Some UI update paths may trigger + // scene/model sync that assumes already-remapped MMU state. sidebar().on_filaments_change(num_filaments); sidebar().obj_list()->update_objects_list_filament_column(num_filaments); @@ -14572,12 +20056,6 @@ void Plater::on_filaments_change(size_t num_filaments) PartPlate* part_plate = plate_list.get_plate(i); part_plate->update_first_layer_print_sequence(num_filaments); } - - for (ModelObject* mo : wxGetApp().model().objects) { - for (ModelVolume* mv : mo->volumes) { - mv->update_extruder_count(num_filaments); - } - } } void Plater::on_bed_type_change(BedType bed_type) @@ -14622,7 +20100,6 @@ bool Plater::check_filament_temp_mixing() int num_filaments = (int)ft_opt->values.size(); std::set used_slots; - // Helper: collect used filament slots from a single config auto collect_from_cfg = [&](const DynamicPrintConfig& cfg) { static const char* keys_1based[] = {"wall_filament", "solid_infill_filament"}; for (auto key : keys_1based) { @@ -14794,6 +20271,9 @@ void Plater::on_config_change(const DynamicPrintConfig &config) } // Orca: update when *_filament changed else if (opt_key == "support_interface_filament" || opt_key == "support_filament" || opt_key == "wall_filament" || + opt_key == "enable_infill_filament_override" || + opt_key == "infill_filament_use_base_first_layers" || + opt_key == "infill_filament_use_base_last_layers" || opt_key == "sparse_infill_filament" || opt_key == "solid_infill_filament") { update_scheduled = true; } @@ -14812,7 +20292,6 @@ void Plater::on_config_change(const DynamicPrintConfig &config) update_title_dirty_status(); } - // Real-time high/low temperature filament mixing check sync_filament_temp_mixing_notification(); } @@ -14896,12 +20375,25 @@ std::vector Plater::get_extruder_colors_from_plater_config(const GC if (wxGetApp().is_gcode_viewer() && result != nullptr) return result->extruder_colors; else { + if (wxGetApp().preset_bundle == nullptr) + return {}; + const Slic3r::DynamicPrintConfig* config = &wxGetApp().preset_bundle->project_config; std::vector filament_colors; if (!config->has("filament_colour")) // in case of a SLA print return filament_colors; filament_colors = (config->option("filament_colour"))->values; + const size_t num_physical = static_cast(std::max(wxGetApp().filaments_cnt(), 0)); + filament_colors.resize(num_physical, "#26A69A"); + + if (include_mixed) { + // Append display colours for enabled mixed (virtual) filaments. + const auto &mixed_mgr = wxGetApp().preset_bundle->mixed_filaments; + for (const auto &dc : mixed_mgr.display_colors()) + filament_colors.push_back(dc); + } + return filament_colors; } } @@ -16155,6 +21647,20 @@ void Plater::reset_gcode_toolpaths() p->reset_gcode_toolpaths(); } +void Plater::post_slice_state_change_update() +{ + p->partplate_list.invalid_all_slice_result(); + reset_gcode_toolpaths(); + wxGetApp().mainframe->update_slice_print_status(MainFrame::SlicePrintEventType::eEventSliceUpdate, true, false); + CallAfter([this]() { + GLCanvas3D* canvas = get_current_canvas3D(); + if (canvas) { + canvas->set_as_dirty(); + canvas->request_extra_frame(); + } + }); +} + const Mouse3DController& Plater::get_mouse3d_controller() const { return p->mouse3d_controller; From 9f50fd7e75e7dc5b1330eee51ff0785a50b27b0a Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Mon, 8 Jun 2026 18:57:27 +0800 Subject: [PATCH 101/135] Fix filament temperature mixing notifications --- src/libslic3r/Print.cpp | 11 -- src/slic3r/GUI/GLCanvas3D.cpp | 2 +- src/slic3r/GUI/GUI_ObjectList.cpp | 6 +- .../GUI/Gizmos/GLGizmoMmuSegmentation.cpp | 2 +- src/slic3r/GUI/NotificationManager.cpp | 19 +++ src/slic3r/GUI/NotificationManager.hpp | 5 + src/slic3r/GUI/PartPlate.cpp | 2 +- src/slic3r/GUI/Plater.cpp | 158 ++++++++++++------ src/slic3r/GUI/Plater.hpp | 12 +- src/slic3r/GUI/Preferences.cpp | 3 + src/slic3r/GUI/Tab.cpp | 3 +- 11 files changed, 152 insertions(+), 71 deletions(-) diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index c0ee6f1ace3..f5eb30af3c6 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -1558,17 +1558,6 @@ StringObjectException Print::validate(StringObjectException *warning, Polygons* if (extruders.empty()) return { L("No extrusions under current settings.") }; - // Check for high/low temperature filament mixing. The check uses - // filament_is_high_temperature from each filament preset. This guard - // works for both single-nozzle and multi-nozzle printers. - if (extruders.size() > 1) { - auto ret = check_multi_filament_valid(*this); - if (!ret.string.empty()) { - ret.type = STRING_EXCEPT_FILAMENTS_DIFFERENT_TEMP; - return ret; - } - } - if (m_config.print_sequence == PrintSequence::ByObject) { if (m_config.timelapse_type == TimelapseType::tlSmooth) return {L("Smooth mode of timelapse is not supported when \"by object\" sequence is enabled.")}; diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp index 846eb49611b..eb079fee6a7 100644 --- a/src/slic3r/GUI/GLCanvas3D.cpp +++ b/src/slic3r/GUI/GLCanvas3D.cpp @@ -4826,7 +4826,7 @@ void GLCanvas3D::do_move(const std::string& snapshot_type) reset_sequential_print_clearance(); - wxGetApp().plater()->sync_filament_temp_mixing_notification(); + wxGetApp().plater()->notify_filament_usage_changed(); m_dirty = true; } diff --git a/src/slic3r/GUI/GUI_ObjectList.cpp b/src/slic3r/GUI/GUI_ObjectList.cpp index 537da98594a..8a802b3cf98 100644 --- a/src/slic3r/GUI/GUI_ObjectList.cpp +++ b/src/slic3r/GUI/GUI_ObjectList.cpp @@ -1099,7 +1099,7 @@ void ObjectList::update_filament_in_config(const wxDataViewItem& item) // update scene wxGetApp().plater()->update(); - wxGetApp().plater()->sync_filament_temp_mixing_notification(); + wxGetApp().plater()->notify_filament_usage_changed(); } void ObjectList::update_name_in_model(const wxDataViewItem& item) const @@ -5922,7 +5922,7 @@ void ObjectList::set_extruder_for_selected_items(const int extruder) // update scene wxGetApp().plater()->update(); - wxGetApp().plater()->sync_filament_temp_mixing_notification(); + wxGetApp().plater()->notify_filament_usage_changed(); // BBS: update extruder/filament column Refresh(); @@ -5982,7 +5982,7 @@ void ObjectList::reload_all_plates(bool notify_partplate) wxGetApp().plater()->update(); // update printable states on canvas wxGetApp().plater()->get_view3D_canvas3D()->update_instance_printable_state_for_objects(obj_idxs); - wxGetApp().plater()->sync_filament_temp_mixing_notification(); + wxGetApp().plater()->notify_filament_usage_changed(); } void ObjectList::on_plate_selected(int plate_index) diff --git a/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp b/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp index 0435434a4e9..82aba7c88cc 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp @@ -1000,7 +1000,7 @@ void GLGizmoMmuSegmentation::update_model_object() wxGetApp().obj_list()->update_info_items(obj_idx); wxGetApp().plater()->get_partplate_list().notify_instance_update(obj_idx, 0); m_parent.post_event(SimpleEvent(EVT_GLCANVAS_SCHEDULE_BACKGROUND_PROCESS)); - wxGetApp().plater()->sync_filament_temp_mixing_notification(); + wxGetApp().plater()->notify_filament_usage_changed(); } } diff --git a/src/slic3r/GUI/NotificationManager.cpp b/src/slic3r/GUI/NotificationManager.cpp index 0ff7d4d3d18..f2be434c92b 100644 --- a/src/slic3r/GUI/NotificationManager.cpp +++ b/src/slic3r/GUI/NotificationManager.cpp @@ -1836,6 +1836,16 @@ void NotificationManager::push_validate_error_notification(StringObjectException set_slicing_progress_hidden(); } +void NotificationManager::close_validate_error_notification(const std::string& text) +{ + close_notification_of_type_and_text(NotificationType::ValidateError, _u8L("Error:") + "\n" + text); +} + +void NotificationManager::close_validate_warning_notification(const std::string& text) +{ + close_notification_of_type_and_text(NotificationType::ValidateWarning, _u8L("WARNING:") + "\n" + text); +} + void NotificationManager::push_slicing_error_notification(const std::string &text, std::vector objs) { std::vector ids; @@ -2003,6 +2013,15 @@ void NotificationManager::close_notification_of_type(const NotificationType type } } } + +void NotificationManager::close_notification_of_type_and_text(const NotificationType type, const std::string& text) +{ + for (std::unique_ptr& notification : m_pop_notifications) { + if (notification->get_type() == type && notification->compare_text(text)) + notification->close(); + } +} + void NotificationManager::remove_slicing_warnings_of_released_objects(const std::vector& living_oids) { for (std::unique_ptr ¬ification : m_pop_notifications) diff --git a/src/slic3r/GUI/NotificationManager.hpp b/src/slic3r/GUI/NotificationManager.hpp index dce17f6080c..27c51f7cd5a 100644 --- a/src/slic3r/GUI/NotificationManager.hpp +++ b/src/slic3r/GUI/NotificationManager.hpp @@ -199,6 +199,8 @@ class NotificationManager void stop_delayed_notifications_of_type(const NotificationType type); // Creates Validate Error notification with a custom text and no fade out. void push_validate_error_notification(StringObjectException const & error); + void close_validate_error_notification(const std::string& text); + void close_validate_warning_notification(const std::string& text); // print host upload void push_upload_job_notification(int id, float filesize, const std::string& filename, const std::string& host, float percentage = 0); void set_upload_job_notification_percentage(int id, const std::string& filename, const std::string& host, float percentage); @@ -844,6 +846,7 @@ class NotificationManager //can be used to create custom notification bool push_notification_data(const NotificationData& notification_data, int timestamp); bool push_notification_data(std::unique_ptr notification, int timestamp); + void close_notification_of_type_and_text(const NotificationType type, const std::string& text); // Delayed notifications goes first to the m_waiting_notifications vector and only after remaining time is <= 0 // and condition callback is success, notification is regular pushed from update function. // Otherwise another delay interval waiting. Timestamp is 0. @@ -898,6 +901,8 @@ class NotificationManager NotificationType::PlaterWarning, NotificationType::ProgressBar, NotificationType::PrintHostUpload, + NotificationType::ValidateError, + NotificationType::ValidateWarning, NotificationType::SimplifySuggestion }; //prepared (basic) notifications diff --git a/src/slic3r/GUI/PartPlate.cpp b/src/slic3r/GUI/PartPlate.cpp index 736943da928..b29ea6d70f7 100644 --- a/src/slic3r/GUI/PartPlate.cpp +++ b/src/slic3r/GUI/PartPlate.cpp @@ -4527,7 +4527,7 @@ int PartPlateList::add_to_plate(int obj_id, int instance_id, int plate_id) } ret = plate->add_instance(obj_id, instance_id, true); - wxGetApp().plater()->sync_filament_temp_mixing_notification(); + wxGetApp().plater()->notify_filament_usage_changed(); return ret; } diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index df3926ed971..46b8596ad35 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -201,6 +201,16 @@ static const std::pair THUMBNAIL_SIZE_3MF = { 512, 5 namespace Slic3r { namespace GUI { +static std::string filament_temp_mixing_warning_text() +{ + return _u8L("Detected both high and low temperature materials. Mixed printing may result in extruder clogging, nozzle damage, or layer adhesion issues."); +} + +static std::string filament_temp_mixing_error_text() +{ + return _u8L("Detected both high and low temperature materials. Mixed printing may result in extruder clogging, nozzle damage, or layer adhesion issues. To continue printing, enable \"Allow mixed printing of high and low temperature materials\" in Preferences."); +} + wxDEFINE_EVENT(EVT_SCHEDULE_BACKGROUND_PROCESS, SimpleEvent); wxDEFINE_EVENT(EVT_SLICING_UPDATE, SlicingStatusEvent); wxDEFINE_EVENT(EVT_SLICING_COMPLETED, wxCommandEvent); @@ -231,6 +241,7 @@ wxDEFINE_EVENT(EVT_PRINT_FROM_SDCARD_VIEW, SimpleEvent); wxDEFINE_EVENT(EVT_CREATE_FILAMENT, SimpleEvent); wxDEFINE_EVENT(EVT_MODIFY_FILAMENT, SimpleEvent); +wxDEFINE_EVENT(EVT_FILAMENT_USAGE_CHANGED, SimpleEvent); wxDEFINE_EVENT(EVT_ADD_FILAMENT, SimpleEvent); wxDEFINE_EVENT(EVT_DEL_FILAMENT, SimpleEvent); wxDEFINE_EVENT(EVT_ADD_CUSTOM_FILAMENT, ColorEvent); @@ -2869,8 +2880,7 @@ void Sidebar::update_presets(Preset::Type preset_type) update_dynamic_filament_list(); - // Real-time high/low temperature filament mixing check after filament preset change - p->plater->sync_filament_temp_mixing_notification(); + p->plater->notify_filament_usage_changed(); break; } @@ -8409,6 +8419,7 @@ struct Plater::priv // PIMPL back pointer ("Q-Pointer") Plater *q; MainFrame *main_frame; + bool filament_usage_sync_pending = false; MenuFactory menus; @@ -9057,6 +9068,10 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame) this->q->Bind(EVT_CREATE_FILAMENT, &priv::on_create_filament, this); this->q->Bind(EVT_MODIFY_FILAMENT, &priv::on_modify_filament, this); this->q->Bind(EVT_ADD_CUSTOM_FILAMENT, &priv::on_add_custom_filament, this); + this->q->Bind(EVT_FILAMENT_USAGE_CHANGED, [this](SimpleEvent&) { + filament_usage_sync_pending = false; + this->q->sync_filament_temp_mixing_notification(); + }); main_frame->m_tabpanel->Bind(wxEVT_NOTEBOOK_PAGE_CHANGING, &priv::on_tab_selection_changing, this); auto* panel_3d = new wxPanel(q); @@ -11250,7 +11265,7 @@ void Plater::priv::remove_curr_plate_all() view3D->remove_curr_plate_all(); this->sidebar->obj_list()->update_selections(); - q->sync_filament_temp_mixing_notification(); + q->notify_filament_usage_changed(); } void Plater::priv::select_all() @@ -11282,6 +11297,7 @@ void Plater::priv::remove(size_t obj_idx) // Delete object from Sidebar list. Do it after update, so that the GLScene selection is updated with the modified model. sidebar->obj_list()->delete_object_from_list(obj_idx); object_list_changed(); + q->notify_filament_usage_changed(); } @@ -11320,6 +11336,8 @@ bool Plater::priv::delete_object_from_model(size_t obj_idx, bool refresh_immedia object_list_changed(); } + q->notify_filament_usage_changed(); + return true; } @@ -11352,6 +11370,7 @@ void Plater::priv::delete_all_objects_from_model() //BBS model.calib_pa_pattern.reset(); model.plates_custom_gcodes.clear(); + q->notify_filament_usage_changed(); } void Plater::priv::reset(bool apply_presets_change) @@ -11731,6 +11750,10 @@ unsigned int Plater::priv::update_background_process(bool force_validation, bool BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": validate err=%1%, warning=%2%")%err.string%warning.string; if (err.string.empty()) { + // Pass a warning from validation and either show a notification, + // or hide the old one. + process_validation_warning(warning); + // Validate passed, but also check filament temp mixing as a final // guard. check_filament_temp_mixing() reads directly from preset // configs and catches cases that Print::validate() may miss (e.g. @@ -11744,9 +11767,6 @@ unsigned int Plater::priv::update_background_process(bool force_validation, bool if (invalidated != Print::APPLY_STATUS_UNCHANGED && background_processing_enabled()) return_state |= UPDATE_BACKGROUND_PROCESS_RESTART; - // Pass a warning from validation and either show a notification, - // or hide the old one. - process_validation_warning(warning); if (printer_technology == ptFFF) { view3D->get_canvas3d()->reset_sequential_print_clearance(); view3D->get_canvas3d()->set_as_dirty(); @@ -13656,14 +13676,8 @@ void Plater::priv::on_action_slice_plate(SimpleEvent&) Model::setPrintSpeedTable(config, print_config); m_slice_all = false; - // Pre-slice confirmation for high/low temp filament mixing - if (wxGetApp().app_config->get_bool("allow_filament_temp_mixing") - && !q->check_filament_temp_mixing()) { - wxMessageDialog dlg(q, _L("该组合可能导致风险,是否继续"), - _L("确认"), wxYES_NO | wxICON_WARNING); - if (dlg.ShowModal() != wxID_YES) - return; - } + if (!q->confirm_filament_temp_mixing_before_slice()) + return; q->reslice(); q->select_view_3D("Preview"); @@ -13684,14 +13698,8 @@ void Plater::priv::on_action_slice_all(SimpleEvent&) Model::setExtruderParams(config, numExtruders); Model::setPrintSpeedTable(config, print_config); - // Pre-slice confirmation for high/low temp filament mixing - if (wxGetApp().app_config->get_bool("allow_filament_temp_mixing") - && !q->check_filament_temp_mixing()) { - wxMessageDialog dlg(q, _L("该组合可能导致风险,是否继续"), - _L("确认"), wxYES_NO | wxICON_WARNING); - if (dlg.ShowModal() != wxID_YES) - return; - } + if (!q->confirm_filament_temp_mixing_before_slice()) + return; m_slice_all = true; m_slice_all_only_has_gcode = true; @@ -17152,7 +17160,10 @@ std::vector Plater::load_files(const std::vector& input_files, p->m_slice_all_only_has_gcode = false; //BBS: wish to reset all plates stats item selected state when load a new file p->preview->get_canvas3d()->reset_select_plate_toolbar_selection(); - return p->load_files(input_files, strategy, ask_multi); + std::vector loaded = p->load_files(input_files, strategy, ask_multi); + if (!loaded.empty()) + notify_filament_usage_changed(); + return loaded; } // To be called when providing a list of files to the GUI slic3r on command line. @@ -17162,7 +17173,7 @@ std::vector Plater::load_files(const std::vector& input_fil paths.reserve(input_files.size()); for (const std::string& path : input_files) paths.emplace_back(path); - return p->load_files(paths, strategy, ask_multi); + return load_files(paths, strategy, ask_multi); } bool Plater::preview_zip_archive(const boost::filesystem::path& archive_path) @@ -18049,8 +18060,10 @@ void Plater::remove_selected() //BBS delete current selected // p->view3D->delete_selected(); GLCanvas3D* canvas = p->get_current_canvas3D(); - if (canvas) + if (canvas) { canvas->delete_selected(); + notify_filament_usage_changed(); + } } void Plater::increase_instances(size_t num) @@ -20100,6 +20113,27 @@ bool Plater::check_filament_temp_mixing() int num_filaments = (int)ft_opt->values.size(); std::set used_slots; + PartPlate* curr_plate = p->partplate_list.get_curr_plate(); + auto object_is_on_curr_plate = [curr_plate](size_t obj_idx, const ModelObject* mo) { + if (!curr_plate) + return true; + for (int inst_idx = 0; inst_idx < (int)mo->instances.size(); ++inst_idx) { + if (curr_plate->contain_instance((int)obj_idx, inst_idx)) + return true; + } + return false; + }; + + bool has_object_on_curr_plate = false; + for (size_t obj_idx = 0; obj_idx < wxGetApp().model().objects.size(); ++obj_idx) { + if (object_is_on_curr_plate(obj_idx, wxGetApp().model().objects[obj_idx])) { + has_object_on_curr_plate = true; + break; + } + } + if (!has_object_on_curr_plate) + return true; + auto collect_from_cfg = [&](const DynamicPrintConfig& cfg) { static const char* keys_1based[] = {"wall_filament", "solid_infill_filament"}; for (auto key : keys_1based) { @@ -20120,23 +20154,13 @@ bool Plater::check_filament_temp_mixing() collect_from_cfg(*this->config()); // Also collect from current plate's config for any plate-level overrides - PartPlate* curr_plate = p->partplate_list.get_curr_plate(); if (curr_plate) collect_from_cfg(*curr_plate->config()); // Collect from ModelVolume painting extruders for objects on the current plate for (size_t obj_idx = 0; obj_idx < wxGetApp().model().objects.size(); ++obj_idx) { const ModelObject* mo = wxGetApp().model().objects[obj_idx]; - bool on_curr_plate = false; - if (curr_plate) { - for (int inst_idx = 0; inst_idx < (int)mo->instances.size(); ++inst_idx) { - if (curr_plate->contain_instance((int)obj_idx, inst_idx)) { - on_curr_plate = true; - break; - } - } - } - if (!on_curr_plate) + if (!object_is_on_curr_plate(obj_idx, mo)) continue; for (const ModelVolume* mv : mo->volumes) { for (int eid : mv->get_extruders()) { @@ -20174,30 +20198,62 @@ bool Plater::check_filament_temp_mixing() return compatible; } +Plater::FilamentTempMixingState Plater::get_filament_temp_mixing_state() +{ + if (check_filament_temp_mixing()) + return FilamentTempMixingState::Compatible; + + return wxGetApp().app_config->get_bool("allow_filament_temp_mixing") ? + FilamentTempMixingState::AllowedWarning : + FilamentTempMixingState::BlockedError; +} + bool Plater::sync_filament_temp_mixing_notification() { - if (check_filament_temp_mixing()) { - get_notification_manager()->close_notification_of_type(NotificationType::ValidateError); - get_notification_manager()->close_notification_of_type(NotificationType::ValidateWarning); + switch (get_filament_temp_mixing_state()) { + case FilamentTempMixingState::Compatible: + get_notification_manager()->close_validate_error_notification(filament_temp_mixing_error_text()); + get_notification_manager()->close_validate_warning_notification(filament_temp_mixing_warning_text()); get_partplate_list().get_curr_plate()->update_apply_result_invalid(false); return true; - } - - StringObjectException err; - err.type = STRING_EXCEPT_FILAMENTS_DIFFERENT_TEMP; - - if (wxGetApp().app_config->get_bool("allow_filament_temp_mixing")) { + case FilamentTempMixingState::AllowedWarning: + get_notification_manager()->close_validate_error_notification(filament_temp_mixing_error_text()); + get_partplate_list().get_curr_plate()->update_apply_result_invalid(false); get_notification_manager()->push_notification( NotificationType::ValidateWarning, NotificationManager::NotificationLevel::WarningNotificationLevel, - _u8L("WARNING:") + "\n" + - _u8L("Detected both high and low temperature materials. Mixed printing may result in extruder clogging, nozzle damage, or layer adhesion issues.")); - } else { - err.string = _u8L("Detected both high and low temperature materials. Mixed printing may result in extruder clogging, nozzle damage, or layer adhesion issues. To continue printing, enable \"Allow mixed printing of high and low temperature materials\" in Preferences."); + _u8L("WARNING:") + "\n" + filament_temp_mixing_warning_text()); + return true; + case FilamentTempMixingState::BlockedError: { + StringObjectException err; + err.type = STRING_EXCEPT_FILAMENTS_DIFFERENT_TEMP; + err.string = filament_temp_mixing_error_text(); + get_notification_manager()->close_validate_warning_notification(filament_temp_mixing_warning_text()); get_notification_manager()->push_validate_error_notification(err); get_partplate_list().get_curr_plate()->update_apply_result_invalid(true); + return false; } - return false; + } + + return true; +} + +bool Plater::confirm_filament_temp_mixing_before_slice() +{ + if (get_filament_temp_mixing_state() != FilamentTempMixingState::AllowedWarning) + return true; + + wxMessageDialog dlg(this, _L("该组合可能导致风险,是否继续"), _L("确认"), wxYES_NO | wxICON_WARNING); + return dlg.ShowModal() == wxID_YES; +} + +void Plater::notify_filament_usage_changed() +{ + if (p->filament_usage_sync_pending) + return; + + p->filament_usage_sync_pending = true; + wxQueueEvent(this, new SimpleEvent(EVT_FILAMENT_USAGE_CHANGED, this)); } void Plater::on_config_change(const DynamicPrintConfig &config) @@ -20292,7 +20348,7 @@ void Plater::on_config_change(const DynamicPrintConfig &config) update_title_dirty_status(); } - sync_filament_temp_mixing_notification(); + notify_filament_usage_changed(); } void Plater::set_bed_shape() const diff --git a/src/slic3r/GUI/Plater.hpp b/src/slic3r/GUI/Plater.hpp index a11ef680bef..429f635cee2 100644 --- a/src/slic3r/GUI/Plater.hpp +++ b/src/slic3r/GUI/Plater.hpp @@ -104,6 +104,7 @@ wxDECLARE_EVENT(EVT_GLCANVAS_COLOR_MODE_CHANGED, SimpleEvent); wxDECLARE_EVENT(EVT_PRINT_FROM_SDCARD_VIEW, SimpleEvent); wxDECLARE_EVENT(EVT_CREATE_FILAMENT, SimpleEvent); wxDECLARE_EVENT(EVT_MODIFY_FILAMENT, SimpleEvent); +wxDECLARE_EVENT(EVT_FILAMENT_USAGE_CHANGED, SimpleEvent); wxDECLARE_EVENT(EVT_ADD_FILAMENT, SimpleEvent); wxDECLARE_EVENT(EVT_DEL_FILAMENT, SimpleEvent); using ColorEvent = Event; @@ -231,6 +232,11 @@ class Plater: public wxPanel { public: using fs_path = boost::filesystem::path; + enum class FilamentTempMixingState { + Compatible, + AllowedWarning, + BlockedError + }; Plater(wxWindow *parent, MainFrame *main_frame); Plater(Plater &&) = delete; @@ -510,9 +516,13 @@ class Plater: public wxPanel /// Check whether high-temperature and low-temperature filaments are mixed on the current plate. /// Returns true if compatible (no mixing), false if mixing is detected and error notification is shown. bool check_filament_temp_mixing(); + FilamentTempMixingState get_filament_temp_mixing_state(); /// Sync notification state with current filament temp mixing status. - /// Returns true if compatible (no mixing), false if mixing detected. + /// Returns true if slicing is allowed, false if high/low temperature mixing blocks slicing. bool sync_filament_temp_mixing_notification(); + bool confirm_filament_temp_mixing_before_slice(); + /// Queue a single UI sync after filament preset/assignment/plate usage changes. + void notify_filament_usage_changed(); void force_filament_colors_update(); void force_print_bed_update(); // On activating the parent window. diff --git a/src/slic3r/GUI/Preferences.cpp b/src/slic3r/GUI/Preferences.cpp index 82291ebe2ca..9ab828e277c 100644 --- a/src/slic3r/GUI/Preferences.cpp +++ b/src/slic3r/GUI/Preferences.cpp @@ -762,6 +762,9 @@ wxBoxSizer *PreferencesDialog::create_item_checkbox(wxString title, wxWindow *pa app_config->set_bool(param, checkbox->GetValue()); app_config->save(); + if (param == "allow_filament_temp_mixing" && wxGetApp().plater()) + wxGetApp().plater()->notify_filament_usage_changed(); + if (param == PRIVACY_POLICY_FLAGS) { app_config->set("app", PRIVACY_POLICY_FLAGS, checkbox->GetValue()); diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index f142647e322..21e1403f52f 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -3111,8 +3111,7 @@ void TabPrintModel::on_value_change(const std::string& opt_key, const boost::any } wxGetApp().params_panel()->notify_object_config_changed(); - // Real-time high/low temperature filament mixing check after object-level config change - wxGetApp().plater()->sync_filament_temp_mixing_notification(); + wxGetApp().plater()->notify_filament_usage_changed(); } void TabPrintModel::reload_config() From da097768644c5169d69d096f4730e4c60002e840 Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Tue, 9 Jun 2026 18:18:47 +0800 Subject: [PATCH 102/135] Fix filament temperature mixing slice guards --- src/slic3r/GUI/GCodeViewer.cpp | 18 ++- src/slic3r/GUI/GLCanvas3D.cpp | 18 ++- src/slic3r/GUI/MainFrame.cpp | 7 +- src/slic3r/GUI/PartPlate.cpp | 4 + src/slic3r/GUI/Plater.cpp | 275 +++++++++++++++++++++++++-------- src/slic3r/GUI/Plater.hpp | 28 +++- 6 files changed, 271 insertions(+), 79 deletions(-) diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp index 23f9788899a..0bf41c17264 100644 --- a/src/slic3r/GUI/GCodeViewer.cpp +++ b/src/slic3r/GUI/GCodeViewer.cpp @@ -4049,10 +4049,10 @@ void GCodeViewer::render_shells(int canvas_width, int canvas_height) //BBS void GCodeViewer::render_all_plates_stats(const std::vector& gcode_result_list, bool show /*= true*/) const { - if (!show) + if (!show || gcode_result_list.empty()) return; - for (auto gcode_result : gcode_result_list) { - if (gcode_result->moves.size() == 0) + for (const GCodeProcessorResult* gcode_result : gcode_result_list) { + if (gcode_result == nullptr || gcode_result->moves.empty()) return; } ImGuiWrapper& imgui = *wxGetApp().imgui(); @@ -4076,7 +4076,7 @@ void GCodeViewer::render_all_plates_stats(const std::vector filament_colors; decode_colors(wxGetApp().plater()->get_extruder_colors_from_plater_config(gcode_result_list.back()), filament_colors); - for (int i = 0; i < filament_colors.size(); i++) { + for (size_t i = 0; i < filament_colors.size(); i++) { filament_colors[i] = adjust_color_for_rendering(filament_colors[i]); } @@ -4170,6 +4170,10 @@ void GCodeViewer::render_all_plates_stats(const std::vector(extruder_id) >= filament_diameters.size() || + static_cast(extruder_id) >= filament_densities.size()) + return std::pair{ 0.0, 0.0 }; + double koef = imperial_units ? 1.0 / GizmoObjectManipulation::in_to_mm : 0.001; std::pair ret = { koef * volume / (PI * sqr(0.5 * filament_diameters[extruder_id])), volume * filament_densities[extruder_id] * 0.001 }; @@ -4183,6 +4187,9 @@ void GCodeViewer::render_all_plates_stats(const std::vectorget_partplate_list(); for (auto plate : plate_list.get_nonempty_plate_list()) { + if (plate == nullptr || plate->get_slice_result() == nullptr) + continue; + auto plate_print_statistics = plate->get_slice_result()->print_statistics; for (const auto& [extruder_id, model_volume] : plate_print_statistics.model_volumes_per_extruder) { model_volume_of_extruders_all_plates[extruder_id] += model_volume; @@ -4201,7 +4208,8 @@ void GCodeViewer::render_all_plates_stats(const std::vectorget_print((PrintBase **) &print, nullptr, nullptr); - total_cost_all_plates += print->print_statistics().total_cost; + if (print != nullptr) + total_cost_all_plates += print->print_statistics().total_cost; } for (auto it = model_volume_of_extruders_all_plates.begin(); it != model_volume_of_extruders_all_plates.end(); it++) diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp index eb079fee6a7..43deee3e279 100644 --- a/src/slic3r/GUI/GLCanvas3D.cpp +++ b/src/slic3r/GUI/GLCanvas3D.cpp @@ -7934,16 +7934,19 @@ void GLCanvas3D::_render_imgui_select_plate_toolbar() } if (m_sel_plate_toolbar.show_stats_item) { all_plates_stats_item->percent = 0.0f; + const std::vector nonempty_plate_list = plate_list.get_nonempty_plate_list(); + const std::vector nonempty_plate_results = plate_list.get_nonempty_plates_slice_results(); - for (auto plate : plate_list.get_nonempty_plate_list()) { + for (PartPlate* plate : nonempty_plate_list) { if (plate->is_slice_result_valid() && plate->is_slice_result_ready_for_print()) sliced_plates_cnt++; } - all_plates_stats_item->percent = (float)(sliced_plates_cnt) / (float)(plate_list.get_nonempty_plate_list().size()) * 100.0f; + if (!nonempty_plate_list.empty()) + all_plates_stats_item->percent = static_cast(sliced_plates_cnt) / static_cast(nonempty_plate_list.size()) * 100.0f; if (all_plates_stats_item->percent == 0.0f) all_plates_stats_item->slice_state = IMToolbarItem::SliceState::UNSLICED; - else if (sliced_plates_cnt == plate_list.get_nonempty_plate_list().size()) + else if (sliced_plates_cnt == nonempty_plate_list.size()) all_plates_stats_item->slice_state = IMToolbarItem::SliceState::SLICED; else if (all_plates_stats_item->percent < 100.0f) all_plates_stats_item->slice_state = IMToolbarItem::SliceState::SLICING; @@ -7958,17 +7961,18 @@ void GLCanvas3D::_render_imgui_select_plate_toolbar() // Changing parameters does not invalid all plates, need extra logic to validate bool gcode_result_valid = true; - for (auto gcode_result : plate_list.get_nonempty_plates_slice_results()) { - if (gcode_result->moves.size() == 0) { + for (const GCodeProcessorResult* gcode_result : nonempty_plate_results) { + if (gcode_result == nullptr || gcode_result->moves.empty()) { gcode_result_valid = false; + break; } } if (all_plates_stats_item->selected && all_plates_stats_item->slice_state == IMToolbarItem::SliceState::SLICED && gcode_result_valid) { - m_gcode_viewer.render_all_plates_stats(plate_list.get_nonempty_plates_slice_results()); + m_gcode_viewer.render_all_plates_stats(nonempty_plate_results); m_render_preview = false; } else{ - m_gcode_viewer.render_all_plates_stats(plate_list.get_nonempty_plates_slice_results(), false); + m_gcode_viewer.render_all_plates_stats(nonempty_plate_results, false); m_render_preview = true; } }else diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index 662210a3483..0bcb1585ac1 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -1894,8 +1894,7 @@ bool MainFrame::get_enable_slice_status() { enable = false; }*/ - //always enable slice_all button - enable = true; + enable = m_plater->has_sliceable_plate_for_slice_all(); } else if (m_slice_select == eSlicePlate) { @@ -1907,6 +1906,10 @@ bool MainFrame::get_enable_slice_status() { enable = false; } + else if (m_plater->is_plate_blocked_by_filament_temp_mixing(part_plate_list.get_curr_plate_index())) + { + enable = false; + } } BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": m_slice_select %1%, enable= %2% ")%m_slice_select %enable; diff --git a/src/slic3r/GUI/PartPlate.cpp b/src/slic3r/GUI/PartPlate.cpp index b29ea6d70f7..9d9a1ab7252 100644 --- a/src/slic3r/GUI/PartPlate.cpp +++ b/src/slic3r/GUI/PartPlate.cpp @@ -4039,6 +4039,8 @@ int PartPlateList::select_plate(int index) return -1; } + const int old_plate_index = m_current_plate; + // BBS: erase unnecessary snapshot if (get_curr_plate_index() != index && m_intialized) { if (m_plater) @@ -4052,6 +4054,8 @@ int PartPlateList::select_plate(int index) m_current_plate = index; m_plate_list[m_current_plate]->set_selected(); + if (old_plate_index != m_current_plate && wxGetApp().plater()) + wxGetApp().plater()->notify_filament_usage_changed(); //BBS if(m_model) diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 46b8596ad35..75608f8d827 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -211,6 +211,72 @@ static std::string filament_temp_mixing_error_text() return _u8L("Detected both high and low temperature materials. Mixed printing may result in extruder clogging, nozzle damage, or layer adhesion issues. To continue printing, enable \"Allow mixed printing of high and low temperature materials\" in Preferences."); } +static bool model_object_is_on_plate(PartPlate* plate, size_t obj_idx, const ModelObject* model_object) +{ + if (model_object == nullptr) + return false; + + if (plate == nullptr) + return true; + + const int object_index = static_cast(obj_idx); + const int instance_count = static_cast(model_object->instances.size()); + for (int instance_index = 0; instance_index < instance_count; ++instance_index) + { + if (plate->contain_instance(object_index, instance_index)) + return true; + } + + return false; +} + +static void collect_filament_slots_from_config( + const DynamicPrintConfig& config, + int num_filaments, + std::set& used_slots) +{ + static const std::vector keys_1based = { + "wall_filament", + "sparse_infill_filament", + "solid_infill_filament" + }; + for (const char* key : keys_1based) + { + const ConfigOptionInt* option = config.option(key); + if (option != nullptr && option->value >= 1 && option->value <= num_filaments) + used_slots.insert(option->value - 1); + } + + static const std::vector keys_0based = { + "support_filament", + "support_interface_filament", + "wipe_tower_filament" + }; + for (const char* key : keys_0based) + { + const ConfigOptionInt* option = config.option(key); + if (option != nullptr && option->value >= 1 && option->value <= num_filaments) + used_slots.insert(option->value - 1); + } + + const ConfigOptionInt* extruder_option = config.option("extruder"); + if (extruder_option != nullptr && extruder_option->value >= 1 && extruder_option->value <= num_filaments) + used_slots.insert(extruder_option->value - 1); +} + +static void collect_filament_slots_from_model_config( + const ModelConfigObject& config, + int num_filaments, + std::set& used_slots) +{ + if (!config.has("extruder")) + return; + + const int extruder_id = config.extruder(); + if (extruder_id >= 1 && extruder_id <= num_filaments) + used_slots.insert(extruder_id - 1); +} + wxDEFINE_EVENT(EVT_SCHEDULE_BACKGROUND_PROCESS, SimpleEvent); wxDEFINE_EVENT(EVT_SLICING_UPDATE, SlicingStatusEvent); wxDEFINE_EVENT(EVT_SLICING_COMPLETED, wxCommandEvent); @@ -8420,6 +8486,9 @@ struct Plater::priv Plater *q; MainFrame *main_frame; bool filament_usage_sync_pending = false; + bool filament_temp_mixing_notification_initialized = false; + int filament_temp_mixing_notification_plate = -1; + FilamentTempMixingState filament_temp_mixing_notification_state = FilamentTempMixingState::Compatible; MenuFactory menus; @@ -13594,7 +13663,11 @@ void Plater::priv::on_process_completed(SlicingProcessCompletedEvent &evt) else { BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(":slicing all, plate %1% finished, start next slice...")%m_cur_slice_plate; - m_cur_slice_plate++; + m_cur_slice_plate = q->find_next_sliceable_plate_for_slice_all(m_cur_slice_plate + 1); + if (m_cur_slice_plate < 0) { + m_is_slicing = false; + return; + } q->Freeze(); q->select_plate(m_cur_slice_plate); @@ -13698,12 +13771,16 @@ void Plater::priv::on_action_slice_all(SimpleEvent&) Model::setExtruderParams(config, numExtruders); Model::setPrintSpeedTable(config, print_config); - if (!q->confirm_filament_temp_mixing_before_slice()) + if (!q->confirm_filament_temp_mixing_before_slice_all()) return; m_slice_all = true; m_slice_all_only_has_gcode = true; - m_cur_slice_plate = 0; + m_cur_slice_plate = q->find_next_sliceable_plate_for_slice_all(0); + if (m_cur_slice_plate < 0) { + m_slice_all = false; + return; + } //select plate q->select_plate(m_cur_slice_plate); q->reslice(); @@ -20103,69 +20180,52 @@ void Plater::config_change_notification(const DynamicPrintConfig &config, const // notification for more options } -bool Plater::check_filament_temp_mixing() +bool Plater::check_filament_temp_mixing(int plate_index) { + const int plate_count = p->partplate_list.get_plate_count(); + if (plate_index < 0 || plate_index >= plate_count) + return true; + const DynamicPrintConfig& full_cfg = wxGetApp().preset_bundle->full_config(); - auto* ft_opt = full_cfg.option("filament_type"); - if (!ft_opt || ft_opt->values.empty()) + const ConfigOptionStrings* filament_type_option = full_cfg.option("filament_type"); + if (filament_type_option == nullptr || filament_type_option->values.empty()) return true; - int num_filaments = (int)ft_opt->values.size(); + const int num_filaments = static_cast(filament_type_option->values.size()); std::set used_slots; - PartPlate* curr_plate = p->partplate_list.get_curr_plate(); - auto object_is_on_curr_plate = [curr_plate](size_t obj_idx, const ModelObject* mo) { - if (!curr_plate) - return true; - for (int inst_idx = 0; inst_idx < (int)mo->instances.size(); ++inst_idx) { - if (curr_plate->contain_instance((int)obj_idx, inst_idx)) - return true; - } - return false; - }; + PartPlate* plate = p->partplate_list.get_plate(plate_index); - bool has_object_on_curr_plate = false; + bool has_object_on_plate = false; for (size_t obj_idx = 0; obj_idx < wxGetApp().model().objects.size(); ++obj_idx) { - if (object_is_on_curr_plate(obj_idx, wxGetApp().model().objects[obj_idx])) { - has_object_on_curr_plate = true; + const ModelObject* model_object = wxGetApp().model().objects[obj_idx]; + if (model_object_is_on_plate(plate, obj_idx, model_object)) { + has_object_on_plate = true; break; } } - if (!has_object_on_curr_plate) + if (!has_object_on_plate) return true; - auto collect_from_cfg = [&](const DynamicPrintConfig& cfg) { - static const char* keys_1based[] = {"wall_filament", "solid_infill_filament"}; - for (auto key : keys_1based) { - auto* opt = cfg.option(key); - if (opt && opt->value >= 1 && opt->value <= num_filaments) - used_slots.insert(opt->value - 1); - } - static const char* keys_0based[] = {"support_filament", "support_interface_filament", "wipe_tower_filament"}; - for (auto key : keys_0based) { - auto* opt = cfg.option(key); - if (opt && opt->value >= 1 && opt->value <= num_filaments) - used_slots.insert(opt->value - 1); - } - }; - // Collect from the Plater's working config for global Process settings. // Defaults are 0 (skipped by the >= 1 check). - collect_from_cfg(*this->config()); + collect_filament_slots_from_config(*this->config(), num_filaments, used_slots); // Also collect from current plate's config for any plate-level overrides - if (curr_plate) - collect_from_cfg(*curr_plate->config()); + if (plate) + collect_filament_slots_from_config(*plate->config(), num_filaments, used_slots); // Collect from ModelVolume painting extruders for objects on the current plate for (size_t obj_idx = 0; obj_idx < wxGetApp().model().objects.size(); ++obj_idx) { - const ModelObject* mo = wxGetApp().model().objects[obj_idx]; - if (!object_is_on_curr_plate(obj_idx, mo)) + const ModelObject* model_object = wxGetApp().model().objects[obj_idx]; + if (!model_object_is_on_plate(plate, obj_idx, model_object)) continue; - for (const ModelVolume* mv : mo->volumes) { - for (int eid : mv->get_extruders()) { - if (eid >= 1 && eid <= num_filaments) - used_slots.insert(eid - 1); + collect_filament_slots_from_model_config(model_object->config, num_filaments, used_slots); + for (const ModelVolume* model_volume : model_object->volumes) { + collect_filament_slots_from_model_config(model_volume->config, num_filaments, used_slots); + for (int extruder_id : model_volume->get_extruders()) { + if (extruder_id >= 1 && extruder_id <= num_filaments) + used_slots.insert(extruder_id - 1); } } } @@ -20181,11 +20241,11 @@ bool Plater::check_filament_temp_mixing() bool has_high = false, has_low = false; for (int slot : used_slots) { - if (slot < (int)bundle->filament_presets.size()) { + if (slot < static_cast(bundle->filament_presets.size())) { const Preset* preset = bundle->filaments.find_preset( bundle->filament_presets[slot], true); - if (preset) { - bool is_high = preset->config.opt_bool("filament_is_high_temperature", 0); + if (preset != nullptr) { + const bool is_high = preset->config.opt_bool("filament_is_high_temperature", 0); if (is_high) has_high = true; else @@ -20193,14 +20253,19 @@ bool Plater::check_filament_temp_mixing() } } } - bool compatible = !(has_high && has_low); + const bool compatible = !(has_high && has_low); return compatible; } -Plater::FilamentTempMixingState Plater::get_filament_temp_mixing_state() +bool Plater::check_filament_temp_mixing() +{ + return check_filament_temp_mixing(p->partplate_list.get_curr_plate_index()); +} + +Plater::FilamentTempMixingState Plater::get_filament_temp_mixing_state(int plate_index) { - if (check_filament_temp_mixing()) + if (check_filament_temp_mixing(plate_index)) return FilamentTempMixingState::Compatible; return wxGetApp().app_config->get_bool("allow_filament_temp_mixing") ? @@ -20208,42 +20273,125 @@ Plater::FilamentTempMixingState Plater::get_filament_temp_mixing_state() FilamentTempMixingState::BlockedError; } +Plater::FilamentTempMixingState Plater::get_filament_temp_mixing_state() +{ + return get_filament_temp_mixing_state(p->partplate_list.get_curr_plate_index()); +} + +bool Plater::is_plate_blocked_by_filament_temp_mixing(int plate_index) +{ + return get_filament_temp_mixing_state(plate_index) == FilamentTempMixingState::BlockedError; +} + +bool Plater::has_sliceable_plate_for_slice_all() +{ + return find_next_sliceable_plate_for_slice_all(0) >= 0; +} + +int Plater::find_next_sliceable_plate_for_slice_all(int start_plate_index) +{ + const int plate_count = p->partplate_list.get_plate_count(); + if (start_plate_index < 0) + start_plate_index = 0; + + for (int plate_index = start_plate_index; plate_index < plate_count; ++plate_index) + { + PartPlate* plate = p->partplate_list.get_plate(plate_index); + if (plate != nullptr && plate->can_slice() && !is_plate_blocked_by_filament_temp_mixing(plate_index)) + return plate_index; + } + + return -1; +} + bool Plater::sync_filament_temp_mixing_notification() { - switch (get_filament_temp_mixing_state()) { + const int curr_plate_index = get_partplate_list().get_curr_plate_index(); + PartPlate* curr_plate = get_partplate_list().get_curr_plate(); + const FilamentTempMixingState mixing_state = get_filament_temp_mixing_state(curr_plate_index); + const bool notification_state_changed = + !p->filament_temp_mixing_notification_initialized || + p->filament_temp_mixing_notification_plate != curr_plate_index || + p->filament_temp_mixing_notification_state != mixing_state; + bool slicing_allowed = true; + + switch (mixing_state) { case FilamentTempMixingState::Compatible: get_notification_manager()->close_validate_error_notification(filament_temp_mixing_error_text()); get_notification_manager()->close_validate_warning_notification(filament_temp_mixing_warning_text()); get_partplate_list().get_curr_plate()->update_apply_result_invalid(false); - return true; + slicing_allowed = true; + break; case FilamentTempMixingState::AllowedWarning: get_notification_manager()->close_validate_error_notification(filament_temp_mixing_error_text()); get_partplate_list().get_curr_plate()->update_apply_result_invalid(false); - get_notification_manager()->push_notification( - NotificationType::ValidateWarning, - NotificationManager::NotificationLevel::WarningNotificationLevel, - _u8L("WARNING:") + "\n" + filament_temp_mixing_warning_text()); - return true; + if (notification_state_changed) { + get_notification_manager()->push_notification( + NotificationType::ValidateWarning, + NotificationManager::NotificationLevel::WarningNotificationLevel, + _u8L("WARNING:") + "\n" + filament_temp_mixing_warning_text()); + } + slicing_allowed = true; + break; case FilamentTempMixingState::BlockedError: { StringObjectException err; err.type = STRING_EXCEPT_FILAMENTS_DIFFERENT_TEMP; err.string = filament_temp_mixing_error_text(); get_notification_manager()->close_validate_warning_notification(filament_temp_mixing_warning_text()); - get_notification_manager()->push_validate_error_notification(err); + if (notification_state_changed) + get_notification_manager()->push_validate_error_notification(err); get_partplate_list().get_curr_plate()->update_apply_result_invalid(true); - return false; + slicing_allowed = false; + break; } } - return true; + p->filament_temp_mixing_notification_initialized = true; + p->filament_temp_mixing_notification_plate = curr_plate_index; + p->filament_temp_mixing_notification_state = mixing_state; + + const bool can_slice = curr_plate != nullptr && curr_plate->can_slice() && slicing_allowed; + p->main_frame->update_slice_print_status(MainFrame::eEventPlateUpdate, can_slice); + return slicing_allowed; } bool Plater::confirm_filament_temp_mixing_before_slice() { - if (get_filament_temp_mixing_state() != FilamentTempMixingState::AllowedWarning) + switch (get_filament_temp_mixing_state()) { + case FilamentTempMixingState::Compatible: + return true; + case FilamentTempMixingState::BlockedError: + sync_filament_temp_mixing_notification(); + return false; + case FilamentTempMixingState::AllowedWarning: + break; + } + + wxMessageDialog dlg(this, _L("This material combination may cause risks. Do you want to continue?"), + _L("Confirm"), wxYES_NO | wxICON_WARNING); + return dlg.ShowModal() == wxID_YES; +} + +bool Plater::confirm_filament_temp_mixing_before_slice_all() +{ + if (!has_sliceable_plate_for_slice_all()) + return false; + + bool has_allowed_warning = false; + for (int plate_index = 0; plate_index < p->partplate_list.get_plate_count(); ++plate_index) + { + if (get_filament_temp_mixing_state(plate_index) == FilamentTempMixingState::AllowedWarning) + { + has_allowed_warning = true; + break; + } + } + + if (!has_allowed_warning) return true; - wxMessageDialog dlg(this, _L("该组合可能导致风险,是否继续"), _L("确认"), wxYES_NO | wxICON_WARNING); + wxMessageDialog dlg(this, _L("This material combination may cause risks. Do you want to continue?"), + _L("Confirm"), wxYES_NO | wxICON_WARNING); return dlg.ShowModal() == wxID_YES; } @@ -21090,6 +21238,7 @@ int Plater::select_plate(int plate_index, bool need_slice) SimpleEvent event(EVT_GLCANVAS_PLATE_SELECT); p->on_plate_selected(event); + notify_filament_usage_changed(); BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(" %1%: plate %2%, return %3%")%__LINE__ %plate_index %ret; return ret; diff --git a/src/slic3r/GUI/Plater.hpp b/src/slic3r/GUI/Plater.hpp index 429f635cee2..f7bf0e93df7 100644 --- a/src/slic3r/GUI/Plater.hpp +++ b/src/slic3r/GUI/Plater.hpp @@ -513,14 +513,38 @@ class Plater: public wxPanel bool update_filament_colors_in_full_config(); void config_change_notification(const DynamicPrintConfig &config, const std::string& key); void on_config_change(const DynamicPrintConfig &config); - /// Check whether high-temperature and low-temperature filaments are mixed on the current plate. - /// Returns true if compatible (no mixing), false if mixing is detected and error notification is shown. + /// @brief Check whether high-temperature and low-temperature filaments are mixed on the current plate. + /// @return True if compatible; false if high/low temperature materials are mixed. bool check_filament_temp_mixing(); + /// @brief Check whether high-temperature and low-temperature filaments are mixed on a specific plate. + /// @param plate_index Plate index to check. + /// @return True if compatible or plate index is invalid; false if high/low temperature materials are mixed. + bool check_filament_temp_mixing(int plate_index); + /// @brief Get high/low temperature material mixing state for the current plate. + /// @return Current plate material mixing state. FilamentTempMixingState get_filament_temp_mixing_state(); + /// @brief Get high/low temperature material mixing state for a specific plate. + /// @param plate_index Plate index to check. + /// @return Plate material mixing state. + FilamentTempMixingState get_filament_temp_mixing_state(int plate_index); + /// @brief Check whether a specific plate is blocked by high/low temperature material mixing. + /// @param plate_index Plate index to check. + /// @return True if slicing this plate is blocked; otherwise false. + bool is_plate_blocked_by_filament_temp_mixing(int plate_index); + /// @brief Check whether slice-all has at least one plate that can be sliced. + /// @return True if any plate can be sliced and is not blocked by material mixing. + bool has_sliceable_plate_for_slice_all(); + /// @brief Find the next plate that can be sliced by slice-all. + /// @param start_plate_index First plate index to check. + /// @return Plate index if found; otherwise -1. + int find_next_sliceable_plate_for_slice_all(int start_plate_index); /// Sync notification state with current filament temp mixing status. /// Returns true if slicing is allowed, false if high/low temperature mixing blocks slicing. bool sync_filament_temp_mixing_notification(); bool confirm_filament_temp_mixing_before_slice(); + /// @brief Confirm warning-level high/low temperature material mixing before slicing all plates. + /// @return True if slice-all can continue; otherwise false. + bool confirm_filament_temp_mixing_before_slice_all(); /// Queue a single UI sync after filament preset/assignment/plate usage changes. void notify_filament_usage_changed(); void force_filament_colors_update(); From ab4044ecd48ceb2c9b0487185c3e7ef042c4f3b7 Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Wed, 10 Jun 2026 12:00:44 +0800 Subject: [PATCH 103/135] Fix filament temperature mixing slice guards Co-Authored-By: Claude Opus 4.7 --- src/slic3r/GUI/MainFrame.cpp | 15 +++++---- src/slic3r/GUI/Plater.cpp | 60 ++++++++++++++++++++++++++++++++---- src/slic3r/GUI/Plater.hpp | 2 ++ 3 files changed, 63 insertions(+), 14 deletions(-) diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index 0bcb1585ac1..e5a020c5310 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -568,7 +568,7 @@ DPIFrame(NULL, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, BORDERLESS_FRAME_ } return;} #endif - if (evt.CmdDown() && evt.GetKeyCode() == 'R') { if (m_slice_enable) { wxGetApp().plater()->update(true, true); wxPostEvent(m_plater, SimpleEvent(EVT_GLTOOLBAR_SLICE_PLATE)); this->m_tabpanel->SetSelection(tpPreview); } return; } + if (evt.CmdDown() && evt.GetKeyCode() == 'R') { if (m_slice_enable) { wxGetApp().plater()->update(true, true); wxPostEvent(m_plater, SimpleEvent(EVT_GLTOOLBAR_SLICE_PLATE)); } return; } if (evt.CmdDown() && evt.ShiftDown() && evt.GetKeyCode() == 'G') { m_plater->apply_background_progress(); m_print_enable = get_enable_print_status(); @@ -1662,7 +1662,6 @@ wxBoxSizer* MainFrame::create_side_tools() else wxPostEvent(m_plater, SimpleEvent(EVT_GLTOOLBAR_SLICE_PLATE)); - this->m_tabpanel->SetSelection(tpPreview); }); m_print_btn->Bind(wxEVT_BUTTON, [this](wxCommandEvent& event) @@ -2057,11 +2056,11 @@ void MainFrame::update_slice_print_status(SlicePrintEventType event, bool can_sl { bool enable_print = true, enable_slice = true; - if (!can_slice) - { - if (m_slice_select == eSlicePlate) - enable_slice = false; - } + if (event == eEventPlateUpdate) + enable_slice = get_enable_slice_status(); + else if (!can_slice) + enable_slice = false; + if (!can_print) enable_print = false; @@ -2073,7 +2072,7 @@ void MainFrame::update_slice_print_status(SlicePrintEventType event, bool can_sl } //process slice logic - if (enable_slice) + if (event != eEventPlateUpdate && enable_slice) { enable_slice = get_enable_slice_status(); } diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 75608f8d827..2d9bc2249a7 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -9398,7 +9398,21 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame) q->Bind(EVT_EXPORT_FINISHED, &priv::on_export_finished, this); q->Bind(EVT_GLVIEWTOOLBAR_3D, [q](SimpleEvent&) { q->select_view_3D("3D"); }); //BBS: set on_slice to false - q->Bind(EVT_GLVIEWTOOLBAR_PREVIEW, [q](SimpleEvent&) { q->select_view_3D("Preview", false); }); + q->Bind(EVT_GLVIEWTOOLBAR_PREVIEW, [q](SimpleEvent&) { + if (q->is_view3D_shown()) { + if (q->has_sliceable_plate_for_slice_all()) { + wxGetApp().mainframe->select_tab(size_t(MainFrame::tp3DEditor)); + wxPostEvent(q, SimpleEvent(EVT_GLTOOLBAR_SLICE_ALL)); + return; + } + if (q->is_plate_blocked_by_filament_temp_mixing(q->get_partplate_list().get_curr_plate_index())) { + wxGetApp().mainframe->select_tab(size_t(MainFrame::tp3DEditor)); + q->sync_filament_temp_mixing_notification(); + return; + } + } + q->select_view_3D("Preview", false); + }); q->Bind(EVT_GLTOOLBAR_SLICE_PLATE, &priv::on_action_slice_plate, this); q->Bind(EVT_GLTOOLBAR_SLICE_ALL, &priv::on_action_slice_all, this); q->Bind(EVT_GLTOOLBAR_PRINT_PLATE, &priv::on_action_print_plate, this); @@ -11833,7 +11847,10 @@ unsigned int Plater::priv::update_background_process(bool force_validation, bool notification_manager->set_all_slicing_errors_gray(true); notification_manager->close_notification_of_type(NotificationType::ValidateError); } - if (invalidated != Print::APPLY_STATUS_UNCHANGED && background_processing_enabled()) + else { + return_state |= UPDATE_BACKGROUND_PROCESS_INVALID; + } + if (filament_ok && invalidated != Print::APPLY_STATUS_UNCHANGED && background_processing_enabled()) return_state |= UPDATE_BACKGROUND_PROCESS_RESTART; if (printer_technology == ptFFF) { @@ -13749,7 +13766,7 @@ void Plater::priv::on_action_slice_plate(SimpleEvent&) Model::setPrintSpeedTable(config, print_config); m_slice_all = false; - if (!q->confirm_filament_temp_mixing_before_slice()) + if (!q->guard_before_slice_plate()) return; q->reslice(); @@ -13771,7 +13788,7 @@ void Plater::priv::on_action_slice_all(SimpleEvent&) Model::setExtruderParams(config, numExtruders); Model::setPrintSpeedTable(config, print_config); - if (!q->confirm_filament_temp_mixing_before_slice_all()) + if (!q->guard_before_slice_all()) return; m_slice_all = true; @@ -19409,6 +19426,18 @@ int Plater::start_next_slice() // Stop arrange and (or) optimize rotation tasks. //this->stop_jobs(); + if (is_plate_blocked_by_filament_temp_mixing(p->partplate_list.get_curr_plate_index())) { + sync_filament_temp_mixing_notification(); + if (p->m_slice_all) { + SlicingProcessCompletedEvent evt(EVT_PROCESS_COMPLETED, 0, + SlicingProcessCompletedEvent::Finished, nullptr); + wxQueueEvent(this, evt.Clone()); + return 0; + } + p->process_completed_with_error = p->partplate_list.get_curr_plate_index(); + return -1; + } + //FIXME Don't reslice if export of G-code or sending to OctoPrint is running. // bitmask of UpdateBackgroundProcessReturnState unsigned int state = this->p->update_background_process(true, false, false); @@ -20355,6 +20384,17 @@ bool Plater::sync_filament_temp_mixing_notification() return slicing_allowed; } +bool Plater::guard_before_slice_plate() +{ + sync_filament_temp_mixing_notification(); + return confirm_filament_temp_mixing_before_slice(); +} + +bool Plater::guard_before_slice_all() +{ + return confirm_filament_temp_mixing_before_slice_all(); +} + bool Plater::confirm_filament_temp_mixing_before_slice() { switch (get_filament_temp_mixing_state()) { @@ -20380,7 +20420,9 @@ bool Plater::confirm_filament_temp_mixing_before_slice_all() bool has_allowed_warning = false; for (int plate_index = 0; plate_index < p->partplate_list.get_plate_count(); ++plate_index) { - if (get_filament_temp_mixing_state(plate_index) == FilamentTempMixingState::AllowedWarning) + PartPlate* plate = p->partplate_list.get_plate(plate_index); + if (plate != nullptr && plate->can_slice() && + get_filament_temp_mixing_state(plate_index) == FilamentTempMixingState::AllowedWarning) { has_allowed_warning = true; break; @@ -21138,6 +21180,8 @@ int Plater::select_plate(int plate_index, bool need_slice) p->process_completed_with_error = -1; p->m_slice_all = false; reset_gcode_toolpaths(); + if (!guard_before_slice_plate()) + return ret; reslice(); } else { @@ -21192,8 +21236,11 @@ int Plater::select_plate(int plate_index, bool need_slice) //p->process_completed_with_error = -1; p->m_slice_all = false; reset_gcode_toolpaths(); - if (model_fits && !validate_err) + if (model_fits && !validate_err) { + if (!guard_before_slice_plate()) + return ret; reslice(); + } else { p->main_frame->update_slice_print_status(MainFrame::eEventPlateUpdate, false); //sometimes the previous print's sliced result is still valid, but the newly added object is laid over the boundary @@ -21238,6 +21285,7 @@ int Plater::select_plate(int plate_index, bool need_slice) SimpleEvent event(EVT_GLCANVAS_PLATE_SELECT); p->on_plate_selected(event); + sync_filament_temp_mixing_notification(); notify_filament_usage_changed(); BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(" %1%: plate %2%, return %3%")%__LINE__ %plate_index %ret; diff --git a/src/slic3r/GUI/Plater.hpp b/src/slic3r/GUI/Plater.hpp index f7bf0e93df7..1280f3529dc 100644 --- a/src/slic3r/GUI/Plater.hpp +++ b/src/slic3r/GUI/Plater.hpp @@ -541,6 +541,8 @@ class Plater: public wxPanel /// Sync notification state with current filament temp mixing status. /// Returns true if slicing is allowed, false if high/low temperature mixing blocks slicing. bool sync_filament_temp_mixing_notification(); + bool guard_before_slice_plate(); + bool guard_before_slice_all(); bool confirm_filament_temp_mixing_before_slice(); /// @brief Confirm warning-level high/low temperature material mixing before slicing all plates. /// @return True if slice-all can continue; otherwise false. From cd6aa6895374b7d62eb0208743d5f2634f797841 Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Wed, 10 Jun 2026 14:25:40 +0800 Subject: [PATCH 104/135] fix: add solid_infill_filament to Plater config for filament temp mixing detection The solid_infill_filament key was missing from p->config's default key list, causing collect_filament_slots_from_config() to never detect solid infill filament usage. This made the high/low temperature mixing guard silently skip solid infill while correctly catching all other auxiliary materials. Co-Authored-By: Claude Opus 4.7 --- src/slic3r/GUI/Plater.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 2d9bc2249a7..409abe281f3 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -9069,7 +9069,7 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame) "extruder_colour", "filament_colour", "material_colour", "printable_height", "printer_model", "printer_technology", // These values are necessary to construct SlicingParameters by the Canvas3D variable layer height editor. "layer_height", "initial_layer_print_height", "min_layer_height", "max_layer_height", - "brim_width", "wall_loops", "wall_filament", "sparse_infill_density", "enable_infill_filament_override", "infill_filament_use_base_first_layers", "infill_filament_use_base_last_layers", "sparse_infill_filament", "top_shell_layers", + "brim_width", "wall_loops", "wall_filament", "sparse_infill_density", "enable_infill_filament_override", "infill_filament_use_base_first_layers", "infill_filament_use_base_last_layers", "sparse_infill_filament", "solid_infill_filament", "top_shell_layers", "enable_support", "support_filament", "support_interface_filament", "support_top_z_distance", "support_bottom_z_distance", "raft_layers", "wipe_tower_rotation_angle", "wipe_tower_cone_angle", "wipe_tower_extra_spacing", "wipe_tower_extra_flow", "local_z_wipe_tower_purge_lines", "wipe_tower_max_purge_speed", From a31ce58e14628bf5ac8a856e6ba521dc3f78a7c1 Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Wed, 10 Jun 2026 14:28:41 +0800 Subject: [PATCH 105/135] Fix tab out of sync when switching between Prepare and Preview views Co-Authored-By: Claude Opus 4.7 --- src/slic3r/GUI/Plater.cpp | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 409abe281f3..0d813417b9f 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -8523,6 +8523,7 @@ struct Plater::priv PartPlateList partplate_list; //BBS: add a flag to ignore cancel event bool m_ignore_event{false}; + bool m_in_select_view_3D{false}; bool m_slice_all{false}; bool m_is_slicing {false}; bool m_is_publishing {false}; @@ -9703,12 +9704,17 @@ void Plater::priv::apply_free_camera_correction(bool apply/* = true*/) //BBS: add no slice option void Plater::priv::select_view_3D(const std::string& name, bool no_slice) { + if (m_in_select_view_3D) + return; + m_in_select_view_3D = true; + if (name == "3D") { BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << __LINE__ << "select view3D"; if (q->only_gcode_mode() || q->using_exported_file()) { BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format("goto preview page when loading gcode/exported_3mf"); } set_current_panel(view3D, no_slice); + wxGetApp().mainframe->select_tab(size_t(MainFrame::tp3DEditor)); } else if (name == "Preview") { BOOST_LOG_TRIVIAL(info) << "select preview"; @@ -9721,10 +9727,12 @@ void Plater::priv::select_view_3D(const std::string& name, bool no_slice) Model::setExtruderParams(config, numExtruders); Model::setPrintSpeedTable(config, print_config); set_current_panel(preview, no_slice); + wxGetApp().mainframe->select_tab(size_t(MainFrame::tpPreview)); } else if (name == "Assemble") { BOOST_LOG_TRIVIAL(info) << "select assemble view"; set_current_panel(assemble_view, no_slice); + wxGetApp().mainframe->select_tab(size_t(MainFrame::tp3DEditor)); } //BBS update selection @@ -9732,6 +9740,7 @@ void Plater::priv::select_view_3D(const std::string& name, bool no_slice) selection_changed(); apply_free_camera_correction(false); + m_in_select_view_3D = false; } void Plater::priv::select_next_view_3D() From 1b97cada69aebbbb89a67d1b6b1ac4b430f01bdb Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Wed, 10 Jun 2026 14:31:49 +0800 Subject: [PATCH 106/135] fix: per-plate filament temp mixing check using global config check_filament_temp_mixing incorrectly used the Plater's global config (p->config) when checking per-plate filament temperature mixing. This caused false positives on plates that don't use mixing when a 3MF project contains both mixed and non-mixed plates. Remove the global config collection and instead resolve the default extruder for objects with extruder=0 from full_config(), so only the filament slots actually used by each plate's objects are considered. Co-Authored-By: Claude Opus 4.7 --- src/slic3r/GUI/Plater.cpp | 44 +++++++++++++++++++++------------------ 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 0d813417b9f..09cc6a2a606 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -264,19 +264,6 @@ static void collect_filament_slots_from_config( used_slots.insert(extruder_option->value - 1); } -static void collect_filament_slots_from_model_config( - const ModelConfigObject& config, - int num_filaments, - std::set& used_slots) -{ - if (!config.has("extruder")) - return; - - const int extruder_id = config.extruder(); - if (extruder_id >= 1 && extruder_id <= num_filaments) - used_slots.insert(extruder_id - 1); -} - wxDEFINE_EVENT(EVT_SCHEDULE_BACKGROUND_PROCESS, SimpleEvent); wxDEFINE_EVENT(EVT_SLICING_UPDATE, SlicingStatusEvent); wxDEFINE_EVENT(EVT_SLICING_COMPLETED, wxCommandEvent); @@ -20245,22 +20232,31 @@ bool Plater::check_filament_temp_mixing(int plate_index) if (!has_object_on_plate) return true; - // Collect from the Plater's working config for global Process settings. - // Defaults are 0 (skipped by the >= 1 check). - collect_filament_slots_from_config(*this->config(), num_filaments, used_slots); - // Also collect from current plate's config for any plate-level overrides if (plate) collect_filament_slots_from_config(*plate->config(), num_filaments, used_slots); - // Collect from ModelVolume painting extruders for objects on the current plate + // Collect from ModelVolume painting extruders and object configs for + // objects on the current plate. Track whether any object relies on the + // global default extruder so we can resolve it at the end. + bool uses_default_extruder = false; for (size_t obj_idx = 0; obj_idx < wxGetApp().model().objects.size(); ++obj_idx) { const ModelObject* model_object = wxGetApp().model().objects[obj_idx]; if (!model_object_is_on_plate(plate, obj_idx, model_object)) continue; - collect_filament_slots_from_model_config(model_object->config, num_filaments, used_slots); + + const int obj_extruder = model_object->config.extruder(); + if (obj_extruder >= 1 && obj_extruder <= num_filaments) + used_slots.insert(obj_extruder - 1); + else if (obj_extruder == 0) + uses_default_extruder = true; + for (const ModelVolume* model_volume : model_object->volumes) { - collect_filament_slots_from_model_config(model_volume->config, num_filaments, used_slots); + if (model_volume->config.has("extruder")) { + const int vol_extruder = model_volume->config.extruder(); + if (vol_extruder >= 1 && vol_extruder <= num_filaments) + used_slots.insert(vol_extruder - 1); + } for (int extruder_id : model_volume->get_extruders()) { if (extruder_id >= 1 && extruder_id <= num_filaments) used_slots.insert(extruder_id - 1); @@ -20268,6 +20264,14 @@ bool Plater::check_filament_temp_mixing(int plate_index) } } + // Resolve the global default extruder if any object on this plate + // uses extruder=0 (meaning "inherit from global config"). + if (uses_default_extruder) { + const ConfigOptionInt* extruder_opt = full_cfg.option("extruder"); + if (extruder_opt != nullptr && extruder_opt->value >= 1 && extruder_opt->value <= num_filaments) + used_slots.insert(extruder_opt->value - 1); + } + if (used_slots.empty()) return true; From ef47bb7bc5636ff269485a85a7c5de2804036b04 Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Wed, 10 Jun 2026 15:16:37 +0800 Subject: [PATCH 107/135] Fix tab view sync and filament temp mixing notification lifecycle - Sync main tab with view panel in select_view_3D via select_tab - Skip redundant GL toolbar event post when view already matches tab - Allow Preview tab switch when plate is blocked by temp mixing - Always re-push filament temp mixing notification to handle external dismiss Co-Authored-By: Claude Opus 4.7 --- src/slic3r/GUI/MainFrame.cpp | 6 ++-- src/slic3r/GUI/Plater.cpp | 66 +++++++++++++++++------------------- 2 files changed, 36 insertions(+), 36 deletions(-) diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index e5a020c5310..7d0b0ebff51 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -1046,11 +1046,13 @@ void MainFrame::init_tabpanel() { m_last_selected_tab = m_tabpanel->GetSelection(); if (panel == m_plater) { if (sel == tp3DEditor) { - wxPostEvent(m_plater, SimpleEvent(EVT_GLVIEWTOOLBAR_3D)); + if (!m_plater || !m_plater->is_view3D_shown()) + wxPostEvent(m_plater, SimpleEvent(EVT_GLVIEWTOOLBAR_3D)); m_param_panel->OnActivate(); } else if (sel == tpPreview) { - wxPostEvent(m_plater, SimpleEvent(EVT_GLVIEWTOOLBAR_PREVIEW)); + if (!m_plater || m_plater->is_view3D_shown()) + wxPostEvent(m_plater, SimpleEvent(EVT_GLVIEWTOOLBAR_PREVIEW)); m_param_panel->OnActivate(); } } diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 09cc6a2a606..956f4beb2b9 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -264,6 +264,19 @@ static void collect_filament_slots_from_config( used_slots.insert(extruder_option->value - 1); } +static void collect_filament_slots_from_model_config( + const ModelConfigObject& config, + int num_filaments, + std::set& used_slots) +{ + if (!config.has("extruder")) + return; + + const int extruder_id = config.extruder(); + if (extruder_id >= 1 && extruder_id <= num_filaments) + used_slots.insert(extruder_id - 1); +} + wxDEFINE_EVENT(EVT_SCHEDULE_BACKGROUND_PROCESS, SimpleEvent); wxDEFINE_EVENT(EVT_SLICING_UPDATE, SlicingStatusEvent); wxDEFINE_EVENT(EVT_SLICING_COMPLETED, wxCommandEvent); @@ -8510,7 +8523,6 @@ struct Plater::priv PartPlateList partplate_list; //BBS: add a flag to ignore cancel event bool m_ignore_event{false}; - bool m_in_select_view_3D{false}; bool m_slice_all{false}; bool m_is_slicing {false}; bool m_is_publishing {false}; @@ -9394,8 +9406,8 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame) return; } if (q->is_plate_blocked_by_filament_temp_mixing(q->get_partplate_list().get_curr_plate_index())) { - wxGetApp().mainframe->select_tab(size_t(MainFrame::tp3DEditor)); q->sync_filament_temp_mixing_notification(); + q->select_view_3D("Preview", true); return; } } @@ -9691,10 +9703,6 @@ void Plater::priv::apply_free_camera_correction(bool apply/* = true*/) //BBS: add no slice option void Plater::priv::select_view_3D(const std::string& name, bool no_slice) { - if (m_in_select_view_3D) - return; - m_in_select_view_3D = true; - if (name == "3D") { BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << __LINE__ << "select view3D"; if (q->only_gcode_mode() || q->using_exported_file()) { @@ -9727,7 +9735,6 @@ void Plater::priv::select_view_3D(const std::string& name, bool no_slice) selection_changed(); apply_free_camera_correction(false); - m_in_select_view_3D = false; } void Plater::priv::select_next_view_3D() @@ -20232,31 +20239,27 @@ bool Plater::check_filament_temp_mixing(int plate_index) if (!has_object_on_plate) return true; + // Collect from the Plater's working config for global Process settings. + // Defaults are 0 (skipped by the >= 1 check). + collect_filament_slots_from_config(*this->config(), num_filaments, used_slots); + // Also collect from current plate's config for any plate-level overrides if (plate) collect_filament_slots_from_config(*plate->config(), num_filaments, used_slots); - // Collect from ModelVolume painting extruders and object configs for - // objects on the current plate. Track whether any object relies on the - // global default extruder so we can resolve it at the end. + // Collect from ModelVolume painting extruders for objects on the + // current plate. Also track whether any object relies on the global + // default extruder (extruder=0) so we can resolve it at the end. bool uses_default_extruder = false; for (size_t obj_idx = 0; obj_idx < wxGetApp().model().objects.size(); ++obj_idx) { const ModelObject* model_object = wxGetApp().model().objects[obj_idx]; if (!model_object_is_on_plate(plate, obj_idx, model_object)) continue; - - const int obj_extruder = model_object->config.extruder(); - if (obj_extruder >= 1 && obj_extruder <= num_filaments) - used_slots.insert(obj_extruder - 1); - else if (obj_extruder == 0) + collect_filament_slots_from_model_config(model_object->config, num_filaments, used_slots); + if (model_object->config.extruder() == 0) uses_default_extruder = true; - for (const ModelVolume* model_volume : model_object->volumes) { - if (model_volume->config.has("extruder")) { - const int vol_extruder = model_volume->config.extruder(); - if (vol_extruder >= 1 && vol_extruder <= num_filaments) - used_slots.insert(vol_extruder - 1); - } + collect_filament_slots_from_model_config(model_volume->config, num_filaments, used_slots); for (int extruder_id : model_volume->get_extruders()) { if (extruder_id >= 1 && extruder_id <= num_filaments) used_slots.insert(extruder_id - 1); @@ -20265,7 +20268,9 @@ bool Plater::check_filament_temp_mixing(int plate_index) } // Resolve the global default extruder if any object on this plate - // uses extruder=0 (meaning "inherit from global config"). + // uses extruder=0. p->config does not include the "extruder" key + // (it is not in the initializer list at priv constructor), so we + // must read it from full_config() instead. if (uses_default_extruder) { const ConfigOptionInt* extruder_opt = full_cfg.option("extruder"); if (extruder_opt != nullptr && extruder_opt->value >= 1 && extruder_opt->value <= num_filaments) @@ -20351,10 +20356,6 @@ bool Plater::sync_filament_temp_mixing_notification() const int curr_plate_index = get_partplate_list().get_curr_plate_index(); PartPlate* curr_plate = get_partplate_list().get_curr_plate(); const FilamentTempMixingState mixing_state = get_filament_temp_mixing_state(curr_plate_index); - const bool notification_state_changed = - !p->filament_temp_mixing_notification_initialized || - p->filament_temp_mixing_notification_plate != curr_plate_index || - p->filament_temp_mixing_notification_state != mixing_state; bool slicing_allowed = true; switch (mixing_state) { @@ -20367,12 +20368,10 @@ bool Plater::sync_filament_temp_mixing_notification() case FilamentTempMixingState::AllowedWarning: get_notification_manager()->close_validate_error_notification(filament_temp_mixing_error_text()); get_partplate_list().get_curr_plate()->update_apply_result_invalid(false); - if (notification_state_changed) { - get_notification_manager()->push_notification( - NotificationType::ValidateWarning, - NotificationManager::NotificationLevel::WarningNotificationLevel, - _u8L("WARNING:") + "\n" + filament_temp_mixing_warning_text()); - } + get_notification_manager()->push_notification( + NotificationType::ValidateWarning, + NotificationManager::NotificationLevel::WarningNotificationLevel, + _u8L("WARNING:") + "\n" + filament_temp_mixing_warning_text()); slicing_allowed = true; break; case FilamentTempMixingState::BlockedError: { @@ -20380,8 +20379,7 @@ bool Plater::sync_filament_temp_mixing_notification() err.type = STRING_EXCEPT_FILAMENTS_DIFFERENT_TEMP; err.string = filament_temp_mixing_error_text(); get_notification_manager()->close_validate_warning_notification(filament_temp_mixing_warning_text()); - if (notification_state_changed) - get_notification_manager()->push_validate_error_notification(err); + get_notification_manager()->push_validate_error_notification(err); get_partplate_list().get_curr_plate()->update_apply_result_invalid(true); slicing_allowed = false; break; From ffde9f58da88afe2484c0f3bbbb3d3eb6e516982 Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Wed, 10 Jun 2026 15:47:57 +0800 Subject: [PATCH 108/135] fix: unify filament temp mixing confirm dialog style and add translations Replace wxMessageDialog with MessageDialog in both mixing confirm functions to match the "High and Low Temperature Material Mixing Risk" dialog style in Preferences. Change dialog title to "Confirm slicing" with custom Confirm/Cancel button labels. Add Chinese translations for "Confirm slicing" and the mixing risk message text. Co-Authored-By: Claude Opus 4.7 --- localization/i18n/Snapmaker_Orca.pot | 6 ++++++ localization/i18n/zh_CN/Snapmaker_Orca_zh_CN.po | 6 ++++++ src/slic3r/GUI/Plater.cpp | 16 ++++++++++------ 3 files changed, 22 insertions(+), 6 deletions(-) diff --git a/localization/i18n/Snapmaker_Orca.pot b/localization/i18n/Snapmaker_Orca.pot index e0de29e2733..bbccf182eda 100644 --- a/localization/i18n/Snapmaker_Orca.pot +++ b/localization/i18n/Snapmaker_Orca.pot @@ -2850,6 +2850,12 @@ msgstr "" msgid "Confirm" msgstr "" +msgid "Confirm slicing" +msgstr "" + +msgid "This material combination may cause risks. Do you want to continue?" +msgstr "" + msgid "Close" msgstr "" diff --git a/localization/i18n/zh_CN/Snapmaker_Orca_zh_CN.po b/localization/i18n/zh_CN/Snapmaker_Orca_zh_CN.po index 353d1f9b3f1..3c74376d6e4 100644 --- a/localization/i18n/zh_CN/Snapmaker_Orca_zh_CN.po +++ b/localization/i18n/zh_CN/Snapmaker_Orca_zh_CN.po @@ -2826,6 +2826,9 @@ msgstr "AMS 材料设置" msgid "Confirm" msgstr "确定" +msgid "Confirm slicing" +msgstr "请确认是否切片" + msgid "Close" msgstr "关闭" @@ -15262,6 +15265,9 @@ msgstr "检测到高温材料与低温材料同时使用。混合打印可能导 msgid "Detected both high and low temperature materials. Mixed printing may result in extruder clogging, nozzle damage, or layer adhesion issues. To continue printing, enable \"Allow mixed printing of high and low temperature materials\" in Preferences." msgstr "检测到高温材料与低温材料同时使用。混合打印可能导致挤出机堵塞、喷嘴损坏或层间粘附问题。如需继续打印,请在「偏好设置」中开启 \"允许高/低温材料混合打印\"。" +msgid "This material combination may cause risks. Do you want to continue?" +msgstr "此材料组合可能存在风险,是否继续?" + msgid "Allow high/low temperature filament mixing" msgstr "允许高/低温材料混合打印" diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 956f4beb2b9..6e3dcaace3f 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -20418,9 +20418,11 @@ bool Plater::confirm_filament_temp_mixing_before_slice() break; } - wxMessageDialog dlg(this, _L("This material combination may cause risks. Do you want to continue?"), - _L("Confirm"), wxYES_NO | wxICON_WARNING); - return dlg.ShowModal() == wxID_YES; + MessageDialog dlg(this, _L("This material combination may cause risks. Do you want to continue?"), + _L("Confirm slicing"), wxICON_WARNING | wxOK | wxCANCEL); + dlg.SetButtonLabel(wxID_OK, _L("Confirm")); + dlg.SetButtonLabel(wxID_CANCEL, _L("Cancel")); + return dlg.ShowModal() == wxID_OK; } bool Plater::confirm_filament_temp_mixing_before_slice_all() @@ -20443,9 +20445,11 @@ bool Plater::confirm_filament_temp_mixing_before_slice_all() if (!has_allowed_warning) return true; - wxMessageDialog dlg(this, _L("This material combination may cause risks. Do you want to continue?"), - _L("Confirm"), wxYES_NO | wxICON_WARNING); - return dlg.ShowModal() == wxID_YES; + MessageDialog dlg(this, _L("This material combination may cause risks. Do you want to continue?"), + _L("Confirm slicing"), wxICON_WARNING | wxOK | wxCANCEL); + dlg.SetButtonLabel(wxID_OK, _L("Confirm")); + dlg.SetButtonLabel(wxID_CANCEL, _L("Cancel")); + return dlg.ShowModal() == wxID_OK; } void Plater::notify_filament_usage_changed() From 0efd7335f62e8193a4bc3dc245d93666e4cb96b3 Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Thu, 11 Jun 2026 12:12:07 +0800 Subject: [PATCH 109/135] fix: exclude global Process config from per-plate filament temp mixing check The global Plater config's wall_filament/solid_infill_filament etc. were unconditionally collected for every plate, causing false positives when a filament assigned in the global config was not actually used on a given plate (e.g., solid_infill_filament=3(ABS) flagged Plate 2 which only used extruder=0(PLA-AERO)). Remove collect_filament_slots_from_config(*this->config(), ...) so the per-plate check only considers plate-level overrides, object/volume extruder assignments, and global default extruder resolution. Co-Authored-By: Claude Opus 4.7 --- src/slic3r/GUI/Plater.cpp | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 6e3dcaace3f..ed7d097f770 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -20239,10 +20239,6 @@ bool Plater::check_filament_temp_mixing(int plate_index) if (!has_object_on_plate) return true; - // Collect from the Plater's working config for global Process settings. - // Defaults are 0 (skipped by the >= 1 check). - collect_filament_slots_from_config(*this->config(), num_filaments, used_slots); - // Also collect from current plate's config for any plate-level overrides if (plate) collect_filament_slots_from_config(*plate->config(), num_filaments, used_slots); From d3760b9d33f37854dd17be937bd657b02915987c Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Thu, 11 Jun 2026 15:26:53 +0800 Subject: [PATCH 110/135] fix: "Slice All" button stuck disabled after loading multi-plate project After loading a 3MF with plates that have different filament temp mixing states, update_slice_print_status(eEventSliceUpdate, false) from update_background_process hard-disabled the button without consulting get_enable_slice_status(), which correctly checks all plates in eSliceAll mode. Also initialize m_apply_invalid for all plates synchronously after load instead of waiting for the async event on the current plate only. Co-Authored-By: Claude Opus 4.7 --- src/slic3r/GUI/MainFrame.cpp | 5 ++++- src/slic3r/GUI/Plater.cpp | 18 +++++++++++++++++- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index 7d0b0ebff51..ef15a3909ae 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -2061,7 +2061,10 @@ void MainFrame::update_slice_print_status(SlicePrintEventType event, bool can_sl if (event == eEventPlateUpdate) enable_slice = get_enable_slice_status(); else if (!can_slice) - enable_slice = false; + // Don't hard-disable the slice button; let get_enable_slice_status() + // decide. In eSliceAll mode other plates may still be sliceable even + // if the caller thinks the current plate isn't. + enable_slice = get_enable_slice_status(); if (!can_print) enable_print = false; diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index ed7d097f770..8e1a6bd850d 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -17258,8 +17258,24 @@ std::vector Plater::load_files(const std::vector& input_files, //BBS: wish to reset all plates stats item selected state when load a new file p->preview->get_canvas3d()->reset_select_plate_toolbar_selection(); std::vector loaded = p->load_files(input_files, strategy, ask_multi); - if (!loaded.empty()) + if (!loaded.empty()) { + // After loading a project, initialize the filament temp mixing state + // for ALL plates, not just the current one. This ensures each plate's + // m_apply_invalid flag is correct so that "Slice All" mode correctly + // detects which plates are sliceable. + PartPlateList& plate_list = get_partplate_list(); + for (int i = 0; i < plate_list.get_plate_count(); ++i) { + FilamentTempMixingState state = get_filament_temp_mixing_state(i); + PartPlate* plate = plate_list.get_plate(i); + if (plate) { + plate->update_apply_result_invalid( + state == FilamentTempMixingState::BlockedError); + } + } notify_filament_usage_changed(); + // Force a sync for the current plate's notification display + sync_filament_temp_mixing_notification(); + } return loaded; } From 51cb4fa482bcae6f096c259d98affc4e6bc364f1 Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Thu, 11 Jun 2026 17:00:32 +0800 Subject: [PATCH 111/135] fix: restore filament temp mixing validation in Print::validate for CLI, fix style issues - Restore check_multi_filament_valid() in Print::validate() so CLI slicing is protected against high/low temperature filament mixing (the GUI layer has its own per-plate check that runs before validate()). This also reactivates the CLI_FILAMENTS_DIFFERENT_TEMP error path. - Fix Allman brace style for all new code in Plater.cpp (18 occurrences). - Wrap two long string literals (206, 211) to fit within 120 characters. - Add const qualifier on immutable local variable in load_files(). - Add Doxygen comments for guard_before_slice_plate/all and confirm_filament_temp_mixing_before_slice in Plater.hpp. - Fix indentation consistency in NotificationManager.hpp. Co-Authored-By: Claude Opus 4.7 --- src/libslic3r/Print.cpp | 9 ++++ src/slic3r/GUI/NotificationManager.hpp | 6 +-- src/slic3r/GUI/Plater.cpp | 72 ++++++++++++++++++-------- src/slic3r/GUI/Plater.hpp | 7 ++- 4 files changed, 67 insertions(+), 27 deletions(-) diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index f5eb30af3c6..137d9fd68d4 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -1558,6 +1558,15 @@ StringObjectException Print::validate(StringObjectException *warning, Polygons* if (extruders.empty()) return { L("No extrusions under current settings.") }; + // Check for high/low temperature filament mixing. + if (extruders.size() > 1) { + auto ret = check_multi_filament_valid(*this); + if (!ret.string.empty()) { + ret.type = STRING_EXCEPT_FILAMENTS_DIFFERENT_TEMP; + return ret; + } + } + if (m_config.print_sequence == PrintSequence::ByObject) { if (m_config.timelapse_type == TimelapseType::tlSmooth) return {L("Smooth mode of timelapse is not supported when \"by object\" sequence is enabled.")}; diff --git a/src/slic3r/GUI/NotificationManager.hpp b/src/slic3r/GUI/NotificationManager.hpp index 27c51f7cd5a..dd3d4ff9457 100644 --- a/src/slic3r/GUI/NotificationManager.hpp +++ b/src/slic3r/GUI/NotificationManager.hpp @@ -901,9 +901,9 @@ class NotificationManager NotificationType::PlaterWarning, NotificationType::ProgressBar, NotificationType::PrintHostUpload, - NotificationType::ValidateError, - NotificationType::ValidateWarning, - NotificationType::SimplifySuggestion + NotificationType::ValidateError, + NotificationType::ValidateWarning, + NotificationType::SimplifySuggestion }; //prepared (basic) notifications // non-static so its not loaded too early. If static, the translations wont load correctly. diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 8e1a6bd850d..b37ad3f82a5 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -203,12 +203,18 @@ namespace GUI { static std::string filament_temp_mixing_warning_text() { - return _u8L("Detected both high and low temperature materials. Mixed printing may result in extruder clogging, nozzle damage, or layer adhesion issues."); + return _u8L("Detected both high and low temperature materials. " + "Mixed printing may result in extruder clogging, " + "nozzle damage, or layer adhesion issues."); } static std::string filament_temp_mixing_error_text() { - return _u8L("Detected both high and low temperature materials. Mixed printing may result in extruder clogging, nozzle damage, or layer adhesion issues. To continue printing, enable \"Allow mixed printing of high and low temperature materials\" in Preferences."); + return _u8L("Detected both high and low temperature materials. " + "Mixed printing may result in extruder clogging, " + "nozzle damage, or layer adhesion issues. " + "To continue printing, enable \"Allow mixed printing " + "of high and low temperature materials\" in Preferences."); } static bool model_object_is_on_plate(PartPlate* plate, size_t obj_idx, const ModelObject* model_object) @@ -13684,7 +13690,8 @@ void Plater::priv::on_process_completed(SlicingProcessCompletedEvent &evt) { BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(":slicing all, plate %1% finished, start next slice...")%m_cur_slice_plate; m_cur_slice_plate = q->find_next_sliceable_plate_for_slice_all(m_cur_slice_plate + 1); - if (m_cur_slice_plate < 0) { + if (m_cur_slice_plate < 0) + { m_is_slicing = false; return; } @@ -13797,7 +13804,8 @@ void Plater::priv::on_action_slice_all(SimpleEvent&) m_slice_all = true; m_slice_all_only_has_gcode = true; m_cur_slice_plate = q->find_next_sliceable_plate_for_slice_all(0); - if (m_cur_slice_plate < 0) { + if (m_cur_slice_plate < 0) + { m_slice_all = false; return; } @@ -17258,16 +17266,19 @@ std::vector Plater::load_files(const std::vector& input_files, //BBS: wish to reset all plates stats item selected state when load a new file p->preview->get_canvas3d()->reset_select_plate_toolbar_selection(); std::vector loaded = p->load_files(input_files, strategy, ask_multi); - if (!loaded.empty()) { + if (!loaded.empty()) + { // After loading a project, initialize the filament temp mixing state // for ALL plates, not just the current one. This ensures each plate's // m_apply_invalid flag is correct so that "Slice All" mode correctly // detects which plates are sliceable. PartPlateList& plate_list = get_partplate_list(); - for (int i = 0; i < plate_list.get_plate_count(); ++i) { - FilamentTempMixingState state = get_filament_temp_mixing_state(i); + for (int i = 0; i < plate_list.get_plate_count(); ++i) + { + const FilamentTempMixingState state = get_filament_temp_mixing_state(i); PartPlate* plate = plate_list.get_plate(i); - if (plate) { + if (plate) + { plate->update_apply_result_invalid( state == FilamentTempMixingState::BlockedError); } @@ -19445,9 +19456,11 @@ int Plater::start_next_slice() // Stop arrange and (or) optimize rotation tasks. //this->stop_jobs(); - if (is_plate_blocked_by_filament_temp_mixing(p->partplate_list.get_curr_plate_index())) { + if (is_plate_blocked_by_filament_temp_mixing(p->partplate_list.get_curr_plate_index())) + { sync_filament_temp_mixing_notification(); - if (p->m_slice_all) { + if (p->m_slice_all) + { SlicingProcessCompletedEvent evt(EVT_PROCESS_COMPLETED, 0, SlicingProcessCompletedEvent::Finished, nullptr); wxQueueEvent(this, evt.Clone()); @@ -20245,9 +20258,11 @@ bool Plater::check_filament_temp_mixing(int plate_index) PartPlate* plate = p->partplate_list.get_plate(plate_index); bool has_object_on_plate = false; - for (size_t obj_idx = 0; obj_idx < wxGetApp().model().objects.size(); ++obj_idx) { + for (size_t obj_idx = 0; obj_idx < wxGetApp().model().objects.size(); ++obj_idx) + { const ModelObject* model_object = wxGetApp().model().objects[obj_idx]; - if (model_object_is_on_plate(plate, obj_idx, model_object)) { + if (model_object_is_on_plate(plate, obj_idx, model_object)) + { has_object_on_plate = true; break; } @@ -20263,16 +20278,19 @@ bool Plater::check_filament_temp_mixing(int plate_index) // current plate. Also track whether any object relies on the global // default extruder (extruder=0) so we can resolve it at the end. bool uses_default_extruder = false; - for (size_t obj_idx = 0; obj_idx < wxGetApp().model().objects.size(); ++obj_idx) { + for (size_t obj_idx = 0; obj_idx < wxGetApp().model().objects.size(); ++obj_idx) + { const ModelObject* model_object = wxGetApp().model().objects[obj_idx]; if (!model_object_is_on_plate(plate, obj_idx, model_object)) continue; collect_filament_slots_from_model_config(model_object->config, num_filaments, used_slots); if (model_object->config.extruder() == 0) uses_default_extruder = true; - for (const ModelVolume* model_volume : model_object->volumes) { + for (const ModelVolume* model_volume : model_object->volumes) + { collect_filament_slots_from_model_config(model_volume->config, num_filaments, used_slots); - for (int extruder_id : model_volume->get_extruders()) { + for (int extruder_id : model_volume->get_extruders()) + { if (extruder_id >= 1 && extruder_id <= num_filaments) used_slots.insert(extruder_id - 1); } @@ -20283,7 +20301,8 @@ bool Plater::check_filament_temp_mixing(int plate_index) // uses extruder=0. p->config does not include the "extruder" key // (it is not in the initializer list at priv constructor), so we // must read it from full_config() instead. - if (uses_default_extruder) { + if (uses_default_extruder) + { const ConfigOptionInt* extruder_opt = full_cfg.option("extruder"); if (extruder_opt != nullptr && extruder_opt->value >= 1 && extruder_opt->value <= num_filaments) used_slots.insert(extruder_opt->value - 1); @@ -20299,11 +20318,14 @@ bool Plater::check_filament_temp_mixing(int plate_index) PresetBundle* bundle = wxGetApp().preset_bundle; bool has_high = false, has_low = false; - for (int slot : used_slots) { - if (slot < static_cast(bundle->filament_presets.size())) { + for (int slot : used_slots) + { + if (slot < static_cast(bundle->filament_presets.size())) + { const Preset* preset = bundle->filaments.find_preset( bundle->filament_presets[slot], true); - if (preset != nullptr) { + if (preset != nullptr) + { const bool is_high = preset->config.opt_bool("filament_is_high_temperature", 0); if (is_high) has_high = true; @@ -20370,7 +20392,8 @@ bool Plater::sync_filament_temp_mixing_notification() const FilamentTempMixingState mixing_state = get_filament_temp_mixing_state(curr_plate_index); bool slicing_allowed = true; - switch (mixing_state) { + switch (mixing_state) + { case FilamentTempMixingState::Compatible: get_notification_manager()->close_validate_error_notification(filament_temp_mixing_error_text()); get_notification_manager()->close_validate_warning_notification(filament_temp_mixing_warning_text()); @@ -20420,7 +20443,8 @@ bool Plater::guard_before_slice_all() bool Plater::confirm_filament_temp_mixing_before_slice() { - switch (get_filament_temp_mixing_state()) { + switch (get_filament_temp_mixing_state()) + { case FilamentTempMixingState::Compatible: return true; case FilamentTempMixingState::BlockedError: @@ -21263,12 +21287,14 @@ int Plater::select_plate(int plate_index, bool need_slice) //p->process_completed_with_error = -1; p->m_slice_all = false; reset_gcode_toolpaths(); - if (model_fits && !validate_err) { + if (model_fits && !validate_err) + { if (!guard_before_slice_plate()) return ret; reslice(); } - else { + else + { p->main_frame->update_slice_print_status(MainFrame::eEventPlateUpdate, false); //sometimes the previous print's sliced result is still valid, but the newly added object is laid over the boundary //then the print toolpath will be shown, so we should not refresh print here, only onload shell diff --git a/src/slic3r/GUI/Plater.hpp b/src/slic3r/GUI/Plater.hpp index 1280f3529dc..9d3873a9c21 100644 --- a/src/slic3r/GUI/Plater.hpp +++ b/src/slic3r/GUI/Plater.hpp @@ -232,7 +232,8 @@ class Plater: public wxPanel { public: using fs_path = boost::filesystem::path; - enum class FilamentTempMixingState { + enum class FilamentTempMixingState + { Compatible, AllowedWarning, BlockedError @@ -541,8 +542,12 @@ class Plater: public wxPanel /// Sync notification state with current filament temp mixing status. /// Returns true if slicing is allowed, false if high/low temperature mixing blocks slicing. bool sync_filament_temp_mixing_notification(); + /// Check and guard filament temp mixing before slicing current plate. bool guard_before_slice_plate(); + /// Check and guard filament temp mixing before slicing all plates. bool guard_before_slice_all(); + /// @brief Show confirmation dialog for allowed high/low temperature mixing before slice. + /// @return True if slicing can proceed; false if blocked or user cancelled. bool confirm_filament_temp_mixing_before_slice(); /// @brief Confirm warning-level high/low temperature material mixing before slicing all plates. /// @return True if slice-all can continue; otherwise false. From 297c3e5ab363c56e154716b057e4668f1d4cf96d Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Thu, 11 Jun 2026 20:11:13 +0800 Subject: [PATCH 112/135] fix: conditional global key collection and CLI-only filament temp mixing guard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Plater.cpp: collect wall/sparse/solid_infill_filament from global process config only when objects on the plate use extruder=0 (default). Global feature keys (wipe_tower/support) are always collected. This fixes both missed detection (plate with e=0 objects whose wall_filament differs from wipe_tower) and ensures correctness. - Snapmaker_Orca.cpp: add explicit filament temp mixing check before Print::validate() in CLI slicing path, using the public check_multi_filaments_compatibility(). CLI has no preference concept so mixing always blocks. - Print.cpp: keep validate() clean — the check belongs in the respective entry points (GUI: sync_filament_temp_mixing_notification, CLI: explicit guard) rather than in the shared validate() path. Co-Authored-By: Claude Opus 4.7 --- src/Snapmaker_Orca.cpp | 21 +++++++++++++++++++++ src/libslic3r/Print.cpp | 9 --------- src/slic3r/GUI/Plater.cpp | 39 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 60 insertions(+), 9 deletions(-) diff --git a/src/Snapmaker_Orca.cpp b/src/Snapmaker_Orca.cpp index 6f173e1b046..365be8d3679 100644 --- a/src/Snapmaker_Orca.cpp +++ b/src/Snapmaker_Orca.cpp @@ -4991,6 +4991,27 @@ int CLI::run(int argc, char **argv) print->apply(model, new_print_config); BOOST_LOG_TRIVIAL(info) << boost::format("set no_check to %1%:")%no_check; print->set_no_check_flag(no_check);//BBS + // CLI guard: check high/low temp filament mixing. + // The GUI has Plater::sync_filament_temp_mixing_notification() + // which respects user preferences. For CLI we always block. + if (print_fff) { + const auto& pcfg = print_fff->config(); + const auto& extruders = print_fff->extruders(); + std::vector ftypes; + ftypes.reserve(extruders.size()); + for (unsigned int eid : extruders) + ftypes.push_back(pcfg.filament_type.get_at(eid)); + if (!Print::check_multi_filaments_compatibility(ftypes)) { + boost::nowide::cerr + << "Cannot print multiple filaments which have " + "large difference of temperature together." + << std::endl; + record_exit_reson(outfile_dir, CLI_FILAMENTS_DIFFERENT_TEMP, + index + 1, cli_errors[CLI_FILAMENTS_DIFFERENT_TEMP], + sliced_info); + flush_and_exit(CLI_FILAMENTS_DIFFERENT_TEMP); + } + } StringObjectException warning; auto err = print->validate(&warning); if (!err.string.empty()) { diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index 137d9fd68d4..f5eb30af3c6 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -1558,15 +1558,6 @@ StringObjectException Print::validate(StringObjectException *warning, Polygons* if (extruders.empty()) return { L("No extrusions under current settings.") }; - // Check for high/low temperature filament mixing. - if (extruders.size() > 1) { - auto ret = check_multi_filament_valid(*this); - if (!ret.string.empty()) { - ret.type = STRING_EXCEPT_FILAMENTS_DIFFERENT_TEMP; - return ret; - } - } - if (m_config.print_sequence == PrintSequence::ByObject) { if (m_config.timelapse_type == TimelapseType::tlSmooth) return {L("Smooth mode of timelapse is not supported when \"by object\" sequence is enabled.")}; diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index b37ad3f82a5..3c58ffe44c8 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -20297,6 +20297,45 @@ bool Plater::check_filament_temp_mixing(int plate_index) } } + // Collect from the Plater working config. The approach balances + // sensitivity against false positives: + // - Global features (wipe tower, support) always apply → always collected. + // - Feature-specific keys (wall_filament, infill) depend on the global + // process defaults. They are only collected when at least one object + // on the plate uses the default extruder (e=0), which means those + // defaults WILL affect the actual slicing output. + { + // Always collect: features that cannot be overridden per-object. + static const std::vector always_collect = { + "wipe_tower_filament", + "support_filament", + "support_interface_filament" + }; + for (const char* key : always_collect) + { + const ConfigOptionInt* option = this->config()->option(key); + if (option != nullptr && option->value >= 1 && option->value <= num_filaments) + used_slots.insert(option->value - 1); + } + + // If any object uses e=0, the global process defaults for + // wall / infill extruders apply and must be collected. + if (uses_default_extruder) + { + static const std::vector default_keys = { + "wall_filament", + "sparse_infill_filament", + "solid_infill_filament" + }; + for (const char* key : default_keys) + { + const ConfigOptionInt* option = this->config()->option(key); + if (option != nullptr && option->value >= 1 && option->value <= num_filaments) + used_slots.insert(option->value - 1); + } + } + } + // Resolve the global default extruder if any object on this plate // uses extruder=0. p->config does not include the "extruder" key // (it is not in the initializer list at priv constructor), so we From 9e552aaadf6c107bc781ec57e111a4e0d59cd973 Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Fri, 12 Jun 2026 11:36:56 +0800 Subject: [PATCH 113/135] fix: collect per-object feature keys in filament temp mixing check MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit collect_filament_slots_from_model_config() only checked the extruder key, missing per-object overrides of wall_filament, solid_infill_filament, etc. When a user sets a different filament for solid infill at the object level, the check failed to detect high/low temperature mixing even though the slicer would actually use that filament during printing. Now also collects: wall_filament, sparse_infill_filament, solid_infill_filament, support_filament, support_interface_filament, wipe_tower_filament — matching the actual behavior of apply_to_print_region_config() which applies these per-object keys. Co-Authored-By: Claude Opus 4.7 --- src/slic3r/GUI/Plater.cpp | 30 +++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 3c58ffe44c8..20cab464cd3 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -275,12 +275,32 @@ static void collect_filament_slots_from_model_config( int num_filaments, std::set& used_slots) { - if (!config.has("extruder")) - return; + if (config.has("extruder")) + { + const int extruder_id = config.extruder(); + if (extruder_id >= 1 && extruder_id <= num_filaments) + used_slots.insert(extruder_id - 1); + } - const int extruder_id = config.extruder(); - if (extruder_id >= 1 && extruder_id <= num_filaments) - used_slots.insert(extruder_id - 1); + // Per-object feature-specific keys (wall_filament, etc.) may be + // overridden independently of the object's primary extruder. + static const std::vector feature_keys = { + "wall_filament", + "sparse_infill_filament", + "solid_infill_filament", + "support_filament", + "support_interface_filament", + "wipe_tower_filament" + }; + for (const char* key : feature_keys) + { + if (config.has(key)) + { + const int val = config.opt_int(key); + if (val >= 1 && val <= num_filaments) + used_slots.insert(val - 1); + } + } } wxDEFINE_EVENT(EVT_SCHEDULE_BACKGROUND_PROCESS, SimpleEvent); From 15af562c8911f201dd8b3a20c6a0c2b661b5b902 Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Fri, 12 Jun 2026 14:38:47 +0800 Subject: [PATCH 114/135] fix: skip already-sliced plates in slice-all confirmation dialog confirm_filament_temp_mixing_before_slice_all() checked all sliceable plates regardless of whether they already had valid slice results. After slicing one mixed plate and returning to slice-all, the dialog appeared again for that already-sliced plate -- a pointless prompt. Now only unsliced plates with AllowedWarning trigger the dialog. Co-Authored-By: Claude Opus 4.7 --- src/slic3r/GUI/Plater.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 20cab464cd3..62d72d4f246 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -20525,11 +20525,14 @@ bool Plater::confirm_filament_temp_mixing_before_slice_all() if (!has_sliceable_plate_for_slice_all()) return false; + // Only count plates that can be sliced AND haven't been sliced + // yet. Already-sliced plates don't need re-confirmation. bool has_allowed_warning = false; for (int plate_index = 0; plate_index < p->partplate_list.get_plate_count(); ++plate_index) { PartPlate* plate = p->partplate_list.get_plate(plate_index); if (plate != nullptr && plate->can_slice() && + !plate->is_slice_result_valid() && get_filament_temp_mixing_state(plate_index) == FilamentTempMixingState::AllowedWarning) { has_allowed_warning = true; From 722e8d84440e61680863f8fc7b099557507d400a Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Mon, 15 Jun 2026 09:50:42 +0800 Subject: [PATCH 115/135] fix: wxEntry encoding mismatch when exe path contains non-ASCII characters boost::nowide::narrow() converts argv[0] to UTF-8, but wxEntry expects the system ANSI codepage (CP_ACP). On Windows, rebuild argv[0] from GetModuleFileNameW with proper CP_ACP conversion to avoid "Command line argument 0 couldn't be converted to Unicode" warning from wxWidgets. Co-Authored-By: Claude Opus 4.7 --- src/slic3r/GUI/GUI_Init.cpp | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/src/slic3r/GUI/GUI_Init.cpp b/src/slic3r/GUI/GUI_Init.cpp index 96f377e8263..ec22e5770a5 100644 --- a/src/slic3r/GUI/GUI_Init.cpp +++ b/src/slic3r/GUI/GUI_Init.cpp @@ -16,6 +16,11 @@ #include #include +#ifdef _WIN32 + #include + #include +#endif + #if __APPLE__ #include #endif // __APPLE__ @@ -53,6 +58,32 @@ int GUI_Run(GUI_InitParams ¶ms) GUI::GUI_App::SetInstance(gui); gui->init_params = ¶ms; +#ifdef _WIN32 + // argv[0] from boost::nowide::narrow() is UTF-8, but wxEntry expects + // the system ANSI codepage (CP_ACP). When the executable path contains + // non-ASCII characters, the encoding mismatch causes wxWidgets to + // show: "Command line argument 0 couldn't be converted to Unicode". + // Rebuild argv[0] from the real module path using the proper codepage. + { + wchar_t module_path[MAX_PATH + 1] = {0}; + GetModuleFileNameW(nullptr, module_path, MAX_PATH); + char module_ansi[MAX_PATH * 2 + 1] = {0}; + WideCharToMultiByte(CP_ACP, WC_NO_BEST_FIT_CHARS, + module_path, -1, module_ansi, + sizeof(module_ansi), nullptr, nullptr); + if (module_ansi[0] == '\0') { + // Fallback: conversion failed, use ASCII-safe short path + wchar_t short_path[MAX_PATH + 1] = {0}; + GetShortPathNameW(module_path, short_path, MAX_PATH); + WideCharToMultiByte(CP_ACP, 0, short_path, -1, + module_ansi, sizeof(module_ansi), + nullptr, nullptr); + } + int safe_argc = 1; + char* safe_argv[] = { const_cast(module_ansi), nullptr }; + return wxEntry(safe_argc, safe_argv); + } +#else if (params.argc > 1) { // STUDIO-273 wxWidgets report error when opening some files with specific names // wxWidgets does not handle parameters, so intercept parameters here, only keep the app name @@ -63,6 +94,7 @@ int GUI_Run(GUI_InitParams ¶ms) } else { return wxEntry(params.argc, params.argv); } +#endif } catch (const Slic3r::Exception &ex) { BOOST_LOG_TRIVIAL(error) << ex.what() << std::endl; wxMessageBox(boost::nowide::widen(ex.what()), _L("Snapmaker Orca GUI initialization failed"), wxICON_STOP); From 7581559b8b029de8a232c8fd3296fba5cf2ad721 Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Mon, 15 Jun 2026 18:22:49 +0800 Subject: [PATCH 116/135] =?UTF-8?q?fix:=20address=20code=20review=20findin?= =?UTF-8?q?gs=20=E2=80=94=20brace=20style,=20API=20return=20checks,=20atom?= =?UTF-8?q?ic=20flag?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix Allman brace style violations in Snapmaker_Orca.cpp and NotificationManager.cpp (opening brace on same line → own line) - Check WideCharToMultiByte return value instead of assuming failure when output buffer is empty; also check short-path fallback result - Replace bool filament_usage_sync_pending with std::atomic to avoid potential races when notify_filament_usage_changed is called from multiple code paths in quick succession Co-Authored-By: Claude Opus 4.7 --- src/Snapmaker_Orca.cpp | 3 ++- src/slic3r/GUI/GUI_Init.cpp | 21 ++++++++++++++------- src/slic3r/GUI/NotificationManager.cpp | 3 ++- src/slic3r/GUI/Plater.cpp | 3 ++- 4 files changed, 20 insertions(+), 10 deletions(-) diff --git a/src/Snapmaker_Orca.cpp b/src/Snapmaker_Orca.cpp index 365be8d3679..2e2f4421e3e 100644 --- a/src/Snapmaker_Orca.cpp +++ b/src/Snapmaker_Orca.cpp @@ -4994,7 +4994,8 @@ int CLI::run(int argc, char **argv) // CLI guard: check high/low temp filament mixing. // The GUI has Plater::sync_filament_temp_mixing_notification() // which respects user preferences. For CLI we always block. - if (print_fff) { + if (print_fff) + { const auto& pcfg = print_fff->config(); const auto& extruders = print_fff->extruders(); std::vector ftypes; diff --git a/src/slic3r/GUI/GUI_Init.cpp b/src/slic3r/GUI/GUI_Init.cpp index ec22e5770a5..84f10ec3997 100644 --- a/src/slic3r/GUI/GUI_Init.cpp +++ b/src/slic3r/GUI/GUI_Init.cpp @@ -68,16 +68,23 @@ int GUI_Run(GUI_InitParams ¶ms) wchar_t module_path[MAX_PATH + 1] = {0}; GetModuleFileNameW(nullptr, module_path, MAX_PATH); char module_ansi[MAX_PATH * 2 + 1] = {0}; - WideCharToMultiByte(CP_ACP, WC_NO_BEST_FIT_CHARS, - module_path, -1, module_ansi, - sizeof(module_ansi), nullptr, nullptr); - if (module_ansi[0] == '\0') { + const int converted = WideCharToMultiByte( + CP_ACP, WC_NO_BEST_FIT_CHARS, + module_path, -1, module_ansi, + sizeof(module_ansi), nullptr, nullptr); + if (converted == 0) + { // Fallback: conversion failed, use ASCII-safe short path wchar_t short_path[MAX_PATH + 1] = {0}; GetShortPathNameW(module_path, short_path, MAX_PATH); - WideCharToMultiByte(CP_ACP, 0, short_path, -1, - module_ansi, sizeof(module_ansi), - nullptr, nullptr); + if (GetLastError() == ERROR_INSUFFICIENT_BUFFER || + WideCharToMultiByte(CP_ACP, 0, short_path, -1, + module_ansi, sizeof(module_ansi), + nullptr, nullptr) == 0) + { + // Both primary and fallback failed; keep empty string, + // wxEntry will use its own default behavior. + } } int safe_argc = 1; char* safe_argv[] = { const_cast(module_ansi), nullptr }; diff --git a/src/slic3r/GUI/NotificationManager.cpp b/src/slic3r/GUI/NotificationManager.cpp index f2be434c92b..5b2dff06b95 100644 --- a/src/slic3r/GUI/NotificationManager.cpp +++ b/src/slic3r/GUI/NotificationManager.cpp @@ -2016,7 +2016,8 @@ void NotificationManager::close_notification_of_type(const NotificationType type void NotificationManager::close_notification_of_type_and_text(const NotificationType type, const std::string& text) { - for (std::unique_ptr& notification : m_pop_notifications) { + for (std::unique_ptr& notification : m_pop_notifications) + { if (notification->get_type() == type && notification->compare_text(text)) notification->close(); } diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 62d72d4f246..725385c29e9 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -8511,7 +8512,7 @@ struct Plater::priv // PIMPL back pointer ("Q-Pointer") Plater *q; MainFrame *main_frame; - bool filament_usage_sync_pending = false; + std::atomic filament_usage_sync_pending{false}; bool filament_temp_mixing_notification_initialized = false; int filament_temp_mixing_notification_plate = -1; FilamentTempMixingState filament_temp_mixing_notification_state = FilamentTempMixingState::Compatible; From dca80fd0dad3651c821bdfe677a4e81f851d4e92 Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Tue, 16 Jun 2026 14:38:34 +0800 Subject: [PATCH 117/135] fix: use atomic exchange in notify_filament_usage_changed and remove redundant trigger in select_plate - Replace check-then-set pattern with exchange(true) for correct atomic test-and-set semantics in notify_filament_usage_changed(). - Remove notify_filament_usage_changed() call from select_plate() since sync_filament_temp_mixing_notification() is already called directly. Co-Authored-By: Claude Opus 4.7 --- src/slic3r/GUI/Plater.cpp | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 725385c29e9..e442cc338a1 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -20553,10 +20553,9 @@ bool Plater::confirm_filament_temp_mixing_before_slice_all() void Plater::notify_filament_usage_changed() { - if (p->filament_usage_sync_pending) + if (p->filament_usage_sync_pending.exchange(true)) return; - p->filament_usage_sync_pending = true; wxQueueEvent(this, new SimpleEvent(EVT_FILAMENT_USAGE_CHANGED, this)); } @@ -21402,7 +21401,6 @@ int Plater::select_plate(int plate_index, bool need_slice) SimpleEvent event(EVT_GLCANVAS_PLATE_SELECT); p->on_plate_selected(event); sync_filament_temp_mixing_notification(); - notify_filament_usage_changed(); BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(" %1%: plate %2%, return %3%")%__LINE__ %plate_index %ret; return ret; From f8187a2023580e399c965fbfdb48d1e4c5d688f9 Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Tue, 16 Jun 2026 14:53:26 +0800 Subject: [PATCH 118/135] fix: Selection::clear() takes no arguments, use conditional guard instead The cherry-pick in a9ca118bd6 changed m_selection.clear() to m_selection.clear(mode == ResetVolumesMode::Normal), but Selection::clear() accepts zero parameters. Wrap the call in an explicit if-guard to preserve the intent of skipping clear() during CanvasDestruction. Co-Authored-By: Claude Opus 4.7 --- src/slic3r/GUI/GLCanvas3D.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp index 43deee3e279..3c4943628a0 100644 --- a/src/slic3r/GUI/GLCanvas3D.cpp +++ b/src/slic3r/GUI/GLCanvas3D.cpp @@ -1350,7 +1350,8 @@ void GLCanvas3D::reset_volumes(ResetVolumesMode mode) _set_current(); - m_selection.clear(mode == ResetVolumesMode::Normal); + if (mode == ResetVolumesMode::Normal) + m_selection.clear(); m_volumes.clear(); m_dirty = true; From d7a99a94369a317aed779428ea079067c46ab79a Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Wed, 17 Jun 2026 11:14:34 +0800 Subject: [PATCH 119/135] fix: extract WideCharToMultiByte lambda and fix brace style in MainFrame Co-Authored-By: Claude Opus 4.7 --- src/slic3r/GUI/GUI_Init.cpp | 25 ++++++++++--------------- src/slic3r/GUI/MainFrame.cpp | 10 +++++++++- 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/src/slic3r/GUI/GUI_Init.cpp b/src/slic3r/GUI/GUI_Init.cpp index 84f10ec3997..cf677304066 100644 --- a/src/slic3r/GUI/GUI_Init.cpp +++ b/src/slic3r/GUI/GUI_Init.cpp @@ -68,26 +68,21 @@ int GUI_Run(GUI_InitParams ¶ms) wchar_t module_path[MAX_PATH + 1] = {0}; GetModuleFileNameW(nullptr, module_path, MAX_PATH); char module_ansi[MAX_PATH * 2 + 1] = {0}; - const int converted = WideCharToMultiByte( - CP_ACP, WC_NO_BEST_FIT_CHARS, - module_path, -1, module_ansi, - sizeof(module_ansi), nullptr, nullptr); - if (converted == 0) - { + auto to_acp = [&](const wchar_t* src, DWORD flags = 0) { + return WideCharToMultiByte(CP_ACP, flags, src, -1, + module_ansi, sizeof(module_ansi), + nullptr, nullptr); + }; + if (to_acp(module_path, WC_NO_BEST_FIT_CHARS) == 0) { // Fallback: conversion failed, use ASCII-safe short path wchar_t short_path[MAX_PATH + 1] = {0}; GetShortPathNameW(module_path, short_path, MAX_PATH); - if (GetLastError() == ERROR_INSUFFICIENT_BUFFER || - WideCharToMultiByte(CP_ACP, 0, short_path, -1, - module_ansi, sizeof(module_ansi), - nullptr, nullptr) == 0) - { - // Both primary and fallback failed; keep empty string, - // wxEntry will use its own default behavior. - } + to_acp(short_path); + // If the fallback also fails, module_ansi stays empty; + // wxEntry will use its own default behavior. } int safe_argc = 1; - char* safe_argv[] = { const_cast(module_ansi), nullptr }; + char* safe_argv[] = { module_ansi, nullptr }; return wxEntry(safe_argc, safe_argv); } #else diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index ef15a3909ae..1e46a3e8198 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -568,7 +568,15 @@ DPIFrame(NULL, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, BORDERLESS_FRAME_ } return;} #endif - if (evt.CmdDown() && evt.GetKeyCode() == 'R') { if (m_slice_enable) { wxGetApp().plater()->update(true, true); wxPostEvent(m_plater, SimpleEvent(EVT_GLTOOLBAR_SLICE_PLATE)); } return; } + if (evt.CmdDown() && evt.GetKeyCode() == 'R') + { + if (m_slice_enable) + { + wxGetApp().plater()->update(true, true); + wxPostEvent(m_plater, SimpleEvent(EVT_GLTOOLBAR_SLICE_PLATE)); + } + return; + } if (evt.CmdDown() && evt.ShiftDown() && evt.GetKeyCode() == 'G') { m_plater->apply_background_progress(); m_print_enable = get_enable_print_status(); From 6aee0a74857776aa92087cc3015a8e3a24ff808c Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Wed, 17 Jun 2026 11:53:23 +0800 Subject: [PATCH 120/135] =?UTF-8?q?fix:=20resolve=20coding=20standard=20vi?= =?UTF-8?q?olations=20=E2=80=94=20tabs=20to=20spaces,=20Allman=20braces,?= =?UTF-8?q?=20C-style=20cast?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace hard tabs with 4-space indentation in NotificationManager.hpp and PartPlate.cpp (11 occurrences) - Convert single-line if-block to multi-line Allman style in Preferences.cpp - Replace C-style (int) cast with static_cast in MixedFilamentDialog.cpp - Extract WideCharToMultiByte lambda and remove empty if-body in GUI_Init.cpp - Reformat Ctrl+R shortcut handler to Allman style in MainFrame.cpp - Remove duplicate translation line in zh_CN.po Co-Authored-By: Claude Opus 4.7 --- localization/i18n/zh_CN/Snapmaker_Orca_zh_CN.po | 1 - src/slic3r/GUI/MixedFilamentDialog.cpp | 2 +- src/slic3r/GUI/NotificationManager.hpp | 16 ++++++++-------- src/slic3r/GUI/PartPlate.cpp | 8 ++++---- src/slic3r/GUI/Preferences.cpp | 6 +++++- 5 files changed, 18 insertions(+), 15 deletions(-) diff --git a/localization/i18n/zh_CN/Snapmaker_Orca_zh_CN.po b/localization/i18n/zh_CN/Snapmaker_Orca_zh_CN.po index 8e72be5e654..37498c9a8aa 100644 --- a/localization/i18n/zh_CN/Snapmaker_Orca_zh_CN.po +++ b/localization/i18n/zh_CN/Snapmaker_Orca_zh_CN.po @@ -15279,7 +15279,6 @@ msgstr "擦拭塔熨烫面积" msgid "Ironing area for prime tower interface layer (where different materials meet)." msgstr "擦拭塔的起始点熨烫面积" -msgstr "擦拭塔的起始点熨烫面积" msgid "High and Low Temperature Material Mixing Risk" msgstr "高/低温材料混打风险" diff --git a/src/slic3r/GUI/MixedFilamentDialog.cpp b/src/slic3r/GUI/MixedFilamentDialog.cpp index cb6b3c96c72..b607cdf847d 100644 --- a/src/slic3r/GUI/MixedFilamentDialog.cpp +++ b/src/slic3r/GUI/MixedFilamentDialog.cpp @@ -2874,7 +2874,7 @@ void MixedFilamentDialog::build_swatch_grid() if (cand.n_rows == 2) min_w = std::min(100 - cand.b_pct, cand.b_pct); else if (cand.n_rows == 3) - min_w = (int)(std::min({cand.wx, cand.wy, cand.wz}) * 100.0 + 0.5); + min_w = static_cast(std::min({cand.wx, cand.wy, cand.wz}) * 100.0 + 0.5); // n_rows == 1 or unknown: badge is always visible (sentinel 100). m_swatch_min_weights.push_back(min_w); diff --git a/src/slic3r/GUI/NotificationManager.hpp b/src/slic3r/GUI/NotificationManager.hpp index ffd759dc7b2..92f656c53f1 100644 --- a/src/slic3r/GUI/NotificationManager.hpp +++ b/src/slic3r/GUI/NotificationManager.hpp @@ -150,7 +150,7 @@ enum class NotificationType BBLPreviewOnlyMode, BBLPrinterConfigUpdateAvailable, BBLUserPresetExceedLimit, - FilamentIncompatibleMixed, + FilamentIncompatibleMixed, }; class NotificationManager @@ -200,9 +200,9 @@ class NotificationManager void stop_delayed_notifications_of_type(const NotificationType type); // Creates Validate Error notification with a custom text and no fade out. void push_validate_error_notification(StringObjectException const & error); - void close_validate_error_notification(const std::string& text); - void close_validate_warning_notification(const std::string& text); - // print host upload + void close_validate_error_notification(const std::string& text); + void close_validate_warning_notification(const std::string& text); + // print host upload void push_upload_job_notification(int id, float filesize, const std::string& filename, const std::string& host, float percentage = 0); void set_upload_job_notification_percentage(int id, const std::string& filename, const std::string& host, float percentage); void upload_job_notification_show_canceled(int id, const std::string& filename, const std::string& host); @@ -847,7 +847,7 @@ class NotificationManager //can be used to create custom notification bool push_notification_data(const NotificationData& notification_data, int timestamp); bool push_notification_data(std::unique_ptr notification, int timestamp); - void close_notification_of_type_and_text(const NotificationType type, const std::string& text); + void close_notification_of_type_and_text(const NotificationType type, const std::string& text); // Delayed notifications goes first to the m_waiting_notifications vector and only after remaining time is <= 0 // and condition callback is success, notification is regular pushed from update function. // Otherwise another delay interval waiting. Timestamp is 0. @@ -902,9 +902,9 @@ class NotificationManager NotificationType::PlaterWarning, NotificationType::ProgressBar, NotificationType::PrintHostUpload, - NotificationType::ValidateError, - NotificationType::ValidateWarning, - NotificationType::SimplifySuggestion + NotificationType::ValidateError, + NotificationType::ValidateWarning, + NotificationType::SimplifySuggestion }; //prepared (basic) notifications // non-static so its not loaded too early. If static, the translations wont load correctly. diff --git a/src/slic3r/GUI/PartPlate.cpp b/src/slic3r/GUI/PartPlate.cpp index 9a1610959a2..435b72d0933 100644 --- a/src/slic3r/GUI/PartPlate.cpp +++ b/src/slic3r/GUI/PartPlate.cpp @@ -4039,7 +4039,7 @@ int PartPlateList::select_plate(int index) return -1; } - const int old_plate_index = m_current_plate; + const int old_plate_index = m_current_plate; // BBS: erase unnecessary snapshot if (get_curr_plate_index() != index && m_intialized) { @@ -4054,8 +4054,8 @@ int PartPlateList::select_plate(int index) m_current_plate = index; m_plate_list[m_current_plate]->set_selected(); - if (old_plate_index != m_current_plate && wxGetApp().plater()) - wxGetApp().plater()->notify_filament_usage_changed(); + if (old_plate_index != m_current_plate && wxGetApp().plater()) + wxGetApp().plater()->notify_filament_usage_changed(); //BBS if(m_model) @@ -4531,7 +4531,7 @@ int PartPlateList::add_to_plate(int obj_id, int instance_id, int plate_id) } ret = plate->add_instance(obj_id, instance_id, true); - wxGetApp().plater()->notify_filament_usage_changed(); + wxGetApp().plater()->notify_filament_usage_changed(); return ret; } diff --git a/src/slic3r/GUI/Preferences.cpp b/src/slic3r/GUI/Preferences.cpp index 9ab828e277c..9b7d4b2bd37 100644 --- a/src/slic3r/GUI/Preferences.cpp +++ b/src/slic3r/GUI/Preferences.cpp @@ -748,7 +748,11 @@ wxBoxSizer *PreferencesDialog::create_item_checkbox(wxString title, wxWindow *pa //// save config auto reentry_guard = std::make_shared(false); checkbox->Bind(wxEVT_TOGGLEBUTTON, [this, checkbox, param, confirm_cb, reentry_guard](wxCommandEvent &e) { - if (*reentry_guard) { e.Skip(); return; } + if (*reentry_guard) + { + e.Skip(); + return; + } if (confirm_cb) { bool old_val = !checkbox->GetValue(); bool new_val = checkbox->GetValue(); From 941edde9d1cdc4770e5126eaf76fb427fbc7b6b5 Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Wed, 17 Jun 2026 11:59:33 +0800 Subject: [PATCH 121/135] fix: restore Snapmaker.json to upstream version to prevent version regression The older commit 613f316cc8 inadvertently downgraded the profile version from 02.02.49.02 to 02.02.47.02 and removed filament entries added by upstream. Restore the file to match upstream/feature_top_cover exactly. Co-Authored-By: Claude Opus 4.7 --- resources/profiles/Snapmaker.json | 165 +++++++++++++++++++++++++++++- 1 file changed, 162 insertions(+), 3 deletions(-) diff --git a/resources/profiles/Snapmaker.json b/resources/profiles/Snapmaker.json index d45befa9452..0e4a746a8a3 100644 --- a/resources/profiles/Snapmaker.json +++ b/resources/profiles/Snapmaker.json @@ -1,6 +1,6 @@ { "name": "Snapmaker", - "version": "02.02.47.02", + "version": "02.02.49.02", "force_update": "0", "description": "Snapmaker configurations", "machine_model_list": [ @@ -1521,6 +1521,10 @@ "name": "Generic PLA @base2", "sub_path": "filament/Generic PLA @base2.json" }, + { + "name": "Snapmaker PLA Wood @U1 0.4 nozzle", + "sub_path": "filament/Snapmaker PLA Wood @U1 0.4 nozzle.json" + }, { "name": "Generic PLA @U1 0.2 nozzle", "sub_path": "filament/Generic PLA @U1 0.2 nozzle.json" @@ -1735,9 +1739,164 @@ { "name": "Snapmaker PLA SnapSpeed @U1 0.8 nozzle", "sub_path": "filament/Snapmaker PLA SnapSpeed @U1 0.8 nozzle.json" + }, + { + "name": "Snapmaker TPU 90A @U1 0.6 nozzle", + "sub_path": "filament/Snapmaker TPU 90A @U1 0.6 nozzle.json" + }, + { + "name": "Snapmaker TPU 90A @U1 0.8 nozzle", + "sub_path": "filament/Snapmaker TPU 90A @U1 0.8 nozzle.json" + }, + { + "name": "Generic PC @U1 0.6 nozzle", + "sub_path": "filament/Generic PC @U1 0.6 nozzle.json" + }, + { + "name": "Generic PC @U1 0.8 nozzle", + "sub_path": "filament/Generic PC @U1 0.8 nozzle.json" + }, + { + "name": "Snapmaker TPU 95A HF @U1 0.6 nozzle", + "sub_path": "filament/Snapmaker TPU 95A HF @U1 0.6 nozzle.json" + }, + { + "name": "Snapmaker TPU 95A HF @U1 0.8 nozzle", + "sub_path": "filament/Snapmaker TPU 95A HF @U1 0.8 nozzle.json" + }, + { + "name": "Generic ABS @U1 0.2 nozzle", + "sub_path": "filament/Generic ABS @U1 0.2 nozzle.json" + }, + { + "name": "Generic ABS @U1 0.6 nozzle", + "sub_path": "filament/Generic ABS @U1 0.6 nozzle.json" + }, + { + "name": "Generic ABS @U1 0.8 nozzle", + "sub_path": "filament/Generic ABS @U1 0.8 nozzle.json" + }, + { + "name": "Snapmaker PLA-CF @U1 0.4 nozzle", + "sub_path": "filament/Snapmaker PLA-CF @U1 0.4 nozzle.json" + }, + { + "name": "Snapmaker PLA-CF @U1 0.6 nozzle", + "sub_path": "filament/Snapmaker PLA-CF @U1 0.6 nozzle.json" + }, + { + "name": "Snapmaker PLA-CF @U1 0.8 nozzle", + "sub_path": "filament/Snapmaker PLA-CF @U1 0.8 nozzle.json" + }, + { + "name": "Snapmaker PLA Wood @U1 0.6 nozzle", + "sub_path": "filament/Snapmaker PLA Wood @U1 0.6 nozzle.json" + }, + { + "name": "Snapmaker PLA Wood @U1 0.8 nozzle", + "sub_path": "filament/Snapmaker PLA Wood @U1 0.8 nozzle.json" + }, + { + "name": "Generic Support For PLA @U1 0.2 nozzle", + "sub_path": "filament/Generic Support For PLA @U1 0.2 nozzle.json" + }, + { + "name": "Generic Support For PLA @U1 0.6 nozzle", + "sub_path": "filament/Generic Support For PLA @U1 0.6 nozzle.json" + }, + { + "name": "Generic Support For PLA @U1 0.8 nozzle", + "sub_path": "filament/Generic Support For PLA @U1 0.8 nozzle.json" + }, + { + "name": "Snapmaker PLA Silk @U1 0.2 nozzle", + "sub_path": "filament/Snapmaker PLA Silk @U1 0.2 nozzle.json" + }, + { + "name": "Snapmaker PLA Silk @U1 0.6 nozzle", + "sub_path": "filament/Snapmaker PLA Silk @U1 0.6 nozzle.json" + }, + { + "name": "Snapmaker PLA Silk @U1 0.8 nozzle", + "sub_path": "filament/Snapmaker PLA Silk @U1 0.8 nozzle.json" + }, + { + "name": "Snapmaker PLA Glow @U1 base", + "sub_path": "filament/Snapmaker PLA Glow @U1 base.json" + }, + { + "name": "Snapmaker PLA Glow @U1 0.4 nozzle", + "sub_path": "filament/Snapmaker PLA Glow @U1 0.4 nozzle.json" + }, + { + "name": "Snapmaker PLA Translucent @U1 base", + "sub_path": "filament/Snapmaker PLA Translucent @U1 base.json" + }, + { + "name": "Snapmaker PLA Translucent @U1 0.2 nozzle", + "sub_path": "filament/Snapmaker PLA Translucent @U1 0.2 nozzle.json" + }, + { + "name": "Snapmaker PLA Translucent @U1 0.4 nozzle", + "sub_path": "filament/Snapmaker PLA Translucent @U1 0.4 nozzle.json" + }, + { + "name": "Snapmaker PLA Translucent @U1 0.6 nozzle", + "sub_path": "filament/Snapmaker PLA Translucent @U1 0.6 nozzle.json" + }, + { + "name": "Snapmaker PLA Translucent @U1 0.8 nozzle", + "sub_path": "filament/Snapmaker PLA Translucent @U1 0.8 nozzle.json" + }, + { + "name": "Snapmaker PETG Translucent @U1 base", + "sub_path": "filament/Snapmaker PETG Translucent @U1 base.json" + }, + { + "name": "Snapmaker PETG Translucent @U1 0.2 nozzle", + "sub_path": "filament/Snapmaker PETG Translucent @U1 0.2 nozzle.json" + }, + { + "name": "Snapmaker PETG Translucent @U1 0.4 nozzle", + "sub_path": "filament/Snapmaker PETG Translucent @U1 0.4 nozzle.json" + }, + { + "name": "Snapmaker PETG Translucent @U1 0.6 nozzle", + "sub_path": "filament/Snapmaker PETG Translucent @U1 0.6 nozzle.json" + }, + { + "name": "Snapmaker PETG Translucent @U1 0.8 nozzle", + "sub_path": "filament/Snapmaker PETG Translucent @U1 0.8 nozzle.json" + }, + { + "name": "Snapmaker PVA @U1 0.6 nozzle", + "sub_path": "filament/Snapmaker PVA @U1 0.6 nozzle.json" + }, + { + "name": "Snapmaker PVA @U1 0.8 nozzle", + "sub_path": "filament/Snapmaker PVA @U1 0.8 nozzle.json" + }, + { + "name": "Snapmaker Breakaway Support For PLA @U1 0.2 nozzle", + "sub_path": "filament/Snapmaker Breakaway Support For PLA @U1 0.2 nozzle.json" + }, + { + "name": "Snapmaker Breakaway Support For PLA @U1 0.6 nozzle", + "sub_path": "filament/Snapmaker Breakaway Support For PLA @U1 0.6 nozzle.json" + }, + { + "name": "Snapmaker Breakaway Support For PLA @U1 0.8 nozzle", + "sub_path": "filament/Snapmaker Breakaway Support For PLA @U1 0.8 nozzle.json" + }, + { + "name": "Snapmaker PLA Full Spectrum @U1 0.4 nozzle", + "sub_path": "filament/Snapmaker PLA Full Spectrum @U1 0.4 nozzle.json" + }, + { + "name": "Snapmaker Breakaway Support For PLA @U1", + "sub_path": "filament/Snapmaker Breakaway Support For PLA @U1.json" } - - ], + ], "machine_list": [ { "name": "fdm_klipper", From a509fca5bbdfa768512fb457b6b1470e9be7cfab Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Wed, 17 Jun 2026 12:23:36 +0800 Subject: [PATCH 122/135] =?UTF-8?q?fix:=20boost::any=5Fcast=20?= =?UTF-8?q?=E2=86=92=20boost::any=5Fcast=20for=20extruders=5Fcount?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit extruders_count config option has type coInt, so the underlying boost::any stores an int. Casting it as size_t directly throws boost::bad_any_cast, which wxWidgets reports as "Unhandled unknown exception; terminating the application." Use the safe pattern already established at line 4688: cast to int first, then convert to size_t. Co-Authored-By: Claude Opus 4.7 --- src/slic3r/GUI/Tab.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index 97b6481fac4..9a591d28f90 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -1839,7 +1839,7 @@ void Tab::on_value_change(const std::string& opt_key, const boost::any& value) //Orca: sync filament num if it's a multi tool printer if (opt_key == "extruders_count" && !m_config->opt_bool("single_extruder_multi_material")){ - auto num_extruder = boost::any_cast(value); + auto num_extruder = size_t(boost::any_cast(value)); int old_filament_size = wxGetApp().preset_bundle->filament_presets.size(); std::vector new_colors; for (int i = old_filament_size; i < num_extruder; ++i) { From 954218ac8af5122b544ed5b81bf1825b99d66ea7 Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Wed, 17 Jun 2026 12:24:26 +0800 Subject: [PATCH 123/135] fix: use static_cast instead of functional-style cast Replace size_t(...) with static_cast(...) to comply with coding standard that prohibits C-style casts. Also fix the same issue in the existing code at line 4688. Co-Authored-By: Claude Opus 4.7 --- src/slic3r/GUI/Tab.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index 9a591d28f90..2eac0219653 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -1839,7 +1839,7 @@ void Tab::on_value_change(const std::string& opt_key, const boost::any& value) //Orca: sync filament num if it's a multi tool printer if (opt_key == "extruders_count" && !m_config->opt_bool("single_extruder_multi_material")){ - auto num_extruder = size_t(boost::any_cast(value)); + auto num_extruder = static_cast(boost::any_cast(value)); int old_filament_size = wxGetApp().preset_bundle->filament_presets.size(); std::vector new_colors; for (int i = old_filament_size; i < num_extruder; ++i) { @@ -4685,7 +4685,7 @@ if (is_marlin_flavor) // Otherwise, boost::any_cast causes an "unhandled unknown exception" const auto v = optgroup_sh->get_value("extruders_count"); if (v.empty()) return; - size_t extruders_count = size_t(boost::any_cast(v)); + size_t extruders_count = static_cast(boost::any_cast(v)); wxTheApp->CallAfter([this, opt_key, value, extruders_count]() { if (opt_key == "extruders_count" || opt_key == "single_extruder_multi_material") { extruders_count_changed(extruders_count); From 20ad2b29b80d889a8f1926f701db490bdff40adc Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Wed, 17 Jun 2026 14:23:21 +0800 Subject: [PATCH 124/135] fix: remove undefined config options and fix corrupted Chinese translation file MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two root causes for "Unhandled unknown exception" at startup: 1. Plater.cpp referenced three config options that were never defined in PrintConfig.cpp: enable_infill_filament_override, infill_filament_use_base_first_layers, infill_filament_use_base_last_layers. This caused "Unknown option exception" during on_init_inner() when the config system looked up these keys. 2. zh_CN .po file had a duplicate block of 5 msgid/msgstr entries inserted during the main merge, plus a malformed line with two msgstr strings concatenated. msgfmt reported 8 fatal errors. Also fixes boost::any_cast → static_cast(boost::any_cast) in Tab.cpp to match the safe pattern at line 4688 (coInt options store int). Co-Authored-By: Claude Opus 4.7 --- .../i18n/zh_CN/Snapmaker_Orca_zh_CN.po | 33 ------------------- src/slic3r/GUI/Plater.cpp | 5 +-- 2 files changed, 1 insertion(+), 37 deletions(-) diff --git a/localization/i18n/zh_CN/Snapmaker_Orca_zh_CN.po b/localization/i18n/zh_CN/Snapmaker_Orca_zh_CN.po index 37498c9a8aa..04473a8cc55 100644 --- a/localization/i18n/zh_CN/Snapmaker_Orca_zh_CN.po +++ b/localization/i18n/zh_CN/Snapmaker_Orca_zh_CN.po @@ -15259,45 +15259,12 @@ msgstr "" "\n" "确认启用此功能吗?" -msgid "Detected both high and low temperature materials. Mixed printing may result in extruder clogging, nozzle damage, or layer adhesion issues." -msgstr "检测到高温材料与低温材料同时使用。混合打印可能导致挤出机堵塞、喷嘴损坏或层间粘附问题。" - -msgid "Detected both high and low temperature materials. Mixed printing may result in extruder clogging, nozzle damage, or layer adhesion issues. To continue printing, enable \"Allow mixed printing of high and low temperature materials\" in Preferences." -msgstr "检测到高温材料与低温材料同时使用。混合打印可能导致挤出机堵塞、喷嘴损坏或层间粘附问题。如需继续打印,请在「偏好设置」中开启 \"允许高/低温材料混合打印\"。" - -msgid "This material combination may cause risks. Do you want to continue?" -msgstr "此材料组合可能存在风险,是否继续?" - -msgid "Allow high/low temperature filament mixing" -msgstr "允许高/低温材料混合打印" - -msgid "If enabled, allows printing with both high-temperature and low-temperature filaments simultaneously." -msgstr "启用后允许高温材料与低温材料同时打印"msgstr "耗材%d参与混色的比例过高,可能会影响混色效果。" - msgid "Tower ironing area" msgstr "擦拭塔熨烫面积" msgid "Ironing area for prime tower interface layer (where different materials meet)." msgstr "擦拭塔的起始点熨烫面积" -msgid "High and Low Temperature Material Mixing Risk" -msgstr "高/低温材料混打风险" - -msgid "" -"Mixing materials with significantly different printing temperatures may result in:\n" -"· Extruder clogging\n" -"· Nozzle damage\n" -"· Layer adhesion issues\n" -"\n" -"Do you want to enable this feature?" -msgstr "" -"混合使用打印温度差异较大的材料,可能导致:\n" -"· 挤出机堵塞\n" -"· 喷嘴损坏\n" -"· 层间粘附问题\n" -"\n" -"确认启用此功能吗?" - msgid "Detected both high and low temperature materials. Mixed printing may result in extruder clogging, nozzle damage, or layer adhesion issues." msgstr "检测到高温材料与低温材料同时使用。混合打印可能导致挤出机堵塞、喷嘴损坏或层间粘附问题。" diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index eaa69dcf838..f510362cad7 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -9099,7 +9099,7 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame) "extruder_colour", "filament_colour", "material_colour", "printable_height", "printer_model", "printer_technology", // These values are necessary to construct SlicingParameters by the Canvas3D variable layer height editor. "layer_height", "initial_layer_print_height", "min_layer_height", "max_layer_height", - "brim_width", "wall_loops", "wall_filament", "sparse_infill_density", "enable_infill_filament_override", "infill_filament_use_base_first_layers", "infill_filament_use_base_last_layers", "sparse_infill_filament", "solid_infill_filament", "top_shell_layers", + "brim_width", "wall_loops", "wall_filament", "sparse_infill_density", "sparse_infill_filament", "solid_infill_filament", "top_shell_layers", "enable_support", "support_filament", "support_interface_filament", "support_top_z_distance", "support_bottom_z_distance", "raft_layers", "wipe_tower_rotation_angle", "wipe_tower_cone_angle", "wipe_tower_extra_spacing", "wipe_tower_extra_flow", "local_z_wipe_tower_purge_lines", "wipe_tower_max_purge_speed", @@ -20787,9 +20787,6 @@ void Plater::on_config_change(const DynamicPrintConfig &config) } // Orca: update when *_filament changed else if (opt_key == "support_interface_filament" || opt_key == "support_filament" || opt_key == "wall_filament" || - opt_key == "enable_infill_filament_override" || - opt_key == "infill_filament_use_base_first_layers" || - opt_key == "infill_filament_use_base_last_layers" || opt_key == "sparse_infill_filament" || opt_key == "solid_infill_filament") { update_scheduled = true; } From 472d650c87058077fa5804d88819d1faa29bf7a5 Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Wed, 17 Jun 2026 18:59:10 +0800 Subject: [PATCH 125/135] fix: restore attribution text and remove internal dev version label in About dialog Co-Authored-By: Claude Opus 4.7 --- src/slic3r/GUI/AboutDialog.cpp | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/slic3r/GUI/AboutDialog.cpp b/src/slic3r/GUI/AboutDialog.cpp index f32ba083061..7b6f58227c4 100644 --- a/src/slic3r/GUI/AboutDialog.cpp +++ b/src/slic3r/GUI/AboutDialog.cpp @@ -285,14 +285,13 @@ AboutDialog::AboutDialog() // SM Beta - // text_list.push_back(_L("Note: The firmware version of SnapmakerU1 should be no less than") + " " + std::string(MIN_FIRM_VER)); - // text_list.push_back(_L("Snapmaker Orca is forked from Orca Slicer by SoftFever.")); - // text_list.push_back(_L("OrcaSlicer is forked from Bambu Studio by Bambu Lab.")); - // text_list.push_back(_L("Bambu Studio is forked from PrusaSlicer by Prusa Research, which is from Slic3r by Alessandro Ranellucci and the RepRap community. Orca Slicer incorporates a lot of features from SuperSlicer by @supermerill.")); - // text_list.push_back(_L("Snapmaker Orca integrates some features from the 3D printing open-source community, with specific attributions provided in the code comments.")); - // text_list.push_back(_L("")); - // text_list.push_back(_L("Snapmaker Orca is licensed under the GNU Affero General Public License, version 3.")); - text_list.push_back(_L("2026-06-08:顶盖二期1.4.0版本")); + text_list.push_back(_L("Note: The firmware version of SnapmakerU1 should be no less than") + " " + std::string(MIN_FIRM_VER)); + text_list.push_back(_L("Snapmaker Orca is forked from Orca Slicer by SoftFever.")); + text_list.push_back(_L("OrcaSlicer is forked from Bambu Studio by Bambu Lab.")); + text_list.push_back(_L("Bambu Studio is forked from PrusaSlicer by Prusa Research, which is from Slic3r by Alessandro Ranellucci and the RepRap community. Orca Slicer incorporates a lot of features from SuperSlicer by @supermerill.")); + text_list.push_back(_L("Snapmaker Orca integrates some features from the 3D printing open-source community, with specific attributions provided in the code comments.")); + text_list.push_back(_L("")); + text_list.push_back(_L("Snapmaker Orca is licensed under the GNU Affero General Public License, version 3.")); text_sizer->Add( 0, 0, 0, wxTOP, FromDIP(33)); bool is_zh = wxGetApp().app_config->get("language") == "zh_CN"; From f5eb2ad3dcb3e0ac3fa04bc19ea28943c00886cc Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Wed, 17 Jun 2026 19:54:58 +0800 Subject: [PATCH 126/135] fix: move null guard for plate before first use in check_filament_temp_mixing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The if (plate) guard was placed after plate had already been passed to model_object_is_on_plate() and dereferenced. Move it to immediately after plate is obtained from get_plate(), with an early return. Also fix model_object_is_on_plate() to return false (not true) when plate is null — no object can be on a null plate. Co-Authored-By: Claude Opus 4.7 --- src/slic3r/GUI/Plater.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index f510362cad7..343739f4889 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -226,7 +226,7 @@ static bool model_object_is_on_plate(PartPlate* plate, size_t obj_idx, const Mod return false; if (plate == nullptr) - return true; + return false; const int object_index = static_cast(obj_idx); const int instance_count = static_cast(model_object->instances.size()); @@ -20434,6 +20434,8 @@ bool Plater::check_filament_temp_mixing(int plate_index) std::set used_slots; PartPlate* plate = p->partplate_list.get_plate(plate_index); + if (plate == nullptr) + return true; bool has_object_on_plate = false; for (size_t obj_idx = 0; obj_idx < wxGetApp().model().objects.size(); ++obj_idx) @@ -20449,8 +20451,7 @@ bool Plater::check_filament_temp_mixing(int plate_index) return true; // Also collect from current plate's config for any plate-level overrides - if (plate) - collect_filament_slots_from_config(*plate->config(), num_filaments, used_slots); + collect_filament_slots_from_config(*plate->config(), num_filaments, used_slots); // Collect from ModelVolume painting extruders for objects on the // current plate. Also track whether any object relies on the global From 60350f634a619778fd7fb4dccf91e780f634e14d Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Wed, 17 Jun 2026 20:01:57 +0800 Subject: [PATCH 127/135] fix: add default cases with logging to FilamentTempMixingState switches All three switch statements on the FilamentTempMixingState enum now have explicit default cases that log the unknown value via BOOST_LOG_TRIVIAL and fall through to a safe default behavior (allow slicing). - sync_filament_temp_mixing_notification(): default logs warning and sets slicing_allowed = true - confirm_filament_temp_mixing_before_slice(): default logs warning and returns true (allow) Co-Authored-By: Claude Opus 4.7 --- src/slic3r/GUI/Plater.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 343739f4889..afdbf252719 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -20637,6 +20637,10 @@ bool Plater::sync_filament_temp_mixing_notification() slicing_allowed = false; break; } + default: + BOOST_LOG_TRIVIAL(warning) << "[Plater] sync_filament_temp_mixing_notification: unknown mixing_state " << static_cast(mixing_state); + slicing_allowed = true; + break; } p->filament_temp_mixing_notification_initialized = true; @@ -20670,6 +20674,10 @@ bool Plater::confirm_filament_temp_mixing_before_slice() return false; case FilamentTempMixingState::AllowedWarning: break; + default: + BOOST_LOG_TRIVIAL(warning) << "[Plater] confirm_filament_temp_mixing_before_slice: unknown state " + << static_cast(get_filament_temp_mixing_state()); + return true; } MessageDialog dlg(this, _L("This material combination may cause risks. Do you want to continue?"), From 8d1ecc0114aa66d8bb0ee3010f7f0ccab217accd Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Wed, 17 Jun 2026 20:05:26 +0800 Subject: [PATCH 128/135] fix: validate curr_plate before use in sync_filament_temp_mixing_notification Add null guard for curr_plate immediately after obtaining it from get_curr_plate(), with an early return and warning log. Also replace redundant get_partplate_list().get_curr_plate() calls in the switch branches with the already-fetched curr_plate pointer. Co-Authored-By: Claude Opus 4.7 --- src/slic3r/GUI/Plater.cpp | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index afdbf252719..4afcead1c5d 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -20607,6 +20607,10 @@ bool Plater::sync_filament_temp_mixing_notification() { const int curr_plate_index = get_partplate_list().get_curr_plate_index(); PartPlate* curr_plate = get_partplate_list().get_curr_plate(); + if (curr_plate == nullptr) { + BOOST_LOG_TRIVIAL(warning) << "[Plater] sync_filament_temp_mixing_notification: curr_plate is null"; + return true; + } const FilamentTempMixingState mixing_state = get_filament_temp_mixing_state(curr_plate_index); bool slicing_allowed = true; @@ -20615,12 +20619,12 @@ bool Plater::sync_filament_temp_mixing_notification() case FilamentTempMixingState::Compatible: get_notification_manager()->close_validate_error_notification(filament_temp_mixing_error_text()); get_notification_manager()->close_validate_warning_notification(filament_temp_mixing_warning_text()); - get_partplate_list().get_curr_plate()->update_apply_result_invalid(false); + curr_plate->update_apply_result_invalid(false); slicing_allowed = true; break; case FilamentTempMixingState::AllowedWarning: get_notification_manager()->close_validate_error_notification(filament_temp_mixing_error_text()); - get_partplate_list().get_curr_plate()->update_apply_result_invalid(false); + curr_plate->update_apply_result_invalid(false); get_notification_manager()->push_notification( NotificationType::ValidateWarning, NotificationManager::NotificationLevel::WarningNotificationLevel, @@ -20633,7 +20637,7 @@ bool Plater::sync_filament_temp_mixing_notification() err.string = filament_temp_mixing_error_text(); get_notification_manager()->close_validate_warning_notification(filament_temp_mixing_warning_text()); get_notification_manager()->push_validate_error_notification(err); - get_partplate_list().get_curr_plate()->update_apply_result_invalid(true); + curr_plate->update_apply_result_invalid(true); slicing_allowed = false; break; } @@ -20647,7 +20651,7 @@ bool Plater::sync_filament_temp_mixing_notification() p->filament_temp_mixing_notification_plate = curr_plate_index; p->filament_temp_mixing_notification_state = mixing_state; - const bool can_slice = curr_plate != nullptr && curr_plate->can_slice() && slicing_allowed; + const bool can_slice = curr_plate->can_slice() && slicing_allowed; p->main_frame->update_slice_print_status(MainFrame::eEventPlateUpdate, can_slice); return slicing_allowed; } From 87c1d6972002cb99c8747ecdadbe43fbc524394c Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Wed, 17 Jun 2026 20:07:59 +0800 Subject: [PATCH 129/135] fix: remove CMAKE_POLICY_VERSION_MINIMUM override from CMakeLists.txt Co-Authored-By: Claude Opus 4.7 --- CMakeLists.txt | 4 ---- 1 file changed, 4 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 5313ddb10d5..c509e8d7287 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,9 +1,5 @@ cmake_minimum_required(VERSION 3.13) -# Allow building with CMake newer than 3.28 without failing on dependency cmake -# config files that use cmake_policy(VERSION ...3.28) range syntax -set(CMAKE_POLICY_VERSION_MINIMUM 3.28) - # Verify that your CMake version is exactly 3.31.x series or lower on windows if ( ((MSVC) OR (WIN32)) AND (${CMAKE_VERSION} VERSION_GREATER_EQUAL "4.0") ) message(FATAL_ERROR "Only cmake versions between 3.13.x and 3.31.x is supported on windows. Detected version: ${CMAKE_VERSION}") From 742834c3e60e3dc8bb770e15bf5eb9a62d5394f4 Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Wed, 17 Jun 2026 20:12:56 +0800 Subject: [PATCH 130/135] chore: regenerate .pot template from source (xgettext --full) Co-Authored-By: Claude Opus 4.7 --- localization/i18n/Snapmaker_Orca.pot | 1598 ++++++++++++++++++-------- 1 file changed, 1111 insertions(+), 487 deletions(-) diff --git a/localization/i18n/Snapmaker_Orca.pot b/localization/i18n/Snapmaker_Orca.pot index bbccf182eda..3753372e9ab 100644 --- a/localization/i18n/Snapmaker_Orca.pot +++ b/localization/i18n/Snapmaker_Orca.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-09-28 18:44+0800\n" +"POT-Creation-Date: 2026-06-17 20:11+0800\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -202,6 +202,17 @@ msgstr "" msgid "Shortcut Key " msgstr "" +#, possible-c-format, possible-boost-format +msgid "Filament %d" +msgstr "" + +msgid "Selected filament" +msgstr "" + +#, possible-c-format, possible-boost-format +msgid "/ %d" +msgstr "" + msgid "Triangle" msgstr "" @@ -1446,7 +1457,8 @@ msgstr "" msgid "%1% was replaced with %2%" msgstr "" -msgid "The configuration may be generated by a newer version of Snapmaker Orca." +msgid "" +"The configuration may be generated by a newer version of Snapmaker Orca." msgstr "" msgid "Some values have been replaced. Please check them:" @@ -1469,12 +1481,9 @@ msgid "" "Configuration file \"%1%\" was loaded, but some values were not recognized." msgstr "" -msgid "Based on PrusaSlicer and BambuStudio" -msgstr "" - msgid "" -"Snapmaker Orca will terminate because of running out of memory. It may be a bug. " -"It will be appreciated if you report the issue to our team." +"Snapmaker Orca will terminate because of running out of memory.It may be a " +"bug. It will be appreciated if you report the issue to our team." msgstr "" msgid "Fatal error" @@ -1530,9 +1539,6 @@ msgstr "" msgid "Remember my choice" msgstr "" -msgid "Loading configuration" -msgstr "" - #, possible-c-format, possible-boost-format msgid "Click to download new version in default browser: %s" msgstr "" @@ -1546,8 +1552,12 @@ msgstr "" msgid "Info" msgstr "" +msgid "The configuration is up to date." +msgstr "" + msgid "" -"The Snapmaker Orca configuration file may be corrupted and cannot be parsed.\n" +"The Snapmaker Orca configuration file may be corrupted and cannot be " +"parsed.\n" "Snapmaker Orca has attempted to recreate the configuration file.\n" "Please note, application settings will be lost, but printer profiles will " "not be affected." @@ -1577,6 +1587,9 @@ msgstr "" msgid "Choose one file (gcode/3mf):" msgstr "" +msgid "import failed!" +msgstr "" + msgid "Some presets are modified." msgstr "" @@ -1595,11 +1608,12 @@ msgid "Open Project" msgstr "" msgid "" -"The version of Snapmaker Orca is too low and needs to be updated to the latest " -"version before it can be used normally." +"The version of Snapmaker Orca is too low and needs to be updated to the " +"latest version before it can be used normally" msgstr "" -msgid "Privacy Policy Update" +#, possible-c-format, possible-boost-format +msgid "request to server update soft fail with body:%s,error:%s,status:%d" msgstr "" msgid "" @@ -1664,6 +1678,13 @@ msgstr "" msgid "Fatal error, exception caught: %1%" msgstr "" +msgid "Default" +msgstr "" + +#, possible-c-format, possible-boost-format +msgid "Mixed Filament %d" +msgstr "" + msgid "Quality" msgstr "" @@ -1873,13 +1894,6 @@ msgstr "" msgid "Set filament for selected items" msgstr "" -msgid "Default" -msgstr "" - -#, possible-c-format, possible-boost-format -msgid "Filament %d" -msgstr "" - msgid "current" msgstr "" @@ -2003,43 +2017,13 @@ msgstr "" msgid "Auto orient the object to improve print quality." msgstr "" -msgid "Merge with" -msgstr "" - -msgid "Import Profile" -msgstr "" - -msgid "Import Web Resource" -msgstr "" - -msgid "The following profiles could not be imported due to outdated versions." -msgstr "" - -msgid "This web resouce could not be imported due to outdated versions." -msgstr "" - -msgid "Import Failed" -msgstr "" - -msgid "Please choose a system profile package file:" -msgstr "" - -msgid "Please choose a web resource package file:" -msgstr "" - -msgid "Updating the web resources requires application restart." -msgstr "" - -msgid "Updating the system resources requires application restart." -msgstr "" - -msgid "Print Preprocessing" +msgid "Edit" msgstr "" -msgid "Pretreat the uploaded content" +msgid "Delete this filament" msgstr "" -msgid "Wait for the temperature when changing tools" +msgid "Merge with" msgstr "" msgid "Select All" @@ -2573,9 +2557,6 @@ msgid "" "load or unload filaments." msgstr "" -msgid "Edit" -msgstr "" - msgid "" "All the selected objects are on a locked plate.\n" "Cannot auto-arrange these objects." @@ -2812,7 +2793,7 @@ msgstr "" msgid "GNU Affero General Public License, version 3" msgstr "" -msgid "Snapmaker Orca is based on PrusaSlicer and BambuStudio" +msgid "Snapmaker Orca is Based on Orca Slicer" msgstr "" msgid "Libraries" @@ -2827,33 +2808,41 @@ msgstr "" msgid "About %s" msgstr "" -msgid "Snapmaker Orca is based on BambuStudio, PrusaSlicer, and SuperSlicer." +msgid "Snapmaker Orca " msgstr "" -msgid "BambuStudio is originally based on PrusaSlicer by PrusaResearch." +msgid "Note: The firmware version of SnapmakerU1 should be no less than" +msgstr "" + +msgid "Snapmaker Orca is forked from Orca Slicer by SoftFever." msgstr "" -msgid "PrusaSlicer is originally based on Slic3r by Alessandro Ranellucci." +msgid "OrcaSlicer is forked from Bambu Studio by Bambu Lab." msgstr "" msgid "" -"Slic3r was created by Alessandro Ranellucci with the help of many other " -"contributors." +"Bambu Studio is forked from PrusaSlicer by Prusa Research, which is from " +"Slic3r by Alessandro Ranellucci and the RepRap community. Orca Slicer " +"incorporates a lot of features from SuperSlicer by @supermerill." msgstr "" -msgid "Version" +msgid "" +"Snapmaker Orca integrates some features from the 3D printing open-source " +"community, with specific attributions provided in the code comments." msgstr "" -msgid "AMS Materials Setting" +msgid "" +"Snapmaker Orca is licensed under the GNU Affero General Public License, " +"version 3." msgstr "" -msgid "Confirm" +msgid "Version" msgstr "" -msgid "Confirm slicing" +msgid "AMS Materials Setting" msgstr "" -msgid "This material combination may cause risks. Do you want to continue?" +msgid "Confirm" msgstr "" msgid "Close" @@ -3644,6 +3633,7 @@ msgid "" "Reset to 0." msgstr "" +#, possible-c-format, possible-boost-format msgid "" "Lock depth should smaller than skin depth.\n" "Reset to 50% of skin depth." @@ -4322,7 +4312,7 @@ msgstr "" msgid "Size:" msgstr "" -#, possible-boost-format +#, possible-c-format, possible-boost-format msgid "" "Conflicts of G-code paths have been found at layer %d, z = %.2lf mm. Please " "separate the conflicted objects farther (%s <-> %s)." @@ -4476,12 +4466,6 @@ msgstr "" msgid "Export all sliced file" msgstr "" -msgid "Print all" -msgstr "" - -msgid "Send all" -msgstr "" - msgid "Keyboard Shortcuts" msgstr "" @@ -4494,12 +4478,30 @@ msgstr "" msgid "Show Configuration Folder" msgstr "" +msgid "Export Logs" +msgstr "" + +msgid "Export application logs as ZIP file" +msgstr "" + msgid "Show Tip of the Day" msgstr "" msgid "Check for Update" msgstr "" +msgid "Check for Process Preset Updates" +msgstr "" + +msgid "Check for Web Resource Updates" +msgstr "" + +msgid "Import Profile" +msgstr "" + +msgid "Import Web Resource" +msgstr "" + msgid "Open Network Test" msgstr "" @@ -4566,7 +4568,7 @@ msgstr "" msgid "Open a project file" msgstr "" -msgid "Recent files" +msgid "Recent projects" msgstr "" msgid "Save Project" @@ -4914,9 +4916,32 @@ msgstr "" msgid "The project is no longer available." msgstr "" +msgid "downloading the model" +msgstr "" + +#, possible-c-format, possible-boost-format +msgid "The downloaded file '%s' is not a valid 3MF project file." +msgstr "" + +msgid "Invalid File" +msgstr "" + msgid "Filament Settings" msgstr "" +msgid "No logs found." +msgstr "" + +msgid "Save logs as ZIP file" +msgstr "" + +msgid "Logs exported successfully." +msgstr "" + +#, possible-c-format, possible-boost-format +msgid "Failed to export logs: %s" +msgstr "" + msgid "" "Do you want to synchronize your personal data from Bambu Cloud?\n" "It contains the following information:\n" @@ -5111,8 +5136,8 @@ msgid "Failed to parse model information." msgstr "" msgid "" -"The .gcode.3mf file contains no G-code data. Please slice it with Orca " -"Slicer and export a new .gcode.3mf file." +"The .gcode.3mf file contains no G-code data. Please slice it with Snapmaker " +"Orca and export a new .gcode.3mf file." msgstr "" #, possible-c-format, possible-boost-format @@ -5416,6 +5441,65 @@ msgid "" "to give a positive rating (4 or 5 stars)." msgstr "" +msgid "select file" +msgstr "" + +msgid "UpLoad Failed" +msgstr "" + +msgid " has already been uploaded" +msgstr "" + +msgid "UpLoad Successfully" +msgstr "" + +msgid "Save file" +msgstr "" + +msgid " download failed" +msgstr "" + +msgid "DownLoad Failed" +msgstr "" + +msgid " has already been downloaded" +msgstr "" + +msgid "DownLoad Successfully" +msgstr "" + +msgid "setting failed" +msgstr "" + +msgid "Print Job Setting" +msgstr "" + +msgid "Create Failed" +msgstr "" + +msgid "" +"Connection has been disconnected and recovery attempt failed. Please " +"reconnect." +msgstr "" + +msgid "Machine Disconnected" +msgstr "" + +msgid "The target machine model has been detected as" +msgstr "" + +msgid "Please bind the nozzle information" +msgstr "" + +msgid "Nozzle Bind" +msgstr "" + +msgid "The target machine model has not been detected. Please bind manually." +msgstr "" + +msgid "Machine Bind" +msgstr "" + msgid "Status" msgstr "" @@ -5449,15 +5533,12 @@ msgstr "" msgid "%s information" msgstr "" -msgid "Skip" -msgstr "" - msgid "Newer 3mf version" msgstr "" msgid "" -"The 3mf file version is in Beta and it is newer than the current Snapmaker Orca " -"version." +"The 3mf file version is in Beta and it is newer than the current Snapmaker " +"Orca version." msgstr "" msgid "If you would like to try Snapmaker Orca Beta, you may click to" @@ -5469,7 +5550,8 @@ msgstr "" msgid "The 3mf file version is newer than the current Snapmaker Orca version." msgstr "" -msgid "Update your Snapmaker Orca could enable all functionality in the 3mf file." +msgid "" +"Updating your Snapmaker Orca could enable all functionality in the 3MF file." msgstr "" msgid "Current Version: " @@ -5590,6 +5672,9 @@ msgstr "" msgid "Error:" msgstr "" +msgid "WARNING:" +msgstr "" + msgid "Warning:" msgstr "" @@ -5608,9 +5693,6 @@ msgstr "" msgid " Click here to install it." msgstr "" -msgid "WARNING:" -msgstr "" - msgid "Your model needs support! Please enable support material." msgstr "" @@ -5754,6 +5836,21 @@ msgstr "" msgid " plate %1%:" msgstr "" +msgid "" +"Detected both high and low temperature materials. Mixed printing may result " +"in extruder clogging, nozzle damage, or layer adhesion issues." +msgstr "" + +msgid "" +"Detected both high and low temperature materials. Mixed printing may result " +"in extruder clogging, nozzle damage, or layer adhesion issues. To continue " +"printing, enable \"Allow mixed printing of high and low temperature materials" +"\" in Preferences." +msgstr "" + +msgid "Nozzle" +msgstr "" + msgid "Invalid name, the following characters are not allowed:" msgstr "" @@ -5778,148 +5875,397 @@ msgstr "" msgid "Filament changes" msgstr "" -msgid "Click to edit preset" +msgid "Add Color" msgstr "" -msgid "Connection" +msgid "" +"Pick from the current filament gamut. The dialog previews the closest 2-" +"color, 3-color, or 4-color FilamentMixer recipe before it is added." msgstr "" -msgid "Bed type" +msgid "Hex" msgstr "" -msgid "Flushing volumes" +msgid "" +"Enter a hex color like #00FF88. The picker will snap to the closest " +"supported FilamentMixer color." msgstr "" -msgid "Add one filament" +msgid "" +"Classic color picker. The result will snap to the closest supported " +"FilamentMixer color." msgstr "" -msgid "Remove last filament" +msgid "" +"Minimum percent for each participating color. Higher values block highly " +"skewed mixes." msgstr "" -msgid "Synchronize filament list from AMS" +msgid "Requested" msgstr "" -msgid "Set filaments to use" +msgid "Creates" msgstr "" -msgid "Search plate, object and part." +msgid "Exact preset mixes" msgstr "" -msgid "Pellets" +msgid "Matching closest supported mix..." msgstr "" -msgid "" -"No AMS filaments. Please select a printer in 'Device' page to load AMS info." +msgid "Calculating closest supported mix..." msgstr "" -msgid "Sync filaments with AMS" +#, possible-c-format, possible-boost-format +msgid "%d%% min" msgstr "" -msgid "" -"Sync filaments with AMS will drop all current selected filament presets and " -"colors. Do you want to continue?" +msgid "Use a valid hex color like #00FF88." msgstr "" -msgid "" -"Already did a synchronization, do you want to sync only changes or resync " -"all?" +msgid "No supported 2-color, 3-color, or 4-color recipe found." msgstr "" -msgid "Sync" +#, possible-c-format, possible-boost-format +msgid "Matching %s..." msgstr "" -msgid "Resync" +#, possible-c-format, possible-boost-format +msgid "Requested %s, closest recipe delta: %.2f" msgstr "" -msgid "There are no compatible filaments, and sync is not performed." +msgid "" +"Unable to create a color mix from the current physical filament colors " +"within the selected range." msgstr "" -msgid "" -"There are some unknown filaments mapped to generic preset. Please update " -"Snapmaker Orca or restart Snapmaker Orca to check if there is an update to system " -"presets." +msgid "Brass" msgstr "" -#, possible-boost-format -msgid "Do you want to save changes to \"%1%\"?" +msgid "Synchronize nozzle information" msgstr "" -#, possible-c-format, possible-boost-format msgid "" -"Successfully unmounted. The device %s (%s) can now be safely removed from " -"the computer." +"Printer not connected. Please go to the home page or the device page to " +"connect the printer." msgstr "" -#, possible-c-format, possible-boost-format -msgid "Ejecting of device %s (%s) has failed." +msgid "Note" msgstr "" -msgid "Previous unsaved project detected, do you want to restore it?" +msgid "" +"No nozzle information detected. Please go to the printer settings to " +"configure the nozzle." msgstr "" -msgid "Restore" +msgid "" +"Note: Inconsistent nozzle diameters. Current version does not support mixed " +"diameter printing. Please select one nozzle for this print." msgstr "" -msgid "" -"The current hot bed temperature is relatively high. The nozzle may be " -"clogged when printing this filament in a closed enclosure. Please open the " -"front door and/or remove the upper glass." +msgid "Set Nozzle Diameter" msgstr "" -msgid "" -"The nozzle hardness required by the filament is higher than the default " -"nozzle hardness of the printer. Please replace the hardened nozzle or " -"filament, otherwise, the nozzle will be attrited or damaged." +msgid "Nozzle settings synchronized successfully" msgstr "" -msgid "" -"Enabling traditional timelapse photography may cause surface imperfections. " -"It is recommended to change to smooth mode." +msgid "settings" msgstr "" -msgid "Expand sidebar" +msgid "Connect to printer" msgstr "" -msgid "Collapse sidebar" +msgid "The machine has been connected and is currently in working mode" msgstr "" -msgid "Tab" +msgid "Bed type" msgstr "" -#, possible-c-format, possible-boost-format -msgid "Loading file: %s" +msgid "Filament Management" msgstr "" -msgid "The 3mf is not supported by Snapmaker Orca, load geometry data only." +msgid "Flushing volumes" msgstr "" -msgid "Load 3mf" +msgid "Synchronize filament list from AMS" msgstr "" -msgid "" -"This project was created with an Snapmaker Orca 2.3.1-alpha and uses infill " -"rotation template settings that may not work properly with your current " -"infill pattern. This could result in weak support or print quality issues." +msgid "Set filaments to use" msgstr "" -msgid "" -"Would you like Snapmaker Orca to automatically fix this by clearing the rotation " -"template settings?" +msgid "Click to edit preset" msgstr "" -#, possible-c-format, possible-boost-format -msgid "" -"The 3mf's version %s is newer than %s's version %s, found following " -"unrecognized keys:" +msgid "Remove last filament" msgstr "" -msgid "You'd better upgrade your software.\n" +msgid "Add one filament" msgstr "" -#, possible-c-format, possible-boost-format -msgid "" -"The 3mf's version %s is newer than %s's version %s, Suggest to upgrade your " -"software." +msgid "Mixed Filaments" +msgstr "" + +msgid "Add Gradient" +msgstr "" + +msgid "Add Pattern" +msgstr "" + +msgid "" +"Unable to create a color match from the current physical filament colors." +msgstr "" + +msgid "Search plate, object and part." +msgstr "" + +msgid "Pellet Configuration" +msgstr "" + +msgid "Gradient Mix Weights" +msgstr "" + +msgid "Pick a point in the gradient map to control multi-filament mix." +msgstr "" + +msgid "Click to choose a physical filament color" +msgstr "" + +msgid "Pattern" +msgstr "" + +msgid "" +"Manual repeating pattern. Digits 1-9 for filament IDs 1-9. Use [N] for IDs " +">= 10 (e.g. [12]). Use commas to define per-perimeter groups, e.g. 12,21. " +"Example: 11112222, 12,21, or 1234." +msgstr "" + +#, possible-c-format, possible-boost-format +msgid "Append filament %d to pattern" +msgstr "" + +msgid "" +"Positive bias recesses the second filament in the pair; negative bias " +"recesses the first filament.\n" +"\n" +"The color chip shows which filament the current value affects.\n" +"\n" +"Grouped wall patterns and Local-Z dithering ignore it." +msgstr "" + +msgid "Bias" +msgstr "" + +msgid "Limit Local-Z" +msgstr "" + +msgid "" +"Store a per-color Local-Z cadence cap. It applies when Local-Z dithering " +"mode is enabled in print settings." +msgstr "" + +msgid "Max sublayers" +msgstr "" + +msgid "" +"Maximum number of Local-Z sublayers this color may use before its cadence " +"repeats." +msgstr "" + +#, possible-c-format, possible-boost-format +msgid "%d-color layer cycle" +msgstr "" + +#, possible-c-format, possible-boost-format +msgid "Simple %d%%/%d%%" +msgstr "" + +#, possible-c-format, possible-boost-format +msgid "%d%%/%d%%" +msgstr "" + +msgid "None (Selected)" +msgstr "" + +msgid "Color Mixing" +msgstr "" + +msgid "" +"No mixed filaments yet. Use Add Gradient, Add Pattern, or Add Color to " +"create one." +msgstr "" + +msgid "(Pattern)" +msgstr "" + +msgid "(Color)" +msgstr "" + +msgid "Drag to reorder mixed filaments in this panel." +msgstr "" + +msgid "Enabled" +msgstr "" + +msgid "Delete mixed filament" +msgstr "" + +msgid "Click to edit automatic mixed filament settings (saved as custom)." +msgstr "" + +msgid "Click to expand/retract mixed filament settings" +msgstr "" + +msgid "" +"The target mixed filament uses this physical filament as a component. " +"Merging will remove this physical filament and may invalidate the mixed " +"filament. Continue?" +msgstr "" + +msgid "" +"This filament is used in the following mixed filament configurations:\n" +"\n" +msgstr "" + +#, possible-c-format, possible-boost-format +msgid "• Mixed Filament %d\n" +msgstr "" + +msgid "" +"\n" +"Merging this filament will invalidate these mixed filament configurations. " +"Continue?" +msgstr "" + +msgid "" +"\n" +"Deleting this filament will invalidate these mixed filament configurations. " +"Continue?" +msgstr "" + +msgid "" +"No AMS filaments. Please select a printer in 'Device' page to load AMS info." +msgstr "" + +msgid "Sync filaments with AMS" +msgstr "" + +msgid "" +"Sync filaments with AMS will drop all current selected filament presets and " +"colors. Do you want to continue?" +msgstr "" + +msgid "" +"Already did a synchronization, do you want to sync only changes or resync " +"all?" +msgstr "" + +msgid "Sync" +msgstr "" + +msgid "Resync" +msgstr "" + +msgid "There are no compatible filaments, and sync is not performed." +msgstr "" + +msgid "" +"There are some unknown filaments mapped to generic preset. Please update " +"Snapmaker Orca or restart Snapmaker Orca to check if there is an update to " +"system presets." +msgstr "" + +msgid "Note: Changing this will sync all other nozzles to the same diameter." +msgstr "" + +msgid "Don't show this again" +msgstr "" + +msgid "Left Nozzle" +msgstr "" + +msgid "Right Nozzle" +msgstr "" + +#, possible-boost-format +msgid "Do you want to save changes to \"%1%\"?" +msgstr "" + +#, possible-c-format, possible-boost-format +msgid "" +"Successfully unmounted. The device %s (%s) can now be safely removed from " +"the computer." +msgstr "" + +#, possible-c-format, possible-boost-format +msgid "Ejecting of device %s (%s) has failed." +msgstr "" + +msgid "Previous unsaved project detected, do you want to restore it?" +msgstr "" + +msgid "Restore" +msgstr "" + +msgid "" +"The current hot bed temperature is relatively high. The nozzle may be " +"clogged when printing this filament in a closed enclosure. Please open the " +"front door and/or remove the upper glass." +msgstr "" + +msgid "" +"The nozzle hardness required by the filament is higher than the default " +"nozzle hardness of the printer. Please replace the hardened nozzle or " +"filament, otherwise, the nozzle will be attrited or damaged." +msgstr "" + +msgid "" +"Enabling traditional timelapse photography may cause surface imperfections. " +"It is recommended to change to smooth mode." +msgstr "" + +msgid "Expand sidebar" +msgstr "" + +msgid "Collapse sidebar" +msgstr "" + +msgid "Tab" +msgstr "" + +#, possible-c-format, possible-boost-format +msgid "Loading file: %s" +msgstr "" + +msgid "The 3mf is not supported by Snapmaker Orca, load geometry data only." +msgstr "" + +msgid "Load 3mf" +msgstr "" + +msgid "" +"This project was created with an Snapmaker Orca and uses infill rotation " +"template settings that may not work properly with your current infill " +"pattern. This could result in weak support or print quality issues." +msgstr "" + +msgid "" +"Would you like Snapmaker Orca to automatically fix this by clearing the " +"rotation template settings?" +msgstr "" + +#, possible-c-format, possible-boost-format +msgid "" +"The 3mf's version %s is newer than %s's version %s, found following " +"unrecognized keys:" +msgstr "" + +msgid "You'd better upgrade your software.\n" +msgstr "" + +#, possible-c-format, possible-boost-format +msgid "" +"The 3mf's version %s is newer than %s's version %s, Suggest to upgrade your " +"software." msgstr "" msgid "Invalid values found in the 3mf:" @@ -5951,6 +6297,11 @@ msgstr "" msgid "Customized Preset" msgstr "" +msgid "" +"Printing by object with caution. This function may cause the print head to " +"collide with printed parts during switching." +msgstr "" + msgid "Name of components inside step file is not UTF8 format!" msgstr "" @@ -6038,6 +6389,33 @@ msgstr "" msgid "The selected object couldn't be split." msgstr "" +msgid "(unknown)" +msgstr "" + +#, possible-c-format, possible-boost-format +msgid "Note: Using a %s mm %s nozzle for %s is not recommended." +msgstr "" + +msgid "" +"Note: Filament may not adhere well to the smooth PEI plate on the first " +"layer. Apply glue before printing." +msgstr "" + +msgid "" +"Note: Filament may stick too strongly to the smooth PEI plate. Apply glue to " +"protect the plate and ease part removal." +msgstr "" + +msgid "" +"Note: Low adhesion to the graphic effect plate may cause failure. Use a " +"different filament instead." +msgstr "" + +msgid "" +"Mixed filaments contain incompatible material types. Please correct the " +"mixed filaments settings before slicing." +msgstr "" + msgid "Another export job is running." msgstr "" @@ -6093,6 +6471,15 @@ msgstr "" msgid "Please resolve the slicing errors and publish again." msgstr "" +#, possible-c-format, possible-boost-format +msgid "" +"Using %d physical filaments will create %d auto-generated gradients.\n" +"Do you want to create them now?" +msgstr "" + +msgid "Auto gradients" +msgstr "" + msgid "" "Network Plug-in is not detected. Network related features are unavailable." msgstr "" @@ -6102,6 +6489,9 @@ msgid "" "The loaded file contains G-code only, cannot enter the Prepare page." msgstr "" +msgid "Cannot enable both Variable Layer Height and Subdivide Mix Layer." +msgstr "" + msgid "You can keep the modified presets to the new project or discard them" msgstr "" @@ -6283,6 +6673,12 @@ msgid "" "Suggest to use auto-arrange to avoid collisions when printing." msgstr "" +msgid "This material combination may cause risks. Do you want to continue?" +msgstr "" + +msgid "Confirm slicing" +msgstr "" + msgid "Send G-code" msgstr "" @@ -6370,12 +6766,18 @@ msgstr "" msgid "Changing application language" msgstr "" -msgid "Changing the region will log out your account.\n" +msgid "Changing the region will log out your account and restart.\n" msgstr "" msgid "Region selection" msgstr "" +msgid "Change Region" +msgstr "" + +msgid "Switching the region requires application restart.\n" +msgstr "" + msgid "Second" msgstr "" @@ -6406,9 +6808,6 @@ msgstr "" msgid "Asia-Pacific" msgstr "" -msgid "China" -msgstr "" - msgid "Chinese Mainland" msgstr "" @@ -6424,27 +6823,6 @@ msgstr "" msgid "Login Region" msgstr "" -msgid "Stealth Mode" -msgstr "" - -msgid "" -"This stops the transmission of data to Bambu's cloud services. Users who " -"don't use BBL machines or use LAN mode only can safely turn on this function." -msgstr "" - -msgid "Enable network plugin" -msgstr "" - -msgid "Use legacy network plugin (Takes effect after restarting Orca)" -msgstr "" - -msgid "" -"Disable to use latest network plugin that supports new BambuLab firmwares." -msgstr "" - -msgid "Check for stable updates only" -msgstr "" - msgid "Metric" msgstr "" @@ -6517,6 +6895,27 @@ msgstr "" msgid "If enabled, reverses the direction of zoom with mouse wheel." msgstr "" +msgid "Allow high/low temperature filament mixing" +msgstr "" + +msgid "" +"If enabled, allows printing with both high-temperature and low-temperature " +"filaments simultaneously." +msgstr "" + +msgid "" +"Mixing materials with significantly different printing temperatures may " +"result in:\n" +"· Extruder clogging\n" +"· Nozzle damage\n" +"· Layer adhesion issues\n" +"\n" +"Do you want to enable this feature?" +msgstr "" + +msgid "High and Low Temperature Material Mixing Risk" +msgstr "" + msgid "Orbit speed multiplier" msgstr "" @@ -6577,9 +6976,6 @@ msgstr "" msgid "Auto arrange plate after object cloning" msgstr "" -msgid "Network" -msgstr "" - msgid "Auto sync user presets (Printer/Filament/Process)" msgstr "" @@ -6601,19 +6997,22 @@ msgstr "" msgid "Associate .3mf files to Snapmaker Orca" msgstr "" -msgid "If enabled, sets Snapmaker Orca as default application to open .3mf files" +msgid "" +"If enabled, sets Snapmaker Orca as default application to open .3mf files" msgstr "" msgid "Associate .stl files to Snapmaker Orca" msgstr "" -msgid "If enabled, sets Snapmaker Orca as default application to open .stl files" +msgid "" +"If enabled, sets Snapmaker Orca as default application to open .stl files" msgstr "" msgid "Associate .step/.stp files to Snapmaker Orca" msgstr "" -msgid "If enabled, sets Snapmaker Orca as default application to open .step files" +msgid "" +"If enabled, sets Snapmaker Orca as default application to open .step files" msgstr "" msgid "Associate web links to Snapmaker Orca" @@ -6677,6 +7076,18 @@ msgstr "" msgid "Enable Dark mode" msgstr "" +msgid "Enable dark mode" +msgstr "" + +msgid "User Experience" +msgstr "" + +msgid "Join Customer Experience Improvement Program." +msgstr "" + +msgid "What data would be collected?" +msgstr "" + msgid "Develop mode" msgstr "" @@ -6797,6 +7208,9 @@ msgstr "" msgid "Edit preset" msgstr "" +msgid "Machine Filament" +msgstr "" + msgid "Project-inside presets" msgstr "" @@ -6978,6 +7392,9 @@ msgstr "" msgid "Bambu Textured PEI Plate" msgstr "" +msgid "Graphic Effect Plate" +msgstr "" + msgid "Send print job to" msgstr "" @@ -7390,8 +7807,8 @@ msgstr "" msgid "" "When recording timelapse without toolhead, it is recommended to add a " "\"Timelapse Wipe Tower\" \n" -"by right-click the empty position of build plate and choose \"Add " -"Primitive\"->\"Timelapse Wipe Tower\"." +"by right-click the empty position of build plate and choose \"Add Primitive" +"\"->\"Timelapse Wipe Tower\"." msgstr "" msgid "" @@ -7544,6 +7961,9 @@ msgstr "" msgid "Ooze prevention" msgstr "" +msgid "Color Mixing (Experimental)" +msgstr "" + msgid "Skirt" msgstr "" @@ -7601,9 +8021,6 @@ msgstr "" msgid "Print temperature" msgstr "" -msgid "Nozzle" -msgstr "" - msgid "Nozzle temperature when printing" msgstr "" @@ -7656,6 +8073,11 @@ msgid "" "the filament does not support printing on the Textured PEI Plate." msgstr "" +msgid "" +"Bed temperature when the Graphic Effect Plate is installed. A value of 0 " +"means the filament does not support printing on the Graphic Effect Plate." +msgstr "" + msgid "Volumetric speed limitation" msgstr "" @@ -7725,6 +8147,9 @@ msgstr "" msgid "Compatible process profiles" msgstr "" +msgid "Smooth PEI Plate" +msgstr "" + msgid "Printable space" msgstr "" @@ -8099,9 +8524,6 @@ msgstr "" msgid "No updates available." msgstr "" -msgid "The configuration is up to date." -msgstr "" - msgid "Obj file Import color" msgstr "" @@ -8133,13 +8555,17 @@ msgstr "" msgid "Add consumable extruder after existing extruders." msgstr "" -msgid "Keep color" +msgid "Reset mapped extruders." msgstr "" -msgid "Add this cluster color as a new filament." +msgid "" +"Warning: The count of newly added and \n" +"current extruders exceeds 16." msgstr "" -msgid "Reset mapped extruders." +msgid "" +"Note: The color has been selected, you can choose OK \n" +"to continue or manually adjust it." msgstr "" msgid "Cluster colors" @@ -8148,14 +8574,10 @@ msgstr "" msgid "Map Filament" msgstr "" -msgid "" -"Note: The color has been selected, you can choose OK \n" -"to continue or manually adjust it." +msgid "Keep color" msgstr "" -msgid "" -"Warning: The count of newly added and \n" -"current extruders exceeds 16." +msgid "Add this cluster color as a new filament." msgstr "" msgid "Ramming customization" @@ -8251,7 +8673,7 @@ msgstr "" msgid "" "Your system is missing H.264 codecs for GStreamer, which are required to " -"play video. (Try installing the gstreamer1.0-plugins-bad or gstreamer1.0-" +"play video. (Try installing the gstreamer1.0-plugins-bad or gstreamer1.0-" "libav packages, then restart Snapmaker Orca?)" msgstr "" @@ -8264,6 +8686,9 @@ msgstr "" msgid "Login" msgstr "" +msgid "Log out" +msgstr "" + msgid "The configuration package is changed in previous Config Guide" msgstr "" @@ -8503,7 +8928,8 @@ msgid "Network plug-in update" msgstr "" msgid "" -"Click OK to update the Network plug-in when Snapmaker Orca launches next time." +"Click OK to update the Network plug-in when Snapmaker Orca launches next " +"time." msgstr "" #, possible-c-format, possible-boost-format @@ -8516,6 +8942,9 @@ msgstr "" msgid "Skip this Version" msgstr "" +msgid "Check for stable updates only" +msgstr "" + msgid "Done" msgstr "" @@ -8559,11 +8988,11 @@ msgid "Connect the printer using IP and access code" msgstr "" msgid "" -"Step 1. Please confirm Snapmaker Orca and your printer are in the same LAN." +"Step 1, please confirm Snapmaker Orca and your printer are in the same LAN." msgstr "" msgid "" -"Step 2. If the IP and Access Code below are different from the actual values " +"Step 2, if the IP and Access Code below are different from the actual values " "on your printer, please correct them." msgstr "" @@ -8721,6 +9150,27 @@ msgstr "" msgid "Copying of file %1% to %2% failed: %3%" msgstr "" +#, possible-c-format, possible-boost-format +msgid "Flutter resource download failed: %s" +msgstr "" + +#, possible-c-format, possible-boost-format +msgid "Profiles resource download failed: %s" +msgstr "" + +#, possible-c-format, possible-boost-format +msgid "" +"request to server update web resource fail with body:%s,error:%s,status:%d" +msgstr "" + +#, possible-c-format, possible-boost-format +msgid "" +"request to server update preset resource fail with body:%s,error:%s,status:%d" +msgstr "" + +msgid "needed, but current version is " +msgstr "" + msgid "Need to check the unsaved changes before configuration updates." msgstr "" @@ -8730,6 +9180,30 @@ msgstr "" msgid " updated to " msgstr "" +msgid "Import Failed" +msgstr "" + +msgid "This web resouce could not be imported due to outdated versions." +msgstr "" + +msgid "Updating the web resources requires application restart." +msgstr "" + +msgid "Snapmaker Orca" +msgstr "" + +msgid "Update web resources" +msgstr "" + +msgid "Please choose a web resource package file:" +msgstr "" + +msgid "Please choose a system profile package file:" +msgstr "" + +msgid "The following profiles could not be imported due to outdated versions." +msgstr "" + msgid "Open G-code file:" msgstr "" @@ -9302,8 +9776,7 @@ msgstr "" msgid "Avoid crossing walls" msgstr "" -msgid "" -"Detour to avoid traveling across walls, which may cause blobs on the surface." +msgid "Detour and avoid to travel across wall which may cause blob on surface" msgstr "" msgid "Avoid crossing walls - Max detour length" @@ -9352,6 +9825,11 @@ msgid "" "filament does not support printing on the Textured PEI Plate." msgstr "" +msgid "" +"Bed temperature for layers except the initial one. A value of 0 means the " +"filament does not support printing on the Graphic Effect Plate." +msgstr "" + msgid "Initial layer" msgstr "" @@ -9388,6 +9866,11 @@ msgid "" "not support printing on the Textured PEI Plate." msgstr "" +msgid "" +"Bed temperature of the initial layer. A value of 0 means the filament does " +"not support printing on the Graphic Effect Plate." +msgstr "" + msgid "Bed types supported by the printer." msgstr "" @@ -10267,7 +10750,7 @@ msgid "" "This option sets the max point for the allowed bed mesh area. Due to the " "probe's XY offset, most printers are unable to probe the entire bed. To " "ensure the probe point does not go outside the bed area, the minimum and " -"maximum points of the bed mesh should be set appropriately. Snapmaker Orca " +"maximum points of the bed mesh should be set appropriately. Snapmaker_Orca " "ensures that adaptive_bed_mesh_min/adaptive_bed_mesh_max values do not " "exceed these min/max points. This information can usually be obtained from " "your printer manufacturer. The default setting is (99999, 99999), which " @@ -10457,6 +10940,18 @@ msgid "" "Right click to reset value to system default." msgstr "" +msgid "small thumb" +msgstr "" + +msgid "first small thumb" +msgstr "" + +msgid "big thumb" +msgstr "" + +msgid "first big thumb" +msgstr "" + msgid "Filament notes" msgstr "" @@ -10504,6 +10999,14 @@ msgid "" "typically 0. For statistics only." msgstr "" +msgid "Wait for the temperature when changing tools" +msgstr "" + +msgid "" +"It will use the M109 instead of M104 T[target] after changing tools if this " +"is set to true" +msgstr "" + msgid "" "Filament diameter is used to calculate extrusion in G-code, so it is " "important and should be accurate." @@ -10616,8 +11119,9 @@ msgid "" "After a tool change, the exact position of the newly loaded filament inside " "the nozzle may not be known, and the filament pressure is likely not yet " "stable. Before purging the print head into an infill or a sacrificial " -"object, Snapmaker Orca will always prime this amount of material into the wipe " -"tower to produce successive infill or sacrificial object extrusions reliably." +"object, Snapmaker Orca will always prime this amount of material into the " +"wipe tower to produce successive infill or sacrificial object extrusions " +"reliably." msgstr "" msgid "Speed of the last cooling move" @@ -10679,6 +11183,14 @@ msgid "" "Support material is commonly used to print supports and support interfaces." msgstr "" +msgid "Is high-temperature filament" +msgstr "" + +msgid "" +"Indicates whether this is a high-temperature filament that requires elevated " +"printing temperatures." +msgstr "" + msgid "Softening temperature" msgstr "" @@ -10910,8 +11422,8 @@ msgid "mm/s² or %" msgstr "" msgid "" -"Acceleration of sparse infill. If the value is expressed as a percentage " -"(e.g. 100%), it will be calculated based on the default acceleration." +"Acceleration of sparse infill. If the value is expressed as a percentage (e." +"g. 100%), it will be calculated based on the default acceleration." msgstr "" msgid "" @@ -11012,10 +11524,10 @@ msgstr "" msgid "" "Fan speed will be ramped up linearly from zero at layer " -"\"close_fan_the_first_x_layers\" to maximum at layer " -"\"full_fan_speed_layer\". \"full_fan_speed_layer\" will be ignored if lower " -"than \"close_fan_the_first_x_layers\", in which case the fan will be running " -"at maximum allowed speed at layer \"close_fan_the_first_x_layers\" + 1." +"\"close_fan_the_first_x_layers\" to maximum at layer \"full_fan_speed_layer" +"\". \"full_fan_speed_layer\" will be ignored if lower than " +"\"close_fan_the_first_x_layers\", in which case the fan will be running at " +"maximum allowed speed at layer \"close_fan_the_first_x_layers\" + 1." msgstr "" msgid "layer" @@ -11252,9 +11764,6 @@ msgstr "" msgid "Stainless steel" msgstr "" -msgid "Brass" -msgstr "" - msgid "Nozzle HRC" msgstr "" @@ -12107,6 +12616,224 @@ msgid "" "environment variables." msgstr "" +msgid "Dithering cadence height A" +msgstr "" + +msgid "" +"Layer height contribution of component A for dithering virtual filaments. " +"Set to 0 to use normal 1-layer A / 1-layer B alternation.\n" +"\n" +"Detailed mixed filament setting explanations will be published once the " +"project wiki is available." +msgstr "" + +msgid "Dithering cadence height B" +msgstr "" + +msgid "" +"Layer height contribution of component B for dithering virtual filaments. " +"Set to 0 to use normal 1-layer A / 1-layer B alternation.\n" +"\n" +"Detailed mixed filament setting explanations will be published once the " +"project wiki is available." +msgstr "" + +msgid "Height-weighted cadence" +msgstr "" + +msgid "" +"Enable height-weighted cadence for mixed filaments. Limitation: only one " +"height-weighted mixed color should be present at a given Z plane, because " +"independent per-color layer heights are not supported and the resulting " +"layer height applies to the whole plane. When disabled, layer-cycle cadence " +"is used.\n" +"\n" +"Detailed mixed filament setting explanations will be published once the " +"project wiki is available." +msgstr "" + +msgid "Local-Z lower height bound" +msgstr "" + +msgid "" +"Lower bound used when Local-Z mixed-filament dithering chooses per-color " +"sublayer heights.\n" +"\n" +"Smaller values let Local-Z use thinner sublayers for a color when needed.\n" +"\n" +"Detailed mixed filament setting explanations will be published once the " +"project wiki is available." +msgstr "" + +msgid "Local-Z upper height bound" +msgstr "" + +msgid "" +"Upper bound used when Local-Z mixed-filament dithering chooses per-color " +"sublayer heights.\n" +"\n" +"Larger values let Local-Z use thicker sublayers for a color when needed.\n" +"\n" +"Detailed mixed filament setting explanations will be published once the " +"project wiki is available." +msgstr "" + +msgid "Advanced dithering" +msgstr "" + +msgid "" +"Distribute mixed filament layer-cycle cadence using an advanced ordered " +"dithering pattern instead of a simple contiguous A-then-B run. This can " +"reduce visible striping for some hues.\n" +"\n" +"This is an even more experimental mode and the perceived color may differ " +"from normal dithering for the same filament pair and ratio.\n" +"\n" +"Detailed mixed filament setting explanations will be published once the " +"project wiki is available." +msgstr "" + +msgid "Pointillisme pixel size" +msgstr "" + +msgid "" +"Length of one pointillisme segment along an extrusion path for same-layer " +"pointillisme mode. Set to 0 to use automatic nozzle-based sizing.\n" +"\n" +"Warning: Same-layer pointillisme is extremely experimental and may produce " +"unusable results." +msgstr "" + +msgid "Pointillisme line gap" +msgstr "" + +msgid "" +"Optional non-extruded spacing between adjacent pointillisme segments. " +"Increase carefully to improve separation and print quality.\n" +"\n" +"Warning: Same-layer pointillisme is extremely experimental and may produce " +"unusable results." +msgstr "" + +msgid "Enable mixed filament bias" +msgstr "" + +msgid "" +"Show and apply the per-row mixed filament Bias control.\n" +"\n" +"When enabled, the selected filament in a mixed pair is recessed slightly so " +"the other component becomes more visible.\n" +"\n" +"Bias is ignored for grouped wall patterns, same-layer pointillisme, and " +"Local Z dithering." +msgstr "" + +msgid "Selective Expansion contraction" +msgstr "" + +msgid "" +"XY offset applied to mixed-filament painted regions before region " +"assignment.\n" +"\n" +"Positive values contract the mixed zone inward. Negative values expand it " +"outward.\n" +"\n" +"This applies to mixed filament usage in layer cadence, height cadence, same-" +"layer pointillisme, and local Z dithering." +msgstr "" + +msgid "Collapse same-color mixed regions" +msgstr "" + +msgid "" +"Merge ordinary mixed-filament painted regions into a single area when they " +"resolve to the same physical filament on a layer.\n" +"\n" +"This improves continuity for adjacent same-color areas.\n" +"\n" +"Subdivide Mix Layer disables this behavior, and gradient mixed regions also " +"bypass it because they use the Local-Z pipeline." +msgstr "" + +msgid "Mixed filament custom definitions" +msgstr "" + +msgid "" +"Serialized custom mixed filament rows.\n" +"\n" +"Detailed mixed filament setting explanations will be published once the " +"project wiki is available." +msgstr "" + +msgid "Dithering Z step size" +msgstr "" + +msgid "" +"Layer height used in Z zones painted with dithering (mixed virtual " +"filaments). Set to 0 to keep normal layer height in those zones.\n" +"\n" +"Detailed mixed filament setting explanations will be published once the " +"project wiki is available." +msgstr "" + +msgid "Subdivide Mix Layer" +msgstr "" + +msgid "Material" +msgstr "" + +msgid "" +"Enable \"Subdivide Mix Layer\" for mixing areas. Layer height will be " +"subdivided for better color mixing results." +msgstr "" + +msgid "Full domain" +msgstr "" + +msgid "" +"Experimental. Apply Local-Z thinning across whole mixed-color regions " +"instead of limiting the effect strictly to painted mixed masks.\n" +"\n" +"Only available when Subdivide Mix Layer is enabled." +msgstr "" + +msgid "Apply subdivision to infill" +msgstr "" + +msgid "" +"Experimental. When Subdivide Mix Layer is enabled, also apply the same " +"subdivision to infill inside mixed-color areas.\n" +"\n" +"This is enabled automatically with Subdivide Mix Layer. Turn it off to keep " +"infill on the normal layer height.\n" +"\n" +"It can improve internal color mixing, but may add toolchanges and affect " +"infill behavior." +msgstr "" + +msgid "Use direct multicolor Local-Z solver" +msgstr "" + +msgid "" +"Experimental. For mixed rows with 3 or more physical filaments, allocate " +"Local-Z sublayers directly across all components with carry-over error " +"between layers instead of collapsing them into pair cadence.\n" +"\n" +"This can reduce visible banding in multicolor Local-Z blends at the cost of " +"more toolchanges. It is ignored when explicit Local-Z A/B heights are set." +msgstr "" + +msgid "Use step size in painted zones only" +msgstr "" + +msgid "" +"When enabled, dithering Z step size is applied only where mixed filament is " +"painted. Unpainted zones keep their original layer height.\n" +"\n" +"Detailed mixed filament setting explanations will be published once the " +"project wiki is available." +msgstr "" + msgid "Printer type" msgstr "" @@ -12207,6 +12934,9 @@ msgid "" "change." msgstr "" +msgid "Retraction Length (Toolchange)" +msgstr "" + msgid "Z-hop height" msgstr "" @@ -12244,6 +12974,9 @@ msgstr "" msgid "Spiral" msgstr "" +msgid "Z hop when moving to tower" +msgstr "" + msgid "Traveling angle" msgstr "" @@ -12468,8 +13201,8 @@ msgid "Role base wipe speed" msgstr "" msgid "" -"The wipe speed is determined by the speed of the current extrusion role. " -"e.g. if a wipe action is executed immediately following an outer wall " +"The wipe speed is determined by the speed of the current extrusion role. e." +"g. if a wipe action is executed immediately following an outer wall " "extrusion, the speed of the outer wall extrusion will be utilized for the " "wipe action." msgstr "" @@ -12551,9 +13284,6 @@ msgid "" "with them. To avoid this, increase the skirt distance value.\n" msgstr "" -msgid "Enabled" -msgstr "" - msgid "Skirt type" msgstr "" @@ -12696,6 +13426,14 @@ msgid "" "the tool in advance." msgstr "" +msgid "Preheat delta temperature" +msgstr "" + +msgid "" +"Allow user to set the Preheat temperature. If target temperature is 220 and " +"Preheat delta temperature is -30, then the preheat temperature will be 190" +msgstr "" + msgid "Preheat steps" msgstr "" @@ -12739,6 +13477,26 @@ msgstr "" msgid "Enable filament ramming" msgstr "" +msgid "Ramming line width ratio" +msgstr "" + +msgid "" +"This is used to decide the line width of wipe tower when ramming, ramming " +"line width = [this ratio] * extruder * 1.25" +msgstr "" + +msgid "Enable change pressure advance when wiping" +msgstr "" + +msgid "If it's set to false, the pressure advance value will not be changed." +msgstr "" + +msgid "Pressure advance value when ramming" +msgstr "" + +msgid "Set_Pressure_advance [this value] when ramming on wipe tower" +msgstr "" + msgid "No sparse layers (beta)" msgstr "" @@ -13158,8 +13916,8 @@ msgstr "" msgid "" "Enable this option for automated chamber temperature control. This option " -"activates the emitting of an M191 command before the " -"\"machine_start_gcode\"\n" +"activates the emitting of an M191 command before the \"machine_start_gcode" +"\"\n" " which sets the chamber temperature and waits until it is reached. In " "addition, it emits an M141 command at the end of the print to turn off the " "chamber heater, if present.\n" @@ -13323,6 +14081,24 @@ msgstr "" msgid "Width of the brim." msgstr "" +msgid "Brim chamfer" +msgstr "" + +msgid "" +"Enable gradual layer-by-layer reduction of the brim around the prime tower. " +"This creates a chamfered/tapered effect, reducing material usage while " +"maintaining first layer adhesion." +msgstr "" + +msgid "Max chamfer width" +msgstr "" + +msgid "" +"Maximum width of the chamfer zone measured from the tower perimeter. The " +"brim will reduce within this distance. Larger values create a more gradual " +"taper but take more layers to complete." +msgstr "" + msgid "Stabilization cone apex angle" msgstr "" @@ -13331,29 +14107,10 @@ msgid "" "Larger angle means wider base." msgstr "" -msgid "Maximum wipe tower print speed" +msgid "Maximum wipe tower print speed of out wall" msgstr "" -msgid "" -"The maximum print speed when purging in the wipe tower and printing the wipe " -"tower sparse layers. When purging, if the sparse infill speed or calculated " -"speed from the filament max volumetric speed is lower, the lowest will be " -"used instead.\n" -"\n" -"When printing the sparse layers, if the internal perimeter speed or " -"calculated speed from the filament max volumetric speed is lower, the lowest " -"will be used instead.\n" -"\n" -"Increasing this speed may affect the tower's stability as well as increase " -"the force with which the nozzle collides with any blobs that may have formed " -"on the wipe tower.\n" -"\n" -"Before increasing this parameter beyond the default of 90 mm/s, make sure " -"your printer can reliably bridge at the increased speeds and that ooze when " -"tool changing is well controlled.\n" -"\n" -"For the wipe tower external perimeters the internal perimeter speed is used " -"regardless of this setting." +msgid "Maximum wipe tower print speed of out wall." msgstr "" msgid "Wall type" @@ -13394,6 +14151,16 @@ msgid "" "use the one that is available (non-soluble would be preferred)." msgstr "" +msgid "Wall gap" +msgstr "" + +msgid "" +"Create small gaps in the wipe tower outer wall at tool change entry points. " +"The first extrusion path after a filament change will enter through the gap, " +"leaving the filament blob on the gap edge instead of on the outer wall " +"surface." +msgstr "" + msgid "Purging volumes - load/unload volumes" msgstr "" @@ -13443,6 +14210,18 @@ msgid "" "is adjusted automatically." msgstr "" +msgid "Local-Z mini wipe lines" +msgstr "" + +msgid "" +"Number of purge lines reserved for each runtime Local-Z wipe tower " +"toolchange. Higher values improve cleanup but increase tower depth. Only " +"used when Local-Z dithering and the prime tower are enabled." +msgstr "" + +msgid "lines" +msgstr "" + msgid "Idle temperature" msgstr "" @@ -13452,6 +14231,17 @@ msgid "" "0 to disable." msgstr "" +msgid "Tower ironing area" +msgstr "" + +msgid "" +"Ironing area for prime tower interface layer (where different materials " +"meet)." +msgstr "" + +msgid "mm²" +msgstr "" + msgid "X-Y hole compensation" msgstr "" @@ -13880,8 +14670,8 @@ msgid "Debug level" msgstr "" msgid "" -"Sets debug logging level. 0:fatal, 1:error, 2:warning, 3:info, 4:debug, " -"5:trace\n" +"Sets debug logging level. 0:fatal, 1:error, 2:warning, 3:info, 4:debug, 5:" +"trace\n" msgstr "" msgid "Enable timelapse for print" @@ -14686,9 +15476,6 @@ msgid "" "- Different filament brand and family (Brand = Bambu, Family = Basic, Matte)" msgstr "" -msgid "Pattern" -msgstr "" - msgid "Method" msgstr "" @@ -14699,12 +15486,6 @@ msgstr "" msgid "TPU is not supported for Flow Dynamics Auto-Calibration." msgstr "" -msgid "" -"Cannot print multiple filaments which have large difference of temperature " -"together. Otherwise, the extruder and nozzle may be blocked or damaged " -"during printing" -msgstr "" - msgid "Connecting to printer" msgstr "" @@ -15007,13 +15788,18 @@ msgstr "" msgid "Upload to Printer Host with the following filename:" msgstr "" -msgid "Use forward slashes ( / ) as a directory separator if needed." +msgid "" +"Please do not include the special characters #, *, ;, \\, /, :, \", <, >, or " +"| in filenames." msgstr "" msgid "Upload to storage" msgstr "" -msgid "Switch to Device tab after upload." +#, possible-c-format, possible-boost-format +msgid "" +"The filename '%s' contains special characters (#, *, ;, \\, /, :, \", <, >, " +"or |) which may cause issues.Do you wish to continue?" msgstr "" #, possible-c-format, possible-boost-format @@ -15060,6 +15846,12 @@ msgstr "" msgid "Error uploading to print host" msgstr "" +msgid "Use forward slashes ( / ) as a directory separator if needed." +msgstr "" + +msgid "Switch to Device tab after upload." +msgstr "" + msgid "" "The selected bed type does not match the file. Please confirm before " "starting the print." @@ -15128,6 +15920,9 @@ msgstr "" msgid "Export Log" msgstr "" +msgid "Clear Log" +msgstr "" + msgid "Snapmaker Orca Version:" msgstr "" @@ -15137,10 +15932,10 @@ msgstr "" msgid "DNS Server:" msgstr "" -msgid "Test Snapmaker Orca (GitHub)" +msgid "Test Snapmaker Orca(GitHub)" msgstr "" -msgid "Test Snapmaker Orca (GitHub):" +msgid "Test Snapmaker Orca(GitHub):" msgstr "" msgid "Test bing.com" @@ -15149,9 +15944,49 @@ msgstr "" msgid "Test bing.com:" msgstr "" +msgid "Test LAN Device" +msgstr "" + +msgid "Test LAN Device:" +msgstr "" + +msgid "Test Cloud Server" +msgstr "" + +msgid "Test Cloud Server:" +msgstr "" + +msgid "Test Login API" +msgstr "" + +msgid "Test Login API:" +msgstr "" + +msgid "Test Upload API" +msgstr "" + +msgid "Test Upload API:" +msgstr "" + msgid "Log Info" msgstr "" +msgid "Network test started" +msgstr "" + +msgid "" +"Please enter the LAN device IP address for testing (leave empty to skip):" +msgstr "" + +msgid "LAN Device Test - Sequence Mode" +msgstr "" + +msgid "Please enter the device IP address:" +msgstr "" + +msgid "LAN Device Test" +msgstr "" + msgid "Select filament preset" msgstr "" @@ -15247,8 +16082,8 @@ msgid "" msgstr "" msgid "" -"We would rename the presets as \"Vendor Type Serial @printer you " -"selected\".\n" +"We would rename the presets as \"Vendor Type Serial @printer you selected" +"\".\n" "To add preset for more printers, please go to printer selection" msgstr "" @@ -15586,9 +16421,6 @@ msgstr "" msgid "Edit Preset" msgstr "" -msgid "For more information, please check out Wiki" -msgstr "" - msgid "Collapse" msgstr "" @@ -15767,6 +16599,15 @@ msgstr "" msgid "Could not connect to Prusa Connect" msgstr "" +msgid "Connection to Moonraker works correctly." +msgstr "" + +msgid "Could not connect to Moonraker" +msgstr "" + +msgid "Note: Moonraker version at least 1.1.0 is required." +msgstr "" + msgid "Connection to Repetier is working correctly." msgstr "" @@ -16047,220 +16888,3 @@ msgstr "" msgid "Please select single object." msgstr "" - -#: resources/data/hints.ini: [hint:Precise wall] -msgid "Precise wall\nDid you know that turning on precise wall can improve precision and layer consistency?" -msgstr "" - -#: resources/data/hints.ini: [hint:Sandwich mode] -msgid "Sandwich mode\nDid you know that you can use sandwich mode (inner-outer-inner) to improve precision and layer consistency if your model doesn't have very steep overhangs?" -msgstr "" - -#: resources/data/hints.ini: [hint:Chamber temperature] -msgid "Chamber temperature\nDid you know that Snapmaker Orca supports chamber temperature?" -msgstr "" - -#: resources/data/hints.ini: [hint:Calibration] -msgid "Calibration\nDid you know that calibrating your printer can do wonders? Check out our beloved calibration solution in Snapmaker Orca." -msgstr "" - -#: resources/data/hints.ini: [hint:Auxiliary fan] -msgid "Auxiliary fan\nDid you know that Snapmaker Orca supports Auxiliary part cooling fan?" -msgstr "" - -#: resources/data/hints.ini: [hint:Air filtration] -msgid "Air filtration/Exhaust Fan\nDid you know that Snapmaker Orca can support Air filtration/Exhaust Fan?" -msgstr "" - -#: resources/data/hints.ini: [hint:G-code window] -msgid "G-code window\nYou can turn on/off the G-code window by pressing the C key." -msgstr "" - -#: resources/data/hints.ini: [hint:Switch workspaces] -msgid "Switch workspaces\nYou can switch between Prepare and Preview workspaces by pressing the Tab key." -msgstr "" - -#: resources/data/hints.ini: [hint:How to use keyboard shortcuts] -msgid "How to use keyboard shortcuts\nDid you know that Snapmaker Orca offers a wide range of keyboard shortcuts and 3D scene operations?" -msgstr "" - -#: resources/data/hints.ini: [hint:Reverse on odd] -msgid "Reverse on odd\nDid you know that Reverse on odd feature can significantly improve the surface quality of your overhangs?" -msgstr "" - -#: resources/data/hints.ini: [hint:Cut Tool] -msgid "Cut Tool\nDid you know that you can cut a model at any angle and position with the cutting tool?" -msgstr "" - -#: resources/data/hints.ini: [hint:Fix Model] -msgid "Fix Model\nDid you know that you can fix a corrupted 3D model to avoid a lot of slicing problems on the Windows system?" -msgstr "" - -#: resources/data/hints.ini: [hint:Timelapse] -msgid "Timelapse\nDid you know that you can generate a timelapse video during each print?" -msgstr "" - -#: resources/data/hints.ini: [hint:Auto-Arrange] -msgid "Auto-Arrange\nDid you know that you can auto-arrange all the objects in your project?" -msgstr "" - -#: resources/data/hints.ini: [hint:Auto-Orient] -msgid "Auto-Orient\nDid you know that you can rotate objects to an optimal orientation for printing with a simple click?" -msgstr "" - -#: resources/data/hints.ini: [hint:Lay on Face] -msgid "Lay on Face\nDid you know that you can quickly orient a model so that one of its faces sits on the print bed? Select the \"Place on face\" function or press the F key." -msgstr "" - -#: resources/data/hints.ini: [hint:Object List] -msgid "Object List\nDid you know that you can view all objects/parts in a list and change settings for each object/part?" -msgstr "" - -#: resources/data/hints.ini: [hint:Search Functionality] -msgid "Search Functionality\nDid you know that you use the Search tool to quickly find a specific Snapmaker Orca setting?" -msgstr "" - -#: resources/data/hints.ini: [hint:Simplify Model] -msgid "Simplify Model\nDid you know that you can reduce the number of triangles in a mesh using the Simplify mesh feature? Right-click the model and select Simplify model." -msgstr "" - -#: resources/data/hints.ini: [hint:Slicing Parameter Table] -msgid "Slicing Parameter Table\nDid you know that you can view all objects/parts on a table and change settings for each object/part?" -msgstr "" - -#: resources/data/hints.ini: [hint:Split to Objects/Parts] -msgid "Split to Objects/Parts\nDid you know that you can split a big object into small ones for easy colorizing or printing?" -msgstr "" - -#: resources/data/hints.ini: [hint:Subtract a Part] -msgid "Subtract a Part\nDid you know that you can subtract one mesh from another using the Negative part modifier? That way you can, for example, create easily resizable holes directly in Snapmaker Orca." -msgstr "" - -#: resources/data/hints.ini: [hint:STEP] -msgid "STEP\nDid you know that you can improve your print quality by slicing a STEP file instead of an STL?\nSnapmaker Orca supports slicing STEP files, providing smoother results than a lower resolution STL. Give it a try!" -msgstr "" - -#: resources/data/hints.ini: [hint:Z seam location] -msgid "Z seam location\nDid you know that you can customize the location of the Z seam, and even paint it on your print, to have it in a less visible location? This improves the overall look of your model. Check it out!" -msgstr "" - -#: resources/data/hints.ini: [hint:Fine-tuning for flow rate] -msgid "Fine-tuning for flow rate\nDid you know that flow rate can be fine-tuned for even better-looking prints? Depending on the material, you can improve the overall finish of the printed model by doing some fine-tuning." -msgstr "" - -#: resources/data/hints.ini: [hint:Split your prints into plates] -msgid "Split your prints into plates\nDid you know that you can split a model that has a lot of parts into individual plates ready to print? This will simplify the process of keeping track of all the parts." -msgstr "" - -#: resources/data/hints.ini: [hint:Speed up your print with Adaptive Layer Height] -msgid "Speed up your print with Adaptive Layer Height\nDid you know that you can print a model even faster, by using the Adaptive Layer Height option? Check it out!" -msgstr "" - -#: resources/data/hints.ini: [hint:Support painting] -msgid "Support painting\nDid you know that you can paint the location of your supports? This feature makes it easy to place the support material only on the sections of the model that actually need it." -msgstr "" - -#: resources/data/hints.ini: [hint:Different types of supports] -msgid "Different types of supports\nDid you know that you can choose from multiple types of supports? Tree supports work great for organic models, while saving filament and improving print speed. Check them out!" -msgstr "" - -#: resources/data/hints.ini: [hint:Printing Silk Filament] -msgid "Printing Silk Filament\nDid you know that Silk filament needs special consideration to print it successfully? Higher temperature and lower speed are always recommended for the best results." -msgstr "" - -#: resources/data/hints.ini: [hint:Brim for better adhesion] -msgid "Brim for better adhesion\nDid you know that when printed models have a small contact interface with the printing surface, it's recommended to use a brim?" -msgstr "" - -#: resources/data/hints.ini: [hint:Set parameters for multiple objects] -msgid "Set parameters for multiple objects\nDid you know that you can set slicing parameters for all selected objects at once?" -msgstr "" - -#: resources/data/hints.ini: [hint:Stack objects] -msgid "Stack objects\nDid you know that you can stack objects as a whole one?" -msgstr "" - -#: resources/data/hints.ini: [hint:Flush into support/objects/infill] -msgid "Flush into support/objects/infill\nDid you know that you can save wasted filament by flushing it into support/objects/infill during filament change?" -msgstr "" - -#: resources/data/hints.ini: [hint:Improve strength] -msgid "Improve strength\nDid you know that you can use more wall loops and higher sparse infill density to improve the strength of the model?" -msgstr "" - -#: resources/data/hints.ini: [hint:When do you need to print with the printer door opened] -msgid "When do you need to print with the printer door opened?\nDid you know that opening the printer door can reduce the probability of extruder/hotend clogging when printing lower temperature filament with a higher enclosure temperature? More info about this in the Wiki." -msgstr "" - -#: resources/data/hints.ini: [hint:Avoid warping] -msgid "Avoid warping\nDid you know that when printing materials that are prone to warping such as ABS, appropriately increasing the heatbed temperature can reduce the probability of warping?" -msgstr "" - -msgid "Ramming line width ratio" -msgstr "" - -msgid "Enable change pressure advance when wiping" -msgstr "" - -msgid "Pressure advance value when ramming" -msgstr "" - -msgid "Ramming line width ratio" -msgstr "" - -msgid "Enable change pressure advance when wiping" -msgstr "" - -msgid "Pressure advance value when ramming" -msgstr "" - -msgid "Z hop when moving to tower" -msgstr "" - -msgid "Retraction Length(Toolchange)" -msgstr "" - -msgid "Face and face assembly" -msgstr "" - -msgid "Point and point assembly" -msgstr "" - -msgid "Please do not include the special characters #, *, ;, \\, /, :, \", <, >, or | in filenames." -msgstr "" - -msgid "The filename '%s' contains special characters (#, *, ;, \\, /, :, \", <, >, or |) which may cause issues.Do you wish to continue?" -msgstr "" - -msgid "Connection has been disconnected and recovery attempt failed. Please reconnect." -msgstr "" - -msgid "User Experience" -msgstr "" - -msgid "Join Customer Experience Improvement Program." -msgstr "" - -msgid "What data would be collected?" -msgstr "" - -msgid "Note: The firmware version of SnapmakerU1 should be no less than" -msgstr "" - -msgid "Snapmaker Orca is forked from Orca Slicer by SoftFever." -msgstr "" - -msgid "Bambu Studio is forked from PrusaSlicer by Prusa Research, which is from Slic3r by Alessandro Ranellucci and the RepRap community. Orca Slicer incorporates a lot of features from SuperSlicer by @supermerill." -msgstr "" - -msgid "Snapmaker Orca integrates some features from the 3D printing open-source community, with specific attributions provided in the code comments." -msgstr " - -msgid "Snapmaker Orca is licensed under the GNU Affero General Public License, version 3." -msgstr "" - -msgid "OrcaSlicer is forked from Bambu Studio by Bambu Lab." -msgstr "" - -msgid "Incompatible filament types cannot be mixed. Please correct the selection." -msgstr "" From 915a8952acdc91ef96307f731559b1a69ac97bfb Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Thu, 18 Jun 2026 09:56:53 +0800 Subject: [PATCH 131/135] Revert "chore: regenerate .pot template from source (xgettext --full)" This reverts commit 742834c3e60e3dc8bb770e15bf5eb9a62d5394f4. --- localization/i18n/Snapmaker_Orca.pot | 1620 ++++++++------------------ 1 file changed, 498 insertions(+), 1122 deletions(-) diff --git a/localization/i18n/Snapmaker_Orca.pot b/localization/i18n/Snapmaker_Orca.pot index 3753372e9ab..bbccf182eda 100644 --- a/localization/i18n/Snapmaker_Orca.pot +++ b/localization/i18n/Snapmaker_Orca.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-06-17 20:11+0800\n" +"POT-Creation-Date: 2025-09-28 18:44+0800\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -202,17 +202,6 @@ msgstr "" msgid "Shortcut Key " msgstr "" -#, possible-c-format, possible-boost-format -msgid "Filament %d" -msgstr "" - -msgid "Selected filament" -msgstr "" - -#, possible-c-format, possible-boost-format -msgid "/ %d" -msgstr "" - msgid "Triangle" msgstr "" @@ -1457,8 +1446,7 @@ msgstr "" msgid "%1% was replaced with %2%" msgstr "" -msgid "" -"The configuration may be generated by a newer version of Snapmaker Orca." +msgid "The configuration may be generated by a newer version of Snapmaker Orca." msgstr "" msgid "Some values have been replaced. Please check them:" @@ -1481,9 +1469,12 @@ msgid "" "Configuration file \"%1%\" was loaded, but some values were not recognized." msgstr "" +msgid "Based on PrusaSlicer and BambuStudio" +msgstr "" + msgid "" -"Snapmaker Orca will terminate because of running out of memory.It may be a " -"bug. It will be appreciated if you report the issue to our team." +"Snapmaker Orca will terminate because of running out of memory. It may be a bug. " +"It will be appreciated if you report the issue to our team." msgstr "" msgid "Fatal error" @@ -1539,6 +1530,9 @@ msgstr "" msgid "Remember my choice" msgstr "" +msgid "Loading configuration" +msgstr "" + #, possible-c-format, possible-boost-format msgid "Click to download new version in default browser: %s" msgstr "" @@ -1552,12 +1546,8 @@ msgstr "" msgid "Info" msgstr "" -msgid "The configuration is up to date." -msgstr "" - msgid "" -"The Snapmaker Orca configuration file may be corrupted and cannot be " -"parsed.\n" +"The Snapmaker Orca configuration file may be corrupted and cannot be parsed.\n" "Snapmaker Orca has attempted to recreate the configuration file.\n" "Please note, application settings will be lost, but printer profiles will " "not be affected." @@ -1587,9 +1577,6 @@ msgstr "" msgid "Choose one file (gcode/3mf):" msgstr "" -msgid "import failed!" -msgstr "" - msgid "Some presets are modified." msgstr "" @@ -1608,12 +1595,11 @@ msgid "Open Project" msgstr "" msgid "" -"The version of Snapmaker Orca is too low and needs to be updated to the " -"latest version before it can be used normally" +"The version of Snapmaker Orca is too low and needs to be updated to the latest " +"version before it can be used normally." msgstr "" -#, possible-c-format, possible-boost-format -msgid "request to server update soft fail with body:%s,error:%s,status:%d" +msgid "Privacy Policy Update" msgstr "" msgid "" @@ -1678,13 +1664,6 @@ msgstr "" msgid "Fatal error, exception caught: %1%" msgstr "" -msgid "Default" -msgstr "" - -#, possible-c-format, possible-boost-format -msgid "Mixed Filament %d" -msgstr "" - msgid "Quality" msgstr "" @@ -1894,6 +1873,13 @@ msgstr "" msgid "Set filament for selected items" msgstr "" +msgid "Default" +msgstr "" + +#, possible-c-format, possible-boost-format +msgid "Filament %d" +msgstr "" + msgid "current" msgstr "" @@ -2017,13 +2003,43 @@ msgstr "" msgid "Auto orient the object to improve print quality." msgstr "" -msgid "Edit" +msgid "Merge with" +msgstr "" + +msgid "Import Profile" msgstr "" -msgid "Delete this filament" +msgid "Import Web Resource" msgstr "" -msgid "Merge with" +msgid "The following profiles could not be imported due to outdated versions." +msgstr "" + +msgid "This web resouce could not be imported due to outdated versions." +msgstr "" + +msgid "Import Failed" +msgstr "" + +msgid "Please choose a system profile package file:" +msgstr "" + +msgid "Please choose a web resource package file:" +msgstr "" + +msgid "Updating the web resources requires application restart." +msgstr "" + +msgid "Updating the system resources requires application restart." +msgstr "" + +msgid "Print Preprocessing" +msgstr "" + +msgid "Pretreat the uploaded content" +msgstr "" + +msgid "Wait for the temperature when changing tools" msgstr "" msgid "Select All" @@ -2557,6 +2573,9 @@ msgid "" "load or unload filaments." msgstr "" +msgid "Edit" +msgstr "" + msgid "" "All the selected objects are on a locked plate.\n" "Cannot auto-arrange these objects." @@ -2793,7 +2812,7 @@ msgstr "" msgid "GNU Affero General Public License, version 3" msgstr "" -msgid "Snapmaker Orca is Based on Orca Slicer" +msgid "Snapmaker Orca is based on PrusaSlicer and BambuStudio" msgstr "" msgid "Libraries" @@ -2808,41 +2827,33 @@ msgstr "" msgid "About %s" msgstr "" -msgid "Snapmaker Orca " +msgid "Snapmaker Orca is based on BambuStudio, PrusaSlicer, and SuperSlicer." msgstr "" -msgid "Note: The firmware version of SnapmakerU1 should be no less than" -msgstr "" - -msgid "Snapmaker Orca is forked from Orca Slicer by SoftFever." +msgid "BambuStudio is originally based on PrusaSlicer by PrusaResearch." msgstr "" -msgid "OrcaSlicer is forked from Bambu Studio by Bambu Lab." +msgid "PrusaSlicer is originally based on Slic3r by Alessandro Ranellucci." msgstr "" msgid "" -"Bambu Studio is forked from PrusaSlicer by Prusa Research, which is from " -"Slic3r by Alessandro Ranellucci and the RepRap community. Orca Slicer " -"incorporates a lot of features from SuperSlicer by @supermerill." +"Slic3r was created by Alessandro Ranellucci with the help of many other " +"contributors." msgstr "" -msgid "" -"Snapmaker Orca integrates some features from the 3D printing open-source " -"community, with specific attributions provided in the code comments." +msgid "Version" msgstr "" -msgid "" -"Snapmaker Orca is licensed under the GNU Affero General Public License, " -"version 3." +msgid "AMS Materials Setting" msgstr "" -msgid "Version" +msgid "Confirm" msgstr "" -msgid "AMS Materials Setting" +msgid "Confirm slicing" msgstr "" -msgid "Confirm" +msgid "This material combination may cause risks. Do you want to continue?" msgstr "" msgid "Close" @@ -3633,7 +3644,6 @@ msgid "" "Reset to 0." msgstr "" -#, possible-c-format, possible-boost-format msgid "" "Lock depth should smaller than skin depth.\n" "Reset to 50% of skin depth." @@ -4312,7 +4322,7 @@ msgstr "" msgid "Size:" msgstr "" -#, possible-c-format, possible-boost-format +#, possible-boost-format msgid "" "Conflicts of G-code paths have been found at layer %d, z = %.2lf mm. Please " "separate the conflicted objects farther (%s <-> %s)." @@ -4466,22 +4476,22 @@ msgstr "" msgid "Export all sliced file" msgstr "" -msgid "Keyboard Shortcuts" +msgid "Print all" msgstr "" -msgid "Show the list of the keyboard shortcuts" +msgid "Send all" msgstr "" -msgid "Setup Wizard" +msgid "Keyboard Shortcuts" msgstr "" -msgid "Show Configuration Folder" +msgid "Show the list of the keyboard shortcuts" msgstr "" -msgid "Export Logs" +msgid "Setup Wizard" msgstr "" -msgid "Export application logs as ZIP file" +msgid "Show Configuration Folder" msgstr "" msgid "Show Tip of the Day" @@ -4490,18 +4500,6 @@ msgstr "" msgid "Check for Update" msgstr "" -msgid "Check for Process Preset Updates" -msgstr "" - -msgid "Check for Web Resource Updates" -msgstr "" - -msgid "Import Profile" -msgstr "" - -msgid "Import Web Resource" -msgstr "" - msgid "Open Network Test" msgstr "" @@ -4568,7 +4566,7 @@ msgstr "" msgid "Open a project file" msgstr "" -msgid "Recent projects" +msgid "Recent files" msgstr "" msgid "Save Project" @@ -4916,32 +4914,9 @@ msgstr "" msgid "The project is no longer available." msgstr "" -msgid "downloading the model" -msgstr "" - -#, possible-c-format, possible-boost-format -msgid "The downloaded file '%s' is not a valid 3MF project file." -msgstr "" - -msgid "Invalid File" -msgstr "" - msgid "Filament Settings" msgstr "" -msgid "No logs found." -msgstr "" - -msgid "Save logs as ZIP file" -msgstr "" - -msgid "Logs exported successfully." -msgstr "" - -#, possible-c-format, possible-boost-format -msgid "Failed to export logs: %s" -msgstr "" - msgid "" "Do you want to synchronize your personal data from Bambu Cloud?\n" "It contains the following information:\n" @@ -5136,8 +5111,8 @@ msgid "Failed to parse model information." msgstr "" msgid "" -"The .gcode.3mf file contains no G-code data. Please slice it with Snapmaker " -"Orca and export a new .gcode.3mf file." +"The .gcode.3mf file contains no G-code data. Please slice it with Orca " +"Slicer and export a new .gcode.3mf file." msgstr "" #, possible-c-format, possible-boost-format @@ -5441,65 +5416,6 @@ msgid "" "to give a positive rating (4 or 5 stars)." msgstr "" -msgid "select file" -msgstr "" - -msgid "UpLoad Failed" -msgstr "" - -msgid " has already been uploaded" -msgstr "" - -msgid "UpLoad Successfully" -msgstr "" - -msgid "Save file" -msgstr "" - -msgid " download failed" -msgstr "" - -msgid "DownLoad Failed" -msgstr "" - -msgid " has already been downloaded" -msgstr "" - -msgid "DownLoad Successfully" -msgstr "" - -msgid "setting failed" -msgstr "" - -msgid "Print Job Setting" -msgstr "" - -msgid "Create Failed" -msgstr "" - -msgid "" -"Connection has been disconnected and recovery attempt failed. Please " -"reconnect." -msgstr "" - -msgid "Machine Disconnected" -msgstr "" - -msgid "The target machine model has been detected as" -msgstr "" - -msgid "Please bind the nozzle information" -msgstr "" - -msgid "Nozzle Bind" -msgstr "" - -msgid "The target machine model has not been detected. Please bind manually." -msgstr "" - -msgid "Machine Bind" -msgstr "" - msgid "Status" msgstr "" @@ -5533,12 +5449,15 @@ msgstr "" msgid "%s information" msgstr "" +msgid "Skip" +msgstr "" + msgid "Newer 3mf version" msgstr "" msgid "" -"The 3mf file version is in Beta and it is newer than the current Snapmaker " -"Orca version." +"The 3mf file version is in Beta and it is newer than the current Snapmaker Orca " +"version." msgstr "" msgid "If you would like to try Snapmaker Orca Beta, you may click to" @@ -5550,8 +5469,7 @@ msgstr "" msgid "The 3mf file version is newer than the current Snapmaker Orca version." msgstr "" -msgid "" -"Updating your Snapmaker Orca could enable all functionality in the 3MF file." +msgid "Update your Snapmaker Orca could enable all functionality in the 3mf file." msgstr "" msgid "Current Version: " @@ -5672,9 +5590,6 @@ msgstr "" msgid "Error:" msgstr "" -msgid "WARNING:" -msgstr "" - msgid "Warning:" msgstr "" @@ -5693,6 +5608,9 @@ msgstr "" msgid " Click here to install it." msgstr "" +msgid "WARNING:" +msgstr "" + msgid "Your model needs support! Please enable support material." msgstr "" @@ -5836,21 +5754,6 @@ msgstr "" msgid " plate %1%:" msgstr "" -msgid "" -"Detected both high and low temperature materials. Mixed printing may result " -"in extruder clogging, nozzle damage, or layer adhesion issues." -msgstr "" - -msgid "" -"Detected both high and low temperature materials. Mixed printing may result " -"in extruder clogging, nozzle damage, or layer adhesion issues. To continue " -"printing, enable \"Allow mixed printing of high and low temperature materials" -"\" in Preferences." -msgstr "" - -msgid "Nozzle" -msgstr "" - msgid "Invalid name, the following characters are not allowed:" msgstr "" @@ -5875,397 +5778,148 @@ msgstr "" msgid "Filament changes" msgstr "" -msgid "Add Color" +msgid "Click to edit preset" msgstr "" -msgid "" -"Pick from the current filament gamut. The dialog previews the closest 2-" -"color, 3-color, or 4-color FilamentMixer recipe before it is added." +msgid "Connection" msgstr "" -msgid "Hex" +msgid "Bed type" msgstr "" -msgid "" -"Enter a hex color like #00FF88. The picker will snap to the closest " -"supported FilamentMixer color." +msgid "Flushing volumes" msgstr "" -msgid "" -"Classic color picker. The result will snap to the closest supported " -"FilamentMixer color." +msgid "Add one filament" msgstr "" -msgid "" -"Minimum percent for each participating color. Higher values block highly " -"skewed mixes." +msgid "Remove last filament" msgstr "" -msgid "Requested" +msgid "Synchronize filament list from AMS" msgstr "" -msgid "Creates" +msgid "Set filaments to use" msgstr "" -msgid "Exact preset mixes" +msgid "Search plate, object and part." msgstr "" -msgid "Matching closest supported mix..." +msgid "Pellets" msgstr "" -msgid "Calculating closest supported mix..." +msgid "" +"No AMS filaments. Please select a printer in 'Device' page to load AMS info." msgstr "" -#, possible-c-format, possible-boost-format -msgid "%d%% min" +msgid "Sync filaments with AMS" msgstr "" -msgid "Use a valid hex color like #00FF88." +msgid "" +"Sync filaments with AMS will drop all current selected filament presets and " +"colors. Do you want to continue?" msgstr "" -msgid "No supported 2-color, 3-color, or 4-color recipe found." +msgid "" +"Already did a synchronization, do you want to sync only changes or resync " +"all?" msgstr "" -#, possible-c-format, possible-boost-format -msgid "Matching %s..." +msgid "Sync" msgstr "" -#, possible-c-format, possible-boost-format -msgid "Requested %s, closest recipe delta: %.2f" +msgid "Resync" msgstr "" -msgid "" -"Unable to create a color mix from the current physical filament colors " -"within the selected range." +msgid "There are no compatible filaments, and sync is not performed." msgstr "" -msgid "Brass" +msgid "" +"There are some unknown filaments mapped to generic preset. Please update " +"Snapmaker Orca or restart Snapmaker Orca to check if there is an update to system " +"presets." msgstr "" -msgid "Synchronize nozzle information" +#, possible-boost-format +msgid "Do you want to save changes to \"%1%\"?" msgstr "" +#, possible-c-format, possible-boost-format msgid "" -"Printer not connected. Please go to the home page or the device page to " -"connect the printer." +"Successfully unmounted. The device %s (%s) can now be safely removed from " +"the computer." msgstr "" -msgid "Note" +#, possible-c-format, possible-boost-format +msgid "Ejecting of device %s (%s) has failed." msgstr "" -msgid "" -"No nozzle information detected. Please go to the printer settings to " -"configure the nozzle." +msgid "Previous unsaved project detected, do you want to restore it?" msgstr "" -msgid "" -"Note: Inconsistent nozzle diameters. Current version does not support mixed " -"diameter printing. Please select one nozzle for this print." +msgid "Restore" msgstr "" -msgid "Set Nozzle Diameter" +msgid "" +"The current hot bed temperature is relatively high. The nozzle may be " +"clogged when printing this filament in a closed enclosure. Please open the " +"front door and/or remove the upper glass." msgstr "" -msgid "Nozzle settings synchronized successfully" +msgid "" +"The nozzle hardness required by the filament is higher than the default " +"nozzle hardness of the printer. Please replace the hardened nozzle or " +"filament, otherwise, the nozzle will be attrited or damaged." msgstr "" -msgid "settings" +msgid "" +"Enabling traditional timelapse photography may cause surface imperfections. " +"It is recommended to change to smooth mode." msgstr "" -msgid "Connect to printer" +msgid "Expand sidebar" msgstr "" -msgid "The machine has been connected and is currently in working mode" +msgid "Collapse sidebar" msgstr "" -msgid "Bed type" +msgid "Tab" msgstr "" -msgid "Filament Management" +#, possible-c-format, possible-boost-format +msgid "Loading file: %s" msgstr "" -msgid "Flushing volumes" +msgid "The 3mf is not supported by Snapmaker Orca, load geometry data only." msgstr "" -msgid "Synchronize filament list from AMS" +msgid "Load 3mf" msgstr "" -msgid "Set filaments to use" +msgid "" +"This project was created with an Snapmaker Orca 2.3.1-alpha and uses infill " +"rotation template settings that may not work properly with your current " +"infill pattern. This could result in weak support or print quality issues." msgstr "" -msgid "Click to edit preset" +msgid "" +"Would you like Snapmaker Orca to automatically fix this by clearing the rotation " +"template settings?" msgstr "" -msgid "Remove last filament" +#, possible-c-format, possible-boost-format +msgid "" +"The 3mf's version %s is newer than %s's version %s, found following " +"unrecognized keys:" msgstr "" -msgid "Add one filament" +msgid "You'd better upgrade your software.\n" msgstr "" -msgid "Mixed Filaments" -msgstr "" - -msgid "Add Gradient" -msgstr "" - -msgid "Add Pattern" -msgstr "" - -msgid "" -"Unable to create a color match from the current physical filament colors." -msgstr "" - -msgid "Search plate, object and part." -msgstr "" - -msgid "Pellet Configuration" -msgstr "" - -msgid "Gradient Mix Weights" -msgstr "" - -msgid "Pick a point in the gradient map to control multi-filament mix." -msgstr "" - -msgid "Click to choose a physical filament color" -msgstr "" - -msgid "Pattern" -msgstr "" - -msgid "" -"Manual repeating pattern. Digits 1-9 for filament IDs 1-9. Use [N] for IDs " -">= 10 (e.g. [12]). Use commas to define per-perimeter groups, e.g. 12,21. " -"Example: 11112222, 12,21, or 1234." -msgstr "" - -#, possible-c-format, possible-boost-format -msgid "Append filament %d to pattern" -msgstr "" - -msgid "" -"Positive bias recesses the second filament in the pair; negative bias " -"recesses the first filament.\n" -"\n" -"The color chip shows which filament the current value affects.\n" -"\n" -"Grouped wall patterns and Local-Z dithering ignore it." -msgstr "" - -msgid "Bias" -msgstr "" - -msgid "Limit Local-Z" -msgstr "" - -msgid "" -"Store a per-color Local-Z cadence cap. It applies when Local-Z dithering " -"mode is enabled in print settings." -msgstr "" - -msgid "Max sublayers" -msgstr "" - -msgid "" -"Maximum number of Local-Z sublayers this color may use before its cadence " -"repeats." -msgstr "" - -#, possible-c-format, possible-boost-format -msgid "%d-color layer cycle" -msgstr "" - -#, possible-c-format, possible-boost-format -msgid "Simple %d%%/%d%%" -msgstr "" - -#, possible-c-format, possible-boost-format -msgid "%d%%/%d%%" -msgstr "" - -msgid "None (Selected)" -msgstr "" - -msgid "Color Mixing" -msgstr "" - -msgid "" -"No mixed filaments yet. Use Add Gradient, Add Pattern, or Add Color to " -"create one." -msgstr "" - -msgid "(Pattern)" -msgstr "" - -msgid "(Color)" -msgstr "" - -msgid "Drag to reorder mixed filaments in this panel." -msgstr "" - -msgid "Enabled" -msgstr "" - -msgid "Delete mixed filament" -msgstr "" - -msgid "Click to edit automatic mixed filament settings (saved as custom)." -msgstr "" - -msgid "Click to expand/retract mixed filament settings" -msgstr "" - -msgid "" -"The target mixed filament uses this physical filament as a component. " -"Merging will remove this physical filament and may invalidate the mixed " -"filament. Continue?" -msgstr "" - -msgid "" -"This filament is used in the following mixed filament configurations:\n" -"\n" -msgstr "" - -#, possible-c-format, possible-boost-format -msgid "• Mixed Filament %d\n" -msgstr "" - -msgid "" -"\n" -"Merging this filament will invalidate these mixed filament configurations. " -"Continue?" -msgstr "" - -msgid "" -"\n" -"Deleting this filament will invalidate these mixed filament configurations. " -"Continue?" -msgstr "" - -msgid "" -"No AMS filaments. Please select a printer in 'Device' page to load AMS info." -msgstr "" - -msgid "Sync filaments with AMS" -msgstr "" - -msgid "" -"Sync filaments with AMS will drop all current selected filament presets and " -"colors. Do you want to continue?" -msgstr "" - -msgid "" -"Already did a synchronization, do you want to sync only changes or resync " -"all?" -msgstr "" - -msgid "Sync" -msgstr "" - -msgid "Resync" -msgstr "" - -msgid "There are no compatible filaments, and sync is not performed." -msgstr "" - -msgid "" -"There are some unknown filaments mapped to generic preset. Please update " -"Snapmaker Orca or restart Snapmaker Orca to check if there is an update to " -"system presets." -msgstr "" - -msgid "Note: Changing this will sync all other nozzles to the same diameter." -msgstr "" - -msgid "Don't show this again" -msgstr "" - -msgid "Left Nozzle" -msgstr "" - -msgid "Right Nozzle" -msgstr "" - -#, possible-boost-format -msgid "Do you want to save changes to \"%1%\"?" -msgstr "" - -#, possible-c-format, possible-boost-format -msgid "" -"Successfully unmounted. The device %s (%s) can now be safely removed from " -"the computer." -msgstr "" - -#, possible-c-format, possible-boost-format -msgid "Ejecting of device %s (%s) has failed." -msgstr "" - -msgid "Previous unsaved project detected, do you want to restore it?" -msgstr "" - -msgid "Restore" -msgstr "" - -msgid "" -"The current hot bed temperature is relatively high. The nozzle may be " -"clogged when printing this filament in a closed enclosure. Please open the " -"front door and/or remove the upper glass." -msgstr "" - -msgid "" -"The nozzle hardness required by the filament is higher than the default " -"nozzle hardness of the printer. Please replace the hardened nozzle or " -"filament, otherwise, the nozzle will be attrited or damaged." -msgstr "" - -msgid "" -"Enabling traditional timelapse photography may cause surface imperfections. " -"It is recommended to change to smooth mode." -msgstr "" - -msgid "Expand sidebar" -msgstr "" - -msgid "Collapse sidebar" -msgstr "" - -msgid "Tab" -msgstr "" - -#, possible-c-format, possible-boost-format -msgid "Loading file: %s" -msgstr "" - -msgid "The 3mf is not supported by Snapmaker Orca, load geometry data only." -msgstr "" - -msgid "Load 3mf" -msgstr "" - -msgid "" -"This project was created with an Snapmaker Orca and uses infill rotation " -"template settings that may not work properly with your current infill " -"pattern. This could result in weak support or print quality issues." -msgstr "" - -msgid "" -"Would you like Snapmaker Orca to automatically fix this by clearing the " -"rotation template settings?" -msgstr "" - -#, possible-c-format, possible-boost-format -msgid "" -"The 3mf's version %s is newer than %s's version %s, found following " -"unrecognized keys:" -msgstr "" - -msgid "You'd better upgrade your software.\n" -msgstr "" - -#, possible-c-format, possible-boost-format -msgid "" -"The 3mf's version %s is newer than %s's version %s, Suggest to upgrade your " -"software." +#, possible-c-format, possible-boost-format +msgid "" +"The 3mf's version %s is newer than %s's version %s, Suggest to upgrade your " +"software." msgstr "" msgid "Invalid values found in the 3mf:" @@ -6297,11 +5951,6 @@ msgstr "" msgid "Customized Preset" msgstr "" -msgid "" -"Printing by object with caution. This function may cause the print head to " -"collide with printed parts during switching." -msgstr "" - msgid "Name of components inside step file is not UTF8 format!" msgstr "" @@ -6389,33 +6038,6 @@ msgstr "" msgid "The selected object couldn't be split." msgstr "" -msgid "(unknown)" -msgstr "" - -#, possible-c-format, possible-boost-format -msgid "Note: Using a %s mm %s nozzle for %s is not recommended." -msgstr "" - -msgid "" -"Note: Filament may not adhere well to the smooth PEI plate on the first " -"layer. Apply glue before printing." -msgstr "" - -msgid "" -"Note: Filament may stick too strongly to the smooth PEI plate. Apply glue to " -"protect the plate and ease part removal." -msgstr "" - -msgid "" -"Note: Low adhesion to the graphic effect plate may cause failure. Use a " -"different filament instead." -msgstr "" - -msgid "" -"Mixed filaments contain incompatible material types. Please correct the " -"mixed filaments settings before slicing." -msgstr "" - msgid "Another export job is running." msgstr "" @@ -6471,15 +6093,6 @@ msgstr "" msgid "Please resolve the slicing errors and publish again." msgstr "" -#, possible-c-format, possible-boost-format -msgid "" -"Using %d physical filaments will create %d auto-generated gradients.\n" -"Do you want to create them now?" -msgstr "" - -msgid "Auto gradients" -msgstr "" - msgid "" "Network Plug-in is not detected. Network related features are unavailable." msgstr "" @@ -6489,9 +6102,6 @@ msgid "" "The loaded file contains G-code only, cannot enter the Prepare page." msgstr "" -msgid "Cannot enable both Variable Layer Height and Subdivide Mix Layer." -msgstr "" - msgid "You can keep the modified presets to the new project or discard them" msgstr "" @@ -6673,12 +6283,6 @@ msgid "" "Suggest to use auto-arrange to avoid collisions when printing." msgstr "" -msgid "This material combination may cause risks. Do you want to continue?" -msgstr "" - -msgid "Confirm slicing" -msgstr "" - msgid "Send G-code" msgstr "" @@ -6766,18 +6370,12 @@ msgstr "" msgid "Changing application language" msgstr "" -msgid "Changing the region will log out your account and restart.\n" +msgid "Changing the region will log out your account.\n" msgstr "" msgid "Region selection" msgstr "" -msgid "Change Region" -msgstr "" - -msgid "Switching the region requires application restart.\n" -msgstr "" - msgid "Second" msgstr "" @@ -6808,6 +6406,9 @@ msgstr "" msgid "Asia-Pacific" msgstr "" +msgid "China" +msgstr "" + msgid "Chinese Mainland" msgstr "" @@ -6823,6 +6424,27 @@ msgstr "" msgid "Login Region" msgstr "" +msgid "Stealth Mode" +msgstr "" + +msgid "" +"This stops the transmission of data to Bambu's cloud services. Users who " +"don't use BBL machines or use LAN mode only can safely turn on this function." +msgstr "" + +msgid "Enable network plugin" +msgstr "" + +msgid "Use legacy network plugin (Takes effect after restarting Orca)" +msgstr "" + +msgid "" +"Disable to use latest network plugin that supports new BambuLab firmwares." +msgstr "" + +msgid "Check for stable updates only" +msgstr "" + msgid "Metric" msgstr "" @@ -6876,44 +6498,23 @@ msgid "" "2D window center." msgstr "" -msgid "Use free camera" -msgstr "" - -msgid "If enabled, use free camera. If not enabled, use constrained camera." -msgstr "" - -msgid "Swap pan and rotate mouse buttons" -msgstr "" - -msgid "" -"If enabled, swaps the left and right mouse buttons pan and rotate functions." -msgstr "" - -msgid "Reverse mouse zoom" +msgid "Use free camera" msgstr "" -msgid "If enabled, reverses the direction of zoom with mouse wheel." +msgid "If enabled, use free camera. If not enabled, use constrained camera." msgstr "" -msgid "Allow high/low temperature filament mixing" +msgid "Swap pan and rotate mouse buttons" msgstr "" msgid "" -"If enabled, allows printing with both high-temperature and low-temperature " -"filaments simultaneously." +"If enabled, swaps the left and right mouse buttons pan and rotate functions." msgstr "" -msgid "" -"Mixing materials with significantly different printing temperatures may " -"result in:\n" -"· Extruder clogging\n" -"· Nozzle damage\n" -"· Layer adhesion issues\n" -"\n" -"Do you want to enable this feature?" +msgid "Reverse mouse zoom" msgstr "" -msgid "High and Low Temperature Material Mixing Risk" +msgid "If enabled, reverses the direction of zoom with mouse wheel." msgstr "" msgid "Orbit speed multiplier" @@ -6976,6 +6577,9 @@ msgstr "" msgid "Auto arrange plate after object cloning" msgstr "" +msgid "Network" +msgstr "" + msgid "Auto sync user presets (Printer/Filament/Process)" msgstr "" @@ -6997,22 +6601,19 @@ msgstr "" msgid "Associate .3mf files to Snapmaker Orca" msgstr "" -msgid "" -"If enabled, sets Snapmaker Orca as default application to open .3mf files" +msgid "If enabled, sets Snapmaker Orca as default application to open .3mf files" msgstr "" msgid "Associate .stl files to Snapmaker Orca" msgstr "" -msgid "" -"If enabled, sets Snapmaker Orca as default application to open .stl files" +msgid "If enabled, sets Snapmaker Orca as default application to open .stl files" msgstr "" msgid "Associate .step/.stp files to Snapmaker Orca" msgstr "" -msgid "" -"If enabled, sets Snapmaker Orca as default application to open .step files" +msgid "If enabled, sets Snapmaker Orca as default application to open .step files" msgstr "" msgid "Associate web links to Snapmaker Orca" @@ -7076,18 +6677,6 @@ msgstr "" msgid "Enable Dark mode" msgstr "" -msgid "Enable dark mode" -msgstr "" - -msgid "User Experience" -msgstr "" - -msgid "Join Customer Experience Improvement Program." -msgstr "" - -msgid "What data would be collected?" -msgstr "" - msgid "Develop mode" msgstr "" @@ -7208,9 +6797,6 @@ msgstr "" msgid "Edit preset" msgstr "" -msgid "Machine Filament" -msgstr "" - msgid "Project-inside presets" msgstr "" @@ -7392,9 +6978,6 @@ msgstr "" msgid "Bambu Textured PEI Plate" msgstr "" -msgid "Graphic Effect Plate" -msgstr "" - msgid "Send print job to" msgstr "" @@ -7807,8 +7390,8 @@ msgstr "" msgid "" "When recording timelapse without toolhead, it is recommended to add a " "\"Timelapse Wipe Tower\" \n" -"by right-click the empty position of build plate and choose \"Add Primitive" -"\"->\"Timelapse Wipe Tower\"." +"by right-click the empty position of build plate and choose \"Add " +"Primitive\"->\"Timelapse Wipe Tower\"." msgstr "" msgid "" @@ -7961,9 +7544,6 @@ msgstr "" msgid "Ooze prevention" msgstr "" -msgid "Color Mixing (Experimental)" -msgstr "" - msgid "Skirt" msgstr "" @@ -8021,6 +7601,9 @@ msgstr "" msgid "Print temperature" msgstr "" +msgid "Nozzle" +msgstr "" + msgid "Nozzle temperature when printing" msgstr "" @@ -8073,11 +7656,6 @@ msgid "" "the filament does not support printing on the Textured PEI Plate." msgstr "" -msgid "" -"Bed temperature when the Graphic Effect Plate is installed. A value of 0 " -"means the filament does not support printing on the Graphic Effect Plate." -msgstr "" - msgid "Volumetric speed limitation" msgstr "" @@ -8147,9 +7725,6 @@ msgstr "" msgid "Compatible process profiles" msgstr "" -msgid "Smooth PEI Plate" -msgstr "" - msgid "Printable space" msgstr "" @@ -8524,6 +8099,9 @@ msgstr "" msgid "No updates available." msgstr "" +msgid "The configuration is up to date." +msgstr "" + msgid "Obj file Import color" msgstr "" @@ -8555,17 +8133,13 @@ msgstr "" msgid "Add consumable extruder after existing extruders." msgstr "" -msgid "Reset mapped extruders." +msgid "Keep color" msgstr "" -msgid "" -"Warning: The count of newly added and \n" -"current extruders exceeds 16." +msgid "Add this cluster color as a new filament." msgstr "" -msgid "" -"Note: The color has been selected, you can choose OK \n" -"to continue or manually adjust it." +msgid "Reset mapped extruders." msgstr "" msgid "Cluster colors" @@ -8574,10 +8148,14 @@ msgstr "" msgid "Map Filament" msgstr "" -msgid "Keep color" +msgid "" +"Note: The color has been selected, you can choose OK \n" +"to continue or manually adjust it." msgstr "" -msgid "Add this cluster color as a new filament." +msgid "" +"Warning: The count of newly added and \n" +"current extruders exceeds 16." msgstr "" msgid "Ramming customization" @@ -8673,7 +8251,7 @@ msgstr "" msgid "" "Your system is missing H.264 codecs for GStreamer, which are required to " -"play video. (Try installing the gstreamer1.0-plugins-bad or gstreamer1.0-" +"play video. (Try installing the gstreamer1.0-plugins-bad or gstreamer1.0-" "libav packages, then restart Snapmaker Orca?)" msgstr "" @@ -8686,9 +8264,6 @@ msgstr "" msgid "Login" msgstr "" -msgid "Log out" -msgstr "" - msgid "The configuration package is changed in previous Config Guide" msgstr "" @@ -8928,8 +8503,7 @@ msgid "Network plug-in update" msgstr "" msgid "" -"Click OK to update the Network plug-in when Snapmaker Orca launches next " -"time." +"Click OK to update the Network plug-in when Snapmaker Orca launches next time." msgstr "" #, possible-c-format, possible-boost-format @@ -8942,9 +8516,6 @@ msgstr "" msgid "Skip this Version" msgstr "" -msgid "Check for stable updates only" -msgstr "" - msgid "Done" msgstr "" @@ -8988,11 +8559,11 @@ msgid "Connect the printer using IP and access code" msgstr "" msgid "" -"Step 1, please confirm Snapmaker Orca and your printer are in the same LAN." +"Step 1. Please confirm Snapmaker Orca and your printer are in the same LAN." msgstr "" msgid "" -"Step 2, if the IP and Access Code below are different from the actual values " +"Step 2. If the IP and Access Code below are different from the actual values " "on your printer, please correct them." msgstr "" @@ -9150,27 +8721,6 @@ msgstr "" msgid "Copying of file %1% to %2% failed: %3%" msgstr "" -#, possible-c-format, possible-boost-format -msgid "Flutter resource download failed: %s" -msgstr "" - -#, possible-c-format, possible-boost-format -msgid "Profiles resource download failed: %s" -msgstr "" - -#, possible-c-format, possible-boost-format -msgid "" -"request to server update web resource fail with body:%s,error:%s,status:%d" -msgstr "" - -#, possible-c-format, possible-boost-format -msgid "" -"request to server update preset resource fail with body:%s,error:%s,status:%d" -msgstr "" - -msgid "needed, but current version is " -msgstr "" - msgid "Need to check the unsaved changes before configuration updates." msgstr "" @@ -9180,30 +8730,6 @@ msgstr "" msgid " updated to " msgstr "" -msgid "Import Failed" -msgstr "" - -msgid "This web resouce could not be imported due to outdated versions." -msgstr "" - -msgid "Updating the web resources requires application restart." -msgstr "" - -msgid "Snapmaker Orca" -msgstr "" - -msgid "Update web resources" -msgstr "" - -msgid "Please choose a web resource package file:" -msgstr "" - -msgid "Please choose a system profile package file:" -msgstr "" - -msgid "The following profiles could not be imported due to outdated versions." -msgstr "" - msgid "Open G-code file:" msgstr "" @@ -9776,7 +9302,8 @@ msgstr "" msgid "Avoid crossing walls" msgstr "" -msgid "Detour and avoid to travel across wall which may cause blob on surface" +msgid "" +"Detour to avoid traveling across walls, which may cause blobs on the surface." msgstr "" msgid "Avoid crossing walls - Max detour length" @@ -9825,11 +9352,6 @@ msgid "" "filament does not support printing on the Textured PEI Plate." msgstr "" -msgid "" -"Bed temperature for layers except the initial one. A value of 0 means the " -"filament does not support printing on the Graphic Effect Plate." -msgstr "" - msgid "Initial layer" msgstr "" @@ -9866,11 +9388,6 @@ msgid "" "not support printing on the Textured PEI Plate." msgstr "" -msgid "" -"Bed temperature of the initial layer. A value of 0 means the filament does " -"not support printing on the Graphic Effect Plate." -msgstr "" - msgid "Bed types supported by the printer." msgstr "" @@ -10750,7 +10267,7 @@ msgid "" "This option sets the max point for the allowed bed mesh area. Due to the " "probe's XY offset, most printers are unable to probe the entire bed. To " "ensure the probe point does not go outside the bed area, the minimum and " -"maximum points of the bed mesh should be set appropriately. Snapmaker_Orca " +"maximum points of the bed mesh should be set appropriately. Snapmaker Orca " "ensures that adaptive_bed_mesh_min/adaptive_bed_mesh_max values do not " "exceed these min/max points. This information can usually be obtained from " "your printer manufacturer. The default setting is (99999, 99999), which " @@ -10940,18 +10457,6 @@ msgid "" "Right click to reset value to system default." msgstr "" -msgid "small thumb" -msgstr "" - -msgid "first small thumb" -msgstr "" - -msgid "big thumb" -msgstr "" - -msgid "first big thumb" -msgstr "" - msgid "Filament notes" msgstr "" @@ -10999,14 +10504,6 @@ msgid "" "typically 0. For statistics only." msgstr "" -msgid "Wait for the temperature when changing tools" -msgstr "" - -msgid "" -"It will use the M109 instead of M104 T[target] after changing tools if this " -"is set to true" -msgstr "" - msgid "" "Filament diameter is used to calculate extrusion in G-code, so it is " "important and should be accurate." @@ -11119,9 +10616,8 @@ msgid "" "After a tool change, the exact position of the newly loaded filament inside " "the nozzle may not be known, and the filament pressure is likely not yet " "stable. Before purging the print head into an infill or a sacrificial " -"object, Snapmaker Orca will always prime this amount of material into the " -"wipe tower to produce successive infill or sacrificial object extrusions " -"reliably." +"object, Snapmaker Orca will always prime this amount of material into the wipe " +"tower to produce successive infill or sacrificial object extrusions reliably." msgstr "" msgid "Speed of the last cooling move" @@ -11183,14 +10679,6 @@ msgid "" "Support material is commonly used to print supports and support interfaces." msgstr "" -msgid "Is high-temperature filament" -msgstr "" - -msgid "" -"Indicates whether this is a high-temperature filament that requires elevated " -"printing temperatures." -msgstr "" - msgid "Softening temperature" msgstr "" @@ -11422,8 +10910,8 @@ msgid "mm/s² or %" msgstr "" msgid "" -"Acceleration of sparse infill. If the value is expressed as a percentage (e." -"g. 100%), it will be calculated based on the default acceleration." +"Acceleration of sparse infill. If the value is expressed as a percentage " +"(e.g. 100%), it will be calculated based on the default acceleration." msgstr "" msgid "" @@ -11524,10 +11012,10 @@ msgstr "" msgid "" "Fan speed will be ramped up linearly from zero at layer " -"\"close_fan_the_first_x_layers\" to maximum at layer \"full_fan_speed_layer" -"\". \"full_fan_speed_layer\" will be ignored if lower than " -"\"close_fan_the_first_x_layers\", in which case the fan will be running at " -"maximum allowed speed at layer \"close_fan_the_first_x_layers\" + 1." +"\"close_fan_the_first_x_layers\" to maximum at layer " +"\"full_fan_speed_layer\". \"full_fan_speed_layer\" will be ignored if lower " +"than \"close_fan_the_first_x_layers\", in which case the fan will be running " +"at maximum allowed speed at layer \"close_fan_the_first_x_layers\" + 1." msgstr "" msgid "layer" @@ -11764,6 +11252,9 @@ msgstr "" msgid "Stainless steel" msgstr "" +msgid "Brass" +msgstr "" + msgid "Nozzle HRC" msgstr "" @@ -12609,229 +12100,11 @@ msgid "" msgstr "" msgid "" -"If you want to process the output G-code through custom scripts, just list " -"their absolute paths here. Separate multiple scripts with a semicolon. " -"Scripts will be passed the absolute path to the G-code file as the first " -"argument, and they can access the Snapmaker Orca config settings by reading " -"environment variables." -msgstr "" - -msgid "Dithering cadence height A" -msgstr "" - -msgid "" -"Layer height contribution of component A for dithering virtual filaments. " -"Set to 0 to use normal 1-layer A / 1-layer B alternation.\n" -"\n" -"Detailed mixed filament setting explanations will be published once the " -"project wiki is available." -msgstr "" - -msgid "Dithering cadence height B" -msgstr "" - -msgid "" -"Layer height contribution of component B for dithering virtual filaments. " -"Set to 0 to use normal 1-layer A / 1-layer B alternation.\n" -"\n" -"Detailed mixed filament setting explanations will be published once the " -"project wiki is available." -msgstr "" - -msgid "Height-weighted cadence" -msgstr "" - -msgid "" -"Enable height-weighted cadence for mixed filaments. Limitation: only one " -"height-weighted mixed color should be present at a given Z plane, because " -"independent per-color layer heights are not supported and the resulting " -"layer height applies to the whole plane. When disabled, layer-cycle cadence " -"is used.\n" -"\n" -"Detailed mixed filament setting explanations will be published once the " -"project wiki is available." -msgstr "" - -msgid "Local-Z lower height bound" -msgstr "" - -msgid "" -"Lower bound used when Local-Z mixed-filament dithering chooses per-color " -"sublayer heights.\n" -"\n" -"Smaller values let Local-Z use thinner sublayers for a color when needed.\n" -"\n" -"Detailed mixed filament setting explanations will be published once the " -"project wiki is available." -msgstr "" - -msgid "Local-Z upper height bound" -msgstr "" - -msgid "" -"Upper bound used when Local-Z mixed-filament dithering chooses per-color " -"sublayer heights.\n" -"\n" -"Larger values let Local-Z use thicker sublayers for a color when needed.\n" -"\n" -"Detailed mixed filament setting explanations will be published once the " -"project wiki is available." -msgstr "" - -msgid "Advanced dithering" -msgstr "" - -msgid "" -"Distribute mixed filament layer-cycle cadence using an advanced ordered " -"dithering pattern instead of a simple contiguous A-then-B run. This can " -"reduce visible striping for some hues.\n" -"\n" -"This is an even more experimental mode and the perceived color may differ " -"from normal dithering for the same filament pair and ratio.\n" -"\n" -"Detailed mixed filament setting explanations will be published once the " -"project wiki is available." -msgstr "" - -msgid "Pointillisme pixel size" -msgstr "" - -msgid "" -"Length of one pointillisme segment along an extrusion path for same-layer " -"pointillisme mode. Set to 0 to use automatic nozzle-based sizing.\n" -"\n" -"Warning: Same-layer pointillisme is extremely experimental and may produce " -"unusable results." -msgstr "" - -msgid "Pointillisme line gap" -msgstr "" - -msgid "" -"Optional non-extruded spacing between adjacent pointillisme segments. " -"Increase carefully to improve separation and print quality.\n" -"\n" -"Warning: Same-layer pointillisme is extremely experimental and may produce " -"unusable results." -msgstr "" - -msgid "Enable mixed filament bias" -msgstr "" - -msgid "" -"Show and apply the per-row mixed filament Bias control.\n" -"\n" -"When enabled, the selected filament in a mixed pair is recessed slightly so " -"the other component becomes more visible.\n" -"\n" -"Bias is ignored for grouped wall patterns, same-layer pointillisme, and " -"Local Z dithering." -msgstr "" - -msgid "Selective Expansion contraction" -msgstr "" - -msgid "" -"XY offset applied to mixed-filament painted regions before region " -"assignment.\n" -"\n" -"Positive values contract the mixed zone inward. Negative values expand it " -"outward.\n" -"\n" -"This applies to mixed filament usage in layer cadence, height cadence, same-" -"layer pointillisme, and local Z dithering." -msgstr "" - -msgid "Collapse same-color mixed regions" -msgstr "" - -msgid "" -"Merge ordinary mixed-filament painted regions into a single area when they " -"resolve to the same physical filament on a layer.\n" -"\n" -"This improves continuity for adjacent same-color areas.\n" -"\n" -"Subdivide Mix Layer disables this behavior, and gradient mixed regions also " -"bypass it because they use the Local-Z pipeline." -msgstr "" - -msgid "Mixed filament custom definitions" -msgstr "" - -msgid "" -"Serialized custom mixed filament rows.\n" -"\n" -"Detailed mixed filament setting explanations will be published once the " -"project wiki is available." -msgstr "" - -msgid "Dithering Z step size" -msgstr "" - -msgid "" -"Layer height used in Z zones painted with dithering (mixed virtual " -"filaments). Set to 0 to keep normal layer height in those zones.\n" -"\n" -"Detailed mixed filament setting explanations will be published once the " -"project wiki is available." -msgstr "" - -msgid "Subdivide Mix Layer" -msgstr "" - -msgid "Material" -msgstr "" - -msgid "" -"Enable \"Subdivide Mix Layer\" for mixing areas. Layer height will be " -"subdivided for better color mixing results." -msgstr "" - -msgid "Full domain" -msgstr "" - -msgid "" -"Experimental. Apply Local-Z thinning across whole mixed-color regions " -"instead of limiting the effect strictly to painted mixed masks.\n" -"\n" -"Only available when Subdivide Mix Layer is enabled." -msgstr "" - -msgid "Apply subdivision to infill" -msgstr "" - -msgid "" -"Experimental. When Subdivide Mix Layer is enabled, also apply the same " -"subdivision to infill inside mixed-color areas.\n" -"\n" -"This is enabled automatically with Subdivide Mix Layer. Turn it off to keep " -"infill on the normal layer height.\n" -"\n" -"It can improve internal color mixing, but may add toolchanges and affect " -"infill behavior." -msgstr "" - -msgid "Use direct multicolor Local-Z solver" -msgstr "" - -msgid "" -"Experimental. For mixed rows with 3 or more physical filaments, allocate " -"Local-Z sublayers directly across all components with carry-over error " -"between layers instead of collapsing them into pair cadence.\n" -"\n" -"This can reduce visible banding in multicolor Local-Z blends at the cost of " -"more toolchanges. It is ignored when explicit Local-Z A/B heights are set." -msgstr "" - -msgid "Use step size in painted zones only" -msgstr "" - -msgid "" -"When enabled, dithering Z step size is applied only where mixed filament is " -"painted. Unpainted zones keep their original layer height.\n" -"\n" -"Detailed mixed filament setting explanations will be published once the " -"project wiki is available." +"If you want to process the output G-code through custom scripts, just list " +"their absolute paths here. Separate multiple scripts with a semicolon. " +"Scripts will be passed the absolute path to the G-code file as the first " +"argument, and they can access the Snapmaker Orca config settings by reading " +"environment variables." msgstr "" msgid "Printer type" @@ -12934,9 +12207,6 @@ msgid "" "change." msgstr "" -msgid "Retraction Length (Toolchange)" -msgstr "" - msgid "Z-hop height" msgstr "" @@ -12974,9 +12244,6 @@ msgstr "" msgid "Spiral" msgstr "" -msgid "Z hop when moving to tower" -msgstr "" - msgid "Traveling angle" msgstr "" @@ -13201,8 +12468,8 @@ msgid "Role base wipe speed" msgstr "" msgid "" -"The wipe speed is determined by the speed of the current extrusion role. e." -"g. if a wipe action is executed immediately following an outer wall " +"The wipe speed is determined by the speed of the current extrusion role. " +"e.g. if a wipe action is executed immediately following an outer wall " "extrusion, the speed of the outer wall extrusion will be utilized for the " "wipe action." msgstr "" @@ -13284,6 +12551,9 @@ msgid "" "with them. To avoid this, increase the skirt distance value.\n" msgstr "" +msgid "Enabled" +msgstr "" + msgid "Skirt type" msgstr "" @@ -13426,14 +12696,6 @@ msgid "" "the tool in advance." msgstr "" -msgid "Preheat delta temperature" -msgstr "" - -msgid "" -"Allow user to set the Preheat temperature. If target temperature is 220 and " -"Preheat delta temperature is -30, then the preheat temperature will be 190" -msgstr "" - msgid "Preheat steps" msgstr "" @@ -13477,26 +12739,6 @@ msgstr "" msgid "Enable filament ramming" msgstr "" -msgid "Ramming line width ratio" -msgstr "" - -msgid "" -"This is used to decide the line width of wipe tower when ramming, ramming " -"line width = [this ratio] * extruder * 1.25" -msgstr "" - -msgid "Enable change pressure advance when wiping" -msgstr "" - -msgid "If it's set to false, the pressure advance value will not be changed." -msgstr "" - -msgid "Pressure advance value when ramming" -msgstr "" - -msgid "Set_Pressure_advance [this value] when ramming on wipe tower" -msgstr "" - msgid "No sparse layers (beta)" msgstr "" @@ -13916,8 +13158,8 @@ msgstr "" msgid "" "Enable this option for automated chamber temperature control. This option " -"activates the emitting of an M191 command before the \"machine_start_gcode" -"\"\n" +"activates the emitting of an M191 command before the " +"\"machine_start_gcode\"\n" " which sets the chamber temperature and waits until it is reached. In " "addition, it emits an M141 command at the end of the print to turn off the " "chamber heater, if present.\n" @@ -14081,24 +13323,6 @@ msgstr "" msgid "Width of the brim." msgstr "" -msgid "Brim chamfer" -msgstr "" - -msgid "" -"Enable gradual layer-by-layer reduction of the brim around the prime tower. " -"This creates a chamfered/tapered effect, reducing material usage while " -"maintaining first layer adhesion." -msgstr "" - -msgid "Max chamfer width" -msgstr "" - -msgid "" -"Maximum width of the chamfer zone measured from the tower perimeter. The " -"brim will reduce within this distance. Larger values create a more gradual " -"taper but take more layers to complete." -msgstr "" - msgid "Stabilization cone apex angle" msgstr "" @@ -14107,10 +13331,29 @@ msgid "" "Larger angle means wider base." msgstr "" -msgid "Maximum wipe tower print speed of out wall" +msgid "Maximum wipe tower print speed" msgstr "" -msgid "Maximum wipe tower print speed of out wall." +msgid "" +"The maximum print speed when purging in the wipe tower and printing the wipe " +"tower sparse layers. When purging, if the sparse infill speed or calculated " +"speed from the filament max volumetric speed is lower, the lowest will be " +"used instead.\n" +"\n" +"When printing the sparse layers, if the internal perimeter speed or " +"calculated speed from the filament max volumetric speed is lower, the lowest " +"will be used instead.\n" +"\n" +"Increasing this speed may affect the tower's stability as well as increase " +"the force with which the nozzle collides with any blobs that may have formed " +"on the wipe tower.\n" +"\n" +"Before increasing this parameter beyond the default of 90 mm/s, make sure " +"your printer can reliably bridge at the increased speeds and that ooze when " +"tool changing is well controlled.\n" +"\n" +"For the wipe tower external perimeters the internal perimeter speed is used " +"regardless of this setting." msgstr "" msgid "Wall type" @@ -14151,16 +13394,6 @@ msgid "" "use the one that is available (non-soluble would be preferred)." msgstr "" -msgid "Wall gap" -msgstr "" - -msgid "" -"Create small gaps in the wipe tower outer wall at tool change entry points. " -"The first extrusion path after a filament change will enter through the gap, " -"leaving the filament blob on the gap edge instead of on the outer wall " -"surface." -msgstr "" - msgid "Purging volumes - load/unload volumes" msgstr "" @@ -14210,18 +13443,6 @@ msgid "" "is adjusted automatically." msgstr "" -msgid "Local-Z mini wipe lines" -msgstr "" - -msgid "" -"Number of purge lines reserved for each runtime Local-Z wipe tower " -"toolchange. Higher values improve cleanup but increase tower depth. Only " -"used when Local-Z dithering and the prime tower are enabled." -msgstr "" - -msgid "lines" -msgstr "" - msgid "Idle temperature" msgstr "" @@ -14231,17 +13452,6 @@ msgid "" "0 to disable." msgstr "" -msgid "Tower ironing area" -msgstr "" - -msgid "" -"Ironing area for prime tower interface layer (where different materials " -"meet)." -msgstr "" - -msgid "mm²" -msgstr "" - msgid "X-Y hole compensation" msgstr "" @@ -14670,8 +13880,8 @@ msgid "Debug level" msgstr "" msgid "" -"Sets debug logging level. 0:fatal, 1:error, 2:warning, 3:info, 4:debug, 5:" -"trace\n" +"Sets debug logging level. 0:fatal, 1:error, 2:warning, 3:info, 4:debug, " +"5:trace\n" msgstr "" msgid "Enable timelapse for print" @@ -15476,6 +14686,9 @@ msgid "" "- Different filament brand and family (Brand = Bambu, Family = Basic, Matte)" msgstr "" +msgid "Pattern" +msgstr "" + msgid "Method" msgstr "" @@ -15486,6 +14699,12 @@ msgstr "" msgid "TPU is not supported for Flow Dynamics Auto-Calibration." msgstr "" +msgid "" +"Cannot print multiple filaments which have large difference of temperature " +"together. Otherwise, the extruder and nozzle may be blocked or damaged " +"during printing" +msgstr "" + msgid "Connecting to printer" msgstr "" @@ -15788,18 +15007,13 @@ msgstr "" msgid "Upload to Printer Host with the following filename:" msgstr "" -msgid "" -"Please do not include the special characters #, *, ;, \\, /, :, \", <, >, or " -"| in filenames." +msgid "Use forward slashes ( / ) as a directory separator if needed." msgstr "" msgid "Upload to storage" msgstr "" -#, possible-c-format, possible-boost-format -msgid "" -"The filename '%s' contains special characters (#, *, ;, \\, /, :, \", <, >, " -"or |) which may cause issues.Do you wish to continue?" +msgid "Switch to Device tab after upload." msgstr "" #, possible-c-format, possible-boost-format @@ -15846,12 +15060,6 @@ msgstr "" msgid "Error uploading to print host" msgstr "" -msgid "Use forward slashes ( / ) as a directory separator if needed." -msgstr "" - -msgid "Switch to Device tab after upload." -msgstr "" - msgid "" "The selected bed type does not match the file. Please confirm before " "starting the print." @@ -15920,9 +15128,6 @@ msgstr "" msgid "Export Log" msgstr "" -msgid "Clear Log" -msgstr "" - msgid "Snapmaker Orca Version:" msgstr "" @@ -15932,10 +15137,10 @@ msgstr "" msgid "DNS Server:" msgstr "" -msgid "Test Snapmaker Orca(GitHub)" +msgid "Test Snapmaker Orca (GitHub)" msgstr "" -msgid "Test Snapmaker Orca(GitHub):" +msgid "Test Snapmaker Orca (GitHub):" msgstr "" msgid "Test bing.com" @@ -15944,49 +15149,9 @@ msgstr "" msgid "Test bing.com:" msgstr "" -msgid "Test LAN Device" -msgstr "" - -msgid "Test LAN Device:" -msgstr "" - -msgid "Test Cloud Server" -msgstr "" - -msgid "Test Cloud Server:" -msgstr "" - -msgid "Test Login API" -msgstr "" - -msgid "Test Login API:" -msgstr "" - -msgid "Test Upload API" -msgstr "" - -msgid "Test Upload API:" -msgstr "" - msgid "Log Info" msgstr "" -msgid "Network test started" -msgstr "" - -msgid "" -"Please enter the LAN device IP address for testing (leave empty to skip):" -msgstr "" - -msgid "LAN Device Test - Sequence Mode" -msgstr "" - -msgid "Please enter the device IP address:" -msgstr "" - -msgid "LAN Device Test" -msgstr "" - msgid "Select filament preset" msgstr "" @@ -16082,8 +15247,8 @@ msgid "" msgstr "" msgid "" -"We would rename the presets as \"Vendor Type Serial @printer you selected" -"\".\n" +"We would rename the presets as \"Vendor Type Serial @printer you " +"selected\".\n" "To add preset for more printers, please go to printer selection" msgstr "" @@ -16421,6 +15586,9 @@ msgstr "" msgid "Edit Preset" msgstr "" +msgid "For more information, please check out Wiki" +msgstr "" + msgid "Collapse" msgstr "" @@ -16599,15 +15767,6 @@ msgstr "" msgid "Could not connect to Prusa Connect" msgstr "" -msgid "Connection to Moonraker works correctly." -msgstr "" - -msgid "Could not connect to Moonraker" -msgstr "" - -msgid "Note: Moonraker version at least 1.1.0 is required." -msgstr "" - msgid "Connection to Repetier is working correctly." msgstr "" @@ -16888,3 +16047,220 @@ msgstr "" msgid "Please select single object." msgstr "" + +#: resources/data/hints.ini: [hint:Precise wall] +msgid "Precise wall\nDid you know that turning on precise wall can improve precision and layer consistency?" +msgstr "" + +#: resources/data/hints.ini: [hint:Sandwich mode] +msgid "Sandwich mode\nDid you know that you can use sandwich mode (inner-outer-inner) to improve precision and layer consistency if your model doesn't have very steep overhangs?" +msgstr "" + +#: resources/data/hints.ini: [hint:Chamber temperature] +msgid "Chamber temperature\nDid you know that Snapmaker Orca supports chamber temperature?" +msgstr "" + +#: resources/data/hints.ini: [hint:Calibration] +msgid "Calibration\nDid you know that calibrating your printer can do wonders? Check out our beloved calibration solution in Snapmaker Orca." +msgstr "" + +#: resources/data/hints.ini: [hint:Auxiliary fan] +msgid "Auxiliary fan\nDid you know that Snapmaker Orca supports Auxiliary part cooling fan?" +msgstr "" + +#: resources/data/hints.ini: [hint:Air filtration] +msgid "Air filtration/Exhaust Fan\nDid you know that Snapmaker Orca can support Air filtration/Exhaust Fan?" +msgstr "" + +#: resources/data/hints.ini: [hint:G-code window] +msgid "G-code window\nYou can turn on/off the G-code window by pressing the C key." +msgstr "" + +#: resources/data/hints.ini: [hint:Switch workspaces] +msgid "Switch workspaces\nYou can switch between Prepare and Preview workspaces by pressing the Tab key." +msgstr "" + +#: resources/data/hints.ini: [hint:How to use keyboard shortcuts] +msgid "How to use keyboard shortcuts\nDid you know that Snapmaker Orca offers a wide range of keyboard shortcuts and 3D scene operations?" +msgstr "" + +#: resources/data/hints.ini: [hint:Reverse on odd] +msgid "Reverse on odd\nDid you know that Reverse on odd feature can significantly improve the surface quality of your overhangs?" +msgstr "" + +#: resources/data/hints.ini: [hint:Cut Tool] +msgid "Cut Tool\nDid you know that you can cut a model at any angle and position with the cutting tool?" +msgstr "" + +#: resources/data/hints.ini: [hint:Fix Model] +msgid "Fix Model\nDid you know that you can fix a corrupted 3D model to avoid a lot of slicing problems on the Windows system?" +msgstr "" + +#: resources/data/hints.ini: [hint:Timelapse] +msgid "Timelapse\nDid you know that you can generate a timelapse video during each print?" +msgstr "" + +#: resources/data/hints.ini: [hint:Auto-Arrange] +msgid "Auto-Arrange\nDid you know that you can auto-arrange all the objects in your project?" +msgstr "" + +#: resources/data/hints.ini: [hint:Auto-Orient] +msgid "Auto-Orient\nDid you know that you can rotate objects to an optimal orientation for printing with a simple click?" +msgstr "" + +#: resources/data/hints.ini: [hint:Lay on Face] +msgid "Lay on Face\nDid you know that you can quickly orient a model so that one of its faces sits on the print bed? Select the \"Place on face\" function or press the F key." +msgstr "" + +#: resources/data/hints.ini: [hint:Object List] +msgid "Object List\nDid you know that you can view all objects/parts in a list and change settings for each object/part?" +msgstr "" + +#: resources/data/hints.ini: [hint:Search Functionality] +msgid "Search Functionality\nDid you know that you use the Search tool to quickly find a specific Snapmaker Orca setting?" +msgstr "" + +#: resources/data/hints.ini: [hint:Simplify Model] +msgid "Simplify Model\nDid you know that you can reduce the number of triangles in a mesh using the Simplify mesh feature? Right-click the model and select Simplify model." +msgstr "" + +#: resources/data/hints.ini: [hint:Slicing Parameter Table] +msgid "Slicing Parameter Table\nDid you know that you can view all objects/parts on a table and change settings for each object/part?" +msgstr "" + +#: resources/data/hints.ini: [hint:Split to Objects/Parts] +msgid "Split to Objects/Parts\nDid you know that you can split a big object into small ones for easy colorizing or printing?" +msgstr "" + +#: resources/data/hints.ini: [hint:Subtract a Part] +msgid "Subtract a Part\nDid you know that you can subtract one mesh from another using the Negative part modifier? That way you can, for example, create easily resizable holes directly in Snapmaker Orca." +msgstr "" + +#: resources/data/hints.ini: [hint:STEP] +msgid "STEP\nDid you know that you can improve your print quality by slicing a STEP file instead of an STL?\nSnapmaker Orca supports slicing STEP files, providing smoother results than a lower resolution STL. Give it a try!" +msgstr "" + +#: resources/data/hints.ini: [hint:Z seam location] +msgid "Z seam location\nDid you know that you can customize the location of the Z seam, and even paint it on your print, to have it in a less visible location? This improves the overall look of your model. Check it out!" +msgstr "" + +#: resources/data/hints.ini: [hint:Fine-tuning for flow rate] +msgid "Fine-tuning for flow rate\nDid you know that flow rate can be fine-tuned for even better-looking prints? Depending on the material, you can improve the overall finish of the printed model by doing some fine-tuning." +msgstr "" + +#: resources/data/hints.ini: [hint:Split your prints into plates] +msgid "Split your prints into plates\nDid you know that you can split a model that has a lot of parts into individual plates ready to print? This will simplify the process of keeping track of all the parts." +msgstr "" + +#: resources/data/hints.ini: [hint:Speed up your print with Adaptive Layer Height] +msgid "Speed up your print with Adaptive Layer Height\nDid you know that you can print a model even faster, by using the Adaptive Layer Height option? Check it out!" +msgstr "" + +#: resources/data/hints.ini: [hint:Support painting] +msgid "Support painting\nDid you know that you can paint the location of your supports? This feature makes it easy to place the support material only on the sections of the model that actually need it." +msgstr "" + +#: resources/data/hints.ini: [hint:Different types of supports] +msgid "Different types of supports\nDid you know that you can choose from multiple types of supports? Tree supports work great for organic models, while saving filament and improving print speed. Check them out!" +msgstr "" + +#: resources/data/hints.ini: [hint:Printing Silk Filament] +msgid "Printing Silk Filament\nDid you know that Silk filament needs special consideration to print it successfully? Higher temperature and lower speed are always recommended for the best results." +msgstr "" + +#: resources/data/hints.ini: [hint:Brim for better adhesion] +msgid "Brim for better adhesion\nDid you know that when printed models have a small contact interface with the printing surface, it's recommended to use a brim?" +msgstr "" + +#: resources/data/hints.ini: [hint:Set parameters for multiple objects] +msgid "Set parameters for multiple objects\nDid you know that you can set slicing parameters for all selected objects at once?" +msgstr "" + +#: resources/data/hints.ini: [hint:Stack objects] +msgid "Stack objects\nDid you know that you can stack objects as a whole one?" +msgstr "" + +#: resources/data/hints.ini: [hint:Flush into support/objects/infill] +msgid "Flush into support/objects/infill\nDid you know that you can save wasted filament by flushing it into support/objects/infill during filament change?" +msgstr "" + +#: resources/data/hints.ini: [hint:Improve strength] +msgid "Improve strength\nDid you know that you can use more wall loops and higher sparse infill density to improve the strength of the model?" +msgstr "" + +#: resources/data/hints.ini: [hint:When do you need to print with the printer door opened] +msgid "When do you need to print with the printer door opened?\nDid you know that opening the printer door can reduce the probability of extruder/hotend clogging when printing lower temperature filament with a higher enclosure temperature? More info about this in the Wiki." +msgstr "" + +#: resources/data/hints.ini: [hint:Avoid warping] +msgid "Avoid warping\nDid you know that when printing materials that are prone to warping such as ABS, appropriately increasing the heatbed temperature can reduce the probability of warping?" +msgstr "" + +msgid "Ramming line width ratio" +msgstr "" + +msgid "Enable change pressure advance when wiping" +msgstr "" + +msgid "Pressure advance value when ramming" +msgstr "" + +msgid "Ramming line width ratio" +msgstr "" + +msgid "Enable change pressure advance when wiping" +msgstr "" + +msgid "Pressure advance value when ramming" +msgstr "" + +msgid "Z hop when moving to tower" +msgstr "" + +msgid "Retraction Length(Toolchange)" +msgstr "" + +msgid "Face and face assembly" +msgstr "" + +msgid "Point and point assembly" +msgstr "" + +msgid "Please do not include the special characters #, *, ;, \\, /, :, \", <, >, or | in filenames." +msgstr "" + +msgid "The filename '%s' contains special characters (#, *, ;, \\, /, :, \", <, >, or |) which may cause issues.Do you wish to continue?" +msgstr "" + +msgid "Connection has been disconnected and recovery attempt failed. Please reconnect." +msgstr "" + +msgid "User Experience" +msgstr "" + +msgid "Join Customer Experience Improvement Program." +msgstr "" + +msgid "What data would be collected?" +msgstr "" + +msgid "Note: The firmware version of SnapmakerU1 should be no less than" +msgstr "" + +msgid "Snapmaker Orca is forked from Orca Slicer by SoftFever." +msgstr "" + +msgid "Bambu Studio is forked from PrusaSlicer by Prusa Research, which is from Slic3r by Alessandro Ranellucci and the RepRap community. Orca Slicer incorporates a lot of features from SuperSlicer by @supermerill." +msgstr "" + +msgid "Snapmaker Orca integrates some features from the 3D printing open-source community, with specific attributions provided in the code comments." +msgstr " + +msgid "Snapmaker Orca is licensed under the GNU Affero General Public License, version 3." +msgstr "" + +msgid "OrcaSlicer is forked from Bambu Studio by Bambu Lab." +msgstr "" + +msgid "Incompatible filament types cannot be mixed. Please correct the selection." +msgstr "" From 3ab7fb7a0229f9c3f760ac009faeb2877b9cf3d1 Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Thu, 18 Jun 2026 11:27:52 +0800 Subject: [PATCH 132/135] fix: adjust purifier DELAY_OFF from 180 to 600 in U1 machine start gcode Extends the purifier delay-off time for chamber warming and weak cooling modes to ensure adequate post-print air filtration. Co-Authored-By: Claude Opus 4.7 --- .../profiles/Snapmaker/machine/Snapmaker U1 (0.2 nozzle).json | 2 +- .../profiles/Snapmaker/machine/Snapmaker U1 (0.4 nozzle).json | 2 +- .../profiles/Snapmaker/machine/Snapmaker U1 (0.6 nozzle).json | 2 +- .../profiles/Snapmaker/machine/Snapmaker U1 (0.8 nozzle).json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.2 nozzle).json b/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.2 nozzle).json index 978de4eadfc..7663d34be3d 100644 --- a/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.2 nozzle).json +++ b/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.2 nozzle).json @@ -48,7 +48,7 @@ "20", "12" ], - "machine_start_gcode": ";===== date: 20260128 =====================\\n\\nPRINT_START\\nDEFECT_DETECTION_START\\nSET_PRINT_STATS_INFO TOTAL_LAYER={total_layer_count} CURRENT_LAYER=0\\nTIMELAPSE_START\\nM140 S{bed_temperature_initial_layer_single}\\nM104 T{initial_extruder} S140\\nM204 S10000\\n{if chamber_cooling_mode==0}\\n; 保温\\nSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=180\\n{elsif chamber_cooling_mode==2}\\n; 强冷\\nSET_PURIFIER_MODE MODE=1 DESIRE_TEMP=42 ALARM_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=0\\n{else}\\n; 弱冷\\nSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=0 FAN_SPEED=0.6 DELAY_OFF=180\t\t\\n{endif}\\nG28 X Y\\nDEFECT_DETECT_NOODLE_FIRST\\n;===== 床面异物检测 ========\\nT{initial_extruder}\\nG90\\nDEFECT_DETECTION_DETECT_BED\\n;===== 取放头检测 =================\\nSM_PRINT_CHECK_SWITCH_EXTRUDER\\n\\n;===== 自动进料 & 挤出流量 & 预挤出 ======================\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=1 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=0\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=0\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=2 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=1\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=1\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=3 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=2\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=2\\nSM_PRINT_AUTO_FEED EXTRUDER=3\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=3\\nM104 S0 T0 A0\\nM104 S0 T1 A0\\nM104 S0 T2 A0\\nM104 S0 T3 A0\\nM104 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\n\\n;===== 粗回零 =================\\nT{initial_extruder}\\nM106 S255\\nM106 P2 S0\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\nROUGHLY_CLEAN_NOZZLE_WITH_DISCARD\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nG28 Z I140 J140\\n\\n;===== 检测钢板 =================\\nDETECT_BED_PLATE\\n\\n;===== 深度清洁喷嘴 =================\\nG90\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 S{nozzle_temperature[initial_extruder] - 50}\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_1\\nM104 S{nozzle_temperature[initial_extruder] - 90}\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_2\\n\\n;===== 精回零 =================\\nM106 S255\\nM109 S{nozzle_temperature[initial_extruder] - 90}\\nM190 S{bed_temperature_initial_layer_single}\\nM107 P2\\nG90\\nG0 Z5 F10000\\nWAIT_CHAMBER_TEMP TIMEOUT=180\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{else} \\nG28 Z \\n{endif} \\n\\n\\n;===== 热床调平 =================\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{else} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 \\n{endif} \\n\\n\\n;===== 画起始线 =================\\nG90\\nG1 Z1.5\\nG0 X85 Y1 Z2 F18000\\nM109 S{nozzle_temperature_initial_layer[initial_extruder]}\\nG1 Z0.2\\nM83\\nG1 X185 E15 F360\\nG1 Z1.5\\n\\nG90\\nM106 S0", + "machine_start_gcode": ";===== date: 20260128 =====================\\n\\nPRINT_START\\nDEFECT_DETECTION_START\\nSET_PRINT_STATS_INFO TOTAL_LAYER={total_layer_count} CURRENT_LAYER=0\\nTIMELAPSE_START\\nM140 S{bed_temperature_initial_layer_single}\\nM104 T{initial_extruder} S140\\nM204 S10000\\n{if chamber_cooling_mode==0}\\n; 保温\\nSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=600\\n{elsif chamber_cooling_mode==2}\\n; 强冷\\nSET_PURIFIER_MODE MODE=1 DESIRE_TEMP=42 ALARM_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=0\\n{else}\\n; 弱冷\\nSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=0 FAN_SPEED=0.6 DELAY_OFF=600\t\t\\n{endif}\\nG28 X Y\\nDEFECT_DETECT_NOODLE_FIRST\\n;===== 床面异物检测 ========\\nT{initial_extruder}\\nG90\\nDEFECT_DETECTION_DETECT_BED\\n;===== 取放头检测 =================\\nSM_PRINT_CHECK_SWITCH_EXTRUDER\\n\\n;===== 自动进料 & 挤出流量 & 预挤出 ======================\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=1 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=0\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=0\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=2 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=1\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=1\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=3 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=2\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=2\\nSM_PRINT_AUTO_FEED EXTRUDER=3\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=3\\nM104 S0 T0 A0\\nM104 S0 T1 A0\\nM104 S0 T2 A0\\nM104 S0 T3 A0\\nM104 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\n\\n;===== 粗回零 =================\\nT{initial_extruder}\\nM106 S255\\nM106 P2 S0\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\nROUGHLY_CLEAN_NOZZLE_WITH_DISCARD\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nG28 Z I140 J140\\n\\n;===== 检测钢板 =================\\nDETECT_BED_PLATE\\n\\n;===== 深度清洁喷嘴 =================\\nG90\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 S{nozzle_temperature[initial_extruder] - 50}\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_1\\nM104 S{nozzle_temperature[initial_extruder] - 90}\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_2\\n\\n;===== 精回零 =================\\nM106 S255\\nM109 S{nozzle_temperature[initial_extruder] - 90}\\nM190 S{bed_temperature_initial_layer_single}\\nM107 P2\\nG90\\nG0 Z5 F10000\\nWAIT_CHAMBER_TEMP TIMEOUT=180\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{else} \\nG28 Z \\n{endif} \\n\\n\\n;===== 热床调平 =================\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{else} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 \\n{endif} \\n\\n\\n;===== 画起始线 =================\\nG90\\nG1 Z1.5\\nG0 X85 Y1 Z2 F18000\\nM109 S{nozzle_temperature_initial_layer[initial_extruder]}\\nG1 Z0.2\\nM83\\nG1 X185 E15 F360\\nG1 Z1.5\\n\\nG90\\nM106 S0", "machine_tool_change_time": "5", "max_layer_height": [ "0.14", diff --git a/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.4 nozzle).json b/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.4 nozzle).json index 07d714dcf4d..44303996552 100644 --- a/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.4 nozzle).json +++ b/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.4 nozzle).json @@ -48,7 +48,7 @@ "20", "12" ], - "machine_start_gcode": ";===== date: 20260128 =====================\\n\\nPRINT_START\\nDEFECT_DETECTION_START\\nSET_PRINT_STATS_INFO TOTAL_LAYER={total_layer_count} CURRENT_LAYER=0\\nTIMELAPSE_START\\nM140 S{bed_temperature_initial_layer_single}\\nM104 T{initial_extruder} S140\\nM204 S10000\\n{if chamber_cooling_mode==0}\\n; 保温\\nSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=180\\n{elsif chamber_cooling_mode==2}\\n; 强冷\\nSET_PURIFIER_MODE MODE=1 DESIRE_TEMP=42 ALARM_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=0\\n{else}\\n; 弱冷\\nSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=0 FAN_SPEED=0.6 DELAY_OFF=180\t\t\\n{endif}\\nG28 X Y\\nDEFECT_DETECT_NOODLE_FIRST\\n;===== 床面异物检测 ========\\nT{initial_extruder}\\nG90\\nDEFECT_DETECTION_DETECT_BED\\n;===== 取放头检测 =================\\nSM_PRINT_CHECK_SWITCH_EXTRUDER\\n\\n;===== 自动进料 & 挤出流量 & 预挤出 ======================\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=1 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=0\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=0\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=2 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=1\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=1\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=3 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=2\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=2\\nSM_PRINT_AUTO_FEED EXTRUDER=3\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=3\\nM104 S0 T0 A0\\nM104 S0 T1 A0\\nM104 S0 T2 A0\\nM104 S0 T3 A0\\nM104 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\n\\n;===== 粗回零 =================\\nT{initial_extruder}\\nM106 S255\\nM106 P2 S0\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\nROUGHLY_CLEAN_NOZZLE_WITH_DISCARD\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nG28 Z I140 J140\\n\\n;===== 检测钢板 =================\\nDETECT_BED_PLATE\\n\\n;===== 深度清洁喷嘴 =================\\nG90\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 S{nozzle_temperature[initial_extruder] - 50}\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_1\\nM104 S{nozzle_temperature[initial_extruder] - 90}\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_2\\n\\n;===== 精回零 =================\\nM106 S255\\nM109 S{nozzle_temperature[initial_extruder] - 90}\\nM190 S{bed_temperature_initial_layer_single}\\nM107 P2\\nG90\\nG0 Z5 F10000\\nWAIT_CHAMBER_TEMP TIMEOUT=180\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{else} \\nG28 Z \\n{endif} \\n\\n\\n;===== 热床调平 =================\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{else} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 \\n{endif} \\n\\n\\n;===== 画起始线 =================\\nG90\\nG1 Z1.5\\nG0 X85 Y1 Z2 F18000\\nM109 S{nozzle_temperature_initial_layer[initial_extruder]}\\nG1 Z0.2\\nM83\\nG1 X185 E15 F360\\nG1 Z1.5\\n\\nG90\\nM106 S0", + "machine_start_gcode": ";===== date: 20260128 =====================\\n\\nPRINT_START\\nDEFECT_DETECTION_START\\nSET_PRINT_STATS_INFO TOTAL_LAYER={total_layer_count} CURRENT_LAYER=0\\nTIMELAPSE_START\\nM140 S{bed_temperature_initial_layer_single}\\nM104 T{initial_extruder} S140\\nM204 S10000\\n{if chamber_cooling_mode==0}\\n; 保温\\nSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=600\\n{elsif chamber_cooling_mode==2}\\n; 强冷\\nSET_PURIFIER_MODE MODE=1 DESIRE_TEMP=42 ALARM_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=0\\n{else}\\n; 弱冷\\nSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=0 FAN_SPEED=0.6 DELAY_OFF=600\t\t\\n{endif}\\nG28 X Y\\nDEFECT_DETECT_NOODLE_FIRST\\n;===== 床面异物检测 ========\\nT{initial_extruder}\\nG90\\nDEFECT_DETECTION_DETECT_BED\\n;===== 取放头检测 =================\\nSM_PRINT_CHECK_SWITCH_EXTRUDER\\n\\n;===== 自动进料 & 挤出流量 & 预挤出 ======================\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=1 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=0\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=0\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=2 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=1\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=1\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=3 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=2\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=2\\nSM_PRINT_AUTO_FEED EXTRUDER=3\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=3\\nM104 S0 T0 A0\\nM104 S0 T1 A0\\nM104 S0 T2 A0\\nM104 S0 T3 A0\\nM104 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\n\\n;===== 粗回零 =================\\nT{initial_extruder}\\nM106 S255\\nM106 P2 S0\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\nROUGHLY_CLEAN_NOZZLE_WITH_DISCARD\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nG28 Z I140 J140\\n\\n;===== 检测钢板 =================\\nDETECT_BED_PLATE\\n\\n;===== 深度清洁喷嘴 =================\\nG90\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 S{nozzle_temperature[initial_extruder] - 50}\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_1\\nM104 S{nozzle_temperature[initial_extruder] - 90}\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_2\\n\\n;===== 精回零 =================\\nM106 S255\\nM109 S{nozzle_temperature[initial_extruder] - 90}\\nM190 S{bed_temperature_initial_layer_single}\\nM107 P2\\nG90\\nG0 Z5 F10000\\nWAIT_CHAMBER_TEMP TIMEOUT=180\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{else} \\nG28 Z \\n{endif} \\n\\n\\n;===== 热床调平 =================\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{else} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 \\n{endif} \\n\\n\\n;===== 画起始线 =================\\nG90\\nG1 Z1.5\\nG0 X85 Y1 Z2 F18000\\nM109 S{nozzle_temperature_initial_layer[initial_extruder]}\\nG1 Z0.2\\nM83\\nG1 X185 E15 F360\\nG1 Z1.5\\n\\nG90\\nM106 S0", "machine_tool_change_time": "5", "max_layer_height": [ "0.32", diff --git a/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.6 nozzle).json b/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.6 nozzle).json index 92d5c57ec84..83c35a06631 100644 --- a/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.6 nozzle).json +++ b/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.6 nozzle).json @@ -48,7 +48,7 @@ "20", "12" ], - "machine_start_gcode": ";===== date: 20260128 =====================\\n\\nPRINT_START\\nDEFECT_DETECTION_START\\nSET_PRINT_STATS_INFO TOTAL_LAYER={total_layer_count} CURRENT_LAYER=0\\nTIMELAPSE_START\\nM140 S{bed_temperature_initial_layer_single}\\nM104 T{initial_extruder} S140\\nM204 S10000\\n{if chamber_cooling_mode==0}\\n; 保温\\nSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=180\\n{elsif chamber_cooling_mode==2}\\n; 强冷\\nSET_PURIFIER_MODE MODE=1 DESIRE_TEMP=42 ALARM_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=0\\n{else}\\n; 弱冷\\nSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=0 FAN_SPEED=0.6 DELAY_OFF=180\t\t\\n{endif}\\nG28 X Y\\nDEFECT_DETECT_NOODLE_FIRST\\n;===== 床面异物检测 ========\\nT{initial_extruder}\\nG90\\nDEFECT_DETECTION_DETECT_BED\\n;===== 取放头检测 =================\\nSM_PRINT_CHECK_SWITCH_EXTRUDER\\n\\n;===== 自动进料 & 挤出流量 & 预挤出 ======================\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=1 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=0\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=0\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=2 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=1\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=1\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=3 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=2\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=2\\nSM_PRINT_AUTO_FEED EXTRUDER=3\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=3\\nM104 S0 T0 A0\\nM104 S0 T1 A0\\nM104 S0 T2 A0\\nM104 S0 T3 A0\\nM104 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\n\\n;===== 粗回零 =================\\nT{initial_extruder}\\nM106 S255\\nM106 P2 S0\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\nROUGHLY_CLEAN_NOZZLE_WITH_DISCARD\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nG28 Z I140 J140\\n\\n;===== 检测钢板 =================\\nDETECT_BED_PLATE\\n\\n;===== 深度清洁喷嘴 =================\\nG90\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 S{nozzle_temperature[initial_extruder] - 50}\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_1\\nM104 S{nozzle_temperature[initial_extruder] - 90}\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_2\\n\\n;===== 精回零 =================\\nM106 S255\\nM109 S{nozzle_temperature[initial_extruder] - 90}\\nM190 S{bed_temperature_initial_layer_single}\\nM107 P2\\nG90\\nG0 Z5 F10000\\nWAIT_CHAMBER_TEMP TIMEOUT=180\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{else} \\nG28 Z \\n{endif} \\n\\n\\n;===== 热床调平 =================\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{else} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 \\n{endif} \\n\\n\\n;===== 画起始线 =================\\nG90\\nG1 Z1.5\\nG0 X85 Y1 Z2 F18000\\nM109 S{nozzle_temperature_initial_layer[initial_extruder]}\\nG1 Z0.2\\nM83\\nG1 X185 E15 F360\\nG1 Z1.5\\n\\nG90\\nM106 S0", + "machine_start_gcode": ";===== date: 20260128 =====================\\n\\nPRINT_START\\nDEFECT_DETECTION_START\\nSET_PRINT_STATS_INFO TOTAL_LAYER={total_layer_count} CURRENT_LAYER=0\\nTIMELAPSE_START\\nM140 S{bed_temperature_initial_layer_single}\\nM104 T{initial_extruder} S140\\nM204 S10000\\n{if chamber_cooling_mode==0}\\n; 保温\\nSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=600\\n{elsif chamber_cooling_mode==2}\\n; 强冷\\nSET_PURIFIER_MODE MODE=1 DESIRE_TEMP=42 ALARM_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=0\\n{else}\\n; 弱冷\\nSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=0 FAN_SPEED=0.6 DELAY_OFF=600\t\t\\n{endif}\\nG28 X Y\\nDEFECT_DETECT_NOODLE_FIRST\\n;===== 床面异物检测 ========\\nT{initial_extruder}\\nG90\\nDEFECT_DETECTION_DETECT_BED\\n;===== 取放头检测 =================\\nSM_PRINT_CHECK_SWITCH_EXTRUDER\\n\\n;===== 自动进料 & 挤出流量 & 预挤出 ======================\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=1 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=0\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=0\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=2 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=1\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=1\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=3 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=2\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=2\\nSM_PRINT_AUTO_FEED EXTRUDER=3\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=3\\nM104 S0 T0 A0\\nM104 S0 T1 A0\\nM104 S0 T2 A0\\nM104 S0 T3 A0\\nM104 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\n\\n;===== 粗回零 =================\\nT{initial_extruder}\\nM106 S255\\nM106 P2 S0\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\nROUGHLY_CLEAN_NOZZLE_WITH_DISCARD\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nG28 Z I140 J140\\n\\n;===== 检测钢板 =================\\nDETECT_BED_PLATE\\n\\n;===== 深度清洁喷嘴 =================\\nG90\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 S{nozzle_temperature[initial_extruder] - 50}\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_1\\nM104 S{nozzle_temperature[initial_extruder] - 90}\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_2\\n\\n;===== 精回零 =================\\nM106 S255\\nM109 S{nozzle_temperature[initial_extruder] - 90}\\nM190 S{bed_temperature_initial_layer_single}\\nM107 P2\\nG90\\nG0 Z5 F10000\\nWAIT_CHAMBER_TEMP TIMEOUT=180\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{else} \\nG28 Z \\n{endif} \\n\\n\\n;===== 热床调平 =================\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{else} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 \\n{endif} \\n\\n\\n;===== 画起始线 =================\\nG90\\nG1 Z1.5\\nG0 X85 Y1 Z2 F18000\\nM109 S{nozzle_temperature_initial_layer[initial_extruder]}\\nG1 Z0.2\\nM83\\nG1 X185 E15 F360\\nG1 Z1.5\\n\\nG90\\nM106 S0", "machine_tool_change_time": "5", "max_layer_height": [ "0.42", diff --git a/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.8 nozzle).json b/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.8 nozzle).json index b4d30a1b4cc..c6f05feec84 100644 --- a/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.8 nozzle).json +++ b/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.8 nozzle).json @@ -48,7 +48,7 @@ "20", "12" ], - "machine_start_gcode": ";===== date: 20260128 =====================\\n\\nPRINT_START\\nDEFECT_DETECTION_START\\nSET_PRINT_STATS_INFO TOTAL_LAYER={total_layer_count} CURRENT_LAYER=0\\nTIMELAPSE_START\\nM140 S{bed_temperature_initial_layer_single}\\nM104 T{initial_extruder} S140\\nM204 S10000\\n{if chamber_cooling_mode==0}\\n; 保温\\nSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=180\\n{elsif chamber_cooling_mode==2}\\n; 强冷\\nSET_PURIFIER_MODE MODE=1 DESIRE_TEMP=42 ALARM_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=0\\n{else}\\n; 弱冷\\nSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=0 FAN_SPEED=0.6 DELAY_OFF=180\t\t\\n{endif}\\nG28 X Y\\nDEFECT_DETECT_NOODLE_FIRST\\n;===== 床面异物检测 ========\\nT{initial_extruder}\\nG90\\nDEFECT_DETECTION_DETECT_BED\\n;===== 取放头检测 =================\\nSM_PRINT_CHECK_SWITCH_EXTRUDER\\n\\n;===== 自动进料 & 挤出流量 & 预挤出 ======================\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=1 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=0\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=0\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=2 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=1\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=1\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=3 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=2\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=2\\nSM_PRINT_AUTO_FEED EXTRUDER=3\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=3\\nM104 S0 T0 A0\\nM104 S0 T1 A0\\nM104 S0 T2 A0\\nM104 S0 T3 A0\\nM104 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\n\\n;===== 粗回零 =================\\nT{initial_extruder}\\nM106 S255\\nM106 P2 S0\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\nROUGHLY_CLEAN_NOZZLE_WITH_DISCARD\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nG28 Z I140 J140\\n\\n;===== 检测钢板 =================\\nDETECT_BED_PLATE\\n\\n;===== 深度清洁喷嘴 =================\\nG90\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 S{nozzle_temperature[initial_extruder] - 50}\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_1\\nM104 S{nozzle_temperature[initial_extruder] - 90}\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_2\\n\\n;===== 精回零 =================\\nM106 S255\\nM109 S{nozzle_temperature[initial_extruder] - 90}\\nM190 S{bed_temperature_initial_layer_single}\\nM107 P2\\nG90\\nG0 Z5 F10000\\nWAIT_CHAMBER_TEMP TIMEOUT=180\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{else} \\nG28 Z \\n{endif} \\n\\n\\n;===== 热床调平 =================\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{else} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 \\n{endif} \\n\\n\\n;===== 画起始线 =================\\nG90\\nG1 Z1.5\\nG0 X85 Y1 Z2 F18000\\nM109 S{nozzle_temperature_initial_layer[initial_extruder]}\\nG1 Z0.2\\nM83\\nG1 X185 E15 F360\\nG1 Z1.5\\n\\nG90\\nM106 S0", + "machine_start_gcode": ";===== date: 20260128 =====================\\n\\nPRINT_START\\nDEFECT_DETECTION_START\\nSET_PRINT_STATS_INFO TOTAL_LAYER={total_layer_count} CURRENT_LAYER=0\\nTIMELAPSE_START\\nM140 S{bed_temperature_initial_layer_single}\\nM104 T{initial_extruder} S140\\nM204 S10000\\n{if chamber_cooling_mode==0}\\n; 保温\\nSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=600\\n{elsif chamber_cooling_mode==2}\\n; 强冷\\nSET_PURIFIER_MODE MODE=1 DESIRE_TEMP=42 ALARM_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=0\\n{else}\\n; 弱冷\\nSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=0 FAN_SPEED=0.6 DELAY_OFF=600\t\t\\n{endif}\\nG28 X Y\\nDEFECT_DETECT_NOODLE_FIRST\\n;===== 床面异物检测 ========\\nT{initial_extruder}\\nG90\\nDEFECT_DETECTION_DETECT_BED\\n;===== 取放头检测 =================\\nSM_PRINT_CHECK_SWITCH_EXTRUDER\\n\\n;===== 自动进料 & 挤出流量 & 预挤出 ======================\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=1 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=0\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=0\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=2 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=1\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=1\\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=3 TEMP=140\\nSM_PRINT_AUTO_FEED EXTRUDER=2\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=2\\nSM_PRINT_AUTO_FEED EXTRUDER=3\\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=3\\nM104 S0 T0 A0\\nM104 S0 T1 A0\\nM104 S0 T2 A0\\nM104 S0 T3 A0\\nM104 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\n\\n;===== 粗回零 =================\\nT{initial_extruder}\\nM106 S255\\nM106 P2 S0\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\\nROUGHLY_CLEAN_NOZZLE_WITH_DISCARD\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nG28 Z I140 J140\\n\\n;===== 检测钢板 =================\\nDETECT_BED_PLATE\\n\\n;===== 深度清洁喷嘴 =================\\nG90\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nM109 S{nozzle_temperature[initial_extruder] - 50}\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_1\\nM104 S{nozzle_temperature[initial_extruder] - 90}\\nG0 Z5 F10000\\nMOVE_TO_DISCARD_FILAMENT_POSITION\\nROUGHLY_CLEAN_NOZZLE\\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\\nFINELY_CLEAN_NOZZLE_STAGE_2\\n\\n;===== 精回零 =================\\nM106 S255\\nM109 S{nozzle_temperature[initial_extruder] - 90}\\nM190 S{bed_temperature_initial_layer_single}\\nM107 P2\\nG90\\nG0 Z5 F10000\\nWAIT_CHAMBER_TEMP TIMEOUT=180\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nG28 Z Z_OFFSET -0.07 \\n{else} \\nG28 Z \\n{endif} \\n\\n\\n;===== 热床调平 =================\\n{if curr_bed_type==\\\"High Temp Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{elsif curr_bed_type==\\\"Graphic Effect Plate\\\"} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 Z_OFFSET=-0.07 \\n{else} \\nBED_MESH_CALIBRATE PROBE_COUNT=11,11 \\n{endif} \\n\\n\\n;===== 画起始线 =================\\nG90\\nG1 Z1.5\\nG0 X85 Y1 Z2 F18000\\nM109 S{nozzle_temperature_initial_layer[initial_extruder]}\\nG1 Z0.2\\nM83\\nG1 X185 E15 F360\\nG1 Z1.5\\n\\nG90\\nM106 S0", "machine_tool_change_time": "5", "max_layer_height": [ "0.56", From 3a30e68dd8317899de3d49203020a56a4eec5bfe Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Thu, 18 Jun 2026 17:32:07 +0800 Subject: [PATCH 133/135] fix: prevent auto-switching to Prepare tab on startup Defer the final tab selection in post_init() via CallAfter so that the pending EVT_GLVIEWTOOLBAR_3D event (posted by PAGE_CHANGED handler during GL init) is processed first. Also fix the Home page WebView URL from path=1 (Prepare) to path=0 (Home). Co-Authored-By: Claude Opus 4.7 --- src/slic3r/GUI/GUI_App.cpp | 14 ++++++++++---- src/slic3r/GUI/WebViewDialog.cpp | 2 +- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp index e59593251c8..2a012368e6f 100644 --- a/src/slic3r/GUI/GUI_App.cpp +++ b/src/slic3r/GUI/GUI_App.cpp @@ -1082,11 +1082,17 @@ void GUI_App::post_init() BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << "Found glcontext not ready, postpone the init"; } //#endif - if (is_editor()) - mainframe->select_tab(size_t(0)); - if (app_config->get("default_page") == "1") - mainframe->select_tab(size_t(1)); mainframe->Thaw(); + // Defer the final tab selection to after pending events are + // processed. During GL init, the PAGE_CHANGED handler posts + // EVT_GLVIEWTOOLBAR_3D which would undo a synchronous + // select_tab(0) and switch back to the Prepare tab. + CallAfter([this] { + if (is_editor() && app_config->get("default_page") != "1") + mainframe->select_tab(size_t(0)); + else if (app_config->get("default_page") == "1") + mainframe->select_tab(size_t(1)); + }); plater_->trigger_restore_project(1); BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ", end load_gl_resources"; } diff --git a/src/slic3r/GUI/WebViewDialog.cpp b/src/slic3r/GUI/WebViewDialog.cpp index 9cb30f5438e..b6bbe6786b5 100644 --- a/src/slic3r/GUI/WebViewDialog.cpp +++ b/src/slic3r/GUI/WebViewDialog.cpp @@ -37,7 +37,7 @@ namespace GUI { WebViewPanel::WebViewPanel(wxWindow *parent) : wxPanel(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize) { - wxString url = wxString::FromUTF8(LOCALHOST_URL + std::to_string(wxGetApp().get_page_http_port()) + "/web/flutter_web/index.html?path=1"); + wxString url = wxString::FromUTF8(LOCALHOST_URL + std::to_string(wxGetApp().get_page_http_port()) + "/web/flutter_web/index.html?path=0"); // wxString url = wxString::Format("file://%s/web/homepage/index.html?path=homepage.html", from_u8(resources_dir())); // wxString url = wxString("http://127.0.0.1:") + wxString(std::to_string(PAGE_HTTP_PORT)) + wxString("/web/flutter_web/index.html?path=1"); url = wxGetApp().get_international_url(url); From cfbe5576f7b13a21b7444a305cddefa2eb6f6a78 Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Thu, 18 Jun 2026 18:09:24 +0800 Subject: [PATCH 134/135] fix: add exception handling around config wizard, fix UpdateVersionDialog file:// URL - Wrap config_wizard_startup/run_wizard with try-catch to prevent crash from propagating to wxWidgets event loop and terminating the application - Fix UpdateVersionDialog release note URL: use fs::absolute() to ensure file:// URL has full path with drive letter on Windows Co-Authored-By: Claude Opus 4.7 --- src/slic3r/GUI/GUI_App.cpp | 50 +++++++++++++++++++++++-------- src/slic3r/GUI/ReleaseNote.cpp | 8 +++-- src/slic3r/GUI/WebGuideDialog.cpp | 4 +-- 3 files changed, 44 insertions(+), 18 deletions(-) diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp index 2a012368e6f..fc339ab8db3 100644 --- a/src/slic3r/GUI/GUI_App.cpp +++ b/src/slic3r/GUI/GUI_App.cpp @@ -1142,6 +1142,7 @@ void GUI_App::post_init() // Neither wxShowEvent nor wxWindowCreateEvent work reliably. if (this->preset_updater) { // G-Code Viewer does not initialize preset_updater. CallAfter([this] { + try { bool cw_showed = this->config_wizard_startup(); SSWCP_MqttAgent_Instance::m_dialog = new WebPresetDialog(this); @@ -1153,7 +1154,13 @@ void GUI_App::post_init() this->preset_updater->sync(http_url, language, network_ver, sys_preset ? preset_bundle : nullptr); this->preset_updater->sync_web_async(true); this->check_new_version_sf(false, false); - + } catch (const std::exception& e) { + BOOST_LOG_TRIVIAL(error) << "CallAfter config wizard exception: " << e.what(); + flush_logs(); + } catch (...) { + BOOST_LOG_TRIVIAL(error) << "CallAfter config wizard unknown exception"; + flush_logs(); + } }); } @@ -6846,7 +6853,16 @@ bool GUI_App::run_wizard(ConfigWizard::RunReason reason, ConfigWizard::StartPage start_page == ConfigWizard::SP_PRINTERS ? GuideFrame::BBL_MODELS_ONLY : GuideFrame::BBL_MODELS; wizard.SetStartPage(page); - bool res = wizard.run(); + bool res = false; + try { + res = wizard.run(); + } catch (const std::exception& e) { + BOOST_LOG_TRIVIAL(error) << "run_wizard exception: " << e.what(); + flush_logs(); + } catch (...) { + BOOST_LOG_TRIVIAL(error) << "run_wizard unknown exception"; + flush_logs(); + } if (res) { load_current_presets(); @@ -7095,20 +7111,28 @@ void GUI_App::user_login_notify(const json& res) bool GUI_App::config_wizard_startup() { auto isAgree = wxGetApp().app_config->get("app", PRIVACY_POLICY_FLAGS); - user_update_privacy_notify(isAgree == "true"); + user_update_privacy_notify(isAgree == "true"); BOOST_LOG_TRIVIAL(warning) << "config_wizard_startup changed the privacy policy with: " << (isAgree); - if (!m_app_conf_exists || preset_bundle->printers.only_default_printers()) { - BOOST_LOG_TRIVIAL(info) << "run wizard..."; - run_wizard(ConfigWizard::RR_DATA_EMPTY); - BOOST_LOG_TRIVIAL(info) << "finished run wizard"; + try { + if (!m_app_conf_exists || preset_bundle->printers.only_default_printers()) { + BOOST_LOG_TRIVIAL(info) << "run wizard..."; + run_wizard(ConfigWizard::RR_DATA_EMPTY); + BOOST_LOG_TRIVIAL(info) << "finished run wizard"; - return true; - } + return true; + } - if (isAgree.empty()) - { - run_wizard(ConfigWizard::RR_DATA_EMPTY); // Compatible with older versions - return true; + if (isAgree.empty()) + { + run_wizard(ConfigWizard::RR_DATA_EMPTY); // Compatible with older versions + return true; + } + } catch (const std::exception& e) { + BOOST_LOG_TRIVIAL(error) << "config_wizard_startup exception: " << e.what(); + flush_logs(); + } catch (...) { + BOOST_LOG_TRIVIAL(error) << "config_wizard_startup unknown exception"; + flush_logs(); } return false; diff --git a/src/slic3r/GUI/ReleaseNote.cpp b/src/slic3r/GUI/ReleaseNote.cpp index 5015d4dad2c..2c9cd1e480a 100644 --- a/src/slic3r/GUI/ReleaseNote.cpp +++ b/src/slic3r/GUI/ReleaseNote.cpp @@ -301,9 +301,10 @@ UpdateVersionDialog::UpdateVersionDialog(wxWindow *parent) fs::path ph(data_dir()); ph /= "resources/tooltip/releasenote.html"; if (!fs::exists(ph)) { - ph = resources_dir(); - ph /= "tooltip/releasenote.html"; + ph = fs::path(resources_dir()) / "tooltip/releasenote.html"; } + // Make absolute to get proper "C:/..." prefix for file:// URL + ph = fs::absolute(ph); auto url = ph.string(); std::replace(url.begin(), url.end(), '\\', '/'); url = "file:///" + url; @@ -459,7 +460,8 @@ void UpdateVersionDialog::OnError(wxWebViewEvent& event) case wxWEBVIEW_NAV_ERR_USER_CANCELLED: e = "wxWEBVIEW_NAV_ERR_USER_CANCELLED"; break; case wxWEBVIEW_NAV_ERR_OTHER: e = "wxWEBVIEW_NAV_ERR_OTHER"; break; } - BOOST_LOG_TRIVIAL(fatal) << __FUNCTION__<< boost::format(":UpdateVersionDialog error loading page %1% %2% %3% %4%") % event.GetURL() % event.GetTarget() %e %event.GetString(); + BOOST_LOG_TRIVIAL(error) << __FUNCTION__<< boost::format(":UpdateVersionDialog error loading page %1% %2% %3% %4%") % event.GetURL() % event.GetTarget() %e %event.GetString(); + flush_logs(); event.Skip(); } diff --git a/src/slic3r/GUI/WebGuideDialog.cpp b/src/slic3r/GUI/WebGuideDialog.cpp index 10b000e8135..39b88fc8299 100644 --- a/src/slic3r/GUI/WebGuideDialog.cpp +++ b/src/slic3r/GUI/WebGuideDialog.cpp @@ -610,8 +610,8 @@ void GuideFrame::OnError(wxWebViewEvent& event) case wxWEBVIEW_NAV_ERR_OTHER: e = "wxWEBVIEW_NAV_ERR_OTHER"; break; } - BOOST_LOG_TRIVIAL(fatal) << __FUNCTION__<< boost::format(":GuideFrame error loading page %1% %2% %3% %4%") % event.GetURL() % event.GetTarget() %e % event.GetString(); - + BOOST_LOG_TRIVIAL(error) << __FUNCTION__<< boost::format(":GuideFrame error loading page %1% %2% %3% %4%") % event.GetURL() % event.GetTarget() %e % event.GetString(); + flush_logs(); } void GuideFrame::OnScriptResponseMessage(wxCommandEvent &WXUNUSED(evt)) From b3912b8b3dba62f96ae8006b65679b52770db296 Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Thu, 18 Jun 2026 20:01:21 +0800 Subject: [PATCH 135/135] fix: remove flush_logs() from webview OnError callbacks to avoid per-event I/O overhead Co-Authored-By: Claude Opus 4.7 --- src/slic3r/GUI/ReleaseNote.cpp | 1 - src/slic3r/GUI/WebGuideDialog.cpp | 1 - 2 files changed, 2 deletions(-) diff --git a/src/slic3r/GUI/ReleaseNote.cpp b/src/slic3r/GUI/ReleaseNote.cpp index 2c9cd1e480a..c0b6fc6bd05 100644 --- a/src/slic3r/GUI/ReleaseNote.cpp +++ b/src/slic3r/GUI/ReleaseNote.cpp @@ -461,7 +461,6 @@ void UpdateVersionDialog::OnError(wxWebViewEvent& event) case wxWEBVIEW_NAV_ERR_OTHER: e = "wxWEBVIEW_NAV_ERR_OTHER"; break; } BOOST_LOG_TRIVIAL(error) << __FUNCTION__<< boost::format(":UpdateVersionDialog error loading page %1% %2% %3% %4%") % event.GetURL() % event.GetTarget() %e %event.GetString(); - flush_logs(); event.Skip(); } diff --git a/src/slic3r/GUI/WebGuideDialog.cpp b/src/slic3r/GUI/WebGuideDialog.cpp index 39b88fc8299..785f16ee6c0 100644 --- a/src/slic3r/GUI/WebGuideDialog.cpp +++ b/src/slic3r/GUI/WebGuideDialog.cpp @@ -611,7 +611,6 @@ void GuideFrame::OnError(wxWebViewEvent& event) } BOOST_LOG_TRIVIAL(error) << __FUNCTION__<< boost::format(":GuideFrame error loading page %1% %2% %3% %4%") % event.GetURL() % event.GetTarget() %e % event.GetString(); - flush_logs(); } void GuideFrame::OnScriptResponseMessage(wxCommandEvent &WXUNUSED(evt))