refactor: rename ixex_carriage_width → ixex_nozzle_clearance; fix strip math and carriage box visualization

Config key rename (breaking for saved profiles — call out in PR):
  ixex_carriage_width_x/y → ixex_nozzle_clearance_x/y
  Labels updated to "Nozzle Clearance X/Y" with consistent tooltips
  describing the measurement as nozzle-to-collision-side-edge distance.

Strip math fix:
  Previously halved the clearance value (× 0.5) under the assumption
  the nozzle was centered in the carriage. The measurement is now the
  literal nozzle-to-edge distance, so the × 0.5 factor is removed.
  The collision strip width now equals the configured value directly.

Carriage box visualization (GCodeViewer):
  - Add per-carriage box_offset_x/y so the nozzle marker sits at the
    physically correct edge of the footprint box rather than centered.
  - X: zone-based by default (nozzle at inner edge facing bed center).
    Copy secondaries inherit the primary's X orientation (same movement
    direction). Mirror secondaries use the collision-side edge.
  - Y: always row-based regardless of copy/mirror mode. Gantry is always
    behind the nozzle (high-Y); front-row primaries with a back-row
    secondary override to place nozzle at the low-Y edge.
  - Fix stale mesh bug: GLModel::init_from() is a no-op when already
    initialized. Call reset() before init_from() so mesh rebuilds
    correctly when nozzle clearance values change in config.
  - Remove m_ixex_toolhead_box_dims (was the now-unnecessary cache key).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Clifford Garwood
2026-04-06 14:57:57 -04:00
co-authored by Claude Sonnet 4.6
parent a9ad17cea2
commit 167211c85c
7 changed files with 73 additions and 43 deletions
+1 -1
View File
@@ -1042,7 +1042,7 @@ static std::vector<std::string> s_Preset_printer_options {
"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_nozzle_clearance_x", "ixex_nozzle_clearance_y", "ixex_carriage_margin", "ixex_viz_theme",
"ixex_mode_names", "ixex_mode_active_tools", "ixex_mode_gcodes"
};
+6 -6
View File
@@ -5530,17 +5530,17 @@ void PrintConfigDef::init_fff_params()
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 = this->add("ixex_nozzle_clearance_x", coFloat);
def->label = L("Nozzle Clearance X");
def->tooltip = L("Distance (mm) from the nozzle to the collision-side carriage edge in X. Used to calculate the width of the collision exclusion strip at each X boundary.");
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 = this->add("ixex_nozzle_clearance_y", coFloat);
def->label = L("Nozzle Clearance Y");
def->tooltip = L("Distance (mm) from the nozzle to the collision-side carriage edge in Y. Used to calculate the width of the collision exclusion strip at each Y boundary.");
def->sidetext = L("mm");
def->min = 0;
def->mode = comAdvanced;
+2 -2
View File
@@ -1341,8 +1341,8 @@ PRINT_CONFIG_CLASS_DEFINE(
((ConfigOptionInt, ixex_tools_per_gantry))
((ConfigOptionInt, ixex_primary_col))
((ConfigOptionInt, ixex_primary_row))
((ConfigOptionFloat, ixex_carriage_width_x))
((ConfigOptionFloat, ixex_carriage_width_y))
((ConfigOptionFloat, ixex_nozzle_clearance_x))
((ConfigOptionFloat, ixex_nozzle_clearance_y))
((ConfigOptionStrings, ixex_mode_names))
((ConfigOptionStrings, ixex_mode_active_tools))
((ConfigOptionStrings, ixex_mode_gcodes))
+47 -16
View File
@@ -1521,7 +1521,7 @@ void GCodeViewer::render(int canvas_width, int canvas_height, int right_margin)
{ 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; };
struct CarriageDraw { Vec3f pos; ColorRGBA color; float box_offset_x = 0.0f; float box_offset_y = 0.0f; };
std::vector<CarriageDraw> carriage_box_draws;
float ixex_box_wx = 0.0f, ixex_box_wy = 0.0f;
{
@@ -1545,8 +1545,8 @@ void GCodeViewer::render(int canvas_width, int canvas_height, int right_margin)
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");
auto* wx_opt = printer_cfg.opt<ConfigOptionFloat>("ixex_nozzle_clearance_x");
auto* wy_opt = printer_cfg.opt<ConfigOptionFloat>("ixex_nozzle_clearance_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;
@@ -1665,8 +1665,19 @@ void GCodeViewer::render(int canvas_width, int canvas_height, int right_margin)
}
}
// Primary carriage box
carriage_box_draws.push_back({ prim_pos, s_carriage_colors[0] });
// Primary carriage box: nozzle sits at the collision-side edge.
// Default to centered; override once we know which side the secondaries are on.
float pri_box_offset_x = (pri_phys_col == 0) ? 0.0f : -ixex_box_wx; // nozzle at inner edge (facing center of bed)
float pri_box_offset_y = -ixex_box_wy; // nozzle at high-Y edge by default (gantry always behind nozzle); override if secondary is behind
for (int i = 0; i < sec_count; ++i) {
int sc = phys_col_of(sec_tool_ids[i]);
int sr = phys_row_of(sec_tool_ids[i]);
if (sc > pri_phys_col) { pri_box_offset_x = 0.0f; }
else if (sc < pri_phys_col) { pri_box_offset_x = -ixex_box_wx; }
if (sr > pri_phys_row) { pri_box_offset_y = 0.0f; }
else if (sr < pri_phys_row) { pri_box_offset_y = -ixex_box_wy; }
}
carriage_box_draws.push_back({ prim_pos, s_carriage_colors[0], pri_box_offset_x, pri_box_offset_y });
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;
@@ -1704,8 +1715,26 @@ void GCodeViewer::render(int canvas_width, int canvas_height, int right_margin)
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);
// X: copy tools move in sync with primary so they share the same
// X-facing orientation. Mirror tools approach from the opposite side
// so the nozzle sits at the collision-side edge.
float sec_box_offset_x;
if (sec_state == 2) {
sec_box_offset_x = pri_box_offset_x;
} else {
if (sec_phys_col > pri_phys_col) sec_box_offset_x = -ixex_box_wx;
else if (sec_phys_col < pri_phys_col) sec_box_offset_x = 0.0f;
else sec_box_offset_x = pri_box_offset_x;
}
// Y: always row-based — gantry is always behind nozzle regardless of
// copy/mirror (a back-row copy still has its gantry at the back).
float sec_box_offset_y;
if (sec_phys_row > pri_phys_row) sec_box_offset_y = -ixex_box_wy;
else if (sec_phys_row < pri_phys_row) sec_box_offset_y = 0.0f;
else sec_box_offset_y = -ixex_box_wy;
carriage_box_draws.push_back({
sec_pos, s_carriage_colors[(i + 1) % s_carriage_colors.size()] });
sec_pos, s_carriage_colors[(i + 1) % s_carriage_colors.size()],
sec_box_offset_x, sec_box_offset_y });
}
}
}
@@ -1719,17 +1748,19 @@ void GCodeViewer::render(int canvas_width, int canvas_height, int right_margin)
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.
// Each box is ixex_nozzle_clearance_x × ixex_nozzle_clearance_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
// Rebuild box mesh every frame — dimensions can change via config edit without a
// G-code reload, so dimension-based caching isn't safe.
// Mesh origin: nozzle at x=0, centered in Y, Z starts at nozzle tip level.
// Per-carriage box_offset_x shifts the mesh left or right so the nozzle lands
// at the correct (collision-side) edge.
{
const float box_h = std::max(ixex_box_wx, ixex_box_wy);
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);
// No vertex pre-shifting — box_offset_x/y in the per-carriage transform
// positions the nozzle at the correct collision-side edge.
m_ixex_toolhead_box.reset();
m_ixex_toolhead_box.init_from(its);
}
@@ -1744,7 +1775,7 @@ void GCodeViewer::render(int canvas_width, int canvas_height, int right_margin)
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>());
(draw.pos + Vec3f(draw.box_offset_x, draw.box_offset_y, 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) *
-1
View File
@@ -207,7 +207,6 @@ private:
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;
+15 -15
View File
@@ -653,18 +653,18 @@ void PartPlate::calc_ixex_zones()
// 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 width is the literal nozzle clearance value on the primary side only:
// right X boundary: [bnd_x - nozzle_clearance_x, bnd_x]
// left X boundary: [bnd_x, bnd_x + nozzle_clearance_x]
// top Y boundary: [bnd_y - nozzle_clearance_y, bnd_y]
// bottom Y boundary:[bnd_y, bnd_y + nozzle_clearance_y]
//
// 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* cw_opt = printer_cfg.option<ConfigOptionFloat>("ixex_nozzle_clearance_x");
auto* ch_opt = printer_cfg.option<ConfigOptionFloat>("ixex_nozzle_clearance_y");
auto* mgn_opt = printer_cfg.option<ConfigOptionFloat>("ixex_carriage_margin");
double carriage_w = cw_opt ? cw_opt->value : 0.0;
double carriage_h = ch_opt ? ch_opt->value : 0.0;
@@ -729,36 +729,36 @@ void PartPlate::calc_ixex_zones()
if (r == pri_row - 1 && c == pri_col) has_bottom_sec = true;
}
// X-axis boundaries (vertical strips, width = carriage_w/2 on primary side)
// X-axis boundaries (vertical strips, width = nozzle_clearance_x 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;
double strip_inner = bnd_x - carriage_w;
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;
double strip_inner = bnd_x + carriage_w;
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)
// Y-axis boundaries (horizontal strips, width = nozzle_clearance_y 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;
double strip_inner = bnd_y - carriage_h;
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;
double strip_inner = bnd_y + carriage_h;
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);
@@ -779,8 +779,8 @@ static std::string build_ixex_cache_key()
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* cw_opt = printer_cfg.option<ConfigOptionFloat>("ixex_nozzle_clearance_x");
auto* ch_opt = printer_cfg.option<ConfigOptionFloat>("ixex_nozzle_clearance_y");
auto* mgn_opt = printer_cfg.option<ConfigOptionFloat>("ixex_carriage_margin");
return (mode_opt ? mode_opt->value : "primary")
+ "|" + std::to_string(n_col_opt ? n_col_opt->value : 2)
+2 -2
View File
@@ -5002,8 +5002,8 @@ void TabPrinter::build_fff()
};
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_nozzle_clearance_x");
optgroup->append_single_option_line("ixex_nozzle_clearance_y");
optgroup->append_single_option_line("ixex_carriage_margin");
{