diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index b8df0aab53..4625a631f4 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -1042,7 +1042,7 @@ static std::vector 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" }; diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index 5c75c87622..cf4fba50d8 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -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; diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index 6c70772cda..1e89eba383 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -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)) diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp index 0ea8749b33..b4e2a9627c 100644 --- a/src/slic3r/GUI/GCodeViewer.cpp +++ b/src/slic3r/GUI/GCodeViewer.cpp @@ -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 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("ixex_mode_active_tools"); auto* tpg_opt = printer_cfg.opt("ixex_tools_per_gantry"); - auto* wx_opt = printer_cfg.opt("ixex_carriage_width_x"); - auto* wy_opt = printer_cfg.opt("ixex_carriage_width_y"); + auto* wx_opt = printer_cfg.opt("ixex_nozzle_clearance_x"); + auto* wy_opt = printer_cfg.opt("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()); + (draw.pos + Vec3f(draw.box_offset_x, draw.box_offset_y, m_z_offset)).cast()); shader->set_uniform("view_model_matrix", view_matrix * model_matrix); const Matrix3d view_normal_matrix = view_matrix.matrix().block(0, 0, 3, 3) * diff --git a/src/slic3r/GUI/GCodeViewer.hpp b/src/slic3r/GUI/GCodeViewer.hpp index 71880da962..ee3c1e1ec7 100644 --- a/src/slic3r/GUI/GCodeViewer.hpp +++ b/src/slic3r/GUI/GCodeViewer.hpp @@ -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 m_filament_diameters; diff --git a/src/slic3r/GUI/PartPlate.cpp b/src/slic3r/GUI/PartPlate.cpp index f48e12cb76..16a87009a1 100644 --- a/src/slic3r/GUI/PartPlate.cpp +++ b/src/slic3r/GUI/PartPlate.cpp @@ -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("ixex_carriage_width_x"); - auto* ch_opt = printer_cfg.option("ixex_carriage_width_y"); + auto* cw_opt = printer_cfg.option("ixex_nozzle_clearance_x"); + auto* ch_opt = printer_cfg.option("ixex_nozzle_clearance_y"); auto* mgn_opt = printer_cfg.option("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("ixex_parallel_mode"); auto* n_col_opt = printer_cfg.option("ixex_tools_per_gantry"); auto* n_row_opt = printer_cfg.option("ixex_gantry_count"); - auto* cw_opt = printer_cfg.option("ixex_carriage_width_x"); - auto* ch_opt = printer_cfg.option("ixex_carriage_width_y"); + auto* cw_opt = printer_cfg.option("ixex_nozzle_clearance_x"); + auto* ch_opt = printer_cfg.option("ixex_nozzle_clearance_y"); auto* mgn_opt = printer_cfg.option("ixex_carriage_margin"); return (mode_opt ? mode_opt->value : "primary") + "|" + std::to_string(n_col_opt ? n_col_opt->value : 2) diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index dd1d6d84f9..77283cfd8a 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -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"); {