mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-10 17:21:10 +00:00
feat: iMEX temperature handling and pre-slice warnings
Temperature emission (GCode.cpp, PrintConfig.cpp, ConfigManipulation.cpp): - Set temperatures for all active tools in layer_change_gcode for iMEX parallel modes (primary + secondary carriages) - Fix filament temperature commands so Layer 1 temperatures only emit on the first layer; subsequent layers use normal layer-change temperatures - Remove stray temperature commands that fired outside the intended context - Consolidate iMEX temperature handling into the second-layer transition; clean up emission logic and naming throughout Pre-slice warning dialogs (PartPlate.cpp/hpp, Plater.cpp): - Collect per-plate IMEX warnings before slicing: multi-material conflict, bed temperature mismatch between carriages (>5 °C delta), and filament type incompatibility - Show a dismissible Yes/No dialog from both "Slice Plate" and "Slice All" actions; No returns to 3D view, Yes proceeds to slice - Refresh plate thumbnails after the panel switch so previously-generated thumbnails are not left black Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
5aa624b025
commit
ad0b42fd37
+76
-60
@@ -2391,6 +2391,46 @@ static BambuBedType to_bambu_bed_type(BedType type)
|
||||
return bambu_bed_type;
|
||||
}
|
||||
|
||||
// Orca IMEX: Returns the tool indices active in the current IMEX mode.
|
||||
// In copy/mirror parallel modes, secondary carriages (T1-T3) never receive tool-change
|
||||
// commands — the firmware mirrors the primary's moves — so they don't appear in
|
||||
// tool_ordering.all_extruders(). This helper parses imex_mode_active_tools to enumerate them.
|
||||
// Format: "0:P,1:C,2:M,3:M" — only the leading integer index is used.
|
||||
static std::vector<int> get_imex_active_tools(const Print& print)
|
||||
{
|
||||
std::vector<int> active_tools;
|
||||
|
||||
if (!print.config().is_imex.value || print.objects().empty())
|
||||
return active_tools;
|
||||
|
||||
const std::string& raw_mode = print.objects().front()->config().imex_parallel_mode.value;
|
||||
const std::string active_mode = raw_mode.empty() ? "primary" : raw_mode;
|
||||
|
||||
const auto* mode_names_opt = print.config().option<ConfigOptionStrings>("imex_mode_names");
|
||||
const auto* tools_opt = print.config().option<ConfigOptionStrings>("imex_mode_active_tools");
|
||||
|
||||
if (!mode_names_opt || !tools_opt || mode_names_opt->values.empty() || tools_opt->values.empty())
|
||||
return active_tools;
|
||||
|
||||
for (size_t i = 0; i < mode_names_opt->values.size(); ++i) {
|
||||
if (i >= tools_opt->values.size() || mode_names_opt->values[i] != active_mode)
|
||||
continue;
|
||||
std::istringstream ss(tools_opt->values[i]);
|
||||
std::string token;
|
||||
while (std::getline(ss, token, ',')) {
|
||||
token.erase(std::remove_if(token.begin(), token.end(), ::isspace), token.end());
|
||||
if (token.empty()) continue;
|
||||
try {
|
||||
int idx = std::stoi(token); // leading integer before optional ':role' suffix
|
||||
if (idx >= 0 && idx < (int)MAXIMUM_EXTRUDER_NUMBER)
|
||||
active_tools.push_back(idx);
|
||||
} catch (...) {}
|
||||
}
|
||||
break;
|
||||
}
|
||||
return active_tools;
|
||||
}
|
||||
|
||||
void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGeneratorCallback thumbnail_cb)
|
||||
{
|
||||
PROFILE_FUNC();
|
||||
@@ -2825,56 +2865,13 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato
|
||||
for (unsigned int extruder : tool_ordering.all_extruders())
|
||||
is_extruder_used[extruder] = true;
|
||||
|
||||
// Orca iMEX: Also mark secondary carriages as used in parallel modes
|
||||
// so is_extruder_used[N] is true for all physical tools in the active mode
|
||||
if (print.config().is_imex.value && !print.objects().empty()) {
|
||||
// Get the active mode for the first plate (all plates should have same mode in a single print)
|
||||
// Assumes all objects share the same mode — validated upstream by the UI
|
||||
const std::string& raw_mode = print.objects().front()->config().imex_parallel_mode.value;
|
||||
std::string active_mode = raw_mode.empty() ? "primary" : raw_mode;
|
||||
|
||||
// Look up the mode's active tools from printer config
|
||||
const auto* mode_names_opt = print.config().option<ConfigOptionStrings>("imex_mode_names");
|
||||
const auto* tools_opt = print.config().option<ConfigOptionStrings>("imex_mode_active_tools");
|
||||
|
||||
if (mode_names_opt && tools_opt && !mode_names_opt->values.empty() && !tools_opt->values.empty()) {
|
||||
// Linear scan to find the matching mode (fine for small fixed sets of modes)
|
||||
for (size_t i = 0; i < mode_names_opt->values.size(); ++i) {
|
||||
if (i < tools_opt->values.size() && mode_names_opt->values[i] == active_mode) {
|
||||
const std::string& active_tools_str = tools_opt->values[i];
|
||||
// Skip empty tool strings (no active tools in this mode)
|
||||
if (active_tools_str.empty()) continue;
|
||||
|
||||
// Parse "idx:P,idx:C,idx:M" format to extract tool indices
|
||||
std::istringstream ss(active_tools_str);
|
||||
std::string token;
|
||||
while (std::getline(ss, token, ',')) {
|
||||
token.erase(std::remove_if(token.begin(), token.end(), ::isspace), token.end());
|
||||
if (token.empty()) continue;
|
||||
try {
|
||||
auto colon = token.find(':');
|
||||
int idx;
|
||||
if (colon != std::string::npos) {
|
||||
idx = std::stoi(token.substr(0, colon));
|
||||
} else {
|
||||
idx = std::stoi(token);
|
||||
}
|
||||
// Mark this tool as used (idx is 0-based tool index)
|
||||
// idx < 0 would require a leading '-' in the string, which would fail stoi first
|
||||
if (idx >= 0 && idx < (int)is_extruder_used.size()) {
|
||||
is_extruder_used[idx] = true;
|
||||
}
|
||||
} catch (const std::invalid_argument&) {
|
||||
// not a valid integer token, skip
|
||||
} catch (const std::out_of_range&) {
|
||||
// index out of integer range, skip
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Orca IMEX: Also mark secondary carriages as used in parallel modes
|
||||
// so is_extruder_used[N] is true for all physical tools in the active mode.
|
||||
// Secondary carriages never appear in tool_ordering.all_extruders() because
|
||||
// the firmware duplicates the primary's moves — no tool-change commands are emitted.
|
||||
for (int tool_idx : get_imex_active_tools(print))
|
||||
if (tool_idx < (int)is_extruder_used.size())
|
||||
is_extruder_used[tool_idx] = true;
|
||||
|
||||
this->placeholder_parser().set("is_extruder_used", new ConfigOptionBools(is_extruder_used));
|
||||
|
||||
@@ -4409,6 +4406,7 @@ std::string GCode::generate_skirt(const Print &print,
|
||||
// In non-sequential mode, process_layer is called per each print_z height with all object and support layers accumulated.
|
||||
// For multi-material prints, this routine minimizes extruder switches by gathering extruder specific extrusion paths
|
||||
// and performing the extruder specific extrusions together.
|
||||
|
||||
LayerResult GCode::process_layer(
|
||||
const Print &print,
|
||||
// Set of object & print layers of the same PrintObject and with the same print_z.
|
||||
@@ -4559,6 +4557,7 @@ LayerResult GCode::process_layer(
|
||||
+ "\n";
|
||||
config.set_key_value("max_layer_z", new ConfigOptionFloat(m_max_layer_z));
|
||||
}
|
||||
|
||||
//BBS: set layer time fan speed after layer change gcode
|
||||
gcode += ";_SET_FAN_SPEED_CHANGING_LAYER\n";
|
||||
|
||||
@@ -4675,16 +4674,31 @@ LayerResult GCode::process_layer(
|
||||
gcode += m_writer.set_jerk_xy(m_config.default_jerk.value);
|
||||
}
|
||||
|
||||
// Transition from 1st to 2nd layer. Adjust nozzle temperatures as prescribed by the nozzle dependent
|
||||
// nozzle_temperature_initial_layer vs. temperature settings.
|
||||
for (const Extruder& extruder : m_writer.extruders()) {
|
||||
if ((print.config().single_extruder_multi_material.value || m_ooze_prevention.enable) &&
|
||||
extruder.id() != m_writer.filament()->id())
|
||||
// In single extruder multi material mode, set the temperature for the current extruder only.
|
||||
continue;
|
||||
int temperature = print.config().nozzle_temperature.get_at(extruder.id());
|
||||
if (temperature > 0 && temperature != print.config().nozzle_temperature_initial_layer.get_at(extruder.id()))
|
||||
gcode += m_writer.set_temperature(temperature, false, extruder.id());
|
||||
// Transition from 1st to 2nd layer: set non-initial-layer nozzle temperatures.
|
||||
// IMEX parallel modes: secondary carriages (T1-T3) never receive tool-change commands,
|
||||
// so they're not in m_writer.extruders() and multiple_extruders==false (max id==0).
|
||||
// Use the static set_temperature path for IMEX so the T index is always emitted.
|
||||
if (print.config().is_imex.value) {
|
||||
// All active tools need explicit temps — none receive tool-change commands,
|
||||
// so we can't rely on the condition used for non-IMEX (temp != initial_layer_temp).
|
||||
// A tool whose initial and regular temps are the same still needs to be set here.
|
||||
const int num_nozzles = (int)print.config().nozzle_temperature.values.size();
|
||||
for (int tool_idx : get_imex_active_tools(print)) {
|
||||
if (tool_idx >= num_nozzles) continue;
|
||||
int temperature = print.config().nozzle_temperature.values[tool_idx];
|
||||
if (temperature > 0)
|
||||
gcode += GCodeWriter::set_temperature(temperature, m_writer.config.gcode_flavor, false, tool_idx, "set IMEX tool temperature");
|
||||
}
|
||||
} else {
|
||||
for (const Extruder& extruder : m_writer.extruders()) {
|
||||
if ((print.config().single_extruder_multi_material.value || m_ooze_prevention.enable) &&
|
||||
extruder.id() != m_writer.filament()->id())
|
||||
// In single extruder multi material mode, set the temperature for the current extruder only.
|
||||
continue;
|
||||
int temperature = print.config().nozzle_temperature.get_at(extruder.id());
|
||||
if (temperature > 0 && temperature != print.config().nozzle_temperature_initial_layer.get_at(extruder.id()))
|
||||
gcode += m_writer.set_temperature(temperature, false, extruder.id());
|
||||
}
|
||||
}
|
||||
|
||||
// BBS
|
||||
@@ -7534,6 +7548,7 @@ std::string GCode::set_extruder(unsigned int new_filament_id, double print_z, bo
|
||||
gcode += this->placeholder_parser_process("filament_start_gcode", filament_start_gcode, new_filament_id, &config);
|
||||
check_add_eol(gcode);
|
||||
}
|
||||
|
||||
if (m_config.enable_pressure_advance.get_at(new_filament_id)) {
|
||||
gcode += m_writer.set_pressure_advance(m_config.pressure_advance.get_at(new_filament_id));
|
||||
// Orca: Adaptive PA
|
||||
@@ -7828,6 +7843,7 @@ std::string GCode::set_extruder(unsigned int new_filament_id, double print_z, bo
|
||||
}
|
||||
check_add_eol(gcode);
|
||||
}
|
||||
|
||||
// Set the new extruder to the operating temperature.
|
||||
if (m_ooze_prevention.enable)
|
||||
gcode += m_ooze_prevention.post_toolchange(*this);
|
||||
|
||||
@@ -10511,16 +10511,16 @@ OtherSlicingStatesConfigDef::OtherSlicingStatesConfigDef()
|
||||
new_def("in_head_wrap_detect_zone", coBool, "In head wrap detect zone", "Indicates if the first layer overlaps with the head wrap zone.");
|
||||
|
||||
def = this->add("imex_mode", coString);
|
||||
def->label = L("iMEX active mode");
|
||||
def->tooltip = L("Name of the active iMEX parallel print mode for this plate (e.g. 'primary', 'mirror', 'copy'). Empty string if iMEX is not enabled.");
|
||||
def->label = L("IMEX active mode");
|
||||
def->tooltip = L("Name of the active IMEX parallel print mode for this plate (e.g. 'primary', 'mirror', 'copy'). Empty string if IMEX is not enabled.");
|
||||
|
||||
def = this->add("imex_mode_index", coInt);
|
||||
def->label = L("iMEX active mode index");
|
||||
def->tooltip = L("Zero-based index of the active iMEX parallel print mode within imex_mode_names.");
|
||||
def->label = L("IMEX active mode index");
|
||||
def->tooltip = L("Zero-based index of the active IMEX parallel print mode within imex_mode_names.");
|
||||
|
||||
def = this->add("imex_mode_gcode", coString);
|
||||
def->label = L("iMEX active mode G-code");
|
||||
def->tooltip = L("The raw mode G-code template for the active iMEX parallel print mode, after placeholder evaluation. Globals defined here flow into machine_start_gcode.");
|
||||
def->label = L("IMEX active mode G-code");
|
||||
def->tooltip = L("The raw mode G-code template for the active IMEX parallel print mode, after placeholder evaluation. Globals defined here flow into machine_start_gcode.");
|
||||
}
|
||||
|
||||
PrintStatisticsConfigDef::PrintStatisticsConfigDef()
|
||||
|
||||
@@ -793,6 +793,9 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, co
|
||||
bool has_ironing = (config->opt_enum<IroningType>("ironing_type") != IroningType::NoIroning);
|
||||
for (auto el : { "ironing_pattern", "ironing_flow", "ironing_spacing", "ironing_angle", "ironing_inset", "ironing_angle_fixed" })
|
||||
toggle_line(el, has_ironing);
|
||||
bool has_rectilinear_ironing = (config->opt_enum<InfillPattern>("ironing_pattern") == InfillPattern::ipRectilinear);
|
||||
for (auto el : {"ironing_angle", "ironing_angle_fixed"})
|
||||
toggle_field(el, has_ironing && has_rectilinear_ironing);
|
||||
|
||||
toggle_line("ironing_speed", has_ironing || has_support_ironing);
|
||||
|
||||
|
||||
@@ -925,7 +925,7 @@ bool PartPlate::has_imex_placement_violations()
|
||||
|
||||
bool PartPlate::has_imex_multimaterial_conflict() const
|
||||
{
|
||||
// Condition 1: iMEX is active and the plate mode is non-primary
|
||||
// Condition 1: IMEX is active and the plate mode is non-primary
|
||||
auto* pb = wxGetApp().preset_bundle;
|
||||
if (!pb) return false;
|
||||
auto* is_imex_opt = pb->printers.get_edited_preset().config.option<ConfigOptionBool>("is_imex");
|
||||
@@ -1254,8 +1254,8 @@ void PartPlate::calc_vertex_for_icons(int index, PickingModel &model)
|
||||
init_raycaster_from_model(model);
|
||||
}
|
||||
|
||||
// Positions the iMEX multi-material warning badge as a small overlay at the bottom-right
|
||||
// corner of the iMEX mode icon. The icon slot index matches the one used in calc_vertex_for_icons.
|
||||
// Positions the IMEX multi-material warning badge as a small overlay at the bottom-right
|
||||
// corner of the IMEX mode icon. The icon slot index matches the one used in calc_vertex_for_icons.
|
||||
void PartPlate::calc_vertex_for_imex_warn_badge(int imex_icon_index, GLModel &model)
|
||||
{
|
||||
model.reset();
|
||||
@@ -1268,7 +1268,7 @@ void PartPlate::calc_vertex_for_imex_warn_badge(int imex_icon_index, GLModel &mo
|
||||
float gap_y = PARTPLATE_ICON_GAP_Y * factor;
|
||||
float gap_top = PARTPLATE_ICON_GAP_TOP * factor;
|
||||
|
||||
// Centre of the iMEX mode icon (top-left corner = p after offset)
|
||||
// Centre of the IMEX mode icon (top-left corner = p after offset)
|
||||
p += Vec2d(gap_left, -1 * (imex_icon_index * (size + gap_y) + gap_top));
|
||||
|
||||
// Badge is half the icon size, anchored to the bottom-right corner of the icon slot
|
||||
@@ -1282,7 +1282,7 @@ void PartPlate::calc_vertex_for_imex_warn_badge(int imex_icon_index, GLModel &mo
|
||||
poly.contour.append({ scale_(bp(0)) , scale_(bp(1) + badge)});
|
||||
|
||||
if (!init_model_from_poly(model, poly, GROUND_Z + 0.01f))
|
||||
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << "Unable to generate geometry for iMEX warn badge\n";
|
||||
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << "Unable to generate geometry for IMEX warn badge\n";
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -1796,11 +1796,11 @@ void PartPlate::render_icons(bool bottom, bool only_name, int hover_id)
|
||||
} else {
|
||||
render_icon_texture(m_imex_mode_icon.model, m_partplate_list->m_imex_mode_texture);
|
||||
}
|
||||
// Warning badge: iMEX parallel mode active alongside multi-material objects
|
||||
// Warning badge: IMEX parallel mode active alongside multi-material objects
|
||||
if (has_imex_multimaterial_conflict()) {
|
||||
render_icon_texture(m_imex_warn_icon, m_partplate_list->m_imex_warn_texture);
|
||||
if (hover_id == (int)PLATE_IMEX_MODE_ID)
|
||||
show_tooltip(_u8L("Warning: this plate uses a parallel iMEX mode with multi-material objects. Proceed with caution — verify your G-code handles this combination correctly."));
|
||||
show_tooltip(_u8L("Warning: this plate uses a parallel IMEX mode with multi-material objects. Proceed with caution — verify your G-code handles this combination correctly."));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4682,11 +4682,11 @@ void PartPlateList::generate_icon_textures()
|
||||
}
|
||||
}
|
||||
|
||||
// iMEX multi-material conflict warning badge
|
||||
// IMEX multi-material conflict warning badge
|
||||
{
|
||||
file_name = path + "obj_warning.svg";
|
||||
if (!m_imex_warn_texture.load_from_svg_file(file_name, true, false, false, icon_size)) {
|
||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(":load file %1% failed (iMEX warn badge)") % file_name;
|
||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(":load file %1% failed (IMEX warn badge)") % file_name;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -156,7 +156,7 @@ private:
|
||||
PickingModel m_plate_name_edit_icon;
|
||||
PickingModel m_move_front_icon;
|
||||
PickingModel m_imex_mode_icon;
|
||||
GLModel m_imex_warn_icon; // warning badge: iMEX parallel mode + multi-material conflict
|
||||
GLModel m_imex_warn_icon; // warning badge: IMEX parallel mode + multi-material conflict
|
||||
GLModel m_plate_idx_icon;
|
||||
GLTexture m_texture;
|
||||
|
||||
@@ -456,7 +456,7 @@ public:
|
||||
}
|
||||
// Returns true if any instance on this plate overlaps an IDEX/IQEX secondary or collision zone.
|
||||
bool has_imex_placement_violations();
|
||||
// Returns true when iMEX parallel mode is active (non-primary) AND the plate
|
||||
// Returns true when IMEX parallel mode is active (non-primary) AND the plate
|
||||
// has objects assigned to more than one filament — a combination that warrants caution.
|
||||
bool has_imex_multimaterial_conflict() const;
|
||||
// Returns the primary-zone bounding box in mm when an IDEX/IQEX parallel mode is active.
|
||||
@@ -642,7 +642,7 @@ class PartPlateList : public ObjectBase
|
||||
GLTexture m_plate_name_edit_hovered_texture;
|
||||
GLTexture m_imex_mode_texture;
|
||||
GLTexture m_imex_mode_hovered_texture;
|
||||
GLTexture m_imex_warn_texture; // warning badge for iMEX + multi-material conflict
|
||||
GLTexture m_imex_warn_texture; // warning badge for IMEX + multi-material conflict
|
||||
GLTexture m_idx_textures[MAX_PLATE_COUNT];
|
||||
// set render option
|
||||
bool render_bedtype_logo = true;
|
||||
|
||||
+150
-40
@@ -6,6 +6,7 @@
|
||||
#include <algorithm>
|
||||
#include <numeric>
|
||||
#include <limits>
|
||||
#include <sstream>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <regex>
|
||||
@@ -9778,25 +9779,131 @@ void Plater::priv::on_action_open_project(SimpleEvent&)
|
||||
}
|
||||
|
||||
//BBS: GUI refactor: slice plate
|
||||
// Orca IMEX: Collects all IMEX-related warnings for a plate before slicing.
|
||||
// Returns one formatted bullet string per issue. Empty means no concerns.
|
||||
// Checks:
|
||||
// 1. Multi-material objects alongside a parallel mode (existing badge condition)
|
||||
// 2. Bed temperature conflict: primary tool controls the bed; secondary tools warned
|
||||
// if their optimal bed temp differs from the primary's by more than 5°C
|
||||
// 3. Filament type mismatch: materials with incompatible requirements
|
||||
static std::vector<wxString> collect_imex_warnings(PartPlate* plate)
|
||||
{
|
||||
std::vector<wxString> warnings;
|
||||
if (!plate) return warnings;
|
||||
|
||||
// Check 1: multi-material conflict (same condition that drives the badge)
|
||||
if (plate->has_imex_multimaterial_conflict())
|
||||
warnings.push_back(_L("Multi-material objects detected — secondary carriages may not behave as expected with multi-material prints."));
|
||||
|
||||
// Checks 2 & 3 only apply when a non-primary parallel mode is active
|
||||
const std::string mode = plate->get_imex_mode();
|
||||
if (mode == "primary") return warnings;
|
||||
|
||||
PresetBundle* bundle = wxGetApp().preset_bundle;
|
||||
if (!bundle) return warnings;
|
||||
|
||||
// Resolve active tool indices for this mode from printer config
|
||||
const auto& printer_cfg = bundle->printers.get_edited_preset().config;
|
||||
const auto* mode_names_opt = printer_cfg.option<ConfigOptionStrings>("imex_mode_names");
|
||||
const auto* tools_opt = printer_cfg.option<ConfigOptionStrings>("imex_mode_active_tools");
|
||||
|
||||
// Upper bound: filament_presets.size() is the practical limit, capped at MAXIMUM_EXTRUDER_NUMBER (64)
|
||||
const size_t max_tool = std::min(bundle->filament_presets.size(), MAXIMUM_EXTRUDER_NUMBER);
|
||||
|
||||
std::vector<int> active_tools;
|
||||
if (mode_names_opt && tools_opt) {
|
||||
for (size_t i = 0; i < mode_names_opt->values.size(); ++i) {
|
||||
if (i >= tools_opt->values.size() || mode_names_opt->values[i] != mode) continue;
|
||||
std::istringstream ss(tools_opt->values[i]);
|
||||
std::string token;
|
||||
while (std::getline(ss, token, ',')) {
|
||||
token.erase(std::remove_if(token.begin(), token.end(), ::isspace), token.end());
|
||||
if (token.empty()) continue;
|
||||
try {
|
||||
int idx = std::stoi(token);
|
||||
if (idx >= 0 && (max_tool == 0 || (size_t)idx < max_tool))
|
||||
active_tools.push_back(idx);
|
||||
} catch (...) {}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (active_tools.size() < 2) return warnings;
|
||||
|
||||
const int primary_tool = active_tools[0];
|
||||
const DynamicPrintConfig& full_cfg = bundle->full_config();
|
||||
// Bed temps are per-plate-type; resolve the active plate's bed type to get the right key.
|
||||
const BedType bed_type = bundle->project_config.opt_enum<BedType>("curr_bed_type");
|
||||
const std::string bed_temp_key = get_bed_temp_key(bed_type);
|
||||
const auto* bed_temps = bed_temp_key.empty() ? nullptr : full_cfg.option<ConfigOptionInts>(bed_temp_key);
|
||||
const auto& filament_presets = bundle->filament_presets;
|
||||
|
||||
// Primary tool's bed temp and filament type
|
||||
const int primary_bed_temp = (bed_temps && primary_tool < (int)bed_temps->values.size())
|
||||
? bed_temps->values[primary_tool] : 0;
|
||||
std::string primary_display_type;
|
||||
if (primary_tool < (int)filament_presets.size()) {
|
||||
Preset* p = bundle->filaments.find_preset(filament_presets[primary_tool]);
|
||||
if (p) p->get_filament_type(primary_display_type);
|
||||
}
|
||||
if (primary_display_type.empty()) primary_display_type = "unknown";
|
||||
|
||||
for (size_t i = 1; i < active_tools.size(); ++i) {
|
||||
const int tool_idx = active_tools[i];
|
||||
|
||||
std::string secondary_display_type;
|
||||
if (tool_idx < (int)filament_presets.size()) {
|
||||
Preset* p = bundle->filaments.find_preset(filament_presets[tool_idx]);
|
||||
if (p) p->get_filament_type(secondary_display_type);
|
||||
}
|
||||
if (secondary_display_type.empty()) secondary_display_type = "unknown";
|
||||
|
||||
// Check 2: bed temperature — primary wins, secondary may not get what it needs
|
||||
if (bed_temps && primary_bed_temp > 0 && tool_idx < (int)bed_temps->values.size()) {
|
||||
const int secondary_bed_temp = bed_temps->values[tool_idx];
|
||||
if (secondary_bed_temp > 0 && std::abs(secondary_bed_temp - primary_bed_temp) > 5) {
|
||||
warnings.push_back(wxString::Format(
|
||||
_L("Bed temperature conflict: T%d (%s) sets the bed to %d\u00B0C \u2014 "
|
||||
"T%d (%s) requires %d\u00B0C. The primary tool controls the bed; "
|
||||
"secondary tools print at whatever temperature it sets."),
|
||||
primary_tool, primary_display_type, primary_bed_temp,
|
||||
tool_idx, secondary_display_type, secondary_bed_temp));
|
||||
}
|
||||
}
|
||||
|
||||
// Check 3: filament type mismatch
|
||||
if (primary_display_type != secondary_display_type &&
|
||||
primary_display_type != "unknown" && secondary_display_type != "unknown") {
|
||||
warnings.push_back(wxString::Format(
|
||||
_L("Filament type mismatch: T%d uses %s and T%d uses %s. "
|
||||
"These materials have incompatible requirements and are not designed to print together."),
|
||||
primary_tool, primary_display_type,
|
||||
tool_idx, secondary_display_type));
|
||||
}
|
||||
}
|
||||
|
||||
return warnings;
|
||||
}
|
||||
|
||||
void Plater::priv::on_action_slice_plate(SimpleEvent&)
|
||||
{
|
||||
if (q != nullptr) {
|
||||
BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << ":received slice plate event\n" ;
|
||||
|
||||
// iMEX parallel mode + multi-material caution check (current plate only)
|
||||
// IMEX parallel mode warnings (current plate only)
|
||||
{
|
||||
PartPlate* plate = partplate_list.get_curr_plate();
|
||||
if (plate && plate->has_imex_multimaterial_conflict()) {
|
||||
std::vector<wxString> warnings = collect_imex_warnings(plate);
|
||||
if (!warnings.empty()) {
|
||||
int plate_num = partplate_list.get_curr_plate_index() + 1;
|
||||
wxString msg = wxString::Format(
|
||||
_L("Plate %d has an IDEX/IQEX parallel mode active alongside multi-material objects.\n\n"
|
||||
"Secondary tool heads operate at the firmware level and may not behave as expected with multi-material prints. "
|
||||
"Proceed with caution and verify your G-code handles this combination correctly.\n\n"
|
||||
"Continue slicing?"),
|
||||
plate_num);
|
||||
MessageDialog dlg(q, msg, _L("IDEX/IQEX Multi-Material Caution"), wxICON_WARNING | wxYES | wxNO);
|
||||
if (dlg.ShowModal() != wxID_YES)
|
||||
wxString msg = wxString::Format(_L("Plate %d has IDEX/IQEX parallel mode active with the following concerns:\n\n"), plate_num);
|
||||
for (const wxString& w : warnings)
|
||||
msg += L"\u2022 " + w + "\n\n";
|
||||
msg += _L("Continue slicing?");
|
||||
MessageDialog dlg(q, msg, _L("IDEX/IQEX Parallel Mode Warning"), wxICON_WARNING | wxYES | wxNO);
|
||||
if (dlg.ShowModal() != wxID_YES) {
|
||||
q->select_view_3D("3D");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9811,6 +9918,8 @@ void Plater::priv::on_action_slice_plate(SimpleEvent&)
|
||||
m_slice_all = false;
|
||||
q->reslice();
|
||||
q->select_view_3D("Preview");
|
||||
// Regenerate any thumbnails that were wiped by the panel switch above.
|
||||
q->update_all_plate_thumbnails();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9818,30 +9927,30 @@ void Plater::priv::on_action_slice_plate(SimpleEvent&)
|
||||
void Plater::priv::on_action_slice_all(SimpleEvent&)
|
||||
{
|
||||
if (q != nullptr) {
|
||||
BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << ":received slice project event\n" ;
|
||||
|
||||
// iMEX parallel mode + multi-material caution check (all plates)
|
||||
// IMEX parallel mode warnings (all plates)
|
||||
{
|
||||
std::vector<int> conflict_plates;
|
||||
wxString combined_msg;
|
||||
std::vector<PartPlate*> warned_plates;
|
||||
int plate_count = partplate_list.get_plate_count();
|
||||
for (int i = 0; i < plate_count; ++i) {
|
||||
PartPlate* plate = partplate_list.get_plate(i);
|
||||
if (plate && plate->has_imex_multimaterial_conflict())
|
||||
conflict_plates.push_back(i + 1); // 1-based for display
|
||||
std::vector<wxString> warnings = collect_imex_warnings(plate);
|
||||
if (warnings.empty()) continue;
|
||||
warned_plates.push_back(plate);
|
||||
combined_msg += wxString::Format(_L("Plate %d:\n"), i + 1);
|
||||
for (const wxString& w : warnings)
|
||||
combined_msg += L" \u2022 " + w + "\n";
|
||||
combined_msg += "\n";
|
||||
}
|
||||
if (!conflict_plates.empty()) {
|
||||
wxString plate_list;
|
||||
for (int n : conflict_plates)
|
||||
plate_list += wxString::Format(" %d", n);
|
||||
wxString msg = wxString::Format(
|
||||
_L("Plate(s)%s have an IDEX/IQEX parallel mode active alongside multi-material objects.\n\n"
|
||||
"Secondary tool heads operate at the firmware level and may not behave as expected with multi-material prints. "
|
||||
"Proceed with caution and verify your G-code handles this combination correctly.\n\n"
|
||||
"Continue slicing?"),
|
||||
plate_list);
|
||||
MessageDialog dlg(q, msg, _L("IDEX/IQEX Multi-Material Caution"), wxICON_WARNING | wxYES | wxNO);
|
||||
if (dlg.ShowModal() != wxID_YES)
|
||||
if (!combined_msg.empty()) {
|
||||
wxString msg = _L("The following IDEX/IQEX parallel mode concerns were detected:\n\n")
|
||||
+ combined_msg
|
||||
+ _L("Continue slicing?");
|
||||
MessageDialog dlg(q, msg, _L("IDEX/IQEX Parallel Mode Warning"), wxICON_WARNING | wxYES | wxNO);
|
||||
if (dlg.ShowModal() != wxID_YES) {
|
||||
q->select_view_3D("3D");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9861,6 +9970,8 @@ void Plater::priv::on_action_slice_all(SimpleEvent&)
|
||||
q->reslice();
|
||||
if (!m_is_publishing)
|
||||
q->select_view_3D("Preview");
|
||||
// Regenerate any thumbnails that were wiped by the panel switch above.
|
||||
q->update_all_plate_thumbnails();
|
||||
//BBS: wish to select all plates stats item
|
||||
preview->get_canvas3d()->_update_select_plate_toolbar_stats_item(true);
|
||||
}
|
||||
@@ -13394,7 +13505,6 @@ void Plater::invalid_all_plate_thumbnails()
|
||||
{
|
||||
if (using_exported_file() || skip_thumbnail_invalid)
|
||||
return;
|
||||
BOOST_LOG_TRIVIAL(info) << "thumb: invalid all";
|
||||
for (int i = 0; i < get_partplate_list().get_plate_count(); i++) {
|
||||
PartPlate* plate = get_partplate_list().get_plate(i);
|
||||
plate->thumbnail_data.reset();
|
||||
@@ -15256,10 +15366,10 @@ int Plater::export_3mf(const boost::filesystem::path& output_path, SaveStrategy
|
||||
ThumbnailData* thumbnail_data = &p->partplate_list.get_plate(i)->thumbnail_data;
|
||||
if (p->partplate_list.get_plate(i)->thumbnail_data.is_valid() && using_exported_file()) {
|
||||
//no need to generate thumbnail
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": non need to re-generate thumbnail for gcode/exported mode of plate %1%")%i;
|
||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": non need to re-generate thumbnail for gcode/exported mode of plate %1%")%i;
|
||||
}
|
||||
else {
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": re-generate thumbnail for plate %1%") % i;
|
||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": re-generate thumbnail for plate %1%") % i;
|
||||
const ThumbnailsParams thumbnail_params = { {}, false, true, true, true, i };
|
||||
p->generate_thumbnail(p->partplate_list.get_plate(i)->thumbnail_data, THUMBNAIL_SIZE_3MF.first, THUMBNAIL_SIZE_3MF.second,
|
||||
thumbnail_params, Camera::EType::Ortho);
|
||||
@@ -15269,9 +15379,9 @@ int Plater::export_3mf(const boost::filesystem::path& output_path, SaveStrategy
|
||||
ThumbnailData *no_light_thumbnail_data = &p->partplate_list.get_plate(i)->no_light_thumbnail_data;
|
||||
if (p->partplate_list.get_plate(i)->no_light_thumbnail_data.is_valid() && using_exported_file()) {
|
||||
// no need to generate thumbnail
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": non need to re-generate thumbnail for gcode/exported mode of plate %1%") % i;
|
||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": non need to re-generate thumbnail for gcode/exported mode of plate %1%") % i;
|
||||
} else {
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": re-generate thumbnail for plate %1%") % i;
|
||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": re-generate thumbnail for plate %1%") % i;
|
||||
const ThumbnailsParams thumbnail_params = {{}, false, true, true, true, i};
|
||||
p->generate_thumbnail(p->partplate_list.get_plate(i)->no_light_thumbnail_data, THUMBNAIL_SIZE_3MF.first, THUMBNAIL_SIZE_3MF.second, thumbnail_params,
|
||||
Camera::EType::Ortho, Camera::ViewAngleType::Iso, false, true);
|
||||
@@ -15286,10 +15396,10 @@ int Plater::export_3mf(const boost::filesystem::path& output_path, SaveStrategy
|
||||
ThumbnailData* top_thumbnail = &p->partplate_list.get_plate(i)->top_thumbnail_data;
|
||||
if (top_thumbnail->is_valid() && using_exported_file()) {
|
||||
//no need to generate thumbnail
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": non need to re-generate top_thumbnail for gcode/exported mode of plate %1%")%i;
|
||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": non need to re-generate top_thumbnail for gcode/exported mode of plate %1%")%i;
|
||||
}
|
||||
else {
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": re-generate top_thumbnail for plate %1%") % i;
|
||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": re-generate top_thumbnail for plate %1%") % i;
|
||||
const ThumbnailsParams thumbnail_params = { {}, false, true, false, true, i };
|
||||
p->generate_thumbnail(p->partplate_list.get_plate(i)->top_thumbnail_data, THUMBNAIL_SIZE_3MF.first, THUMBNAIL_SIZE_3MF.second, thumbnail_params,
|
||||
Camera::EType::Ortho, Camera::ViewAngleType::Top_Plate, false);
|
||||
@@ -15299,10 +15409,10 @@ int Plater::export_3mf(const boost::filesystem::path& output_path, SaveStrategy
|
||||
ThumbnailData* picking_thumbnail = &p->partplate_list.get_plate(i)->pick_thumbnail_data;
|
||||
if (picking_thumbnail->is_valid() && using_exported_file()) {
|
||||
//no need to generate thumbnail
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": non need to re-generate pick_thumbnail for gcode/exported mode of plate %1%")%i;
|
||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": non need to re-generate pick_thumbnail for gcode/exported mode of plate %1%")%i;
|
||||
}
|
||||
else {
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": re-generate pick_thumbnail for plate %1%") % i;
|
||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": re-generate pick_thumbnail for plate %1%") % i;
|
||||
const ThumbnailsParams thumbnail_params = { {}, false, true, false, true, i };
|
||||
p->generate_thumbnail(p->partplate_list.get_plate(i)->pick_thumbnail_data, THUMBNAIL_SIZE_3MF.first, THUMBNAIL_SIZE_3MF.second, thumbnail_params,
|
||||
Camera::EType::Ortho, Camera::ViewAngleType::Top_Plate, true,true);
|
||||
@@ -16326,7 +16436,7 @@ void Plater::on_config_change(const DynamicPrintConfig &config)
|
||||
if (bed_shape_changed)
|
||||
set_bed_shape();
|
||||
|
||||
// After any bed-shape or is_imex change, ensure the iMEX mode icon geometry and
|
||||
// After any bed-shape or is_imex change, ensure the IMEX mode icon geometry and
|
||||
// raycaster are correct. set_shape() short-circuits when the bed is unchanged,
|
||||
// so we call refresh_imex_icons() explicitly whenever is_imex is active.
|
||||
// Always done AFTER set_bed_shape() so m_shape is current.
|
||||
|
||||
Reference in New Issue
Block a user