feat: per-plate iXex mode selection with undo, dark mode icons, and menu fixes

- Add per-plate iXex mode icon to the plate toolbar (normal, hover, dark, dark-hover SVG variants)
- Left-click cycles through available modes; right-click shows a popup menu with all modes as radio items
- Mode changes are recorded in the undo/redo snapshot system
- Fix double context menu: suppress EVT_GLCANVAS_PLATE_RIGHT_CLICK and EVT_GLCANVAS_RIGHT_CLICK when the iXex icon popup was already shown
- Remove ixex_parallel_mode combo from Print Settings > Other > Special mode (superseded by per-plate icon)
- Remove dead code: refresh_ixex_mode_combo(), m_ixex_mode_combo member, related Tab reload hook
- iXex mode persisted in 3MF project files via existing plate metadata serialization

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Clifford Garwood
2026-04-07 23:25:22 -04:00
co-authored by Claude Sonnet 4.6
parent f8565df113
commit ec40172019
15 changed files with 226 additions and 91 deletions
+10
View File
@@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" width="43" height="43" viewBox="0 0 43 43">
<rect x="1.5" y="1.5" width="40" height="40" rx="3.5" style="fill:#e9e9e9"/>
<path d="M38,2a3,3,0,0,1,3,3V38a3,3,0,0,1-3,3H5a3,3,0,0,1-3-3V5A3,3,0,0,1,5,2H38m0-1H5A4,4,0,0,0,1,5V38a4,4,0,0,0,4,4H38a4,4,0,0,0,4-4V5a4,4,0,0,0-4-4Z" style="fill:#545454;stroke-width:1.0015748;stroke-dasharray:none"/>
<path d="M 16.798963,13.679793 14.08412,14.062706 12.860469,16.516209 11.657363,14.052567 8.9458146,13.646979 10.917098,11.741451 10.46492,9.0372811 12.886345,10.323242 15.318433,9.0575608 14.843673,11.757857 Z" style="fill:none;stroke:#545454;stroke-width:0.8;stroke-dasharray:none" transform="translate(2,0.6593574)"/>
<path d="M 16.798963,13.679793 14.08412,14.062706 12.860469,16.516209 11.657363,14.052567 8.9458146,13.646979 10.917098,11.741451 10.46492,9.0372811 12.886345,10.323242 15.318433,9.0575608 14.843673,11.757857 Z" style="fill:none;stroke:#545454;stroke-width:0.8;stroke-dasharray:none" transform="translate(15.543848,0.6593574)"/>
<path d="M 16.798963,13.679793 14.08412,14.062706 12.860469,16.516209 11.657363,14.052567 8.9458146,13.646979 10.917098,11.741451 10.46492,9.0372811 12.886345,10.323242 15.318433,9.0575608 14.843673,11.757857 Z" style="fill:none;stroke:#545454;stroke-width:0.8;stroke-dasharray:none" transform="translate(1.9999996,17.214798)"/>
<path d="M 16.798963,13.679793 14.08412,14.062706 12.860469,16.516209 11.657363,14.052567 8.9458146,13.646979 10.917098,11.741451 10.46492,9.0372811 12.886345,10.323242 15.318433,9.0575608 14.843673,11.757857 Z" style="fill:none;stroke:#545454;stroke-width:0.8;stroke-dasharray:none" transform="translate(15.517972,17.291251)"/>
<path d="M 21.527502,7.146574 V 35.745655" style="fill:#545454;stroke:#545454;stroke-width:0.8;stroke-linecap:round;stroke-dasharray:none"/>
<path d="M 8.3002566,21.575891 H 34.251585" style="fill:#545454;stroke:#545454;stroke-width:0.8;stroke-linecap:round;stroke-dasharray:none"/>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

+10
View File
@@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" width="43" height="43" viewBox="0 0 43 43">
<rect x="1.5" y="1.5" width="40" height="40" rx="3.5" style="fill:#54545a"/>
<path d="M38,2a3,3,0,0,1,3,3V38a3,3,0,0,1-3,3H5a3,3,0,0,1-3-3V5A3,3,0,0,1,5,2H38m0-1H5A4,4,0,0,0,1,5V38a4,4,0,0,0,4,4H38a4,4,0,0,0,4-4V5a4,4,0,0,0-4-4Z" style="fill:#b3b3b3;stroke-width:1.0015748;stroke-dasharray:none"/>
<path d="M 16.798963,13.679793 14.08412,14.062706 12.860469,16.516209 11.657363,14.052567 8.9458146,13.646979 10.917098,11.741451 10.46492,9.0372811 12.886345,10.323242 15.318433,9.0575608 14.843673,11.757857 Z" style="fill:none;stroke:#c4c4c4;stroke-width:0.8;stroke-dasharray:none" transform="translate(2,0.6593574)"/>
<path d="M 16.798963,13.679793 14.08412,14.062706 12.860469,16.516209 11.657363,14.052567 8.9458146,13.646979 10.917098,11.741451 10.46492,9.0372811 12.886345,10.323242 15.318433,9.0575608 14.843673,11.757857 Z" style="fill:none;stroke:#c4c4c4;stroke-width:0.8;stroke-dasharray:none" transform="translate(15.543848,0.6593574)"/>
<path d="M 16.798963,13.679793 14.08412,14.062706 12.860469,16.516209 11.657363,14.052567 8.9458146,13.646979 10.917098,11.741451 10.46492,9.0372811 12.886345,10.323242 15.318433,9.0575608 14.843673,11.757857 Z" style="fill:none;stroke:#c4c4c4;stroke-width:0.8;stroke-dasharray:none" transform="translate(1.9999996,17.214798)"/>
<path d="M 16.798963,13.679793 14.08412,14.062706 12.860469,16.516209 11.657363,14.052567 8.9458146,13.646979 10.917098,11.741451 10.46492,9.0372811 12.886345,10.323242 15.318433,9.0575608 14.843673,11.757857 Z" style="fill:none;stroke:#c4c4c4;stroke-width:0.8;stroke-dasharray:none" transform="translate(15.517972,17.291251)"/>
<path d="M 21.527502,7.146574 V 35.745655" style="fill:#c4c4c4;stroke:#c4c4c4;stroke-width:0.8;stroke-linecap:round;stroke-dasharray:none"/>
<path d="M 8.3002566,21.575891 H 34.251585" style="fill:#c4c4c4;stroke:#c4c4c4;stroke-width:0.8;stroke-linecap:round;stroke-dasharray:none"/>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

@@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" width="43" height="43" viewBox="0 0 43 43">
<rect x="1.5" y="1.5" width="40" height="40" rx="3.5" style="fill:#e9e9e9"/>
<path d="M38,2a3,3,0,0,1,3,3V38a3,3,0,0,1-3,3H5a3,3,0,0,1-3-3V5A3,3,0,0,1,5,2H38m0-1H5A4,4,0,0,0,1,5V38a4,4,0,0,0,4,4H38a4,4,0,0,0,4-4V5a4,4,0,0,0-4-4Z" style="fill:#009688;stroke-width:1.0015748;stroke-dasharray:none"/>
<path d="M 16.798963,13.679793 14.08412,14.062706 12.860469,16.516209 11.657363,14.052567 8.9458146,13.646979 10.917098,11.741451 10.46492,9.0372811 12.886345,10.323242 15.318433,9.0575608 14.843673,11.757857 Z" style="fill:none;stroke:#009688;stroke-width:0.8;stroke-dasharray:none" transform="translate(2,0.6593574)"/>
<path d="M 16.798963,13.679793 14.08412,14.062706 12.860469,16.516209 11.657363,14.052567 8.9458146,13.646979 10.917098,11.741451 10.46492,9.0372811 12.886345,10.323242 15.318433,9.0575608 14.843673,11.757857 Z" style="fill:none;stroke:#009688;stroke-width:0.8;stroke-dasharray:none" transform="translate(15.543848,0.6593574)"/>
<path d="M 16.798963,13.679793 14.08412,14.062706 12.860469,16.516209 11.657363,14.052567 8.9458146,13.646979 10.917098,11.741451 10.46492,9.0372811 12.886345,10.323242 15.318433,9.0575608 14.843673,11.757857 Z" style="fill:none;stroke:#009688;stroke-width:0.8;stroke-dasharray:none" transform="translate(1.9999996,17.214798)"/>
<path d="M 16.798963,13.679793 14.08412,14.062706 12.860469,16.516209 11.657363,14.052567 8.9458146,13.646979 10.917098,11.741451 10.46492,9.0372811 12.886345,10.323242 15.318433,9.0575608 14.843673,11.757857 Z" style="fill:none;stroke:#009688;stroke-width:0.8;stroke-dasharray:none" transform="translate(15.517972,17.291251)"/>
<path d="M 21.527502,7.146574 V 35.745655" style="fill:#009688;stroke:#009688;stroke-width:0.8;stroke-linecap:round;stroke-dasharray:none"/>
<path d="M 8.3002566,21.575891 H 34.251585" style="fill:#009688;stroke:#009688;stroke-width:0.8;stroke-linecap:round;stroke-dasharray:none"/>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

@@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" width="43" height="43" viewBox="0 0 43 43">
<rect x="1.5" y="1.5" width="40" height="40" rx="3.5" style="fill:#54545a"/>
<path d="M38,2a3,3,0,0,1,3,3V38a3,3,0,0,1-3,3H5a3,3,0,0,1-3-3V5A3,3,0,0,1,5,2H38m0-1H5A4,4,0,0,0,1,5V38a4,4,0,0,0,4,4H38a4,4,0,0,0,4-4V5a4,4,0,0,0-4-4Z" style="fill:#009688;stroke-width:1.0015748;stroke-dasharray:none"/>
<path d="M 16.798963,13.679793 14.08412,14.062706 12.860469,16.516209 11.657363,14.052567 8.9458146,13.646979 10.917098,11.741451 10.46492,9.0372811 12.886345,10.323242 15.318433,9.0575608 14.843673,11.757857 Z" style="fill:none;stroke:#009688;stroke-width:0.8;stroke-dasharray:none" transform="translate(2,0.6593574)"/>
<path d="M 16.798963,13.679793 14.08412,14.062706 12.860469,16.516209 11.657363,14.052567 8.9458146,13.646979 10.917098,11.741451 10.46492,9.0372811 12.886345,10.323242 15.318433,9.0575608 14.843673,11.757857 Z" style="fill:none;stroke:#009688;stroke-width:0.8;stroke-dasharray:none" transform="translate(15.543848,0.6593574)"/>
<path d="M 16.798963,13.679793 14.08412,14.062706 12.860469,16.516209 11.657363,14.052567 8.9458146,13.646979 10.917098,11.741451 10.46492,9.0372811 12.886345,10.323242 15.318433,9.0575608 14.843673,11.757857 Z" style="fill:none;stroke:#009688;stroke-width:0.8;stroke-dasharray:none" transform="translate(1.9999996,17.214798)"/>
<path d="M 16.798963,13.679793 14.08412,14.062706 12.860469,16.516209 11.657363,14.052567 8.9458146,13.646979 10.917098,11.741451 10.46492,9.0372811 12.886345,10.323242 15.318433,9.0575608 14.843673,11.757857 Z" style="fill:none;stroke:#009688;stroke-width:0.8;stroke-dasharray:none" transform="translate(15.517972,17.291251)"/>
<path d="M 21.527502,7.146574 V 35.745655" style="fill:#009688;stroke:#009688;stroke-width:0.8;stroke-linecap:round;stroke-dasharray:none"/>
<path d="M 8.3002566,21.575891 H 34.251585" style="fill:#009688;stroke:#009688;stroke-width:0.8;stroke-linecap:round;stroke-dasharray:none"/>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

+10
View File
@@ -333,6 +333,7 @@ static constexpr const char* FIRST_LAYER_PRINT_SEQUENCE_ATTR = "first_layer_prin
static constexpr const char* OTHER_LAYERS_PRINT_SEQUENCE_ATTR = "other_layers_print_sequence";
static constexpr const char* OTHER_LAYERS_PRINT_SEQUENCE_NUMS_ATTR = "other_layers_print_sequence_nums";
static constexpr const char* SPIRAL_VASE_MODE = "spiral_mode";
static constexpr const char* IXEX_PARALLEL_MODE_ATTR = "ixex_parallel_mode";
static constexpr const char* FILAMENT_MAP_MODE_ATTR = "filament_map_mode";
static constexpr const char* FILAMENT_MAP_ATTR = "filament_maps";
static constexpr const char* LIMIT_FILAMENT_MAP_ATTR = "limit_filament_maps";
@@ -4292,6 +4293,9 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
std::istringstream(value) >> std::boolalpha >> spiral_mode;
m_curr_plater->config.set_key_value("spiral_mode", new ConfigOptionBool(spiral_mode));
}
else if (key == IXEX_PARALLEL_MODE_ATTR) {
m_curr_plater->config.set_key_value("ixex_parallel_mode", new ConfigOptionString(value));
}
else if (key == FILAMENT_MAP_MODE_ATTR)
{
FilamentMapMode map_mode = FilamentMapMode::fmmAutoForFlush;
@@ -7773,6 +7777,12 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
if (spiral_mode_opt)
stream << " <" << METADATA_TAG << " " << KEY_ATTR << "=\"" << SPIRAL_VASE_MODE << "\" " << VALUE_ATTR << "=\"" << spiral_mode_opt->getBool() << "\"/>\n";
{
auto* ixex_mode_opt = plate_data->config.option<ConfigOptionString>("ixex_parallel_mode");
if (ixex_mode_opt && !ixex_mode_opt->value.empty() && ixex_mode_opt->value != "primary")
stream << " <" << METADATA_TAG << " " << KEY_ATTR << "=\"" << IXEX_PARALLEL_MODE_ATTR << "\" " << VALUE_ATTR << "=\"" << ixex_mode_opt->value << "\"/>\n";
}
//filament map related
ConfigOption* filament_map_mode_opt = plate_data->config.option("filament_map_mode");
t_config_enum_names filament_map_mode_names = ConfigOptionEnum<FilamentMapMode>::get_enum_names();
-16
View File
@@ -5506,22 +5506,6 @@ void PrintConfigDef::init_fff_params()
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionInt(2));
def = this->add("ixex_primary_col", coInt);
def->label = L("Primary Tool Column");
def->tooltip = L("X index (0-based) of the primary toolhead. 0 = leftmost column. Determines which zone is the active print area.");
def->min = 0;
def->max = 3;
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionInt(0));
def = this->add("ixex_primary_row", coInt);
def->label = L("Primary Tool Row");
def->tooltip = L("Y index (0-based) of the primary toolhead's gantry. 0 = front gantry. Determines which zone is the active print area.");
def->min = 0;
def->max = 3;
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionInt(0));
def = this->add("ixex_tool_layout", coString);
def->label = L("Tool 0 Corner");
def->tooltip = L("Physical corner of the bed where tool T0 (index 0) is located. "
-2
View File
@@ -1339,8 +1339,6 @@ PRINT_CONFIG_CLASS_DEFINE(
((ConfigOptionBool, is_ixex))
((ConfigOptionInt, ixex_gantry_count))
((ConfigOptionInt, ixex_tools_per_gantry))
((ConfigOptionInt, ixex_primary_col))
((ConfigOptionInt, ixex_primary_row))
((ConfigOptionFloat, ixex_nozzle_clearance_x))
((ConfigOptionFloat, ixex_nozzle_clearance_y))
((ConfigOptionStrings, ixex_mode_names))
+7 -1
View File
@@ -1533,7 +1533,13 @@ void GCodeViewer::render(int canvas_width, int canvas_height, int right_margin)
if (is_ixex_opt && is_ixex_opt->value) {
const DynamicPrintConfig& process_cfg = preset_bundle->prints.get_edited_preset().config;
auto* mode_opt = process_cfg.opt<ConfigOptionString>("ixex_parallel_mode");
const std::string mode = mode_opt ? mode_opt->value : "primary";
std::string mode = mode_opt ? mode_opt->value : "primary";
// Per-plate mode overrides the process preset.
if (auto* plate = wxGetApp().plater()->get_partplate_list().get_curr_plate()) {
std::string plate_mode = plate->get_ixex_mode();
if (plate_mode != "primary")
mode = plate_mode;
}
if (mode != m_ixex_last_mode) {
m_sequential_view.m_ixex_secondary_markers.clear();
+4 -3
View File
@@ -4648,9 +4648,10 @@ void GLCanvas3D::on_mouse(wxMouseEvent& evt)
}
//BBS change plate selection
bool plate_icon_popup_shown = false;
if (!m_hover_plate_idxs.empty() && (m_canvas_type == CanvasView3D) && !m_mouse.dragging) {
int hover_idx = m_hover_plate_idxs.front();
wxGetApp().plater()->select_plate_by_hover_id(hover_idx, true);
plate_icon_popup_shown = (wxGetApp().plater()->select_plate_by_hover_id(hover_idx, true) == 1);
if (m_hover_volume_idxs.empty())
deselect_all();
render();
@@ -4670,10 +4671,10 @@ void GLCanvas3D::on_mouse(wxMouseEvent& evt)
if (!m_mouse.ignore_right_up && m_gizmos.get_current_type() == GLGizmosManager::EType::Undefined) {
//BBS post right click event
if (!m_hover_plate_idxs.empty()) {
if (!m_hover_plate_idxs.empty() && !plate_icon_popup_shown) {
post_event(RBtnPlateEvent(EVT_GLCANVAS_PLATE_RIGHT_CLICK, { logical_pos, m_hover_plate_idxs.front() }));
}
else {
else if (!plate_icon_popup_shown) {
// do not post the event if the user is panning the scene
// or if right click was done over the wipe tower
bool post_right_click_event = m_hover_volume_idxs.empty() || !m_volumes.volumes[get_first_hover_volume_idx()]->is_wipe_tower;
+12 -5
View File
@@ -542,9 +542,16 @@ void ArrangeJob::process(Ctl &ctl)
// When an iXex parallel mode is active, constrain auto-arrange to the primary zone only
// and treat carriage collision strips as hard excluded regions.
// NOTE: m_ixex_primary_zone_box and ixex_collision_zones() are in global (world) coordinates
// because they are derived from m_shape which includes the plate origin offset. The arranger
// always works in plate-local space (origin = 0,0), so we subtract the plate origin here.
if (PartPlate* curr_plate = partplate_list.get_curr_plate()) {
if (auto pz = curr_plate->ixex_primary_zone()) {
BoundingBox scaled_pz = scaled(*pz);
Vec3d plate_origin = curr_plate->get_origin();
double ox = plate_origin.x(), oy = plate_origin.y();
BoundingBoxf pz_local(Vec2d(pz->min.x() - ox, pz->min.y() - oy),
Vec2d(pz->max.x() - ox, pz->max.y() - oy));
BoundingBox scaled_pz = scaled(pz_local);
bedpts = {
{ scaled_pz.min.x(), scaled_pz.min.y() },
{ scaled_pz.max.x(), scaled_pz.min.y() },
@@ -553,10 +560,10 @@ void ArrangeJob::process(Ctl &ctl)
};
for (const BoundingBoxf3& cz : curr_plate->ixex_collision_zones()) {
Polygon poly({
{ scaled(cz.min.x()), scaled(cz.min.y()) },
{ scaled(cz.max.x()), scaled(cz.min.y()) },
{ scaled(cz.max.x()), scaled(cz.max.y()) },
{ scaled(cz.min.x()), scaled(cz.max.y()) },
{ scaled(cz.min.x() - ox), scaled(cz.min.y() - oy) },
{ scaled(cz.max.x() - ox), scaled(cz.min.y() - oy) },
{ scaled(cz.max.x() - ox), scaled(cz.max.y() - oy) },
{ scaled(cz.min.x() - ox), scaled(cz.max.y() - oy) },
});
arrangement::ArrangePolygon ap;
ap.poly.contour = poly;
+89 -7
View File
@@ -365,6 +365,34 @@ std::vector<Vec2d> PartPlate::get_plate_wrapping_detection_area() const
return std::vector<Vec2d>();
}
std::string PartPlate::get_ixex_mode() const
{
if (m_config.has("ixex_parallel_mode")) {
auto* opt = m_config.option<ConfigOptionString>("ixex_parallel_mode");
if (opt && !opt->value.empty())
return opt->value;
}
return "primary";
}
void PartPlate::set_ixex_mode(const std::string& mode)
{
if (mode.empty() || mode == "primary") {
m_config.erase("ixex_parallel_mode");
} else {
m_config.set_key_value("ixex_parallel_mode", new ConfigOptionString(mode));
}
update_slice_result_valid_state(false);
m_ixex_zones_mode_cache = "\x01"; // force zone rebuild
}
void PartPlate::reset_ixex_mode()
{
m_config.erase("ixex_parallel_mode");
update_slice_result_valid_state(false);
m_ixex_zones_mode_cache = "\x01";
}
void PartPlate::set_spiral_vase_mode(bool spiral_mode, bool as_global)
{
std::string key = "spiral_mode";
@@ -475,9 +503,14 @@ void PartPlate::calc_ixex_zones()
if (!is_ixex_opt || !is_ixex_opt->value)
return;
const DynamicPrintConfig& process_cfg = wxGetApp().preset_bundle->prints.get_edited_preset().config;
auto* mode_opt = process_cfg.option<ConfigOptionString>("ixex_parallel_mode");
std::string active_mode = mode_opt ? mode_opt->value : "primary";
// Per-plate mode takes priority over the process preset.
std::string active_mode = get_ixex_mode();
if (active_mode == "primary") {
const DynamicPrintConfig& process_cfg = wxGetApp().preset_bundle->prints.get_edited_preset().config;
auto* mode_opt = process_cfg.option<ConfigOptionString>("ixex_parallel_mode");
if (mode_opt && !mode_opt->value.empty())
active_mode = mode_opt->value;
}
if (active_mode == "primary" || active_mode.empty())
return;
@@ -767,7 +800,8 @@ void PartPlate::calc_ixex_zones()
}
// Build a cache key from the current iXex config options, or "" if iXex is off.
static std::string build_ixex_cache_key()
// Reads per-plate mode from m_config first, falling back to the process preset.
std::string PartPlate::build_ixex_cache_key() const
{
if (!wxGetApp().preset_bundle)
return "";
@@ -775,14 +809,20 @@ static std::string build_ixex_cache_key()
auto* is_ixex_opt = printer_cfg.option<ConfigOptionBool>("is_ixex");
if (!is_ixex_opt || !is_ixex_opt->value)
return "";
const DynamicPrintConfig& process_cfg = wxGetApp().preset_bundle->prints.get_edited_preset().config;
auto* mode_opt = process_cfg.option<ConfigOptionString>("ixex_parallel_mode");
// Per-plate mode takes priority over process preset.
std::string active_mode = get_ixex_mode();
if (active_mode == "primary") {
const DynamicPrintConfig& process_cfg = wxGetApp().preset_bundle->prints.get_edited_preset().config;
auto* mode_opt = process_cfg.option<ConfigOptionString>("ixex_parallel_mode");
if (mode_opt && !mode_opt->value.empty())
active_mode = mode_opt->value;
}
auto* n_col_opt = printer_cfg.option<ConfigOptionInt>("ixex_tools_per_gantry");
auto* n_row_opt = printer_cfg.option<ConfigOptionInt>("ixex_gantry_count");
auto* cw_opt = printer_cfg.option<ConfigOptionFloat>("ixex_nozzle_clearance_x");
auto* ch_opt = printer_cfg.option<ConfigOptionFloat>("ixex_nozzle_clearance_y");
auto* mgn_opt = printer_cfg.option<ConfigOptionFloat>("ixex_carriage_margin");
return (mode_opt ? mode_opt->value : "primary")
return active_mode
+ "|" + std::to_string(n_col_opt ? n_col_opt->value : 2)
+ "x" + std::to_string(n_row_opt ? n_row_opt->value : 1)
+ "|cw" + std::to_string(cw_opt ? (int)cw_opt->value : 0)
@@ -1643,6 +1683,20 @@ void PartPlate::render_icons(bool bottom, bool only_name, int hover_id)
} else
render_icon_texture(m_move_front_icon.model, m_partplate_list->m_move_front_texture);
// iXex mode icon — only when is_ixex is active
{
PresetBundle* pb = wxGetApp().preset_bundle;
auto* is_ixex_opt = pb ? pb->printers.get_edited_preset().config.option<ConfigOptionBool>("is_ixex") : nullptr;
if (is_ixex_opt && is_ixex_opt->value) {
if (hover_id == (int)PLATE_IXEX_MODE_ID) {
render_icon_texture(m_ixex_mode_icon.model, m_partplate_list->m_ixex_mode_hovered_texture);
std::string cur = get_ixex_mode();
show_tooltip(_u8L("iXex mode: ") + cur + _u8L(" (left-click to cycle, right-click for menu)"));
} else {
render_icon_texture(m_ixex_mode_icon.model, m_partplate_list->m_ixex_mode_texture);
}
}
}
if (m_partplate_list->render_plate_settings) {
bool has_plate_settings = get_bed_type() != BedType::btDefault || get_print_seq() != PrintSequence::ByDefault || !get_first_layer_print_sequence().empty() || !get_other_layers_print_sequence().empty() || has_spiral_mode_config();
@@ -1952,6 +2006,13 @@ void PartPlate::register_raycasters_for_picking(GLCanvas3D &canvas)
bool dual_bbl = (preset && preset->is_bbl_vendor() && preset->get_printer_extruder_count() == 2);
if (dual_bbl)
register_model_for_picking(canvas, m_plate_filament_map_icon, picking_id_component(PLATE_FILAMENT_MAP_ID));
// Register iXex mode icon only when iXex is active on the current printer preset.
if (preset) {
auto* is_ixex_opt = preset->printers.get_edited_preset().config.option<ConfigOptionBool>("is_ixex");
if (is_ixex_opt && is_ixex_opt->value)
register_model_for_picking(canvas, m_ixex_mode_icon, picking_id_component(PLATE_IXEX_MODE_ID));
}
}
int PartPlate::picking_id_component(int idx) const
@@ -3605,6 +3666,11 @@ bool PartPlate::set_shape(const Pointfs& shape, const Pointfs& exclude_areas, co
dual_bbl = (preset->is_bbl_vendor() && preset->get_printer_extruder_count() == 2);
calc_vertex_for_icons(dual_bbl ? 5 : 6, m_plate_filament_map_icon);
calc_vertex_for_icons(dual_bbl ? 6 : 5, m_move_front_icon);
{
auto* is_ixex_opt = preset->printers.get_edited_preset().config.option<ConfigOptionBool>("is_ixex");
if (is_ixex_opt && is_ixex_opt->value)
calc_vertex_for_icons(dual_bbl ? 7 : 6, m_ixex_mode_icon);
}
calc_vertex_for_number(0, false, m_plate_idx_icon);
// calc vertex for plate name
@@ -4474,6 +4540,20 @@ void PartPlateList::generate_icon_textures()
}
}
// iXex mode icon textures (fall back gracefully if SVG not present yet)
{
file_name = path + (m_is_dark ? "plate_ixex_mode_dark.svg" : "plate_ixex_mode.svg");
if (!m_ixex_mode_texture.load_from_svg_file(file_name, true, false, false, icon_size)) {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(":load file %1% failed (iXex mode icon)") % file_name;
}
}
{
file_name = path + (m_is_dark ? "plate_ixex_mode_hover_dark.svg" : "plate_ixex_mode_hover.svg");
if (!m_ixex_mode_hovered_texture.load_from_svg_file(file_name, true, false, false, icon_size)) {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(":load file %1% failed (iXex mode hover icon)") % file_name;
}
}
std::string text_str = "01";
// ORCA also scale font size to prevent low res texture
int size = wxGetApp().em_unit() * PARTPLATE_ICON_SIZE;
@@ -4517,6 +4597,8 @@ void PartPlateList::release_icon_textures()
m_plate_set_filament_map_hovered_texture.reset();
m_plate_name_edit_texture.reset();
m_plate_name_edit_hovered_texture.reset();
m_ixex_mode_texture.reset();
m_ixex_mode_hovered_texture.reset();
for (int i = 0;i < MAX_PLATE_COUNT; i++) {
m_idx_textures[i].reset();
}
+10 -1
View File
@@ -155,6 +155,7 @@ private:
PickingModel m_plate_filament_map_icon;
PickingModel m_plate_name_edit_icon;
PickingModel m_move_front_icon;
PickingModel m_ixex_mode_icon;
GLModel m_plate_idx_icon;
GLTexture m_texture;
@@ -188,6 +189,7 @@ private:
void calc_height_limit();
void calc_ixex_zones();
void ensure_ixex_zones();
std::string build_ixex_cache_key() const;
void render_ixex_zones(bool force_default_color);
void calc_vertex_for_number(int index, bool one_number, GLModel &buffer);
void calc_vertex_for_plate_name_edit_icon(GLTexture *texture, int index, PickingModel &model);
@@ -220,7 +222,8 @@ private:
public:
static constexpr unsigned int PLATE_NAME_HOVER_ID = 6;
static constexpr unsigned int PLATE_FILAMENT_MAP_ID = 8;
static constexpr unsigned int GRABBER_COUNT = 9;
static constexpr unsigned int PLATE_IXEX_MODE_ID = 9;
static constexpr unsigned int GRABBER_COUNT = 10;
static ColorRGBA SELECT_COLOR;
static ColorRGBA UNSELECT_COLOR;
@@ -280,6 +283,10 @@ public:
bool get_spiral_vase_mode() const;
void set_spiral_vase_mode(bool spiral_mode, bool as_global);
std::string get_ixex_mode() const;
void set_ixex_mode(const std::string& mode);
void reset_ixex_mode();
std::vector<Vec2d> get_plate_wrapping_detection_area() const;
//static const int plate_x_offset = 20; //mm
@@ -627,6 +634,8 @@ class PartPlateList : public ObjectBase
GLTexture m_plate_set_filament_map_hovered_texture;
GLTexture m_plate_name_edit_texture;
GLTexture m_plate_name_edit_hovered_texture;
GLTexture m_ixex_mode_texture;
GLTexture m_ixex_mode_hovered_texture;
GLTexture m_idx_textures[MAX_PLATE_COUNT];
// set render option
bool render_bedtype_logo = true;
+54
View File
@@ -17640,7 +17640,61 @@ int Plater::select_plate_by_hover_id(int hover_id, bool right_click, bool isModi
update();
p->partplate_list.select_plate(0);
}
else if (action == (int)PartPlate::PLATE_IXEX_MODE_ID)
{
ret = select_plate(plate_index);
if (!ret) {
PartPlate* curr_plate = p->partplate_list.get_curr_plate();
// Build ordered mode list: "primary" first, then all named modes.
std::vector<std::string> modes;
modes.push_back("primary");
const DynamicPrintConfig& printer_cfg = wxGetApp().preset_bundle->printers.get_edited_preset().config;
auto* names_opt = printer_cfg.option<ConfigOptionStrings>("ixex_mode_names");
if (names_opt) {
for (const auto& n : names_opt->values)
if (!n.empty()) modes.push_back(n);
}
if (right_click) {
// Show a popup menu with all modes.
wxMenu menu;
std::string current = curr_plate->get_ixex_mode();
for (size_t i = 0; i < modes.size(); ++i) {
wxMenuItem* item = menu.AppendRadioItem(wxID_HIGHEST + (int)i, from_u8(modes[i]));
if (modes[i] == current)
item->Check(true);
}
menu.Bind(wxEVT_MENU, [this, curr_plate, &modes](wxCommandEvent& e) {
int idx = e.GetId() - wxID_HIGHEST;
if (idx >= 0 && idx < (int)modes.size()) {
take_snapshot("set ixex mode");
curr_plate->set_ixex_mode(modes[idx]);
update_project_dirty_from_presets();
set_plater_dirty(true);
update();
}
});
p->view3D->get_canvas3d()->get_wxglcanvas()->PopupMenu(&menu);
ret = 1; // signal to caller: popup was shown, suppress plate context menu
} else {
// Left-click: cycle to next mode.
std::string current = curr_plate->get_ixex_mode();
auto it = std::find(modes.begin(), modes.end(), current);
size_t next_idx = (it == modes.end()) ? 0 : ((it - modes.begin() + 1) % modes.size());
std::string next_mode = modes[next_idx];
if (next_mode != current) {
take_snapshot("set ixex mode");
curr_plate->set_ixex_mode(next_mode);
update_project_dirty_from_presets();
set_plater_dirty(true);
update();
}
}
} else {
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << "can not select plate %1%" << plate_index;
ret = -1;
}
}
else
{
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << "invalid action %1%, with right_click=%2%" << action << right_click;
-54
View File
@@ -2674,26 +2674,6 @@ void TabPrint::build()
optgroup->append_single_option_line("timelapse_type", "others_settings_special_mode#timelapse");
optgroup->append_single_option_line("enable_wrapping_detection");
// Dynamic combo box — choices come from the printer preset's ixex_mode_names.
// Use create_line_with_widget so the undo bitmaps are set (has_undo_ui()=true),
// ensuring the widget x-position matches other option fields in OG_CustomCtrl.
create_line_with_widget(optgroup.get(), "ixex_parallel_mode", "", [this](wxWindow* parent) -> wxSizer* {
m_ixex_mode_combo = new ComboBox(parent, wxID_ANY, wxEmptyString,
wxDefaultPosition,
wxSize(12 * wxGetApp().em_unit(), -1),
0, nullptr, wxCB_READONLY);
m_ixex_mode_combo->GetDropDown().SetUseContentWidth(true);
refresh_ixex_mode_combo();
m_ixex_mode_combo->Bind(wxEVT_COMBOBOX, [this](wxCommandEvent&) {
if (!m_ixex_mode_combo || !m_config) return;
std::string val = m_ixex_mode_combo->GetStringSelection().ToStdString();
m_config->set_key_value("ixex_parallel_mode", new ConfigOptionString(val));
on_value_change("ixex_parallel_mode", val);
});
auto* s = new wxBoxSizer(wxHORIZONTAL);
s->Add(m_ixex_mode_combo, 0, wxALIGN_CENTER_VERTICAL);
return s;
});
optgroup = page->new_optgroup(L("Fuzzy Skin"), L"fuzzy_skin");
optgroup->append_single_option_line("fuzzy_skin", "others_settings_fuzzy_skin");
@@ -2752,36 +2732,6 @@ void TabPrint::reload_config()
{
this->compatible_widget_reload(m_compatible_printers);
Tab::reload_config();
refresh_ixex_mode_combo();
}
void TabPrint::refresh_ixex_mode_combo()
{
if (!m_ixex_mode_combo) return;
// Rebuild choices: "primary" + names from the active printer preset
m_ixex_mode_combo->Clear();
m_ixex_mode_combo->Append("primary");
if (m_preset_bundle) {
auto* names = m_preset_bundle->printers.get_edited_preset().config
.option<ConfigOptionStrings>("ixex_mode_names");
if (names) {
for (const auto& n : names->values)
if (!n.empty()) m_ixex_mode_combo->Append(wxString::FromUTF8(n));
}
}
// Restore current selection from process config (safe: option may not exist in older presets)
std::string cur = "primary";
if (m_config) {
auto* opt = m_config->option<ConfigOptionString>("ixex_parallel_mode");
if (opt) cur = opt->value;
}
if (!m_ixex_mode_combo->SetStringSelection(cur)) {
m_ixex_mode_combo->SetSelection(0); // fall back to "primary"
// The active mode no longer exists — reset config so stale name doesn't crash downstream.
if (m_config)
m_config->set_key_value("ixex_parallel_mode", new ConfigOptionString("primary"));
on_value_change("ixex_parallel_mode", std::string("primary"));
}
}
void TabPrint::update_description_lines()
@@ -2903,7 +2853,6 @@ void TabPrint::clear_pages()
m_recommended_thin_wall_thickness_description_line = nullptr;
m_top_bottom_shell_thickness_explanation = nullptr;
m_ixex_mode_combo = nullptr;
}
//BBS: GUI refactor
@@ -5059,9 +5008,6 @@ void TabPrinter::build_fff()
m_config->set_key_value("ixex_mode_gcodes", new ConfigOptionStrings(gcodes));
update_dirty();
on_value_change("ixex_mode_names", std::string(""));
// Refresh the process tab's mode dropdown immediately
if (auto* print_tab = dynamic_cast<TabPrint*>(wxGetApp().get_tab(Preset::TYPE_PRINT)))
print_tab->refresh_ixex_mode_combo();
};
auto* sizer = new wxBoxSizer(wxHORIZONTAL);
sizer->Add(m_ixex_modes_ctrl, 1, wxEXPAND);
-2
View File
@@ -464,12 +464,10 @@ public:
void update() override;
void clear_pages() override;
bool supports_printer_technology(const PrinterTechnology tech) const override { return tech == ptFFF; }
void refresh_ixex_mode_combo();
private:
ogStaticText* m_recommended_thin_wall_thickness_description_line = nullptr;
ogStaticText* m_top_bottom_shell_thickness_explanation = nullptr;
ComboBox* m_ixex_mode_combo {nullptr};
};
class TabPrintModel : public TabPrint