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
+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) *