mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-10 17:21:10 +00:00
feat: Add iXex parallel printing support for IDEX and IQEX printers
Introduces first-class parallel printing (copy/mirror modes) for printers with multiple independent X-axis carriages. Branded iXex (independent X extruder), targeting Klipper firmware with a firmware-agnostic design. - PrintConfig: new printer options declaring iXex capability and geometry (is_ixex, ixex_gantry_count, ixex_tools_per_gantry, carriage dims, tool layout, and per-mode name/role/gcode arrays) - Preset: iXex keys registered in printer and process preset option lists - Tab: IXexModesCtrl visual grid editor in Printer preset tab; mode dropdown in Process → Others tab; clear_pages() nulls iXex pointers to prevent dangling-pointer crash on preset save - PartPlate: 2D zone visualization (active/dimmed/dividers) and placement-violation detection (has_ixex_placement_violations) that blocks slicing when objects fall outside the primary zone - Plater: violation detection wired into update_background_process so the Slice button is disabled with an error notification on violation - GCode: mode-activation G-code injected before machine_start_gcode - GCodeViewer: multi-carriage toolhead markers in sequential preview, filament legend annotated with active carriage count and mode name
This commit is contained in:
@@ -3054,6 +3054,24 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato
|
||||
file.write(m_writer.set_chamber_temperature(max_chamber_temp, true)); // set chamber_temperature
|
||||
}
|
||||
|
||||
// iXex: inject mode G-code before machine_start_gcode so the carriage mode
|
||||
// macro is active before any motion, homing, or probing in start G-code.
|
||||
if (print.config().is_ixex.value && !print.objects().empty()) {
|
||||
const std::string& ixex_mode = print.objects().front()->config().ixex_parallel_mode.value;
|
||||
if (!ixex_mode.empty() && ixex_mode != "primary") {
|
||||
const auto& mode_names = print.config().ixex_mode_names.values;
|
||||
const auto& mode_gcodes = print.config().ixex_mode_gcodes.values;
|
||||
for (size_t i = 0; i < mode_names.size() && i < mode_gcodes.size(); ++i) {
|
||||
if (mode_names[i] == ixex_mode && !mode_gcodes[i].empty()) {
|
||||
std::string ixex_gcode = this->placeholder_parser_process(
|
||||
"ixex_mode_gcode", mode_gcodes[i], initial_extruder_id);
|
||||
file.writeln(ixex_gcode);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Write the custom start G-code
|
||||
file.writeln(machine_start_gcode);
|
||||
|
||||
|
||||
@@ -605,6 +605,12 @@ void Preset::save(DynamicPrintConfig* parent_config)
|
||||
ConfigOptionVectorBase* opt_vec_inherit = static_cast<ConfigOptionVectorBase*>(parent_config->option(option));
|
||||
if (opt_vec_src->size() == 1)
|
||||
opt_dst->set(opt_src);
|
||||
else if (opt_vec_src->size() != opt_vec_inherit->size()) {
|
||||
// Size mismatch (e.g. new multi-extruder profile inheriting from a
|
||||
// single-extruder base): nil-delta encoding requires matching sizes,
|
||||
// so fall back to storing the full vector.
|
||||
opt_dst->set(opt_src);
|
||||
}
|
||||
else if (key_set1->find(option) != key_set1->end()) {
|
||||
opt_vec_dst->set_with_nil(opt_vec_src, opt_vec_inherit, 1);
|
||||
}
|
||||
@@ -955,6 +961,8 @@ static std::vector<std::string> s_Preset_print_options {
|
||||
"enable_wrapping_detection",
|
||||
"seam_slope_type", "seam_slope_conditional", "scarf_angle_threshold", "scarf_joint_speed", "scarf_joint_flow_ratio", "seam_slope_start_height", "seam_slope_entire_loop", "seam_slope_min_length", "seam_slope_steps", "seam_slope_inner_walls", "scarf_overhang_threshold",
|
||||
"interlocking_beam", "interlocking_orientation", "interlocking_beam_layer_count", "interlocking_depth", "interlocking_boundary_avoidance", "interlocking_beam_width","calib_flowrate_topinfill_special_order",
|
||||
// iXex parallel print mode (per-print selection)
|
||||
"ixex_parallel_mode",
|
||||
};
|
||||
|
||||
static std::vector<std::string> s_Preset_filament_options {/*"filament_colour", */ "default_filament_colour", "required_nozzle_HRC", "filament_diameter", "pellet_flow_coefficient", "volumetric_speed_coefficients", "filament_type",
|
||||
@@ -1031,7 +1039,11 @@ static std::vector<std::string> s_Preset_printer_options {
|
||||
"cooling_tube_length", "high_current_on_filament_swap", "parking_pos_retraction", "extra_loading_move", "wipe_tower_type", "purge_in_prime_tower", "enable_filament_ramming",
|
||||
"z_offset",
|
||||
"disable_m73", "preferred_orientation", "emit_machine_limits_to_gcode", "pellet_modded_printer", "support_multi_bed_types", "default_bed_type", "bed_mesh_min","bed_mesh_max","bed_mesh_probe_distance", "adaptive_bed_mesh_margin", "enable_long_retraction_when_cut","long_retractions_when_cut","retraction_distances_when_cut",
|
||||
"bed_temperature_formula", "nozzle_flush_dataset"
|
||||
"bed_temperature_formula", "nozzle_flush_dataset",
|
||||
// iXex (independent X extruder) — printer capability and user-defined modes
|
||||
"is_ixex", "ixex_gantry_count", "ixex_tools_per_gantry", "ixex_tool_layout",
|
||||
"ixex_carriage_width_x", "ixex_carriage_width_y", "ixex_carriage_margin", "ixex_viz_theme",
|
||||
"ixex_mode_names", "ixex_mode_active_tools", "ixex_mode_gcodes"
|
||||
};
|
||||
|
||||
static std::vector<std::string> s_Preset_sla_print_options {
|
||||
@@ -1444,6 +1456,12 @@ Preset* PresetCollection::get_preset_differed_for_save(Preset& preset)
|
||||
ConfigOptionVectorBase* opt_vec_inherit = static_cast<ConfigOptionVectorBase*>(parent_preset->config.option(option));
|
||||
if (opt_vec_src->size() == 1)
|
||||
opt_dst->set(opt_src);
|
||||
else if (opt_vec_src->size() != opt_vec_inherit->size()) {
|
||||
// Size mismatch (e.g. new multi-extruder profile inheriting from a
|
||||
// single-extruder base): nil-delta encoding requires matching sizes,
|
||||
// so fall back to storing the full vector.
|
||||
opt_dst->set(opt_src);
|
||||
}
|
||||
else if (key_set1->find(option) != key_set1->end()) {
|
||||
opt_vec_dst->set_with_nil(opt_vec_src, opt_vec_inherit, 1);
|
||||
}
|
||||
|
||||
@@ -193,6 +193,7 @@ static t_config_enum_values s_keys_map_WipeTowerType {
|
||||
};
|
||||
CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(WipeTowerType)
|
||||
|
||||
|
||||
static t_config_enum_values s_keys_map_FuzzySkinMode {
|
||||
{ "displacement", int(FuzzySkinMode::Displacement) },
|
||||
{ "extrusion", int(FuzzySkinMode::Extrusion) },
|
||||
@@ -5482,6 +5483,108 @@ void PrintConfigDef::init_fff_params()
|
||||
def->mode = comAdvanced;
|
||||
def->set_default_value(new ConfigOptionBool(true));
|
||||
|
||||
// iXex (independent X extruder) — parallel printing support for IDEX/IQEX printers
|
||||
def = this->add("is_ixex", coBool);
|
||||
def->label = L("iXex Printer");
|
||||
def->tooltip = L("Enable iXex (independent X extruder) support for IDEX/IQEX printers with multiple independent carriages.");
|
||||
def->mode = comAdvanced;
|
||||
def->set_default_value(new ConfigOptionBool(false));
|
||||
|
||||
def = this->add("ixex_gantry_count", coInt);
|
||||
def->label = L("Gantry Count");
|
||||
def->tooltip = L("Number of independent Y-axis gantries. 1 for IDEX/single-rail IQEX. 2 for dual-gantry systems (divides bed into rows).");
|
||||
def->min = 1;
|
||||
def->max = 2;
|
||||
def->mode = comAdvanced;
|
||||
def->set_default_value(new ConfigOptionInt(1));
|
||||
|
||||
def = this->add("ixex_tools_per_gantry", coInt);
|
||||
def->label = L("Tools per Gantry");
|
||||
def->tooltip = L("Number of independent toolheads per gantry along X. 2 for IDEX-style.");
|
||||
def->min = 1;
|
||||
def->max = 2;
|
||||
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. "
|
||||
"Determines how tool indices map to bed zones. "
|
||||
"front = lower Y (near the operator), rear = higher Y (back of machine).");
|
||||
def->mode = comAdvanced;
|
||||
def->set_default_value(new ConfigOptionString("front-left"));
|
||||
|
||||
def = this->add("ixex_carriage_width_x", coFloat);
|
||||
def->label = L("Carriage Width X");
|
||||
def->tooltip = L("Toolhead footprint in the X direction (mm). Used to calculate usable bed zones and detect carriage collisions.");
|
||||
def->sidetext = L("mm");
|
||||
def->min = 0;
|
||||
def->mode = comAdvanced;
|
||||
def->set_default_value(new ConfigOptionFloat(30.0));
|
||||
|
||||
def = this->add("ixex_carriage_width_y", coFloat);
|
||||
def->label = L("Carriage Width Y");
|
||||
def->tooltip = L("Toolhead footprint in the Y direction (mm). Used for collision detection.");
|
||||
def->sidetext = L("mm");
|
||||
def->min = 0;
|
||||
def->mode = comAdvanced;
|
||||
def->set_default_value(new ConfigOptionFloat(30.0));
|
||||
|
||||
def = this->add("ixex_carriage_margin", coFloat);
|
||||
def->label = L("Safety Margin");
|
||||
def->tooltip = L("Non-blocking advisory clearance strip (mm) drawn inside the primary zone at each carriage boundary. Parts placed within this strip will still slice; it is a visual reminder to leave extra clearance near the zone edge.");
|
||||
def->sidetext = L("mm");
|
||||
def->min = 0;
|
||||
def->max = 200;
|
||||
def->mode = comAdvanced;
|
||||
def->set_default_value(new ConfigOptionFloat(0.0));
|
||||
|
||||
def = this->add("ixex_viz_theme", coString);
|
||||
def->label = L("Visualization Theme");
|
||||
def->tooltip = L("Color theme for iXex bed zone visualization. Choose a colorblind-friendly theme if needed.");
|
||||
def->mode = comAdvanced;
|
||||
def->set_default_value(new ConfigOptionString("standard"));
|
||||
|
||||
def = this->add("ixex_parallel_mode", coString);
|
||||
def->label = L("iXex Print Mode");
|
||||
def->tooltip = L("Name of the active iXex parallel print mode, or \"primary\" for single-carriage printing. Requires iXex Printer enabled in the Printer preset (Printer \u2192 iXex tab).");
|
||||
def->mode = comAdvanced;
|
||||
def->set_default_value(new ConfigOptionString("primary"));
|
||||
|
||||
def = this->add("ixex_mode_names", coStrings);
|
||||
def->label = L("iXex Mode Names");
|
||||
def->tooltip = L("Display names for each user-defined iXex parallel print mode.");
|
||||
def->mode = comAdvanced;
|
||||
def->set_default_value(new ConfigOptionStrings());
|
||||
|
||||
def = this->add("ixex_mode_active_tools", coStrings);
|
||||
def->label = L("iXex Mode Active Tools");
|
||||
def->tooltip = L("Tool role assignments for each mode. Format: \"idx:P,idx:C,idx:M\" where P=Primary, C=Copy, M=Mirror (e.g. \"0:P,1:C,2:M,3:M\"). Managed by the iXex Modes editor in the Printer preset.");
|
||||
def->mode = comAdvanced;
|
||||
def->set_default_value(new ConfigOptionStrings());
|
||||
|
||||
def = this->add("ixex_mode_gcodes", coStrings);
|
||||
def->label = L("iXex Mode G-codes");
|
||||
def->tooltip = L("G-code or macro call to inject at print start for each mode.");
|
||||
def->mode = comAdvanced;
|
||||
def->set_default_value(new ConfigOptionStrings());
|
||||
|
||||
def = this->add("manual_filament_change", coBool);
|
||||
def->label = L("Manual Filament Change");
|
||||
def->tooltip = L("Enable this option to omit the custom Change filament G-code only at the beginning of the print. "
|
||||
|
||||
@@ -63,6 +63,7 @@ enum class WipeTowerType {
|
||||
Type2,
|
||||
};
|
||||
|
||||
|
||||
enum PrintHostType {
|
||||
htPrusaLink, htPrusaConnect, htOctoPrint, htDuet, htFlashAir, htAstroBox, htRepetier, htMKS, htESP3D, htCrealityPrint, htObico, htFlashforge, htSimplyPrint, htElegooLink
|
||||
};
|
||||
@@ -496,6 +497,7 @@ CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(GCodeFlavor)
|
||||
CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(FuzzySkinType)
|
||||
CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(FuzzySkinMode)
|
||||
CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(WipeTowerType)
|
||||
|
||||
CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(NoiseType)
|
||||
CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(InfillPattern)
|
||||
CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(IroningType)
|
||||
@@ -1035,6 +1037,9 @@ PRINT_CONFIG_CLASS_DEFINE(
|
||||
|
||||
// Orca: internal use only
|
||||
((ConfigOptionBool, calib_flowrate_topinfill_special_order)) // ORCA: special flag for flow rate calibration
|
||||
|
||||
// iXex parallel print mode (per-print selection, stores mode name or "primary")
|
||||
((ConfigOptionString, ixex_parallel_mode))
|
||||
)
|
||||
|
||||
// This object is mapped to Perl as Slic3r::Config::PrintRegion.
|
||||
@@ -1332,6 +1337,17 @@ PRINT_CONFIG_CLASS_DEFINE(
|
||||
((ConfigOptionStrings, filament_start_gcode))
|
||||
((ConfigOptionBool, single_extruder_multi_material))
|
||||
((ConfigOptionBool, manual_filament_change))
|
||||
// iXex (independent X extruder) — parallel printing support for IDEX/IQEX printers
|
||||
((ConfigOptionBool, is_ixex))
|
||||
((ConfigOptionInt, ixex_gantry_count))
|
||||
((ConfigOptionInt, ixex_tools_per_gantry))
|
||||
((ConfigOptionInt, ixex_primary_col))
|
||||
((ConfigOptionInt, ixex_primary_row))
|
||||
((ConfigOptionFloat, ixex_carriage_width_x))
|
||||
((ConfigOptionFloat, ixex_carriage_width_y))
|
||||
((ConfigOptionStrings, ixex_mode_names))
|
||||
((ConfigOptionStrings, ixex_mode_active_tools))
|
||||
((ConfigOptionStrings, ixex_mode_gcodes))
|
||||
((ConfigOptionBool, single_extruder_multi_material_priming))
|
||||
((ConfigOptionBool, wipe_tower_no_sparse_layers))
|
||||
((ConfigOptionString, change_filament_gcode))
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include "libslic3r/ClipperUtils.hpp"
|
||||
#include "libslic3r/Print.hpp"
|
||||
#include "libslic3r/Geometry.hpp"
|
||||
#include "libslic3r/TriangleMesh.hpp"
|
||||
#include "libslic3r/Model.hpp"
|
||||
#include "libslic3r/Utils.hpp"
|
||||
#include "libslic3r/LocalesUtils.hpp"
|
||||
@@ -43,6 +44,7 @@
|
||||
|
||||
#include <array>
|
||||
#include <algorithm>
|
||||
#include <sstream>
|
||||
#include <chrono>
|
||||
|
||||
|
||||
@@ -911,6 +913,9 @@ void GCodeViewer::SequentialView::render(const bool has_render_path, float legen
|
||||
// marker.set_world_offset(current_offset);
|
||||
marker.render(canvas_width, canvas_height, view_type);
|
||||
marker.render_position_window(viewer, canvas_width, canvas_height, view_type);
|
||||
// iXex secondary carriage markers
|
||||
for (auto& sec : m_ixex_secondary_markers)
|
||||
sec.render(canvas_width, canvas_height, view_type);
|
||||
}
|
||||
|
||||
//float bottom = wxGetApp().plater()->get_current_canvas3D()->get_canvas_size().get_height();
|
||||
@@ -965,6 +970,7 @@ void GCodeViewer::init(ConfigOptionMode mode, PresetBundle* preset_bundle)
|
||||
}
|
||||
}
|
||||
|
||||
m_marker_filename = filename;
|
||||
m_sequential_view.marker.init(filename);
|
||||
|
||||
// initializes point sizes
|
||||
@@ -1504,9 +1510,254 @@ void GCodeViewer::render(int canvas_width, int canvas_height, int right_margin)
|
||||
const libvgcode::PathVertex& curr_vertex = m_viewer.get_current_vertex();
|
||||
m_sequential_view.marker.set_world_position(libvgcode::convert(curr_vertex.position));
|
||||
m_sequential_view.marker.set_z_offset(m_z_offset + 0.5f);
|
||||
|
||||
// iXex: compute all carriage positions; update secondary nozzle markers;
|
||||
// carriage_box_draws is populated for toolhead footprint rendering after sequential_view.render().
|
||||
// Okabe-Ito colorblind-safe palette — excludes orange (#E69F00) and sky blue (#56B4E9)
|
||||
// which are used by the bed zone fills, ensuring the markers contrast against the background.
|
||||
static const std::array<ColorRGBA, 4> s_carriage_colors = {{
|
||||
{ 1.000f, 1.000f, 1.000f, 0.65f }, // primary — white
|
||||
{ 0.941f, 0.894f, 0.259f, 0.65f }, // secondary 1 — yellow (#F0E442)
|
||||
{ 0.835f, 0.369f, 0.000f, 0.65f }, // secondary 2 — vermilion (#D55E00)
|
||||
{ 0.800f, 0.475f, 0.655f, 0.65f }, // secondary 3 — reddish purple (#CC79A7)
|
||||
}};
|
||||
struct CarriageDraw { Vec3f pos; ColorRGBA color; };
|
||||
std::vector<CarriageDraw> carriage_box_draws;
|
||||
float ixex_box_wx = 0.0f, ixex_box_wy = 0.0f;
|
||||
{
|
||||
PresetBundle* preset_bundle = wxGetApp().preset_bundle;
|
||||
bool ixex_active = false;
|
||||
if (preset_bundle && m_sequential_view.m_show_marker) {
|
||||
const DynamicPrintConfig& printer_cfg = preset_bundle->printers.get_edited_preset().config;
|
||||
auto* is_ixex_opt = printer_cfg.opt<ConfigOptionBool>("is_ixex");
|
||||
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";
|
||||
|
||||
if (mode != m_ixex_last_mode) {
|
||||
m_sequential_view.m_ixex_secondary_markers.clear();
|
||||
m_ixex_last_mode = mode;
|
||||
}
|
||||
|
||||
if (!mode.empty() && mode != "primary") {
|
||||
auto* mode_names_opt = printer_cfg.opt<ConfigOptionStrings>("ixex_mode_names");
|
||||
auto* active_tools_opt = printer_cfg.opt<ConfigOptionStrings>("ixex_mode_active_tools");
|
||||
|
||||
auto* tpg_opt = printer_cfg.opt<ConfigOptionInt>("ixex_tools_per_gantry");
|
||||
auto* wx_opt = printer_cfg.opt<ConfigOptionFloat>("ixex_carriage_width_x");
|
||||
auto* wy_opt = printer_cfg.opt<ConfigOptionFloat>("ixex_carriage_width_y");
|
||||
int tools_per_gantry = tpg_opt ? std::max(1, tpg_opt->value) : 1;
|
||||
ixex_box_wx = wx_opt ? (float)wx_opt->value : 30.0f;
|
||||
ixex_box_wy = wy_opt ? (float)wy_opt->value : 30.0f;
|
||||
|
||||
// Parse "idx:P/C/M" format — matches PartPlate::calc_ixex_zones() exactly.
|
||||
// Primary = state 1 (P), Copy = 2 (C), Mirror = 3 (M).
|
||||
// Inactive tools (state 0) are not included in the stored string.
|
||||
int pri_tool = -1;
|
||||
std::vector<int> sec_tool_ids;
|
||||
std::map<int,int> sec_tool_states; // tool_id -> 2=Copy, 3=Mirror
|
||||
if (mode_names_opt && active_tools_opt) {
|
||||
for (size_t i = 0; i < mode_names_opt->values.size(); ++i) {
|
||||
if (i < active_tools_opt->values.size() && mode_names_opt->values[i] == mode) {
|
||||
std::istringstream ss(active_tools_opt->values[i]);
|
||||
std::string tok;
|
||||
while (std::getline(ss, tok, ',')) {
|
||||
tok.erase(std::remove_if(tok.begin(), tok.end(), ::isspace), tok.end());
|
||||
if (tok.empty()) continue;
|
||||
try {
|
||||
auto colon = tok.find(':');
|
||||
int idx = std::stoi(tok.substr(0, colon != std::string::npos ? colon : tok.size()));
|
||||
int state = 1; // default: primary
|
||||
if (colon != std::string::npos) {
|
||||
char role = std::toupper((unsigned char)tok[colon + 1]);
|
||||
if (role == 'C') state = 2;
|
||||
else if (role == 'M') state = 3;
|
||||
}
|
||||
if (state == 1) pri_tool = idx;
|
||||
else {
|
||||
sec_tool_ids.push_back(idx);
|
||||
sec_tool_states[idx] = state;
|
||||
}
|
||||
} catch (...) {}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
int sec_count = (int)sec_tool_ids.size();
|
||||
|
||||
if (sec_count > 0) {
|
||||
ixex_active = true;
|
||||
|
||||
// Lazy init secondary nozzle markers
|
||||
if ((int)m_sequential_view.m_ixex_secondary_markers.size() != sec_count) {
|
||||
m_sequential_view.m_ixex_secondary_markers.resize(sec_count);
|
||||
for (int i = 0; i < sec_count; ++i) {
|
||||
m_sequential_view.m_ixex_secondary_markers[i].init(m_marker_filename);
|
||||
m_sequential_view.m_ixex_secondary_markers[i].set_color(
|
||||
s_carriage_colors[(i + 1) % s_carriage_colors.size()]);
|
||||
}
|
||||
}
|
||||
|
||||
// Bed X and Y bounds — read from the current plate's shape, which is
|
||||
// in world/GL coordinates (same space as curr_vertex.position), and is
|
||||
// the exact same source used by PartPlate::calc_ixex_zones().
|
||||
float bed_x_min, bed_x_max, bed_y_min, bed_y_max;
|
||||
{
|
||||
PartPlate* curr_plate = wxGetApp().plater()->get_partplate_list().get_curr_plate();
|
||||
const Pointfs& plate_shape = curr_plate->get_shape();
|
||||
if (!plate_shape.empty()) {
|
||||
bed_x_min = (float)plate_shape[0].x();
|
||||
bed_x_max = bed_x_min;
|
||||
bed_y_min = (float)plate_shape[0].y();
|
||||
bed_y_max = bed_y_min;
|
||||
for (const auto& pt : plate_shape) {
|
||||
bed_x_min = std::min(bed_x_min, (float)pt.x());
|
||||
bed_x_max = std::max(bed_x_max, (float)pt.x());
|
||||
bed_y_min = std::min(bed_y_min, (float)pt.y());
|
||||
bed_y_max = std::max(bed_y_max, (float)pt.y());
|
||||
}
|
||||
} else {
|
||||
// Fallback: use toolpath bounding box extent
|
||||
bed_x_min = (float)m_paths_bounding_box.min.x();
|
||||
bed_x_max = (float)m_paths_bounding_box.max.x();
|
||||
bed_y_min = (float)m_paths_bounding_box.min.y();
|
||||
bed_y_max = (float)m_paths_bounding_box.max.y();
|
||||
}
|
||||
}
|
||||
|
||||
auto* gc_opt = printer_cfg.opt<ConfigOptionInt>("ixex_gantry_count");
|
||||
int gantry_count = gc_opt ? std::max(1, gc_opt->value) : 1;
|
||||
|
||||
// Apply the same flip logic as PartPlate::calc_ixex_zones() so physical
|
||||
// grid positions match the bed zone visualization.
|
||||
auto* layout_opt = printer_cfg.opt<ConfigOptionString>("ixex_tool_layout");
|
||||
const std::string layout_str = layout_opt ? layout_opt->value : "front-left";
|
||||
const bool flip_x = (layout_str == "front-right" || layout_str == "rear-right");
|
||||
const bool flip_y = (layout_str == "rear-left" || layout_str == "rear-right");
|
||||
|
||||
auto phys_col_of = [&](int tid) -> int {
|
||||
int raw = tid % tools_per_gantry;
|
||||
return flip_x ? (tools_per_gantry - 1 - raw) : raw;
|
||||
};
|
||||
auto phys_row_of = [&](int tid) -> int {
|
||||
int raw = tid / tools_per_gantry;
|
||||
return flip_y ? (gantry_count - 1 - raw) : raw;
|
||||
};
|
||||
|
||||
const Vec3f prim_pos = libvgcode::convert(curr_vertex.position);
|
||||
const float strip_width = (bed_x_max - bed_x_min) / (float)tools_per_gantry;
|
||||
const float row_strip_height = (bed_y_max - bed_y_min) / (float)gantry_count;
|
||||
const int pri_phys_col = (pri_tool >= 0) ? phys_col_of(pri_tool) : 0;
|
||||
const int pri_phys_row = (pri_tool >= 0) ? phys_row_of(pri_tool) : 0;
|
||||
|
||||
// Pre-pass: for each physical row, find the Copy reference column.
|
||||
// The primary row's reference is the primary itself.
|
||||
// Non-primary rows need a Copy tool; Mirror tools on that row
|
||||
// reflect that Copy's X position.
|
||||
std::map<int,int> row_copy_col;
|
||||
row_copy_col[pri_phys_row] = pri_phys_col;
|
||||
for (int i = 0; i < sec_count; ++i) {
|
||||
if (sec_tool_states[sec_tool_ids[i]] == 2) {
|
||||
row_copy_col[phys_row_of(sec_tool_ids[i])] =
|
||||
phys_col_of(sec_tool_ids[i]);
|
||||
}
|
||||
}
|
||||
|
||||
// Primary carriage box
|
||||
carriage_box_draws.push_back({ prim_pos, s_carriage_colors[0] });
|
||||
|
||||
const float pri_zone_x = bed_x_min + (float)pri_phys_col * strip_width;
|
||||
const float pri_zone_y = bed_y_min + (float)pri_phys_row * row_strip_height;
|
||||
const float rel_x = prim_pos.x() - pri_zone_x;
|
||||
const float rel_y = prim_pos.y() - pri_zone_y;
|
||||
|
||||
for (int i = 0; i < sec_count; ++i) {
|
||||
int sec_phys_col = phys_col_of(sec_tool_ids[i]);
|
||||
int sec_phys_row = phys_row_of(sec_tool_ids[i]);
|
||||
const int sec_state = sec_tool_states[sec_tool_ids[i]];
|
||||
|
||||
// Y: all tools in a row share a Y rail, so Y is always
|
||||
// zone-relative copy from the primary.
|
||||
float sec_zone_y = bed_y_min + (float)sec_phys_row * row_strip_height;
|
||||
float sec_y = sec_zone_y + rel_y;
|
||||
|
||||
// X: Copy places the tool at the same relative position within
|
||||
// its own zone. Mirror reflects the Copy reference on this row.
|
||||
float sec_x;
|
||||
if (sec_state == 2) {
|
||||
float sec_zone_x = bed_x_min + (float)sec_phys_col * strip_width;
|
||||
sec_x = sec_zone_x + rel_x;
|
||||
} else {
|
||||
// Mirror: find the Copy on the same row and reflect its X.
|
||||
auto ref_it = row_copy_col.find(sec_phys_row);
|
||||
if (ref_it != row_copy_col.end()) {
|
||||
float ref_zone_x = bed_x_min + (float)ref_it->second * strip_width;
|
||||
float ref_x = ref_zone_x + rel_x;
|
||||
sec_x = bed_x_min + bed_x_max - ref_x;
|
||||
} else {
|
||||
sec_x = bed_x_min + bed_x_max - prim_pos.x();
|
||||
}
|
||||
}
|
||||
|
||||
Vec3f sec_pos{ sec_x, sec_y, prim_pos.z() };
|
||||
m_sequential_view.m_ixex_secondary_markers[i].set_world_position(sec_pos);
|
||||
m_sequential_view.m_ixex_secondary_markers[i].set_z_offset(m_z_offset + 0.5f);
|
||||
carriage_box_draws.push_back({
|
||||
sec_pos, s_carriage_colors[(i + 1) % s_carriage_colors.size()] });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!ixex_active)
|
||||
m_sequential_view.m_ixex_secondary_markers.clear();
|
||||
}
|
||||
|
||||
// BBS fixed buttom margin. m_moves_slider.pos_y
|
||||
m_sequential_view.render(!m_no_render_path, legend_height, &m_viewer, m_viewer.get_current_vertex().gcode_id, canvas_width, canvas_height - bottom_margin * m_scale, right_margin * m_scale, m_viewer.get_view_type());
|
||||
|
||||
// iXex: render toolhead footprint boxes for each active carriage.
|
||||
// Each box is ixex_carriage_width_x × ixex_carriage_width_y, sitting above the nozzle tip.
|
||||
if (!carriage_box_draws.empty() && ixex_box_wx > 0.0f && ixex_box_wy > 0.0f) {
|
||||
// Rebuild box mesh if dimensions changed
|
||||
const Vec2f new_dims{ ixex_box_wx, ixex_box_wy };
|
||||
if (!m_ixex_toolhead_box.is_initialized() || new_dims != m_ixex_toolhead_box_dims) {
|
||||
m_ixex_toolhead_box_dims = new_dims;
|
||||
const float box_h = std::max(ixex_box_wx, ixex_box_wy); // height ~ largest horizontal dim
|
||||
indexed_triangle_set its = its_make_cube((double)ixex_box_wx, (double)ixex_box_wy, (double)box_h);
|
||||
// Center in X and Y; Z starts at 0 (nozzle tip level)
|
||||
for (auto& v : its.vertices)
|
||||
v += Vec3f(-ixex_box_wx * 0.5f, -ixex_box_wy * 0.5f, 0.0f);
|
||||
m_ixex_toolhead_box.init_from(its);
|
||||
}
|
||||
|
||||
GLShaderProgram* shader = wxGetApp().get_shader("gouraud_light");
|
||||
if (shader) {
|
||||
glsafe(::glEnable(GL_BLEND));
|
||||
glsafe(::glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA));
|
||||
shader->start_using();
|
||||
shader->set_uniform("emission_factor", 0.0f);
|
||||
const Camera& camera = wxGetApp().plater()->get_camera();
|
||||
shader->set_uniform("projection_matrix", camera.get_projection_matrix());
|
||||
const Transform3d& view_matrix = camera.get_view_matrix();
|
||||
for (const auto& draw : carriage_box_draws) {
|
||||
const Transform3d model_matrix = Geometry::translation_transform(
|
||||
(draw.pos + Vec3f(0.0f, 0.0f, m_z_offset)).cast<double>());
|
||||
shader->set_uniform("view_model_matrix", view_matrix * model_matrix);
|
||||
const Matrix3d view_normal_matrix =
|
||||
view_matrix.matrix().block(0, 0, 3, 3) *
|
||||
model_matrix.matrix().block(0, 0, 3, 3).inverse().transpose();
|
||||
shader->set_uniform("view_normal_matrix", view_normal_matrix);
|
||||
m_ixex_toolhead_box.set_color(draw.color);
|
||||
m_ixex_toolhead_box.render();
|
||||
}
|
||||
shader->stop_using();
|
||||
glsafe(::glDisable(GL_BLEND));
|
||||
}
|
||||
}
|
||||
|
||||
#if VGCODE_ENABLE_COG_AND_TOOL_MARKERS
|
||||
if (is_legend_shown()) {
|
||||
ImGuiWrapper& imgui = *Slic3r::GUI::wxGetApp().imgui();
|
||||
@@ -2706,6 +2957,7 @@ void GCodeViewer::render_all_plates_stats(const std::vector<const GCodeProcessor
|
||||
char buf[64];
|
||||
::sprintf(buf, "%.2f", total_cost_all_plates);
|
||||
imgui.text(buf);
|
||||
|
||||
}
|
||||
ImGui::End();
|
||||
ImGui::PopStyleColor(6);
|
||||
|
||||
@@ -101,6 +101,7 @@ public:
|
||||
|
||||
bool is_visible() const { return m_visible; }
|
||||
void set_visible(bool visible) { m_visible = visible; }
|
||||
void set_color(const ColorRGBA& color) { m_model.set_color(color); }
|
||||
|
||||
void render(int canvas_width, int canvas_height, const libvgcode::EViewType& view_type);
|
||||
void render_position_window(const libvgcode::Viewer* viewer, int canvas_width, int canvas_height, const libvgcode::EViewType& view_type);
|
||||
@@ -149,6 +150,7 @@ public:
|
||||
};
|
||||
|
||||
Marker marker;
|
||||
std::vector<Marker> m_ixex_secondary_markers; // one per active secondary carriage in iXex mode
|
||||
GCodeWindow gcode_window;
|
||||
float m_scale = 1.0;
|
||||
bool m_show_marker = false;
|
||||
@@ -202,6 +204,10 @@ private:
|
||||
|
||||
ConfigOptionMode m_user_mode;
|
||||
bool m_fold = {false};
|
||||
std::string m_marker_filename; // cached for lazy secondary marker init
|
||||
std::string m_ixex_last_mode; // detect mode changes for secondary marker rebuild
|
||||
GLModel m_ixex_toolhead_box; // shared box mesh for all carriage footprint overlays
|
||||
Vec2f m_ixex_toolhead_box_dims{ 0.0f, 0.0f }; // (width_x, width_y) — rebuild when changed
|
||||
|
||||
size_t m_extruders_count;
|
||||
std::vector<float> m_filament_diameters;
|
||||
|
||||
@@ -196,6 +196,11 @@ wxPoint OG_CustomCtrl::get_pos(const Line& line, Field* field_in/* = nullptr*/)
|
||||
if (line.widget) {
|
||||
#ifndef DISABLE_BLINKING
|
||||
h_pos += (line.has_undo_ui() ? 3 : 1) * blinking_button_width;
|
||||
#else
|
||||
// When blinking is disabled, widget lines still need to honour
|
||||
// option_label_at_right the same way regular lines do below.
|
||||
if (opt_group->option_label_at_right)
|
||||
add_buttons_width(blinking_button_width);
|
||||
#endif
|
||||
|
||||
for (auto child : line.widget_sizer->GetChildren())
|
||||
|
||||
+568
-45
@@ -3,6 +3,8 @@
|
||||
#include <numeric>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <sstream>
|
||||
#include <set>
|
||||
#include <regex>
|
||||
#include <future>
|
||||
#include <GL/glew.h>
|
||||
@@ -451,6 +453,495 @@ void PartPlate::calc_exclude_triangles(const ExPolygon &poly)
|
||||
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ":Unable to create exclude triangles\n";
|
||||
}
|
||||
|
||||
// Forward declaration (defined later in this file)
|
||||
static bool init_model_from_lines(GLModel &model, const Lines &lines, float z);
|
||||
|
||||
void PartPlate::calc_ixex_zones()
|
||||
{
|
||||
m_ixex_copy_zones.clear();
|
||||
m_ixex_mirror_zones.clear();
|
||||
m_ixex_zone_borders.reset();
|
||||
// Clear blocking vectors at the top so early returns don't leave stale data.
|
||||
m_ixex_secondary_zone_boxes.clear();
|
||||
m_ixex_collision_zones.clear();
|
||||
m_ixex_collision_overlay.clear();
|
||||
m_ixex_margin_overlay.clear();
|
||||
|
||||
if (!wxGetApp().preset_bundle)
|
||||
return;
|
||||
|
||||
const DynamicPrintConfig& printer_cfg = wxGetApp().preset_bundle->printers.get_edited_preset().config;
|
||||
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");
|
||||
std::string active_mode = mode_opt ? mode_opt->value : "primary";
|
||||
if (active_mode == "primary" || active_mode.empty())
|
||||
return;
|
||||
|
||||
// Grid dimensions and tool layout from printer config
|
||||
auto* gantry_opt = printer_cfg.option<ConfigOptionInt>("ixex_gantry_count");
|
||||
auto* tpg_opt = printer_cfg.option<ConfigOptionInt>("ixex_tools_per_gantry");
|
||||
auto* layout_opt = printer_cfg.option<ConfigOptionString>("ixex_tool_layout");
|
||||
|
||||
int n_cols = tpg_opt ? std::max(1, tpg_opt->value) : 2;
|
||||
int n_rows = gantry_opt ? std::max(1, gantry_opt->value) : 1;
|
||||
|
||||
// Layout: which corner is T0? 0=front-left, 1=front-right, 2=rear-left, 3=rear-right
|
||||
// flip_x: col 0 is right(max-X); flip_y: row 0 is rear(max-Y)
|
||||
std::string layout_str = layout_opt ? layout_opt->value : "front-left";
|
||||
bool flip_x = (layout_str == "front-right" || layout_str == "rear-right");
|
||||
bool flip_y = (layout_str == "rear-left" || layout_str == "rear-right");
|
||||
|
||||
// Convert tool index to physical (col=X-index, row=Y-index), col/row 0 = min-X/min-Y
|
||||
auto tool_to_phys = [&](int idx) -> std::pair<int,int> {
|
||||
int raw_col = idx % n_cols, raw_row = idx / n_cols;
|
||||
return { flip_x ? (n_cols - 1 - raw_col) : raw_col,
|
||||
flip_y ? (n_rows - 1 - raw_row) : raw_row };
|
||||
};
|
||||
|
||||
int pri_col = 0, pri_row = 0;
|
||||
|
||||
if (n_cols == 1 && n_rows == 1)
|
||||
return; // nothing to dim with a single zone
|
||||
|
||||
// Look up secondary tool indices (active in mode, but NOT the primary tool)
|
||||
auto* names_opt = printer_cfg.option<ConfigOptionStrings>("ixex_mode_names");
|
||||
auto* tools_opt = printer_cfg.option<ConfigOptionStrings>("ixex_mode_active_tools");
|
||||
std::string active_tools_str;
|
||||
if (names_opt && tools_opt) {
|
||||
for (size_t i = 0; i < names_opt->values.size(); ++i) {
|
||||
if (names_opt->values[i] == active_mode && i < tools_opt->values.size()) {
|
||||
active_tools_str = tools_opt->values[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Parse "idx:P/C/M" format → map<tool_idx, state> (1=Primary, 2=Copy, 3=Mirror)
|
||||
// Backwards compat: plain "idx" → Primary
|
||||
std::map<int,int> tool_states;
|
||||
{
|
||||
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, state = 1;
|
||||
if (colon != std::string::npos) {
|
||||
idx = std::stoi(token.substr(0, colon));
|
||||
char role = std::toupper((unsigned char)token[colon + 1]);
|
||||
if (role == 'C') state = 2;
|
||||
else if (role == 'M') state = 3;
|
||||
} else {
|
||||
idx = std::stoi(token);
|
||||
}
|
||||
if (idx >= 0 && idx < n_rows * n_cols)
|
||||
tool_states[idx] = state;
|
||||
} catch (...) {}
|
||||
}
|
||||
}
|
||||
|
||||
// Identify the Primary tool from the mode definition
|
||||
for (auto& [idx, state] : tool_states) {
|
||||
if (state == 1) { auto [c, r] = tool_to_phys(idx); pri_col = c; pri_row = r; break; }
|
||||
}
|
||||
|
||||
// Separate copy and mirror secondary cells using physical coordinates
|
||||
std::set<std::pair<int,int>> copy_cells, mirror_cells;
|
||||
for (auto& [idx, state] : tool_states) {
|
||||
auto [c, r] = tool_to_phys(idx);
|
||||
if (c == pri_col && r == pri_row) continue; // skip primary
|
||||
if (state == 2) copy_cells.insert({c, r});
|
||||
else if (state == 3) mirror_cells.insert({c, r});
|
||||
}
|
||||
|
||||
// All active secondary cells combined (for separation axis computation)
|
||||
std::set<std::pair<int,int>> all_secondary;
|
||||
for (auto& p : copy_cells) all_secondary.insert(p);
|
||||
for (auto& p : mirror_cells) all_secondary.insert(p);
|
||||
|
||||
auto bed_ext = get_extents(m_shape);
|
||||
double x_min = bed_ext.min(0), x_max = bed_ext.max(0);
|
||||
double y_min = bed_ext.min(1), y_max = bed_ext.max(1);
|
||||
double zone_w = (x_max - x_min) / n_cols;
|
||||
double zone_h = (y_max - y_min) / n_rows;
|
||||
|
||||
// Separation axes: row-sep = secondaries on a different gantry, col-sep = different column
|
||||
bool has_row_sep = false, has_col_sep = false;
|
||||
for (const auto& [sc, sr] : all_secondary) {
|
||||
if (sr != pri_row) has_row_sep = true;
|
||||
if (sc != pri_col) has_col_sep = true;
|
||||
}
|
||||
|
||||
// Primary zone extent (the clear printable area):
|
||||
// row-sep only → full bed width × primary row's Y band
|
||||
// col-sep only → primary col's X band × full bed height
|
||||
// both → primary quadrant
|
||||
auto in_primary_zone = [&](int col, int row) {
|
||||
bool row_ok = !has_row_sep || (row == pri_row);
|
||||
bool col_ok = !has_col_sep || (col == pri_col);
|
||||
return row_ok && col_ok;
|
||||
};
|
||||
|
||||
ExPolygon bed_poly;
|
||||
generate_print_polygon(bed_poly);
|
||||
|
||||
// Build a clipped filled GLModel for an expanded zone rect and push into a vector.
|
||||
auto push_zone_fill = [&](std::vector<GLModel>& vec, double cx0, double cx1, double cy0, double cy1) {
|
||||
ExPolygon cell;
|
||||
cell.contour.append({ scale_(cx0), scale_(cy0) });
|
||||
cell.contour.append({ scale_(cx1), scale_(cy0) });
|
||||
cell.contour.append({ scale_(cx1), scale_(cy1) });
|
||||
cell.contour.append({ scale_(cx0), scale_(cy1) });
|
||||
ExPolygons clipped = intersection_ex({ cell }, { bed_poly });
|
||||
if (!clipped.empty()) {
|
||||
GLModel m;
|
||||
if (init_model_from_poly(m, clipped.front(), GROUND_Z))
|
||||
vec.push_back(std::move(m));
|
||||
}
|
||||
};
|
||||
|
||||
// Build expanded zone rects for a set of cells.
|
||||
// When secondaries share only a row difference (same column as primary) → expand to full X width.
|
||||
// When secondaries share only a column difference → expand to full Y height.
|
||||
// When both axes differ → per-quadrant.
|
||||
struct BoxRect { double x0, x1, y0, y1; };
|
||||
auto make_boxes = [&](const std::set<std::pair<int,int>>& cells) -> std::vector<BoxRect> {
|
||||
std::vector<BoxRect> boxes;
|
||||
if (cells.empty()) return boxes;
|
||||
if (has_row_sep && !has_col_sep) {
|
||||
std::set<int> rows; for (auto& [c,r] : cells) rows.insert(r);
|
||||
for (int sr : rows)
|
||||
boxes.push_back({x_min, x_max, y_min + sr*zone_h, y_min + (sr+1)*zone_h});
|
||||
} else if (has_col_sep && !has_row_sep) {
|
||||
std::set<int> cols; for (auto& [c,r] : cells) cols.insert(c);
|
||||
for (int sc : cols)
|
||||
boxes.push_back({x_min + sc*zone_w, x_min + (sc+1)*zone_w, y_min, y_max});
|
||||
} else {
|
||||
for (auto& [sc,sr] : cells)
|
||||
boxes.push_back({x_min + sc*zone_w, x_min + (sc+1)*zone_w,
|
||||
y_min + sr*zone_h, y_min + (sr+1)*zone_h});
|
||||
}
|
||||
return boxes;
|
||||
};
|
||||
|
||||
// Build fills for copy and mirror zones using expanded boxes (inactive cells not rendered).
|
||||
auto push_expanded_fills = [&](std::vector<GLModel>& vec, const std::set<std::pair<int,int>>& cells) {
|
||||
for (const auto& b : make_boxes(cells))
|
||||
push_zone_fill(vec, b.x0, b.x1, b.y0, b.y1);
|
||||
};
|
||||
push_expanded_fills(m_ixex_copy_zones, copy_cells);
|
||||
push_expanded_fills(m_ixex_mirror_zones, mirror_cells);
|
||||
|
||||
// White borders: one outline per expanded box of each active secondary type.
|
||||
auto build_rect_lines = [](const std::vector<BoxRect>& boxes) -> Lines {
|
||||
Lines lines;
|
||||
for (const auto& b : boxes) {
|
||||
lines.emplace_back(Point(scale_(b.x0), scale_(b.y0)), Point(scale_(b.x1), scale_(b.y0)));
|
||||
lines.emplace_back(Point(scale_(b.x1), scale_(b.y0)), Point(scale_(b.x1), scale_(b.y1)));
|
||||
lines.emplace_back(Point(scale_(b.x1), scale_(b.y1)), Point(scale_(b.x0), scale_(b.y1)));
|
||||
lines.emplace_back(Point(scale_(b.x0), scale_(b.y1)), Point(scale_(b.x0), scale_(b.y0)));
|
||||
}
|
||||
return lines;
|
||||
};
|
||||
|
||||
// Combine copy+mirror for borders (each expands independently then we collect all lines)
|
||||
Lines all_border_lines = build_rect_lines(make_boxes(copy_cells));
|
||||
auto mirror_border_lines = build_rect_lines(make_boxes(mirror_cells));
|
||||
all_border_lines.insert(all_border_lines.end(), mirror_border_lines.begin(), mirror_border_lines.end());
|
||||
|
||||
if (!all_border_lines.empty())
|
||||
if (!init_model_from_lines(m_ixex_zone_borders, all_border_lines, GROUND_Z_GRIDLINE))
|
||||
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": unable to create iXex zone border lines\n";
|
||||
|
||||
// --- Secondary zone blocking ---
|
||||
// Collect the expanded bounding boxes for all secondary (copy+mirror) zones.
|
||||
// check_outside() uses these to prevent objects being placed outside the primary zone.
|
||||
for (const auto& b : make_boxes(copy_cells))
|
||||
m_ixex_secondary_zone_boxes.push_back(
|
||||
BoundingBoxf3(Vec3d(b.x0, b.y0, -1.0), Vec3d(b.x1, b.y1, 1e4)));
|
||||
for (const auto& b : make_boxes(mirror_cells))
|
||||
m_ixex_secondary_zone_boxes.push_back(
|
||||
BoundingBoxf3(Vec3d(b.x0, b.y0, -1.0), Vec3d(b.x1, b.y1, 1e4)));
|
||||
|
||||
// --- Carriage collision danger strips ---
|
||||
// Only add strips at boundaries of the PRIMARY zone — objects are only placed in the
|
||||
// primary zone, so secondary-to-secondary boundaries have no relevance.
|
||||
//
|
||||
// Strip width is carriage/2 on the primary side only:
|
||||
// right X boundary: [bnd_x - carriage_w/2, bnd_x]
|
||||
// left X boundary: [bnd_x, bnd_x + carriage_w/2]
|
||||
// top Y boundary: [bnd_y - carriage_h/2, bnd_y]
|
||||
// bottom Y boundary:[bnd_y, bnd_y + carriage_h/2]
|
||||
//
|
||||
// Strip length matches the primary zone extent (same expansion logic as make_boxes):
|
||||
// !has_row_sep → full bed height; has_row_sep → primary row only
|
||||
// !has_col_sep → full bed width; has_col_sep → primary column only
|
||||
|
||||
auto* cw_opt = printer_cfg.option<ConfigOptionFloat>("ixex_carriage_width_x");
|
||||
auto* ch_opt = printer_cfg.option<ConfigOptionFloat>("ixex_carriage_width_y");
|
||||
auto* mgn_opt = printer_cfg.option<ConfigOptionFloat>("ixex_carriage_margin");
|
||||
double carriage_w = cw_opt ? cw_opt->value : 0.0;
|
||||
double carriage_h = ch_opt ? ch_opt->value : 0.0;
|
||||
double margin = mgn_opt ? mgn_opt->value : 0.0;
|
||||
|
||||
// Helper: build a filled GLModel polygon and push into margin overlay (advisory, non-blocking)
|
||||
auto add_margin_fill = [&](double sx0, double sx1, double sy0, double sy1) {
|
||||
ExPolygon cell;
|
||||
cell.contour.append({ scale_(sx0), scale_(sy0) });
|
||||
cell.contour.append({ scale_(sx1), scale_(sy0) });
|
||||
cell.contour.append({ scale_(sx1), scale_(sy1) });
|
||||
cell.contour.append({ scale_(sx0), scale_(sy1) });
|
||||
ExPolygons clipped = intersection_ex({ cell }, { bed_poly });
|
||||
if (!clipped.empty()) {
|
||||
GLModel m;
|
||||
if (init_model_from_poly(m, clipped.front(), GROUND_Z))
|
||||
m_ixex_margin_overlay.push_back(std::move(m));
|
||||
}
|
||||
};
|
||||
|
||||
// Primary zone extent — mirrors in_primary_zone / make_boxes expansion logic
|
||||
double pz_x0 = has_col_sep ? x_min + pri_col * zone_w : x_min;
|
||||
double pz_x1 = has_col_sep ? x_min + (pri_col + 1) * zone_w : x_max;
|
||||
double pz_y0 = has_row_sep ? y_min + pri_row * zone_h : y_min;
|
||||
double pz_y1 = has_row_sep ? y_min + (pri_row + 1) * zone_h : y_max;
|
||||
|
||||
// Helper: build a BoundingBoxf3 strip, clip to bed, add to members
|
||||
auto add_strip = [&](double sx0, double sx1, double sy0, double sy1) {
|
||||
BoundingBoxf3 box;
|
||||
box.min = Vec3d(sx0, sy0, -1.0);
|
||||
box.max = Vec3d(sx1, sy1, 1e4);
|
||||
m_ixex_collision_zones.push_back(box);
|
||||
|
||||
ExPolygon cell;
|
||||
cell.contour.append({ scale_(sx0), scale_(sy0) });
|
||||
cell.contour.append({ scale_(sx1), scale_(sy0) });
|
||||
cell.contour.append({ scale_(sx1), scale_(sy1) });
|
||||
cell.contour.append({ scale_(sx0), scale_(sy1) });
|
||||
ExPolygons clipped = intersection_ex({ cell }, { bed_poly });
|
||||
if (!clipped.empty()) {
|
||||
GLModel m;
|
||||
if (init_model_from_poly(m, clipped.front(), GROUND_Z))
|
||||
m_ixex_collision_overlay.push_back(std::move(m));
|
||||
}
|
||||
};
|
||||
|
||||
// Determine which directions have mirror secondaries adjacent to the primary.
|
||||
// Only mirror tools can cause carriage collisions — they move toward each other.
|
||||
// Copy tools always move in the same direction, so no collision strip is needed.
|
||||
bool has_right_sec = false, has_left_sec = false;
|
||||
bool has_top_sec = false, has_bottom_sec = false;
|
||||
for (auto& [idx, state] : tool_states) {
|
||||
if (state != 3) continue; // only mirror tools (state==3) require collision strips
|
||||
auto [c, r] = tool_to_phys(idx);
|
||||
// X-boundary strips: any mirror in an adjacent column can cause an X collision.
|
||||
if (c == pri_col + 1) has_right_sec = true;
|
||||
if (c == pri_col - 1) has_left_sec = true;
|
||||
// Y-boundary strips: only a mirror DIRECTLY above/below (same column) causes a Y collision.
|
||||
// A mirror in a different column that happens to be in an adjacent row is only an X risk.
|
||||
if (r == pri_row + 1 && c == pri_col) has_top_sec = true;
|
||||
if (r == pri_row - 1 && c == pri_col) has_bottom_sec = true;
|
||||
}
|
||||
|
||||
// X-axis boundaries (vertical strips, width = carriage_w/2 on primary side)
|
||||
if (carriage_w > 0.0) {
|
||||
if (has_right_sec) {
|
||||
double bnd_x = x_min + (pri_col + 1) * zone_w;
|
||||
double strip_inner = bnd_x - carriage_w * 0.5;
|
||||
add_strip(strip_inner, bnd_x, pz_y0, pz_y1);
|
||||
if (margin > 0.0)
|
||||
add_margin_fill(strip_inner - margin, strip_inner, pz_y0, pz_y1);
|
||||
}
|
||||
if (has_left_sec) {
|
||||
double bnd_x = x_min + pri_col * zone_w;
|
||||
double strip_inner = bnd_x + carriage_w * 0.5;
|
||||
add_strip(bnd_x, strip_inner, pz_y0, pz_y1);
|
||||
if (margin > 0.0)
|
||||
add_margin_fill(strip_inner, strip_inner + margin, pz_y0, pz_y1);
|
||||
}
|
||||
}
|
||||
|
||||
// Y-axis boundaries (horizontal strips, width = carriage_h/2 on primary side)
|
||||
if (carriage_h > 0.0) {
|
||||
if (has_top_sec) {
|
||||
double bnd_y = y_min + (pri_row + 1) * zone_h;
|
||||
double strip_inner = bnd_y - carriage_h * 0.5;
|
||||
add_strip(pz_x0, pz_x1, strip_inner, bnd_y);
|
||||
if (margin > 0.0)
|
||||
add_margin_fill(pz_x0, pz_x1, strip_inner - margin, strip_inner);
|
||||
}
|
||||
if (has_bottom_sec) {
|
||||
double bnd_y = y_min + pri_row * zone_h;
|
||||
double strip_inner = bnd_y + carriage_h * 0.5;
|
||||
add_strip(pz_x0, pz_x1, bnd_y, strip_inner);
|
||||
if (margin > 0.0)
|
||||
add_margin_fill(pz_x0, pz_x1, strip_inner, strip_inner + margin);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Build a cache key from the current iXex config options, or "" if iXex is off.
|
||||
static std::string build_ixex_cache_key()
|
||||
{
|
||||
if (!wxGetApp().preset_bundle)
|
||||
return "";
|
||||
const DynamicPrintConfig& printer_cfg = wxGetApp().preset_bundle->printers.get_edited_preset().config;
|
||||
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");
|
||||
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_carriage_width_x");
|
||||
auto* ch_opt = printer_cfg.option<ConfigOptionFloat>("ixex_carriage_width_y");
|
||||
auto* mgn_opt = printer_cfg.option<ConfigOptionFloat>("ixex_carriage_margin");
|
||||
return (mode_opt ? mode_opt->value : "primary")
|
||||
+ "|" + 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)
|
||||
+ "|ch" + std::to_string(ch_opt ? (int)ch_opt->value : 0)
|
||||
+ "|mg" + std::to_string(mgn_opt ? (int)(mgn_opt->value * 10) : 0);
|
||||
}
|
||||
|
||||
// Ensure zone geometry (secondary boxes, collision strips, visual meshes) is up to date.
|
||||
// Called before any collision check AND before rendering so both paths share the same data.
|
||||
void PartPlate::ensure_ixex_zones()
|
||||
{
|
||||
std::string cache_key = build_ixex_cache_key();
|
||||
if (cache_key != m_ixex_zones_mode_cache) {
|
||||
m_ixex_zones_mode_cache = cache_key;
|
||||
calc_ixex_zones();
|
||||
}
|
||||
}
|
||||
|
||||
bool PartPlate::has_ixex_placement_violations()
|
||||
{
|
||||
ensure_ixex_zones();
|
||||
if (m_ixex_secondary_zone_boxes.empty() && m_ixex_collision_zones.empty())
|
||||
return false;
|
||||
for (const auto& pr : obj_to_instance_set) {
|
||||
int obj_id = pr.first;
|
||||
int instance_id = pr.second;
|
||||
if (!valid_instance(obj_id, instance_id))
|
||||
continue;
|
||||
ModelInstance* instance = m_model->objects[obj_id]->instances[instance_id];
|
||||
Polygon hull = instance->convex_hull_2d();
|
||||
if (hull.points.empty())
|
||||
continue;
|
||||
for (const auto& box : m_ixex_secondary_zone_boxes) {
|
||||
if (!intersection({box.polygon(true)}, {hull}).empty())
|
||||
return true;
|
||||
}
|
||||
for (const auto& strip : m_ixex_collision_zones) {
|
||||
if (!intersection({strip.polygon(true)}, {hull}).empty())
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void PartPlate::render_ixex_zones(bool force_default_color)
|
||||
{
|
||||
if (force_default_color)
|
||||
return;
|
||||
|
||||
ensure_ixex_zones();
|
||||
|
||||
// Read visualization theme from printer config.
|
||||
struct IXexTheme {
|
||||
ColorRGBA copy; // secondary copy zone fill
|
||||
ColorRGBA mirror; // secondary mirror zone fill
|
||||
ColorRGBA border; // zone outline
|
||||
ColorRGBA danger; // blocking collision strip
|
||||
ColorRGBA margin; // advisory safety margin
|
||||
};
|
||||
|
||||
// Standard (Okabe-Ito orange + sky blue)
|
||||
static const IXexTheme k_standard = {
|
||||
{ 0.337f, 0.706f, 0.914f, 0.45f }, // copy — sky blue #56B4E9
|
||||
{ 0.902f, 0.624f, 0.000f, 0.45f }, // mirror — orange #E69F00
|
||||
{ 1.000f, 1.000f, 1.000f, 0.90f }, // border — white
|
||||
{ 0.850f, 0.100f, 0.100f, 0.55f }, // danger — red
|
||||
{ 0.300f, 0.900f, 0.200f, 0.40f }, // margin — lime green
|
||||
};
|
||||
// Deuteranopia / Protanopia: avoids red-green confusion.
|
||||
// Danger strip uses strong blue-violet (red invisible to protanopes).
|
||||
static const IXexTheme k_deuteranopia = {
|
||||
{ 0.000f, 0.447f, 0.698f, 0.50f }, // copy — blue #0072B2
|
||||
{ 0.941f, 0.894f, 0.259f, 0.50f }, // mirror — yellow #F0E442
|
||||
{ 1.000f, 1.000f, 1.000f, 0.90f }, // border — white
|
||||
{ 0.200f, 0.100f, 0.800f, 0.65f }, // danger — blue-violet (red not visible)
|
||||
{ 0.800f, 0.475f, 0.655f, 0.45f }, // margin — reddish purple #CC79A7
|
||||
};
|
||||
// Tritanopia: avoids blue-yellow confusion.
|
||||
// Copy uses vermilion, mirror uses reddish-purple, margin uses teal.
|
||||
static const IXexTheme k_tritanopia = {
|
||||
{ 0.835f, 0.369f, 0.000f, 0.50f }, // copy — vermilion #D55E00
|
||||
{ 0.800f, 0.475f, 0.655f, 0.50f }, // mirror — reddish purple #CC79A7
|
||||
{ 1.000f, 1.000f, 1.000f, 0.90f }, // border — white
|
||||
{ 0.850f, 0.100f, 0.100f, 0.55f }, // danger — red (visible to tritanopes)
|
||||
{ 0.000f, 0.700f, 0.600f, 0.45f }, // margin — teal
|
||||
};
|
||||
// High contrast: saturated, higher alpha for low-vision users.
|
||||
static const IXexTheme k_high_contrast = {
|
||||
{ 0.000f, 0.600f, 1.000f, 0.65f }, // copy — vivid blue
|
||||
{ 1.000f, 0.600f, 0.000f, 0.65f }, // mirror — vivid orange
|
||||
{ 1.000f, 1.000f, 0.000f, 1.00f }, // border — bright yellow
|
||||
{ 1.000f, 0.000f, 0.000f, 0.75f }, // danger — full red
|
||||
{ 0.000f, 1.000f, 0.200f, 0.60f }, // margin — bright green
|
||||
};
|
||||
|
||||
const IXexTheme* theme = &k_standard;
|
||||
if (wxGetApp().preset_bundle) {
|
||||
const DynamicPrintConfig& pcfg = wxGetApp().preset_bundle->printers.get_edited_preset().config;
|
||||
if (auto* t = pcfg.option<ConfigOptionString>("ixex_viz_theme")) {
|
||||
if (t->value == "deuteranopia") theme = &k_deuteranopia;
|
||||
else if (t->value == "tritanopia") theme = &k_tritanopia;
|
||||
else if (t->value == "high_contrast") theme = &k_high_contrast;
|
||||
}
|
||||
}
|
||||
|
||||
// Both fills and border lines use the flat shader already active from render().
|
||||
glsafe(::glDepthMask(GL_FALSE));
|
||||
|
||||
if (!m_ixex_copy_zones.empty())
|
||||
for (GLModel& z : m_ixex_copy_zones)
|
||||
if (z.is_initialized()) { z.set_color(theme->copy); z.render(); }
|
||||
|
||||
if (!m_ixex_mirror_zones.empty())
|
||||
for (GLModel& z : m_ixex_mirror_zones)
|
||||
if (z.is_initialized()) { z.set_color(theme->mirror); z.render(); }
|
||||
|
||||
glsafe(::glDepthMask(GL_TRUE));
|
||||
|
||||
if (m_ixex_zone_borders.is_initialized()) {
|
||||
m_ixex_zone_borders.set_color(theme->border);
|
||||
glsafe(::glLineWidth(2.0f));
|
||||
m_ixex_zone_borders.render();
|
||||
glsafe(::glLineWidth(1.0f));
|
||||
}
|
||||
|
||||
if (!m_ixex_collision_overlay.empty()) {
|
||||
glsafe(::glDepthMask(GL_FALSE));
|
||||
for (GLModel& s : m_ixex_collision_overlay)
|
||||
if (s.is_initialized()) { s.set_color(theme->danger); s.render(); }
|
||||
glsafe(::glDepthMask(GL_TRUE));
|
||||
}
|
||||
|
||||
if (!m_ixex_margin_overlay.empty()) {
|
||||
glsafe(::glDepthMask(GL_FALSE));
|
||||
for (GLModel& b : m_ixex_margin_overlay)
|
||||
if (b.is_initialized()) { b.set_color(theme->margin); b.render(); }
|
||||
glsafe(::glDepthMask(GL_TRUE));
|
||||
}
|
||||
}
|
||||
|
||||
void PartPlate::calc_triangles_from_polygon(const ExPolygon &poly, GLModel &render_model){
|
||||
if (poly.empty()) {
|
||||
render_model.reset();
|
||||
@@ -1882,7 +2373,7 @@ bool PartPlate::check_mixture_of_pla_and_petg(const DynamicPrintConfig &config)
|
||||
if (filament_type == "PETG")
|
||||
has_petg = true;
|
||||
} else {
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " check error:array bound";
|
||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << " check error:array bound";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2147,7 +2638,7 @@ arrangement::ArrangePolygon PartPlate::estimate_wipe_tower_polygon(const Dynamic
|
||||
if (wipe_tower_brim_width_opt) {
|
||||
wp_brim_width = wipe_tower_brim_width_opt->getFloat();
|
||||
if (wp_brim_width < 0) wp_brim_width = WipeTower::get_auto_brim_by_height((float) wt_size.z());
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format("arrange wipe_tower: wp_brim_width %1%") % wp_brim_width;
|
||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format("arrange wipe_tower: wp_brim_width %1%") % wp_brim_width;
|
||||
}
|
||||
|
||||
x = std::clamp(x, margin, (float)plate_width - w - margin - wp_brim_width);
|
||||
@@ -2211,9 +2702,9 @@ void PartPlate::set_pos_and_size(Vec3d& origin, int width, int depth, int height
|
||||
bool size_changed = false; //size changed means the machine changed
|
||||
bool pos_changed = false;
|
||||
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": plate_id %1%, before, origin {%2%,%3%,%4%}, plate_width %5%, plate_depth %6%, plate_height %7%")\
|
||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": plate_id %1%, before, origin {%2%,%3%,%4%}, plate_width %5%, plate_depth %6%, plate_height %7%")\
|
||||
% m_plate_index % m_origin.x() % m_origin.y() % m_origin.z() % m_width % m_depth % m_height;
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": with_instance_move %1%, after, origin {%2%,%3%,%4%}, plate_width %5%, plate_depth %6%, plate_height %7%")\
|
||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": with_instance_move %1%, after, origin {%2%,%3%,%4%}, plate_width %5%, plate_depth %6%, plate_height %7%")\
|
||||
% with_instance_move % origin.x() % origin.y() % origin.z() % width % depth % height;
|
||||
size_changed = ((width != m_width) || (depth != m_depth) || (height != m_height));
|
||||
pos_changed = (m_origin != origin);
|
||||
@@ -2253,7 +2744,7 @@ void PartPlate::set_pos_and_size(Vec3d& origin, int width, int depth, int height
|
||||
offset.x() = offset.x() + off_x;
|
||||
offset.y() = offset.y() + off_y;
|
||||
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": object %1%, instance %2%, moved {%3%,%4%} to {%5%, %6%}")\
|
||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": object %1%, instance %2%, moved {%3%,%4%} to {%5%, %6%}")\
|
||||
% obj_id % instance_id % off_x % off_y % offset.x() % offset.y();
|
||||
|
||||
instance->set_offset(offset);
|
||||
@@ -2390,7 +2881,7 @@ bool PartPlate::is_valid_gcode_file()
|
||||
return false;
|
||||
boost::filesystem::path gcode_file(m_gcode_result->filename);
|
||||
if (!boost::filesystem::exists(gcode_file)) {
|
||||
BOOST_LOG_TRIVIAL(info) << "invalid gcode file, file is missing, file = " << m_gcode_result->filename;
|
||||
BOOST_LOG_TRIVIAL(warning) << "invalid gcode file, file is missing, file = " << m_gcode_result->filename;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
@@ -2469,6 +2960,9 @@ bool PartPlate::contain_instance_totally(int obj_id, int instance_id) const
|
||||
//check whether instance is outside the plate or not
|
||||
bool PartPlate::check_outside(int obj_id, int instance_id, BoundingBoxf3* bounding_box)
|
||||
{
|
||||
// Ensure iXex zone geometry is current before any placement check.
|
||||
ensure_ixex_zones();
|
||||
|
||||
bool outside = true;
|
||||
|
||||
ModelObject* object = m_model->objects[obj_id];
|
||||
@@ -2512,6 +3006,32 @@ bool PartPlate::check_outside(int obj_id, int instance_id, BoundingBoxf3* boundi
|
||||
outside = false;
|
||||
}
|
||||
|
||||
// iXex placement check (prepare-mode bounding-box test).
|
||||
// Block objects that overlap secondary (copy/mirror) zones or the carriage
|
||||
// danger strip at the primary zone boundary. Reuses the existing
|
||||
// outside=true → instance_outside_set → update_states() → blocks slicing path.
|
||||
if (!outside && (!m_ixex_secondary_zone_boxes.empty() || !m_ixex_collision_zones.empty())) {
|
||||
Polygon obj_hull = instance->convex_hull_2d(); // scaled Clipper coords
|
||||
// 1. Object must not touch any secondary zone.
|
||||
for (const auto& box : m_ixex_secondary_zone_boxes) {
|
||||
Polygon p = box.polygon(true);
|
||||
if (!intersection({ p }, { obj_hull }).empty()) {
|
||||
outside = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// 2. Object must not enter the carriage danger strip inside the primary zone.
|
||||
if (!outside) {
|
||||
for (const auto& strip : m_ixex_collision_zones) {
|
||||
Polygon strip_poly = strip.polygon(true);
|
||||
if (!intersection({ strip_poly }, { obj_hull }).empty()) {
|
||||
outside = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return outside;
|
||||
}
|
||||
|
||||
@@ -2602,7 +3122,7 @@ int PartPlate::add_instance(int obj_id, int instance_id, bool move_position, Bou
|
||||
m_ready_for_slice = true;
|
||||
}
|
||||
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": plate %1% , m_ready_for_slice changes to %2%") % m_plate_index %m_ready_for_slice;
|
||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": plate %1% , m_ready_for_slice changes to %2%") % m_plate_index %m_ready_for_slice;
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -2696,7 +3216,7 @@ void PartPlate::duplicate_all_instance(unsigned int dup_count, bool need_skip, s
|
||||
if (skip_objects.find(instance->loaded_id) != skip_objects.end())
|
||||
{
|
||||
instance->printable = false;
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": skipped object, loaded_id %1%, name %2%, set to unprintable, no need to duplicate") % instance->loaded_id % object->name;
|
||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": skipped object, loaded_id %1%, name %2%, set to unprintable, no need to duplicate") % instance->loaded_id % object->name;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -2708,7 +3228,7 @@ void PartPlate::duplicate_all_instance(unsigned int dup_count, bool need_skip, s
|
||||
for ( size_t new_instance_id = 0; new_instance_id < newObj->instances.size(); new_instance_id++ )
|
||||
{
|
||||
obj_to_instance_set.emplace(std::pair(new_obj_id, new_instance_id));
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": duplicate object into plate: index_pair [%1%,%2%], obj_id %3%") % new_obj_id % new_instance_id % newObj->id().id;
|
||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": duplicate object into plate: index_pair [%1%,%2%], obj_id %3%") % new_obj_id % new_instance_id % newObj->id().id;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2731,10 +3251,10 @@ void PartPlate::duplicate_all_instance(unsigned int dup_count, bool need_skip, s
|
||||
while (skip_objects.find(instance->loaded_id) != skip_objects.end())
|
||||
{
|
||||
instance->loaded_id ++;
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": duplicated id %1% with skip, try new one %2%") %instance->id().id % instance->loaded_id;
|
||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": duplicated id %1% with skip, try new one %2%") %instance->id().id % instance->loaded_id;
|
||||
}
|
||||
}
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": set obj %1% instance %2%'s loaded_id to its id %3%, name %4%") % obj_id %instance_id %instance->loaded_id % object->name;
|
||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": set obj %1% instance %2%'s loaded_id to its id %3%, name %4%") % obj_id %instance_id %instance->loaded_id % object->name;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3056,7 +3576,7 @@ bool PartPlate::set_shape(const Pointfs& shape, const Pointfs& exclude_areas, co
|
||||
|
||||
if ((m_shape == new_shape)&&(m_exclude_area == new_exclude_areas)
|
||||
&&(m_height_to_lid == height_to_lid)&&(m_height_to_rod == height_to_rod)) {
|
||||
BOOST_LOG_TRIVIAL(info) << "PartPlate same shape, skip directly";
|
||||
BOOST_LOG_TRIVIAL(warning) << "PartPlate same shape, skip directly";
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -3101,6 +3621,8 @@ bool PartPlate::set_shape(const Pointfs& shape, const Pointfs& exclude_areas, co
|
||||
|
||||
const BoundingBox& pp_bbox = poly.contour.bounding_box();
|
||||
calc_gridlines(poly, pp_bbox);
|
||||
m_ixex_zones_mode_cache = "\x01"; // force rebuild on next render
|
||||
calc_ixex_zones();
|
||||
|
||||
calc_vertex_for_icons(0, m_del_icon);
|
||||
calc_vertex_for_icons(1, m_orient_icon);
|
||||
@@ -3205,6 +3727,7 @@ void PartPlate::render(const Transform3d& view_matrix, const Transform3d& projec
|
||||
render_background(force_background_color);
|
||||
|
||||
render_exclude_area(force_background_color);
|
||||
render_ixex_zones(force_background_color);
|
||||
if(m_selected && wxGetApp().plater()->get_enable_wrapping_detection()){
|
||||
if(!m_wrapping_detection_triangles.is_initialized()){
|
||||
auto points = get_plate_wrapping_detection_area();
|
||||
@@ -3286,7 +3809,7 @@ void PartPlate::update_states()
|
||||
}
|
||||
}
|
||||
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": plate %1% , m_ready_for_slice changes to %2%") % m_plate_index %m_ready_for_slice;
|
||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": plate %1% , m_ready_for_slice changes to %2%") % m_plate_index %m_ready_for_slice;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -3294,7 +3817,7 @@ void PartPlate::update_states()
|
||||
//invalid sliced result
|
||||
void PartPlate::update_slice_result_valid_state(bool valid)
|
||||
{
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": plate %1% , update slice result from %2% to %3%") % m_plate_index %m_slice_result_valid %valid;
|
||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": plate %1% , update slice result from %2% to %3%") % m_plate_index %m_slice_result_valid %valid;
|
||||
m_slice_result_valid = valid;
|
||||
if (valid)
|
||||
m_slice_percent = 100.0f;
|
||||
@@ -3366,13 +3889,13 @@ int PartPlate::load_gcode_from_file(const std::string& filename)
|
||||
// BBS: use backup path to save temp gcode
|
||||
// auto path = get_tmp_gcode_path();
|
||||
// if (boost::filesystem::exists(boost::filesystem::path(path))) {
|
||||
// BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": file %1% exists, delete it firstly") % filename.c_str();
|
||||
// BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": file %1% exists, delete it firstly") % filename.c_str();
|
||||
// boost::nowide::remove(path.c_str());
|
||||
//}
|
||||
|
||||
// std::error_code error = rename_file(filename, path);
|
||||
// if (error) {
|
||||
// BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format("Failed to rename the output G-code file from %1% to %2%, error code %3%") % filename.c_str() % path.c_str() %
|
||||
// BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format("Failed to rename the output G-code file from %1% to %2%, error code %3%") % filename.c_str() % path.c_str() %
|
||||
//error.message(); return -1;
|
||||
//}
|
||||
if (boost::filesystem::exists(filename)) {
|
||||
@@ -3383,7 +3906,7 @@ int PartPlate::load_gcode_from_file(const std::string& filename)
|
||||
|
||||
update_slice_result_valid_state(true);
|
||||
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": found valid gcode file %1%") % filename.c_str();
|
||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": found valid gcode file %1%") % filename.c_str();
|
||||
}
|
||||
else {
|
||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": can not find gcode file %1%") % filename.c_str();
|
||||
@@ -3714,7 +4237,7 @@ PartPlateList::PartPlateList(int width, int depth, int height, Plater* platerObj
|
||||
:m_plate_width(width), m_plate_depth(depth), m_plate_height(height), m_plater(platerObj), m_model(modelObj), printer_technology(tech),
|
||||
unprintable_plate(this, Vec3d(0.0 + width * (1. + LOGICAL_PART_PLATE_GAP), 0.0, 0.0), width, depth, height, platerObj, modelObj, false, tech)
|
||||
{
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(":plate_width %1%, plate_depth %2%, plate_height %3%") % width % depth % height;
|
||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(":plate_width %1%, plate_depth %2%, plate_height %3%") % width % depth % height;
|
||||
|
||||
init();
|
||||
}
|
||||
@@ -4135,8 +4658,8 @@ void PartPlateList::reset_size(int width, int depth, int height, bool reload_obj
|
||||
{
|
||||
Vec3d origin1, origin2;
|
||||
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(":before size: plate_width %1%, plate_depth %2%, plate_height %3%") % m_plate_width % m_plate_depth % m_plate_height;
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(":after size: plate_width %1%, plate_depth %2%, plate_height %3%") % width % depth % height;
|
||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(":before size: plate_width %1%, plate_depth %2%, plate_height %3%") % m_plate_width % m_plate_depth % m_plate_height;
|
||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(":after size: plate_width %1%, plate_depth %2%, plate_height %3%") % width % depth % height;
|
||||
if ((m_plate_width != width) || (m_plate_depth != depth) || (m_plate_height != height))
|
||||
{
|
||||
m_plate_width = width;
|
||||
@@ -4352,7 +4875,7 @@ int PartPlateList::duplicate_plate(int index)
|
||||
// go over the instances and pair with the object
|
||||
for (size_t new_instance_id = 0; new_instance_id < object_copy->instances.size(); new_instance_id++){
|
||||
new_plate->obj_to_instance_set.emplace(std::pair(new_obj_id, new_instance_id));
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": duplicate object into plate: index_pair [%1%,%2%], obj_id %3%") % new_obj_id % new_instance_id % object_copy->id().id;
|
||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": duplicate object into plate: index_pair [%1%,%2%], obj_id %3%") % new_obj_id % new_instance_id % object_copy->id().id;
|
||||
}
|
||||
}
|
||||
new_plate->translate_all_instance(plate_to_plate_offset);
|
||||
@@ -4715,7 +5238,7 @@ void PartPlateList::update_plate_cols()
|
||||
m_plate_count = m_plate_list.size();
|
||||
|
||||
m_plate_cols = compute_colum_count(m_plate_count);
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(":m_plate_count %1%, m_plate_cols change to %2%") % m_plate_count % m_plate_cols;
|
||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(":m_plate_count %1%, m_plate_cols change to %2%") % m_plate_count % m_plate_cols;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -4796,7 +5319,7 @@ int PartPlateList::lock_plate(int index, bool state)
|
||||
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(":can not get plate for index %1%, size %2%") % index % m_plate_list.size();
|
||||
return -1;
|
||||
}
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(":lock plate %1%, to state %2%") % index % state;
|
||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(":lock plate %1%, to state %2%") % index % state;
|
||||
|
||||
plate->lock(state);
|
||||
|
||||
@@ -5714,7 +6237,7 @@ Print& PartPlateList::get_current_fff_print() const
|
||||
Print* print;
|
||||
|
||||
current_plate = m_plate_list[m_current_plate];
|
||||
//BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(":m_current_plate %1%, current_plate %2%") % m_current_plate % current_plate;
|
||||
//BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(":m_current_plate %1%, current_plate %2%") % m_current_plate % current_plate;
|
||||
assert(current_plate != NULL);
|
||||
|
||||
current_plate->get_print((PrintBase **)&print, nullptr, nullptr);
|
||||
@@ -5728,7 +6251,7 @@ GCodeProcessorResult* PartPlateList::get_current_slice_result() const
|
||||
PartPlate* current_plate;
|
||||
|
||||
current_plate = m_plate_list[m_current_plate];
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(":m_current_plate %1%, current_plate %2%") % m_current_plate % current_plate;
|
||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(":m_current_plate %1%, current_plate %2%") % m_current_plate % current_plate;
|
||||
assert(current_plate != NULL);
|
||||
|
||||
return current_plate->get_slice_result();
|
||||
@@ -5933,7 +6456,7 @@ int PartPlateList::rebuild_plates_after_deserialize(std::vector<bool>& previous_
|
||||
//not used
|
||||
/*if (m_plate_width == 0)
|
||||
{
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": jump to the first init state, need to re-set size!");
|
||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": jump to the first init state, need to re-set size!");
|
||||
Vec3d max = m_plater->get_bed().get_bounding_box(false).max;
|
||||
Vec3d min = m_plater->get_bed().get_bounding_box(false).min;
|
||||
double z = m_plater->config()->opt_float("printable_height");
|
||||
@@ -5947,7 +6470,7 @@ int PartPlateList::rebuild_plates_after_arrangement(bool recycle_plates, bool ex
|
||||
{
|
||||
int ret = 0;
|
||||
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(":before rebuild, plates count %1%, recycle_plates %2%") % m_plate_list.size() % recycle_plates;
|
||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(":before rebuild, plates count %1%, recycle_plates %2%") % m_plate_list.size() % recycle_plates;
|
||||
|
||||
// sort by arrange_order
|
||||
std::sort(m_model->objects.begin(), m_model->objects.end(), [](auto a, auto b) {return a->instances[0]->arrange_order < b->instances[0]->arrange_order; });
|
||||
@@ -5964,7 +6487,7 @@ int PartPlateList::rebuild_plates_after_arrangement(bool recycle_plates, bool ex
|
||||
|| !m_plate_list[i]->has_printable_instances())
|
||||
{
|
||||
//delete it
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(":delete plate %1% for empty") % i;
|
||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(":delete plate %1% for empty") % i;
|
||||
delete_plate(i);
|
||||
}
|
||||
else if (m_plate_list[i]->is_locked()) {
|
||||
@@ -5984,7 +6507,7 @@ int PartPlateList::rebuild_plates_after_arrangement(bool recycle_plates, bool ex
|
||||
}
|
||||
#endif
|
||||
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(":after rebuild, plates count %1%") % m_plate_list.size();
|
||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(":after rebuild, plates count %1%") % m_plate_list.size();
|
||||
return ret;
|
||||
}
|
||||
|
||||
@@ -6002,10 +6525,10 @@ int PartPlateList::store_to_3mf_structure(PlateDataPtrs& plate_data_list, bool w
|
||||
plate_data_item->locked = m_plate_list[i]->m_locked;
|
||||
plate_data_item->plate_index = m_plate_list[i]->m_plate_index;
|
||||
plate_data_item->plate_name = m_plate_list[i]->get_plate_name();
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": plate %1% before load, width %2%, height %3%, size %4%!")
|
||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": plate %1% before load, width %2%, height %3%, size %4%!")
|
||||
%(i+1) %m_plate_list[i]->thumbnail_data.width %m_plate_list[i]->thumbnail_data.height %m_plate_list[i]->thumbnail_data.pixels.size();
|
||||
plate_data_item->plate_thumbnail.load_from(m_plate_list[i]->thumbnail_data);
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": plate %1% after load, width %2%, height %3%, size %4%!")
|
||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": plate %1% after load, width %2%, height %3%, size %4%!")
|
||||
%(i+1) %plate_data_item->plate_thumbnail.width %plate_data_item->plate_thumbnail.height %plate_data_item->plate_thumbnail.pixels.size();
|
||||
plate_data_item->config.apply(*m_plate_list[i]->config());
|
||||
|
||||
@@ -6022,7 +6545,7 @@ int PartPlateList::store_to_3mf_structure(PlateDataPtrs& plate_data_list, bool w
|
||||
plate_data_item->objects_and_instances.emplace_back(it->first, it->second);
|
||||
}
|
||||
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ <<boost::format(": plate %1%, gcode_filename=%2%, with_slice_info=%3%, slice_valid %4%, object item count %5%.")
|
||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ <<boost::format(": plate %1%, gcode_filename=%2%, with_slice_info=%3%, slice_valid %4%, object item count %5%.")
|
||||
%i %m_plate_list[i]->m_gcode_result->filename % with_slice_info %m_plate_list[i]->is_slice_result_valid()%plate_data_item->objects_and_instances.size();
|
||||
|
||||
if (with_slice_info) {
|
||||
@@ -6055,12 +6578,12 @@ int PartPlateList::store_to_3mf_structure(PlateDataPtrs& plate_data_list, bool w
|
||||
}
|
||||
plate_data_item->is_support_used = print->is_support_used();
|
||||
} else {
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format("print is null!");
|
||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format("print is null!");
|
||||
}
|
||||
//parse filament info
|
||||
plate_data_item->parse_filament_info(m_plate_list[i]->get_slice_result());
|
||||
} else {
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << "slice result = " << m_plate_list[i]->get_slice_result()
|
||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << "slice result = " << m_plate_list[i]->get_slice_result()
|
||||
<< ", result valid = " << m_plate_list[i]->is_slice_result_valid();
|
||||
}
|
||||
}
|
||||
@@ -6068,7 +6591,7 @@ int PartPlateList::store_to_3mf_structure(PlateDataPtrs& plate_data_list, bool w
|
||||
|
||||
plate_data_list.push_back(plate_data_item);
|
||||
}
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(":stored %1% plates!") % m_plate_list.size();
|
||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(":stored %1% plates!") % m_plate_list.size();
|
||||
|
||||
return ret;
|
||||
}
|
||||
@@ -6094,7 +6617,7 @@ int PartPlateList::load_from_3mf_structure(PlateDataPtrs& plate_data_list, int f
|
||||
{
|
||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(":plate index %1% seems invalid, skip it")% plate_data_list[i]->plate_index;
|
||||
}
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": plate %1%, gcode_file %2%, is_sliced_valid %3%, toolpath_outside %4%, is_support_used %5% is_label_object_enabled %6%")
|
||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": plate %1%, gcode_file %2%, is_sliced_valid %3%, toolpath_outside %4%, is_support_used %5% is_label_object_enabled %6%")
|
||||
%i %plate_data_list[i]->gcode_file %plate_data_list[i]->is_sliced_valid %plate_data_list[i]->toolpath_outside %plate_data_list[i]->is_support_used %plate_data_list[i]->is_label_object_enabled;
|
||||
//load object and instance from 3mf
|
||||
//just test for file correct or not, we will rebuild later
|
||||
@@ -6123,17 +6646,17 @@ int PartPlateList::load_from_3mf_structure(PlateDataPtrs& plate_data_list, int f
|
||||
gcode_result->warnings = plate_data_list[i]->warnings;
|
||||
gcode_result->filament_maps = plate_data_list[i]->filament_maps;
|
||||
if (m_plater && !plate_data_list[i]->thumbnail_file.empty()) {
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": plate %1%, load thumbnail from %2%.")%(i+1) %plate_data_list[i]->thumbnail_file;
|
||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": plate %1%, load thumbnail from %2%.")%(i+1) %plate_data_list[i]->thumbnail_file;
|
||||
if (boost::filesystem::exists(plate_data_list[i]->thumbnail_file)) {
|
||||
m_plate_list[index]->load_thumbnail_data(plate_data_list[i]->thumbnail_file, m_plate_list[index]->thumbnail_data);
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ <<boost::format(": plate %1% after load, width %2%, height %3%, size %4%!")
|
||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ <<boost::format(": plate %1% after load, width %2%, height %3%, size %4%!")
|
||||
%(i+1) %m_plate_list[index]->thumbnail_data.width %m_plate_list[index]->thumbnail_data.height %m_plate_list[index]->thumbnail_data.pixels.size();
|
||||
}
|
||||
}
|
||||
|
||||
if (m_plater && !plate_data_list[i]->no_light_thumbnail_file.empty()) {
|
||||
if (boost::filesystem::exists(plate_data_list[i]->no_light_thumbnail_file)) {
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": plate %1%, load no_light_thumbnail_file from %2%.")%(i+1) %plate_data_list[i]->no_light_thumbnail_file;
|
||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": plate %1%, load no_light_thumbnail_file from %2%.")%(i+1) %plate_data_list[i]->no_light_thumbnail_file;
|
||||
m_plate_list[index]->load_thumbnail_data(plate_data_list[i]->no_light_thumbnail_file, m_plate_list[index]->no_light_thumbnail_data);
|
||||
}
|
||||
}
|
||||
@@ -6146,13 +6669,13 @@ int PartPlateList::load_from_3mf_structure(PlateDataPtrs& plate_data_list, int f
|
||||
}*/
|
||||
if (m_plater && !plate_data_list[i]->top_file.empty()) {
|
||||
if (boost::filesystem::exists(plate_data_list[i]->top_file)) {
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": plate %1%, load top_thumbnail from %2%.")%(i+1) %plate_data_list[i]->top_file;
|
||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": plate %1%, load top_thumbnail from %2%.")%(i+1) %plate_data_list[i]->top_file;
|
||||
m_plate_list[index]->load_thumbnail_data(plate_data_list[i]->top_file, m_plate_list[index]->top_thumbnail_data);
|
||||
}
|
||||
}
|
||||
if (m_plater && !plate_data_list[i]->pick_file.empty()) {
|
||||
if (boost::filesystem::exists(plate_data_list[i]->pick_file)) {
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": plate %1%, load pick_thumbnail from %2%.")%(i+1) %plate_data_list[i]->pick_file;
|
||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": plate %1%, load pick_thumbnail from %2%.")%(i+1) %plate_data_list[i]->pick_file;
|
||||
m_plate_list[index]->load_thumbnail_data(plate_data_list[i]->pick_file, m_plate_list[index]->pick_thumbnail_data);
|
||||
}
|
||||
}
|
||||
@@ -6588,7 +7111,7 @@ void PartPlateList::on_extruder_count_changed(int extruder_count)
|
||||
for (unsigned int i = 0; i < (unsigned int) m_plate_list.size(); ++i) {
|
||||
m_plate_list[i]->on_extruder_count_changed(extruder_count);
|
||||
}
|
||||
BOOST_LOG_TRIVIAL(info) << boost::format("%1%: extruder_count=%2%")% __FUNCTION__ %extruder_count;
|
||||
BOOST_LOG_TRIVIAL(warning) << boost::format("%1%: extruder_count=%2%")% __FUNCTION__ %extruder_count;
|
||||
}
|
||||
|
||||
void PartPlateList::set_filament_count(int filament_count)
|
||||
@@ -6598,7 +7121,7 @@ void PartPlateList::set_filament_count(int filament_count)
|
||||
{
|
||||
m_plate_list[i]->set_filament_count(filament_count);
|
||||
}
|
||||
BOOST_LOG_TRIVIAL(info) << boost::format("%1%: filament_count=%2%")% __FUNCTION__ %filament_count;
|
||||
BOOST_LOG_TRIVIAL(warning) << boost::format("%1%: filament_count=%2%")% __FUNCTION__ %filament_count;
|
||||
}
|
||||
|
||||
void PartPlateList::on_filament_added(int filament_count)
|
||||
@@ -6608,7 +7131,7 @@ void PartPlateList::on_filament_added(int filament_count)
|
||||
{
|
||||
m_plate_list[i]->on_filament_added();
|
||||
}
|
||||
BOOST_LOG_TRIVIAL(info) << boost::format("%1%: filament_count=%2%")% __FUNCTION__ %filament_count;
|
||||
BOOST_LOG_TRIVIAL(warning) << boost::format("%1%: filament_count=%2%")% __FUNCTION__ %filament_count;
|
||||
}
|
||||
|
||||
void PartPlateList::on_filament_deleted(int filament_count, int filament_id)
|
||||
@@ -6618,7 +7141,7 @@ void PartPlateList::on_filament_deleted(int filament_count, int filament_id)
|
||||
{
|
||||
m_plate_list[i]->on_filament_deleted(filament_count, filament_id);
|
||||
}
|
||||
BOOST_LOG_TRIVIAL(info) << boost::format("%1%: filament_count=%2%, filament_id=%3%")% __FUNCTION__ %filament_count %filament_id;
|
||||
BOOST_LOG_TRIVIAL(warning) << boost::format("%1%: filament_count=%2%, filament_id=%3%")% __FUNCTION__ %filament_count %filament_id;
|
||||
}
|
||||
|
||||
}//end namespace GUI
|
||||
|
||||
@@ -135,6 +135,14 @@ private:
|
||||
GLModel m_logo_triangles;
|
||||
GLModel m_gridlines;
|
||||
GLModel m_gridlines_bolder;
|
||||
std::vector<GLModel> m_ixex_copy_zones; // blue tint (Wong #56B4E9)
|
||||
std::vector<GLModel> m_ixex_mirror_zones; // orange tint (Wong #E69F00)
|
||||
GLModel m_ixex_zone_borders; // white outlines around active secondary zones
|
||||
std::vector<BoundingBoxf3> m_ixex_secondary_zone_boxes; // secondary (copy/mirror) zone rects — objects blocked here
|
||||
std::vector<BoundingBoxf3> m_ixex_collision_zones; // carriage danger strips inside primary zone (mm)
|
||||
std::vector<GLModel> m_ixex_collision_overlay; // red-orange rendered fill for danger strips
|
||||
std::vector<GLModel> m_ixex_margin_overlay; // amber advisory bands just inside collision strips
|
||||
std::string m_ixex_zones_mode_cache{ "\x01" }; // cached mode+topology key; sentinel = unbuilt
|
||||
GLModel m_height_limit_common;
|
||||
GLModel m_height_limit_bottom;
|
||||
GLModel m_height_limit_top;
|
||||
@@ -177,6 +185,9 @@ private:
|
||||
void calc_triangles_from_polygon(const ExPolygon &poly, GLModel& render_model);
|
||||
void calc_gridlines(const ExPolygon& poly, const BoundingBox& pp_bbox);
|
||||
void calc_height_limit();
|
||||
void calc_ixex_zones();
|
||||
void ensure_ixex_zones();
|
||||
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);
|
||||
void calc_vertex_for_icons(int index, PickingModel &model);
|
||||
@@ -432,6 +443,8 @@ public:
|
||||
{
|
||||
return m_ready_for_slice && !m_apply_invalid;
|
||||
}
|
||||
// Returns true if any instance on this plate overlaps an iXex secondary or collision zone.
|
||||
bool has_ixex_placement_violations();
|
||||
void update_slice_ready_status(bool ready_slice)
|
||||
{
|
||||
m_ready_for_slice = ready_slice;
|
||||
|
||||
@@ -7722,6 +7722,15 @@ unsigned int Plater::priv::update_background_process(bool force_validation, bool
|
||||
q->post_process_string_object_exception(err);
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": validate err=%1%, warning=%2%")%err.string%warning.string;
|
||||
|
||||
// iXex placement check: if objects overlap secondary zones, treat it as a
|
||||
// validation error so the standard pathway handles button state, notifications,
|
||||
// and auto-slice blocking consistently.
|
||||
if (err.string.empty()) {
|
||||
PartPlate* ixex_plate = partplate_list.get_curr_plate();
|
||||
if (ixex_plate && ixex_plate->has_ixex_placement_violations())
|
||||
err.string = _u8L("Cannot slice: objects are in secondary zones reserved for iXex parallel printing.");
|
||||
}
|
||||
|
||||
if (err.string.empty()) {
|
||||
this->partplate_list.get_curr_plate()->update_apply_result_invalid(false);
|
||||
notification_manager->set_all_slicing_errors_gray(true);
|
||||
|
||||
@@ -45,6 +45,7 @@
|
||||
|
||||
#include "Widgets/Label.hpp"
|
||||
#include "Widgets/TabCtrl.hpp"
|
||||
#include "Widgets/ComboBox.hpp"
|
||||
#include "MarkdownTip.hpp"
|
||||
#include "Search.hpp"
|
||||
#include "BedShapeDialog.hpp"
|
||||
@@ -2673,6 +2674,26 @@ 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");
|
||||
@@ -2731,6 +2752,36 @@ 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()
|
||||
@@ -4335,6 +4386,324 @@ void TabPrinter::build()
|
||||
m_printer_technology == ptSLA ? build_sla() : build_fff();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// IXexModesCtrl — visual parallel-mode editor for the iXex printer tab
|
||||
//
|
||||
// Tool button states:
|
||||
// 0 = Inactive (grey) — not participating in this mode
|
||||
// 1 = Primary (green) — the tool the slicer generates paths for
|
||||
// 2 = Copy (blue) — firmware duplicates Primary at an offset
|
||||
// 3 = Mirror (amber) — firmware mirrors Primary about an axis
|
||||
//
|
||||
// Active-tools string format: "idx:P,idx:C,idx:M" (backwards-compat: plain "idx" = Primary)
|
||||
// ---------------------------------------------------------------------------
|
||||
class IXexModesCtrl : public wxPanel {
|
||||
public:
|
||||
std::function<void()> on_change;
|
||||
|
||||
// layout: 0=front-left, 1=front-right, 2=rear-left, 3=rear-right (T0 corner)
|
||||
static int parse_layout(const std::string& s) {
|
||||
if (s == "front-right") return 1;
|
||||
if (s == "rear-left") return 2;
|
||||
if (s == "rear-right") return 3;
|
||||
return 0; // "front-left" default
|
||||
}
|
||||
|
||||
IXexModesCtrl(wxWindow* parent, int n_cols, int n_rows, int layout = 0)
|
||||
: wxPanel(parent, wxID_ANY), m_n_cols(std::max(1, n_cols)), m_n_rows(std::max(1, n_rows)), m_layout(layout)
|
||||
{
|
||||
m_outer = new wxBoxSizer(wxVERTICAL);
|
||||
m_rows_sizer = new wxBoxSizer(wxVERTICAL);
|
||||
|
||||
// --- Info panel: instruction text + color legend ---
|
||||
auto* info_panel = new wxPanel(this, wxID_ANY);
|
||||
auto* info_sizer = new wxBoxSizer(wxVERTICAL);
|
||||
|
||||
auto* inst = new wxStaticText(info_panel, wxID_ANY,
|
||||
_L("Each mode defines which tool heads participate and their roles. "
|
||||
"The Primary tool (green) drives all sliced paths. "
|
||||
"Copy (blue) and Mirror (amber) tools follow the Primary at the firmware level. "
|
||||
"Click a tool button to cycle its role — clear the Primary before reassigning it."));
|
||||
inst->Wrap(620);
|
||||
info_sizer->Add(inst, 0, wxBOTTOM, 6);
|
||||
|
||||
// Color legend
|
||||
auto* leg_sizer = new wxBoxSizer(wxHORIZONTAL);
|
||||
struct { int state; const char* label; } legend[] = {
|
||||
{1,"Primary"},{2,"Copy"},{3,"Mirror"}
|
||||
};
|
||||
for (auto& l : legend) {
|
||||
auto* swatch = new wxPanel(info_panel, wxID_ANY, wxDefaultPosition, wxSize(12, 12));
|
||||
swatch->SetBackgroundColour(btn_color(l.state));
|
||||
leg_sizer->Add(swatch, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, 4);
|
||||
leg_sizer->Add(new wxStaticText(info_panel, wxID_ANY, l.label),
|
||||
0, wxALIGN_CENTER_VERTICAL | wxRIGHT, 16);
|
||||
}
|
||||
info_sizer->Add(leg_sizer, 0, wxBOTTOM, 6);
|
||||
info_panel->SetSizer(info_sizer);
|
||||
|
||||
// Column header row (fixed-pixel spacers to approximately align with mode row fields)
|
||||
// Name field: 130px + 6px gap; Tool grid: n_cols*(36+2)-2 px + 6px gap
|
||||
auto* hdr_panel = new wxPanel(this, wxID_ANY);
|
||||
auto* hdr_sizer = new wxBoxSizer(wxHORIZONTAL);
|
||||
auto* hdr_name = new wxStaticText(hdr_panel, wxID_ANY, _L("Name"));
|
||||
auto* hdr_tools = new wxStaticText(hdr_panel, wxID_ANY, _L("Tools"));
|
||||
auto* hdr_gcode = new wxStaticText(hdr_panel, wxID_ANY, _L("G-code"));
|
||||
int grid_px = m_n_cols * 38 - 2; // approx grid panel width
|
||||
hdr_sizer->Add(hdr_name, 0, wxALIGN_CENTER_VERTICAL);
|
||||
hdr_sizer->AddSpacer(std::max(0, 136 - hdr_name->GetBestSize().x)); // "Name" col = 136px total
|
||||
hdr_sizer->Add(hdr_tools, 0, wxALIGN_CENTER_VERTICAL);
|
||||
hdr_sizer->AddSpacer(std::max(0, grid_px + 6 - hdr_tools->GetBestSize().x)); // "Tools" col = grid_px+6 total
|
||||
hdr_sizer->Add(hdr_gcode, 1, wxALIGN_CENTER_VERTICAL);
|
||||
hdr_panel->SetSizer(hdr_sizer);
|
||||
|
||||
auto* add_btn = new wxButton(this, wxID_ANY, _L("+ Add Mode"),
|
||||
wxDefaultPosition, wxDefaultSize, wxBU_EXACTFIT);
|
||||
add_btn->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { add_row(); notify(); });
|
||||
|
||||
m_outer->Add(info_panel, 0, wxEXPAND | wxBOTTOM, 4);
|
||||
m_outer->Add(hdr_panel, 0, wxEXPAND | wxBOTTOM, 2);
|
||||
m_outer->Add(m_rows_sizer, 0, wxEXPAND);
|
||||
m_outer->AddSpacer(4);
|
||||
m_outer->Add(add_btn, 0);
|
||||
SetSizer(m_outer);
|
||||
}
|
||||
|
||||
void set_grid_size(int n_cols, int n_rows, int layout = -1) {
|
||||
n_cols = std::max(1, n_cols);
|
||||
n_rows = std::max(1, n_rows);
|
||||
if (layout < 0) layout = m_layout;
|
||||
if (n_cols == m_n_cols && n_rows == m_n_rows && layout == m_layout) return;
|
||||
auto [names, tools, gcodes] = get_mode_data();
|
||||
clear_rows();
|
||||
m_n_cols = n_cols;
|
||||
m_n_rows = n_rows;
|
||||
m_layout = layout;
|
||||
for (size_t i = 0; i < names.size(); ++i)
|
||||
add_row(names[i], tools[i], gcodes[i]);
|
||||
Layout();
|
||||
}
|
||||
|
||||
void load_from_config(const DynamicPrintConfig& cfg) {
|
||||
clear_rows();
|
||||
auto* names = cfg.option<ConfigOptionStrings>("ixex_mode_names");
|
||||
auto* tools = cfg.option<ConfigOptionStrings>("ixex_mode_active_tools");
|
||||
auto* gcodes = cfg.option<ConfigOptionStrings>("ixex_mode_gcodes");
|
||||
size_t n = names ? names->values.size() : 0;
|
||||
for (size_t i = 0; i < n; ++i)
|
||||
add_row(names->values[i],
|
||||
(tools && i < tools->values.size()) ? tools->values[i] : "",
|
||||
(gcodes && i < gcodes->values.size()) ? gcodes->values[i] : "");
|
||||
Layout();
|
||||
}
|
||||
|
||||
std::tuple<std::vector<std::string>, std::vector<std::string>, std::vector<std::string>>
|
||||
get_mode_data() const {
|
||||
std::vector<std::string> names, tools, gcodes;
|
||||
for (auto& r : m_rows) {
|
||||
std::string nm = r.name->GetValue().ToStdString();
|
||||
if (nm.empty()) continue;
|
||||
names.push_back(nm);
|
||||
tools.push_back(active_tools_string(r));
|
||||
gcodes.push_back(r.gcode->GetValue().ToStdString());
|
||||
}
|
||||
return {names, tools, gcodes};
|
||||
}
|
||||
|
||||
// Parse "idx:P,idx:C,idx:M" → map<tool_idx, state>
|
||||
// Backwards compat: plain "idx" (no role) → state 1 (Primary)
|
||||
static std::map<int,int> parse_tool_states(const std::string& s) {
|
||||
std::map<int,int> result;
|
||||
if (s.empty()) return result;
|
||||
std::istringstream ss(s);
|
||||
std::string tok;
|
||||
while (std::getline(ss, tok, ',')) {
|
||||
tok.erase(std::remove_if(tok.begin(), tok.end(), ::isspace), tok.end());
|
||||
if (tok.empty()) continue;
|
||||
try {
|
||||
auto colon = tok.find(':');
|
||||
int idx, state = 1;
|
||||
if (colon != std::string::npos) {
|
||||
idx = std::stoi(tok.substr(0, colon));
|
||||
char role = std::toupper((unsigned char)tok[colon + 1]);
|
||||
if (role == 'C') state = 2;
|
||||
else if (role == 'M') state = 3;
|
||||
} else {
|
||||
idx = std::stoi(tok);
|
||||
}
|
||||
result[idx] = state;
|
||||
} catch (...) {}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private:
|
||||
// State 0=inactive, 1=primary, 2=copy, 3=mirror
|
||||
static wxColour btn_color(int state) {
|
||||
switch (state) {
|
||||
case 1: return wxColour(50, 160, 50); // green — Primary
|
||||
case 2: return wxColour(60, 120, 210); // blue — Copy
|
||||
case 3: return wxColour(210, 130, 20); // amber — Mirror
|
||||
default: return wxColour(90, 90, 90); // grey — Inactive
|
||||
}
|
||||
}
|
||||
static wxColour btn_fg(int /*state*/) { return *wxWHITE; }
|
||||
static const char* state_role(int state) {
|
||||
switch (state) { case 1: return "P"; case 2: return "C"; case 3: return "M"; default: return ""; }
|
||||
}
|
||||
|
||||
void apply_btn(wxButton* btn, int tool_idx, int state) {
|
||||
// Always label as T{n} — the button color already encodes the role.
|
||||
btn->SetLabel(wxString::Format("T%d", tool_idx));
|
||||
btn->SetBackgroundColour(btn_color(state));
|
||||
btn->SetForegroundColour(btn_fg(state));
|
||||
btn->Refresh();
|
||||
}
|
||||
|
||||
struct Row {
|
||||
wxPanel* panel;
|
||||
wxTextCtrl* name;
|
||||
wxTextCtrl* gcode;
|
||||
std::vector<wxButton*> btns;
|
||||
std::vector<int> btn_states; // parallel to btns
|
||||
// tool_idx for btns[i]: computed during add_row, stored here for quick lookup
|
||||
std::vector<int> btn_tool_idx;
|
||||
};
|
||||
|
||||
void add_row(const std::string& name = "",
|
||||
const std::string& active_tools = "",
|
||||
const std::string& gcode = "")
|
||||
{
|
||||
Row r;
|
||||
r.panel = new wxPanel(this, wxID_ANY);
|
||||
auto* sizer = new wxBoxSizer(wxHORIZONTAL);
|
||||
|
||||
r.name = new wxTextCtrl(r.panel, wxID_ANY, name, wxDefaultPosition, wxSize(130, -1));
|
||||
r.name->Bind(wxEVT_TEXT, [this](wxCommandEvent&) { notify(); });
|
||||
|
||||
auto* grid_panel = new wxPanel(r.panel, wxID_ANY);
|
||||
auto* grid_sizer = new wxGridSizer(m_n_rows, m_n_cols, 2, 2);
|
||||
|
||||
auto tool_states = parse_tool_states(active_tools);
|
||||
wxPanel* this_panel = r.panel;
|
||||
|
||||
// Buttons rendered top=rear (high Y), bottom=front (low Y).
|
||||
// Layout remapping: m_layout encodes which corner T0 is at physically.
|
||||
// flip_x (layout 1,3): col 0 is on the right (max-X) instead of left
|
||||
// flip_y (layout 2,3): row 0 is at the rear (max-Y) instead of front
|
||||
// Display iterates: row from n_rows-1 down to 0 (rear→front = top→bottom).
|
||||
// phys_row = row (loop var maps directly to physical bed Y index).
|
||||
// tool raw index: raw_row = flip_y ? (n_rows-1-row) : row
|
||||
// raw_col = flip_x ? (n_cols-1-col) : col
|
||||
bool flip_x = (m_layout == 1 || m_layout == 3);
|
||||
bool flip_y = (m_layout == 2 || m_layout == 3);
|
||||
for (int row = m_n_rows - 1; row >= 0; --row) {
|
||||
for (int col = 0; col < m_n_cols; ++col) {
|
||||
int raw_row = flip_y ? (m_n_rows - 1 - row) : row;
|
||||
int raw_col = flip_x ? (m_n_cols - 1 - col) : col;
|
||||
int tool_idx = raw_row * m_n_cols + raw_col;
|
||||
int state = 0;
|
||||
auto it = tool_states.find(tool_idx);
|
||||
if (it != tool_states.end()) state = it->second;
|
||||
|
||||
auto* btn = new wxButton(grid_panel, wxID_ANY, wxEmptyString,
|
||||
wxDefaultPosition, wxSize(36, 26), wxBU_EXACTFIT);
|
||||
apply_btn(btn, tool_idx, state);
|
||||
|
||||
int btn_pos = (int)r.btns.size();
|
||||
r.btns.push_back(btn);
|
||||
r.btn_states.push_back(state);
|
||||
r.btn_tool_idx.push_back(tool_idx);
|
||||
|
||||
btn->Bind(wxEVT_BUTTON, [this, this_panel, btn_pos](wxCommandEvent&) {
|
||||
// Find the row by panel pointer (stable across add/remove)
|
||||
for (auto& row_ref : m_rows) {
|
||||
if (row_ref.panel != this_panel) continue;
|
||||
int& st = row_ref.btn_states[btn_pos];
|
||||
|
||||
// Check if another button already holds the Primary state
|
||||
bool other_primary = false;
|
||||
for (int j = 0; j < (int)row_ref.btn_states.size(); ++j)
|
||||
if (j != btn_pos && row_ref.btn_states[j] == 1) { other_primary = true; break; }
|
||||
|
||||
// Advance state, skipping Primary(1) if another tool is already Primary
|
||||
st = (st + 1) % 4;
|
||||
if (st == 1 && other_primary)
|
||||
st = 2; // skip Primary → go straight to Copy
|
||||
|
||||
apply_btn(row_ref.btns[btn_pos], row_ref.btn_tool_idx[btn_pos], st);
|
||||
notify();
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
grid_sizer->Add(btn, 0);
|
||||
}
|
||||
}
|
||||
grid_panel->SetSizer(grid_sizer);
|
||||
|
||||
r.gcode = new wxTextCtrl(r.panel, wxID_ANY, gcode,
|
||||
wxDefaultPosition, wxSize(220, 54), wxTE_MULTILINE);
|
||||
r.gcode->Bind(wxEVT_TEXT, [this](wxCommandEvent&) { notify(); });
|
||||
|
||||
auto* rm = new wxButton(r.panel, wxID_ANY, wxT("\u00d7"),
|
||||
wxDefaultPosition, wxSize(24, 24), wxBU_EXACTFIT);
|
||||
rm->Bind(wxEVT_BUTTON, [this, this_panel](wxCommandEvent&) {
|
||||
remove_row(this_panel);
|
||||
notify();
|
||||
});
|
||||
|
||||
sizer->Add(r.name, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, 6);
|
||||
sizer->Add(grid_panel, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, 6);
|
||||
sizer->Add(r.gcode, 1, wxALIGN_CENTER_VERTICAL | wxRIGHT, 6);
|
||||
sizer->Add(rm, 0, wxALIGN_CENTER_VERTICAL);
|
||||
r.panel->SetSizer(sizer);
|
||||
|
||||
m_rows_sizer->Add(r.panel, 0, wxEXPAND | wxBOTTOM, 4);
|
||||
m_rows.push_back(std::move(r));
|
||||
Layout();
|
||||
}
|
||||
|
||||
void remove_row(wxPanel* panel) {
|
||||
for (size_t i = 0; i < m_rows.size(); ++i) {
|
||||
if (m_rows[i].panel == panel) {
|
||||
m_rows_sizer->Detach(panel);
|
||||
m_rows.erase(m_rows.begin() + i);
|
||||
Layout();
|
||||
// Defer widget destruction so any in-flight GTK events for
|
||||
// panel's children (including the × button we're inside) finish
|
||||
// processing before the wxEvtHandlers are freed.
|
||||
wxTheApp->CallAfter([panel]() { panel->Destroy(); });
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void clear_rows() {
|
||||
for (auto& r : m_rows) { m_rows_sizer->Detach(r.panel); r.panel->Destroy(); }
|
||||
m_rows.clear();
|
||||
}
|
||||
|
||||
std::string active_tools_string(const Row& r) const {
|
||||
std::string s;
|
||||
for (size_t i = 0; i < r.btn_states.size(); ++i) {
|
||||
if (r.btn_states[i] == 0) continue;
|
||||
if (!s.empty()) s += ',';
|
||||
s += std::to_string(r.btn_tool_idx[i]) + ':' + state_role(r.btn_states[i]);
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
void notify() { if (on_change) on_change(); }
|
||||
|
||||
wxBoxSizer* m_outer;
|
||||
wxBoxSizer* m_rows_sizer;
|
||||
std::vector<Row> m_rows;
|
||||
int m_n_cols, m_n_rows;
|
||||
int m_layout {0}; // 0=front-left, 1=front-right, 2=rear-left, 3=rear-right
|
||||
};
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void TabPrinter::build_fff()
|
||||
{
|
||||
if (!m_pages.empty())
|
||||
@@ -4589,6 +4958,117 @@ void TabPrinter::build_fff()
|
||||
option.opt.height = gcode_field_height;//150;
|
||||
optgroup->append_single_option_line(option, "printer_machine_gcode#template-custom-g-code");
|
||||
|
||||
page = add_options_page(L("iXex"), "custom-gcode_multi_material"); // ORCA: icon only visible on placeholders
|
||||
optgroup = page->new_optgroup(L("Carriage Configuration"), L"param_advanced");
|
||||
optgroup->append_single_option_line("is_ixex");
|
||||
optgroup->append_single_option_line("ixex_gantry_count");
|
||||
optgroup->append_single_option_line("ixex_tools_per_gantry");
|
||||
{
|
||||
// Tool 0 corner selector
|
||||
auto opt = optgroup->get_option("ixex_tool_layout");
|
||||
auto line = Line{ opt.opt.label, opt.opt.tooltip };
|
||||
line.append_option(opt);
|
||||
line.widget = [this](wxWindow* parent) -> wxSizer* {
|
||||
static const wxString choices[] = {
|
||||
_L("Front-left"), _L("Front-right"),
|
||||
_L("Rear-left"), _L("Rear-right")
|
||||
};
|
||||
static const char* values[] = { "front-left", "front-right", "rear-left", "rear-right" };
|
||||
m_ixex_layout_combo = new ComboBox(parent, wxID_ANY, wxEmptyString,
|
||||
wxDefaultPosition,
|
||||
wxSize(12 * wxGetApp().em_unit(), -1),
|
||||
4, choices, wxCB_READONLY);
|
||||
m_ixex_layout_combo->GetDropDown().SetUseContentWidth(true);
|
||||
// Set current selection
|
||||
std::string cur = "front-left";
|
||||
if (auto* o = m_config->option<ConfigOptionString>("ixex_tool_layout")) cur = o->value;
|
||||
for (int i = 0; i < 4; ++i)
|
||||
if (cur == values[i]) { m_ixex_layout_combo->SetSelection(i); break; }
|
||||
m_ixex_layout_combo->Bind(wxEVT_COMBOBOX, [this, values](wxCommandEvent&) {
|
||||
int sel = m_ixex_layout_combo->GetSelection();
|
||||
std::string val = (sel >= 0 && sel < 4) ? values[sel] : "front-left";
|
||||
m_config->set_key_value("ixex_tool_layout", new ConfigOptionString(val));
|
||||
on_value_change("ixex_tool_layout", val);
|
||||
if (m_ixex_modes_ctrl)
|
||||
m_ixex_modes_ctrl->set_grid_size(
|
||||
m_config->opt_int("ixex_tools_per_gantry"),
|
||||
m_config->opt_int("ixex_gantry_count"),
|
||||
IXexModesCtrl::parse_layout(val));
|
||||
});
|
||||
auto* s = new wxBoxSizer(wxHORIZONTAL);
|
||||
s->Add(m_ixex_layout_combo, 0, wxALIGN_CENTER_VERTICAL);
|
||||
return s;
|
||||
};
|
||||
optgroup->append_line(line);
|
||||
}
|
||||
optgroup->append_single_option_line("ixex_carriage_width_x");
|
||||
optgroup->append_single_option_line("ixex_carriage_width_y");
|
||||
optgroup->append_single_option_line("ixex_carriage_margin");
|
||||
|
||||
{
|
||||
static const wxString theme_choices[] = {
|
||||
_L("Standard"),
|
||||
_L("Deuteranopia / Protanopia (red-green)"),
|
||||
_L("Tritanopia (blue-yellow)"),
|
||||
_L("High Contrast") };
|
||||
static const char* theme_vals[] = { "standard", "deuteranopia", "tritanopia", "high_contrast" };
|
||||
auto opt = optgroup->get_option("ixex_viz_theme");
|
||||
auto line = Line{ opt.opt.label, opt.opt.tooltip };
|
||||
line.append_option(opt);
|
||||
line.widget = [this](wxWindow* parent) -> wxSizer* {
|
||||
m_ixex_theme_combo = new ComboBox(parent, wxID_ANY, wxEmptyString,
|
||||
wxDefaultPosition,
|
||||
wxSize(22 * wxGetApp().em_unit(), -1),
|
||||
4, theme_choices, wxCB_READONLY);
|
||||
m_ixex_theme_combo->GetDropDown().SetUseContentWidth(true);
|
||||
std::string cur = "standard";
|
||||
if (auto* o = m_config->option<ConfigOptionString>("ixex_viz_theme")) cur = o->value;
|
||||
for (int i = 0; i < 4; ++i)
|
||||
if (cur == theme_vals[i]) { m_ixex_theme_combo->SetSelection(i); break; }
|
||||
m_ixex_theme_combo->Bind(wxEVT_COMBOBOX, [this](wxCommandEvent&) {
|
||||
int sel = m_ixex_theme_combo->GetSelection();
|
||||
std::string val = (sel >= 0 && sel < 4) ? theme_vals[sel] : "standard";
|
||||
m_config->set_key_value("ixex_viz_theme", new ConfigOptionString(val));
|
||||
on_value_change("ixex_viz_theme", val);
|
||||
});
|
||||
auto* s = new wxBoxSizer(wxHORIZONTAL);
|
||||
s->Add(m_ixex_theme_combo, 0, wxALIGN_CENTER_VERTICAL);
|
||||
return s;
|
||||
};
|
||||
optgroup->append_line(line);
|
||||
}
|
||||
|
||||
{
|
||||
// Parallel modes panel: visual toggle-grid list with + button
|
||||
auto modes_og = page->new_optgroup(L("Parallel Modes"), L"param_advanced");
|
||||
auto line = Line{ L("Modes"), L("") };
|
||||
line.full_width = 1;
|
||||
line.widget = [this](wxWindow* parent) -> wxSizer* {
|
||||
int n_cols = m_config->opt_int("ixex_tools_per_gantry");
|
||||
int n_rows = m_config->opt_int("ixex_gantry_count");
|
||||
std::string layout_str = "front-left";
|
||||
if (auto* o = m_config->option<ConfigOptionString>("ixex_tool_layout")) layout_str = o->value;
|
||||
int layout = IXexModesCtrl::parse_layout(layout_str);
|
||||
m_ixex_modes_ctrl = new IXexModesCtrl(parent, n_cols, n_rows, layout);
|
||||
m_ixex_modes_ctrl->load_from_config(*m_config);
|
||||
m_ixex_modes_ctrl->on_change = [this]() {
|
||||
auto [names, tools, gcodes] = m_ixex_modes_ctrl->get_mode_data();
|
||||
m_config->set_key_value("ixex_mode_names", new ConfigOptionStrings(names));
|
||||
m_config->set_key_value("ixex_mode_active_tools", new ConfigOptionStrings(tools));
|
||||
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);
|
||||
return sizer;
|
||||
};
|
||||
modes_og->append_line(line);
|
||||
}
|
||||
|
||||
page = add_options_page(L("Notes"), "custom-gcode_note"); // ORCA: icon only visible on placeholders
|
||||
optgroup = page->new_optgroup(L("Notes"), "note", 0);
|
||||
option = optgroup->get_option("printer_notes");
|
||||
@@ -5168,6 +5648,33 @@ void TabPrinter::reload_config()
|
||||
// so update it implicitly
|
||||
if (m_active_page && m_active_page->title() == "Multimaterial")
|
||||
m_active_page->set_value("extruders_count", int(m_extruders_count));
|
||||
|
||||
if (m_config) {
|
||||
// Sync layout combo
|
||||
if (m_ixex_layout_combo) {
|
||||
static const char* layout_vals[] = { "front-left", "front-right", "rear-left", "rear-right" };
|
||||
std::string cur = "front-left";
|
||||
if (auto* o = m_config->option<ConfigOptionString>("ixex_tool_layout")) cur = o->value;
|
||||
for (int i = 0; i < 4; ++i)
|
||||
if (cur == layout_vals[i]) { m_ixex_layout_combo->SetSelection(i); break; }
|
||||
}
|
||||
// Sync theme combo
|
||||
if (m_ixex_theme_combo) {
|
||||
static const char* theme_vals[] = { "standard", "deuteranopia", "tritanopia", "high_contrast" };
|
||||
std::string cur = "standard";
|
||||
if (auto* o = m_config->option<ConfigOptionString>("ixex_viz_theme")) cur = o->value;
|
||||
for (int i = 0; i < 4; ++i)
|
||||
if (cur == theme_vals[i]) { m_ixex_theme_combo->SetSelection(i); break; }
|
||||
}
|
||||
if (m_ixex_modes_ctrl) {
|
||||
int n_cols = m_config->opt_int("ixex_tools_per_gantry");
|
||||
int n_rows = m_config->opt_int("ixex_gantry_count");
|
||||
std::string layout_str = "front-left";
|
||||
if (auto* o = m_config->option<ConfigOptionString>("ixex_tool_layout")) layout_str = o->value;
|
||||
m_ixex_modes_ctrl->set_grid_size(n_cols, n_rows, IXexModesCtrl::parse_layout(layout_str));
|
||||
m_ixex_modes_ctrl->load_from_config(*m_config);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void TabPrinter::activate_selected_page(std::function<void()> throw_if_canceled)
|
||||
@@ -5184,6 +5691,9 @@ void TabPrinter::clear_pages()
|
||||
{
|
||||
Tab::clear_pages();
|
||||
m_reset_to_filament_color = nullptr;
|
||||
m_ixex_modes_ctrl = nullptr;
|
||||
m_ixex_layout_combo = nullptr;
|
||||
m_ixex_theme_combo = nullptr;
|
||||
}
|
||||
|
||||
void TabPrinter::toggle_options()
|
||||
@@ -5393,6 +5903,15 @@ void TabPrinter::update_fff()
|
||||
m_use_silent_mode = m_config->opt_bool("silent_mode");
|
||||
}
|
||||
|
||||
// Sync iXex tool grid whenever carriage configuration changes.
|
||||
// set_grid_size() is a no-op when dimensions and layout are unchanged.
|
||||
if (m_ixex_modes_ctrl) {
|
||||
int n_cols = m_config->opt_int("ixex_tools_per_gantry");
|
||||
int n_rows = m_config->opt_int("ixex_gantry_count");
|
||||
std::string layout_str = m_config->opt_string("ixex_tool_layout");
|
||||
m_ixex_modes_ctrl->set_grid_size(n_cols, n_rows, IXexModesCtrl::parse_layout(layout_str));
|
||||
}
|
||||
|
||||
toggle_options();
|
||||
}
|
||||
|
||||
|
||||
@@ -41,6 +41,7 @@
|
||||
#include "Widgets/RoundedRectangle.hpp"
|
||||
#include "Widgets/TextInput.hpp"
|
||||
#include "Widgets/CheckBox.hpp" // ORCA
|
||||
#include "Widgets/ComboBox.hpp"
|
||||
|
||||
class TabCtrl;
|
||||
|
||||
@@ -463,10 +464,12 @@ 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
|
||||
@@ -588,6 +591,8 @@ public:
|
||||
void set_custom_gcode(const t_config_option_key& opt_key, const std::string& value) override;
|
||||
};
|
||||
|
||||
class IXexModesCtrl;
|
||||
|
||||
class TabPrinter : public Tab
|
||||
{
|
||||
private:
|
||||
@@ -602,6 +607,9 @@ private:
|
||||
std::vector<PageShp> m_pages_sla;
|
||||
|
||||
wxBoxSizer* m_presets_sizer {nullptr};
|
||||
IXexModesCtrl* m_ixex_modes_ctrl {nullptr};
|
||||
ComboBox* m_ixex_layout_combo {nullptr};
|
||||
ComboBox* m_ixex_theme_combo {nullptr};
|
||||
public:
|
||||
ScalableButton* m_reset_to_filament_color = nullptr;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user