From 7a378d2fc4b06590d98bebe14a2bb8cebdc2257d Mon Sep 17 00:00:00 2001 From: SoftFever Date: Sun, 26 Jul 2026 01:22:53 +0800 Subject: [PATCH 01/16] Sync WipeTower from BambuStudio(through ca1881761) --- src/libslic3r/GCode.cpp | 22 +- src/libslic3r/GCode/GCodeProcessor.cpp | 4 +- src/libslic3r/GCode/WipeTower.cpp | 2123 ++++++++++++++---------- src/libslic3r/GCode/WipeTower.hpp | 219 ++- src/libslic3r/Polyline.hpp | 4 +- src/libslic3r/Print.cpp | 83 +- 6 files changed, 1453 insertions(+), 1002 deletions(-) diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index a08554654a..04ac442da3 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -998,6 +998,7 @@ static std::vector get_path_of_change_filament(const Print& print) std::string change_filament_gcode = gcodegen.config().change_filament_gcode.value; bool is_used_travel_avoid_perimeter = gcodegen.m_config.prime_tower_skip_points.value; + if (is_nozzle_change && !tcr.nozzle_change_result.is_extruder_change) is_used_travel_avoid_perimeter = false; // add nozzle change gcode into change filament gcode std::string nozzle_change_gcode_trans; @@ -1307,20 +1308,23 @@ static std::vector get_path_of_change_filament(const Print& print) } // do unretract after setting current extruder_id - // PETG filaments on a device with a filament switcher get a small (2 mm) pre-extrusion - // before the tool change. has_filament_switcher is a develop-only key read defensively from the - // full config (Orca does not carry it as a static PrintConfig member — same convention as - // enable_filament_dynamic_map); no shipping profile sets it (grep resources/profiles = 0), so - // is_petg_pre_extrusion is always false -> extra_unretract stays 0 -> byte-identical to the plain - // unretract() fleet-wide. The tower-interface contact pre-extrusion length (the - // is_contact_pre_extrusion branch) is NOT applied here; it is only computed as the guard used to - // give the contact path priority over PETG. + // BBS pattern: the wipe tower shifts the toolchange start position outward for the + // tower-interface (contact) pre-extrusion and for the PETG-with-filament-switcher case; + // the pre-extrusion material itself is laid down here as extra unretract on the approach. + // has_filament_switcher is a develop-only key read defensively from the full config (Orca + // does not carry it as a static PrintConfig member — same convention as + // enable_filament_dynamic_map); no shipping profile sets it, so is_petg_pre_extrusion is + // always false fleet-wide. const ConfigOptionBool* has_filament_switcher_opt = gcodegen.m_print->full_print_config().option("has_filament_switcher"); bool is_contact_pre_extrusion = tcr.is_contact && gcodegen.m_config.enable_tower_interface_features; bool is_petg_pre_extrusion = !is_contact_pre_extrusion && gcodegen.config().filament_type.get_at(tcr.new_tool) == "PETG" && has_filament_switcher_opt && has_filament_switcher_opt->value; - float extra_unretract = is_petg_pre_extrusion ? 2.f : 0.f; + float extra_unretract = 0.f; + if (is_contact_pre_extrusion) + extra_unretract = gcodegen.m_config.filament_tower_interface_pre_extrusion_length.get_at(tcr.new_tool); + else if (is_petg_pre_extrusion) + extra_unretract = 2.f; std::string toolchange_unretract_str = (extra_unretract > 0.f) ? gcodegen.unretract(extra_unretract) : gcodegen.unretract(); check_add_eol(toolchange_unretract_str); diff --git a/src/libslic3r/GCode/GCodeProcessor.cpp b/src/libslic3r/GCode/GCodeProcessor.cpp index cc53cd0e51..d66398354c 100644 --- a/src/libslic3r/GCode/GCodeProcessor.cpp +++ b/src/libslic3r/GCode/GCodeProcessor.cpp @@ -1450,8 +1450,8 @@ void GCodeProcessor::run_post_process() // flag) runs none of this. It is pure data construction — it only fills m_filament_blocks / // m_extruder_blocks / m_machine_*_gcode_*_line_id and never touches the exported g-code, so even // the enable_pre_heating fleet stays byte-identical (nothing reads the blocks until the injection - // pass). In practice it also stays empty/degenerate today because no template/code yet emits the - // MACHINE_*_GCODE_* / NOZZLE_CHANGE_* / CP_TOOLCHANGE_WIPE markers it keys off. + // pass). The wipe tower emits the NOZZLE_CHANGE_* (ramming) and CP_TOOLCHANGE_WIPE markers this + // builder keys off; the MACHINE_*_GCODE_* markers come from the machine g-code templates. m_filament_blocks.clear(); m_extruder_blocks.clear(); m_machine_start_gcode_end_line_id = (unsigned int) (-1); diff --git a/src/libslic3r/GCode/WipeTower.cpp b/src/libslic3r/GCode/WipeTower.cpp index 06bca292d4..3fbc0cccaa 100644 --- a/src/libslic3r/GCode/WipeTower.cpp +++ b/src/libslic3r/GCode/WipeTower.cpp @@ -3,11 +3,9 @@ #include #include #include -#include #include #include #include - #include "GCodeProcessor.hpp" #include "BoundingBox.hpp" #include "ClipperUtils.hpp" @@ -17,12 +15,16 @@ namespace Slic3r { -constexpr float flat_iron_speed = 10.f * 60.f; +float flat_iron_speed = 10.f * 60.f; static const double wipe_tower_wall_infill_overlap = 0.0; static constexpr double WIPE_TOWER_RESOLUTION = 0.1; +// Orca: SCALING_FACTOR is a runtime variable (large-printer switch), so this cannot be constexpr #define WT_SIMPLIFY_TOLERANCE_SCALED (0.001 / SCALING_FACTOR) static constexpr int arc_fit_size = 20; #define SCALED_WIPE_TOWER_RESOLUTION (WIPE_TOWER_RESOLUTION / SCALING_FACTOR) +enum class LimitFlow { None, LimitPrintFlow, LimitRammingFlow, LimitRammingFlowNC};//nc:nozzle change +static const std::map nozzle_diameter_to_nozzle_change_width{{0.2f, 0.5f}, {0.4f, 1.0f}, {0.6f, 1.2f}, {0.8f, 1.4f}}; + inline float align_round(float value, float base) { return std::round(value / base) * base; @@ -67,7 +69,7 @@ static bool is_valid_gcode(const std::string &gcode) return is_valid; } -static Polygon chamfer_polygon(Polygon &polygon, double chamfer_dis = 2., double angle_tol = 30. / 180. * PI) +Polygon chamfer_polygon(Polygon &polygon, double chamfer_dis = 2., double angle_tol = 30. / 180. * PI) { if (polygon.points.size() < 3) return polygon; Polygon res; @@ -169,7 +171,7 @@ Polygon WipeTower::rounding_polygon(Polygon &polygon, double rounding /*= 2.*/, return res; } -static Polygon rounding_rectangle(Polygon &polygon, double rounding = 2., double angle_tol = 30. / 180. * PI) { +Polygon rounding_rectangle(Polygon &polygon, double rounding = 2., double angle_tol = 30. / 180. * PI) { if (polygon.points.size() < 3) return polygon; Polygon res; res.points.reserve(polygon.points.size() * 2); @@ -226,7 +228,7 @@ static Polygon rounding_rectangle(Polygon &polygon, double rounding = 2., double return res; } -static std::pair ray_intersetion_line(const Vec2f &a, const Vec2f &v1, const Vec2f &b, const Vec2f &c) +std::pair ray_intersetion_line(const Vec2f &a, const Vec2f &v1, const Vec2f &b, const Vec2f &c) { const Vec2f v2 = c - b; double denom = cross2(v1, v2); @@ -243,19 +245,19 @@ static std::pair ray_intersetion_line(const Vec2f &a, const Vec2f & } return std::pair(false, Vec2f{0, 0}); } -static Polygon scale_polygon(const std::vector &points) { +Polygon scale_polygon(const std::vector &points) { Polygon res; for (const auto &p : points) res.points.push_back(scaled(p)); return res; } -static std::vector unscale_polygon(const Polygon& polygon) +std::vector unscale_polygon(const Polygon& polygon) { std::vector res; for (const auto &p : polygon.points) res.push_back(unscaled(p)); return res; } -static Polygon generate_rectange(const Line &line, coord_t offset) +Polygon generate_rectange(const Line &line, coord_t offset) { Point p1 = line.a; Point p2 = line.b; @@ -294,7 +296,7 @@ struct Segment bool is_valid() const { return start.y() < end.y(); } }; -static std::vector remove_points_from_segment(const Segment &segment, const std::vector &skip_points, double range) +std::vector remove_points_from_segment(const Segment &segment, const std::vector &skip_points, double range) { std::vector result; result.push_back(segment); @@ -333,7 +335,7 @@ struct PointWithFlag int pair_idx; // gap_pair idx bool is_forward; }; -static IntersectionInfo move_point_along_polygon(const std::vector &points, const Vec2f &startPoint, int startIdx, float offset, bool forward, int pair_idx) +IntersectionInfo move_point_along_polygon(const std::vector &points, const Vec2f &startPoint, int startIdx, float offset, bool forward, int pair_idx) { float remainingDistance = offset; IntersectionInfo res; @@ -395,7 +397,7 @@ static IntersectionInfo move_point_along_polygon(const std::vector &point return res; }; -static void insert_points(std::vector &pl, int idx, Vec2f pos, int pair_idx, bool is_forward) +void insert_points(std::vector &pl, int idx, Vec2f pos, int pair_idx, bool is_forward) { int next = (idx + 1) % pl.size(); Vec2f pos1 = pl[idx].pos; @@ -411,15 +413,111 @@ static void insert_points(std::vector &pl, int idx, Vec2f pos, in } } -static Polylines remove_points_from_polygon(const Polygon &polygon, const std::vector &skip_points, double range, bool is_left ,Polygon& insert_skip_pg) +// For skip_point +// TODO: Optimize the skip_point algorithm itself instead of adding guards here +Polygon add_extra_point(const Polygon &polygon, int scale_range) { - assert(polygon.size() > 2); + Polygon res; + if (polygon.size() < 2) return polygon; + + // Compute bounding box of the polygon + auto polygon_box = get_extents(polygon); + + // Anchor point: X at bbox center, Y at bbox bottom + Vec2f anchor_point(float(polygon_box.center()[0]), float(polygon_box.min[1])); + + // Find the edge whose midpoint is closest to the anchor point + size_t closest_edge_idx = 0; + float min_dist_sq = std::numeric_limits::max(); + + for (size_t i = 0; i < polygon.size(); ++i) { + const Point &a_i = polygon[i]; + const Point &b_i = polygon[(i + 1) % polygon.size()]; + + Vec2f a(float(a_i.x()), float(a_i.y())); + Vec2f b(float(b_i.x()), float(b_i.y())); + Vec2f mid = (a + b) * 0.5f; + + float dist_sq = (anchor_point - mid).squaredNorm(); + if (dist_sq < min_dist_sq) { + min_dist_sq = dist_sq; + closest_edge_idx = i; + } + } + + // Edge endpoints (integer space) + const Point &a_i = polygon[closest_edge_idx]; + const Point &b_i = polygon[(closest_edge_idx + 1) % polygon.size()]; + + // Convert to float for geometric computation + Vec2f a(float(a_i.x()), float(a_i.y())); + Vec2f b(float(b_i.x()), float(b_i.y())); + + Vec2f mid = (a + b) * 0.5f; + + // Direction vectors from midpoint towards A and B + Vec2f dir_to_a = a - mid; + Vec2f dir_to_b = b - mid; + + float len_a = dir_to_a.norm(); + float len_b = dir_to_b.norm(); + + // Guard against degenerated edges + if (len_a < EPSILON || len_b < EPSILON) return polygon; + + dir_to_a /= len_a; + dir_to_b /= len_b; + + // Clamp range to avoid overshooting the edge + float max_range = std::min(len_a, len_b) * 0.9f; + float range = std::min(float(scale_range), max_range); + + // Offset points (float space) + Vec2f offset_to_a_f = mid + dir_to_a * range; + Vec2f offset_to_b_f = mid + dir_to_b * range; + + // Safe cast back to scaled integer Point + auto to_int_point = [](const Vec2f &p) { + auto clamp = [](float v) -> coord_t { + constexpr float kMin = float(std::numeric_limits::min()); + constexpr float kMax = float(std::numeric_limits::max()); + v = std::clamp(v, kMin, kMax); + return static_cast(std::lround(v)); + }; + return Point(clamp(p.x()), clamp(p.y())); + }; + + Point mid_i = to_int_point(mid); + Point offset_to_a_i = to_int_point(offset_to_a_f); + Point offset_to_b_i = to_int_point(offset_to_b_f); + + // Rebuild polygon with inserted points + for (size_t i = 0; i < polygon.size(); ++i) { + res.points.push_back(polygon[i]); + + // Insert points right after the selected edge start vertex + if (i == closest_edge_idx) { + res.points.push_back(offset_to_a_i); + res.points.push_back(mid_i); + res.points.push_back(offset_to_b_i); + } + } + + return res; +} + + + +Polylines remove_points_from_polygon(const Polygon &polygon_ori, const std::vector &skip_points, double range, float wt_width, Polygon &insert_skip_pg) +{ + Polygon polygon = add_extra_point(polygon_ori, scale_(range)); + if (polygon.size() < 2) return Polylines{to_polyline(polygon)}; Polylines result; std::vector new_pl; // add intersection points for gaps, where bool indicates whether it's a gap point. std::vector inter_info; - Vec2f ray = is_left ? Vec2f(-1, 0) : Vec2f(1, 0); auto polygon_box = get_extents(polygon); - Point anchor_point = is_left ? Point{polygon_box.max[0], polygon_box.min[1]} : polygon_box.min; // rd:ld + //Point anchor_point = /*is_left ? Point{polygon_box.max[0], polygon_box.min[1]} :*/ polygon_box.min; // rd:ld + Point anchor_point = Point{polygon_box.center()[0], polygon_box.min[1]}; // for next reconnect std::vector points; { points.reserve(polygon.points.size()); @@ -430,6 +528,8 @@ static Polylines remove_points_from_polygon(const Polygon &polygon, const std::v } for (int i = 0; i < skip_points.size(); i++) { + bool is_left = abs(skip_points[i].x()) < wt_width / 2.f; + Vec2f ray = is_left ? Vec2f(-1, 0) : Vec2f(1, 0); for (int j = 0; j < points.size(); j++) { Vec2f& p1 = points[j]; Vec2f& p2 = points[(j + 1) % points.size()]; @@ -493,22 +593,22 @@ static Polylines remove_points_from_polygon(const Polygon &polygon, const std::v return result; } -static Polylines contrust_gap_for_skip_points(const Polygon &polygon, const std::vector & skip_points ,float wt_width,float gap_length,Polygon& insert_skip_polygon) +Polylines contrust_gap_for_skip_points(const Polygon &polygon, const std::vector & skip_points ,float wt_width,float gap_length,Polygon& insert_skip_polygon) { if (skip_points.empty()) { insert_skip_polygon = polygon; return Polylines{to_polyline(polygon)}; } - bool is_left = false; - const auto &pt = skip_points.front(); - if (abs(pt.x()) < wt_width/2.f) { - is_left = true; - } - return remove_points_from_polygon(polygon, skip_points, gap_length, is_left, insert_skip_polygon); + //bool is_left = false; + //const auto &pt = skip_points.front(); + //if (abs(pt.x()) < wt_width/2.f) { + // is_left = true; + //} + return remove_points_from_polygon(polygon, skip_points, gap_length, wt_width, insert_skip_polygon); }; -static Polygon generate_rectange_polygon(const Vec2f &wt_box_min ,const Vec2f & wt_box_max) { +Polygon generate_rectange_polygon(const Vec2f &wt_box_min ,const Vec2f & wt_box_max) { Polygon res; res.points.push_back(scaled(wt_box_min)); res.points.push_back(scaled(Vec2f{wt_box_max[0], wt_box_min[1]})); @@ -520,7 +620,7 @@ static Polygon generate_rectange_polygon(const Vec2f &wt_box_min ,const Vec2f & class WipeTowerWriter { public: - WipeTowerWriter(float layer_height, float line_width, GCodeFlavor flavor, const std::vector& filament_parameters) : + WipeTowerWriter(float layer_height, float line_width, GCodeFlavor flavor, const std::vector& filament_parameters, bool enable_arc_fitting) : m_current_pos(std::numeric_limits::max(), std::numeric_limits::max()), m_current_z(0.f), m_current_feedrate(0.f), @@ -528,9 +628,13 @@ public: m_extrusion_flow(0.f), m_preview_suppressed(false), m_elapsed_time(0.f), - m_gcode_flavor(flavor), - m_filpar(filament_parameters) - { +#if ENABLE_GCODE_VIEWER_DATA_CHECKING + m_default_analyzer_line_width(line_width), +#endif // ENABLE_GCODE_VIEWER_DATA_CHECKING + m_gcode_flavor(flavor), + m_enable_arc_fitting(enable_arc_fitting), + m_filpar(filament_parameters) + { // ORCA: This class is only used by BBL printers, so set the parameter appropriately. // This fixes an issue where the wipe tower was using BBL tags resulting in statistics for purging in the purge tower not being displayed. GCodeProcessor::s_IsBBLPrinter = true; @@ -550,6 +654,18 @@ public: return *this; } +#if ENABLE_GCODE_VIEWER_DATA_CHECKING + WipeTowerWriter& change_analyzer_mm3_per_mm(float len, float e) { + static const float area = float(M_PI) * 1.75f * 1.75f / 4.f; + float mm3_per_mm = (len == 0.f ? 0.f : area * e / len); + // adds tag for processor: + std::stringstream str; + str << ";" << GCodeProcessor::Mm3_Per_Mm_Tag << mm3_per_mm << "\n"; + m_gcode += str.str(); + return *this; + } +#endif // ENABLE_GCODE_VIEWER_DATA_CHECKING + WipeTowerWriter& set_initial_position(const Vec2f &pos, float width = 0.f, float depth = 0.f, float internal_angle = 0.f) { m_wipe_tower_width = width; m_wipe_tower_depth = depth; @@ -587,8 +703,13 @@ public: // Suppress / resume G-code preview in Slic3r. Slic3r will have difficulty to differentiate the various // filament loading and cooling moves from normal extrusion moves. Therefore the writer // is asked to suppres output of some lines, which look like extrusions. +#if ENABLE_GCODE_VIEWER_DATA_CHECKING + WipeTowerWriter& suppress_preview() { change_analyzer_line_width(0.f); m_preview_suppressed = true; return *this; } + WipeTowerWriter& resume_preview() { change_analyzer_line_width(m_default_analyzer_line_width); m_preview_suppressed = false; return *this; } +#else WipeTowerWriter& suppress_preview() { m_preview_suppressed = true; return *this; } - WipeTowerWriter& resume_preview() { m_preview_suppressed = false; return *this; } + WipeTowerWriter& resume_preview() { m_preview_suppressed = false; return *this; } +#endif // ENABLE_GCODE_VIEWER_DATA_CHECKING WipeTowerWriter& feedrate(float f) { @@ -610,9 +731,9 @@ public: float get_and_reset_used_filament_length() { float temp = m_used_filament_length; m_used_filament_length = 0.f; return temp; } // Extrude with an explicitely provided amount of extrusion. - WipeTowerWriter& extrude_explicit(float x, float y, float e, float f = 0.f, bool record_length = false, bool limit_volumetric_flow = true) + WipeTowerWriter &extrude_explicit(float x, float y, float e, float f = 0.f, bool record_length = false ,LimitFlow limit_flow = LimitFlow::LimitPrintFlow) { - if (x == m_current_pos.x() && y == m_current_pos.y() && e == 0.f && (f == 0.f || f == m_current_feedrate)) + if ((std::abs(x - m_current_pos.x()) <= (float)EPSILON) && (std::abs(y - m_current_pos.y()) < (float)EPSILON) && e == 0.f && (f == 0.f || f == m_current_feedrate)) // Neither extrusion nor a travel move. return *this; @@ -627,9 +748,12 @@ public: Vec2f rot(this->rotate(Vec2f(x,y))); // this is where we want to go if (! m_preview_suppressed && e > 0.f && len > 0.f) { - // Width of a squished extrusion, corrected for the roundings of the squished extrusions. +#if ENABLE_GCODE_VIEWER_DATA_CHECKING + change_analyzer_mm3_per_mm(len, e); +#endif // ENABLE_GCODE_VIEWER_DATA_CHECKING + // Width of a squished extrusion, corrected for the roundings of the squished extrusions. // This is left zero if it is a travel move. - float width = e * m_filpar[0].filament_area / (len * m_layer_height); + float width = e * m_filpar[0].filament_area / (len * m_layer_height); // Correct for the roundings of a squished extrusion. width += m_layer_height * float(1. - M_PI / 4.); if (m_extrusions.empty() || m_extrusions.back().pos != rotated_current_pos) @@ -637,6 +761,12 @@ public: m_extrusions.emplace_back(WipeTower::Extrusion(rot, width, m_current_tool)); } + if (e == 0.f) { + m_gcode += set_travel_acceleration(); + } else { + m_gcode += set_normal_acceleration(); + } + m_gcode += "G1"; if (std::abs(rot.x() - rotated_current_pos.x()) > (float)EPSILON) m_gcode += set_format_X(rot.x()); @@ -649,9 +779,12 @@ public: m_gcode += set_format_E(e); if (f != 0.f && f != m_current_feedrate) { - if (limit_volumetric_flow) { + if (limit_flow!= LimitFlow::None) { float e_speed = e / (((len == 0.f) ? std::abs(e) : len) / f * 60.f); - f /= std::max(1.f, e_speed / m_filpar[m_current_tool].max_e_speed); + float tmp = m_filpar[m_current_tool].max_e_speed; + if (limit_flow == LimitFlow::LimitRammingFlow) tmp = m_filpar[m_current_tool].max_e_ramming_speed.first; + else if (limit_flow == LimitFlow::LimitRammingFlowNC) tmp = m_filpar[m_current_tool].max_e_ramming_speed.second; + f /= std::max(1.f, e_speed / tmp); } m_gcode += set_format_F(f); } @@ -666,7 +799,7 @@ public: } // Extrude with an explicitely provided amount of extrusion. - WipeTowerWriter &extrude_arc_explicit(ArcSegment &arc, float f = 0.f, bool record_length = false, bool limit_volumetric_flow = true) + WipeTowerWriter &extrude_arc_explicit(ArcSegment &arc, float f = 0.f, bool record_length = false, LimitFlow limit_flow = LimitFlow::LimitPrintFlow) { float x = (float)unscale(arc.end_point).x(); float y = (float)unscale(arc.end_point).y(); @@ -706,6 +839,13 @@ public: } } + + if (e == 0.f) { + m_gcode += set_travel_acceleration(); + } else { + m_gcode += set_normal_acceleration(); + } + m_gcode += arc.direction == ArcDirection::Arc_Dir_CCW ? "G3" : "G2"; const Vec2f center_offset = this->rotate(unscaled(arc.center)) - rotated_current_pos; m_gcode += set_format_X(rot.x()); @@ -716,9 +856,13 @@ public: if (e != 0.f) m_gcode += set_format_E(e); if (f != 0.f && f != m_current_feedrate) { - if (limit_volumetric_flow) { + if (limit_flow != LimitFlow::None) { float e_speed = e / (((len == 0.f) ? std::abs(e) : len) / f * 60.f); - f /= std::max(1.f, e_speed / m_filpar[m_current_tool].max_e_speed); + float tmp = m_filpar[m_current_tool].max_e_speed; + if (limit_flow == LimitFlow::LimitRammingFlow) tmp = m_filpar[m_current_tool].max_e_ramming_speed.first; + else if (limit_flow == LimitFlow::LimitRammingFlowNC) + tmp = m_filpar[m_current_tool].max_e_ramming_speed.second; + f /= std::max(1.f, e_speed / tmp); } m_gcode += set_format_F(f); } @@ -732,8 +876,10 @@ public: return *this; } - WipeTowerWriter& extrude_explicit(const Vec2f &dest, float e, float f = 0.f, bool record_length = false, bool limit_volumetric_flow = true) - { return extrude_explicit(dest.x(), dest.y(), e, f, record_length); } + WipeTowerWriter &extrude_explicit(const Vec2f &dest, float e, float f = 0.f, bool record_length = false, LimitFlow limit_flow = LimitFlow::LimitPrintFlow) + { + return extrude_explicit(dest.x(), dest.y(), e, f, record_length, limit_flow); + } // Travel to a new XY position. f=0 means use the current value. WipeTowerWriter& travel(float x, float y, float f = 0.f) @@ -743,15 +889,15 @@ public: { return extrude_explicit(dest.x(), dest.y(), 0.f, f); } // Extrude a line from current position to x, y with the extrusion amount given by m_extrusion_flow. - WipeTowerWriter& extrude(float x, float y, float f = 0.f) + WipeTowerWriter &extrude(float x, float y, float f = 0.f, LimitFlow limit_flow = LimitFlow::LimitPrintFlow) { float dx = x - m_current_pos.x(); float dy = y - m_current_pos.y(); - return extrude_explicit(x, y, std::sqrt(dx*dx+dy*dy) * m_extrusion_flow, f, true); + return extrude_explicit(x, y, std::sqrt(dx * dx + dy * dy) * m_extrusion_flow, f, false, limit_flow); } - WipeTowerWriter &extrude_arc(ArcSegment &arc, float f = 0.f) + WipeTowerWriter &extrude_arc(ArcSegment &arc, float f = 0.f, LimitFlow limit_flow = LimitFlow::LimitPrintFlow) { - return extrude_arc_explicit(arc, f, true); + return extrude_arc_explicit(arc, f, false , limit_flow); } WipeTowerWriter& extrude(const Vec2f &dest, const float f = 0.f) @@ -866,7 +1012,12 @@ public: { Polyline pl = to_polyline(wall_polygon); pl.simplify(WT_SIMPLIFY_TOLERANCE_SCALED); - pl.simplify_by_fitting_arc(SCALED_WIPE_TOWER_RESOLUTION); + if (m_enable_arc_fitting) { + pl.simplify_by_fitting_arc(SCALED_WIPE_TOWER_RESOLUTION); + } else { + pl.simplify(SCALED_WIPE_TOWER_RESOLUTION); + pl.reset_to_linear_move(); + } auto get_closet_idx = [this](std::vector &corners) -> int { Vec2f anchor{this->m_current_pos.x(), this->m_current_pos.y()}; @@ -895,6 +1046,9 @@ public: } } + if (segments.empty()) + return (*this); + int index_of_closest = get_closet_idx(segments); int i = index_of_closest; travel(segments[i].start); // travel to the closest points @@ -937,7 +1091,7 @@ public: } float end_point = x() + (farthest_x > x() ? 1.f : -1.f) * x_distance; - return extrude_explicit(end_point, y(), loading_dist, x_speed * 60.f, false, false); + return extrude_explicit(end_point, y(), loading_dist, x_speed * 60.f, false, LimitFlow::None); } // Elevate the extruder head above the current print_z position. @@ -958,8 +1112,8 @@ public: // extrude quickly amount e to x2 with feed f. WipeTowerWriter& ram(float x1, float x2, float dy, float e0, float e, float f) { - extrude_explicit(x1, m_current_pos.y() + dy, e0, f, true, false); - extrude_explicit(x2, m_current_pos.y(), e, 0.f, true, false); + extrude_explicit(x1, m_current_pos.y() + dy, e0, f, true, LimitFlow::None); + extrude_explicit(x2, m_current_pos.y(), e, 0.f, true, LimitFlow::None); return *this; } @@ -968,8 +1122,8 @@ public: // at the current Y position to spread the leaking material. WipeTowerWriter& cool(float x1, float x2, float e1, float e2, float f) { - extrude_explicit(x1, m_current_pos.y(), e1, f, false, false); - extrude_explicit(x2, m_current_pos.y(), e2, false, false); + extrude_explicit(x1, m_current_pos.y(), e1, f, false, LimitFlow::None); + extrude_explicit(x2, m_current_pos.y(), e2, 0.f, false, LimitFlow::None); return *this; } @@ -1002,26 +1156,22 @@ public: return *this; } - // Let the firmware back up the active speed override value. - WipeTowerWriter& speed_override_backup() + // Let the firmware back up the active speed override value. + WipeTowerWriter& speed_override_backup() { // BBS: BBL machine don't support speed backup -#if 0 if (m_gcode_flavor == gcfMarlinLegacy || m_gcode_flavor == gcfMarlinFirmware) m_gcode += "M220 B\n"; -#endif - return *this; + return *this; } - // Let the firmware restore the active speed override value. - WipeTowerWriter& speed_override_restore() - { - // BBS: BBL machine don't support speed restore -#if 0 + // Let the firmware restore the active speed override value. + WipeTowerWriter& speed_override_restore() + { + // BBS: BBL machine don't support speed restore if (m_gcode_flavor == gcfMarlinLegacy || m_gcode_flavor == gcfMarlinFirmware) m_gcode += "M220 R\n"; -#endif - return *this; + return *this; } // Set digital trimpot motor @@ -1115,7 +1265,14 @@ public: } return closestIndex; }; - for (auto &pl : pls) pl.simplify_by_fitting_arc(SCALED_WIPE_TOWER_RESOLUTION); + if (m_enable_arc_fitting) { + for (auto &pl : pls) pl.simplify_by_fitting_arc(SCALED_WIPE_TOWER_RESOLUTION); + } else { + for (auto &pl : pls) { + pl.simplify(SCALED_WIPE_TOWER_RESOLUTION); + pl.reset_to_linear_move(); + } + } std::vector segments; for (const auto &pl : pls) { @@ -1131,9 +1288,11 @@ public: segments.back().is_arc = true; segments.back().arcsegment = pl.fitting_result[i].arc_data; } - } } + if (segments.empty()) + return; + int index_of_closest = get_closet_idx(segments); int i = index_of_closest; travel(segments[i].start); // travel to the closest points @@ -1158,7 +1317,7 @@ public: Vec2f box_max = center + Vec2f{step_length, step_length}; Vec2f box_min = center - Vec2f{step_length, step_length}; int n = std::ceil(edge_length / step_length / 2.f); - assert(n > 0); + if (n <= 0) return; while (n--) { travel(box_max.x(), m_current_pos.y(), feedrate); travel(m_current_pos.x(), box_max.y(), feedrate); @@ -1170,6 +1329,120 @@ public: } } + WipeTowerWriter &format_line_M104(int target_temp, int target_extruder, bool wait_for_moves = true, const std::string &comment = std::string()) + { + std::string buffer; + if (wait_for_moves) + buffer += "M400\n"; + buffer += "M104"; + if (target_extruder != -1) + buffer += (" T" + std::to_string(m_physical_extruder_map[target_extruder])); + buffer += " S" + std::to_string(target_temp) + " N0"; // N0 means the gcode is generated by slicer + if (!comment.empty()) buffer += " ;" + comment; + buffer += '\n'; + append(buffer); + return *this; + } + + WipeTowerWriter &format_line_M109(int target_temp, int target_extruder, const std::string &comment = std::string()) + { + std::string buffer = "M109"; + if (target_extruder != -1) + buffer += (" T" + std::to_string(m_physical_extruder_map[target_extruder])); + buffer += " S" + std::to_string(target_temp) + " N0"; // N0 means the gcode is generated by slicer + if (!comment.empty()) buffer += " ;" + comment; + buffer += '\n'; + append(buffer); + return *this; + }; + + void set_first_layer(bool is_first_layer) { m_is_first_layer = is_first_layer; } + void set_normal_acceleration(const std::vector &accelerations) { m_normal_accelerations = accelerations; }; + void set_first_layer_normal_acceleration(const std::vector &accelerations) { m_first_layer_normal_accelerations = accelerations; }; + void set_travel_acceleration(const std::vector &accelerations) { m_travel_accelerations = accelerations; }; + void set_first_layer_travel_acceleration(const std::vector &accelerations) { m_first_layer_travel_accelerations = accelerations; }; + void set_max_acceleration(unsigned int acceleration) { m_max_acceleration = acceleration; }; + void set_accel_to_decel_enable(bool enable) { m_accel_to_decel_enable = enable; } + void set_accel_to_decel_factor(float factor) { m_accel_to_decel_factor = factor; } + void set_layer_id(int layer_id) { m_layer_id = layer_id; } + void set_multi_nozzle_group_result(const MultiNozzleUtils::LayeredNozzleGroupResult *multi_nozzle_group_result) { m_multi_nozzle_group_result = multi_nozzle_group_result; } + void set_physical_extruder_map(const std::vector &physical_extruder_map) { m_physical_extruder_map = physical_extruder_map; } + +private: + std::string set_normal_acceleration() { + std::vector accelerations = m_is_first_layer ? m_first_layer_normal_accelerations : m_normal_accelerations; + if (accelerations.empty() || !m_multi_nozzle_group_result) + return std::string(); + int extruder_id = m_multi_nozzle_group_result->get_extruder_id(m_current_tool, m_layer_id); + // Orca: get_extruder_id returns -1 when the filament is not covered by the map + // (reachable with a stale manual filament map); skip instead of indexing out of bounds. + if (extruder_id < 0 || extruder_id >= (int) accelerations.size()) + return std::string(); + unsigned int acc = accelerations[extruder_id]; + return set_acceleration_impl(acc); + } + std::string set_travel_acceleration() + { + std::vector accelerations = m_is_first_layer ? m_first_layer_travel_accelerations : m_travel_accelerations; + if (accelerations.empty() || !m_multi_nozzle_group_result) + return std::string(); + int extruder_id = m_multi_nozzle_group_result->get_extruder_id(m_current_tool, m_layer_id); + // Orca: get_extruder_id returns -1 when the filament is not covered by the map + // (reachable with a stale manual filament map); skip instead of indexing out of bounds. + if (extruder_id < 0 || extruder_id >= (int) accelerations.size()) + return std::string(); + unsigned int acc = accelerations[extruder_id]; + return set_acceleration_impl(acc); + } + std::string set_acceleration_impl(unsigned int acceleration) { + // Clamp the acceleration to the allowed maximum. + if (m_max_acceleration > 0 && acceleration > m_max_acceleration) + acceleration = m_max_acceleration; + + if (acceleration == 0 || acceleration == m_last_acceleration) + return std::string(); + + m_last_acceleration = acceleration; + + std::ostringstream gcode; + if (m_gcode_flavor == gcfRepetier) { + // M201: Set max printing acceleration + gcode << "M201 X" << acceleration << " Y" << acceleration; + gcode << "\n"; + // M202: Set max travel acceleration + gcode << "M202 X" << acceleration << " Y" << acceleration; + } else if (m_gcode_flavor == gcfRepRapFirmware) { + // M204: Set default acceleration + gcode << "M204 P" << acceleration; + } else if (m_gcode_flavor == gcfMarlinFirmware) { + // This is new MarlinFirmware with separated print/retraction/travel acceleration. + // Use M204 P, we don't want to override travel acc by M204 S (which is deprecated anyway). + gcode << "M204 P" << acceleration; + } + else if (m_gcode_flavor == gcfKlipper && m_accel_to_decel_enable) { + gcode << "SET_VELOCITY_LIMIT ACCEL_TO_DECEL=" << acceleration * m_accel_to_decel_factor / 100; + gcode << "\nM204 S" << acceleration; + } + else { + // M204: Set default acceleration + gcode << "M204 S" << acceleration; + } + gcode << "\n"; + return gcode.str(); + } + std::vector m_normal_accelerations; + std::vector m_first_layer_normal_accelerations; + std::vector m_travel_accelerations; + std::vector m_first_layer_travel_accelerations; + bool m_is_first_layer{false}; + unsigned int m_max_acceleration{0}; + unsigned int m_last_acceleration{0}; + bool m_accel_to_decel_enable; + float m_accel_to_decel_factor; + const MultiNozzleUtils::LayeredNozzleGroupResult *m_multi_nozzle_group_result{nullptr}; + int m_layer_id = -1; + std::vector m_physical_extruder_map; + private: Vec2f m_start_pos; Vec2f m_current_pos; @@ -1189,8 +1462,12 @@ private: float m_wipe_tower_depth = 0.f; unsigned m_last_fan_speed = 0; int current_temp = -1; +#if ENABLE_GCODE_VIEWER_DATA_CHECKING + const float m_default_analyzer_line_width; +#endif // ENABLE_GCODE_VIEWER_DATA_CHECKING float m_used_filament_length = 0.f; GCodeFlavor m_gcode_flavor; + bool m_enable_arc_fitting = true; const std::vector& m_filpar; std::string set_format_X(float x) @@ -1244,7 +1521,7 @@ WipeTower::ToolChangeResult WipeTower::construct_tcr(WipeTowerWriter& writer, bool is_finish, bool is_tool_change, float purge_volume, - bool is_contact) const + bool is_contact ) const { ToolChangeResult result; result.priming = priming; @@ -1261,9 +1538,8 @@ WipeTower::ToolChangeResult WipeTower::construct_tcr(WipeTowerWriter& writer, result.is_finish_first = is_finish; result.nozzle_change_result = m_nozzle_change_result; result.is_tool_change = is_tool_change; - result.is_contact = is_contact; result.tool_change_start_pos = is_tool_change ? result.start_pos : Vec2f(0, 0); - + result.is_contact = is_contact; // BBS result.purge_volume = purge_volume; return result; @@ -1285,7 +1561,6 @@ WipeTower::ToolChangeResult WipeTower::construct_block_tcr(WipeTowerWriter &writ result.wipe_path = std::move(writer.wipe_path()); result.is_finish_first = is_finish; result.is_tool_change = false; - result.is_contact = false; result.tool_change_start_pos = Vec2f(0, 0); // BBS result.purge_volume = purge_volume; @@ -1480,40 +1755,43 @@ WipeTower::WipeTower(const PrintConfig& config, int plate_idx, Vec3d plate_origi m_enable_wrapping_detection(config.enable_wrapping_detection), m_wrapping_detection_layers(config.wrapping_detection_layers.value && (config.wrapping_exclude_area.values.size() > 2)), m_slice_used_filaments(slice_used_filaments.size()), - m_filaments_change_length(config.filament_change_length.values), m_is_multi_extruder(config.nozzle_diameter.size() > 1), m_use_gap_wall(config.prime_tower_skip_points.value), + // Orca: rib-wall options live under wipe_tower_* names and the wall type is an enum m_use_rib_wall(config.wipe_tower_wall_type.value == WipeTowerWallType::wtwRib), m_extra_rib_length((float)config.wipe_tower_extra_rib_length.value), m_rib_width((float)config.wipe_tower_rib_width.value), m_used_fillet(config.wipe_tower_fillet_wall.value), m_extra_spacing((float)config.prime_tower_infill_gap.value/100.f), m_tower_framework(config.prime_tower_enable_framework.value), + // Orca: prime_tower_max_speed is named wipe_tower_max_purge_speed (same default/min) + m_max_speed((float)config.wipe_tower_max_purge_speed.value*60.f), + m_accel_to_decel_enable(config.accel_to_decel_enable.value), + m_accel_to_decel_factor(config.accel_to_decel_factor.value), + m_printable_height(config.extruder_printable_height.values), m_flat_ironing(config.prime_tower_flat_ironing.value), m_enable_tower_interface_features(config.enable_tower_interface_features.value), - m_enable_tower_interface_cooldown_during_tower(config.enable_tower_interface_cooldown_during_tower.value) + m_physical_extruder_map(config.physical_extruder_map.values), + m_enable_arc_fitting(config.enable_arc_fitting.value) + // Orca: has_filament_switcher is a device-set dynamic key, not a PrintConfig member; + // it is pushed in from Print via set_has_filament_switcher() instead of read here. { + m_contact_speed = 20 * 60.f; + m_filaments_change_length.first = config.filament_change_length.values; + m_filaments_change_length.second = config.filament_change_length_nc.values; + m_hotend_heating_rate = config.hotend_heating_rate.values; + m_hotend_cooling_rate = config.hotend_cooling_rate.values; m_flat_ironing = (m_flat_ironing && m_use_gap_wall); - - // Prime-tower heating during wipe. m_is_multiple_nozzle mirrors the gate used in ToolOrdering/GCode - // (std::any_of extruder_max_nozzle_count > 1); it is false for every current printer, so the - // heating-during-wipe logic in toolchange_wipe_new is inert. - m_hotend_heating_rate = config.hotend_heating_rate.values; - m_physical_extruder_map = config.physical_extruder_map.values; - m_is_multiple_nozzle = std::any_of(config.extruder_max_nozzle_count.values.begin(), - config.extruder_max_nozzle_count.values.end(), - [](int v) { return v > 1; }); - - // Per-extruder printable-height clamp. Empty for single-extruder printers - // (extruder_printable_height = []), so is_valid_last_layer is inert there. - m_printable_height = config.extruder_printable_height.values; - m_last_layer_id.assign(config.nozzle_diameter.size(), -1); + // Orca: default/initial-layer/travel acceleration are object-scope options here (PrintConfig + // members in BBS), so Print pushes the resolved columns in via set_accelerations() instead of + // the ctor reading them from config. + m_max_accels = config.machine_max_acceleration_extruding.values.front(); // Read absolute value of first layer speed, if given as percentage, // it is taken over following default. Speeds from config are not // easily accessible here. const float default_speed = 60.f; - m_first_layer_speed = config.initial_layer_speed.get_at(get_extruder_index(config, (unsigned int)initial_tool)); + m_first_layer_speed = config.initial_layer_speed.get_at(get_extruder_index(config, (unsigned int) initial_tool)); if (m_first_layer_speed == 0.f) // just to make sure autospeed doesn't break it. m_first_layer_speed = default_speed / 2.f; @@ -1550,6 +1828,9 @@ WipeTower::WipeTower(const PrintConfig& config, int plate_idx, Vec3d plate_origi m_bed_bottom_left = m_bed_shape == RectangularBed ? Vec2f(bed_points.front().x(), bed_points.front().y()) : Vec2f::Zero(); + m_last_layer_id.resize(config.nozzle_diameter.size(), -1); + m_origin = {plate_origin[0], plate_origin[1]}; + m_is_multiple_nozzle = std::any_of(config.extruder_max_nozzle_count.values.begin(), config.extruder_max_nozzle_count.values.end(), [](auto &elem) { return elem > 1; }); } @@ -1559,31 +1840,16 @@ void WipeTower::set_extruder(size_t idx, const PrintConfig& config) //while (m_filpar.size() < idx+1) // makes sure the required element is in the vector m_filpar.push_back(FilamentParameters()); - // Orca: one row per filament, indexed by the raw filament id. Under a per-layer nozzle - // grouping the per-variant arrays may hold several columns per filament; the tower has no - // layer dimension here, so it keeps the filament's first column (tower x per-layer - // grouping is a documented follow-up). m_filpar[idx].material = config.filament_type.get_at(idx); + // Orca: wipe_tower_filament (issue #10971) forces a specific filament to print the tower wall by + // marking every other filament as "soluble"; 0 keeps the plain per-filament soluble flag. m_filpar[idx].is_soluble = config.wipe_tower_filament == 0 ? config.filament_soluble.get_at(idx) : (idx != size_t(config.wipe_tower_filament - 1)); // BBS m_filpar[idx].is_support = config.filament_is_support.get_at(idx); m_filpar[idx].nozzle_temperature = config.nozzle_temperature.get_at(idx); m_filpar[idx].nozzle_temperature_initial_layer = config.nozzle_temperature_initial_layer.get_at(idx); m_filpar[idx].category = config.filament_adhesiveness_category.get_at(idx); - { - int interface_temp = config.filament_tower_interface_print_temp.get_at(idx); - if (interface_temp == -1) - interface_temp = config.nozzle_temperature_range_high.get_at(idx); - m_filpar[idx].interface_print_temperature = interface_temp; - } - m_filpar[idx].tower_interface_pre_extrusion_dist = config.filament_tower_interface_pre_extrusion_dist.get_at(idx); - m_filpar[idx].tower_interface_pre_extrusion_length = config.filament_tower_interface_pre_extrusion_length.get_at(idx); - // PETG pre-extrusion offset reuses the tower-interface pre-extrusion distance. Only read by the - // has_filament_switcher-gated PETG branch in get_next_pos (inert fleet-wide). - m_filpar[idx].petg_pre_extrusion_offset_dist = config.filament_tower_interface_pre_extrusion_dist.get_at(idx); - m_filpar[idx].tower_ironing_area = config.filament_tower_ironing_area.get_at(idx); - m_filpar[idx].tower_interface_purge_length = config.filament_tower_interface_purge_volume.get_at(idx); - m_filpar[idx].filament_cooling_before_tower = config.filament_cooling_before_tower.get_at(idx); + m_filpar[idx].flat_iron_area = config.filament_tower_ironing_area.get_at(idx); // If this is a single extruder MM printer, we will use all the SE-specific config values. // Otherwise, the defaults will be used to turn off the SE stuff. @@ -1609,14 +1875,10 @@ void WipeTower::set_extruder(size_t idx, const PrintConfig& config) if (max_vol_speed!= 0.f) m_filpar[idx].max_e_speed = (max_vol_speed / filament_area()); - // Vortek H2C: carousel-specific ramming, precool, and reverse travel parameters + //set extruder change and nozzle change ramming speed { - // Ramming speed: .first = extruder change, .second = nozzle change (carousel) - // Use the dedicated ramming volumetric speed, falling back to max_vol_speed only when - // the setting is nil/-1. float ramming_vol_speed = float(config.filament_ramming_volumetric_speed.get_at(idx)); - if (config.filament_ramming_volumetric_speed.is_nil(idx) || is_approx(config.filament_ramming_volumetric_speed.get_at(idx), -1.)) - ramming_vol_speed = max_vol_speed; + if (config.filament_ramming_volumetric_speed.is_nil(idx) || is_approx(config.filament_ramming_volumetric_speed.get_at(idx), -1.)) ramming_vol_speed = max_vol_speed; m_filpar[idx].max_e_ramming_speed.first = (ramming_vol_speed / filament_area()); float ramming_vol_speed_nc = float(config.filament_ramming_volumetric_speed_nc.get_at(idx)); @@ -1624,51 +1886,57 @@ void WipeTower::set_extruder(size_t idx, const PrintConfig& config) ramming_vol_speed_nc = max_vol_speed; m_filpar[idx].max_e_ramming_speed.second = (ramming_vol_speed_nc / filament_area()); } - { - // Precool target temp: .first = extruder change, .second = nozzle change (carousel) - // Precool is only active when enable_pre_heating is on; otherwise no precool temp/timing is - // applied and the downstream precool_t stays 0, matching printers with pre-heating disabled. - m_filpar[idx].precool_target_temp = {0, 0}; - if (config.enable_pre_heating.value) { - if (!config.filament_pre_cooling_temperature.is_nil(idx) && config.filament_pre_cooling_temperature.get_at(idx) != 0) - m_filpar[idx].precool_target_temp.first = config.filament_pre_cooling_temperature.get_at(idx); - if (!config.filament_pre_cooling_temperature_nc.is_nil(idx) && config.filament_pre_cooling_temperature_nc.get_at(idx) != 0) - m_filpar[idx].precool_target_temp.second = config.filament_pre_cooling_temperature_nc.get_at(idx); - } - } - { - // Precool timing: (nozzle_temp - precool_temp) / hotend_cooling_rate - int extruder_count = m_is_multi_extruder ? 2 : 1; // H2C = 2 extruders - float nozzle_temp = float(config.nozzle_temperature.is_nil(idx) ? 0 : config.nozzle_temperature.get_at(idx)); - float nozzle_temp_fl = float(config.nozzle_temperature_initial_layer.is_nil(idx) ? nozzle_temp : config.nozzle_temperature_initial_layer.get_at(idx)); - m_filpar[idx].precool_t.first.resize(extruder_count, 0.f); - m_filpar[idx].precool_t.second.resize(extruder_count, 0.f); - m_filpar[idx].precool_t_first_layer.first.resize(extruder_count, 0.f); - m_filpar[idx].precool_t_first_layer.second.resize(extruder_count, 0.f); - std::vector cooling_rates = config.hotend_cooling_rate.values; - for (int i = 0; i < extruder_count && i < (int)cooling_rates.size(); i++) { - if (cooling_rates[i] < EPSILON) continue; - if (m_filpar[idx].precool_target_temp.first != 0) { - m_filpar[idx].precool_t.first[i] = std::max(0.f, nozzle_temp - float(m_filpar[idx].precool_target_temp.first)) / float(cooling_rates[i]); - m_filpar[idx].precool_t_first_layer.first[i] = std::max(0.f, nozzle_temp_fl - float(m_filpar[idx].precool_target_temp.first)) / float(cooling_rates[i]); - } - if (m_filpar[idx].precool_target_temp.second != 0) { - m_filpar[idx].precool_t.second[i] = std::max(0.f, nozzle_temp - float(m_filpar[idx].precool_target_temp.second)) / float(cooling_rates[i]); - m_filpar[idx].precool_t_first_layer.second[i] = std::max(0.f, nozzle_temp_fl - float(m_filpar[idx].precool_target_temp.second)) / float(cooling_rates[i]); - } - } - } - { - // Ramming travel time: .first = extruder change, .second = nozzle change (carousel) - m_filpar[idx].ramming_travel_time = {0.f, 0.f}; - if (!config.filament_ramming_travel_time.is_nil(idx)) - m_filpar[idx].ramming_travel_time.first = float(config.filament_ramming_travel_time.get_at(idx)); - if (!config.filament_ramming_travel_time_nc.is_nil(idx)) - m_filpar[idx].ramming_travel_time.second = float(config.filament_ramming_travel_time_nc.get_at(idx)); - } + //set precooling time/precooling target temp during extruder change and nozzle change + { + int extruder_count = m_multi_nozzle_group_result->get_extruder_count(); + m_filpar[idx].precool_t.first.resize(extruder_count, 0.f); + m_filpar[idx].precool_t_first_layer.first.resize(extruder_count, 0.f); + m_filpar[idx].precool_t.second.resize(extruder_count, 0.f); + m_filpar[idx].precool_t_first_layer.second.resize(extruder_count, 0.f); + m_filpar[idx].precool_target_temp.first = 0; + m_filpar[idx].precool_target_temp.second = 0; + float nozzle_temp_first_layer = config.nozzle_temperature_initial_layer.is_nil(idx) ? -1.f : float(config.nozzle_temperature_initial_layer.get_at(idx)); + float nozzle_temp_other_layer = config.nozzle_temperature.is_nil(idx) ? -1.f : float(config.nozzle_temperature.get_at(idx)); + std::vector hotend_cooling_rates = config.hotend_cooling_rate.values; + auto is_need_precooling = [&](bool extruder_change) -> bool + { + bool res = config.enable_pre_heating.value; + if (extruder_change) return res &&!config.filament_pre_cooling_temperature.is_nil(idx) && config.filament_pre_cooling_temperature.get_at(idx) != 0; + return res &&!config.filament_pre_cooling_temperature_nc.is_nil(idx) && config.filament_pre_cooling_temperature_nc.get_at(idx) != 0; + }; + if (is_need_precooling(true)) { + for (int i = 0; i < m_filpar[idx].precool_t.first.size(); i++) { + if (config.hotend_cooling_rate.is_nil(i)) continue; + m_filpar[idx].precool_t.first[i] = std::max(0.f, nozzle_temp_other_layer - float(config.filament_pre_cooling_temperature.get_at(idx))) / float(hotend_cooling_rates[i]); + m_filpar[idx].precool_t_first_layer.first[i] = std::max(0.f, nozzle_temp_first_layer -float(config.filament_pre_cooling_temperature.get_at(idx))) /float(hotend_cooling_rates[i]); + } + m_filpar[idx].precool_target_temp.first = config.filament_pre_cooling_temperature.get_at(idx); + } + + if (is_need_precooling(false)) { + for (int i = 0; i < m_filpar[idx].precool_t.second.size(); i++) { + if (config.hotend_cooling_rate.is_nil(i)) continue; + m_filpar[idx].precool_t.second[i] = std::max(0.f, nozzle_temp_other_layer - float(config.filament_pre_cooling_temperature_nc.get_at(idx))) / float(hotend_cooling_rates[i]); + m_filpar[idx].precool_t_first_layer.second[i] = std::max(0.f, nozzle_temp_first_layer -float(config.filament_pre_cooling_temperature_nc.get_at(idx))) /float(hotend_cooling_rates[i]); + } + m_filpar[idx].precool_target_temp.second = config.filament_pre_cooling_temperature_nc.get_at(idx); + } + + } + //set ramming reverse travel time during extruder change and nozzle change + { + m_filpar[idx].ramming_travel_time = {0, 0}; + if (!config.filament_ramming_travel_time.is_nil(idx)) m_filpar[idx].ramming_travel_time.first = float(config.filament_ramming_travel_time.get_at(idx)); + if (!config.filament_ramming_travel_time_nc.is_nil(idx)) m_filpar[idx].ramming_travel_time.second = float(config.filament_ramming_travel_time_nc.get_at(idx)); + } m_perimeter_width = nozzle_diameter * Width_To_Nozzle_Ratio; // all extruders are now assumed to have the same diameter - m_nozzle_change_perimeter_width = 2*m_perimeter_width; + // Orca: custom presets may use nozzle diameters outside the BBS table; fall back to the + // previous 2*perimeter_width rule (identical to the table for 0.4) instead of throwing. + { + auto nc_width_it = nozzle_diameter_to_nozzle_change_width.find(nozzle_diameter); + m_nozzle_change_perimeter_width = nc_width_it != nozzle_diameter_to_nozzle_change_width.end() ? nc_width_it->second : 2.f * m_perimeter_width; + } // BBS: remove useless config #if 0 if (m_semm) { @@ -1687,6 +1955,18 @@ void WipeTower::set_extruder(size_t idx, const PrintConfig& config) m_filpar[idx].retract_length = config.retraction_length.get_at(idx); m_filpar[idx].retract_speed = config.retraction_speed.get_at(idx); m_filpar[idx].wipe_dist = config.wipe_distance.get_at(idx); + m_filpar[idx].filament_cooling_before_tower = config.filament_cooling_before_tower.get_at(idx); + m_filpar[idx].filament_petg_pre_extrusion_offset_dist = config.filament_tower_interface_pre_extrusion_dist.get_at(idx); + if (config.enable_tower_interface_features.value) { + m_filpar[idx].filament_tower_interface_print_temp = config.filament_tower_interface_print_temp.get_at(idx) == -1 ? config.nozzle_temperature_range_high.get_at(idx) : + config.filament_tower_interface_print_temp.get_at(idx); + m_filpar[idx].filament_tower_interface_pre_extrusion_dist = config.filament_tower_interface_pre_extrusion_dist.get_at(idx); + m_filpar[idx].filament_tower_interface_pre_extrusion_length = config.filament_tower_interface_pre_extrusion_length.get_at(idx); + } else { + m_filpar[idx].filament_tower_interface_print_temp = config.nozzle_temperature.get_at(idx); + m_filpar[idx].filament_tower_interface_pre_extrusion_dist = 0.f; + m_filpar[idx].filament_tower_interface_pre_extrusion_length = 0.f; + } } @@ -1704,13 +1984,13 @@ std::vector WipeTower::prime( return std::vector(); } -Vec2f WipeTower::get_next_pos(const WipeTower::box_coordinates &cleaning_box, float wipe_length, bool interface_layer, size_t interface_tool) +Vec2f WipeTower::get_next_pos(const WipeTower::box_coordinates &cleaning_box, float wipe_length, bool solid_toolchange) { const float &xl = cleaning_box.ld.x(); const float &xr = cleaning_box.rd.x(); int line_count = wipe_length / (xr - xl); - float dy = m_layer_info->extra_spacing * m_perimeter_width; + float dy = m_layer_info->extra_spacing * get_block_gap_width(m_current_tool,false); float y_offset = float(line_count) * dy; const Vec2f pos_offset = Vec2f(0.f, m_depth_traversed); @@ -1733,23 +2013,20 @@ Vec2f WipeTower::get_next_pos(const WipeTower::box_coordinates &cleaning_box, fl break; default: break; } - // Shift the wipe start outward for a PETG pre-extrusion on filament-switcher devices, clamped to the - // shared printable bed. Gated on m_has_filament_switcher, which is false for the whole shipping fleet - // (no profile sets the key), so is_petg_pre_extrusion is always false and res is returned unchanged. - // The tower-interface contact branch is deliberately NOT applied here (enable_tower_interface_features - // DOES ship on H2C/X2D; applying it would change their g-code); is_contact_pre_extrusion is computed - // only as the guard that gives the contact path priority over PETG. - bool is_contact_pre_extrusion = interface_layer && m_enable_tower_interface_features; - bool is_petg_pre_extrusion = !is_contact_pre_extrusion && is_petg_filament(m_current_tool) && m_has_filament_switcher; - if (is_petg_pre_extrusion) { - Vec2f stop_pos = res; - float offset_dist = m_filpar[m_current_tool].petg_pre_extrusion_offset_dist; - auto printer_bbx = unscaled(get_extents(m_shared_print_bed)); // BoundingBoxBase - printer_bbx.translate((-m_wipe_tower_pos - m_rib_offset).cast()); + bool is_contact_pre_extrusion = solid_toolchange && m_enable_tower_interface_features; + bool is_petg_pre_extrusion = !is_contact_pre_extrusion && is_petg_filament(m_current_tool) && m_has_filament_switcher; + if (is_contact_pre_extrusion || is_petg_pre_extrusion) { + Vec2f stop_pos = res; + float filament_tower_interface_pre_extrusion_dist = is_petg_pre_extrusion + ? m_filpar[m_current_tool].filament_petg_pre_extrusion_offset_dist + : m_filpar[m_current_tool].filament_tower_interface_pre_extrusion_dist; + // Orca: unscaled(BoundingBox) here is a template returning BoundingBoxBase, not BoundingBoxf + auto printer_bbx = unscaled(get_extents(m_shared_print_bed)); + printer_bbx.translate((-m_wipe_tower_pos - m_rib_offset).cast()); // first layer never be contact if (stop_pos.x() < m_wipe_tower_width / 2.f) - stop_pos = Vec2f(stop_pos.x() - offset_dist, stop_pos.y()); + stop_pos = Vec2f(stop_pos.x() - filament_tower_interface_pre_extrusion_dist, stop_pos.y()); else - stop_pos = Vec2f(stop_pos.x() + offset_dist, stop_pos.y()); + stop_pos = Vec2f(stop_pos.x() + filament_tower_interface_pre_extrusion_dist, stop_pos.y()); if (stop_pos.x() < printer_bbx.min[0]) stop_pos.x() = printer_bbx.min[0]; if (stop_pos.x() > printer_bbx.max[0]) stop_pos.x() = printer_bbx.max[0]; res = stop_pos; @@ -1759,10 +2036,11 @@ Vec2f WipeTower::get_next_pos(const WipeTower::box_coordinates &cleaning_box, fl WipeTower::ToolChangeResult WipeTower::tool_change(size_t tool, bool extrude_perimeter, bool first_toolchange_to_nonsoluble) { - m_nozzle_change_result.gcode.clear(); - if (!m_filament_map.empty() && tool < m_filament_map.size() && m_filament_map[m_current_tool] != m_filament_map[tool]) { - m_nozzle_change_result = nozzle_change(m_current_tool, tool); - } + //only for tool = unsigned (-1) ,never get here + //m_nozzle_change_result.gcode.clear(); + //if (!m_filament_map.empty() && tool < m_filament_map.size() && m_filament_map[m_current_tool] != m_filament_map[tool]) { + // m_nozzle_change_result = nozzle_change(m_current_tool, tool); + //} size_t old_tool = m_current_tool; @@ -1792,7 +2070,7 @@ WipeTower::ToolChangeResult WipeTower::tool_change(size_t tool, bool extrude_per (tool != (unsigned int)(-1) ? wipe_depth + m_depth_traversed - m_perimeter_width : m_wipe_tower_depth - m_perimeter_width)); - WipeTowerWriter writer(m_layer_height, m_perimeter_width, m_gcode_flavor, m_filpar); + WipeTowerWriter writer(m_layer_height, m_perimeter_width, m_gcode_flavor, m_filpar, m_enable_arc_fitting); writer.set_extrusion_flow(m_extrusion_flow) .set_z(m_z_pos) .set_initial_tool(m_current_tool) @@ -1801,13 +2079,14 @@ WipeTower::ToolChangeResult WipeTower::tool_change(size_t tool, bool extrude_per "; CP TOOLCHANGE START\n") .comment_with_value(" toolchange #", m_num_tool_changes + 1); // the number is zero-based + set_for_wipe_tower_writer(writer); if (tool != (unsigned)(-1)) writer.append(std::string("; material : " + (m_current_tool < m_filpar.size() ? m_filpar[m_current_tool].material : "(NONE)") + " -> " + m_filpar[tool].material + "\n").c_str()) .append(";--------------------\n"); writer.speed_override_backup(); - writer.speed_override(100); + writer.speed_override(100); float feedrate = is_first_layer() ? std::min(m_first_layer_speed * 60.f, 5400.f) : std::min(60.0f * m_filpar[m_current_tool].max_e_speed / m_extrusion_flow, 5400.f); @@ -1857,7 +2136,7 @@ WipeTower::ToolChangeResult WipeTower::tool_change(size_t tool, bool extrude_per } } - Vec2f initial_position = get_next_pos(cleaning_box, wipe_length, false, tool); + Vec2f initial_position = get_next_pos(cleaning_box, wipe_length,false); writer.set_initial_position(initial_position, m_wipe_tower_width, m_wipe_tower_depth, m_internal_rotation); if (extrude_perimeter) { @@ -1901,7 +2180,7 @@ WipeTower::ToolChangeResult WipeTower::tool_change(size_t tool, bool extrude_per //BBS //if (m_set_extruder_trimpot) // writer.set_extruder_trimpot(550); // Reset the extruder current to a normal value. - writer.speed_override_restore(); + writer.speed_override_restore(); writer.feedrate(m_travel_speed * 60.f) .flush_planner_queue() .reset_extruder() @@ -1913,9 +2192,9 @@ WipeTower::ToolChangeResult WipeTower::tool_change(size_t tool, bool extrude_per if (m_current_tool < m_used_filament_length.size()) m_used_filament_length[m_current_tool] += writer.get_and_reset_used_filament_length(); - return construct_tcr(writer, false, old_tool, false, true, purge_volume, false); + return construct_tcr(writer, false, old_tool, false, true, purge_volume,false); } - +#if 0 WipeTower::NozzleChangeResult WipeTower::nozzle_change(int old_filament_id, int new_filament_id) { float wipe_depth = 0.f; @@ -1940,18 +2219,27 @@ WipeTower::NozzleChangeResult WipeTower::nozzle_change(int old_filament_id, int // Otherwise we are going to Unload only. And m_layer_info would be invalid. } + auto format_nozzle_change_line = [](bool start, int old_filament_id, int new_filament_id)->std::string { + char buff[64]; + std::string tag = start ? GCodeProcessor::reserved_tag(GCodeProcessor::ETags::NozzleChangeStart) : GCodeProcessor::reserved_tag(GCodeProcessor::ETags::NozzleChangeEnd); + snprintf(buff, sizeof(buff), ";%s OF%d NF%d\n", tag.c_str(), old_filament_id, new_filament_id); + return std::string(buff); + }; + float nozzle_change_speed = 60.0f * m_filpar[m_current_tool].max_e_speed / m_extrusion_flow; if (is_tpu_filament(m_current_tool)) { nozzle_change_speed *= 0.25; } - WipeTowerWriter writer(m_layer_height, m_perimeter_width, m_gcode_flavor, m_filpar); + WipeTowerWriter writer(m_layer_height, m_perimeter_width, m_gcode_flavor, m_filpar, m_enable_arc_fitting); writer.set_extrusion_flow(m_extrusion_flow) .set_z(m_z_pos) .set_initial_tool(m_current_tool) .set_extrusion_flow(m_extrusion_flow) .set_y_shift(m_y_shift + (new_filament_id != (unsigned int) (-1) && (m_current_shape == SHAPE_REVERSED) ? m_layer_info->depth - m_layer_info->toolchanges_depth() : 0.f)) - .append(format_nozzle_change_tag(true, old_filament_id, new_filament_id)); + .append(format_nozzle_change_line(true,old_filament_id,new_filament_id)); + + set_for_wipe_tower_writer(writer); box_coordinates cleaning_box(Vec2f(m_perimeter_width, m_perimeter_width), m_wipe_tower_width - 2 * m_perimeter_width, (new_filament_id != (unsigned int) (-1) ? wipe_depth + m_depth_traversed - m_perimeter_width : m_wipe_tower_depth - m_perimeter_width)); @@ -2027,14 +2315,14 @@ WipeTower::NozzleChangeResult WipeTower::nozzle_change(int old_filament_id, int } } - writer.append(format_nozzle_change_tag(false, old_filament_id, new_filament_id)); + writer.append(format_nozzle_change_line(false, old_filament_id, new_filament_id)); result.start_pos = writer.start_pos_rotated(); result.end_pos = writer.pos(); - result.gcode = std::move(writer.gcode()); + result.gcode = writer.gcode(); return result; } - +#endif // Ram the hot material out of the melt zone, retract the filament into the cooling tubes and let it cool. void WipeTower::toolchange_Unload( WipeTowerWriter &writer, @@ -2248,12 +2536,12 @@ void WipeTower::toolchange_Wipe( float wipe_length) { // Increase flow on first layer, slow down print. - writer.set_extrusion_flow(m_extrusion_flow * (is_first_layer() ? 1.15f : 1.f)) + writer.set_extrusion_flow(m_extrusion_flow * (is_first_layer() ? m_first_layer_flow_ratio : 1.f)) .append("; CP TOOLCHANGE WIPE\n"); // BBS: add the note for gcode-check, when the flow changed, the width should follow the change if (is_first_layer()) { - writer.append(";" + GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Width) + std::to_string(1.15 * m_perimeter_width) + "\n"); + writer.append(";" + GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Width) + std::to_string(m_first_layer_flow_ratio * m_perimeter_width) + "\n"); } const float& xl = cleaning_box.ld.x(); @@ -2287,7 +2575,7 @@ void WipeTower::toolchange_Wipe( if (m_depth_traversed != 0) writer.travel(xl, writer.y() + dy); #endif - + bool need_change_flow = false; // now the wiping itself: for (int i = 0; true; ++i) { @@ -2382,6 +2670,21 @@ WipeTower::box_coordinates WipeTower::align_perimeter(const WipeTower::box_coord return aligned_box; } +void WipeTower::set_for_wipe_tower_writer(WipeTowerWriter &writer) +{ + writer.set_normal_acceleration(m_normal_accels); + writer.set_travel_acceleration(m_travel_accels); + writer.set_first_layer_normal_acceleration(m_first_layer_normal_accels); + writer.set_first_layer_travel_acceleration(m_first_layer_travel_accels); + writer.set_max_acceleration(m_max_accels); + writer.set_multi_nozzle_group_result(m_multi_nozzle_group_result); + writer.set_accel_to_decel_enable(m_accel_to_decel_enable); + writer.set_accel_to_decel_factor(m_accel_to_decel_factor); + writer.set_first_layer(m_cur_layer_id == 0); + writer.set_layer_id(m_cur_layer_id); + writer.set_physical_extruder_map(m_physical_extruder_map); +} +#if 0 WipeTower::ToolChangeResult WipeTower::finish_layer(bool extrude_perimeter, bool extruder_fill) { assert(! this->layer_finished()); @@ -2389,20 +2692,20 @@ WipeTower::ToolChangeResult WipeTower::finish_layer(bool extrude_perimeter, bool size_t old_tool = m_current_tool; - WipeTowerWriter writer(m_layer_height, m_perimeter_width, m_gcode_flavor, m_filpar); + WipeTowerWriter writer(m_layer_height, m_perimeter_width, m_gcode_flavor, m_filpar, m_enable_arc_fitting); writer.set_extrusion_flow(m_extrusion_flow) .set_z(m_z_pos) .set_initial_tool(m_current_tool) .set_y_shift(m_y_shift - (m_current_shape == SHAPE_REVERSED ? m_layer_info->toolchanges_depth() : 0.f)); + set_for_wipe_tower_writer(writer); + writer.append(";" + GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Wipe_Tower_Start) + "\n"); // Slow down on the 1st layer. bool first_layer = is_first_layer(); // BBS: speed up perimeter speed to 90mm/s for non-first layer float feedrate = first_layer ? std::min(m_first_layer_speed * 60.f, 5400.f) : std::min(60.0f * m_filpar[m_current_tool].max_e_speed / m_extrusion_flow, 5400.f); - if (m_enable_tower_interface_features && m_prev_layer_had_interface) - feedrate = std::min(feedrate, 20.f * 60.f); float fill_box_y = m_layer_info->toolchanges_depth() + m_perimeter_width; box_coordinates fill_box(Vec2f(m_perimeter_width, fill_box_y), m_wipe_tower_width - 2 * m_perimeter_width, m_layer_info->depth - fill_box_y); @@ -2545,12 +2848,40 @@ WipeTower::ToolChangeResult WipeTower::finish_layer(bool extrude_perimeter, bool if (m_current_tool < m_used_filament_length.size()) m_used_filament_length[m_current_tool] += writer.get_and_reset_used_filament_length(); - return construct_tcr(writer, false, old_tool, true, false, 0.f, false); + return construct_tcr(writer, false, old_tool, true, false, 0.f,false); +} +#endif +WipeTower::WipeTowerInfo::ToolChange WipeTower::set_toolchange(int old_tool, int new_tool, float layer_height, float wipe_volume, float purge_volume,int layer_id) +{ + float depth = 0.f; + float width = m_wipe_tower_width - 2 * m_perimeter_width; + float nozzle_change_width = m_wipe_tower_width - (m_nozzle_change_perimeter_width + m_perimeter_width); + float length_to_extrude = volume_to_length(wipe_volume, m_perimeter_width, layer_height); + float toolchange_gap_width = get_block_gap_width(new_tool,false); + float nozzlechange_gap_width = get_block_gap_width(old_tool,true); + float filament_change_length = !is_same_extruder(old_tool, new_tool, layer_id) ? m_filaments_change_length.first[old_tool] : m_filaments_change_length.second[old_tool]; + depth += std::ceil(length_to_extrude / width) * toolchange_gap_width; + // depth *= m_extra_spacing; + + float nozzle_change_depth = 0; + float nozzle_change_length = 0; + if (is_need_ramming(old_tool, new_tool, layer_id)) { + double e_flow = nozzle_change_extrusion_flow(layer_height); + double length = filament_change_length / e_flow; + int nozzle_change_line_count = std::ceil(length / nozzle_change_width); + nozzle_change_depth = nozzle_change_line_count * nozzlechange_gap_width; + depth += nozzle_change_depth; + nozzle_change_length = length; + } + WipeTowerInfo::ToolChange tool_change = WipeTowerInfo::ToolChange(old_tool, new_tool, depth, 0.f, 0.f, wipe_volume, length_to_extrude, purge_volume); + tool_change.nozzle_change_depth = nozzle_change_depth; + tool_change.nozzle_change_length = nozzle_change_length; + return tool_change; } // Appends a toolchange into m_plan and calculates neccessary depth of the corresponding box void WipeTower::plan_toolchange(float z_par, float layer_height_par, unsigned int old_tool, - unsigned int new_tool, float wipe_volume, float purge_volume) + unsigned int new_tool, float wipe_volume_ec,float wipe_volume_nc,float purge_volume) { assert(m_plan.empty() || m_plan.back().z <= z_par + WT_EPSILON); // refuses to add a layer below the last one @@ -2570,7 +2901,8 @@ void WipeTower::plan_toolchange(float z_par, float layer_height_par, unsigned in // BBS: if the wipe tower width is too small, the depth will be infinity if (width <= EPSILON) return; - + int layer_id = static_cast(m_plan.size()) - 1; + float wipe_volume = is_same_extruder(old_tool, new_tool, layer_id) && !is_same_nozzle(old_tool, new_tool, layer_id) ? wipe_volume_nc : wipe_volume_ec; // BBS: remove old filament ramming and first line #if 0 float length_to_extrude = volume_to_length(0.25f * std::accumulate(m_filpar[old_tool].ramming_speed.begin(), m_filpar[old_tool].ramming_speed.end(), 0.f), @@ -2592,39 +2924,24 @@ void WipeTower::plan_toolchange(float z_par, float layer_height_par, unsigned in depth += std::ceil(length_to_extrude / width) * m_perimeter_width; //depth *= m_extra_spacing; - + float filament_change_length = !is_same_extruder(old_tool, new_tool, layer_id) ? m_filaments_change_length.first[old_tool] : m_filaments_change_length.second[old_tool]; float nozzle_change_depth = 0; - if (!m_filament_map.empty() && m_filament_map[old_tool] != m_filament_map[new_tool]) { + float nozzle_change_length = 0; + if (is_need_ramming(old_tool, new_tool, layer_id)) { double e_flow = nozzle_change_extrusion_flow(layer_height_par); - double length = m_filaments_change_length[old_tool] / e_flow; - int nozzle_change_line_count = length / (m_wipe_tower_width - 2*m_nozzle_change_perimeter_width) + 1; - if (has_tpu_filament()) - nozzle_change_depth = m_tpu_fixed_spacing * nozzle_change_line_count * m_nozzle_change_perimeter_width; - else - nozzle_change_depth = nozzle_change_line_count * m_nozzle_change_perimeter_width; - depth += nozzle_change_depth; - } - if (nozzle_change_depth == 0 - && !m_filament_nozzle_map.empty() - && old_tool < m_filament_nozzle_map.size() && new_tool < m_filament_nozzle_map.size() - && m_filament_nozzle_map[old_tool] != m_filament_nozzle_map[new_tool]) { - double e_flow = nozzle_change_extrusion_flow(layer_height_par); - double length = m_filaments_change_length[old_tool] / e_flow; - int nozzle_change_line_count = length / (m_wipe_tower_width - 2*m_nozzle_change_perimeter_width) + 1; - if (has_tpu_filament()) - nozzle_change_depth = m_tpu_fixed_spacing * nozzle_change_line_count * m_nozzle_change_perimeter_width; - else - nozzle_change_depth = nozzle_change_line_count * m_nozzle_change_perimeter_width; + double length = filament_change_length / e_flow; + int nozzle_change_line_count = std::ceil(length / (m_wipe_tower_width - 2*m_nozzle_change_perimeter_width)); + nozzle_change_depth = nozzle_change_line_count * m_nozzle_change_perimeter_width; depth += nozzle_change_depth; + nozzle_change_length = length; } WipeTowerInfo::ToolChange tool_change = WipeTowerInfo::ToolChange(old_tool, new_tool, depth, 0.f, 0.f, wipe_volume, length_to_extrude, purge_volume); tool_change.nozzle_change_depth = nozzle_change_depth; + tool_change.nozzle_change_length = nozzle_change_length; m_plan.back().tool_changes.push_back(tool_change); #endif } - - - +#if 0 void WipeTower::plan_tower() { // BBS @@ -2755,7 +3072,7 @@ void WipeTower::save_on_last_wipe() } } } - +#endif bool WipeTower::is_tpu_filament(int filament_id) const { return m_filpar[filament_id].material == "TPU"; @@ -2766,11 +3083,11 @@ bool WipeTower::is_petg_filament(int filament_id) const return m_filpar[filament_id].material == "PETG"; } -bool WipeTower::is_need_reverse_travel(int filament_id, bool extruder_change) const +bool WipeTower::is_need_reverse_travel(int filament_id,bool extruder_change) const { if (extruder_change) - return m_filpar[filament_id].ramming_travel_time.first > EPSILON; - return m_filpar[filament_id].ramming_travel_time.second > EPSILON; + return m_filpar[filament_id].ramming_travel_time.first > EPSILON && m_filaments_change_length.first[filament_id]>EPSILON; + return m_filpar[filament_id].ramming_travel_time.second > EPSILON && m_filaments_change_length.second[filament_id] > EPSILON; } // BBS: consider both soluable and support properties @@ -2785,15 +3102,13 @@ int WipeTower::first_toolchange_to_nonsoluble_nonsupport( return -1; } -static WipeTower::ToolChangeResult merge_tcr(WipeTower::ToolChangeResult& first, - WipeTower::ToolChangeResult& second) +WipeTower::ToolChangeResult WipeTower::merge_tcr(ToolChangeResult &first, ToolChangeResult &second) { assert(first.new_tool == second.initial_tool); WipeTower::ToolChangeResult out = first; - out.is_contact = first.is_contact || second.is_contact; if ((first.end_pos - second.start_pos).norm() > (float)EPSILON) { std::string travel_gcode = "G1 X" + Slic3r::float_to_string_decimal_point(second.start_pos.x(), 3) + " Y" + - Slic3r::float_to_string_decimal_point(second.start_pos.y(), 3) + " F5400" + "\n"; + Slic3r::float_to_string_decimal_point(second.start_pos.y(), 3) + " F" + std::to_string(m_max_speed) + "\n"; bool need_insert_travel = true; if (second.is_tool_change && is_approx(second.start_pos.x(), second.tool_change_start_pos.x()) @@ -2811,7 +3126,7 @@ static WipeTower::ToolChangeResult merge_tcr(WipeTower::ToolChangeResult& first, out.wipe_path = second.wipe_path; out.initial_tool = first.initial_tool; out.new_tool = second.new_tool; - + out.is_contact = first.is_contact || second.is_contact; if (!first.nozzle_change_result.gcode.empty()) out.nozzle_change_result = first.nozzle_change_result; else if (!second.nozzle_change_result.gcode.empty()) @@ -2834,75 +3149,108 @@ static WipeTower::ToolChangeResult merge_tcr(WipeTower::ToolChangeResult& first, return out; } -void WipeTower::get_wall_skip_points(const WipeTowerInfo &layer) -{ +void WipeTower::get_all_wall_skip_points() { m_wall_skip_points.clear(); + m_wall_skip_points.resize(m_plan.size()); + for (int i = 0; i < m_plan.size(); i++) { + const WipeTowerInfo &layer = m_plan[i]; + get_wall_skip_points(m_plan[i],i); + } +} + + +void WipeTower::get_wall_skip_points(const WipeTowerInfo &layer, int layer_id) +{ + const int pre_access_layer = 4; std::unordered_map cur_block_depth; for (int i = 0; i < int(layer.tool_changes.size()); ++i) { const WipeTowerInfo::ToolChange &tool_change = layer.tool_changes[i]; size_t old_filament = tool_change.old_tool; size_t new_filament = tool_change.new_tool; - float spacing = m_layer_info->extra_spacing; - if (has_tpu_filament() && m_layer_info->extra_spacing < m_tpu_fixed_spacing) spacing = 1; - float nozzle_change_depth = tool_change.nozzle_change_depth * spacing; - // Drop the nozzle-change depth on an extruder's final layer above its printable height - // (inert unless is_valid_last_layer clamps, i.e. multi-extruder near Z-max). - if (!is_valid_last_layer(old_filament, m_cur_layer_id, layer.z)) nozzle_change_depth = 0.f; - //float nozzle_change_depth = tool_change.nozzle_change_depth * (has_tpu_filament() ? m_tpu_fixed_spacing : layer.extra_spacing); - auto* block = get_block_by_category(m_filpar[new_filament].category, false); - if (!block) - continue; - //float wipe_depth = tool_change.required_depth - nozzle_change_depth; - float wipe_depth = ceil(tool_change.wipe_length / (m_wipe_tower_width - 2 * m_perimeter_width)) * m_perimeter_width*layer.extra_spacing; - float process_depth = 0.f; - if (!cur_block_depth.count(m_filpar[new_filament].category)) - cur_block_depth[m_filpar[new_filament].category] = block->start_depth; + float nozzle_change_depth = tool_change.nozzle_change_depth; + float wipe_depth = tool_change.required_depth - nozzle_change_depth; + if (!is_valid_last_layer(old_filament, layer_id, m_plan[layer_id].z)) nozzle_change_depth = 0.f; + auto *block = get_block_by_category(m_filpar[new_filament].category, false); + if (!block) continue; + float process_depth = 0.f; + if (!cur_block_depth.count(m_filpar[new_filament].category)) cur_block_depth[m_filpar[new_filament].category] = block->start_depth; process_depth = cur_block_depth[m_filpar[new_filament].category]; - if (!m_filament_map.empty() && new_filament < m_filament_map.size() && m_filament_map[old_filament] != m_filament_map[new_filament]) { + if (is_need_ramming(new_filament, old_filament, layer_id)) { if (m_filament_categories[new_filament] == m_filament_categories[old_filament]) process_depth += nozzle_change_depth; else { if (!cur_block_depth.count(m_filpar[old_filament].category)) { - auto* old_block = get_block_by_category(m_filpar[old_filament].category, false); - if (!old_block) - continue; + auto *old_block = get_block_by_category(m_filpar[old_filament].category, false); + if (!old_block) continue; cur_block_depth[m_filpar[old_filament].category] = old_block->start_depth; } cur_block_depth[m_filpar[old_filament].category] += nozzle_change_depth; } } - { - Vec2f res; - int index = m_cur_layer_id % 4; - switch (index % 4) { - case 0: res = Vec2f(0, process_depth); break; - case 1: res = Vec2f(m_wipe_tower_width, process_depth + wipe_depth - layer.extra_spacing*m_perimeter_width); break; - case 2: res = Vec2f(m_wipe_tower_width, process_depth); break; - case 3: res = Vec2f(0, process_depth + wipe_depth - layer.extra_spacing * m_perimeter_width); break; - default: break; - } - m_wall_skip_points.emplace_back(res); + float infill_gap_width = get_block_gap_width(new_filament, false); + Vec2f res; + int index = layer_id % 4; + switch (index % 4) { + case 0: res = Vec2f(0, process_depth); break; + case 1: res = Vec2f(m_wipe_tower_width, process_depth + wipe_depth - m_plan[layer_id].extra_spacing * infill_gap_width); break; + case 2: res = Vec2f(m_wipe_tower_width, process_depth); break; + case 3: res = Vec2f(0, process_depth + wipe_depth - m_plan[layer_id].extra_spacing * infill_gap_width); break; + default: break; + } + + m_wall_skip_points[layer_id].emplace_back(res); + + cur_block_depth[m_filpar[new_filament].category] = process_depth + wipe_depth; + + bool solid_toolchange = block->layers_type[layer_id] == WipeTowerLayerType::Contact; + if (solid_toolchange && m_enable_tower_interface_features) { + for (int j = 0; j < pre_access_layer; j++) { + int pre_layer_id = layer_id - j; + if (pre_layer_id < 0) break; + m_wall_skip_points[pre_layer_id].push_back(res); + } } - cur_block_depth[m_filpar[new_filament].category] = process_depth + tool_change.required_depth - tool_change.nozzle_change_depth * layer.extra_spacing; } + if (m_enable_tower_interface_features) { + for (auto &block : m_wipe_tower_blocks) { + float block_depth = cur_block_depth.count(block.filament_adhesiveness_category) ? cur_block_depth[block.filament_adhesiveness_category] : block.start_depth; + if (block_depth + EPSILON >= block.start_depth + block.layer_depths[layer_id] - m_perimeter_width) { continue; } + bool block_solid = block.layers_type[layer_id] == WipeTowerLayerType::Contact; + bool add_skip_point = block_solid && std::abs(block_depth - block.start_depth) < EPSILON; + if (add_skip_point) { + Vec2f res; + int index = layer_id % 4; + + float dy_skip = block.layer_depths[layer_id] - m_perimeter_width; + int n_skip = (int) ((dy_skip + 0.25f * m_perimeter_width) / m_perimeter_width + 1); + float gird_depth = m_perimeter_width * (n_skip - 1); // in sync with finish_block_solid + switch (index % 4) { + case 0: res = Vec2f(0, block_depth); break; + case 1: res = Vec2f(m_wipe_tower_width, block_depth + gird_depth); break; + case 2: res = Vec2f(m_wipe_tower_width, block_depth); break; + case 3: res = Vec2f(0, block_depth + gird_depth); break; + default: break; + } + m_wall_skip_points[layer_id].emplace_back(res); + for (int j = 0; j < pre_access_layer; j++) { + int pre_layer_id = layer_id - j; + if (pre_layer_id < 0) break; + m_wall_skip_points[pre_layer_id].push_back(res); + } + } + } } +} WipeTower::ToolChangeResult WipeTower::tool_change_new(size_t new_tool, bool solid_toolchange,bool solid_nozzlechange) { m_nozzle_change_result.gcode.clear(); - // Skip the cross-extruder nozzle change (ramming) on an extruder's final layer above its printable - // height. is_valid_last_layer is inert unless multi-extruder near Z-max. - if (!m_filament_map.empty() && new_tool < m_filament_map.size() && m_filament_map[m_current_tool] != m_filament_map[new_tool] - && is_valid_last_layer(m_current_tool, m_cur_layer_id, m_z_pos)) { - m_nozzle_change_result = nozzle_change_new(m_current_tool, new_tool, solid_nozzlechange); - } - if (m_nozzle_change_result.gcode.empty() - && !m_filament_nozzle_map.empty() - && m_current_tool < m_filament_nozzle_map.size() && new_tool < m_filament_nozzle_map.size() - && m_filament_nozzle_map[m_current_tool] != m_filament_nozzle_map[new_tool] - && is_valid_last_layer(m_current_tool, m_cur_layer_id, m_z_pos)) { - m_nozzle_change_result = nozzle_change_new(m_current_tool, new_tool, solid_nozzlechange); + bool hotend_change = false; + if (is_need_ramming(m_current_tool,new_tool, m_cur_layer_id)) { + hotend_change = is_same_extruder(m_current_tool, new_tool, m_cur_layer_id); + //If it is the last layer and exceeds the printable height, cancel ramming + if (is_valid_last_layer(m_current_tool, m_cur_layer_id, m_z_pos)) m_nozzle_change_result = ramming(m_current_tool, new_tool, solid_nozzlechange, !hotend_change); } size_t old_tool = m_current_tool; @@ -2919,32 +3267,19 @@ WipeTower::ToolChangeResult WipeTower::tool_change_new(size_t new_tool, bool sol wipe_depth = b.required_depth; purge_volume = b.purge_volume; nozzle_change_depth = b.nozzle_change_depth; - if (has_tpu_filament()) - nozzle_change_line_count = ((b.nozzle_change_depth + WT_EPSILON) / m_nozzle_change_perimeter_width) / 2; - else - nozzle_change_line_count = (b.nozzle_change_depth + WT_EPSILON) / m_nozzle_change_perimeter_width; break; } } - - bool interface_layer = solid_toolchange && m_enable_tower_interface_features; - if (interface_layer && new_tool < m_filpar.size()) { - float extra_purge_length = m_filpar[new_tool].tower_interface_purge_length; - if (extra_purge_length > 0.f) { - purge_volume += extra_purge_length * m_filpar[new_tool].filament_area; - wipe_length += extra_purge_length; - } - } - + m_current_tool = new_tool; WipeTowerBlock* block = get_block_by_category(m_filpar[new_tool].category, false); if (!block) { assert(block != nullptr); return WipeTower::ToolChangeResult(); } m_cur_block = block; - box_coordinates cleaning_box(Vec2f(m_perimeter_width, block->cur_depth), m_wipe_tower_width - 2 * m_perimeter_width, wipe_depth-m_layer_info->extra_spacing*nozzle_change_depth); + box_coordinates cleaning_box(Vec2f(m_perimeter_width, block->cur_depth), m_wipe_tower_width - 2 * m_perimeter_width, wipe_depth-nozzle_change_depth); - WipeTowerWriter writer(m_layer_height, m_perimeter_width, m_gcode_flavor, m_filpar); + WipeTowerWriter writer(m_layer_height, m_perimeter_width, m_gcode_flavor, m_filpar, m_enable_arc_fitting); writer.set_extrusion_flow(m_extrusion_flow) .set_z(m_z_pos) .set_initial_tool(m_current_tool) @@ -2953,6 +3288,8 @@ WipeTower::ToolChangeResult WipeTower::tool_change_new(size_t new_tool, bool sol "; CP TOOLCHANGE START\n") .comment_with_value(" toolchange #", m_num_tool_changes + 1); // the number is zero-based + set_for_wipe_tower_writer(writer); + if (new_tool != (unsigned) (-1)) writer.append( std::string("; material : " + (m_current_tool < m_filpar.size() ? m_filpar[m_current_tool].material : "(NONE)") + " -> " + m_filpar[new_tool].material + "\n").c_str()) .append(";--------------------\n"); @@ -2962,7 +3299,7 @@ WipeTower::ToolChangeResult WipeTower::tool_change_new(size_t new_tool, bool sol // Ram the hot material out of the melt zone, retract the filament into the cooling tubes and let it cool. if (new_tool != (unsigned int) -1) { // This is not the last change. - Vec2f initial_position = get_next_pos(cleaning_box, wipe_length, interface_layer, new_tool); + Vec2f initial_position = get_next_pos(cleaning_box, wipe_length, solid_toolchange); writer.set_initial_position(initial_position, m_wipe_tower_width, m_wipe_tower_depth, m_internal_rotation); writer.append(";" + GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Wipe_Tower_Start) + "\n"); @@ -2970,25 +3307,8 @@ WipeTower::ToolChangeResult WipeTower::tool_change_new(size_t new_tool, bool sol is_first_layer() ? m_filpar[new_tool].nozzle_temperature_initial_layer : m_filpar[new_tool].nozzle_temperature); toolchange_Change(writer, new_tool, m_filpar[new_tool].material); // Change the tool, set a speed override for soluble and flex materials. toolchange_Load(writer, cleaning_box); - - int base_temp = is_first_layer() ? m_filpar[new_tool].nozzle_temperature_initial_layer : m_filpar[new_tool].nozzle_temperature; - if (interface_layer) { - int interface_temp = m_filpar[new_tool].interface_print_temperature; - if (interface_temp > 0 && interface_temp != base_temp) - writer.set_extruder_temp(interface_temp, true); - if (m_enable_tower_interface_cooldown_during_tower && interface_temp > 0 && interface_temp != base_temp) - writer.set_extruder_temp(base_temp, false); - float pre_dist = m_filpar[new_tool].tower_interface_pre_extrusion_dist; - float pre_len = m_filpar[new_tool].tower_interface_pre_extrusion_length; - if (pre_dist > 0.f && pre_len > 0.f) { - bool start_left = (m_cur_layer_id % 4 == 0 || m_cur_layer_id % 4 == 3); - float target_x = writer.x() + (start_left ? pre_dist : -pre_dist); - target_x = std::max(cleaning_box.ld.x(), std::min(cleaning_box.rd.x(), target_x)); - writer.extrude_explicit(target_x, writer.y(), pre_len, 600.f); - } - } - - if (m_is_multi_extruder && is_tpu_filament(new_tool)) { +# if 0 + if (m_is_multi_extruder && is_need_reverse_travel(new_tool)) { float dy = m_layer_info->extra_spacing * m_nozzle_change_perimeter_width; if (m_layer_info->extra_spacing < m_tpu_fixed_spacing) { dy = m_tpu_fixed_spacing * m_nozzle_change_perimeter_width; @@ -3019,22 +3339,15 @@ WipeTower::ToolChangeResult WipeTower::tool_change_new(size_t new_tool, bool sol } writer.travel(initial_position); } - +#endif toolchange_wipe_new(writer, cleaning_box, wipe_length, solid_toolchange); - if (interface_layer) { - int base_temp = is_first_layer() ? m_filpar[new_tool].nozzle_temperature_initial_layer : m_filpar[new_tool].nozzle_temperature; - int interface_temp = m_filpar[new_tool].interface_print_temperature; - if (!m_enable_tower_interface_cooldown_during_tower && interface_temp > 0 && interface_temp != base_temp) - writer.set_extruder_temp(base_temp, false); - } - writer.append(";" + GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Wipe_Tower_End) + "\n"); ++m_num_tool_changes; } else toolchange_Unload(writer, cleaning_box, m_filpar[m_current_tool].material, m_filpar[m_current_tool].nozzle_temperature); - block->cur_depth += (wipe_depth - nozzle_change_depth * m_layer_info->extra_spacing); + block->cur_depth += (wipe_depth - nozzle_change_depth); block->last_filament_change_id = new_tool; // BBS @@ -3050,192 +3363,187 @@ WipeTower::ToolChangeResult WipeTower::tool_change_new(size_t new_tool, bool sol if (m_current_tool < m_used_filament_length.size()) m_used_filament_length[m_current_tool] += writer.get_and_reset_used_filament_length(); - return construct_tcr(writer, false, old_tool, false, true, purge_volume, interface_layer); + return construct_tcr(writer, false, old_tool, false, true, purge_volume, solid_toolchange); } -WipeTower::NozzleChangeResult WipeTower::nozzle_change_new(int old_filament_id, int new_filament_id, bool solid_infill) +//for extruder change and nozzle change +WipeTower::NozzleChangeResult WipeTower::ramming(int old_filament_id, int new_filament_id, bool solid_infill, bool extruder_change) { + auto format_line_M106 = []() { return std::string{"M106 S255\n"};}; + auto format_line_M633 = []() { return std::string{"M633\n"};}; + auto format_line_M632 = [](int filament_id, int nozzle_id) { + std::string buffer = "M632 S" + std::to_string(filament_id); + if (nozzle_id >= 0) + buffer += " H" + std::to_string(nozzle_id); + buffer += " M N\n"; + return buffer; + }; + int nozzle_change_line_count = 0; + float x_offset = m_perimeter_width + (m_nozzle_change_perimeter_width - m_perimeter_width) / 2; + float nozzle_change_box_width = m_wipe_tower_width - 2 * x_offset; + float nozzle_change_depth = 0.f; if (new_filament_id != (unsigned int) (-1)) { for (const auto &b : m_layer_info->tool_changes) if (b.new_tool == new_filament_id) { - if (has_tpu_filament()) - nozzle_change_line_count = ((b.nozzle_change_depth + WT_EPSILON) / m_nozzle_change_perimeter_width) / 2; - else - nozzle_change_line_count = (b.nozzle_change_depth + WT_EPSILON) / m_nozzle_change_perimeter_width; + nozzle_change_line_count = std::ceil(b.nozzle_change_length / nozzle_change_box_width); + nozzle_change_depth = b.nozzle_change_depth; break; } } + auto format_nozzle_change_line = [this](bool start, int old_filament_id, int new_filament_id) -> std::string { + char buff[64]; + // Orca: the nozzle-change markers are standalone tag constants, not ETags entries + // (the Reserved_Tags parallel arrays have a non-BBL variant that must stay aligned). + std::string tag = start ? GCodeProcessor::Nozzle_Change_Start_Tag : GCodeProcessor::Nozzle_Change_End_Tag; + int old_nozzle_id = get_nozzle_id(old_filament_id, m_cur_layer_id); + int new_nozzle_id = get_nozzle_id(new_filament_id, m_cur_layer_id); + snprintf(buff, sizeof(buff), ";%s OF%d NF%d ON%d NN%d\n", tag.c_str(), old_filament_id, new_filament_id,old_nozzle_id,new_nozzle_id); + return std::string(buff); + }; float nz_extrusion_flow = nozzle_change_extrusion_flow(m_layer_height); - bool extruder_change = !is_in_same_extruder(old_filament_id, new_filament_id); - float max_e_ramming = extruder_change - ? m_filpar[m_current_tool].max_e_ramming_speed.first - : m_filpar[m_current_tool].max_e_ramming_speed.second; - if (max_e_ramming < EPSILON) max_e_ramming = m_filpar[m_current_tool].max_e_speed; // fallback - float nozzle_change_speed = 60.0f * max_e_ramming / nz_extrusion_flow; - nozzle_change_speed = solid_infill ? 40.f * 60.f : nozzle_change_speed; - - if (is_tpu_filament(m_current_tool)) { - nozzle_change_speed *= 0.25; - } - float bridge_speed = std::min(60.0f * max_e_ramming / nozzle_change_extrusion_flow(0.2), nozzle_change_speed); - - WipeTowerWriter writer(m_layer_height, m_nozzle_change_perimeter_width, m_gcode_flavor, m_filpar); + WipeTowerWriter writer(m_layer_height, m_nozzle_change_perimeter_width, m_gcode_flavor, m_filpar, m_enable_arc_fitting); writer.set_extrusion_flow(nz_extrusion_flow) .set_z(m_z_pos) .set_initial_tool(m_current_tool) - .set_y_shift(m_y_shift + (new_filament_id != (unsigned int) (-1) && (m_current_shape == SHAPE_REVERSED) ? m_layer_info->depth - m_layer_info->toolchanges_depth() : 0.f)) - .append(format_nozzle_change_tag(true, old_filament_id, new_filament_id)); - - if (!extruder_change && m_is_multiple_nozzle) { - writer.append("M632 S" + std::to_string(new_filament_id) + " M N\n"); - // Use m_physical_extruder_map for heater index (matches format_line_M104 in add_M104_by_requirement) - if (m_filpar[m_current_tool].precool_target_temp.second != 0) { - int logical_ext = m_filament_map.empty() ? 0 : m_filament_map[m_current_tool] - 1; - int phys_ext = (logical_ext >= 0 && logical_ext < (int)m_physical_extruder_map.size()) - ? m_physical_extruder_map[logical_ext] : logical_ext; - writer.append("M400\n"); - writer.append("M104 T" + std::to_string(phys_ext) + " S" + - std::to_string(m_filpar[m_current_tool].precool_target_temp.second) + " N0\n"); - writer.append("M106 S255\n"); - } - writer.append("M633\n"); - } + .set_y_shift(m_y_shift + (new_filament_id != (unsigned int) (-1) && (m_current_shape == SHAPE_REVERSED) ? m_layer_info->depth - m_layer_info->toolchanges_depth() : 0.f)); + set_for_wipe_tower_writer(writer); WipeTowerBlock* block = get_block_by_category(m_filpar[old_filament_id].category, false); if (!block) { assert(false); return WipeTower::NozzleChangeResult(); } - m_cur_block = block; - float dy = m_layer_info->extra_spacing * m_nozzle_change_perimeter_width; - if (has_tpu_filament() && m_extra_spacing < m_tpu_fixed_spacing) - dy = m_tpu_fixed_spacing * m_nozzle_change_perimeter_width; - - float x_offset = m_perimeter_width + (m_nozzle_change_perimeter_width - m_perimeter_width) / 2; - box_coordinates cleaning_box(Vec2f(x_offset,block->cur_depth + (m_nozzle_change_perimeter_width - m_perimeter_width) / 2), - m_wipe_tower_width - 2 * x_offset, - nozzle_change_line_count * dy - (m_nozzle_change_perimeter_width - m_perimeter_width) / 2); + m_cur_block = block; + float dy = is_first_layer() ? m_nozzle_change_perimeter_width : m_layer_info->extra_spacing * get_block_gap_width(m_current_tool, true); + box_coordinates cleaning_box(Vec2f(x_offset, block->cur_depth + (m_nozzle_change_perimeter_width - m_perimeter_width) / 2), + nozzle_change_box_width, + nozzle_change_depth); Vec2f initial_position = cleaning_box.ld; writer.set_initial_position(initial_position, m_wipe_tower_width, m_wipe_tower_depth, m_internal_rotation); - const float &xl = cleaning_box.ld.x(); - const float &xr = cleaning_box.rd.x(); - dy = solid_infill ? m_nozzle_change_perimeter_width : dy; - nozzle_change_line_count = solid_infill ? std::numeric_limits::max() : nozzle_change_line_count; - m_left_to_right = true; - - if (extruder_change) { - float ramming_length = nozzle_change_line_count * (xr - xl); - int extruder_id = m_filament_map.empty() ? 0 : m_filament_map[m_current_tool] - 1; - float precool_t = (extruder_id >= 0 && extruder_id < (int)m_filpar[m_current_tool].precool_t.first.size()) - ? m_filpar[m_current_tool].precool_t.first[extruder_id] : 0.f; - float precool_t_fl = (extruder_id >= 0 && extruder_id < (int)m_filpar[m_current_tool].precool_t_first_layer.first.size()) - ? m_filpar[m_current_tool].precool_t_first_layer.first[extruder_id] : 0.f; - float per_cooling_max_speed = nozzle_change_speed; - if (is_first_layer() && precool_t_fl > EPSILON) - per_cooling_max_speed = ramming_length / precool_t_fl * 60.f; - else if (precool_t > EPSILON) - per_cooling_max_speed = ramming_length / precool_t * 60.f; - if (nozzle_change_speed > per_cooling_max_speed) nozzle_change_speed = per_cooling_max_speed; - if (bridge_speed > per_cooling_max_speed) bridge_speed = per_cooling_max_speed; - } - - int real_nozzle_change_line_count = 0; - bool need_change_flow = false; - for (int i = 0; true; ++i) { - if (need_thick_bridge_flow(writer.pos().y())) { - writer.set_extrusion_flow(nozzle_change_extrusion_flow(0.2)); - writer.append(";" + GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Height) + std::to_string(0.2) + "\n"); - need_change_flow = true; + // --- Nozzle change preamble: notify firmware + precool --- + writer.append(format_nozzle_change_line(true, old_filament_id, new_filament_id)); + if (!extruder_change) { + int new_nozzle_id = m_multi_nozzle_group_result->is_support_dynamic_nozzle_map() + ? get_nozzle_id(new_filament_id, m_cur_layer_id) : -1; + writer.append(format_line_M632(new_filament_id, new_nozzle_id)); + if (m_filpar[m_current_tool].precool_target_temp.second != 0) { + writer.format_line_M104(m_filpar[m_current_tool].precool_target_temp.second, get_extruder_id(m_current_tool, m_cur_layer_id)) + .append(format_line_M106()); } - if (m_left_to_right) - writer.extrude(xr + wipe_tower_wall_infill_overlap * m_perimeter_width, writer.y(), need_change_flow ? bridge_speed : nozzle_change_speed); - else - writer.extrude(xl - wipe_tower_wall_infill_overlap * m_perimeter_width, writer.y(), need_change_flow ? bridge_speed : nozzle_change_speed); - real_nozzle_change_line_count++; - if (i == nozzle_change_line_count - 1) - break; - if ((writer.y() + dy - cleaning_box.ru.y()+(m_nozzle_change_perimeter_width+m_perimeter_width)/2) > (float)EPSILON) break; - if (need_change_flow) { - writer.set_extrusion_flow(nozzle_change_extrusion_flow(m_layer_height)); - writer.append(";" + GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Height) + std::to_string(m_layer_height) + "\n"); - need_change_flow = false; - } - writer.extrude(writer.x(), writer.y() + dy, nozzle_change_speed); - m_left_to_right = !m_left_to_right; + writer.append(format_line_M633()); } - if (need_change_flow) { - writer.append(";" + GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Height) + std::to_string(m_layer_height) + "\n"); - } - writer.set_extrusion_flow(nz_extrusion_flow); // Reset the extrusion flow. - block->cur_depth += real_nozzle_change_line_count * dy; - block->last_nozzle_change_id = old_filament_id; NozzleChangeResult result; - if (!extruder_change && m_is_multiple_nozzle) { - writer.append("M632 S" + std::to_string(new_filament_id) + " M N\n"); - } - if (is_need_reverse_travel(m_current_tool, extruder_change)) { - bool left_to_right = !m_left_to_right; - int tpu_line_count = real_nozzle_change_line_count; - float reverse_speed = nozzle_change_speed * 2; // reverse travel runs at double the nozzle-change speed - float rt_time = extruder_change ? m_filpar[m_current_tool].ramming_travel_time.first - : m_filpar[m_current_tool].ramming_travel_time.second; - float need_reverse_travel_dis = rt_time * reverse_speed / 60.f; - float real_travel_dis = tpu_line_count * (xr - xl - 2 * m_perimeter_width); - if (real_travel_dis < need_reverse_travel_dis) - reverse_speed *= real_travel_dis / need_reverse_travel_dis; - writer.travel(writer.x(), writer.y() + dy/2); + if (nozzle_change_line_count > 0) { + float max_e_ramming_speed = extruder_change ? m_filpar[m_current_tool].max_e_ramming_speed.first : m_filpar[m_current_tool].max_e_ramming_speed.second; + float nozzle_change_speed = 60.0f * max_e_ramming_speed / nz_extrusion_flow; + if (solid_infill) + nozzle_change_speed = std::min(40.f * 60.f, nozzle_change_speed); + float bridge_speed = std::min(60.0f * max_e_ramming_speed / nozzle_change_extrusion_flow(0.2), nozzle_change_speed); + const float &xl = cleaning_box.ld.x(); + const float &xr = cleaning_box.rd.x(); + dy = solid_infill ? m_nozzle_change_perimeter_width : dy; + if (solid_infill) + nozzle_change_line_count = std::floor(EPSILON + (cleaning_box.ru[1] - cleaning_box.rd[1] + (m_nozzle_change_perimeter_width - m_perimeter_width) / 2.f) / + m_nozzle_change_perimeter_width); + m_left_to_right = true; + bool need_change_flow = false; + float ramming_length = nozzle_change_line_count * (xr - xl); + int extruder_id = get_extruder_id(m_current_tool, m_cur_layer_id); + float precool_t = extruder_change ? m_filpar[m_current_tool].precool_t.first[extruder_id] : m_filpar[m_current_tool].precool_t.second[extruder_id]; + float precool_t_first_layer = extruder_change ? m_filpar[m_current_tool].precool_t_first_layer.first[extruder_id] : + m_filpar[m_current_tool].precool_t_first_layer.second[extruder_id]; + float per_cooling_max_speed = nozzle_change_speed; + if (extruder_change) { + if (is_first_layer() && precool_t_first_layer > EPSILON) + per_cooling_max_speed = ramming_length / precool_t_first_layer * 60.f; + else if (precool_t > EPSILON) + per_cooling_max_speed = ramming_length / precool_t * 60.f; + }//BBS:nozzle change does not require forcing a cooldown to a specific temperature. + if (nozzle_change_speed > per_cooling_max_speed) nozzle_change_speed = per_cooling_max_speed; + if (bridge_speed > per_cooling_max_speed) bridge_speed = per_cooling_max_speed; + LimitFlow LimitRamming = extruder_change ? LimitFlow::LimitRammingFlow : LimitFlow::LimitRammingFlowNC; for (int i = 0; true; ++i) { - need_reverse_travel_dis -= (xr - xl - 2 * m_perimeter_width); - float offset_dis = 0.f; - if (need_reverse_travel_dis < 0) - offset_dis = -need_reverse_travel_dis; - if (left_to_right) - writer.travel(xr - m_perimeter_width - offset_dis, writer.y(), reverse_speed); + if (need_thick_bridge_flow(writer.pos().y())) { + writer.set_extrusion_flow(nozzle_change_extrusion_flow(0.2)); + writer.append(";" + GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Height) + std::to_string(0.2) + "\n"); + need_change_flow = true; + } + if (m_left_to_right) + writer.extrude(xr + wipe_tower_wall_infill_overlap * m_perimeter_width, writer.y(), need_change_flow ? bridge_speed : nozzle_change_speed, LimitRamming); else - writer.travel(xl + m_perimeter_width + offset_dis, writer.y(), reverse_speed); - if (need_reverse_travel_dis < EPSILON) break; - if (i == tpu_line_count - 1) + writer.extrude(xl - wipe_tower_wall_infill_overlap * m_perimeter_width, writer.y(), need_change_flow ? bridge_speed : nozzle_change_speed, LimitRamming); + if (i == nozzle_change_line_count - 1) break; - writer.travel(writer.x(), writer.y() - dy); - left_to_right = !left_to_right; + if ((writer.y() + dy - cleaning_box.ru.y()+(m_nozzle_change_perimeter_width+m_perimeter_width)/2) > (float)EPSILON) break; + if (need_change_flow) { + writer.set_extrusion_flow(nozzle_change_extrusion_flow(m_layer_height)); + writer.append(";" + GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Height) + std::to_string(m_layer_height) + "\n"); + need_change_flow = false; + } + writer.extrude(writer.x(), writer.y() + dy, nozzle_change_speed, LimitRamming); + m_left_to_right = !m_left_to_right; } - } else if (is_tpu_filament(m_current_tool)) { - bool left_to_right = !m_left_to_right; - int tpu_line_count = (real_nozzle_change_line_count + 2 - 1) / 2; - nozzle_change_speed *= 2; - writer.travel(writer.x(), writer.y() - m_nozzle_change_perimeter_width); - - for (int i = 0; true; ++i) { - if (left_to_right) - writer.travel(xr - m_perimeter_width, writer.y(), nozzle_change_speed); - else - writer.travel(xl + m_perimeter_width, writer.y(), nozzle_change_speed); - - if (i == tpu_line_count - 1) - break; - - writer.travel(writer.x(), writer.y() - dy); - left_to_right = !left_to_right; + if (need_change_flow) { + writer.append(";" + GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Height) + std::to_string(m_layer_height) + "\n"); } - } else { - result.wipe_path.push_back(writer.pos_rotated()); - if (m_left_to_right) { - result.wipe_path.push_back(Vec2f(0, writer.pos_rotated().y())); + + writer.set_extrusion_flow(nz_extrusion_flow); + block->cur_depth += nozzle_change_depth; + block->last_nozzle_change_id = old_filament_id; + // --- Post-ramming: re-arm nozzle change for travel phase --- + if (!extruder_change) { + int new_nozzle_id = m_multi_nozzle_group_result->is_support_dynamic_nozzle_map() + ? get_nozzle_id(new_filament_id, m_cur_layer_id) : -1; + writer.append(format_line_M632(new_filament_id, new_nozzle_id)); + } + + if (is_need_reverse_travel(m_current_tool, extruder_change)) { + bool left_to_right = !m_left_to_right; + int tpu_line_count = nozzle_change_line_count; + nozzle_change_speed *= 2; // due to nozzle change 2 perimeter + float ramming_travel_time = extruder_change ? m_filpar[m_current_tool].ramming_travel_time.first : m_filpar[m_current_tool].ramming_travel_time.second; + float need_reverse_travel_dis = ramming_travel_time * nozzle_change_speed / 60.f; + float real_travel_dis = tpu_line_count * (xr - xl - 2 * m_perimeter_width); + if (real_travel_dis < need_reverse_travel_dis) + nozzle_change_speed *= real_travel_dis / need_reverse_travel_dis; + writer.travel(writer.x(), writer.y() + dy/2); + + for (int i = 0; true; ++i) { + need_reverse_travel_dis -= (xr - xl - 2 * m_perimeter_width); + float offset_dis = 0.f; + if (need_reverse_travel_dis < 0) { + offset_dis = -need_reverse_travel_dis; + } + if (left_to_right) + writer.travel(xr - m_perimeter_width - offset_dis, writer.y(), nozzle_change_speed); + else + writer.travel(xl + m_perimeter_width + offset_dis , writer.y(), nozzle_change_speed); + if (need_reverse_travel_dis < EPSILON) break; + if (i == tpu_line_count - 1) + break; + + writer.travel(writer.x(), writer.y() - dy); + left_to_right = !left_to_right; + } } else { - result.wipe_path.push_back(Vec2f(m_wipe_tower_width, writer.pos_rotated().y())); + result.wipe_path.push_back(writer.pos_rotated()); + if (m_left_to_right) { + result.wipe_path.push_back(Vec2f(0, writer.pos_rotated().y())); + } else { + result.wipe_path.push_back(Vec2f(m_wipe_tower_width, writer.pos_rotated().y())); + } } + if (!extruder_change) writer.append(format_line_M633()); } - if (!extruder_change && m_is_multiple_nozzle) writer.append("M633\n"); - - writer.append(format_nozzle_change_tag(false, old_filament_id, new_filament_id)); + writer.append(format_nozzle_change_line(false, old_filament_id, new_filament_id)); result.start_pos = writer.start_pos_rotated(); result.origin_start_pos = initial_position; @@ -3250,91 +3558,110 @@ WipeTower::ToolChangeResult WipeTower::finish_layer_new(bool extrude_perimeter, assert(!this->layer_finished()); m_current_layer_finished = true; - WipeTowerWriter writer(m_layer_height, m_perimeter_width, m_gcode_flavor, m_filpar); + WipeTowerWriter writer(m_layer_height, m_perimeter_width, m_gcode_flavor, m_filpar, m_enable_arc_fitting); writer.set_extrusion_flow(m_extrusion_flow) .set_z(m_z_pos) .set_initial_tool(m_current_tool) .set_y_shift(m_y_shift - (m_current_shape == SHAPE_REVERSED ? m_layer_info->toolchanges_depth() : 0.f)); + set_for_wipe_tower_writer(writer); + writer.append(";" + GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Wipe_Tower_Start) + "\n"); // Slow down on the 1st layer. bool first_layer = is_first_layer(); // BBS: speed up perimeter speed to 90mm/s for non-first layer - float feedrate = first_layer ? std::min(m_first_layer_speed * 60.f, 5400.f) : std::min(60.0f * m_filpar[m_current_tool].max_e_speed / m_extrusion_flow, 5400.f); - - float fill_box_depth = m_wipe_tower_depth - 2 * m_perimeter_width; - if (m_wipe_tower_blocks.size() == 1) { - fill_box_depth = m_layer_info->depth - 2 * m_perimeter_width; - } - box_coordinates fill_box(Vec2f(m_perimeter_width, m_perimeter_width), m_wipe_tower_width - 2 * m_perimeter_width, fill_box_depth); - - writer.set_initial_position((m_left_to_right ? fill_box.ru : fill_box.lu), m_wipe_tower_width, m_wipe_tower_depth, m_internal_rotation); + float feedrate = first_layer ? std::min(m_first_layer_speed * 60.f, m_max_speed) : std::min(60.0f * m_filpar[m_current_tool].max_e_speed / m_extrusion_flow, m_max_speed); bool toolchanges_on_layer = m_layer_info->toolchanges_depth() > WT_EPSILON; std::vector finish_rect_wipe_path; - if (extrude_fill_wall) { - // inner perimeter of the sparse section, if there is space for it: - if (fill_box.ru.y() - fill_box.rd.y() > WT_EPSILON) { - writer.rectangle_fill_box(this, fill_box, finish_rect_wipe_path, feedrate); + const bool multi_block_fill = (m_wipe_tower_blocks.size() > 1) && (extrude_fill_wall || extrude_fill); + + // Build list of fill boxes: one per block when multi_block_fill, else one for whole tower. + std::vector fill_boxes; + if (multi_block_fill) { + for (const WipeTowerBlock &block : m_wipe_tower_blocks) { + float block_fill_height = block.depth - 2 * m_perimeter_width; + if (m_cur_layer_id >= 0 && size_t(m_cur_layer_id) < block.layer_depths.size()) + block_fill_height = block.layer_depths[m_cur_layer_id] - 2 * m_perimeter_width; + if (block_fill_height <= WT_EPSILON) + continue; + fill_boxes.emplace_back( + Vec2f(m_perimeter_width, block.start_depth + m_perimeter_width), + m_wipe_tower_width - 2 * m_perimeter_width, + block_fill_height); } } + if (fill_boxes.empty()) { + float fill_box_depth = m_wipe_tower_depth - 2 * m_perimeter_width; + if (m_wipe_tower_blocks.size() == 1) + fill_box_depth = m_layer_info->depth - 2 * m_perimeter_width; + fill_boxes.emplace_back(Vec2f(m_perimeter_width, m_perimeter_width), m_wipe_tower_width - 2 * m_perimeter_width, fill_box_depth); + } - // Extrude infill to support the material to be printed above. - const float dy = (fill_box.lu.y() - fill_box.ld.y() - m_perimeter_width); - float left = fill_box.lu.x() + 2 * m_perimeter_width; - float right = fill_box.ru.x() - 2 * m_perimeter_width; - if (extrude_fill && dy > m_perimeter_width) { - writer.travel(fill_box.ld + Vec2f(m_perimeter_width * 2, 0.f)) - .append(";--------------------\n" - "; CP EMPTY GRID START\n") - .comment_with_value(" layer #", m_num_layer_changes + 1); + writer.set_initial_position((m_left_to_right ? fill_boxes.front().ru : fill_boxes.front().lu), m_wipe_tower_width, m_wipe_tower_depth, m_internal_rotation); - // Is there a soluble filament wiped/rammed at the next layer? - // If so, the infill should not be sparse. - bool solid_infill = m_layer_info + 1 == m_plan.end() ? - false : - std::any_of((m_layer_info + 1)->tool_changes.begin(), (m_layer_info + 1)->tool_changes.end(), - [this](const WipeTowerInfo::ToolChange &tch) { return m_filpar[tch.new_tool].is_soluble || m_filpar[tch.old_tool].is_soluble; }); - solid_infill |= first_layer && m_adhesion; + bool solid_infill = (m_layer_info + 1 == m_plan.end()) ? false : + std::any_of((m_layer_info + 1)->tool_changes.begin(), (m_layer_info + 1)->tool_changes.end(), + [this](const WipeTowerInfo::ToolChange &tch) { return m_filpar[tch.new_tool].is_soluble || m_filpar[tch.old_tool].is_soluble; }); + solid_infill |= first_layer && m_adhesion; - if (solid_infill) { - float sparse_factor = 1.5f; // 1=solid, 2=every other line, etc. - if (first_layer) { // the infill should touch perimeters - left -= m_perimeter_width; - right += m_perimeter_width; - sparse_factor = 1.f; - } - float y = fill_box.ld.y() + m_perimeter_width; - int n = dy / (m_perimeter_width * sparse_factor); - float spacing = (dy - m_perimeter_width) / (n - 1); - int i = 0; - for (i = 0; i < n; ++i) { - writer.extrude(writer.x(), y, feedrate).extrude(i % 2 ? left : right, y); - y = y + spacing; - } - writer.extrude(writer.x(), fill_box.lu.y()); - } else { - // Extrude an inverse U at the left of the region and the sparse infill. - writer.extrude(fill_box.lu + Vec2f(m_perimeter_width * 2, 0.f), feedrate); + for (size_t i = 0; i < fill_boxes.size(); ++i) { + const box_coordinates &fill_box = fill_boxes[i]; + if (i > 0) + writer.travel(m_left_to_right ? fill_box.ru : fill_box.lu); - const int n = 1 + int((right - left) / m_bridging); - const float dx = (right - left) / n; - for (int i = 1; i <= n; ++i) { - float x = left + dx * i; - writer.travel(x, writer.y()); - writer.extrude(x, i % 2 ? fill_box.rd.y() : fill_box.ru.y()); + if (extrude_fill_wall && (fill_box.ru.y() - fill_box.rd.y() > WT_EPSILON)) + writer.rectangle_fill_box(this, fill_box, finish_rect_wipe_path, feedrate); + // Extrude infill to support the material to be printed above. + const float dy = (fill_box.lu.y() - fill_box.ld.y() - m_perimeter_width); + float left = fill_box.lu.x() + 2 * m_perimeter_width; + float right = fill_box.ru.x() - 2 * m_perimeter_width; + + if (extrude_fill && dy > m_perimeter_width) { + writer.travel(fill_box.ld + Vec2f(m_perimeter_width * 2, 0.f)) + .append(";--------------------\n" + "; CP EMPTY GRID START\n") + .comment_with_value(" layer #", m_num_layer_changes + 1); + + if (solid_infill) { + float sparse_factor = 1.5f; // 1=solid, 2=every other line, etc. + if (first_layer) { // the infill should touch perimeters + left -= m_perimeter_width; + right += m_perimeter_width; + sparse_factor = 1.f; + } + float y = fill_box.ld.y() + m_perimeter_width; + int n = dy / (m_perimeter_width * sparse_factor); + float spacing = (dy - m_perimeter_width) / (n - 1); + int i = 0; + for (i = 0; i < n; ++i) { + writer.extrude(writer.x(), y, feedrate).extrude(i % 2 ? left : right, y); + y = y + spacing; + } + writer.extrude(writer.x(), fill_box.lu.y()); + } else { + // Extrude an inverse U at the left of the region and the sparse infill. + writer.extrude(fill_box.lu + Vec2f(m_perimeter_width * 2, 0.f), feedrate); + + const int n = 1 + int((right - left) / m_bridging); + const float dx = (right - left) / n; + for (int i = 1; i <= n; ++i) { + float x = left + dx * i; + writer.travel(x, writer.y()); + writer.extrude(x, i % 2 ? fill_box.rd.y() : fill_box.ru.y()); + } + + finish_rect_wipe_path.clear(); + // BBS: add wipe_path for this case: only with finish rectangle + finish_rect_wipe_path.emplace_back(writer.pos()); + finish_rect_wipe_path.emplace_back(Vec2f(left + dx * n, n % 2 ? fill_box.ru.y() : fill_box.rd.y())); } - finish_rect_wipe_path.clear(); - // BBS: add wipe_path for this case: only with finish rectangle - finish_rect_wipe_path.emplace_back(writer.pos()); - finish_rect_wipe_path.emplace_back(Vec2f(left + dx * n, n % 2 ? fill_box.ru.y() : fill_box.rd.y())); + writer.append("; CP EMPTY GRID END\n" + ";------------------\n\n\n\n\n\n\n"); } - - writer.append("; CP EMPTY GRID END\n" - ";------------------\n\n\n\n\n\n\n"); } // outer perimeter (always): @@ -3419,23 +3746,25 @@ WipeTower::ToolChangeResult WipeTower::finish_layer_new(bool extrude_perimeter, m_used_filament_length[m_current_tool] += writer.get_and_reset_used_filament_length(); m_nozzle_change_result.gcode.clear(); - return construct_tcr(writer, false, m_current_tool, true, false, 0.f, false); + return construct_tcr(writer, false, m_current_tool, true, false, 0.f,false); } WipeTower::ToolChangeResult WipeTower::finish_block(const WipeTowerBlock &block, int filament_id, bool extrude_fill) { - WipeTowerWriter writer(m_layer_height, m_perimeter_width, m_gcode_flavor, m_filpar); + WipeTowerWriter writer(m_layer_height, m_perimeter_width, m_gcode_flavor, m_filpar, m_enable_arc_fitting); writer.set_extrusion_flow(m_extrusion_flow) .set_z(m_z_pos) .set_initial_tool(filament_id) .set_y_shift(m_y_shift - (m_current_shape == SHAPE_REVERSED ? m_layer_info->toolchanges_depth() : 0.f)); + set_for_wipe_tower_writer(writer); + writer.append(";" + GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Wipe_Tower_Start) + "\n"); // Slow down on the 1st layer. bool first_layer = is_first_layer(); // BBS: speed up perimeter speed to 90mm/s for non-first layer - float feedrate = first_layer ? std::min(m_first_layer_speed * 60.f, 5400.f) : std::min(60.0f * m_filpar[filament_id].max_e_speed / m_extrusion_flow, 5400.f); + float feedrate = first_layer ? std::min(m_first_layer_speed * 60.f, m_max_speed) : std::min(60.0f * m_filpar[filament_id].max_e_speed / m_extrusion_flow, m_max_speed); box_coordinates fill_box(Vec2f(0, 0), 0, 0); fill_box = box_coordinates(Vec2f(m_perimeter_width, block.cur_depth), m_wipe_tower_width - 2 * m_perimeter_width, block.start_depth + block.layer_depths[m_cur_layer_id] - block.cur_depth - m_perimeter_width); @@ -3529,49 +3858,96 @@ WipeTower::ToolChangeResult WipeTower::finish_block(const WipeTowerBlock &block, return construct_block_tcr(writer, false, filament_id, true, 0.f); } -WipeTower::ToolChangeResult WipeTower::finish_block_solid(const WipeTowerBlock &block, int filament_id, bool extrude_fill, bool interface_solid) +WipeTower::ToolChangeResult WipeTower::finish_block_solid(const WipeTowerBlock &block, int filament_id, bool extrude_fill, WipeTowerLayerType layer_type) { float layer_height = m_layer_height; float e_flow = m_extrusion_flow; - if (m_cur_layer_id > 1 && !block.solid_infill[m_cur_layer_id - 1] && m_extrusion_flow < extrusion_flow(0.2)) { + if (m_cur_layer_id > 1 && block.layers_type[m_cur_layer_id - 1]==WipeTowerLayerType::Normal && m_extrusion_flow < extrusion_flow(0.2)) { layer_height = 0.2; e_flow = extrusion_flow(0.2); } - WipeTowerWriter writer(layer_height, m_perimeter_width, m_gcode_flavor, m_filpar); + WipeTowerWriter writer(layer_height, m_perimeter_width, m_gcode_flavor, m_filpar, m_enable_arc_fitting); writer.set_extrusion_flow(e_flow) .set_z(m_z_pos) .set_initial_tool(filament_id) .set_y_shift(m_y_shift - (m_current_shape == SHAPE_REVERSED ? m_layer_info->toolchanges_depth() : 0.f)); + set_for_wipe_tower_writer(writer); + writer.append(";" + GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Wipe_Tower_Start) + "\n"); // Slow down on the 1st layer. bool first_layer = is_first_layer(); // BBS: speed up perimeter speed to 90mm/s for non-first layer - float feedrate = first_layer ? std::min(m_first_layer_speed * 60.f, 5400.f) : std::min(60.0f * m_filpar[filament_id].max_e_speed / m_extrusion_flow, 5400.f); - feedrate = interface_solid ? 20.f * 60.f : feedrate; + float feedrate = first_layer ? std::min(m_first_layer_speed * 60.f, m_max_speed) : std::min(60.0f * m_filpar[filament_id].max_e_speed / m_extrusion_flow, m_max_speed); + feedrate = (layer_type == WipeTowerLayerType::Contact || layer_type == WipeTowerLayerType::Contact_UP) ? 20.f * 60.f : feedrate; box_coordinates fill_box(Vec2f(0, 0), 0, 0); fill_box = box_coordinates(Vec2f(m_perimeter_width, block.cur_depth), m_wipe_tower_width - 2 * m_perimeter_width, block.start_depth + block.layer_depths[m_cur_layer_id] - block.cur_depth - m_perimeter_width); - - writer.set_initial_position((m_left_to_right ? fill_box.rd : fill_box.ld), m_wipe_tower_width, m_wipe_tower_depth, m_internal_rotation); - m_left_to_right = !m_left_to_right; bool toolchanges_on_layer = m_layer_info->toolchanges_depth() > WT_EPSILON; - + const float dy = (fill_box.lu.y() - fill_box.ld.y()); + int n = (dy + 0.25 * m_perimeter_width) / m_perimeter_width+1; + float spacing = m_perimeter_width; + Vec2f initial_pos(0, 0); + bool up_to_down = false; + //set initial pos + { + int index = m_cur_layer_id % 4; + float gird_depth = spacing * (n-1); + switch (index % 4) { + case 0: + initial_pos = fill_box.ld; + m_left_to_right = true; + up_to_down = false; + break; + case 1: + initial_pos = Vec2f(fill_box.rd.x(), fill_box.rd.y() + gird_depth); + m_left_to_right = false; + up_to_down = true; + break; + case 2: + initial_pos = fill_box.rd; + m_left_to_right = false; + up_to_down = false; + break; + case 3: + initial_pos = Vec2f(fill_box.ld.x(), gird_depth + fill_box.ld.y()); + m_left_to_right = true; + up_to_down = true; + break; + default: break; + } + } // Extrude infill to support the material to be printed above. - const float dy = (fill_box.lu.y() - fill_box.ld.y()); float left = fill_box.lu.x(); float right = fill_box.ru.x(); std::vector finish_rect_wipe_path; { - writer.append(";--------------------\n" + writer + .append(";--------------------\n" "; CP EMPTY GRID START\n") .comment_with_value(" layer #", m_num_layer_changes + 1); + bool is_full_block = std::abs(block.cur_depth - block.start_depth) < EPSILON; + if (is_full_block && layer_type == WipeTowerLayerType::Contact && m_enable_tower_interface_features ) { + Vec2f stop_pos = initial_pos; + float filament_tower_interface_pre_extrusion_dist = m_filpar[m_current_tool].filament_tower_interface_pre_extrusion_dist; + // Orca: unscaled(BoundingBox) here is a template returning BoundingBoxBase, not BoundingBoxf + auto printer_bbx = unscaled(get_extents(m_shared_print_bed)); + printer_bbx.translate((- m_wipe_tower_pos - m_rib_offset).cast()); // first layer never be contact + if (stop_pos.x() < m_wipe_tower_width/2.f) + stop_pos = Vec2f(stop_pos.x() - filament_tower_interface_pre_extrusion_dist, stop_pos.y()); + else + stop_pos = Vec2f(stop_pos.x() + filament_tower_interface_pre_extrusion_dist, stop_pos.y()); + if (stop_pos.x() < printer_bbx.min[0]) stop_pos.x() = printer_bbx.min[0]; + if (stop_pos.x() > printer_bbx.max[0]) stop_pos.x() = printer_bbx.max[0]; + initial_pos = stop_pos; + if (m_filpar[m_current_tool].filament_tower_interface_print_temp != m_filpar[m_current_tool].nozzle_temperature) + writer.format_line_M109(m_filpar[m_current_tool].filament_tower_interface_print_temp, get_extruder_id(m_current_tool, m_cur_layer_id)); + writer.retract(-m_filpar[m_current_tool].filament_tower_interface_pre_extrusion_length - 2.f, 100.f); + } + writer.set_initial_position(initial_pos, m_wipe_tower_width, m_wipe_tower_depth, m_internal_rotation); - float y = fill_box.ld.y(); - int n = (dy + 0.25 * m_perimeter_width) / m_perimeter_width + 1; - float spacing = m_perimeter_width; int i = 0; for (i = 0; i < n; ++i) { writer.extrude(m_left_to_right ? right : left, writer.y(), feedrate); @@ -3580,10 +3956,10 @@ WipeTower::ToolChangeResult WipeTower::finish_block_solid(const WipeTowerBlock & break; } m_left_to_right = !m_left_to_right; - y = y + spacing; - writer.extrude(writer.x(), y, feedrate); + writer.extrude(writer.x(), writer.y()+spacing*(up_to_down?-1:1), feedrate); } - + if (layer_type == WipeTowerLayerType::Contact && m_enable_tower_interface_features && m_filpar[m_current_tool].filament_tower_interface_print_temp != m_filpar[m_current_tool].nozzle_temperature) + writer.format_line_M104(m_filpar[m_current_tool].nozzle_temperature, get_extruder_id(m_current_tool, m_cur_layer_id)); writer.append("; CP EMPTY GRID END\n" ";------------------\n\n\n\n\n\n\n"); } @@ -3601,50 +3977,53 @@ WipeTower::ToolChangeResult WipeTower::finish_block_solid(const WipeTowerBlock & void WipeTower::toolchange_wipe_new(WipeTowerWriter &writer, const box_coordinates &cleaning_box, float wipe_length,bool solid_tool_toolchange) { - writer.set_extrusion_flow(m_extrusion_flow * (is_first_layer() ? 1.15f : 1.f)).append("; CP TOOLCHANGE WIPE\n"); - + writer.set_extrusion_flow(m_extrusion_flow * (is_first_layer() ? m_first_layer_flow_ratio : 1.f)) + // Orca: CP_TOOLCHANGE_WIPE is a standalone tag constant, not an ETags entry + .append(";" + GCodeProcessor::Toolchange_Wipe_Tag + " CT" + std::to_string(solid_tool_toolchange) + " FL" + std::to_string(is_first_layer()) + "\n"); if (!m_nozzle_change_result.gcode.empty()) writer.change_analyzer_line_width(m_perimeter_width); // BBS: add the note for gcode-check, when the flow changed, the width should follow the change if (is_first_layer()) { - writer.append(";" + GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Width) + std::to_string(1.15 * m_perimeter_width) + "\n"); + writer.append(";" + GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Width) + std::to_string(m_first_layer_flow_ratio * m_perimeter_width) + "\n"); } + + //if (solid_tool_toolchange && m_filpar[m_current_tool].filament_tower_interface_print_temp != m_filpar[m_current_tool].nozzle_temperature) + // writer.append(format_line_M109(m_filpar[m_current_tool].filament_tower_interface_print_temp, m_filament_map[this->m_current_tool] - 1)); + //if (solid_tool_toolchange && m_filpar[m_current_tool].filament_tower_interface_pre_extrusion_length != 0) + // writer.retract(-m_filpar[m_current_tool].filament_tower_interface_pre_extrusion_length, 100.f); + float retract_length = m_filpar[m_current_tool].retract_length; float retract_speed = m_filpar[m_current_tool].retract_speed * 60; - const float ironing_area = m_filpar[m_current_tool].tower_ironing_area; - const bool do_ironing = m_flat_ironing && (!solid_tool_toolchange || !m_enable_tower_interface_features); - const float &xl = cleaning_box.ld.x(); const float &xr = cleaning_box.rd.x(); - + bool should_flat_ironging = m_flat_ironing; + bool should_line_ironing = true; + if (!m_contact_ironing && solid_tool_toolchange) { + should_flat_ironging = false; + should_line_ironing = false; + } + bool should_cooling_before_tower = !solid_tool_toolchange; + bool should_cooling_before_object = false;//interface layer heating print tower, then cooling print object + int cooling_begin_line = 2; float x_to_wipe = wipe_length; - float dy = solid_tool_toolchange ? m_perimeter_width :m_layer_info->extra_spacing * m_perimeter_width; + float dy = is_first_layer() ? m_perimeter_width : m_layer_info->extra_spacing * get_block_gap_width(m_current_tool,false); + if (solid_tool_toolchange) + dy = m_perimeter_width; x_to_wipe = solid_tool_toolchange ? std::numeric_limits::max(): x_to_wipe; - float target_speed = is_first_layer() ? std::min(m_first_layer_speed * 60.f, 4800.f) : 4800.f; - target_speed = solid_tool_toolchange ? 20.f * 60.f : target_speed; - // Nominal wipe-speed schedule. The applied wipe_speed is nominal_speed * speed_factor; speed_factor - // stays 1.0 unless the H2C prime-tower heating-during-wipe model below slows the wipe so the hotend - // can reach temperature (nominal_speed == wipe_speed when speed_factor == 1, i.e. single-nozzle). - float nominal_speed = 0.33f * target_speed; + float target_speed = is_first_layer() ? std::min(m_first_layer_speed * 60.f, m_max_speed) : m_max_speed; + target_speed = solid_tool_toolchange ? m_contact_speed : target_speed; + const std::vector WipeSpeedMap{0.33f * target_speed, 0.375f * target_speed, 0.458f * target_speed, 0.875f * target_speed, + std::min(target_speed, 0.875f * target_speed + 50.f)}; + float wipe_speed = WipeSpeedMap[0]; m_left_to_right = ((m_cur_layer_id + 3) % 4 >= 2); bool is_from_up = (m_cur_layer_id % 2 == 1); - // Prime-tower heating during wipe. Everything here is gated on m_is_multiple_nozzle (false for every - // current printer); the lambdas emit nothing until add_M104_by_requirement's gate opens, so the - // single-nozzle wipe is untouched. - // WipeSpeedMap mirrors the nominal schedule above and is read only by estimate_wipe_time. It is a - // std::array (stack, no per-call heap allocation); values depend on runtime target_speed so it - // cannot be static const. - const std::array WipeSpeedMap{0.33f * target_speed, 0.375f * target_speed, 0.458f * target_speed, - 0.875f * target_speed, std::min(target_speed, 0.875f * target_speed + 50.f)}; - auto estimate_wipe_time = [&cleaning_box, &x_to_wipe, &xr, &xl, &dy, &WipeSpeedMap, &solid_tool_toolchange]() -> float { - int n = std::ceil(x_to_wipe / (xr - xl)); - if (solid_tool_toolchange) n = (cleaning_box.lu[1] - cleaning_box.ld[1]) / dy; + auto estimate_time_kernel = [&WipeSpeedMap,&xr,&xl](int n) { + float time = std::numeric_limits::max(); float one_line_len = xr - xl; - float time = std::numeric_limits::max(); if (n <= 1) time = one_line_len / WipeSpeedMap[0]; else if (n <= 2) @@ -3659,65 +4038,51 @@ void WipeTower::toolchange_wipe_new(WipeTowerWriter &writer, const box_coordinat } return time * 60.f; }; - // Emit the arriving-hotend pre-heat inside the M632/M633 nozzle-change barrier. `M632 S[ H] - // M N` opens the barrier (M = firmware nozzle-change flag, N = slicer generated), the M104 sets the - // arriving hotend temp, and `M633` closes it. H2C's grouping is static (no dynamic nozzle map), so the - // H field is omitted (a dynamic nozzle map would supply a real nozzle id, a static map -1 =>no - // H). The counterproductive fan-on (M106 S255) used for departing-tool cooldown is intentionally - // omitted, since this is a pre-HEAT of the arriving tool. The whole helper is only ever called from - // add_M104_by_requirement, which is gated on m_is_multiple_nozzle (extruder_max_nozzle_count>1) => H2C - // only; every other printer's wipe tower is untouched. - // BBS: extruder change preheat uses M400 + M104 WITHOUT M632/M633 barrier. - // M632 barriers are only for carousel nozzle changes (emitted in nozzle_change_new/ramming). - auto format_line_M104 = [this](int target_temp, int target_extruder = -1, bool wait_for_moves = true, const std::string &comment = "") { - std::string buffer; - if (wait_for_moves) - buffer += "M400\n"; - buffer += "M104"; - if (target_extruder != -1 && target_extruder < (int) m_physical_extruder_map.size()) - buffer += (" T" + std::to_string(m_physical_extruder_map[target_extruder])); - buffer += " S" + std::to_string(target_temp) + " N0"; // N0 means the gcode is generated by the slicer - if (!comment.empty()) buffer += " ;" + comment; - buffer += '\n'; - return buffer; + auto estimate_wipe_time = [&estimate_time_kernel, & cleaning_box, &target_speed, &x_to_wipe, &xr, &xl, &dy, &WipeSpeedMap, &solid_tool_toolchange](int begin_line) -> float { + int n = std::ceil(x_to_wipe / (xr - xl)); + if (solid_tool_toolchange) n = (cleaning_box.lu[1] - cleaning_box.ld[1]) / dy; + float total_time = estimate_time_kernel(n); + float beg_time = n <= 0 ? 0 : estimate_time_kernel(begin_line); + return total_time-beg_time; }; - // m_is_multiple_nozzle gate needed because Orca calls toolchange_wipe_new for ALL printers (BBS has it H2C-only). - bool should_heating = m_is_multiple_nozzle && m_filpar[m_current_tool].filament_cooling_before_tower > EPSILON && - !solid_tool_toolchange && !is_first_layer(); - auto add_M104_by_requirement = [&writer, &format_line_M104, &should_heating, this]() { + + bool should_heating = m_filpar[m_current_tool].filament_cooling_before_tower > EPSILON && !solid_tool_toolchange && !is_first_layer(); + auto add_M104_by_requirement = [&writer, &should_heating, this]() { if (m_filpar[m_current_tool].filament_cooling_before_tower < EPSILON) return; if (!should_heating) return; float target_temp = is_first_layer() ? m_filpar[m_current_tool].nozzle_temperature_initial_layer : m_filpar[m_current_tool].nozzle_temperature; - writer.append(format_line_M104(target_temp, m_filament_map[m_current_tool] - 1)); + writer.format_line_M104(target_temp, get_extruder_id(m_current_tool, m_cur_layer_id)); }; float speed_factor = 1.f; - if (should_heating) { - // The heating-slowdown scaling is disabled — no additional heating time is required, so - // speed_factor stays 1.0. The structure and estimate_wipe_time/WipeSpeedMap are retained for - // future H2C tuning; the divide-by-zero/bounds guard is preserved in the commented body below. - // int extruder_id = m_filament_map[m_current_tool] - 1; - // if (extruder_id >= 0 && extruder_id < (int) m_hotend_heating_rate.size() && m_hotend_heating_rate[extruder_id] > 0.) { - // float estimate_time = estimate_wipe_time(); - // float heat_time = m_filpar[m_current_tool].filament_cooling_before_tower / m_hotend_heating_rate[extruder_id]; - // if (estimate_time < heat_time) speed_factor = estimate_time / heat_time; - // } - (void) estimate_wipe_time; // retain scaffolding above without an unused-lambda warning + if (should_heating) + { + //No additional heating time is required. + //float estimate_time = estimate_wipe_time(0); + //int extruder_id = m_filament_map[m_current_tool] - 1; + //float heat_time = m_filpar[m_current_tool].filament_cooling_before_tower / m_hotend_heating_rate[extruder_id]; + //heat_time /= 2.f; + //speed_factor = estimate_time / (heat_time+estimate_time); + //wipe_speed *= speed_factor; } - float wipe_speed = nominal_speed * speed_factor; - - // now the wiping itself: - for (int i = 0; true; ++i) { - if (i != 0) { - if (nominal_speed < 0.34f * target_speed) - nominal_speed = 0.375f * target_speed; - else if (nominal_speed < 0.377 * target_speed) - nominal_speed = 0.458f * target_speed; - else if (nominal_speed < 0.46f * target_speed) - nominal_speed = 0.875f * target_speed; - else - nominal_speed = std::min(target_speed, nominal_speed + 50.f); - wipe_speed = nominal_speed * speed_factor; + if (should_cooling_before_object) { + int n = (cleaning_box.lu[1] - cleaning_box.ld[1]) / dy; + int extruder_id = get_extruder_id(m_current_tool, m_cur_layer_id); + float cooling_time = (m_filpar[m_current_tool].filament_tower_interface_print_temp - m_filpar[m_current_tool].nozzle_temperature) / m_hotend_cooling_rate[extruder_id]; + if (n < 2) { + float estimate_time = estimate_wipe_time(0); + speed_factor = estimate_time > cooling_time? 1: estimate_time / cooling_time; + cooling_begin_line = 0; + } else { + float estimate_time = estimate_wipe_time(2); + speed_factor = estimate_time > cooling_time ? 1 : estimate_time / cooling_time; + cooling_begin_line = 2; //TODO: No slowdown for the first two lines. } + wipe_speed *= speed_factor; + } + + + for (int i = 0; true; ++i) { + if (i < WipeSpeedMap.size()) wipe_speed = WipeSpeedMap[i] * speed_factor; bool need_change_flow = need_thick_bridge_flow(writer.y()); // BBS: check the bridging area and use the bridge flow @@ -3725,44 +4090,53 @@ void WipeTower::toolchange_wipe_new(WipeTowerWriter &writer, const box_coordinat writer.set_extrusion_flow(extrusion_flow(0.2)); writer.append(";" + GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Height) + std::to_string(0.2) + "\n"); } - + float flat_iron_area = m_filpar[m_current_tool].flat_iron_area; float ironing_length = 3.; + + if (should_cooling_before_object && i == cooling_begin_line) { + writer.format_line_M104(m_filpar[m_current_tool].nozzle_temperature, get_extruder_id(m_current_tool, m_cur_layer_id)); + } + if (i == 0 && m_use_gap_wall) { // BBS: add ironing after extruding start if (m_left_to_right) { - float dx = xr + wipe_tower_wall_infill_overlap * m_perimeter_width - writer.pos().x(); - if (abs(dx) < ironing_length) ironing_length = abs(dx); - writer.extrude(writer.x() + ironing_length, writer.y(), wipe_speed); - writer.retract(retract_length, retract_speed); - writer.travel(writer.x() - 1.5 * ironing_length, writer.y(), 600.); - if (do_ironing && ironing_area > 0.f) { - writer.travel(writer.x() + 0.5f * ironing_length, writer.y(), 240.); - Vec2f pos{writer.x() + 1.f * ironing_length, writer.y()}; - writer.spiral_flat_ironing(writer.pos(), ironing_area, m_perimeter_width, flat_iron_speed); - writer.travel(pos, wipe_speed); - } else - writer.travel(writer.x() + 1.5 * ironing_length, writer.y(), 240.); - writer.retract(-retract_length, retract_speed); - add_M104_by_requirement(); // Pre-heat the arriving hotend during the wipe + if (should_line_ironing) { + float dx = xr + wipe_tower_wall_infill_overlap * m_perimeter_width - writer.pos().x(); + if (abs(dx) < ironing_length) ironing_length = abs(dx); + writer.extrude(writer.x() + ironing_length, writer.y(), wipe_speed); + writer.retract(retract_length, retract_speed); + writer.travel(writer.x() - 1.5 * ironing_length, writer.y(), 600.); + if (should_flat_ironging) { + writer.travel(writer.x() + 0.5f * ironing_length, writer.y(), 240.); + Vec2f pos{writer.x() + 1.f * ironing_length, writer.y()}; + writer.spiral_flat_ironing(writer.pos(), flat_iron_area, m_perimeter_width, flat_iron_speed); + writer.travel(pos, wipe_speed); + } else + writer.travel(writer.x() + 1.5 * ironing_length, writer.y(), 240.); + writer.retract(-retract_length, retract_speed); + } + add_M104_by_requirement(); writer.extrude(xr + wipe_tower_wall_infill_overlap * m_perimeter_width, writer.y(), wipe_speed); } else { - float dx = xl - wipe_tower_wall_infill_overlap * m_perimeter_width - writer.pos().x(); - if (abs(dx) < ironing_length) ironing_length = abs(dx); - writer.extrude(writer.x() - ironing_length, writer.y(), wipe_speed); - writer.retract(retract_length, retract_speed); - writer.travel(writer.x() + 1.5 * ironing_length, writer.y(), 600.); - if (do_ironing && ironing_area > 0.f) { - writer.travel(writer.x() - 0.5f * ironing_length, writer.y(), 240.); - Vec2f pos{writer.x() - 1.0f * ironing_length, writer.y()}; - writer.spiral_flat_ironing(writer.pos(), ironing_area, m_perimeter_width, flat_iron_speed); - writer.travel(pos, wipe_speed); - }else - writer.travel(writer.x() - 1.5 * ironing_length, writer.y(), 240.); - writer.retract(-retract_length, retract_speed); - add_M104_by_requirement(); // Pre-heat the arriving hotend during the wipe + if (should_line_ironing) { + float dx = xl - wipe_tower_wall_infill_overlap * m_perimeter_width - writer.pos().x(); + if (abs(dx) < ironing_length) ironing_length = abs(dx); + writer.extrude(writer.x() - ironing_length, writer.y(), wipe_speed); + writer.retract(retract_length, retract_speed); + writer.travel(writer.x() + 1.5 * ironing_length, writer.y(), 600.); + if (should_flat_ironging) { + writer.travel(writer.x() - 0.5f * ironing_length, writer.y(), 240.); + Vec2f pos{writer.x() - 1.0f * ironing_length, writer.y()}; + writer.spiral_flat_ironing(writer.pos(), flat_iron_area, m_perimeter_width, flat_iron_speed); + writer.travel(pos, wipe_speed); + } else + writer.travel(writer.x() - 1.5 * ironing_length, writer.y(), 240.); + writer.retract(-retract_length, retract_speed); + } + add_M104_by_requirement(); writer.extrude(xl - wipe_tower_wall_infill_overlap * m_perimeter_width, writer.y(), wipe_speed); } } else { - if (i == 0) add_M104_by_requirement(); // Pre-heat the arriving hotend during the wipe + if (i == 0) add_M104_by_requirement(); if (m_left_to_right) writer.extrude(xr + wipe_tower_wall_infill_overlap * m_perimeter_width, writer.y(), wipe_speed); else @@ -3855,67 +4229,6 @@ int WipeTower::get_filament_category(int filament_id) return m_filament_categories[filament_id]; } -bool WipeTower::is_in_same_extruder(int filament_id_1, int filament_id_2) -{ - if (filament_id_1 >= m_filament_map.size() || filament_id_2 >= m_filament_map.size()) - return true; - - return m_filament_map[filament_id_1] == m_filament_map[filament_id_2]; -} - -std::string WipeTower::format_nozzle_change_tag(bool start, int old_filament_id, int new_filament_id) const -{ - const std::string &tag = start ? GCodeProcessor::Nozzle_Change_Start_Tag : GCodeProcessor::Nozzle_Change_End_Tag; - int old_nozzle = (old_filament_id >= 0 && old_filament_id < (int)m_filament_nozzle_map.size()) - ? m_filament_nozzle_map[old_filament_id] : -1; - int new_nozzle = (new_filament_id >= 0 && new_filament_id < (int)m_filament_nozzle_map.size()) - ? m_filament_nozzle_map[new_filament_id] : -1; - char buff[96]; - snprintf(buff, sizeof(buff), ";%s OF%d NF%d ON%d NN%d\n", tag.c_str(), old_filament_id, new_filament_id, old_nozzle, new_nozzle); - return std::string(buff); -} - -// Per-extruder printable-height clamp: is an extruder still allowed to print on this wipe-tower layer, -// or is it its final layer above the extruder's printable height? -// Orca: the arriving extruder id is resolved as m_filament_map[tool]-1 (1-based map, layer-static), -// because Orca's wipe tower is extruder-level rather than tracking a per-layer nozzle map (the same -// idiom the pre-heat path uses in toolchange_wipe_new). Gated on m_is_multi_extruder so that -// single-extruder printers (including ones whose extruder_printable_height defaults to {0}) always -// return true and leave wipe-tower g-code unchanged. -bool WipeTower::is_valid_last_layer(int tool, int layer_id, double layer_z) const -{ - if (!m_is_multi_extruder) - return true; - int extruder_id = (tool >= 0 && tool < (int) m_filament_map.size()) ? m_filament_map[tool] - 1 : -1; - if (extruder_id < 0 || extruder_id >= (int) m_printable_height.size() || extruder_id >= (int) m_last_layer_id.size()) - return true; - if (m_last_layer_id[extruder_id] == layer_id && layer_z > m_printable_height[extruder_id]) - return false; - return true; -} - -// Records, per extruder, the last wipe-tower layer index that uses it, so is_valid_last_layer can -// recognise the extruder's final layer. Inert for single-extruder printers (early return); -// bounds-checked because m_filament_map may be empty/short. -void WipeTower::set_nozzle_last_layer_id() -{ - if (!m_is_multi_extruder) - return; - for (int idx = 0; idx < (int) m_plan.size(); ++idx) { - const auto &info = m_plan[idx]; - for (const auto &tc : info.tool_changes) { - int old_tool = (int) tc.old_tool; - int new_tool = (int) tc.new_tool; - int old_ext = (old_tool >= 0 && old_tool < (int) m_filament_map.size()) ? m_filament_map[old_tool] - 1 : -1; - int new_ext = (new_tool >= 0 && new_tool < (int) m_filament_map.size()) ? m_filament_map[new_tool] - 1 : -1; - if (old_ext >= 0 && old_ext < (int) m_last_layer_id.size()) - m_last_layer_id[old_ext] = idx; - if (new_ext >= 0 && new_ext < (int) m_last_layer_id.size()) - m_last_layer_id[new_ext] = idx; - } - } -} - void WipeTower::reset_block_status() { for (auto &block : m_wipe_tower_blocks) { @@ -3924,6 +4237,39 @@ void WipeTower::reset_block_status() block.last_nozzle_change_id = -1; } } +void WipeTower::set_nozzle_last_layer_id() +{ + for (int idx = 0; idx < m_plan.size(); idx++) { + auto &info = m_plan[idx]; + for(int i =0 ; i= 0) m_last_layer_id[get_extruder_id(old_tool, idx)] = idx; + m_last_layer_id[get_extruder_id(new_tool, idx)] = idx; + } + } +} + +void WipeTower::set_first_layer_flow_ratio(const float flow_ratio) +{ + m_first_layer_flow_ratio = flow_ratio; +} + +// Orca: default/initial-layer/travel acceleration are object-scope options here (PrintConfig +// members read directly in the BBS ctor), so Print resolves the columns and pushes them in. +void WipeTower::set_accelerations(const std::vector &normal, const std::vector &first_layer_normal, + const std::vector &travel, const std::vector &first_layer_travel) +{ + auto to_accels = [](const std::vector &values, std::vector &accels) { + accels.clear(); + for (double value : values) + accels.emplace_back((unsigned int) floor(value + 0.5)); + }; + to_accels(normal, m_normal_accels); + to_accels(first_layer_normal, m_first_layer_normal_accels); + to_accels(travel, m_travel_accels); + to_accels(first_layer_travel, m_first_layer_travel_accels); +} void WipeTower::update_all_layer_depth(float wipe_tower_depth) { @@ -3956,7 +4302,7 @@ void WipeTower::update_all_layer_depth(float wipe_tower_depth) } } -void WipeTower::generate_wipe_tower_blocks() +void WipeTower::generate_wipe_tower_blocks(bool add_solid_flag) { // 1. generate all layer depth m_all_layers_depth.clear(); @@ -3964,7 +4310,7 @@ void WipeTower::generate_wipe_tower_blocks() m_cur_layer_id = 0; for (auto& info : m_plan) { for (const WipeTowerInfo::ToolChange &tool_change : info.tool_changes) { - if (is_in_same_extruder(tool_change.old_tool, tool_change.new_tool)) { + if (!is_need_ramming(tool_change.old_tool, tool_change.new_tool, m_cur_layer_id)) { int filament_adhesiveness_category = get_filament_category(tool_change.new_tool); add_depth_to_block(tool_change.new_tool, filament_adhesiveness_category, tool_change.required_depth); } @@ -3997,35 +4343,13 @@ void WipeTower::generate_wipe_tower_blocks() auto* block = get_block_by_category(iter->first, true); if (block->layer_depths.empty()) { block->layer_depths.resize(all_layer_category_to_depth.size(), 0); - block->solid_infill.resize(all_layer_category_to_depth.size(), false); block->finish_depth.resize(all_layer_category_to_depth.size(), 0); + block->layers_type.resize(all_layer_category_to_depth.size(), WipeTowerLayerType::Normal); } block->depth = std::max(block->depth, iter->second); block->layer_depths[layer_id] = iter->second; } } - - // add solid infill flag - int solid_infill_layer = 4; - for (WipeTowerBlock& block : m_wipe_tower_blocks) { - for (int layer_id = 0; layer_id < all_layer_category_to_depth.size(); ++layer_id) { - std::unordered_map &category_to_depth = all_layer_category_to_depth[layer_id]; - if (is_approx(category_to_depth[block.filament_adhesiveness_category], 0.f)) { - int layer_count = solid_infill_layer; - while (layer_count > 0) { - if (layer_id + layer_count < all_layer_category_to_depth.size()) { - std::unordered_map& up_layer_depth = all_layer_category_to_depth[layer_id + layer_count]; - if (!is_approx(up_layer_depth[block.filament_adhesiveness_category], 0.f)) { - block.solid_infill[layer_id] = true; - break; - } - } - --layer_count; - } - } - } - } - // 4. get real depth for every layer for (int layer_id = m_plan.size() - 1; layer_id >= 0; --layer_id) { m_plan[layer_id].depth = 0; @@ -4045,54 +4369,166 @@ void WipeTower::generate_wipe_tower_blocks() } } } + + // add solid infill flag + if (add_solid_flag) { + int solid_infill_layer_low = 4; + std::vector> layers_used_tools; + + int first_tool = -1; + for (const auto &layer : m_plan) { + if (!layer.tool_changes.empty()) { + first_tool = layer.tool_changes.front().old_tool; + break; + } + } + for (auto &info : m_plan) { + std::unordered_set used_tools; + if (info.tool_changes.empty()) { + used_tools.insert(get_filament_category(first_tool)); + } else { + for (const WipeTowerInfo::ToolChange &tool_change : info.tool_changes) { + used_tools.insert(get_filament_category(tool_change.old_tool)); + used_tools.insert(get_filament_category(tool_change.new_tool)); + } + first_tool = info.tool_changes.back().new_tool; + } + layers_used_tools.push_back(used_tools); + } + + for (WipeTowerBlock &block : m_wipe_tower_blocks) { + for (int layer_id = 0; layer_id < all_layer_category_to_depth.size(); ++layer_id) { + std::unordered_map &category_to_depth = all_layer_category_to_depth[layer_id]; + if (category_to_depth[block.filament_adhesiveness_category] < block.layer_depths[layer_id] - m_perimeter_width) { + bool cur_has_block_category = layers_used_tools[layer_id].count(block.filament_adhesiveness_category); + int layer_count = solid_infill_layer_low; + while (layer_count > 0) { + if (layer_id + layer_count < all_layer_category_to_depth.size()) { + std::unordered_map &up_layer_depth = all_layer_category_to_depth[layer_id + layer_count]; + { + bool up_has_block_category = layers_used_tools[layer_id + layer_count].count(block.filament_adhesiveness_category); + if (cur_has_block_category != up_has_block_category) { + block.layers_type[layer_id] = WipeTowerLayerType::Solid; + break; + } + } + } + --layer_count; + } + } + if (layer_id > 0) { + bool cur_has_block_category = layers_used_tools[layer_id].count(block.filament_adhesiveness_category); + bool pre_has_block_category = layers_used_tools[layer_id - 1].count(block.filament_adhesiveness_category); + if (cur_has_block_category != pre_has_block_category) { block.layers_type[layer_id] = WipeTowerLayerType::Contact; } + if (block.layers_type[layer_id - 1] == WipeTowerLayerType::Contact && block.layers_type[layer_id] != WipeTowerLayerType::Contact) { + block.layers_type[layer_id] = WipeTowerLayerType::Contact_UP; + } + } + } + } + } + +} +void WipeTower::calc_block_infill_gap() +{ + //1.calc block infill gap width + struct BlockInfo + { + bool has_ramming = false; + bool has_reverse_travel = false; + float depth = 0.f; + }; + std::unordered_map block_info; + std::unordered_map high_block_info; + for (int i= (int)m_plan.size()-1;i>=0;i--) + { + for (auto &toolchange : m_plan[i].tool_changes) { + int new_tool =toolchange.new_tool; + int old_tool =toolchange.old_tool; + if (is_need_ramming(old_tool,new_tool, i)) { + bool extruder_change = !is_same_extruder(new_tool, old_tool, i); + block_info[m_filpar[old_tool].category].has_ramming=true; + if (is_need_reverse_travel(old_tool, extruder_change)) block_info[m_filpar[old_tool].category].has_reverse_travel = true; + block_info[m_filpar[old_tool].category].depth += toolchange.nozzle_change_depth; + } + if (!block_info.count(m_filpar[new_tool].category)) block_info.insert({m_filpar[new_tool].category,BlockInfo{}}); + block_info[m_filpar[new_tool].category].depth += toolchange.required_depth - toolchange.nozzle_change_depth; + } + for (auto &block : block_info) { + if (high_block_info.count(block.first) && high_block_info[block.first].depth > block.second.depth) + block.second.depth = high_block_info[block.first].depth; + } + high_block_info = block_info; + + for (auto &block : block_info) { block.second.depth = 0.f;} + if (i == 0) block_info = high_block_info; + } + float max_depth = std::accumulate(block_info.begin(), block_info.end(), 0.f, [](float value, const std::pair &block) { return value + block.second.depth; }); + float height_to_depth = get_limit_depth_by_height(m_wipe_tower_height); + float height_to_spacing = max_depth > height_to_depth ? 1.f : height_to_depth / max_depth; + + float spacing_ratio = m_extra_spacing - 1.f; + float extra_width = spacing_ratio * m_perimeter_width; + float line_gap_tol = 2.f * m_nozzle_change_perimeter_width; //If the block's line_gap is greater than it, the block should be aligned. + for (auto &info : block_info) { + //case1: no ramming, it can always align + if (!info.second.has_ramming) { + m_block_infill_gap_width[info.first].first = m_block_infill_gap_width[info.first].second = extra_width + m_perimeter_width; + } + // case2: has ramming, but no reverse travel + // + else if (!info.second.has_reverse_travel) { + float line_gap = m_nozzle_change_perimeter_width + extra_width; + if (!m_use_rib_wall) line_gap *= height_to_spacing; + if (line_gap < line_gap_tol) { + m_block_infill_gap_width[info.first].first = m_perimeter_width + extra_width; + m_block_infill_gap_width[info.first].second = m_nozzle_change_perimeter_width + extra_width; + } else { + m_block_infill_gap_width[info.first].first = m_block_infill_gap_width[info.first].second = m_nozzle_change_perimeter_width + extra_width; + } + } + // case 3: has ramming and reverse travel + else { + float extra_tpu_fix_spacing = m_tpu_fixed_spacing - 1.f; + float line_gap = m_nozzle_change_perimeter_width + std::max(extra_tpu_fix_spacing * m_perimeter_width, extra_width); + if (!m_use_rib_wall) line_gap = height_to_spacing * line_gap; + if (line_gap < line_gap_tol) { + m_block_infill_gap_width[info.first].first = m_perimeter_width + extra_width; + m_block_infill_gap_width[info.first].second = m_nozzle_change_perimeter_width + std::max(extra_tpu_fix_spacing * m_perimeter_width, extra_width); + } else { + m_block_infill_gap_width[info.first].first = m_block_infill_gap_width[info.first].second = m_nozzle_change_perimeter_width + + std::max(extra_tpu_fix_spacing * m_perimeter_width, extra_width); + } + } + } + + //2. recalculate toolchange depth + for (int idx = 0; idx < m_plan.size(); idx++) { + for (auto &toolchange : m_plan[idx].tool_changes) { + toolchange = set_toolchange(toolchange.old_tool, toolchange.new_tool, m_plan[idx].height, toolchange.wipe_volume, toolchange.purge_volume,idx); + } + } + m_extra_spacing = 1.f; } void WipeTower::plan_tower_new() { if (m_wipe_tower_brim_width < 0) m_wipe_tower_brim_width = get_auto_brim_by_height(m_wipe_tower_height); + calc_block_infill_gap(); if (m_use_rib_wall) { // recalculate wipe_tower_with and layer's depth - generate_wipe_tower_blocks(); + generate_wipe_tower_blocks(false); float max_depth = std::accumulate(m_wipe_tower_blocks.begin(), m_wipe_tower_blocks.end(), 0.f, [](float a, const auto &t) { return a + t.depth; }) + m_perimeter_width; float square_width = align_ceil(std::sqrt(max_depth * m_wipe_tower_width * m_extra_spacing), m_perimeter_width); - //std::cout << " before m_wipe_tower_width = " << m_wipe_tower_width << " max_depth = " << max_depth << std::endl; m_wipe_tower_width = square_width; - float width = m_wipe_tower_width - 2 * m_perimeter_width; for (int idx = 0; idx < m_plan.size(); idx++) { for (auto &toolchange : m_plan[idx].tool_changes) { - float length_to_extrude = toolchange.wipe_length; - float depth = std::ceil(length_to_extrude / width) * m_perimeter_width; - float nozzle_change_depth = 0; - if (!m_filament_map.empty() && m_filament_map[toolchange.old_tool] != m_filament_map[toolchange.new_tool]) { - double e_flow = nozzle_change_extrusion_flow(m_plan[idx].height); - double length = m_filaments_change_length[toolchange.old_tool] / e_flow; - int nozzle_change_line_count = length / (m_wipe_tower_width - 2*m_nozzle_change_perimeter_width) + 1; - if (has_tpu_filament()) - nozzle_change_depth = m_tpu_fixed_spacing * nozzle_change_line_count * m_nozzle_change_perimeter_width; - else - nozzle_change_depth = nozzle_change_line_count * m_nozzle_change_perimeter_width; - depth += nozzle_change_depth; - } - if (nozzle_change_depth == 0 - && !m_filament_nozzle_map.empty() - && toolchange.old_tool < (int)m_filament_nozzle_map.size() && toolchange.new_tool < (int)m_filament_nozzle_map.size() - && m_filament_nozzle_map[toolchange.old_tool] != m_filament_nozzle_map[toolchange.new_tool]) { - double e_flow = nozzle_change_extrusion_flow(m_plan[idx].height); - double length = m_filaments_change_length[toolchange.old_tool] / e_flow; - int nozzle_change_line_count = length / (m_wipe_tower_width - 2*m_nozzle_change_perimeter_width) + 1; - if (has_tpu_filament()) - nozzle_change_depth = m_tpu_fixed_spacing * nozzle_change_line_count * m_nozzle_change_perimeter_width; - else - nozzle_change_depth = nozzle_change_line_count * m_nozzle_change_perimeter_width; - depth += nozzle_change_depth; - } - toolchange.nozzle_change_depth = nozzle_change_depth; - toolchange.required_depth = depth; + toolchange = set_toolchange(toolchange.old_tool, toolchange.new_tool, m_plan[idx].height, toolchange.wipe_volume, toolchange.purge_volume,idx); } } } - generate_wipe_tower_blocks(); + generate_wipe_tower_blocks(true); float max_depth = 0.f; for (const auto &block : m_wipe_tower_blocks) { @@ -4126,28 +4562,29 @@ void WipeTower::plan_tower_new() for (int idx = 0; idx < m_plan.size(); idx++) { auto &info = m_plan[idx]; - if (idx == 0 && m_extra_spacing > 1.f + EPSILON) { + if (idx == 0 /*&& m_extra_spacing > 1.f + EPSILON*/) { // apply solid fill for the first layer info.extra_spacing = 1.f; for (auto &toolchange : info.tool_changes) { - float x_to_wipe = volume_to_length(toolchange.wipe_volume, m_perimeter_width, info.height); + //float x_to_wipe = volume_to_length(toolchange.wipe_volume, m_perimeter_width, info.height); float line_len = m_wipe_tower_width - 2 * m_perimeter_width; - float x_to_wipe_new = x_to_wipe * m_extra_spacing; - x_to_wipe_new = std::floor(x_to_wipe_new / line_len) * line_len; - x_to_wipe_new = std::max(x_to_wipe_new, x_to_wipe); + float wipe_depth = (toolchange.required_depth - toolchange.nozzle_change_depth) * m_extra_spacing; + float wipe_line_count = wipe_depth / m_perimeter_width; + float nozzle_change_depth = toolchange.nozzle_change_depth * m_extra_spacing; - int line_count = std::ceil((x_to_wipe_new - WT_EPSILON) / line_len); - // nozzle change length - int nozzle_change_line_count = (toolchange.nozzle_change_depth + WT_EPSILON) / m_nozzle_change_perimeter_width; + int nozzle_change_line_count = (toolchange.nozzle_change_depth * m_extra_spacing + WT_EPSILON) / m_nozzle_change_perimeter_width; - toolchange.required_depth = line_count * m_perimeter_width + nozzle_change_line_count * m_nozzle_change_perimeter_width; - toolchange.wipe_volume = x_to_wipe_new / x_to_wipe * toolchange.wipe_volume; - toolchange.wipe_length = x_to_wipe_new; + toolchange.required_depth = wipe_depth + nozzle_change_depth; + toolchange.wipe_length = wipe_line_count * line_len; + toolchange.wipe_volume = length_to_volume(toolchange.wipe_length, m_perimeter_width, info.height); + toolchange.nozzle_change_length = nozzle_change_line_count * (m_wipe_tower_width - (m_nozzle_change_perimeter_width + m_perimeter_width)); + toolchange.nozzle_change_depth = nozzle_change_depth; } } else { info.extra_spacing = m_extra_spacing; for (auto &toolchange : info.tool_changes) { toolchange.required_depth *= m_extra_spacing; + toolchange.nozzle_change_depth *= m_extra_spacing; toolchange.wipe_length = volume_to_length(toolchange.wipe_volume, m_perimeter_width, info.height); } } @@ -4155,7 +4592,8 @@ void WipeTower::plan_tower_new() } update_all_layer_depth(max_depth); - set_nozzle_last_layer_id(); // record per-extruder last layer for is_valid_last_layer + set_nozzle_last_layer_id(); + if(m_use_gap_wall) get_all_wall_skip_points(); float diagonal = sqrt(m_wipe_tower_depth * m_wipe_tower_depth + m_wipe_tower_width * m_wipe_tower_width); m_rib_length = std::max({m_rib_length, diagonal}); m_rib_length += m_extra_rib_length; @@ -4227,7 +4665,6 @@ void WipeTower::generate_new(std::vector layer_result; int index = 0; - std::unordered_set solid_blocks_id;// The contact surface of different bonded materials is solid. for (auto layer : m_plan) { reset_block_status(); m_cur_layer_id = index++; - m_prev_layer_had_interface = m_current_layer_has_interface; - m_current_layer_has_interface = !solid_blocks_id.empty(); set_layer(layer.z, layer.height, 0, false, layer.z == m_plan.back().z); - if (m_layer_info->depth < m_perimeter_width) continue; - if (m_wipe_tower_blocks.size() == 1) { if (m_layer_info->depth < m_wipe_tower_depth - m_perimeter_width) { // align y shift to perimeter width @@ -4263,32 +4695,30 @@ void WipeTower::generate_new(std::vector int { + auto get_wall_filament_for_this_layer = [this, &layer, &wall_filament]() -> int { if (layer.tool_changes.size() == 0) return -1; int candidate_id = -1; for (size_t idx = 0; idx < layer.tool_changes.size(); ++idx) { if (idx == 0) { - // An extruder's last-layer filament above its printable height cannot supply the - // outer wall. is_valid_last_layer is inert unless it clamps. - if (layer.tool_changes[idx].old_tool == wall_filament_id && is_valid_last_layer(layer.tool_changes[idx].old_tool, m_cur_layer_id, layer.z)) - return wall_filament_id; - else if (m_filpar[layer.tool_changes[idx].old_tool].category == m_filpar[wall_filament_id].category && - is_valid_last_layer(layer.tool_changes[idx].old_tool, m_cur_layer_id, layer.z)) { + if (layer.tool_changes[idx].old_tool == wall_filament && is_valid_last_layer(layer.tool_changes[idx].old_tool, this->m_cur_layer_id, layer.z)) + return wall_filament; + else if (m_filpar[layer.tool_changes[idx].old_tool].category == m_filpar[wall_filament].category && + is_valid_last_layer(layer.tool_changes[idx].old_tool, this->m_cur_layer_id, layer.z)) { candidate_id = layer.tool_changes[idx].old_tool; } } - if (layer.tool_changes[idx].new_tool == wall_filament_id) { - return wall_filament_id; + if (layer.tool_changes[idx].new_tool == wall_filament) { + return wall_filament; } - if ((candidate_id == -1) && (m_filpar[layer.tool_changes[idx].new_tool].category == m_filpar[wall_filament_id].category)) + if ((candidate_id == -1) && (m_filpar[layer.tool_changes[idx].new_tool].category == m_filpar[wall_filament].category)) candidate_id = layer.tool_changes[idx].new_tool; } return candidate_id == -1 ? layer.tool_changes[0].new_tool : candidate_id; @@ -4300,7 +4730,7 @@ void WipeTower::generate_new(std::vectorlayers_type[m_cur_layer_id] == WipeTowerLayerType::Contact; const auto * block2 = get_block_by_category(m_filpar[layer.tool_changes[i].old_tool].category, false); - id = std::find_if(m_wipe_tower_blocks.begin(), m_wipe_tower_blocks.end(), [&](const WipeTowerBlock &b) { return &b == block2; }) - m_wipe_tower_blocks.begin(); - bool solid_nozzlechange = solid_blocks_id.count(id); + if(block2) solid_nozzlechange = block2->layers_type[m_cur_layer_id] == WipeTowerLayerType::Contact; layer_result.emplace_back(tool_change_new(layer.tool_changes[i].new_tool, solid_toolchange,solid_nozzlechange)); if (i == 0 && (layer.tool_changes[i].old_tool == wall_idx)) { @@ -4348,7 +4777,6 @@ void WipeTower::generate_new(std::vector next_solid_blocks_id; // insert finish block if (wall_idx != -1) { if (layer.tool_changes.empty()) { @@ -4361,7 +4789,8 @@ void WipeTower::generate_new(std::vector> &result) @@ -4521,7 +4940,7 @@ void WipeTower::generate(std::vector> & } if (i == idx) { - layer_result.emplace_back(tool_change(layer.tool_changes[i].new_tool, m_enable_timelapse_print ? false : true)); + layer_result.emplace_back(tool_change(layer.tool_changes[i].new_tool, m_enable_timelapse_print ? false : true, false)); // finish_layer will be called after this toolchange finish_layer_tcr = finish_layer(false, layer.extruder_fill); } @@ -4529,7 +4948,7 @@ void WipeTower::generate(std::vector> & if (idx == -1 && i == 0) { layer_result.emplace_back(tool_change(layer.tool_changes[i].new_tool, false, true)); } else { - layer_result.emplace_back(tool_change(layer.tool_changes[i].new_tool)); + layer_result.emplace_back(tool_change(layer.tool_changes[i].new_tool, false, false)); } } } @@ -4552,21 +4971,23 @@ void WipeTower::generate(std::vector> & result.emplace_back(std::move(layer_result)); } } - +#endif WipeTower::ToolChangeResult WipeTower::only_generate_out_wall(bool is_new_mode) { size_t old_tool = m_current_tool; - WipeTowerWriter writer(m_layer_height, m_perimeter_width, m_gcode_flavor, m_filpar); + WipeTowerWriter writer(m_layer_height, m_perimeter_width, m_gcode_flavor, m_filpar, m_enable_arc_fitting); writer.set_extrusion_flow(m_extrusion_flow) .set_z(m_z_pos) .set_initial_tool(m_current_tool) .set_y_shift(m_y_shift - (m_current_shape == SHAPE_REVERSED ? m_layer_info->toolchanges_depth() : 0.f)); + set_for_wipe_tower_writer(writer); + // Slow down on the 1st layer. bool first_layer = is_first_layer(); // BBS: speed up perimeter speed to 90mm/s for non-first layer - float feedrate = first_layer ? std::min(m_first_layer_speed * 60.f, 5400.f) : std::min(60.0f * m_filpar[m_current_tool].max_e_speed / m_extrusion_flow, 5400.f); + float feedrate = first_layer ? std::min(m_first_layer_speed * 60.f, m_max_speed) : std::min(60.0f * m_filpar[m_current_tool].max_e_speed / m_extrusion_flow, m_max_speed); float fill_box_y = m_layer_info->toolchanges_depth() + m_perimeter_width; box_coordinates fill_box(Vec2f(m_perimeter_width, fill_box_y), m_wipe_tower_width - 2 * m_perimeter_width, m_layer_info->depth - fill_box_y); @@ -4679,7 +5100,9 @@ Polygon WipeTower::generate_support_wall_new(WipeTowerWriter &writer, const box_ } if (!extrude_perimeter) return wall_polygon; - if (skip_points) { result_wall = contrust_gap_for_skip_points(wall_polygon,m_wall_skip_points,m_wipe_tower_width,2.5*m_perimeter_width,insert_skip_polygon); } + if (skip_points) { + result_wall = contrust_gap_for_skip_points(wall_polygon, m_wall_skip_points[m_cur_layer_id], m_wipe_tower_width, 2.5 * m_perimeter_width, insert_skip_polygon); + } else { result_wall.push_back(to_polyline(wall_polygon)); insert_skip_polygon = wall_polygon; @@ -4689,7 +5112,6 @@ Polygon WipeTower::generate_support_wall_new(WipeTowerWriter &writer, const box_ BoundingBox bbox = get_extents(result_wall); m_rib_offset = Vec2f(-unscaled(bbox.min.x()), -unscaled(bbox.min.y())); } - return insert_skip_polygon; } @@ -4699,7 +5121,7 @@ Polygon WipeTower::generate_support_wall(WipeTowerWriter &writer, const box_coor float retract_speed = m_filpar[m_current_tool].retract_speed *60 ; bool is_left = false; bool is_right = false; - for (auto pt : m_wall_skip_points) { + for (auto pt : m_wall_skip_points[m_cur_layer_id]) { if (abs(pt.x()) < EPSILON) { is_left = true; } else if (abs(pt.x() - m_wipe_tower_width) < EPSILON) { @@ -4750,7 +5172,7 @@ Polygon WipeTower::generate_support_wall(WipeTowerWriter &writer, const box_coor index = (index + 1) % 4; if (index == 2) { if (is_right) { - std::vector break_segments = remove_points_from_segment(Segment(wt_box.rd, wt_box.ru), m_wall_skip_points, 2.5 * m_perimeter_width); + std::vector break_segments = remove_points_from_segment(Segment(wt_box.rd, wt_box.ru), m_wall_skip_points[m_cur_layer_id], 2.5 * m_perimeter_width); for (auto iter = break_segments.begin(); iter != break_segments.end(); ++iter) { float dx = iter->start.x() - writer.pos().x(); float dy = iter->start.y() - writer.pos().y(); @@ -4770,7 +5192,7 @@ Polygon WipeTower::generate_support_wall(WipeTowerWriter &writer, const box_coor } } else if (index == 0) { if (is_left) { - std::vector break_segments = remove_points_from_segment(Segment(wt_box.ld, wt_box.lu), m_wall_skip_points, 2.5 * m_perimeter_width); + std::vector break_segments = remove_points_from_segment(Segment(wt_box.ld, wt_box.lu), m_wall_skip_points[m_cur_layer_id], 2.5 * m_perimeter_width); for (auto iter = break_segments.rbegin(); iter != break_segments.rend(); ++iter) { float dx = iter->end.x() - writer.pos().x(); float dy = iter->end.y() - writer.pos().y(); @@ -4831,4 +5253,43 @@ bool WipeTower::need_thick_bridge_flow(float pos_y) const { return false; } +bool WipeTower::is_valid_last_layer(int tool, int layer_id, double layer_z) const +{ + int extruder_id = get_extruder_id(tool, layer_id); + if (extruder_id < 0 || extruder_id >= m_printable_height.size()) return true; + if (m_last_layer_id[extruder_id] == layer_id && layer_z > m_printable_height[extruder_id]) return false; + return true; +} +float WipeTower::get_block_gap_width(int tool,bool is_nozzlechangle) +{ + //assert(m_block_infill_gap_width.count(m_filpar[tool].category));//The code contains logic that attempts to access non-existent blocks, + // such as in case of involving two extruders with only a single head and a single layer, + // some code will attempt to access the block's nozzle_change_gap_width, even though the block does not exist. + if (!m_block_infill_gap_width.count(m_filpar[tool].category)) { + return is_nozzlechangle ? m_nozzle_change_perimeter_width : m_perimeter_width; + } + return is_nozzlechangle ? m_block_infill_gap_width[m_filpar[tool].category].second : m_block_infill_gap_width[m_filpar[tool].category].first; + +} + +bool WipeTower::is_need_ramming(int filament_id_1, int filament_id_2, int layer_id) const +{ + return !m_multi_nozzle_group_result->are_filaments_same_nozzle(filament_id_1, filament_id_2, layer_id); +} +bool WipeTower::is_same_extruder(int filament_id_1, int filament_id_2, int layer_id) const +{ + return m_multi_nozzle_group_result->are_filaments_same_extruder(filament_id_1, filament_id_2, layer_id); +} + +bool WipeTower::is_same_nozzle(int filament_id_1, int filament_id_2, int layer_id) const +{ + return m_multi_nozzle_group_result->are_filaments_same_nozzle(filament_id_1, filament_id_2, layer_id); +} + +int WipeTower::get_nozzle_id(int filament_id, int layer_id) const { return m_multi_nozzle_group_result->get_nozzle_id(filament_id, layer_id); } + +int WipeTower::get_extruder_id(int filament_id, int layer_id) const { + return m_multi_nozzle_group_result->get_extruder_id(filament_id, layer_id); +} + } // namespace Slic3r diff --git a/src/libslic3r/GCode/WipeTower.hpp b/src/libslic3r/GCode/WipeTower.hpp index 508765824e..07f9a8f269 100644 --- a/src/libslic3r/GCode/WipeTower.hpp +++ b/src/libslic3r/GCode/WipeTower.hpp @@ -12,7 +12,7 @@ #include "libslic3r/Polyline.hpp" #include "libslic3r/TriangleMesh.hpp" #include - +#include "libslic3r/MultiNozzleUtils.hpp" namespace Slic3r { @@ -84,7 +84,6 @@ public: bool priming; bool is_tool_change{false}; - bool is_contact{false}; Vec2f tool_change_start_pos; // Pass a polyline so that normal G-code generator can do a wipe for us. @@ -108,6 +107,7 @@ public: // executing the gcode finish_layer_tcr. bool is_finish_first = false; + bool is_contact = false; NozzleChangeResult nozzle_change_result; // Sum the total length of the extrusion. @@ -122,6 +122,8 @@ public: } return e_length; } + // Orca: set by WipeTower2 (non-BBL tower) to force a travel to the tower even when the + // previous position is unknown; read by WipeTowerIntegration::append_tcr2 (GCode.cpp). bool force_travel = false; }; @@ -162,15 +164,12 @@ public: bool priming, size_t old_tool, bool is_finish, - bool is_tool_change, - float purge_volume, - bool is_contact = false) const; + bool is_tool_change, float purge_volume, bool is_contact) const; ToolChangeResult construct_block_tcr(WipeTowerWriter& writer, bool priming, size_t filament_id, - bool is_finish, - float purge_volume) const; + bool is_finish, float purge_volume) const; // x -- x coordinates of wipe tower in mm ( left bottom corner ) @@ -184,9 +183,14 @@ public: // Set the extruder properties. void set_extruder(size_t idx, const PrintConfig& config); + void set_shared_print_bed(const Polygons &bed) { m_shared_print_bed = bed; } + // Orca: has_filament_switcher is not a static PrintConfig member here, so it is pushed in from + // Print via a setter rather than read in the ctor. Device-set only. + void set_has_filament_switcher(bool v) { m_has_filament_switcher = v; } // Appends into internal structure m_plan containing info about the future wipe tower // to be used before building begins. The entries must be added ordered in z. - void plan_toolchange(float z_par, float layer_height_par, unsigned int old_tool, unsigned int new_tool, float wipe_volume = 0.f, float prime_volume = 0.f); + void plan_toolchange(float z_par, float layer_height_par, unsigned int old_tool, unsigned int new_tool, float wipe_volume_ec = 0.f, float wipe_volume_nc = 0.f, float prime_volume = 0.f); + // Iterates through prepared m_plan, generates ToolChangeResults and appends them to "result" void generate(std::vector> &result); @@ -219,9 +223,6 @@ public: } } - void set_wipe_volume(std::vector>& wiping_matrix) { - wipe_volumes = wiping_matrix; - } // Switch to a next layer. void set_layer( @@ -250,7 +251,6 @@ public: // Calculate extrusion flow from desired line width, nozzle diameter, filament diameter and layer_height: m_extrusion_flow = extrusion_flow(layer_height); - // Advance m_layer_info iterator, making sure we got it right while (!m_plan.empty() && m_layer_info->z < print_z - WT_EPSILON && m_layer_info+1 != m_plan.end()) ++m_layer_info; @@ -309,20 +309,9 @@ public: std::vector get_used_filament() const { return m_used_filament_length; } int get_number_of_toolchanges() const { return m_num_tool_changes; } - void set_filament_map(const std::vector &filament_map) { m_filament_map = filament_map; } - // Vortek H2C: filament_id → physical nozzle_id for carousel rotation detection - void set_filament_nozzle_map(const std::vector &nozzle_map) { m_filament_nozzle_map = nozzle_map; } - void set_has_tpu_filament(bool has_tpu) { m_has_tpu_filament = has_tpu; } + bool has_tpu_filament() const { return m_has_tpu_filament; } - - // Orca: has_filament_switcher is not a static PrintConfig member, so it is pushed in from Print - // via a setter rather than read in the ctor. Device-set only. - void set_has_filament_switcher(bool v) { m_has_filament_switcher = v; } - // The region every extruder can reach, used to clamp the PETG pre-extrusion offset to the - // printable bed. - void set_shared_print_bed(const Polygons &bed) { m_shared_print_bed = bed; } - struct FilamentParameters { std::string material = "PLA"; int category; @@ -331,15 +320,15 @@ public: bool is_support = false; int nozzle_temperature = 0; int nozzle_temperature_initial_layer = 0; - int interface_print_temperature = 0; - float loading_speed = 0.f; - float loading_speed_start = 0.f; - float unloading_speed = 0.f; - float unloading_speed_start = 0.f; - float delay = 0.f ; - int cooling_moves = 0; - float cooling_initial_speed = 0.f; - float cooling_final_speed = 0.f; + // BBS: remove useless config + //float loading_speed = 0.f; + //float loading_speed_start = 0.f; + //float unloading_speed = 0.f; + //float unloading_speed_start = 0.f; + //float delay = 0.f ; + //int cooling_moves = 0; + //float cooling_initial_speed = 0.f; + //float cooling_final_speed = 0.f; float ramming_line_width_multiplicator = 1.f; float ramming_step_multiplicator = 1.f; float max_e_speed = std::numeric_limits::max(); @@ -349,41 +338,41 @@ public: float retract_length; float retract_speed; float wipe_dist; - float tower_interface_pre_extrusion_dist = 0.f; - float tower_interface_pre_extrusion_length = 0.f; - // Outward shift of the wipe start for a PETG pre-extrusion on filament-switcher devices; - // set from filament_tower_interface_pre_extrusion_dist. - float petg_pre_extrusion_offset_dist = 0.f; - float tower_ironing_area = 4.f; - float tower_interface_purge_length = 0.f; - // Distance (in mm of filament) that a hotend is allowed to pre-cool before the - // tower is reached; drives the prime-tower heating-during-wipe model (multi-nozzle only). - float filament_cooling_before_tower = 0.f; - // .first = extruder change, .second = nozzle change (carousel) - std::pair max_e_ramming_speed{0.f, 0.f}; - std::pair ramming_travel_time{0.f, 0.f}; - std::pair precool_target_temp{0, 0}; - std::pair,std::vector> precool_t; - std::pair,std::vector> precool_t_first_layer; + std::pair max_e_ramming_speed;//[0]extruder change [1]nozzle change + std::pair ramming_travel_time; // Travel time after ramming + std::pair,std::vector> precool_t;//Pre-cooling time, set to 0 to ensure the ramming speed is controlled solely by ramming volumetric speed. + std::pair, std::vector> precool_t_first_layer; + std::pair precool_target_temp; + float filament_cooling_before_tower = 0.f; + float flat_iron_area; + float filament_tower_interface_print_temp; + float filament_tower_interface_pre_extrusion_dist = 0; + float filament_tower_interface_pre_extrusion_length = 0; + float filament_petg_pre_extrusion_offset_dist = 0; }; - void set_used_filament_ids(const std::vector &used_filament_ids) { m_used_filament_ids = used_filament_ids; }; + void set_used_filament_ids(const std::vector &used_filament_ids) { m_used_filament_ids = used_filament_ids; }; void set_filament_categories(const std::vector & filament_categories) { m_filament_categories = filament_categories;}; - std::vector m_used_filament_ids; + void set_nozzle_group_result(const MultiNozzleUtils::LayeredNozzleGroupResult &multi_nozzle_group_result) { m_multi_nozzle_group_result = &multi_nozzle_group_result; }; + std::vector m_used_filament_ids; std::vector m_filament_categories; + const MultiNozzleUtils::LayeredNozzleGroupResult *m_multi_nozzle_group_result{nullptr}; + + enum class WipeTowerLayerType : unsigned char { Normal, Contact, Solid, Contact_UP};// Contact layer should be solid and reduce feed struct WipeTowerBlock { int block_id{0}; int filament_adhesiveness_category{0}; std::vector layer_depths; - std::vector solid_infill; + //std::vector solid_infill; std::vector finish_depth{0}; // the start pos of finish frame for every layer + std::vector layers_type; // type of the layer, normal, Contact or Solid float depth{0}; float start_depth{0}; float cur_depth{0}; - int last_filament_change_id{-1}; + int last_filament_change_id{-1}; int last_nozzle_change_id{-1}; }; @@ -403,25 +392,33 @@ public: WipeTowerBlock* get_block_by_category(int filament_adhesiveness_category, bool create); void add_depth_to_block(int filament_id, int filament_adhesiveness_category, float depth, bool is_nozzle_change = false); int get_filament_category(int filament_id); - bool is_in_same_extruder(int filament_id_1, int filament_id_2); - // Vortek H2C: format BBS-compatible NOZZLE_CHANGE_START/END tag with OF/NF/ON/NN payload - std::string format_nozzle_change_tag(bool start, int old_filament_id, int new_filament_id) const; void reset_block_status(); int get_wall_filament_for_all_layer(); // for generate new wipe tower void generate_new(std::vector> &result); void plan_tower_new(); - void generate_wipe_tower_blocks(); + void generate_wipe_tower_blocks(bool add_solid_flag); void update_all_layer_depth(float wipe_tower_depth); - + void set_nozzle_last_layer_id(); + void set_first_layer_flow_ratio(const float flow_ratio); + // Orca: default/initial-layer/travel acceleration are object-scope options here (PrintConfig + // members in BBS), so Print pushes the resolved per-variant columns in via this setter. + void set_accelerations(const std::vector &normal, const std::vector &first_layer_normal, + const std::vector &travel, const std::vector &first_layer_travel); + void calc_block_infill_gap(); ToolChangeResult tool_change_new(size_t new_tool, bool solid_change = false, bool solid_nozzlechange=false); - NozzleChangeResult nozzle_change_new(int old_filament_id, int new_filament_id, bool solid_change = false); + NozzleChangeResult ramming(int old_filament_id, int new_filament_id, bool solid_change = false, bool extruder_change = true); // extruder_chang means nozzle_change ToolChangeResult finish_layer_new(bool extrude_perimeter = true, bool extrude_fill = true, bool extrude_fill_wall = true); ToolChangeResult finish_block(const WipeTowerBlock &block, int filament_id, bool extrude_fill = true); - ToolChangeResult finish_block_solid(const WipeTowerBlock &block, int filament_id, bool extrude_fill = true ,bool interface_solid =false); + ToolChangeResult finish_block_solid(const WipeTowerBlock &block, int filament_id, bool extrude_fill = true, WipeTowerLayerType layer_type = WipeTowerLayerType::Normal); void toolchange_wipe_new(WipeTowerWriter &writer, const box_coordinates &cleaning_box, float wipe_length,bool solid_toolchange=false); Vec2f get_rib_offset() const { return m_rib_offset; } + bool is_need_ramming(int filament_id_1, int filament_id_2, int layer_id) const; + bool is_same_extruder(int filament_id_1, int filament_id_2, int layer_id) const; + bool is_same_nozzle(int filament_id_1, int filament_id_2, int layer_id) const; + int get_nozzle_id(int filament_id, int layer_id) const; + int get_extruder_id(int filament_id, int layer_id) const; private: enum wipe_shape // A fill-in direction @@ -441,7 +438,6 @@ private: bool m_enable_wrapping_detection = false; bool m_enable_timelapse_print = false; bool m_semm = true; // Are we using a single extruder multimaterial printer? - bool m_purge_in_prime_tower = false; // Do we purge in the prime tower? Vec2f m_wipe_tower_pos; // Left front corner of the wipe tower in mm. float m_wipe_tower_width; // Width of the wipe tower. float m_wipe_tower_depth = 0.f; // Depth of the wipe tower @@ -459,12 +455,11 @@ private: float m_travel_speed = 0.f; float m_first_layer_speed = 0.f; size_t m_first_layer_idx = size_t(-1); - - std::vector m_filaments_change_length; + Vec2f m_origin; + std::vector m_last_layer_id; + std::pair,std::vector> m_filaments_change_length;//[0]extruder change [1]nozzle change size_t m_cur_layer_id; NozzleChangeResult m_nozzle_change_result; - std::vector m_filament_map; - std::vector m_filament_nozzle_map; // Vortek H2C: filament_id → physical nozzle_id bool m_has_tpu_filament{false}; bool m_is_multi_extruder{false}; bool m_use_gap_wall{false}; @@ -475,33 +470,32 @@ private: bool m_used_fillet{false}; Vec2f m_rib_offset{Vec2f(0.f, 0.f)}; bool m_tower_framework{false}; - + bool m_need_reverse_travel{false}; + bool m_enable_tower_interface_features{false}; // G-code generator parameters. - float m_cooling_tube_retraction = 0.f; - float m_cooling_tube_length = 0.f; - float m_parking_pos_retraction = 0.f; - float m_extra_loading_move = 0.f; + // BBS: remove useless config + //float m_cooling_tube_retraction = 0.f; + //float m_cooling_tube_length = 0.f; + //float m_parking_pos_retraction = 0.f; + //float m_extra_loading_move = 0.f; float m_bridging = 0.f; bool m_no_sparse_layers = false; - bool m_set_extruder_trimpot = false; + // BBS: remove useless config + //bool m_set_extruder_trimpot = false; bool m_adhesion = true; GCodeFlavor m_gcode_flavor; - - // Multi-nozzle prime-tower heating during wipe. m_is_multiple_nozzle gates the whole - // feature; it is false for every current (single-nozzle) printer (extruder_max_nozzle_count - // defaults to 1), so the pre-heat/pre-cool path is inert and wipe-tower g-code is unchanged. - bool m_is_multiple_nozzle = false; - std::vector m_hotend_heating_rate; // config.hotend_heating_rate (deg/s per extruder) - std::vector m_physical_extruder_map; // logical extruder -> physical tool number (M104 T param) - - // Per-extruder printable-height clamp. m_printable_height = config.extruder_printable_height - // (per-extruder Z limit; empty for single-extruder printers, [320,325] for H2D). m_last_layer_id - // records, per extruder, the last wipe-tower layer that uses it. is_valid_last_layer() is gated on - // m_is_multi_extruder so single-extruder wipe-tower g-code is unchanged; the clamp only bites a - // multi-extruder wipe tower whose final per-extruder layer exceeds that extruder's printable - // height (near the Z limit). - std::vector m_printable_height; - std::vector m_last_layer_id; + bool m_is_multiple_nozzle = false; + std::vector m_normal_accels; + std::vector m_first_layer_normal_accels; + std::vector m_travel_accels; + std::vector m_first_layer_travel_accels; + unsigned int m_max_accels; + bool m_accel_to_decel_enable; + float m_accel_to_decel_factor; + bool m_enable_arc_fitting = true; + std::vector m_hotend_heating_rate; + std::vector m_hotend_cooling_rate; + Polygons m_shared_print_bed; // Bed properties enum { @@ -512,10 +506,11 @@ private: float m_bed_width; // width of the bed bounding box Vec2f m_bed_bottom_left; // bottom-left corner coordinates (for rectangular beds) + float m_first_layer_flow_ratio; float m_perimeter_width = 0.4f * Width_To_Nozzle_Ratio; // Width of an extrusion line, also a perimeter spacing for 100% infill. float m_nozzle_change_perimeter_width = 0.4f * Width_To_Nozzle_Ratio; float m_extrusion_flow = 0.038f; //0.029f;// Extrusion flow is derived from m_perimeter_width, layer height and filament diameter. - + std::unordered_map> m_block_infill_gap_width; // categories to infill_gap: toolchange gap, nozzlechange gap // Extruder specific parameters. std::vector m_filpar; @@ -528,50 +523,52 @@ private: // A fill-in direction (positive Y, negative Y) alternates with each layer. wipe_shape m_current_shape = SHAPE_NORMAL; size_t m_current_tool = 0; - // Orca: support mmu wipe tower - std::vector> wipe_volumes; + // BBS + //const std::vector> wipe_volumes; float m_depth_traversed = 0.f; // Current y position at the wipe tower. bool m_current_layer_finished = false; bool m_left_to_right = true; float m_extra_spacing = 1.f; float m_tpu_fixed_spacing = 2; - std::vector m_wall_skip_points; + float m_max_speed = 5400.f; // the maximum printing speed on the prime tower. + std::vector> m_wall_skip_points; std::map m_outer_wall; + std::vector m_printable_height; bool is_first_layer() const { return size_t(m_layer_info - m_plan.begin()) == m_first_layer_idx; } + bool is_valid_last_layer(int tool, int layer_id, double layer_z) const; bool m_flat_ironing=false; - bool m_enable_tower_interface_features=false; - bool m_enable_tower_interface_cooldown_during_tower=false; - // Filament-switcher device flag + shared printable bed for the PETG pre-extrusion offset. - // m_has_filament_switcher is false for the whole shipping fleet (no profile sets the key), so - // the PETG branch in get_next_pos never runs -> no change fleet-wide. - bool m_has_filament_switcher=false; - Polygons m_shared_print_bed; - bool m_prev_layer_had_interface=false; - bool m_current_layer_has_interface=false; + bool m_contact_ironing = false; + bool m_has_filament_switcher = false; + float m_contact_speed = 20 * 60.f; + std::vector m_physical_extruder_map; // Calculates length of extrusion line to extrude given volume float volume_to_length(float volume, float line_width, float layer_height) const { return std::max(0.f, volume / (layer_height * (line_width - layer_height * (1.f - float(M_PI) / 4.f)))); } - + // Calculates volume of extrusion line + float length_to_volume(float length,float line_width, float layer_height) const + { + return std::max(0.f, length * (layer_height * (line_width - layer_height * (1.f - float(M_PI) / 4.f)))); + } // Calculates depth for all layers and propagates them downwards void plan_tower(); // Goes through m_plan and recalculates depths and width of the WT to make it exactly square - experimental void make_wipe_tower_square(); - Vec2f get_next_pos(const WipeTower::box_coordinates &cleaning_box, float wipe_length, bool interface_layer, size_t interface_tool); + Vec2f get_next_pos(const WipeTower::box_coordinates &cleaning_box, float wipe_length, bool solid_toolchange); // Goes through m_plan, calculates border and finish_layer extrusions and subtracts them from last wipe void save_on_last_wipe(); bool is_tpu_filament(int filament_id) const; bool is_petg_filament(int filament_id) const; - bool is_need_reverse_travel(int filament_id, bool extruder_change) const; - + bool is_need_reverse_travel(int filament, bool extruder_change) const; // BBS box_coordinates align_perimeter(const box_coordinates& perimeter_box); + void set_for_wipe_tower_writer(WipeTowerWriter &writer); // to store information about tool changes for a given layer struct WipeTowerInfo{ @@ -584,6 +581,7 @@ private: float wipe_volume; float wipe_length; float nozzle_change_depth{0}; + float nozzle_change_length{0}; // BBS float purge_volume; ToolChange(size_t old, size_t newtool, float depth=0.f, float ramming_depth=0.f, float fwl=0.f, float wv=0.f, float wl = 0, float pv = 0) @@ -613,7 +611,7 @@ private: // ot -1 if there is no such toolchange. int first_toolchange_to_nonsoluble_nonsupport( const std::vector& tool_changes) const; - + WipeTowerInfo::ToolChange set_toolchange(int old_tool, int new_tool, float layer_height, float wipe_volume, float purge_volume,int layer_id); void toolchange_Unload( WipeTowerWriter &writer, const box_coordinates &cleaning_box, @@ -633,13 +631,10 @@ private: WipeTowerWriter &writer, const box_coordinates &cleaning_box, float wipe_volume); - void get_wall_skip_points(const WipeTowerInfo &layer); - - // Per-extruder printable-height clamp (see m_printable_height). is_valid_last_layer returns - // false only for a multi-extruder wipe tower's final per-extruder layer that exceeds that - // extruder's printable height; returns true (no clamp) in every other case. - bool is_valid_last_layer(int tool, int layer_id, double layer_z) const; - void set_nozzle_last_layer_id(); + void get_wall_skip_points(const WipeTowerInfo &layer,int layer_id); + void get_all_wall_skip_points(); + ToolChangeResult merge_tcr(ToolChangeResult &first, ToolChangeResult &second); + float get_block_gap_width(int tool, bool is_nozzlechangle = false); }; diff --git a/src/libslic3r/Polyline.hpp b/src/libslic3r/Polyline.hpp index 88671391db..f52c3c9bbd 100644 --- a/src/libslic3r/Polyline.hpp +++ b/src/libslic3r/Polyline.hpp @@ -129,13 +129,13 @@ public: std::vector fitting_result; //BBS: simplify points by arc fitting void simplify_by_fitting_arc(double tolerance); - //BBS: + void reset_to_linear_move(); + //BBS: Polylines equally_spaced_lines(double distance) const; private: void append_fitting_result_after_append_points(); void append_fitting_result_after_append_polyline(const Polyline& src); - void reset_to_linear_move(); bool split_fitting_result_before_index(const size_t index, Point &new_endpoint, std::vector& data) const; bool split_fitting_result_after_index(const size_t index, Point &new_startpoint, std::vector& data) const; }; diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index 334bda84e9..3f06cd5b37 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -4017,10 +4017,33 @@ void Print::_make_wipe_tower() // in BBL machine, wipe tower is only use to prime extruder. So just use a global wipe volume. WipeTower wipe_tower(m_config, m_plate_index, m_origin, m_wipe_tower_data.tool_ordering.first_extruder(), m_wipe_tower_data.tool_ordering.empty() ? 0.f : m_wipe_tower_data.tool_ordering.back().print_z, m_wipe_tower_data.tool_ordering.all_extruders()); + // Orca: the tower's first-layer flow follows the user's first-layer flow ratio (BBS reads + // its initial_layer_flow_ratio here — STUDIO-14254; first_layer_flow_ratio is Orca's analog, + // default 1.0 in both). Honor the set_other_flow_ratios gate that governs the option + // everywhere else. + wipe_tower.set_first_layer_flow_ratio(m_default_object_config.set_other_flow_ratios + ? float(m_default_region_config.first_layer_flow_ratio) + : 1.f); wipe_tower.set_has_tpu_filament(this->has_tpu_filament()); - wipe_tower.set_filament_map(this->get_filament_maps()); - // Vortek H2C: pass nozzle-level map for carousel rotation detection in tool_change_new() - wipe_tower.set_filament_nozzle_map(this->get_filament_nozzle_maps()); + // Per-layer filament->nozzle grouping. sort_and_build_data() above publishes it on the Print + // for by-layer prints; by-object prints publish only later (psSkirtBrim), so fall back to the + // ToolOrdering's own copy there. set_extruder() below dereferences it, so it must be set first. + auto print_group_result = get_layered_nozzle_group_result(); + const MultiNozzleUtils::LayeredNozzleGroupResult &nozzle_group_result = + print_group_result ? *print_group_result : m_wipe_tower_data.tool_ordering.get_layered_nozzle_group_result(); + wipe_tower.set_nozzle_group_result(nozzle_group_result); + { + // Orca: acceleration options are object-scope (PrintConfig members in BBS), so resolve + // the per-variant columns here; initial_layer_travel_acceleration is FloatOrPercent + // over travel_acceleration and needs the full config to resolve. + std::vector first_layer_travel_accels; + for (size_t i = 0; i < m_config.initial_layer_travel_acceleration.values.size(); ++i) + first_layer_travel_accels.emplace_back(m_full_print_config.get_abs_value_at("initial_layer_travel_acceleration", i)); + wipe_tower.set_accelerations(m_default_object_config.default_acceleration.values, + m_default_object_config.initial_layer_acceleration.values, + m_default_object_config.travel_acceleration.values, + first_layer_travel_accels); + } // Feed the has_filament_switcher device flag (develop-only dynamic key, read defensively from // the full config — no shipping profile sets it) and the shared printable bed used by the PETG // pre-extrusion offset clamp. Both are inert unless has_filament_switcher is set. @@ -4056,27 +4079,19 @@ void Print::_make_wipe_tower() multi_extruder_flush.emplace_back(wipe_volumes); } - // Use NozzleStatusRecorder for per-carousel-slot tracking (BBS pattern). - // The original Orca code tracked per-extruder (2 slots), which collapsed all - // carousel filaments into one slot and caused massive redundant AMS flushing. - auto group_result = get_layered_nozzle_group_result(); + // Per-carousel-slot purge tracking via NozzleStatusRecorder (BBS pattern); the layered + // group result set on the tower above resolves each filament to its nozzle slot per layer. MultiNozzleUtils::NozzleStatusRecorder nozzle_recorder; - // Fallback (group_result == null) per-physical-nozzle tracking, matching the original - // pre-port behavior: remembers the last filament loaded in each physical nozzle slot. - std::vector nozzle_cur_filament_ids(nozzle_nums, (unsigned int) -1); std::vectorfilament_maps = get_filament_maps(); int layer_idx = -1; unsigned int current_filament_id = m_wipe_tower_data.tool_ordering.first_extruder(); // Initialize NozzleStatusRecorder with the first filament's carousel slot - if (group_result) { - auto nozzle = group_result->get_nozzle_for_filament(current_filament_id, layer_idx); + { + auto nozzle = nozzle_group_result.get_nozzle_for_filament(current_filament_id, layer_idx); if (nozzle) nozzle_recorder.set_nozzle_status(nozzle->group_id, current_filament_id, nozzle->extruder_id); - } else { - size_t cur_nozzle_id = filament_maps[current_filament_id] - 1; - nozzle_cur_filament_ids[cur_nozzle_id] = current_filament_id; } for (auto& layer_tools : m_wipe_tower_data.tool_ordering.layer_tools()) { // for all layers @@ -4095,8 +4110,8 @@ void Print::_make_wipe_tower() float volume_to_purge = 0; // Per-carousel-slot purge tracking via NozzleStatusRecorder - if (group_result) { - auto nozzle_info = group_result->get_nozzle_for_filament(filament_id, layer_idx); + { + auto nozzle_info = nozzle_group_result.get_nozzle_for_filament(filament_id, layer_idx); if (nozzle_info) { int extruder_id = nozzle_info->extruder_id; int nozzle_id = nozzle_info->group_id; @@ -4115,22 +4130,6 @@ void Print::_make_wipe_tower() } nozzle_recorder.set_nozzle_status(nozzle_id, filament_id, extruder_id); } - } else { - // Fallback: original Orca per-physical-nozzle path (non-carousel printers). - // Flush source is the last filament that occupied THIS nozzle, guarded so the - // first use of a nozzle incurs no flush. - int nozzle_id = filament_maps[filament_id] - 1; - unsigned int pre_filament_id = nozzle_cur_filament_ids[nozzle_id]; - if (pre_filament_id != (unsigned int) -1 && pre_filament_id != filament_id) { - volume_to_purge = multi_extruder_flush[nozzle_id][pre_filament_id][filament_id]; - float flush_multiplier = (m_config.prime_volume_mode == PrimeVolumeMode::pvmFast) - ? m_config.flush_multiplier_fast.get_at(nozzle_id) - : m_config.flush_multiplier.get_at(nozzle_id); - volume_to_purge *= flush_multiplier; - volume_to_purge = layer_tools.wiping_extrusions().mark_wiping_extrusions( - *this, current_filament_id, filament_id, volume_to_purge); - } - nozzle_cur_filament_ids[nozzle_id] = filament_id; } //During the filament change, the extruder will extrude an extra length of grab_length for the corresponding detection, so the purge can reduce this length. @@ -4138,29 +4137,21 @@ void Print::_make_wipe_tower() float grab_purge_volume = m_config.grab_length.get_at(grab_extruder_id) * 2.4; //(diameter/2)^2*PI=2.4 volume_to_purge = std::max(0.f, volume_to_purge - grab_purge_volume); - // Select prime volume per-filament: nozzle change (carousel rotation) uses - // filament_prime_volume_nc, filament change (same nozzle slot) uses filament_prime_volume. + // Prime volume per-filament: the tower now picks extruder-change vs nozzle-change + // (carousel) internally per plan layer, so pass both candidates (BBS pattern). float wipe_volume_ec = filament_id < m_config.filament_prime_volume.values.size() ? m_config.filament_prime_volume.values[filament_id] : (float) m_config.prime_volume; float wipe_volume_nc = filament_id < m_config.filament_prime_volume_nc.values.size() ? m_config.filament_prime_volume_nc.values[filament_id] : (float) m_config.prime_volume; - - float prime_volume = wipe_volume_ec; - if (group_result) { - bool is_nozzle_change = group_result->are_filaments_same_extruder(current_filament_id, filament_id, layer_idx) && - !group_result->are_filaments_same_nozzle(current_filament_id, filament_id, layer_idx); - if (is_nozzle_change) { - prime_volume = wipe_volume_nc; - } - } if (m_config.prime_volume_mode == PrimeVolumeMode::pvmSaving) { - prime_volume = 15.f; + wipe_volume_ec = 15.f; + wipe_volume_nc = 15.f; } wipe_tower.plan_toolchange((float)layer_tools.print_z, (float)layer_tools.wipe_tower_layer_height, current_filament_id, filament_id, - prime_volume, volume_to_purge); + wipe_volume_ec, wipe_volume_nc, volume_to_purge); current_filament_id = filament_id; } layer_tools.wiping_extrusions().ensure_perimeters_infills_order(*this); From bc016af1c959a61e470d740c5f8d7df8c28aef23 Mon Sep 17 00:00:00 2001 From: SoftFever Date: Mon, 27 Jul 2026 00:25:59 +0800 Subject: [PATCH 02/16] Fix post-slice self-invalidation on custom multi-extruder printers --- src/libslic3r/Config.hpp | 21 ++- src/libslic3r/Print.cpp | 6 +- src/libslic3r/PrintApply.cpp | 16 ++- src/libslic3r/PrintConfig.cpp | 40 ++++++ .../test_config_variant_expansion.cpp | 121 +++++++++++++++++- .../test_toolordering_nozzle_group.cpp | 46 +++++++ 6 files changed, 240 insertions(+), 10 deletions(-) diff --git a/src/libslic3r/Config.hpp b/src/libslic3r/Config.hpp index da36321b6f..6f4117d249 100644 --- a/src/libslic3r/Config.hpp +++ b/src/libslic3r/Config.hpp @@ -2,6 +2,7 @@ #define slic3r_Config_hpp_ #include +#include #include #include #include @@ -780,10 +781,14 @@ public: this->values[i] = rhs_vec->values[i]; modified = true; } else { - if ((i < default_index.size()) && (default_index[i] < default_value.size())) + // Orca: a negative slot (failed variant lookup) must not silently collapse the + // whole array to the first slot's value — the int-vs-size_t comparison used to + // promote -1 past the bounds check. Keep the slot's own value (get_at-style + // clamp) when no valid index is available. + if ((i < default_index.size()) && (default_index[i] >= 0) && (size_t(default_index[i]) < default_value.size())) this->values[i] = default_value[default_index[i]]; else - this->values[i] = default_value[0]; + this->values[i] = default_value[std::min(i, default_value.size() - 1)]; } } return modified; @@ -2106,6 +2111,11 @@ public: throw ConfigurationError("ConfigOptionEnumGeneric: Assigning an incompatible type"); // rhs could be of the following type: ConfigOptionEnumGeneric or ConfigOptionEnum this->value = rhs->getInt(); + // Orca: options embedded in a StaticPrintConfig are constructed without a keys_map; + // adopt the source's so a later serialize() can emit names. + if (this->keys_map == nullptr) + if (auto rhs_generic = dynamic_cast(rhs)) + this->keys_map = rhs_generic->keys_map; } std::string serialize() const override @@ -2162,7 +2172,12 @@ public: if (rhs->type() != this->type()) throw ConfigurationError("ConfigOptionEnumGeneric: Assigning an incompatible type"); // rhs could be of the following type: ConfigOptionEnumsGeneric - this->values = dynamic_cast(rhs)->values; + auto rhs_enums = dynamic_cast(rhs); + this->values = rhs_enums->values; + // Orca: options embedded in a StaticPrintConfig are constructed without a keys_map; + // adopt the source's so a later serialize() emits names instead of empty tokens. + if (this->keys_map == nullptr) + this->keys_map = rhs_enums->keys_map; } std::string serialize() const override diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index 3f06cd5b37..ecc2f39c88 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -3409,7 +3409,11 @@ void Print::update_filament_maps_to_config(std::vector f_maps, std::vector< } else if ((extruder_volume_type_count > extruder_count) && (m_config.filament_volume_map.values.size() > index)) nozzle_volume_type = (NozzleVolumeType)(m_config.filament_volume_map.values[index]); - m_config.filament_map_2.values[index] = m_ori_full_print_config.get_index_for_extruder(f_maps[index], "print_extruder_id", extruder_type, nozzle_volume_type, "print_extruder_variant"); + // Orca: when the process variant columns cannot be matched (degenerate + // print_extruder_id), key the override by plain extruder index like the seeding + // above instead of poisoning the map with -1. + int slot_index = m_ori_full_print_config.get_index_for_extruder(f_maps[index], "print_extruder_id", extruder_type, nozzle_volume_type, "print_extruder_variant"); + m_config.filament_map_2.values[index] = slot_index >= 0 ? slot_index : f_maps[index] - 1; } m_full_print_config = m_ori_full_print_config; diff --git a/src/libslic3r/PrintApply.cpp b/src/libslic3r/PrintApply.cpp index 0956cfe154..e2e9bc737d 100644 --- a/src/libslic3r/PrintApply.cpp +++ b/src/libslic3r/PrintApply.cpp @@ -1355,7 +1355,11 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_ if ((extruder_volume_type_count > extruder_count) && opt_filament_volume_maps && opt_filament_volume_maps->values.size() == filament_maps.size()) nozzle_volume_type = (NozzleVolumeType)(opt_filament_volume_maps->values[index]); - m_config.filament_map_2.values[index] = new_full_config.get_index_for_extruder(filament_maps[index], "print_extruder_id", extruder_type, nozzle_volume_type, "print_extruder_variant"); + // Orca: when the process variant columns cannot be matched (degenerate + // print_extruder_id), key the override by plain extruder index like the seeding + // above instead of poisoning the map with -1. + int slot_index = new_full_config.get_index_for_extruder(filament_maps[index], "print_extruder_id", extruder_type, nozzle_volume_type, "print_extruder_variant"); + m_config.filament_map_2.values[index] = slot_index >= 0 ? slot_index : filament_maps[index] - 1; } // Do not use the ApplyStatus as we will use the max function when updating apply_status. @@ -1411,6 +1415,16 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_ num_extruders_changed = true; } } + else if (! print_diff.empty()) { + // Orca: m_config can diverge from an unchanged full config (e.g. the in-slice retract + // override recompute writing different values than the apply-time computation). The + // invalidation above already fired for print_diff, so repair m_config here as well; + // otherwise the divergence is never corrected and every subsequent apply of the same + // config invalidates the result again, forever. + m_placeholder_parser.apply_config(filament_overrides); + m_config.apply_only(new_full_config, print_diff, true); + m_config.apply(filament_overrides); + } ModelObjectStatusDB model_object_status_db; diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index 8129ff741e..f0d82a9264 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -10495,6 +10495,44 @@ int DynamicPrintConfig::get_extruder_nozzle_volume_count(int extruder_count, std return count; } +// Orca: BBL system profiles ship full-width print_extruder_id/print_extruder_variant columns, but +// custom multi-extruder printers only ever get the machine-scope columns synthesized for them (see +// extend_extruder_variant); the process scope keeps the length-1 defaults, both in presets and in +// 3mf project configs. Expanding with that degenerate map makes every per-extruder lookup fail, and +// because both keys are themselves in print_options_with_variant, the expansion then latches a +// full-width-but-wrong [1,1,...] map that also defeats the generated_extruder_id fallback in +// get_index_for_extruder. Synthesize the process columns from the printer's extruder_variant_list +// (same token walk as extend_extruder_variant) before expanding. +static void ensure_process_variant_columns(DynamicPrintConfig &config, const DynamicPrintConfig &printer_config) +{ + auto id_opt = dynamic_cast(config.option("print_extruder_id")); + auto variant_opt = dynamic_cast(config.option("print_extruder_variant")); + auto list_opt = dynamic_cast(printer_config.option("extruder_variant_list")); + if (!id_opt || !variant_opt || !list_opt) + return; + if (id_opt->values.size() != 1 || variant_opt->values.size() != 1) + return; + + std::vector ids; + std::vector variants; + for (int i = 0; i < int(list_opt->values.size()); ++i) { + std::vector tokens; + boost::split(tokens, list_opt->get_at(i), boost::is_any_of(","), boost::token_compress_on); + for (std::string &token : tokens) { + boost::trim(token); + if (token.empty()) + continue; + ids.push_back(i + 1); + variants.push_back(token); + } + } + // A single column is the legitimate single-extruder layout, not a degenerate one. + if (ids.size() <= 1) + return; + id_opt->values = std::move(ids); + variant_opt->values = std::move(variants); +} + std::vector DynamicPrintConfig::update_values_to_printer_extruders(DynamicPrintConfig& printer_config, int extruder_count, int extruder_nozzle_volume_count, std::vector>& nv_types, std::set& key_set, std::string id_name, std::string variant_name, unsigned int stride, unsigned int extruder_id, NozzleVolumeType filament_nvt) { @@ -10536,6 +10574,8 @@ std::vector DynamicPrintConfig::update_values_to_printer_extruders(DynamicP variant_count = 1; } else { + if (id_name == "print_extruder_id") + ensure_process_variant_columns(*this, printer_config); // Orca: emit the slots first, then size variant_count from what was actually // emitted. extruder_nozzle_volume_count only equals the emitted total when every // extruder carries per-type stats; an extruder with an empty stats entry combined diff --git a/tests/libslic3r/test_config_variant_expansion.cpp b/tests/libslic3r/test_config_variant_expansion.cpp index 59f311eae5..5cff6ecee5 100644 --- a/tests/libslic3r/test_config_variant_expansion.cpp +++ b/tests/libslic3r/test_config_variant_expansion.cpp @@ -43,18 +43,33 @@ TEST_CASE("apply_override fills nil entries from the 0-based default index", "[C REQUIRE(resolved.values == std::vector({30., 42.})); } - SECTION("an index past the machine slots falls back to the first slot") { + SECTION("an index past the machine slots keeps the slot's own value") { std::vector slot_index{5, 0}; ConfigOptionFloats resolved(machine); REQUIRE(resolved.apply_override(&filament, slot_index)); REQUIRE(resolved.values == std::vector({10., 42.})); } - SECTION("a negative index (unresolved slot) falls back to the first slot") { - std::vector slot_index{-1, 0}; + SECTION("a negative index (unresolved slot) keeps the slot's own value") { + ConfigOptionFloatsNullable all_nil; + all_nil.values = {ConfigOptionFloatsNullable::nil_value(), ConfigOptionFloatsNullable::nil_value(), + ConfigOptionFloatsNullable::nil_value()}; + std::vector slot_index{2, -1, 0}; ConfigOptionFloats resolved(machine); - REQUIRE(resolved.apply_override(&filament, slot_index)); - REQUIRE(resolved.values == std::vector({10., 42.})); + REQUIRE(!resolved.apply_override(&all_nil, slot_index)); + REQUIRE(resolved.values == std::vector({30., 20., 10.})); + } + + SECTION("all-nil overrides keyed by unresolved slots leave the machine values intact") { + // The failed-lookup map a degenerate print_extruder_id used to produce; the negative + // slots must not collapse the machine array to its first value. + ConfigOptionFloats per_extruder({100., 70., 70., 70., 100.}); + ConfigOptionFloatsNullable all_nil; + all_nil.values.assign(5, ConfigOptionFloatsNullable::nil_value()); + std::vector slot_index{0, -1, -1, -1, 0}; + ConfigOptionFloats resolved(per_extruder); + REQUIRE(!resolved.apply_override(&all_nil, slot_index)); + REQUIRE(resolved.values == std::vector({100., 70., 70., 70., 100.})); } } @@ -238,6 +253,102 @@ TEST_CASE("update_values_to_printer_extruders expands one slot per (extruder x v } } +TEST_CASE("update_values_to_printer_extruders synthesizes degenerate process variant columns", "[Config]") +{ + // Non-BBL process presets and 3mf project configs keep the length-1 defaults for + // print_extruder_id/print_extruder_variant; only BBL system presets ship full-width columns. + auto add_degenerate_print_columns = [](DynamicPrintConfig &config) { + config.option("print_extruder_id", true)->values = {1}; + config.option("print_extruder_variant", true)->values = {"Direct Drive Standard"}; + config.option("outer_wall_speed", true)->values = {30.}; + }; + + SECTION("a single-column pair on a multi-extruder machine expands to one column per extruder") { + DynamicPrintConfig config; + config.option("extruder_type", true)->values = {etDirectDrive, etDirectDrive}; + config.option("nozzle_volume_type", true)->values = {nvtStandard, nvtStandard}; + config.option("extruder_variant_list", true)->values = {"Direct Drive Standard", "Direct Drive Standard"}; + add_degenerate_print_columns(config); + + std::vector> nozzle_volume_types; + int extruder_count = 2; + int count = config.get_extruder_nozzle_volume_count(extruder_count, nozzle_volume_types); + + std::vector variant_index = config.update_values_to_printer_extruders(config, extruder_count, count, nozzle_volume_types, + print_options_with_variant, "print_extruder_id", "print_extruder_variant"); + + REQUIRE(variant_index == std::vector({0, 1})); + REQUIRE(config.option("print_extruder_id")->values == std::vector({1, 2})); + REQUIRE(config.option("print_extruder_variant")->values == + std::vector({"Direct Drive Standard", "Direct Drive Standard"})); + // width-1 data arrays replicate their only column into every slot + REQUIRE(config.option("outer_wall_speed")->values == std::vector({30., 30.})); + } + + SECTION("a multi-variant list synthesizes one column per (extruder x variant)") { + DynamicPrintConfig config = make_hybrid_printer_config(); + add_degenerate_print_columns(config); + + std::vector> nozzle_volume_types; + int extruder_count = 2; + int count = config.get_extruder_nozzle_volume_count(extruder_count, nozzle_volume_types); + REQUIRE(count == 3); + + std::vector variant_index = config.update_values_to_printer_extruders(config, extruder_count, count, nozzle_volume_types, + print_options_with_variant, "print_extruder_id", "print_extruder_variant"); + + // same slot resolution as the explicit BBL-style 4-column layout + REQUIRE(variant_index == std::vector({0, 2, 3})); + REQUIRE(config.option("print_extruder_id")->values == std::vector({1, 2, 2})); + REQUIRE(config.option("print_extruder_variant")->values == + std::vector({"Direct Drive Standard", "Direct Drive Standard", "Direct Drive High Flow"})); + REQUIRE(config.option("outer_wall_speed")->values == std::vector({30., 30., 30.})); + } + + SECTION("a single-extruder single-column layout is not treated as degenerate") { + DynamicPrintConfig config; + config.option("extruder_type", true)->values = {etDirectDrive}; + config.option("nozzle_volume_type", true)->values = {nvtStandard}; + config.option("extruder_variant_list", true)->values = {"Direct Drive Standard"}; + add_degenerate_print_columns(config); + + std::vector> nozzle_volume_types; + int extruder_count = 1; + int count = config.get_extruder_nozzle_volume_count(extruder_count, nozzle_volume_types); + + config.update_values_to_printer_extruders(config, extruder_count, count, nozzle_volume_types, + print_options_with_variant, "print_extruder_id", "print_extruder_variant"); + + REQUIRE(config.option("print_extruder_id")->values == std::vector({1})); + REQUIRE(config.option("outer_wall_speed")->values == std::vector({30.})); + } + + SECTION("a second expansion leaves the synthesized layout unchanged") { + DynamicPrintConfig config; + config.option("extruder_type", true)->values = {etDirectDrive, etDirectDrive}; + config.option("nozzle_volume_type", true)->values = {nvtStandard, nvtStandard}; + config.option("extruder_variant_list", true)->values = {"Direct Drive Standard", "Direct Drive Standard"}; + add_degenerate_print_columns(config); + + std::vector> nozzle_volume_types; + int extruder_count = 2; + int count = config.get_extruder_nozzle_volume_count(extruder_count, nozzle_volume_types); + + config.update_values_to_printer_extruders(config, extruder_count, count, nozzle_volume_types, + print_options_with_variant, "print_extruder_id", "print_extruder_variant"); + DynamicPrintConfig once = config; + config.update_values_to_printer_extruders(config, extruder_count, count, nozzle_volume_types, + print_options_with_variant, "print_extruder_id", "print_extruder_variant"); + + REQUIRE(config.option("print_extruder_id")->values == + once.option("print_extruder_id")->values); + REQUIRE(config.option("print_extruder_variant")->values == + once.option("print_extruder_variant")->values); + REQUIRE(config.option("outer_wall_speed")->values == + once.option("outer_wall_speed")->values); + } +} + TEST_CASE("update_values_to_printer_extruders_for_multiple_filaments resolves per-filament slots", "[Config]") { auto make_filament_arrays = [](DynamicPrintConfig &config) { diff --git a/tests/libslic3r/test_toolordering_nozzle_group.cpp b/tests/libslic3r/test_toolordering_nozzle_group.cpp index ae6aac4dbe..5eee164eb0 100644 --- a/tests/libslic3r/test_toolordering_nozzle_group.cpp +++ b/tests/libslic3r/test_toolordering_nozzle_group.cpp @@ -500,6 +500,52 @@ TEST_CASE("Re-applying an unchanged config after slicing keeps the result valid" REQUIRE(print.is_step_done(psSlicingFinished)); } +TEST_CASE("A degenerate process variant map on a custom multi-extruder printer slices to a stable result", "[Print][Regression]") +{ + // Non-BBL multi-extruder printers get machine-scope variant columns synthesized on preset + // load (extend_extruder_variant), but nothing ships process-scope print_extruder_id / + // print_extruder_variant: presets and 3mf project configs carry the length-1 defaults. The + // apply-time expansion must synthesize the process columns from extruder_variant_list; + // otherwise the failed per-extruder lookups collapse the per-extruder retract overrides + // during slicing and the post-slice re-apply invalidates every fresh result, forever. + DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); + config.option("nozzle_diameter", true)->values = {0.4, 0.4, 0.4, 0.4, 0.4}; + config.set_num_extruders(5); + // per-extruder machine values that a first-slot collapse would destroy + config.option("retract_before_wipe", true)->values = {100., 70., 70., 70., 100.}; + config.option("z_hop_types", true)->values = {zhtSlope, zhtNormal, zhtNormal, zhtNormal, zhtSlope}; + // filament presets carry the nullable override twins (all-nil = "no override"); they are what + // routes the machine values through apply_override in the in-slice override recompute + config.option("filament_retract_before_wipe", true)->values = + std::vector(5, ConfigOptionPercentsNullable::nil_value()); + config.option("filament_z_hop_types", true)->values = + std::vector(5, ConfigOptionEnumsGenericNullable::nil_value()); + config.option("filament_diameter", true)->values = std::vector(5, 1.75); + config.option("filament_colour", true)->values = {"#FF0000", "#00FF00", "#0000FF", "#FFFF00", "#00FFFF"}; + config.option("filament_map", true)->values = {1, 2, 3, 4, 1}; + + Model model; + model.add_object("cube", "", make_cube(20, 20, 20))->add_instance()->set_offset(Vec3d(100., 100., 0.)); + + Print print; + print.apply(model, config); + print.process(); + REQUIRE(print.is_step_done(psSlicingFinished)); + + // BackgroundSlicingProcess reads the engine-computed maps back into the plate config after + // slicing; the next apply overlays that written-back state. + config.option("filament_map", true)->values = print.get_filament_maps(); + config.option("filament_volume_map", true)->values = print.get_filament_volume_maps(); + config.option("filament_nozzle_map", true)->values = print.get_filament_nozzle_maps(); + + auto status = print.apply(model, config); + REQUIRE(status == PrintBase::APPLY_STATUS_UNCHANGED); + REQUIRE(print.is_step_done(psSlicingFinished)); + // the per-extruder machine values must survive the in-slice override recompute + REQUIRE(print.config().retract_before_wipe.values == std::vector({100., 70., 70., 70., 100.})); + REQUIRE(print.config().z_hop_types.values == std::vector({zhtSlope, zhtNormal, zhtNormal, zhtNormal, zhtSlope})); +} + TEST_CASE("normalize_nozzle_map_per_layer makes per-filament assignments gap-free", "[MultiNozzle][H2C][Dynamic]") { SECTION("gaps inherit the last used nozzle, entries on used layers stay untouched") { From 466c36eaa3931b9d1839357644d577242f6a75eb Mon Sep 17 00:00:00 2001 From: SoftFever Date: Mon, 27 Jul 2026 03:07:07 +0800 Subject: [PATCH 03/16] Complete the rib wipe tower port in WipeTower2 The rib tower is now always square (prime_tower_width is ignored, as the GUI already implies), carries the rib origin offset like the BBL tower so the rib tips sit inside the configured position, clamps the rib length to the tower diagonal, and extends the ribs for short towers. --- src/libslic3r/GCode.cpp | 6 ++- src/libslic3r/GCode/PrintExtents.cpp | 3 +- src/libslic3r/GCode/WipeTower2.cpp | 59 ++++++++++++++++++++++------ src/libslic3r/GCode/WipeTower2.hpp | 11 ++++-- src/libslic3r/Print.cpp | 7 +++- 5 files changed, 66 insertions(+), 20 deletions(-) diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index c88649b267..7550ba8097 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -1422,8 +1422,10 @@ static std::vector get_path_of_change_filament(const Print& print) // We want to rotate and shift all extrusions (gcode postprocessing) and starting and ending position float alpha = m_wipe_tower_rotation / 180.f * float(M_PI); + // The rib-wall offset is tower-local, so it rotates with the tower (unlike the BBL + // tower in append_tcr, which never rotates). Priming lines are absolute bed moves. auto transform_wt_pt = [&alpha, this](const Vec2f &pt) -> Vec2f { - Vec2f out = Eigen::Rotation2Df(alpha) * pt; + Vec2f out = Eigen::Rotation2Df(alpha) * (pt + m_rib_offset); out += m_wipe_tower_pos; return out; }; @@ -1435,7 +1437,7 @@ static std::vector get_path_of_change_filament(const Print& print) end_pos = transform_wt_pt(end_pos); } - Vec2f wipe_tower_offset = tcr.priming ? Vec2f::Zero() : m_wipe_tower_pos; + Vec2f wipe_tower_offset = tcr.priming ? Vec2f::Zero() : Vec2f(m_wipe_tower_pos + Eigen::Rotation2Df(alpha) * m_rib_offset); float wipe_tower_rotation = tcr.priming ? 0.f : alpha; Vec2f plate_origin_2d(m_plate_origin(0), m_plate_origin(1)); diff --git a/src/libslic3r/GCode/PrintExtents.cpp b/src/libslic3r/GCode/PrintExtents.cpp index 4a65ae5ae4..9008adaa9a 100644 --- a/src/libslic3r/GCode/PrintExtents.cpp +++ b/src/libslic3r/GCode/PrintExtents.cpp @@ -143,7 +143,8 @@ BoundingBoxf get_wipe_tower_extrusions_extents(const Print &print, const coordf_ double wipe_tower_y = print.config().wipe_tower_y.get_at(plate_idx) + plate_origin(1); Transform2d trafo = Eigen::Translation2d(wipe_tower_x, wipe_tower_y) * - Eigen::Rotation2Dd(Geometry::deg2rad(print.config().wipe_tower_rotation_angle.value)); + Eigen::Rotation2Dd(Geometry::deg2rad(print.config().wipe_tower_rotation_angle.value)) * + Eigen::Translation2d(print.wipe_tower_data().rib_offset.cast()); // tower-local rib-wall shift, zero unless rib BoundingBoxf bbox; for (const std::vector &tool_changes : print.wipe_tower_data().tool_changes) { diff --git a/src/libslic3r/GCode/WipeTower2.cpp b/src/libslic3r/GCode/WipeTower2.cpp index 34b385d4be..601992f515 100644 --- a/src/libslic3r/GCode/WipeTower2.cpp +++ b/src/libslic3r/GCode/WipeTower2.cpp @@ -2228,15 +2228,21 @@ void WipeTower2::plan_toolchange(float z_par, float layer_height_par, unsigned i return; // this is an actual toolchange - let's calculate depth to reserve on the wipe tower - float width = m_wipe_tower_width - 3*m_perimeter_width; + const bool first_layer_plan = (m_plan.size() - 1) == m_first_layer_idx; + m_plan.back().tool_changes.push_back(set_toolchange(old_tool, new_tool, layer_height_par, wipe_volume, first_layer_plan)); +} + +WipeTower2::WipeTowerInfo::ToolChange WipeTower2::set_toolchange(size_t old_tool, size_t new_tool, float layer_height, float wipe_volume, bool first_layer_plan) +{ + float width = m_wipe_tower_width - 3*m_perimeter_width; float length_to_extrude = volume_to_length(0.25f * std::accumulate(m_filpar[old_tool].ramming_speed.begin(), m_filpar[old_tool].ramming_speed.end(), 0.f), m_perimeter_width * m_filpar[old_tool].ramming_line_width_multiplicator, - layer_height_par); + layer_height); // Orca: Set ramming depth to 0 if ramming is disabled. float ramming_depth = m_enable_filament_ramming ? ((int(length_to_extrude / width) + 1) * (m_perimeter_width * m_filpar[old_tool].ramming_line_width_multiplicator * m_filpar[old_tool].ramming_step_multiplicator) * m_extra_spacing_ramming) : 0; float first_wipe_line = - (width*((length_to_extrude / width)-int(length_to_extrude / width)) - width); - float first_wipe_volume = length_to_volume(first_wipe_line, m_perimeter_width * m_extra_flow, layer_height_par); + float first_wipe_volume = length_to_volume(first_wipe_line, m_perimeter_width * m_extra_flow, layer_height); // ORCA: Keep wipe-depth planning consistent with toolchange_Wipe(). // ORCA: On the first layer, toolchange_Wipe() advances purge rows using @@ -2245,12 +2251,11 @@ void WipeTower2::plan_toolchange(float z_par, float layer_height_par, unsigned i // ORCA: float dy = (is_first_layer() ? m_extra_flow : m_extra_spacing_wipe) * m_perimeter_width; // ORCA: Use the same spacing here so reserved depth matches consumed depth // ORCA: and first-layer purge segments do not leave visible gaps. - const bool first_layer_plan = (m_plan.size() - 1) == m_first_layer_idx; const float planning_spacing = first_layer_plan ? m_extra_flow : m_extra_spacing_wipe; - float wiping_depth = get_wipe_depth(wipe_volume - first_wipe_volume, layer_height_par, m_perimeter_width, m_extra_flow, planning_spacing, width); - - m_plan.back().tool_changes.push_back(WipeTowerInfo::ToolChange(old_tool, new_tool, ramming_depth + wiping_depth, ramming_depth, first_wipe_line, wipe_volume)); + float wiping_depth = get_wipe_depth(wipe_volume - first_wipe_volume, layer_height, m_perimeter_width, m_extra_flow, planning_spacing, width); + + return WipeTowerInfo::ToolChange(old_tool, new_tool, ramming_depth + wiping_depth, ramming_depth, first_wipe_line, wipe_volume); } @@ -2378,9 +2383,35 @@ void WipeTower2::generate(std::vector> } #endif - m_rib_length = std::max({m_rib_length, sqrt(m_wipe_tower_depth * m_wipe_tower_depth + m_wipe_tower_width * m_wipe_tower_width)}); + if (m_wall_type == (int)wtwRib) { + // Rib wall: force a square tower like WipeTower::plan_tower_new(), ignoring the + // configured prime_tower_width (the GUI greys it out in rib mode). The planned depths + // already include the extra-spacing factors, so sqrt(depth * width) preserves the + // purge area. Replan every toolchange for the new width, then re-derive the depths. + float max_depth = 0.f; + for (const auto& current_plan : m_plan) + max_depth = std::max(max_depth, current_plan.depth); + if (max_depth > EPSILON) { + m_wipe_tower_width = align_ceil(std::sqrt(max_depth * m_wipe_tower_width), m_perimeter_width); + for (size_t idx = 0; idx < m_plan.size(); ++idx) + for (auto& toolchange : m_plan[idx].tool_changes) + toolchange = set_toolchange(toolchange.old_tool, toolchange.new_tool, + m_plan[idx].height, toolchange.wipe_volume, + idx == m_first_layer_idx); + plan_tower(); + } + + // Like WipeTower::plan_tower_new(): extend the ribs instead of the tower when the + // tower is smaller than the height-based stability minimum. + const float min_depth = WipeTower::get_limit_depth_by_height(m_wipe_tower_height); + if (m_wipe_tower_depth + EPSILON < min_depth) + m_rib_length = std::max(m_rib_length, min_depth * (float)std::sqrt(2.f)); + } + + const float diagonal = std::sqrt(m_wipe_tower_depth * m_wipe_tower_depth + m_wipe_tower_width * m_wipe_tower_width); + m_rib_length = std::max(m_rib_length, diagonal); m_rib_length += m_extra_rib_length; - m_rib_length = std::max(0.f, m_rib_length); + m_rib_length = std::max(diagonal, m_rib_length); // a negative extra length must not shrink the ribs below the diagonal m_rib_width = std::min(m_rib_width, std::min(m_wipe_tower_depth, m_wipe_tower_width) / 2.f); // Ensure that the rib wall of the wipetower are attached to the infill. @@ -2532,10 +2563,12 @@ Polygon WipeTower2::generate_support_rib_wall(WipeTowerWriter2& insert_skip_polygon = wall_polygon; } writer.generate_path(result_wall, feedrate, retract_length, retract_speed, m_used_fillet); - //if (m_cur_layer_id == 0) { - // BoundingBox bbox = get_extents(result_wall); - // m_rib_offset = Vec2f(-unscaled(bbox.min.x()), -unscaled(bbox.min.y())); - //} + // Tower-local shift that puts the rib wall's protruding first-layer min corner at the + // configured tower position, like WipeTower::generate_support_wall_new(). + if (rib_wall && is_first_layer()) { + BoundingBox bbox = get_extents(result_wall); + m_rib_offset = Vec2f(-unscaled(bbox.min.x()), -unscaled(bbox.min.y())); + } return insert_skip_polygon; } diff --git a/src/libslic3r/GCode/WipeTower2.hpp b/src/libslic3r/GCode/WipeTower2.hpp index 7060eefa4a..6169e8c392 100644 --- a/src/libslic3r/GCode/WipeTower2.hpp +++ b/src/libslic3r/GCode/WipeTower2.hpp @@ -69,9 +69,9 @@ public: const float brim = m_wipe_tower_brim_width_real; return BoundingBoxf(Vec2d(-brim, -brim), Vec2d(double(m_wipe_tower_width) + brim, double(m_wipe_tower_depth) + brim)); } - // WT2 doesn't currently compute a rib-origin compensation like WipeTower (m_rib_offset), - // so expose a zero offset for consistency purposes (to maintain API parity). - Vec2f get_rib_offset() const { return Vec2f::Zero(); } + // Tower-local shift that puts the rib wall's first-layer min corner at the configured + // tower position, like WipeTower::get_rib_offset(). Zero unless the rib wall is used. + Vec2f get_rib_offset() const { return m_rib_offset; } float get_rib_width() const { return m_rib_width; } float get_rib_length() const { return m_rib_length; } @@ -231,6 +231,7 @@ private: float m_rib_width = 10; float m_extra_rib_length = 0; float m_rib_length = 0; + Vec2f m_rib_offset = Vec2f::Zero(); bool m_enable_arc_fitting = false; @@ -372,6 +373,10 @@ private: float spacing); Polygon generate_rib_polygon(const WipeTower::box_coordinates& wt_box); + + // Computes the depth reserved for a toolchange (shared by plan_toolchange() and the + // rib-wall square-tower replanning in generate()). + WipeTowerInfo::ToolChange set_toolchange(size_t old_tool, size_t new_tool, float layer_height, float wipe_volume, bool first_layer_plan); }; diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index ecc2f39c88..371f1e68ea 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -4333,7 +4333,12 @@ void Print::_make_wipe_tower() wipe_tower.get_rib_width(), wipe_tower.get_rib_length(), config().wipe_tower_fillet_wall.value); const Vec3d origin = Vec3d::Zero(); - m_fake_wipe_tower.set_fake_extrusion_data(wipe_tower.position(), wipe_tower.width(), wipe_tower.get_wipe_tower_height(), + // FakeWipeTower::pos is a bed-frame translation applied after rotation + // (getFakeExtrusionPathsFromWipeTower2 rotates about the local origin), so the + // tower-local rib offset must be rotated into the bed frame first. + m_fake_wipe_tower.rib_offset = Eigen::Rotation2Df(Geometry::deg2rad((float)config().wipe_tower_rotation_angle.value)) * + wipe_tower.get_rib_offset(); + m_fake_wipe_tower.set_fake_extrusion_data(wipe_tower.position() + m_fake_wipe_tower.rib_offset, wipe_tower.width(), wipe_tower.get_wipe_tower_height(), config().initial_layer_print_height, m_wipe_tower_data.depth, m_wipe_tower_data.z_and_depth_pairs, m_wipe_tower_data.brim_width, config().wipe_tower_rotation_angle, config().wipe_tower_cone_angle, From 1696d5ca39fb9becfdf422d97ef19b43b0b51620 Mon Sep 17 00:00:00 2001 From: SoftFever Date: Mon, 27 Jul 2026 03:08:47 +0800 Subject: [PATCH 04/16] Use the squared rib tower size in arrange estimates estimate_wipe_tower_polygon reserved the arrange footprint and clamped the tower X position with the raw prime_tower_width, under-reserving space whenever the rib wall squares the tower to a different width. --- src/slic3r/GUI/PartPlate.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/slic3r/GUI/PartPlate.cpp b/src/slic3r/GUI/PartPlate.cpp index 4b52592f2d..4f57c2577d 100644 --- a/src/slic3r/GUI/PartPlate.cpp +++ b/src/slic3r/GUI/PartPlate.cpp @@ -2298,6 +2298,7 @@ arrangement::ArrangePolygon PartPlate::estimate_wipe_tower_polygon(const Dynamic bool enable_wrapping = (wrapping_opt != nullptr) && wrapping_opt->value; wt_size = estimate_wipe_tower_size(config, w, v, extruder_count, plate_extruder_size, use_global_objects, enable_wrapping); int plate_width=m_width, plate_depth=m_depth; + w = wt_size(0); // effective width; differs from prime_tower_width when the rib wall squares the tower float depth = wt_size(1); float margin = WIPE_TOWER_MARGIN + tower_brim_width, wp_brim_width = 0.f; const ConfigOption* wipe_tower_brim_width_opt = config.option("prime_tower_brim_width"); From c3c37e474a93fbe00d560dacf5d1ee22c2e7fa23 Mon Sep 17 00:00:00 2001 From: SoftFever Date: Mon, 27 Jul 2026 12:30:57 +0800 Subject: [PATCH 05/16] Print the WipeTower2 shell with a non-support, non-soluble filament Like the BBL tower: the layer's sparse infill, wall, and brim go to the first toolchange to a non-support/non-soluble filament, or are printed with the incoming filament before any toolchange. The minimal-purge clamp now also covers toolchanges that get no finish-layer saving. Output is unchanged when no support/soluble filament is used. --- src/libslic3r/GCode/WipeTower2.cpp | 81 ++++++++++++++++++++---------- src/libslic3r/GCode/WipeTower2.hpp | 8 +-- 2 files changed, 59 insertions(+), 30 deletions(-) diff --git a/src/libslic3r/GCode/WipeTower2.cpp b/src/libslic3r/GCode/WipeTower2.cpp index 601992f515..ccb68efb7a 100644 --- a/src/libslic3r/GCode/WipeTower2.cpp +++ b/src/libslic3r/GCode/WipeTower2.cpp @@ -1342,6 +1342,7 @@ void WipeTower2::set_extruder(size_t idx, const PrintConfig& config) m_filpar[idx].is_soluble = (idx != size_t(m_wipe_tower_filament - 1)); else m_filpar[idx].is_soluble = config.filament_soluble.get_at(idx); + m_filpar[idx].is_support = config.filament_is_support.get_at(idx); m_filpar[idx].temperature = config.nozzle_temperature.get_at(idx); m_filpar[idx].first_layer_temperature = config.nozzle_temperature_initial_layer.get_at(idx); m_filpar[idx].filament_minimal_purge_on_wipe_tower = config.filament_minimal_purge_on_wipe_tower.get_at(idx); @@ -2293,49 +2294,64 @@ void WipeTower2::save_on_last_wipe() continue; // Which toolchange will finish_layer extrusions be subtracted from? - int idx = first_toolchange_to_nonsoluble(m_layer_info->tool_changes); + int idx = first_toolchange_to_nonsoluble_nonsupport(m_layer_info->tool_changes); if (idx == -1) { // In this case, finish_layer will be called at the very beginning. finish_layer().total_extrusion_length_in_plane(); } + const float width = m_wipe_tower_width - 3*m_perimeter_width; // width we draw into + auto recompute_toolchange = [this, width](WipeTowerInfo::ToolChange& toolchange, float volume_to_save) { + float volume_left_to_wipe = std::max(m_filpar[toolchange.new_tool].filament_minimal_purge_on_wipe_tower, toolchange.wipe_volume_total - volume_to_save); + float volume_we_need_depth_for = std::max(0.f, volume_left_to_wipe - length_to_volume(toolchange.first_wipe_line, m_perimeter_width*m_extra_flow, m_layer_info->height)); + + // ORCA: Keep wipe-depth planning consistent with toolchange_Wipe(). + // ORCA: On the first layer, toolchange_Wipe() advances purge rows using + // ORCA: m_extra_flow * m_perimeter_width, while later layers use + // ORCA: m_extra_spacing_wipe * m_perimeter_width. + // ORCA: float dy = (is_first_layer() ? m_extra_flow : m_extra_spacing_wipe) * m_perimeter_width; + // ORCA: Use the same spacing here so reserved depth matches consumed depth + // ORCA: and first-layer purge segments do not leave visible gaps. + const bool first_layer_plan = size_t(m_layer_info - m_plan.begin()) == m_first_layer_idx; + const float planning_spacing = first_layer_plan ? m_extra_flow : m_extra_spacing_wipe; + + float depth_to_wipe = get_wipe_depth(volume_we_need_depth_for, m_layer_info->height, m_perimeter_width, m_extra_flow, planning_spacing, width); + + toolchange.required_depth = toolchange.ramming_depth + depth_to_wipe; + toolchange.wipe_volume = volume_left_to_wipe; + }; + for (int i=0; itool_changes.size()); ++i) { auto& toolchange = m_layer_info->tool_changes[i]; tool_change(toolchange.new_tool); if (i == idx) { - float width = m_wipe_tower_width - 3*m_perimeter_width; // width we draw into - - float volume_to_save = length_to_volume(finish_layer().total_extrusion_length_in_plane(), m_perimeter_width, m_layer_info->height); - float volume_left_to_wipe = std::max(m_filpar[toolchange.new_tool].filament_minimal_purge_on_wipe_tower, toolchange.wipe_volume_total - volume_to_save); - float volume_we_need_depth_for = std::max(0.f, volume_left_to_wipe - length_to_volume(toolchange.first_wipe_line, m_perimeter_width*m_extra_flow, m_layer_info->height)); - - // ORCA: Keep wipe-depth planning consistent with toolchange_Wipe(). - // ORCA: On the first layer, toolchange_Wipe() advances purge rows using - // ORCA: m_extra_flow * m_perimeter_width, while later layers use - // ORCA: m_extra_spacing_wipe * m_perimeter_width. - // ORCA: float dy = (is_first_layer() ? m_extra_flow : m_extra_spacing_wipe) * m_perimeter_width; - // ORCA: Use the same spacing here so reserved depth matches consumed depth - // ORCA: and first-layer purge segments do not leave visible gaps. - const bool first_layer_plan = size_t(m_layer_info - m_plan.begin()) == m_first_layer_idx; - const float planning_spacing = first_layer_plan ? m_extra_flow : m_extra_spacing_wipe; - - float depth_to_wipe = get_wipe_depth(volume_we_need_depth_for, m_layer_info->height, m_perimeter_width, m_extra_flow, planning_spacing, width); - - toolchange.required_depth = toolchange.ramming_depth + depth_to_wipe; - toolchange.wipe_volume = volume_left_to_wipe; + recompute_toolchange(toolchange, length_to_volume(finish_layer().total_extrusion_length_in_plane(), m_perimeter_width, m_layer_info->height)); + } else if (toolchange.wipe_volume < m_filpar[toolchange.new_tool].filament_minimal_purge_on_wipe_tower) { + // Keep filament_minimal_purge_on_wipe_tower enforced for toolchanges that get + // no finish-layer saving, e.g. a support/soluble filament skipped as the + // finish filament above. Recomputing only when the clamp binds leaves all + // other toolchanges with their planned values bit-for-bit. + recompute_toolchange(toolchange, 0.f); } } } } -// Return index of first toolchange that switches to non-soluble extruder -// ot -1 if there is no such toolchange. -int WipeTower2::first_toolchange_to_nonsoluble( +// Return the index of the toolchange whose new filament should print the layer's +// finish extrusions (sparse infill + wall + brim), or -1 to print them with the +// layer's incoming filament before any toolchange happens. +// Like WipeTower::first_toolchange_to_nonsoluble_nonsupport(): support and soluble +// filaments bond poorly to the material printed on top of them, so they must not +// print the tower's shell when another filament is available on the layer. +int WipeTower2::first_toolchange_to_nonsoluble_nonsupport( const std::vector& tool_changes) const { + if (tool_changes.empty()) + return -1; + // If a specific wipe tower filament is forced, use it to decide where to finish the layer. if (m_wipe_tower_filament > 0) { for (size_t idx = 0; idx < tool_changes.size(); ++idx) { @@ -2344,8 +2360,19 @@ int WipeTower2::first_toolchange_to_nonsoluble( } return -1; } - // Orca: allow calculation of the required depth and wipe volume for soluble toolchanges as well. - return tool_changes.empty() ? -1 : 0; + + auto is_wall_filament = [this](size_t tool) { + return !m_filpar[tool].is_soluble && !m_filpar[tool].is_support; + }; + for (size_t idx = 0; idx < tool_changes.size(); ++idx) + if (is_wall_filament(tool_changes[idx].new_tool)) + return idx; + if (is_wall_filament(tool_changes.front().old_tool)) + return -1; + // Only support/soluble filaments on this layer: keep the first toolchange so the + // finish-layer saving and the minimal-purge clamp still apply to it (Orca depth + // and wipe volume accounting, see save_on_last_wipe()). + return 0; } static WipeTower::ToolChangeResult merge_tcr(WipeTower::ToolChangeResult& first, @@ -2441,7 +2468,7 @@ void WipeTower2::generate(std::vector> if (m_layer_info->depth < m_wipe_tower_depth - m_perimeter_width) m_y_shift = (m_wipe_tower_depth-m_layer_info->depth-m_perimeter_width)/2.f; - int idx = first_toolchange_to_nonsoluble(layer.tool_changes); + int idx = first_toolchange_to_nonsoluble_nonsupport(layer.tool_changes); WipeTower::ToolChangeResult finish_layer_tcr; if (idx == -1) { diff --git a/src/libslic3r/GCode/WipeTower2.hpp b/src/libslic3r/GCode/WipeTower2.hpp index 6169e8c392..792aed4204 100644 --- a/src/libslic3r/GCode/WipeTower2.hpp +++ b/src/libslic3r/GCode/WipeTower2.hpp @@ -149,6 +149,7 @@ public: struct FilamentParameters { std::string material = "PLA"; bool is_soluble = false; + bool is_support = false; int temperature = 0; int first_layer_temperature = 0; int interface_print_temperature = 0; @@ -329,9 +330,10 @@ private: std::vector m_used_filament_length; std::vector>> m_used_filament_length_until_layer; - // Return index of first toolchange that switches to non-soluble extruder - // ot -1 if there is no such toolchange. - int first_toolchange_to_nonsoluble( + // Return the index of the toolchange whose new filament should print the layer's + // finish extrusions (sparse infill + wall + brim), or -1 to print them with the + // layer's incoming filament before any toolchange happens. + int first_toolchange_to_nonsoluble_nonsupport( const std::vector& tool_changes) const; void toolchange_Unload( From 56810c8c7f9952ee3666d0d217eb52e79671f9b1 Mon Sep 17 00:00:00 2001 From: SoftFever Date: Mon, 27 Jul 2026 21:18:53 +0800 Subject: [PATCH 06/16] Port the skip-points gap wall to WipeTower2 prime_tower_skip_points was stubbed for Type2 towers: the wall call hard-coded skip_points=false, the gap cutter received an empty vector, and append_tcr2 never routed the entry travel. Now the toolchange entry positions are precomputed from the finalized plan, the wall is cut open at each entry, and the entry travel approaches around the tower bounding box through the opening when it starts outside the tower. The geometry helpers are re-synced with the BBL versions (add_extra_point guards, per-point side selection). The cone wall keeps its separate path, where the option stays inert. Behavior change: non-BBL towers now honor the (default-on) checkbox with gap walls and routed entries; with the option off the output is unchanged, and the BBL tower path is untouched. --- src/libslic3r/GCode.cpp | 37 ++++++- src/libslic3r/GCode/WipeTower2.cpp | 150 ++++++++++++++++++++++++++--- src/libslic3r/GCode/WipeTower2.hpp | 7 ++ 3 files changed, 178 insertions(+), 16 deletions(-) diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index 7550ba8097..c5a1372f9d 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -1472,7 +1472,42 @@ static std::vector get_path_of_change_filament(const Print& print) // then we could simplify the condition and make it more readable. gcode += gcodegen.retract(); gcodegen.m_avoid_crossing_perimeters.use_external_mp_once(); - gcode += gcodegen.travel_to(wipe_tower_point_to_object_point(gcodegen, start_pos + plate_origin_2d), erMixed, "Travel to a Wipe Tower"); + const Point start_wipe_pos = wipe_tower_point_to_object_point(gcodegen, start_pos + plate_origin_2d); + // With skip points enabled the tower wall has an opening at this tcr's start + // position: approach around the tower's bounding box and enter through it instead + // of dragging the oozing nozzle across the printed wall (append_tcr parity). + // Hops that already start inside the tower stay direct — they never cross the wall. + if (gcodegen.m_config.prime_tower_skip_points.value + && gcodegen.m_config.wipe_tower_wall_type.value != WipeTowerWallType::wtwCone + && !tcr.priming && gcodegen.last_pos_defined()) { + BoundingBox printer_bbx; + if (is_multi_nozzle_printer(gcodegen.m_config)) { + printer_bbx = get_extents(gcodegen.m_print->get_extruder_shared_printable_polygon()); + printer_bbx.min = wipe_tower_point_to_object_point(gcodegen, unscaled(printer_bbx.min) + plate_origin_2d); + printer_bbx.max = wipe_tower_point_to_object_point(gcodegen, unscaled(printer_bbx.max) + plate_origin_2d); + } else { + Points bed_points; + for (const auto& p : gcodegen.m_config.printable_area.values) + bed_points.push_back(wipe_tower_point_to_object_point(gcodegen, p.cast() + plate_origin_2d)); + printer_bbx = BoundingBox(bed_points); + } + // Transform the tower-local bbx corners exactly like the tcr points (rib + // offset, rotation, tower position); a rotated tower gets a conservative + // axis-aligned envelope. + Polygon avoid_points = scaled(m_wipe_tower_bbx).polygon(); + for (auto& p : avoid_points.points) { + Vec2f pp = transform_wt_pt(unscale(p).cast()); + p = wipe_tower_point_to_object_point(gcodegen, pp + plate_origin_2d); + } + BoundingBox avoid_bbx(avoid_points.points); + if (!avoid_bbx.contains(gcodegen.last_pos())) { + Polyline travel_polyline = generate_path_to_wipe_tower(gcodegen.last_pos(), start_wipe_pos, avoid_bbx, printer_bbx); + // The polyline's last point is start_wipe_pos itself — emitted below. + for (size_t i = 0; i + 1 < travel_polyline.points.size(); ++i) + gcode += gcodegen.travel_to(travel_polyline.points[i], erMixed, "Travel to a Wipe Tower"); + } + } + gcode += gcodegen.travel_to(start_wipe_pos, erMixed, "Travel to a Wipe Tower"); gcode += gcodegen.unretract(); } else { // When this is multiextruder printer without any ramming, we can just change diff --git a/src/libslic3r/GCode/WipeTower2.cpp b/src/libslic3r/GCode/WipeTower2.cpp index ccb68efb7a..03ce6a80e5 100644 --- a/src/libslic3r/GCode/WipeTower2.cpp +++ b/src/libslic3r/GCode/WipeTower2.cpp @@ -428,16 +428,109 @@ static void insert_points(std::vector& pl, int idx, Vec2f pos, in } } -static Polylines remove_points_from_polygon( - const Polygon& polygon, const std::vector& skip_points, double range, bool is_left, Polygon& insert_skip_pg) +// For skip_point +// TODO: Optimize the skip_point algorithm itself instead of adding guards here +static Polygon add_extra_point(const Polygon& polygon, int scale_range) { - assert(polygon.size() > 2); + Polygon res; + if (polygon.size() < 2) return polygon; + + // Compute bounding box of the polygon + auto polygon_box = get_extents(polygon); + + // Anchor point: X at bbox center, Y at bbox bottom + Vec2f anchor_point(float(polygon_box.center()[0]), float(polygon_box.min[1])); + + // Find the edge whose midpoint is closest to the anchor point + size_t closest_edge_idx = 0; + float min_dist_sq = std::numeric_limits::max(); + + for (size_t i = 0; i < polygon.size(); ++i) { + const Point &a_i = polygon[i]; + const Point &b_i = polygon[(i + 1) % polygon.size()]; + + Vec2f a(float(a_i.x()), float(a_i.y())); + Vec2f b(float(b_i.x()), float(b_i.y())); + Vec2f mid = (a + b) * 0.5f; + + float dist_sq = (anchor_point - mid).squaredNorm(); + if (dist_sq < min_dist_sq) { + min_dist_sq = dist_sq; + closest_edge_idx = i; + } + } + + // Edge endpoints (integer space) + const Point &a_i = polygon[closest_edge_idx]; + const Point &b_i = polygon[(closest_edge_idx + 1) % polygon.size()]; + + // Convert to float for geometric computation + Vec2f a(float(a_i.x()), float(a_i.y())); + Vec2f b(float(b_i.x()), float(b_i.y())); + + Vec2f mid = (a + b) * 0.5f; + + // Direction vectors from midpoint towards A and B + Vec2f dir_to_a = a - mid; + Vec2f dir_to_b = b - mid; + + float len_a = dir_to_a.norm(); + float len_b = dir_to_b.norm(); + + // Guard against degenerated edges + if (len_a < EPSILON || len_b < EPSILON) return polygon; + + dir_to_a /= len_a; + dir_to_b /= len_b; + + // Clamp range to avoid overshooting the edge + float max_range = std::min(len_a, len_b) * 0.9f; + float range = std::min(float(scale_range), max_range); + + // Offset points (float space) + Vec2f offset_to_a_f = mid + dir_to_a * range; + Vec2f offset_to_b_f = mid + dir_to_b * range; + + // Safe cast back to scaled integer Point + auto to_int_point = [](const Vec2f &p) { + auto clamp = [](float v) -> coord_t { + constexpr float kMin = float(std::numeric_limits::min()); + constexpr float kMax = float(std::numeric_limits::max()); + v = std::clamp(v, kMin, kMax); + return static_cast(std::lround(v)); + }; + return Point(clamp(p.x()), clamp(p.y())); + }; + + Point mid_i = to_int_point(mid); + Point offset_to_a_i = to_int_point(offset_to_a_f); + Point offset_to_b_i = to_int_point(offset_to_b_f); + + // Rebuild polygon with inserted points + for (size_t i = 0; i < polygon.size(); ++i) { + res.points.push_back(polygon[i]); + + // Insert points right after the selected edge start vertex + if (i == closest_edge_idx) { + res.points.push_back(offset_to_a_i); + res.points.push_back(mid_i); + res.points.push_back(offset_to_b_i); + } + } + + return res; +} + +static Polylines remove_points_from_polygon( + const Polygon& polygon_ori, const std::vector& skip_points, double range, float wt_width, Polygon& insert_skip_pg) +{ + Polygon polygon = add_extra_point(polygon_ori, scale_(range)); + if (polygon.size() < 2) return Polylines{to_polyline(polygon)}; Polylines result; std::vector new_pl; // add intersection points for gaps, where bool indicates whether it's a gap point. std::vector inter_info; - Vec2f ray = is_left ? Vec2f(-1, 0) : Vec2f(1, 0); auto polygon_box = get_extents(polygon); - Point anchor_point = is_left ? Point{polygon_box.max[0], polygon_box.min[1]} : polygon_box.min; // rd:ld + Point anchor_point = Point{polygon_box.center()[0], polygon_box.min[1]}; // for next reconnect std::vector points; { points.reserve(polygon.points.size()); @@ -449,6 +542,8 @@ static Polylines remove_points_from_polygon( } for (int i = 0; i < skip_points.size(); i++) { + bool is_left = abs(skip_points[i].x()) < wt_width / 2.f; + Vec2f ray = is_left ? Vec2f(-1, 0) : Vec2f(1, 0); for (int j = 0; j < points.size(); j++) { Vec2f& p1 = points[j]; Vec2f& p2 = points[(j + 1) % points.size()]; @@ -526,12 +621,7 @@ static Polylines contrust_gap_for_skip_points( insert_skip_polygon = polygon; return Polylines{to_polyline(polygon)}; } - bool is_left = false; - const auto& pt = skip_points.front(); - if (abs(pt.x()) < wt_width / 2.f) { - is_left = true; - } - return remove_points_from_polygon(polygon, skip_points, gap_length, is_left, insert_skip_polygon); + return remove_points_from_polygon(polygon, skip_points, gap_length, wt_width, insert_skip_polygon); }; static Polygon generate_rectange_polygon(const Vec2f& wt_box_min, const Vec2f& wt_box_max) @@ -1272,6 +1362,8 @@ WipeTower2::WipeTower2(const PrintConfig& config, const PrintRegionConfig& defau m_rib_width(config.wipe_tower_rib_width), m_extra_rib_length(config.wipe_tower_extra_rib_length), m_wall_type((int)config.wipe_tower_wall_type), + // The cone wall has its own fully separate generator with no gap machinery. + m_use_gap_wall(config.prime_tower_skip_points.value && config.wipe_tower_wall_type.value != wtwCone), m_flat_ironing(config.prime_tower_flat_ironing.value), m_enable_tower_interface_features(config.enable_tower_interface_features.value), m_enable_tower_interface_cooldown_during_tower(config.enable_tower_interface_cooldown_during_tower.value) @@ -2111,7 +2203,7 @@ WipeTower::ToolChangeResult WipeTower2::finish_layer() poly = generate_support_cone_wall(writer, wt_box, feedrate, infill_cone, spacing); } else { WipeTower::box_coordinates wt_box(Vec2f(0.f, 0.f), m_wipe_tower_width, m_layer_info->depth + m_perimeter_width); - poly = generate_support_rib_wall(writer, wt_box, feedrate, first_layer, m_wall_type == (int)wtwRib, true, false); + poly = generate_support_rib_wall(writer, wt_box, feedrate, first_layer, m_wall_type == (int)wtwRib, true, m_use_gap_wall); } // brim (first layer only) @@ -2397,6 +2489,23 @@ static WipeTower::ToolChangeResult merge_tcr(WipeTower::ToolChangeResult& first, // Processes vector m_plan and calls respective functions to generate G-code for the wipe tower // Resulting ToolChangeResults are appended into vector "result" +// Precompute, for every plan layer, the wall openings ("skip points") at each toolchange's +// entry, like WipeTower::get_all_wall_skip_points(). The entry is where tool_change() +// starts: cleaning_box.ld + (0, m_depth_traversed), with m_depth_traversed advancing by +// required_depth per toolchange — reproduced here from the finalized plan so each gap +// coincides with the entry travel's target (tcr.start_pos, pre-rotation frame). +void WipeTower2::compute_wall_skip_points() +{ + m_wall_skip_points.assign(m_plan.size(), std::vector()); + for (size_t layer_id = 0; layer_id < m_plan.size(); ++layer_id) { + float depth_traversed = 0.f; + for (const auto& toolchange : m_plan[layer_id].tool_changes) { + m_wall_skip_points[layer_id].emplace_back(m_perimeter_width / 2.f, m_perimeter_width / 2.f + depth_traversed); + depth_traversed += toolchange.required_depth; + } + } +} + void WipeTower2::generate(std::vector> &result) { if (m_plan.empty()) @@ -2442,6 +2551,9 @@ void WipeTower2::generate(std::vector> m_rib_width = std::min(m_rib_width, std::min(m_wipe_tower_depth, m_wipe_tower_width) / 2.f); // Ensure that the rib wall of the wipetower are attached to the infill. + if (m_use_gap_wall) + compute_wall_skip_points(); + m_layer_info = m_plan.begin(); m_current_height = 0.f; @@ -2583,7 +2695,14 @@ Polygon WipeTower2::generate_support_rib_wall(WipeTowerWriter2& return wall_polygon; if (skip_points) { - result_wall = contrust_gap_for_skip_points(wall_polygon, std::vector(), m_wipe_tower_width, 2.5 * m_perimeter_width, + // Cut the wall open at each toolchange's entry (see compute_wall_skip_points()). + // The vector is empty during the save_on_last_wipe planning passes, which therefore + // measure the un-gapped wall — same approximation as the BBL tower. + static const std::vector no_skip_points; + const size_t layer_id = size_t(m_layer_info - m_plan.begin()); + const std::vector& layer_skip_points = + layer_id < m_wall_skip_points.size() ? m_wall_skip_points[layer_id] : no_skip_points; + result_wall = contrust_gap_for_skip_points(wall_polygon, layer_skip_points, m_wipe_tower_width, 2.5 * m_perimeter_width, insert_skip_polygon); } else { result_wall.push_back(to_polyline(wall_polygon)); @@ -2591,9 +2710,10 @@ Polygon WipeTower2::generate_support_rib_wall(WipeTowerWriter2& } writer.generate_path(result_wall, feedrate, retract_length, retract_speed, m_used_fillet); // Tower-local shift that puts the rib wall's protruding first-layer min corner at the - // configured tower position, like WipeTower::generate_support_wall_new(). + // configured tower position, like WipeTower::generate_support_wall_new(). Measured on + // the un-gapped outline so a wall gap cannot shift the tower. if (rib_wall && is_first_layer()) { - BoundingBox bbox = get_extents(result_wall); + BoundingBox bbox = get_extents(insert_skip_polygon); m_rib_offset = Vec2f(-unscaled(bbox.min.x()), -unscaled(bbox.min.y())); } diff --git a/src/libslic3r/GCode/WipeTower2.hpp b/src/libslic3r/GCode/WipeTower2.hpp index 792aed4204..12e6573d73 100644 --- a/src/libslic3r/GCode/WipeTower2.hpp +++ b/src/libslic3r/GCode/WipeTower2.hpp @@ -233,6 +233,11 @@ private: float m_extra_rib_length = 0; float m_rib_length = 0; Vec2f m_rib_offset = Vec2f::Zero(); + bool m_use_gap_wall = false; + // Per plan layer, each toolchange's entry position (tower-local, un-shifted frame): + // where the wall is cut open so the entry travel does not cross the printed wall. + // Filled by compute_wall_skip_points() once the plan is final. + std::vector> m_wall_skip_points; bool m_enable_arc_fitting = false; @@ -376,6 +381,8 @@ private: Polygon generate_rib_polygon(const WipeTower::box_coordinates& wt_box); + void compute_wall_skip_points(); + // Computes the depth reserved for a toolchange (shared by plan_toolchange() and the // rib-wall square-tower replanning in generate()). WipeTowerInfo::ToolChange set_toolchange(size_t old_tool, size_t new_tool, float layer_height, float wipe_volume, bool first_layer_plan); From 5b475e5e989b773af8740fce324a3f7cd2f24240 Mon Sep 17 00:00:00 2001 From: SoftFever Date: Tue, 28 Jul 2026 00:15:59 +0800 Subject: [PATCH 07/16] Route the in-place toolchange tower entry through the skip-point gap On multi-tool printers without ramming the tool changes away from the tower and the entry travel is the tcr's own positioning move, which went straight across the printed wall. Append the avoid-perimeter path to the change-filament gcode instead, so the head approaches around the tower and enters through the wall opening (append_tcr parity). --- src/libslic3r/GCode.cpp | 120 ++++++++++++++++++++++++++++------------ src/libslic3r/GCode.hpp | 1 + 2 files changed, 85 insertions(+), 36 deletions(-) diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index c5a1372f9d..a93899036f 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -888,6 +888,50 @@ static std::vector get_path_of_change_filament(const Print& print) return res; } + // With skip points enabled the Type2 tower wall has an opening at each toolchange's + // entry (tcr.start_pos): route the approach around the tower's bounding box so the + // nozzle enters through that opening instead of dragging across the printed wall + // (append_tcr parity). Emits only the waypoints leading up to the opening — the + // caller still travels to start_wipe_pos itself. Returns an empty string when the + // option is off, the cone wall is active (it has no gap machinery), or the approach + // already starts inside the tower: such hops never cross the wall and must stay direct. + std::string WipeTowerIntegration::travel_to_tower_gap(GCode &gcodegen, const Point &route_start, const Point &start_wipe_pos) const + { + if (!gcodegen.m_config.prime_tower_skip_points.value + || gcodegen.m_config.wipe_tower_wall_type.value == WipeTowerWallType::wtwCone) + return {}; + const Vec2f plate_origin_2d(m_plate_origin(0), m_plate_origin(1)); + BoundingBox printer_bbx; + if (is_multi_nozzle_printer(gcodegen.m_config)) { + printer_bbx = get_extents(gcodegen.m_print->get_extruder_shared_printable_polygon()); + printer_bbx.min = wipe_tower_point_to_object_point(gcodegen, unscaled(printer_bbx.min) + plate_origin_2d); + printer_bbx.max = wipe_tower_point_to_object_point(gcodegen, unscaled(printer_bbx.max) + plate_origin_2d); + } else { + Points bed_points; + for (const auto& p : gcodegen.m_config.printable_area.values) + bed_points.push_back(wipe_tower_point_to_object_point(gcodegen, p.cast() + plate_origin_2d)); + printer_bbx = BoundingBox(bed_points); + } + // Transform the tower-local bbx corners exactly like the tcr points (rib + // offset, rotation, tower position); a rotated tower gets a conservative + // axis-aligned envelope. + const float alpha = m_wipe_tower_rotation / 180.f * float(M_PI); + Polygon avoid_points = scaled(m_wipe_tower_bbx).polygon(); + for (auto& p : avoid_points.points) { + Vec2f pp = Eigen::Rotation2Df(alpha) * (unscale(p).cast() + m_rib_offset) + m_wipe_tower_pos; + p = wipe_tower_point_to_object_point(gcodegen, pp + plate_origin_2d); + } + BoundingBox avoid_bbx(avoid_points.points); + if (avoid_bbx.contains(route_start)) + return {}; + Polyline travel_polyline = generate_path_to_wipe_tower(route_start, start_wipe_pos, avoid_bbx, printer_bbx); + std::string gcode; + // The polyline's last point is start_wipe_pos itself — emitted by the caller. + for (size_t i = 0; i + 1 < travel_polyline.points.size(); ++i) + gcode += gcodegen.travel_to(travel_polyline.points[i], erMixed, "Travel to a Wipe Tower"); + return gcode; + } + std::string WipeTowerIntegration::append_tcr(GCode& gcodegen, const WipeTower::ToolChangeResult& tcr, int new_filament_id, double z) const { if (new_filament_id != -1 && new_filament_id != tcr.new_tool) @@ -1467,51 +1511,22 @@ static std::vector get_path_of_change_filament(const Print& print) || is_ramming || tool_change_on_wipe_tower); - if (should_travel_to_tower || gcodegen.m_need_change_layer_lift_z) { + const bool travel_to_tower_now = should_travel_to_tower || gcodegen.m_need_change_layer_lift_z; + if (travel_to_tower_now) { // FIXME: It would be better if the wipe tower set the force_travel flag for all toolchanges, // then we could simplify the condition and make it more readable. gcode += gcodegen.retract(); gcodegen.m_avoid_crossing_perimeters.use_external_mp_once(); const Point start_wipe_pos = wipe_tower_point_to_object_point(gcodegen, start_pos + plate_origin_2d); - // With skip points enabled the tower wall has an opening at this tcr's start - // position: approach around the tower's bounding box and enter through it instead - // of dragging the oozing nozzle across the printed wall (append_tcr parity). - // Hops that already start inside the tower stay direct — they never cross the wall. - if (gcodegen.m_config.prime_tower_skip_points.value - && gcodegen.m_config.wipe_tower_wall_type.value != WipeTowerWallType::wtwCone - && !tcr.priming && gcodegen.last_pos_defined()) { - BoundingBox printer_bbx; - if (is_multi_nozzle_printer(gcodegen.m_config)) { - printer_bbx = get_extents(gcodegen.m_print->get_extruder_shared_printable_polygon()); - printer_bbx.min = wipe_tower_point_to_object_point(gcodegen, unscaled(printer_bbx.min) + plate_origin_2d); - printer_bbx.max = wipe_tower_point_to_object_point(gcodegen, unscaled(printer_bbx.max) + plate_origin_2d); - } else { - Points bed_points; - for (const auto& p : gcodegen.m_config.printable_area.values) - bed_points.push_back(wipe_tower_point_to_object_point(gcodegen, p.cast() + plate_origin_2d)); - printer_bbx = BoundingBox(bed_points); - } - // Transform the tower-local bbx corners exactly like the tcr points (rib - // offset, rotation, tower position); a rotated tower gets a conservative - // axis-aligned envelope. - Polygon avoid_points = scaled(m_wipe_tower_bbx).polygon(); - for (auto& p : avoid_points.points) { - Vec2f pp = transform_wt_pt(unscale(p).cast()); - p = wipe_tower_point_to_object_point(gcodegen, pp + plate_origin_2d); - } - BoundingBox avoid_bbx(avoid_points.points); - if (!avoid_bbx.contains(gcodegen.last_pos())) { - Polyline travel_polyline = generate_path_to_wipe_tower(gcodegen.last_pos(), start_wipe_pos, avoid_bbx, printer_bbx); - // The polyline's last point is start_wipe_pos itself — emitted below. - for (size_t i = 0; i + 1 < travel_polyline.points.size(); ++i) - gcode += gcodegen.travel_to(travel_polyline.points[i], erMixed, "Travel to a Wipe Tower"); - } - } + if (!tcr.priming && gcodegen.last_pos_defined()) + gcode += travel_to_tower_gap(gcodegen, gcodegen.last_pos(), start_wipe_pos); gcode += gcodegen.travel_to(start_wipe_pos, erMixed, "Travel to a Wipe Tower"); gcode += gcodegen.unretract(); } else { // When this is multiextruder printer without any ramming, we can just change - // the tool without travelling to the tower. + // the tool without travelling to the tower. The tower entry travel then lives + // inside the tcr gcode; with skip points on it is rerouted below, once the + // toolchange gcode (and the head position it ends at) is known. } if (will_go_down) { @@ -1534,6 +1549,39 @@ static std::vector get_path_of_change_filament(const Print& print) toolchange_temp_override = interface_temp; } toolchange_gcode_str = gcodegen.set_extruder(new_extruder_id, tcr.print_z, false, toolchange_temp_override); // TODO: toolchange_z vs print_z + if (!travel_to_tower_now && !tcr.priming && needs_toolchange + && gcodegen.m_config.prime_tower_skip_points.value + && gcodegen.m_config.wipe_tower_wall_type.value != WipeTowerWallType::wtwCone) { + // The tool changed in place (multi-tool printer without ramming), so the + // tower entry is the tcr's own positioning move — a straight line across + // the printed wall. Route it around the tower and in through the wall + // opening instead, riding at the end of the change_filament_gcode + // substitution so the generator's positioning move degrades to a + // zero-length one (append_tcr parity: travel after the filament change, + // retracted, with the new filament). + Vec3f last_gcode_pos = gcodegen.writer().get_position().cast(); + Point route_start; + bool have_start = false; + if (GCodeProcessor::get_last_position_from_gcode(toolchange_gcode_str, last_gcode_pos)) { + // A custom change_filament_gcode may have moved the head (tool docks + // etc.); recover the real position from the emitted gcode. + route_start = gcodegen.gcode_to_point(Vec2d(last_gcode_pos.x(), last_gcode_pos.y()) + plate_origin_2d.cast()); + have_start = true; + } else if (gcodegen.last_pos_defined()) { + route_start = gcodegen.last_pos(); + have_start = true; + } + if (have_start) { + gcodegen.set_last_pos(route_start); + gcodegen.m_avoid_crossing_perimeters.use_external_mp_once(); + const Point start_wipe_pos = wipe_tower_point_to_object_point(gcodegen, start_pos + plate_origin_2d); + std::string travel = travel_to_tower_gap(gcodegen, route_start, start_wipe_pos); + travel += gcodegen.travel_to(start_wipe_pos, erMixed, "Travel to a Wipe Tower"); + check_add_eol(travel); + toolchange_gcode_str += travel; + gcodegen.set_last_pos(start_wipe_pos); + } + } if (gcodegen.config().enable_prime_tower) { deretraction_str += gcodegen.writer().travel_to_z(z, "Force restore layer Z", true); Vec3d position{gcodegen.writer().get_position()}; diff --git a/src/libslic3r/GCode.hpp b/src/libslic3r/GCode.hpp index 558d601e53..bda9cc43c0 100644 --- a/src/libslic3r/GCode.hpp +++ b/src/libslic3r/GCode.hpp @@ -132,6 +132,7 @@ private: std::string append_tcr(GCode &gcodegen, const WipeTower::ToolChangeResult &tcr, int new_extruder_id, double z = -1.) const; Polyline generate_path_to_wipe_tower(const Point &start_pos, const Point &end_pos, const BoundingBox &avoid_polygon, const BoundingBox &printer_bbx) const; std::string append_tcr2(GCode &gcodegen, const WipeTower::ToolChangeResult &tcr, int new_extruder_id, double z = -1.) const; + std::string travel_to_tower_gap(GCode &gcodegen, const Point &route_start, const Point &start_wipe_pos) const; // Postprocesses gcode: rotates and moves G1 extrusions and returns result std::string post_process_wipe_tower_moves(const WipeTower::ToolChangeResult& tcr, const Vec2f& translation, float angle) const; From bef47b2c70085fccb7788f36c4b2505543c2c650 Mon Sep 17 00:00:00 2001 From: SoftFever Date: Tue, 28 Jul 2026 21:21:50 +0800 Subject: [PATCH 08/16] Iron the purge start out through the skip-point gap in WipeTower2 Port the BBL tower's entry line ironing: extrude the first 3 mm of the purge, retract, drag the nozzle 1.5x back out through the wall gap at F600, creep back at F240 and unretract, so the toolchange start blob ends up in the gap instead of on the wall. Fires only when the purge starts at the left-edge entry heading right (in-place toolchangers); SEMM ram/cooling wipes start mid-box and the priming line has no wall, so both keep their previous output. --- src/libslic3r/GCode/WipeTower2.cpp | 27 ++++++++++++++++++++++++--- src/libslic3r/GCode/WipeTower2.hpp | 3 ++- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/src/libslic3r/GCode/WipeTower2.cpp b/src/libslic3r/GCode/WipeTower2.cpp index 03ce6a80e5..c1d46e11c4 100644 --- a/src/libslic3r/GCode/WipeTower2.cpp +++ b/src/libslic3r/GCode/WipeTower2.cpp @@ -1571,11 +1571,11 @@ std::vector WipeTower2::prime( toolchange_Load(writer, cleaning_box); // Prime the tool. if (idx_tool + 1 == tools.size()) { // Last tool should not be unloaded, but it should be wiped enough to become of a pure color. - toolchange_Wipe(writer, cleaning_box, wipe_volumes[tools[idx_tool-1]][tool], false); + toolchange_Wipe(writer, cleaning_box, wipe_volumes[tools[idx_tool-1]][tool], false, true); } else { // Ram the hot material out of the melt zone, retract the filament into the cooling tubes and let it cool. //writer.travel(writer.x(), writer.y() + m_perimeter_width, 7200); - toolchange_Wipe(writer, cleaning_box , 20.f, false); + toolchange_Wipe(writer, cleaning_box , 20.f, false, true); WipeTower::box_coordinates box = cleaning_box; box.translate(0.f, writer.y() - cleaning_box.ld.y() + m_perimeter_width); toolchange_Unload(writer, box , m_filpar[m_current_tool].material, m_filpar[m_current_tool].first_layer_temperature, m_filpar[tools[idx_tool + 1]].first_layer_temperature); @@ -2013,7 +2013,8 @@ void WipeTower2::toolchange_Wipe( WipeTowerWriter2 &writer, const WipeTower::box_coordinates &cleaning_box, float wipe_volume, - bool interface_layer) + bool interface_layer, + bool priming) { // Increase flow on first layer, slow down print. writer.set_extrusion_flow(m_extrusion_flow * (is_first_layer() ? 1.18f : 1.f)) @@ -2056,6 +2057,26 @@ void WipeTower2::toolchange_Wipe( } float traversed_x = writer.x(); + + // BBS gap wall: iron the first few mm of the purge, then drag the retracted nozzle + // back out through the wall gap so the toolchange start blob is not left on the wall. + // WT2's entry gap always sits at the left-edge entry point, so only iron when the + // purge actually starts there heading right (in-place toolchangers do; SEMM + // ram/cooling moves leave the nozzle mid-box, far from any gap). + if (i == 0 && m_use_gap_wall && !interface_layer && !priming && m_left_to_right && + writer.x() - xl < 2.5f * line_width) { + float ironing_length = 3.f; + if (xr - writer.x() < ironing_length) + ironing_length = std::max(xr - writer.x(), 0.f); + const float retract_length = m_filpar[m_current_tool].retract_length; + const float retract_speed = m_filpar[m_current_tool].retract_speed * 60.f; + writer.extrude(writer.x() + ironing_length, writer.y(), wipe_speed); + writer.retract(retract_length, retract_speed); + writer.travel(writer.x() - 1.5f * ironing_length, writer.y(), 600.f); + writer.travel(writer.x() + 1.5f * ironing_length, writer.y(), 240.f); + writer.retract(-retract_length, retract_speed); + } + if (m_left_to_right) writer.extrude(xr - (i % 4 == 0 ? 0 : 1.5f*line_width), writer.y(), wipe_speed); else diff --git a/src/libslic3r/GCode/WipeTower2.hpp b/src/libslic3r/GCode/WipeTower2.hpp index 12e6573d73..855434d863 100644 --- a/src/libslic3r/GCode/WipeTower2.hpp +++ b/src/libslic3r/GCode/WipeTower2.hpp @@ -361,7 +361,8 @@ private: WipeTowerWriter2 &writer, const WipeTower::box_coordinates &cleaning_box, float wipe_volume, - bool interface_layer); + bool interface_layer, + bool priming = false); Polygon generate_support_rib_wall(WipeTowerWriter2& writer, From 9292db2f9fd229df7e963693f202e6587bc9dbca Mon Sep 17 00:00:00 2001 From: SoftFever Date: Wed, 29 Jul 2026 13:23:29 +0800 Subject: [PATCH 09/16] Reserve WipeTower2 toolchange depth to match the printed purge The planner reserved ramming rows gated only on enable_filament_ramming and sized them with the SEMM 0.25s time step, while toolchange_Unload rams on (semm && enable_filament_ramming) || filament_multitool_ramming with the multitool time step. Disabling multitool ramming therefore left ~3 unprinted rows per toolchange as blank bands in the tower. Without ramming the first wipe line also needs reserved depth of its own (it no longer rides the last ramming row), plus the y_step/2 offset the wipe start inherits from the ramming start position - otherwise the tightened boxes truncate the ordered purge at the box edge. --- src/libslic3r/GCode/WipeTower2.cpp | 28 ++++++++++++++++++++++------ src/libslic3r/GCode/WipeTower2.hpp | 5 +++++ 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/src/libslic3r/GCode/WipeTower2.cpp b/src/libslic3r/GCode/WipeTower2.cpp index c1d46e11c4..82beef263f 100644 --- a/src/libslic3r/GCode/WipeTower2.cpp +++ b/src/libslic3r/GCode/WipeTower2.cpp @@ -2346,15 +2346,29 @@ void WipeTower2::plan_toolchange(float z_par, float layer_height_par, unsigned i m_plan.back().tool_changes.push_back(set_toolchange(old_tool, new_tool, layer_height_par, wipe_volume, first_layer_plan)); } +float WipeTower2::wipe_start_depth_offset(size_t old_tool) const +{ + const bool do_ramming = (m_semm && m_enable_filament_ramming) || m_filpar[old_tool].multitool_ramming; + if (do_ramming) + return 0.f; + return 0.5f * m_perimeter_width * m_filpar[old_tool].ramming_line_width_multiplicator * m_filpar[old_tool].ramming_step_multiplicator * m_extra_spacing_ramming; +} + WipeTower2::WipeTowerInfo::ToolChange WipeTower2::set_toolchange(size_t old_tool, size_t new_tool, float layer_height, float wipe_volume, bool first_layer_plan) { float width = m_wipe_tower_width - 3*m_perimeter_width; - float length_to_extrude = volume_to_length(0.25f * std::accumulate(m_filpar[old_tool].ramming_speed.begin(), m_filpar[old_tool].ramming_speed.end(), 0.f), + float length_to_extrude = volume_to_length((m_semm ? 0.25f : m_filpar[old_tool].multitool_ramming_time) * std::accumulate(m_filpar[old_tool].ramming_speed.begin(), m_filpar[old_tool].ramming_speed.end(), 0.f), m_perimeter_width * m_filpar[old_tool].ramming_line_width_multiplicator, layer_height); - // Orca: Set ramming depth to 0 if ramming is disabled. - float ramming_depth = m_enable_filament_ramming ? ((int(length_to_extrude / width) + 1) * (m_perimeter_width * m_filpar[old_tool].ramming_line_width_multiplicator * m_filpar[old_tool].ramming_step_multiplicator) * m_extra_spacing_ramming) : 0; - float first_wipe_line = - (width*((length_to_extrude / width)-int(length_to_extrude / width)) - width); + // Orca: Reserve ramming depth only when toolchange_Unload() will actually ram + // (same condition as its do_ramming), otherwise the unprinted reservation + // leaves blank bands between the purge boxes. + const bool do_ramming = (m_semm && m_enable_filament_ramming) || m_filpar[old_tool].multitool_ramming; + float ramming_depth = do_ramming ? ((int(length_to_extrude / width) + 1) * (m_perimeter_width * m_filpar[old_tool].ramming_line_width_multiplicator * m_filpar[old_tool].ramming_step_multiplicator) * m_extra_spacing_ramming) : 0; + // first_wipe_line rides for free on the last (partially used) ramming row, which + // is already covered by ramming_depth. Without ramming that row does not exist, + // so the whole wipe volume needs reserved wiping depth. + float first_wipe_line = do_ramming ? - (width*((length_to_extrude / width)-int(length_to_extrude / width)) - width) : 0.f; float first_wipe_volume = length_to_volume(first_wipe_line, m_perimeter_width * m_extra_flow, layer_height); @@ -2367,7 +2381,8 @@ WipeTower2::WipeTowerInfo::ToolChange WipeTower2::set_toolchange(size_t old_tool // ORCA: and first-layer purge segments do not leave visible gaps. const float planning_spacing = first_layer_plan ? m_extra_flow : m_extra_spacing_wipe; - float wiping_depth = get_wipe_depth(wipe_volume - first_wipe_volume, layer_height, m_perimeter_width, m_extra_flow, planning_spacing, width); + float wiping_depth = get_wipe_depth(wipe_volume - first_wipe_volume, layer_height, m_perimeter_width, m_extra_flow, planning_spacing, width) + + wipe_start_depth_offset(old_tool); return WipeTowerInfo::ToolChange(old_tool, new_tool, ramming_depth + wiping_depth, ramming_depth, first_wipe_line, wipe_volume); } @@ -2429,7 +2444,8 @@ void WipeTower2::save_on_last_wipe() const bool first_layer_plan = size_t(m_layer_info - m_plan.begin()) == m_first_layer_idx; const float planning_spacing = first_layer_plan ? m_extra_flow : m_extra_spacing_wipe; - float depth_to_wipe = get_wipe_depth(volume_we_need_depth_for, m_layer_info->height, m_perimeter_width, m_extra_flow, planning_spacing, width); + float depth_to_wipe = get_wipe_depth(volume_we_need_depth_for, m_layer_info->height, m_perimeter_width, m_extra_flow, planning_spacing, width) + + wipe_start_depth_offset(toolchange.old_tool); toolchange.required_depth = toolchange.ramming_depth + depth_to_wipe; toolchange.wipe_volume = volume_left_to_wipe; diff --git a/src/libslic3r/GCode/WipeTower2.hpp b/src/libslic3r/GCode/WipeTower2.hpp index 855434d863..627204ce28 100644 --- a/src/libslic3r/GCode/WipeTower2.hpp +++ b/src/libslic3r/GCode/WipeTower2.hpp @@ -387,6 +387,11 @@ private: // Computes the depth reserved for a toolchange (shared by plan_toolchange() and the // rib-wall square-tower replanning in generate()). WipeTowerInfo::ToolChange set_toolchange(size_t old_tool, size_t new_tool, float layer_height, float wipe_volume, bool first_layer_plan); + + // Depth consumed before the first wipe row when no ramming is printed (the wipe + // still starts y_step/2 into the box, see toolchange_Unload()); 0 with ramming, + // whose planned rows already carry the slack for it. + float wipe_start_depth_offset(size_t old_tool) const; }; From 36bd453ac8f0fe01a5ea72bac2779264d898d0e5 Mon Sep 17 00:00:00 2001 From: SoftFever Date: Wed, 29 Jul 2026 16:37:06 +0800 Subject: [PATCH 10/16] Tile WipeTower2 purge rows contiguously across toolchange blocks Without ramming, each purge block reserved one wipe pitch more than its rows occupy (ceil+1 rounding plus the ram-geometry start offset), and the wipe began a full pitch inside the block, leaving a blank band of exactly two pitches between adjacent blocks. Plan the block as whole wipe rows, start the first row so the row lattice continues across the block boundary, and fill the reserved box instead of stopping at the ordered volume, mirroring how the BBL WipeTower keeps planned depth identical to printed rows. Ram-printing toolchanges (SEMM with ramming enabled, multitool ramming) are unchanged. --- src/libslic3r/GCode/WipeTower2.cpp | 43 ++++++++++++++++++------------ src/libslic3r/GCode/WipeTower2.hpp | 8 ++---- 2 files changed, 28 insertions(+), 23 deletions(-) diff --git a/src/libslic3r/GCode/WipeTower2.cpp b/src/libslic3r/GCode/WipeTower2.cpp index 82beef263f..487a7d6c1c 100644 --- a/src/libslic3r/GCode/WipeTower2.cpp +++ b/src/libslic3r/GCode/WipeTower2.cpp @@ -1673,6 +1673,11 @@ WipeTower::ToolChangeResult WipeTower2::tool_change(size_t tool) // Ram the hot material out of the melt zone, retract the filament into the cooling tubes and let it cool. if (tool != (unsigned int)-1){ // This is not the last change. + // Without a ram the box was planned as whole wipe rows; the wipe then fills it + // completely so adjacent purge blocks stay contiguous. Uses the old tool + // (m_current_tool before toolchange_Change), same condition as + // toolchange_Unload()'s do_ramming. + const bool fill_box = !((m_semm && m_enable_filament_ramming) || m_filpar[m_current_tool].multitool_ramming); auto new_tool_temp = is_first_layer() ? m_filpar[tool].first_layer_temperature : m_filpar[tool].temperature; toolchange_Unload(writer, cleaning_box, m_filpar[m_current_tool].material, (is_first_layer() ? m_filpar[m_current_tool].first_layer_temperature : m_filpar[m_current_tool].temperature), @@ -1695,7 +1700,7 @@ WipeTower::ToolChangeResult WipeTower2::tool_change(size_t tool) writer.extrude_explicit(target_x, writer.y(), pre_len, 600.f); } } - toolchange_Wipe(writer, cleaning_box, wipe_volume, interface_layer); // Wipe the newly loaded filament until the end of the assigned wipe area. + toolchange_Wipe(writer, cleaning_box, wipe_volume, interface_layer, false, fill_box); // Wipe the newly loaded filament until the end of the assigned wipe area. if (interface_layer) { int interface_temp = m_filpar[tool].interface_print_temperature; if (!m_enable_tower_interface_cooldown_during_tower && interface_temp > 0 && interface_temp != base_temp) @@ -1933,8 +1938,18 @@ void WipeTower2::toolchange_Unload( Vec2f pos = Vec2f(end_of_ramming.x(), end_of_ramming.y() + (y_step/m_extra_spacing_ramming-m_perimeter_width) / 2.f + m_perimeter_width); if (do_ramming) writer.travel(pos, 2400.f); - else - writer.set_position(pos); + else { + // Orca: with no ram printed there is no ramming geometry to align with. Start the + // first wipe row so the purge row lattice continues across the block boundary + // (previous box's last row top edge sits at its box top): with the planned depth + // of rows * dy, the last row's top edge then lands exactly on this box's top and + // no blank band is left between adjacent purge blocks. Mirrors dy/line_width in + // toolchange_Wipe(). + const float wipe_dy = (is_first_layer() ? m_extra_flow : m_extra_spacing_wipe) * m_perimeter_width; + const float wipe_line_width = m_perimeter_width * m_extra_flow; + writer.set_position(Vec2f(end_of_ramming.x(), + cleaning_box.ld.y() + m_depth_traversed + wipe_dy - (m_perimeter_width + wipe_line_width) / 2.f + m_perimeter_width)); + } writer.resume_preview() .flush_planner_queue(); @@ -2014,7 +2029,8 @@ void WipeTower2::toolchange_Wipe( const WipeTower::box_coordinates &cleaning_box, float wipe_volume, bool interface_layer, - bool priming) + bool priming, + bool fill_box) { // Increase flow on first layer, slow down print. writer.set_extrusion_flow(m_extrusion_flow * (is_first_layer() ? 1.18f : 1.f)) @@ -2092,7 +2108,10 @@ void WipeTower2::toolchange_Wipe( traversed_x -= writer.x(); x_to_wipe -= std::abs(traversed_x); - if (x_to_wipe < WT_EPSILON) { + // Orca: with no ram printed the box was planned as whole wipe rows; fill it + // completely (quantizing the purge up to the planned rows) so the next block + // can start right above it without a blank band in between. + if (!fill_box && x_to_wipe < WT_EPSILON) { writer.travel(m_left_to_right ? xl + 1.5f*line_width : xr - 1.5f*line_width, writer.y(), 7200); break; } @@ -2346,14 +2365,6 @@ void WipeTower2::plan_toolchange(float z_par, float layer_height_par, unsigned i m_plan.back().tool_changes.push_back(set_toolchange(old_tool, new_tool, layer_height_par, wipe_volume, first_layer_plan)); } -float WipeTower2::wipe_start_depth_offset(size_t old_tool) const -{ - const bool do_ramming = (m_semm && m_enable_filament_ramming) || m_filpar[old_tool].multitool_ramming; - if (do_ramming) - return 0.f; - return 0.5f * m_perimeter_width * m_filpar[old_tool].ramming_line_width_multiplicator * m_filpar[old_tool].ramming_step_multiplicator * m_extra_spacing_ramming; -} - WipeTower2::WipeTowerInfo::ToolChange WipeTower2::set_toolchange(size_t old_tool, size_t new_tool, float layer_height, float wipe_volume, bool first_layer_plan) { float width = m_wipe_tower_width - 3*m_perimeter_width; @@ -2381,8 +2392,7 @@ WipeTower2::WipeTowerInfo::ToolChange WipeTower2::set_toolchange(size_t old_tool // ORCA: and first-layer purge segments do not leave visible gaps. const float planning_spacing = first_layer_plan ? m_extra_flow : m_extra_spacing_wipe; - float wiping_depth = get_wipe_depth(wipe_volume - first_wipe_volume, layer_height, m_perimeter_width, m_extra_flow, planning_spacing, width) - + wipe_start_depth_offset(old_tool); + float wiping_depth = get_wipe_depth(wipe_volume - first_wipe_volume, layer_height, m_perimeter_width, m_extra_flow, planning_spacing, width); return WipeTowerInfo::ToolChange(old_tool, new_tool, ramming_depth + wiping_depth, ramming_depth, first_wipe_line, wipe_volume); } @@ -2444,8 +2454,7 @@ void WipeTower2::save_on_last_wipe() const bool first_layer_plan = size_t(m_layer_info - m_plan.begin()) == m_first_layer_idx; const float planning_spacing = first_layer_plan ? m_extra_flow : m_extra_spacing_wipe; - float depth_to_wipe = get_wipe_depth(volume_we_need_depth_for, m_layer_info->height, m_perimeter_width, m_extra_flow, planning_spacing, width) - + wipe_start_depth_offset(toolchange.old_tool); + float depth_to_wipe = get_wipe_depth(volume_we_need_depth_for, m_layer_info->height, m_perimeter_width, m_extra_flow, planning_spacing, width); toolchange.required_depth = toolchange.ramming_depth + depth_to_wipe; toolchange.wipe_volume = volume_left_to_wipe; diff --git a/src/libslic3r/GCode/WipeTower2.hpp b/src/libslic3r/GCode/WipeTower2.hpp index 627204ce28..e264497f5a 100644 --- a/src/libslic3r/GCode/WipeTower2.hpp +++ b/src/libslic3r/GCode/WipeTower2.hpp @@ -362,7 +362,8 @@ private: const WipeTower::box_coordinates &cleaning_box, float wipe_volume, bool interface_layer, - bool priming = false); + bool priming = false, + bool fill_box = false); Polygon generate_support_rib_wall(WipeTowerWriter2& writer, @@ -387,11 +388,6 @@ private: // Computes the depth reserved for a toolchange (shared by plan_toolchange() and the // rib-wall square-tower replanning in generate()). WipeTowerInfo::ToolChange set_toolchange(size_t old_tool, size_t new_tool, float layer_height, float wipe_volume, bool first_layer_plan); - - // Depth consumed before the first wipe row when no ramming is printed (the wipe - // still starts y_step/2 into the box, see toolchange_Unload()); 0 with ramming, - // whose planned rows already carry the slack for it. - float wipe_start_depth_offset(size_t old_tool) const; }; From 67e1ce03a601a67b630000ab85a7fc9aebcdf7b5 Mon Sep 17 00:00:00 2001 From: SoftFever Date: Wed, 29 Jul 2026 18:48:10 +0800 Subject: [PATCH 11/16] Scrub the WipeTower2 toolchange entry with the BBL flat-ironing spiral The entry scrub now matches the BBL tower's toolchange_wipe_new sequence: after the ironing drag the retracted nozzle runs a dry expanding-square spiral centred on the wall-gap entry point before resuming the purge row. The spiral runs whenever the gap wall is on (disable per filament via filament_tower_ironing_area = 0); WipeTower2 no longer reads prime_tower_flat_ironing. --- src/libslic3r/GCode/WipeTower2.cpp | 25 ++++++++++--------------- src/libslic3r/GCode/WipeTower2.hpp | 1 - 2 files changed, 10 insertions(+), 16 deletions(-) diff --git a/src/libslic3r/GCode/WipeTower2.cpp b/src/libslic3r/GCode/WipeTower2.cpp index 487a7d6c1c..c032a1f2c2 100644 --- a/src/libslic3r/GCode/WipeTower2.cpp +++ b/src/libslic3r/GCode/WipeTower2.cpp @@ -24,7 +24,6 @@ namespace Slic3r { -static constexpr float flat_iron_area = 4.f; constexpr float flat_iron_speed = 10.f * 60.f; static const double wipe_tower_wall_infill_overlap = 0.0; static constexpr double WIPE_TOWER_RESOLUTION = 0.1; @@ -1364,7 +1363,6 @@ WipeTower2::WipeTower2(const PrintConfig& config, const PrintRegionConfig& defau m_wall_type((int)config.wipe_tower_wall_type), // The cone wall has its own fully separate generator with no gap machinery. m_use_gap_wall(config.prime_tower_skip_points.value && config.wipe_tower_wall_type.value != wtwCone), - m_flat_ironing(config.prime_tower_flat_ironing.value), m_enable_tower_interface_features(config.enable_tower_interface_features.value), m_enable_tower_interface_cooldown_during_tower(config.enable_tower_interface_cooldown_during_tower.value) { @@ -2060,9 +2058,6 @@ void WipeTower2::toolchange_Wipe( m_left_to_right = !m_left_to_right; } - const bool do_ironing = m_flat_ironing && (!interface_layer || !m_enable_tower_interface_features); - const float ironing_area = m_filpar[m_current_tool].tower_ironing_area; - // now the wiping itself: for (int i = 0; true; ++i) { if (i!=0) { @@ -2075,10 +2070,12 @@ void WipeTower2::toolchange_Wipe( float traversed_x = writer.x(); // BBS gap wall: iron the first few mm of the purge, then drag the retracted nozzle - // back out through the wall gap so the toolchange start blob is not left on the wall. - // WT2's entry gap always sits at the left-edge entry point, so only iron when the - // purge actually starts there heading right (in-place toolchangers do; SEMM - // ram/cooling moves leave the nozzle mid-box, far from any gap). + // back out through the wall gap and scrub it with a dry spiral centred on the entry + // point so the toolchange start blob is not left on the wall (same sequence as the + // BBL tower's toolchange_wipe_new; the spiral self-disables when the filament's + // tower ironing area is 0). WT2's entry gap always sits at the left-edge entry + // point, so only iron when the purge actually starts there heading right (in-place + // toolchangers do; SEMM ram/cooling moves leave the nozzle mid-box, far from any gap). if (i == 0 && m_use_gap_wall && !interface_layer && !priming && m_left_to_right && writer.x() - xl < 2.5f * line_width) { float ironing_length = 3.f; @@ -2089,7 +2086,10 @@ void WipeTower2::toolchange_Wipe( writer.extrude(writer.x() + ironing_length, writer.y(), wipe_speed); writer.retract(retract_length, retract_speed); writer.travel(writer.x() - 1.5f * ironing_length, writer.y(), 600.f); - writer.travel(writer.x() + 1.5f * ironing_length, writer.y(), 240.f); + writer.travel(writer.x() + 0.5f * ironing_length, writer.y(), 240.f); + const Vec2f iron_end(writer.x() + ironing_length, writer.y()); + writer.spiral_flat_ironing(writer.pos(), m_filpar[m_current_tool].tower_ironing_area, m_perimeter_width, flat_iron_speed); + writer.travel(iron_end, wipe_speed); writer.retract(-retract_length, retract_speed); } @@ -2098,11 +2098,6 @@ void WipeTower2::toolchange_Wipe( else writer.extrude(xl + (i % 4 == 1 ? 0 : 1.5f*line_width), writer.y(), wipe_speed); - if (i == 0 && do_ironing && ironing_area > 0.f) { - writer.travel(writer.x(), writer.y(), 600.f); - writer.spiral_flat_ironing(writer.pos(), ironing_area, m_perimeter_width, 10.f * 60.f); - } - if (writer.y()+float(EPSILON) > cleaning_box.lu.y()-0.5f*line_width) break; // in case next line would not fit diff --git a/src/libslic3r/GCode/WipeTower2.hpp b/src/libslic3r/GCode/WipeTower2.hpp index e264497f5a..46af0e426e 100644 --- a/src/libslic3r/GCode/WipeTower2.hpp +++ b/src/libslic3r/GCode/WipeTower2.hpp @@ -221,7 +221,6 @@ private: float m_perimeter_speed = 0.f; float m_first_layer_speed = 0.f; size_t m_first_layer_idx = size_t(-1); - bool m_flat_ironing = false; bool m_enable_tower_interface_features = false; bool m_enable_tower_interface_cooldown_during_tower = false; bool m_prev_layer_had_interface = false; From 086bbf986be86fa612d0ca86b1672f1a25cdca6e Mon Sep 17 00:00:00 2001 From: SoftFever Date: Wed, 29 Jul 2026 19:59:33 +0800 Subject: [PATCH 12/16] Restart the WipeTower2 wipe at the box boundary after multitool ramming With the gap wall on a multi-tool printer, quantize the ram band up to its whole reserved rows (as the BBL tower does for the old-tool purge) and start CP TOOLCHANGE WIPE at the left-edge boundary on a fresh row below it instead of continuing from wherever the ram serpentine ended. The entry scrub then runs at the wall gap on ram toolchanges too, and the wipe box is whole rows, so it is filled completely like the no-ram case. SEMM and skip-points-off behavior is unchanged. --- src/libslic3r/GCode/WipeTower2.cpp | 69 +++++++++++++++++++++++++----- 1 file changed, 59 insertions(+), 10 deletions(-) diff --git a/src/libslic3r/GCode/WipeTower2.cpp b/src/libslic3r/GCode/WipeTower2.cpp index c032a1f2c2..b846302abb 100644 --- a/src/libslic3r/GCode/WipeTower2.cpp +++ b/src/libslic3r/GCode/WipeTower2.cpp @@ -1671,11 +1671,13 @@ WipeTower::ToolChangeResult WipeTower2::tool_change(size_t tool) // Ram the hot material out of the melt zone, retract the filament into the cooling tubes and let it cool. if (tool != (unsigned int)-1){ // This is not the last change. - // Without a ram the box was planned as whole wipe rows; the wipe then fills it - // completely so adjacent purge blocks stay contiguous. Uses the old tool - // (m_current_tool before toolchange_Change), same condition as - // toolchange_Unload()'s do_ramming. - const bool fill_box = !((m_semm && m_enable_filament_ramming) || m_filpar[m_current_tool].multitool_ramming); + // Without a ram — or with the boundary wipe start, where the ram band is + // quantized to whole rows — the box is planned as whole wipe rows; the wipe + // then fills it completely so adjacent purge blocks stay contiguous. Uses the + // old tool (m_current_tool before toolchange_Change), same conditions as + // toolchange_Unload()'s do_ramming / boundary_wipe_start. + const bool do_ram_old = (m_semm && m_enable_filament_ramming) || m_filpar[m_current_tool].multitool_ramming; + const bool fill_box = !do_ram_old || (!m_semm && m_use_gap_wall); auto new_tool_temp = is_first_layer() ? m_filpar[tool].first_layer_temperature : m_filpar[tool].temperature; toolchange_Unload(writer, cleaning_box, m_filpar[m_current_tool].material, (is_first_layer() ? m_filpar[m_current_tool].first_layer_temperature : m_filpar[m_current_tool].temperature), @@ -1756,6 +1758,12 @@ void WipeTower2::toolchange_Unload( // Orca: Do ramming when SEMM and ramming is enabled or when multi tool head when ramming is enabled on the multi tool. const bool do_ramming = (m_semm && m_enable_filament_ramming) || m_filpar[m_current_tool].multitool_ramming; const bool cold_ramming = m_is_mk4mmu3; + // Orca: see set_toolchange() — quantized ram band + wipe restart at the boundary. + const bool boundary_wipe_start = do_ramming && !m_semm && m_use_gap_wall; + float planned_ramming_depth = 0.f; + if (boundary_wipe_start && m_layer_info != m_plan.end()) + for (const auto& tch : m_layer_info->tool_changes) + if (tch.old_tool == m_current_tool) { planned_ramming_depth = tch.ramming_depth; break; } if (do_ramming) { writer.travel(ramming_start_pos); // move to starting position @@ -1768,7 +1776,8 @@ void WipeTower2::toolchange_Unload( writer.set_position(ramming_start_pos); // if the ending point of the ram would end up in mid air, align it with the end of the wipe tower: - if (do_ramming && (m_layer_info > m_plan.begin() && m_layer_info < m_plan.end() && (m_layer_info-1!=m_plan.begin() || !m_adhesion ))) { + // (with a boundary wipe start the band is quantized to whole rows below, so no phase alignment is needed) + if (do_ramming && !boundary_wipe_start && (m_layer_info > m_plan.begin() && m_layer_info < m_plan.end() && (m_layer_info-1!=m_plan.begin() || !m_adhesion ))) { // this is y of the center of previous sparse infill border float sparse_beginning_y = 0.f; @@ -1827,6 +1836,28 @@ void WipeTower2::toolchange_Unload( e_done = 0; } } + + // Orca: quantize the ram band up to the whole reserved rows (BBL quantizes the + // old-tool purge the same way) so no unprinted void is left between the band and + // the wipe restarting at the boundary below it. + if (planned_ramming_depth > 0.f) { + const int reserved_rows = std::max(1, int(std::round(planned_ramming_depth / y_step))); + const float last_row_y = ramming_start_pos.y() + (reserved_rows - 1) * y_step; + // Same bead model as the ramming segments above: E per mm of ram line. + const float e_per_mm = 1.f / (volume_to_length(1.f, line_width, m_layer_height) * filament_area()); + const float fill_feed = m_filpar[m_current_tool].ramming_speed.empty() ? 3000.f : + 60.f * volume_to_length(m_filpar[m_current_tool].ramming_speed.back(), line_width, m_layer_height); + while (true) { + const float target_x = m_left_to_right ? xr : xl; + if (std::abs(target_x - writer.x()) > WT_EPSILON) + writer.ram(writer.x(), target_x, 0.f, 0.f, e_per_mm * std::abs(target_x - writer.x()), fill_feed); + if (writer.y() + 0.5f * y_step > last_row_y) + break; + writer.travel(writer.x(), writer.y() + y_step, 7200); + m_left_to_right = !m_left_to_right; + } + } + Vec2f end_of_ramming(writer.x(),writer.y()); writer.change_analyzer_line_width(m_perimeter_width); // so the next lines are not affected by ramming_line_width_multiplier @@ -1934,7 +1965,18 @@ void WipeTower2::toolchange_Unload( // this is to align ramming and future wiping extrusions, so the future y-steps can be uniform from the start: // the perimeter_width will later be subtracted, it is there to not load while moving over just extruded material Vec2f pos = Vec2f(end_of_ramming.x(), end_of_ramming.y() + (y_step/m_extra_spacing_ramming-m_perimeter_width) / 2.f + m_perimeter_width); - if (do_ramming) + if (planned_ramming_depth > 0.f) { + // Orca: restart the wipe at the left-edge boundary on a fresh row below the + // quantized ram band so the entry scrub always runs at the wall gap (BBL keeps + // CP_TOOLCHANGE_WIPE starting at a box corner the same way). Same lattice + // formula as the no-ram branch below, offset by the ram band. + const float wipe_dy = (is_first_layer() ? m_extra_flow : m_extra_spacing_wipe) * m_perimeter_width; + const float wipe_line_width = m_perimeter_width * m_extra_flow; + writer.travel(Vec2f(ramming_start_pos.x(), + cleaning_box.ld.y() + m_depth_traversed + planned_ramming_depth + wipe_dy - (m_perimeter_width + wipe_line_width) / 2.f + m_perimeter_width), 2400.f); + m_left_to_right = true; + } + else if (do_ramming) writer.travel(pos, 2400.f); else { // Orca: with no ram printed there is no ramming geometry to align with. Start the @@ -2370,11 +2412,18 @@ WipeTower2::WipeTowerInfo::ToolChange WipeTower2::set_toolchange(size_t old_tool // (same condition as its do_ramming), otherwise the unprinted reservation // leaves blank bands between the purge boxes. const bool do_ramming = (m_semm && m_enable_filament_ramming) || m_filpar[old_tool].multitool_ramming; + // Orca: with the gap wall on a multi-tool printer the ram band is quantized up to + // the whole reserved rows and the wipe restarts at the left-edge boundary on a + // fresh row below it (BBL parity: the old-tool purge is whole rows and the wipe + // always starts at the box corner, where the entry scrub runs). SEMM keeps the + // stock continue-from-ram-end behavior. Must match toolchange_Unload()/tool_change(). + const bool boundary_wipe_start = do_ramming && !m_semm && m_use_gap_wall; float ramming_depth = do_ramming ? ((int(length_to_extrude / width) + 1) * (m_perimeter_width * m_filpar[old_tool].ramming_line_width_multiplicator * m_filpar[old_tool].ramming_step_multiplicator) * m_extra_spacing_ramming) : 0; // first_wipe_line rides for free on the last (partially used) ramming row, which - // is already covered by ramming_depth. Without ramming that row does not exist, - // so the whole wipe volume needs reserved wiping depth. - float first_wipe_line = do_ramming ? - (width*((length_to_extrude / width)-int(length_to_extrude / width)) - width) : 0.f; + // is already covered by ramming_depth. Without ramming that row does not exist + // (and with a boundary wipe start the ram band is quantized to whole rows), so + // the whole wipe volume needs reserved wiping depth. + float first_wipe_line = (do_ramming && !boundary_wipe_start) ? - (width*((length_to_extrude / width)-int(length_to_extrude / width)) - width) : 0.f; float first_wipe_volume = length_to_volume(first_wipe_line, m_perimeter_width * m_extra_flow, layer_height); From 252df70ec47761a3cab7486ef058c45c7a88c501 Mon Sep 17 00:00:00 2001 From: SoftFever Date: Wed, 29 Jul 2026 20:58:21 +0800 Subject: [PATCH 13/16] Move the WipeTower2 wall gap to the wipe start row for ram toolchanges --- src/libslic3r/GCode/WipeTower2.cpp | 26 +++++++++++++++++++++----- src/libslic3r/GCode/WipeTower2.hpp | 9 +++++++++ 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/src/libslic3r/GCode/WipeTower2.cpp b/src/libslic3r/GCode/WipeTower2.cpp index b846302abb..7f51d92466 100644 --- a/src/libslic3r/GCode/WipeTower2.cpp +++ b/src/libslic3r/GCode/WipeTower2.cpp @@ -1616,8 +1616,9 @@ WipeTower::ToolChangeResult WipeTower2::tool_change(size_t tool) float wipe_area = 0.f; float wipe_volume = 0.f; + float ramming_depth = 0.f; bool interface_layer = m_enable_tower_interface_features && m_current_layer_has_interface; - + // Finds this toolchange info if (tool != (unsigned int)(-1)) { @@ -1625,6 +1626,7 @@ WipeTower::ToolChangeResult WipeTower2::tool_change(size_t tool) if ( b.new_tool == tool ) { wipe_volume = b.wipe_volume; wipe_area = b.required_depth; + ramming_depth = b.ramming_depth; break; } } @@ -1663,6 +1665,12 @@ WipeTower::ToolChangeResult WipeTower2::tool_change(size_t tool) writer.speed_override(100); Vec2f initial_position = cleaning_box.ld + Vec2f(0.f, m_depth_traversed); + // With a boundary wipe start the wall gap sits at the first wipe row below the + // quantized ram band; enter there too so the routed entry, the gap and the wipe + // scrub all share one opening. toolchange_Unload() climbs back up to the ram band + // along the box interior. + if (!m_semm && m_use_gap_wall && ramming_depth > 0.f) + initial_position.y() += wipe_start_offset_after_ram(ramming_depth, is_first_layer()); writer.set_initial_position(initial_position, m_wipe_tower_width, m_wipe_tower_depth, m_internal_rotation); // Increase the extruder driver current to allow fast ramming. @@ -1766,6 +1774,10 @@ void WipeTower2::toolchange_Unload( if (tch.old_tool == m_current_tool) { planned_ramming_depth = tch.ramming_depth; break; } if (do_ramming) { + if (boundary_wipe_start) + // The entry sits at the wall gap on the first wipe row past the reserved + // ram band; step inward first, then move to the band clear of the wall. + writer.travel(Vec2f(ramming_start_pos.x(), writer.y())); writer.travel(ramming_start_pos); // move to starting position if (! m_is_mk4mmu3) writer.disable_linear_advance(); @@ -1970,10 +1982,9 @@ void WipeTower2::toolchange_Unload( // quantized ram band so the entry scrub always runs at the wall gap (BBL keeps // CP_TOOLCHANGE_WIPE starting at a box corner the same way). Same lattice // formula as the no-ram branch below, offset by the ram band. - const float wipe_dy = (is_first_layer() ? m_extra_flow : m_extra_spacing_wipe) * m_perimeter_width; - const float wipe_line_width = m_perimeter_width * m_extra_flow; writer.travel(Vec2f(ramming_start_pos.x(), - cleaning_box.ld.y() + m_depth_traversed + planned_ramming_depth + wipe_dy - (m_perimeter_width + wipe_line_width) / 2.f + m_perimeter_width), 2400.f); + cleaning_box.ld.y() + m_depth_traversed + + wipe_start_offset_after_ram(planned_ramming_depth, is_first_layer()) + m_perimeter_width), 2400.f); m_left_to_right = true; } else if (do_ramming) @@ -2584,13 +2595,18 @@ static WipeTower::ToolChangeResult merge_tcr(WipeTower::ToolChangeResult& first, // starts: cleaning_box.ld + (0, m_depth_traversed), with m_depth_traversed advancing by // required_depth per toolchange — reproduced here from the finalized plan so each gap // coincides with the entry travel's target (tcr.start_pos, pre-rotation frame). +// With a boundary wipe start the entry, the wipe and its scrub sit on the first wipe row +// below the quantized ram band, so the gap moves there with them (BBL cuts its gap at the +// CP_TOOLCHANGE_WIPE start row too, never at the ram band). void WipeTower2::compute_wall_skip_points() { m_wall_skip_points.assign(m_plan.size(), std::vector()); for (size_t layer_id = 0; layer_id < m_plan.size(); ++layer_id) { float depth_traversed = 0.f; for (const auto& toolchange : m_plan[layer_id].tool_changes) { - m_wall_skip_points[layer_id].emplace_back(m_perimeter_width / 2.f, m_perimeter_width / 2.f + depth_traversed); + const float ram_offset = (!m_semm && toolchange.ramming_depth > 0.f) ? + wipe_start_offset_after_ram(toolchange.ramming_depth, layer_id == m_first_layer_idx) : 0.f; + m_wall_skip_points[layer_id].emplace_back(m_perimeter_width / 2.f, m_perimeter_width / 2.f + depth_traversed + ram_offset); depth_traversed += toolchange.required_depth; } } diff --git a/src/libslic3r/GCode/WipeTower2.hpp b/src/libslic3r/GCode/WipeTower2.hpp index 46af0e426e..439515cdd4 100644 --- a/src/libslic3r/GCode/WipeTower2.hpp +++ b/src/libslic3r/GCode/WipeTower2.hpp @@ -284,6 +284,15 @@ private: bool is_first_layer() const { return size_t(m_layer_info - m_plan.begin()) == m_first_layer_idx; } + // With a boundary wipe start (multitool ram, non-SEMM, gap wall) the wipe begins on a + // fresh row below the quantized ram band. Y offset from the box start to that first + // wipe row; must stay in sync with the alignment travel in toolchange_Unload(). + float wipe_start_offset_after_ram(float ramming_depth, bool first_layer) const + { + const float wipe_dy = (first_layer ? m_extra_flow : m_extra_spacing_wipe) * m_perimeter_width; + return ramming_depth + wipe_dy - (m_perimeter_width + m_perimeter_width * m_extra_flow) / 2.f; + } + // Calculates extrusion flow needed to produce required line width for given layer height float extrusion_flow(float layer_height = -1.f) const // negative layer_height - return current m_extrusion_flow { From 3ab9cf53d096838c7f9dde66163e20c0d01fa670 Mon Sep 17 00:00:00 2001 From: SoftFever Date: Wed, 29 Jul 2026 23:46:42 +0800 Subject: [PATCH 14/16] code cleanup --- src/libslic3r/GCode.cpp | 102 ++++--- src/libslic3r/GCode.hpp | 2 + src/libslic3r/GCode/WipeTower.hpp | 5 + src/libslic3r/GCode/WipeTower2.cpp | 417 +++-------------------------- src/libslic3r/GCode/WipeTower2.hpp | 39 ++- 5 files changed, 120 insertions(+), 445 deletions(-) diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index a93899036f..53c9733a9b 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -13,6 +13,7 @@ #include "GCode/PrintExtents.hpp" #include "GCode/Thumbnails.hpp" #include "GCode/WipeTower.hpp" +#include "GCode/WipeTower2.hpp" #include "ShortestPath.hpp" #include "Print.hpp" #include "Utils.hpp" @@ -888,18 +889,22 @@ static std::vector get_path_of_change_filament(const Print& print) return res; } - // With skip points enabled the Type2 tower wall has an opening at each toolchange's - // entry (tcr.start_pos): route the approach around the tower's bounding box so the - // nozzle enters through that opening instead of dragging across the printed wall - // (append_tcr parity). Emits only the waypoints leading up to the opening — the - // caller still travels to start_wipe_pos itself. Returns an empty string when the - // option is off, the cone wall is active (it has no gap machinery), or the approach - // already starts inside the tower: such hops never cross the wall and must stay direct. - std::string WipeTowerIntegration::travel_to_tower_gap(GCode &gcodegen, const Point &route_start, const Point &start_wipe_pos) const + // Type2 tower-local point -> bed frame. The rib-wall offset is tower-local, so it + // rotates with the tower (unlike the BBL tower in append_tcr, which never rotates). + Vec2f WipeTowerIntegration::transform_wt2_pt(const Vec2f &pt) const + { + const float alpha = m_wipe_tower_rotation / 180.f * float(M_PI); + return Eigen::Rotation2Df(alpha) * (pt + m_rib_offset) + m_wipe_tower_pos; + } + + // Printable-area bounds for tower-approach routing, in object coordinates (shared by + // the BBL avoid-perimeter path in append_tcr and the Type2 skip-points router). + // Multi-nozzle: clamp the travel bounds to the region every extruder can reach + // (get_extruder_shared_printable_polygon) instead of the full bed. Gated on the + // multi-nozzle predicate so every existing single/dual printer keeps the historic + // full-printable_area routing byte-identical. + BoundingBox WipeTowerIntegration::printer_travel_bounds(GCode &gcodegen) const { - if (!gcodegen.m_config.prime_tower_skip_points.value - || gcodegen.m_config.wipe_tower_wall_type.value == WipeTowerWallType::wtwCone) - return {}; const Vec2f plate_origin_2d(m_plate_origin(0), m_plate_origin(1)); BoundingBox printer_bbx; if (is_multi_nozzle_printer(gcodegen.m_config)) { @@ -912,19 +917,30 @@ static std::vector get_path_of_change_filament(const Print& print) bed_points.push_back(wipe_tower_point_to_object_point(gcodegen, p.cast() + plate_origin_2d)); printer_bbx = BoundingBox(bed_points); } - // Transform the tower-local bbx corners exactly like the tcr points (rib - // offset, rotation, tower position); a rotated tower gets a conservative - // axis-aligned envelope. - const float alpha = m_wipe_tower_rotation / 180.f * float(M_PI); - Polygon avoid_points = scaled(m_wipe_tower_bbx).polygon(); - for (auto& p : avoid_points.points) { - Vec2f pp = Eigen::Rotation2Df(alpha) * (unscale(p).cast() + m_rib_offset) + m_wipe_tower_pos; - p = wipe_tower_point_to_object_point(gcodegen, pp + plate_origin_2d); - } + return printer_bbx; + } + + // With skip points enabled the Type2 tower wall has an opening at each toolchange's + // entry (tcr.start_pos): route the approach around the tower's bounding box so the + // nozzle enters through that opening instead of dragging across the printed wall + // (append_tcr parity). Emits only the waypoints leading up to the opening — the + // caller still travels to start_wipe_pos itself. Returns an empty string when the + // gap wall is off (option off or cone wall) or the approach already starts inside + // the tower: such hops never cross the wall and must stay direct. + std::string WipeTowerIntegration::travel_to_tower_gap(GCode &gcodegen, const Point &route_start, const Point &start_wipe_pos) const + { + if (!WipeTower2::use_gap_wall(gcodegen.m_config)) + return {}; + const Vec2f plate_origin_2d(m_plate_origin(0), m_plate_origin(1)); + // Transform the tower-local bbx corners exactly like the tcr points; a rotated + // tower gets a conservative axis-aligned envelope. + Polygon avoid_points = scaled(m_wipe_tower_bbx).polygon(); + for (auto& p : avoid_points.points) + p = wipe_tower_point_to_object_point(gcodegen, transform_wt2_pt(unscale(p).cast()) + plate_origin_2d); BoundingBox avoid_bbx(avoid_points.points); if (avoid_bbx.contains(route_start)) return {}; - Polyline travel_polyline = generate_path_to_wipe_tower(route_start, start_wipe_pos, avoid_bbx, printer_bbx); + Polyline travel_polyline = generate_path_to_wipe_tower(route_start, start_wipe_pos, avoid_bbx, printer_travel_bounds(gcodegen)); std::string gcode; // The polyline's last point is start_wipe_pos itself — emitted by the caller. for (size_t i = 0; i + 1 < travel_polyline.points.size(); ++i) @@ -1305,24 +1321,7 @@ static std::vector get_path_of_change_filament(const Print& print) Vec2f gcode_last_pos2d{gcode_last_pos[0], gcode_last_pos[1]}; Point gcode_last_pos2d_object = gcodegen.gcode_to_point(gcode_last_pos2d.cast() + plate_origin_2d.cast()); Point start_wipe_pos = wipe_tower_point_to_object_point(gcodegen, tool_change_start_pos + plate_origin_2d); - BoundingBox avoid_bbx, printer_bbx; - { - // set printer_bbx - // Multi-nozzle: clamp the avoid-perimeter travel bounds to the region every - // extruder can reach (get_extruder_shared_printable_polygon) instead of the full - // bed. Gated on the multi-nozzle predicate so H2D and every existing single/dual - // printer keep the historic full-printable_area routing byte-identical. - if (is_multi_nozzle_printer(gcodegen.m_config)) { - printer_bbx = get_extents(gcodegen.m_print->get_extruder_shared_printable_polygon()); - printer_bbx.min = wipe_tower_point_to_object_point(gcodegen, unscaled(printer_bbx.min) + plate_origin_2d); - printer_bbx.max = wipe_tower_point_to_object_point(gcodegen, unscaled(printer_bbx.max) + plate_origin_2d); - } else { - Pointfs bed_pointsf = gcodegen.m_config.printable_area.values; - Points bed_points; - for (auto p : bed_pointsf) { bed_points.push_back(wipe_tower_point_to_object_point(gcodegen, p.cast() + plate_origin_2d)); } - printer_bbx = BoundingBox(bed_points); - } - } + BoundingBox avoid_bbx, printer_bbx = printer_travel_bounds(gcodegen); { // set avoid_bbx avoid_bbx = scaled(m_wipe_tower_bbx); @@ -1466,19 +1465,13 @@ static std::vector get_path_of_change_filament(const Print& print) // We want to rotate and shift all extrusions (gcode postprocessing) and starting and ending position float alpha = m_wipe_tower_rotation / 180.f * float(M_PI); - // The rib-wall offset is tower-local, so it rotates with the tower (unlike the BBL - // tower in append_tcr, which never rotates). Priming lines are absolute bed moves. - auto transform_wt_pt = [&alpha, this](const Vec2f &pt) -> Vec2f { - Vec2f out = Eigen::Rotation2Df(alpha) * (pt + m_rib_offset); - out += m_wipe_tower_pos; - return out; - }; - + // Priming lines are absolute bed moves; everything else is tower-local + // (transform_wt2_pt). Vec2f start_pos = tcr.start_pos; Vec2f end_pos = tcr.end_pos; if (!tcr.priming) { - start_pos = transform_wt_pt(start_pos); - end_pos = transform_wt_pt(end_pos); + start_pos = transform_wt2_pt(start_pos); + end_pos = transform_wt2_pt(end_pos); } Vec2f wipe_tower_offset = tcr.priming ? Vec2f::Zero() : Vec2f(m_wipe_tower_pos + Eigen::Rotation2Df(alpha) * m_rib_offset); @@ -1511,13 +1504,13 @@ static std::vector get_path_of_change_filament(const Print& print) || is_ramming || tool_change_on_wipe_tower); + const Point start_wipe_pos = wipe_tower_point_to_object_point(gcodegen, start_pos + plate_origin_2d); const bool travel_to_tower_now = should_travel_to_tower || gcodegen.m_need_change_layer_lift_z; if (travel_to_tower_now) { // FIXME: It would be better if the wipe tower set the force_travel flag for all toolchanges, // then we could simplify the condition and make it more readable. gcode += gcodegen.retract(); gcodegen.m_avoid_crossing_perimeters.use_external_mp_once(); - const Point start_wipe_pos = wipe_tower_point_to_object_point(gcodegen, start_pos + plate_origin_2d); if (!tcr.priming && gcodegen.last_pos_defined()) gcode += travel_to_tower_gap(gcodegen, gcodegen.last_pos(), start_wipe_pos); gcode += gcodegen.travel_to(start_wipe_pos, erMixed, "Travel to a Wipe Tower"); @@ -1549,9 +1542,7 @@ static std::vector get_path_of_change_filament(const Print& print) toolchange_temp_override = interface_temp; } toolchange_gcode_str = gcodegen.set_extruder(new_extruder_id, tcr.print_z, false, toolchange_temp_override); // TODO: toolchange_z vs print_z - if (!travel_to_tower_now && !tcr.priming && needs_toolchange - && gcodegen.m_config.prime_tower_skip_points.value - && gcodegen.m_config.wipe_tower_wall_type.value != WipeTowerWallType::wtwCone) { + if (!travel_to_tower_now && !tcr.priming && WipeTower2::use_gap_wall(gcodegen.m_config)) { // The tool changed in place (multi-tool printer without ramming), so the // tower entry is the tcr's own positioning move — a straight line across // the printed wall. Route it around the tower and in through the wall @@ -1574,8 +1565,7 @@ static std::vector get_path_of_change_filament(const Print& print) if (have_start) { gcodegen.set_last_pos(route_start); gcodegen.m_avoid_crossing_perimeters.use_external_mp_once(); - const Point start_wipe_pos = wipe_tower_point_to_object_point(gcodegen, start_pos + plate_origin_2d); - std::string travel = travel_to_tower_gap(gcodegen, route_start, start_wipe_pos); + std::string travel = travel_to_tower_gap(gcodegen, route_start, start_wipe_pos); travel += gcodegen.travel_to(start_wipe_pos, erMixed, "Travel to a Wipe Tower"); check_add_eol(travel); toolchange_gcode_str += travel; @@ -1767,7 +1757,7 @@ static std::vector get_path_of_change_filament(const Print& print) // Prepare a future wipe. gcodegen.m_wipe.reset_path(); for (const Vec2f& wipe_pt : tcr.wipe_path) - gcodegen.m_wipe.path.points.emplace_back(wipe_tower_point_to_object_point(gcodegen, transform_wt_pt(wipe_pt) + plate_origin_2d)); + gcodegen.m_wipe.path.points.emplace_back(wipe_tower_point_to_object_point(gcodegen, transform_wt2_pt(wipe_pt) + plate_origin_2d)); } // Let the planner know we are traveling between objects. diff --git a/src/libslic3r/GCode.hpp b/src/libslic3r/GCode.hpp index bda9cc43c0..2d2ff5bc27 100644 --- a/src/libslic3r/GCode.hpp +++ b/src/libslic3r/GCode.hpp @@ -133,6 +133,8 @@ private: Polyline generate_path_to_wipe_tower(const Point &start_pos, const Point &end_pos, const BoundingBox &avoid_polygon, const BoundingBox &printer_bbx) const; std::string append_tcr2(GCode &gcodegen, const WipeTower::ToolChangeResult &tcr, int new_extruder_id, double z = -1.) const; std::string travel_to_tower_gap(GCode &gcodegen, const Point &route_start, const Point &start_wipe_pos) const; + Vec2f transform_wt2_pt(const Vec2f &pt) const; + BoundingBox printer_travel_bounds(GCode &gcodegen) const; // Postprocesses gcode: rotates and moves G1 extrusions and returns result std::string post_process_wipe_tower_moves(const WipeTower::ToolChangeResult& tcr, const Vec2f& translation, float angle) const; diff --git a/src/libslic3r/GCode/WipeTower.hpp b/src/libslic3r/GCode/WipeTower.hpp index 07f9a8f269..3dbbf03ffb 100644 --- a/src/libslic3r/GCode/WipeTower.hpp +++ b/src/libslic3r/GCode/WipeTower.hpp @@ -20,6 +20,11 @@ class WipeTowerWriter; class PrintConfig; enum GCodeFlavor : unsigned char; +// Cuts the tower wall polygon open at each skip point (a toolchange's entry position) +// so the entry travel can pass through instead of crossing the printed wall. Defined in +// WipeTower.cpp, shared by WipeTower and WipeTower2. +Polylines contrust_gap_for_skip_points( + const Polygon& polygon, const std::vector& skip_points, float wt_width, float gap_length, Polygon& insert_skip_polygon); class WipeTower { diff --git a/src/libslic3r/GCode/WipeTower2.cpp b/src/libslic3r/GCode/WipeTower2.cpp index 7f51d92466..e3e5f075c1 100644 --- a/src/libslic3r/GCode/WipeTower2.cpp +++ b/src/libslic3r/GCode/WipeTower2.cpp @@ -233,24 +233,6 @@ static Polygon rounding_rectangle(Polygon& polygon, double rounding = 2., double return res; } -static std::pair ray_intersetion_line(const Vec2f& a, const Vec2f& v1, const Vec2f& b, const Vec2f& c) -{ - const Vec2f v2 = c - b; - double denom = cross2(v1, v2); - if (fabs(denom) < EPSILON) - return {false, Vec2f(0, 0)}; - const Vec2f v12 = (a - b); - double nume_a = cross2(v2, v12); - double nume_b = cross2(v1, v12); - double t1 = nume_a / denom; - double t2 = nume_b / denom; - if (t1 >= 0 && t2 >= 0 && t2 <= 1.) { - // Get the intersection point. - Vec2f res = a + t1 * v1; - return std::pair(true, res); - } - return std::pair(false, Vec2f{0, 0}); -} static Polygon scale_polygon(const std::vector& points) { Polygon res; @@ -295,6 +277,7 @@ static Polygon generate_rectange(const Line& line, coord_t offset) return poly; }; +// Straight or arc-fitted wall segment used by WipeTowerWriter2::generate_path(). struct Segment { Vec2f start; @@ -305,324 +288,6 @@ struct Segment bool is_valid() const { return start.y() < end.y(); } }; -static std::vector remove_points_from_segment(const Segment& segment, const std::vector& skip_points, double range) -{ - std::vector result; - result.push_back(segment); - float x = segment.start.x(); - - for (const Vec2f& point : skip_points) { - std::vector newResult; - for (const auto& seg : result) { - if (point.y() + range <= seg.start.y() || point.y() - range >= seg.end.y()) { - newResult.push_back(seg); - } else { - if (point.y() - range > seg.start.y()) { - newResult.push_back(Segment(Vec2f(x, seg.start.y()), Vec2f(x, point.y() - range))); - } - if (point.y() + range < seg.end.y()) { - newResult.push_back(Segment(Vec2f(x, point.y() + range), Vec2f(x, seg.end.y()))); - } - } - } - - result = newResult; - } - - result.erase(std::remove_if(result.begin(), result.end(), [](const Segment& seg) { return !seg.is_valid(); }), result.end()); - return result; -} - -struct IntersectionInfo -{ - Vec2f pos; - int idx; - int pair_idx; // gap_pair idx - float dis_from_idx; - bool is_forward; -}; - -struct PointWithFlag -{ - Vec2f pos; - int pair_idx; // gap_pair idx - bool is_forward; -}; -static IntersectionInfo move_point_along_polygon( - const std::vector& points, const Vec2f& startPoint, int startIdx, float offset, bool forward, int pair_idx) -{ - float remainingDistance = offset; - IntersectionInfo res; - int mod = points.size(); - if (forward) { - int next = (startIdx + 1) % mod; - remainingDistance -= (points[next] - startPoint).norm(); - if (remainingDistance <= 0) { - res.idx = startIdx; - res.pos = startPoint + (points[next] - startPoint).normalized() * offset; - res.pair_idx = pair_idx; - res.dis_from_idx = (points[startIdx] - res.pos).norm(); - return res; - } else { - for (int i = (startIdx + 1) % mod; i != startIdx; i = (i + 1) % mod) { - float segmentLength = (points[(i + 1) % mod] - points[i]).norm(); - if (remainingDistance <= segmentLength) { - float ratio = remainingDistance / segmentLength; - res.idx = i; - res.pos = points[i] + ratio * (points[(i + 1) % mod] - points[i]); - res.dis_from_idx = remainingDistance; - res.pair_idx = pair_idx; - return res; - } - remainingDistance -= segmentLength; - } - res.idx = (startIdx - 1 + mod) % mod; - res.pos = points[startIdx]; - res.pair_idx = pair_idx; - res.dis_from_idx = (res.pos - points[res.idx]).norm(); - } - } else { - int next = (startIdx + 1) % mod; - remainingDistance -= (points[startIdx] - startPoint).norm(); - if (remainingDistance <= 0) { - res.idx = startIdx; - res.pos = startPoint - (points[next] - points[startIdx]).normalized() * offset; - res.dis_from_idx = (res.pos - points[startIdx]).norm(); - res.pair_idx = pair_idx; - return res; - } - for (int i = (startIdx - 1 + mod) % mod; i != startIdx; i = (i - 1 + mod) % mod) { - float segmentLength = (points[(i + 1) % mod] - points[i]).norm(); - if (remainingDistance <= segmentLength) { - float ratio = remainingDistance / segmentLength; - res.idx = i; - res.pos = points[(i + 1) % mod] - ratio * (points[(i + 1) % mod] - points[i]); - res.dis_from_idx = segmentLength - remainingDistance; - res.pair_idx = pair_idx; - return res; - } - remainingDistance -= segmentLength; - } - res.idx = startIdx; - res.pos = points[res.idx]; - res.pair_idx = pair_idx; - res.dis_from_idx = 0; - } - return res; -}; - -static void insert_points(std::vector& pl, int idx, Vec2f pos, int pair_idx, bool is_forward) -{ - int next = (idx + 1) % pl.size(); - Vec2f pos1 = pl[idx].pos; - Vec2f pos2 = pl[next].pos; - if ((pos - pos1).squaredNorm() < EPSILON) { - pl[idx].pair_idx = pair_idx; - pl[idx].is_forward = is_forward; - } else if ((pos - pos2).squaredNorm() < EPSILON) { - pl[next].pair_idx = pair_idx; - pl[next].is_forward = is_forward; - } else { - pl.insert(pl.begin() + idx + 1, PointWithFlag{pos, pair_idx, is_forward}); - } -} - -// For skip_point -// TODO: Optimize the skip_point algorithm itself instead of adding guards here -static Polygon add_extra_point(const Polygon& polygon, int scale_range) -{ - Polygon res; - if (polygon.size() < 2) return polygon; - - // Compute bounding box of the polygon - auto polygon_box = get_extents(polygon); - - // Anchor point: X at bbox center, Y at bbox bottom - Vec2f anchor_point(float(polygon_box.center()[0]), float(polygon_box.min[1])); - - // Find the edge whose midpoint is closest to the anchor point - size_t closest_edge_idx = 0; - float min_dist_sq = std::numeric_limits::max(); - - for (size_t i = 0; i < polygon.size(); ++i) { - const Point &a_i = polygon[i]; - const Point &b_i = polygon[(i + 1) % polygon.size()]; - - Vec2f a(float(a_i.x()), float(a_i.y())); - Vec2f b(float(b_i.x()), float(b_i.y())); - Vec2f mid = (a + b) * 0.5f; - - float dist_sq = (anchor_point - mid).squaredNorm(); - if (dist_sq < min_dist_sq) { - min_dist_sq = dist_sq; - closest_edge_idx = i; - } - } - - // Edge endpoints (integer space) - const Point &a_i = polygon[closest_edge_idx]; - const Point &b_i = polygon[(closest_edge_idx + 1) % polygon.size()]; - - // Convert to float for geometric computation - Vec2f a(float(a_i.x()), float(a_i.y())); - Vec2f b(float(b_i.x()), float(b_i.y())); - - Vec2f mid = (a + b) * 0.5f; - - // Direction vectors from midpoint towards A and B - Vec2f dir_to_a = a - mid; - Vec2f dir_to_b = b - mid; - - float len_a = dir_to_a.norm(); - float len_b = dir_to_b.norm(); - - // Guard against degenerated edges - if (len_a < EPSILON || len_b < EPSILON) return polygon; - - dir_to_a /= len_a; - dir_to_b /= len_b; - - // Clamp range to avoid overshooting the edge - float max_range = std::min(len_a, len_b) * 0.9f; - float range = std::min(float(scale_range), max_range); - - // Offset points (float space) - Vec2f offset_to_a_f = mid + dir_to_a * range; - Vec2f offset_to_b_f = mid + dir_to_b * range; - - // Safe cast back to scaled integer Point - auto to_int_point = [](const Vec2f &p) { - auto clamp = [](float v) -> coord_t { - constexpr float kMin = float(std::numeric_limits::min()); - constexpr float kMax = float(std::numeric_limits::max()); - v = std::clamp(v, kMin, kMax); - return static_cast(std::lround(v)); - }; - return Point(clamp(p.x()), clamp(p.y())); - }; - - Point mid_i = to_int_point(mid); - Point offset_to_a_i = to_int_point(offset_to_a_f); - Point offset_to_b_i = to_int_point(offset_to_b_f); - - // Rebuild polygon with inserted points - for (size_t i = 0; i < polygon.size(); ++i) { - res.points.push_back(polygon[i]); - - // Insert points right after the selected edge start vertex - if (i == closest_edge_idx) { - res.points.push_back(offset_to_a_i); - res.points.push_back(mid_i); - res.points.push_back(offset_to_b_i); - } - } - - return res; -} - -static Polylines remove_points_from_polygon( - const Polygon& polygon_ori, const std::vector& skip_points, double range, float wt_width, Polygon& insert_skip_pg) -{ - Polygon polygon = add_extra_point(polygon_ori, scale_(range)); - if (polygon.size() < 2) return Polylines{to_polyline(polygon)}; - Polylines result; - std::vector new_pl; // add intersection points for gaps, where bool indicates whether it's a gap point. - std::vector inter_info; - auto polygon_box = get_extents(polygon); - Point anchor_point = Point{polygon_box.center()[0], polygon_box.min[1]}; // for next reconnect - std::vector points; - { - points.reserve(polygon.points.size()); - int idx = polygon.closest_point_index(anchor_point); - Polyline tmp_poly = polygon.split_at_index(idx); - for (auto& p : tmp_poly) - points.push_back(unscale(p).cast()); - points.pop_back(); - } - - for (int i = 0; i < skip_points.size(); i++) { - bool is_left = abs(skip_points[i].x()) < wt_width / 2.f; - Vec2f ray = is_left ? Vec2f(-1, 0) : Vec2f(1, 0); - for (int j = 0; j < points.size(); j++) { - Vec2f& p1 = points[j]; - Vec2f& p2 = points[(j + 1) % points.size()]; - auto [is_inter, inter_pos] = ray_intersetion_line(skip_points[i], ray, p1, p2); - if (is_inter) { - IntersectionInfo forward = move_point_along_polygon(points, inter_pos, j, range, true, i); - IntersectionInfo backward = move_point_along_polygon(points, inter_pos, j, range, false, i); - backward.is_forward = false; - forward.is_forward = true; - inter_info.push_back(backward); - inter_info.push_back(forward); - break; - } - } - } - - // insert point to new_pl - for (const auto& p : points) - new_pl.push_back({p, -1}); - std::sort(inter_info.begin(), inter_info.end(), [](const IntersectionInfo& lhs, const IntersectionInfo& rhs) { - if (rhs.idx == lhs.idx) - return lhs.dis_from_idx < rhs.dis_from_idx; - return lhs.idx < rhs.idx; - }); - for (int i = inter_info.size() - 1; i >= 0; i--) { - insert_points(new_pl, inter_info[i].idx, inter_info[i].pos, inter_info[i].pair_idx, inter_info[i].is_forward); - } - - { - // set insert_pg for wipe_path - for (auto& p : new_pl) - insert_skip_pg.points.push_back(scaled(p.pos)); - } - - int beg = 0; - bool skip = true; - int i = beg; - Polyline pl; - - do { - if (skip || new_pl[i].pair_idx == -1) { - pl.points.push_back(scaled(new_pl[i].pos)); - i = (i + 1) % new_pl.size(); - skip = false; - } else { - if (!pl.points.empty()) { - pl.points.push_back(scaled(new_pl[i].pos)); - result.push_back(pl); - pl.points.clear(); - } - int left = new_pl[i].pair_idx; - int j = (i + 1) % new_pl.size(); - while (j != beg && new_pl[j].pair_idx != left) { - if (new_pl[j].pair_idx != -1 && !new_pl[j].is_forward) - left = new_pl[j].pair_idx; - j = (j + 1) % new_pl.size(); - } - i = j; - skip = true; - } - } while (i != beg); - - if (!pl.points.empty()) { - if (new_pl[i].pair_idx == -1) - pl.points.push_back(scaled(new_pl[i].pos)); - result.push_back(pl); - } - return result; -} - -static Polylines contrust_gap_for_skip_points( - const Polygon& polygon, const std::vector& skip_points, float wt_width, float gap_length, Polygon& insert_skip_polygon) -{ - if (skip_points.empty()) { - insert_skip_polygon = polygon; - return Polylines{to_polyline(polygon)}; - } - return remove_points_from_polygon(polygon, skip_points, gap_length, wt_width, insert_skip_polygon); -}; - static Polygon generate_rectange_polygon(const Vec2f& wt_box_min, const Vec2f& wt_box_max) { Polygon res; @@ -1334,6 +999,12 @@ WipeTower::ToolChangeResult WipeTower2::construct_tcr(WipeTowerWriter2& writer, +bool WipeTower2::use_gap_wall(const PrintConfig& config) +{ + // The cone wall has its own fully separate generator with no gap machinery. + return config.prime_tower_skip_points.value && config.wipe_tower_wall_type.value != wtwCone; +} + WipeTower2::WipeTower2(const PrintConfig& config, const PrintRegionConfig& default_region_config,int plate_idx, Vec3d plate_origin, const std::vector>& wiping_matrix, size_t initial_tool) : m_semm(config.single_extruder_multi_material.value), m_enable_filament_ramming(config.enable_filament_ramming.value), @@ -1361,8 +1032,7 @@ WipeTower2::WipeTower2(const PrintConfig& config, const PrintRegionConfig& defau m_rib_width(config.wipe_tower_rib_width), m_extra_rib_length(config.wipe_tower_extra_rib_length), m_wall_type((int)config.wipe_tower_wall_type), - // The cone wall has its own fully separate generator with no gap machinery. - m_use_gap_wall(config.prime_tower_skip_points.value && config.wipe_tower_wall_type.value != wtwCone), + m_use_gap_wall(use_gap_wall(config)), m_enable_tower_interface_features(config.enable_tower_interface_features.value), m_enable_tower_interface_cooldown_during_tower(config.enable_tower_interface_cooldown_during_tower.value) { @@ -1664,13 +1334,9 @@ WipeTower::ToolChangeResult WipeTower2::tool_change(size_t tool) writer.speed_override_backup(); writer.speed_override(100); - Vec2f initial_position = cleaning_box.ld + Vec2f(0.f, m_depth_traversed); - // With a boundary wipe start the wall gap sits at the first wipe row below the - // quantized ram band; enter there too so the routed entry, the gap and the wipe - // scrub all share one opening. toolchange_Unload() climbs back up to the ram band - // along the box interior. - if (!m_semm && m_use_gap_wall && ramming_depth > 0.f) - initial_position.y() += wipe_start_offset_after_ram(ramming_depth, is_first_layer()); + // On a boundary wipe start this enters at the wall gap on the first wipe row; + // toolchange_Unload() then climbs back up to the ram band along the box interior. + Vec2f initial_position = toolchange_entry_pos(m_depth_traversed, ramming_depth, is_first_layer()); writer.set_initial_position(initial_position, m_wipe_tower_width, m_wipe_tower_depth, m_internal_rotation); // Increase the extruder driver current to allow fast ramming. @@ -1682,10 +1348,8 @@ WipeTower::ToolChangeResult WipeTower2::tool_change(size_t tool) // Without a ram — or with the boundary wipe start, where the ram band is // quantized to whole rows — the box is planned as whole wipe rows; the wipe // then fills it completely so adjacent purge blocks stay contiguous. Uses the - // old tool (m_current_tool before toolchange_Change), same conditions as - // toolchange_Unload()'s do_ramming / boundary_wipe_start. - const bool do_ram_old = (m_semm && m_enable_filament_ramming) || m_filpar[m_current_tool].multitool_ramming; - const bool fill_box = !do_ram_old || (!m_semm && m_use_gap_wall); + // old tool (m_current_tool before toolchange_Change). + const bool fill_box = !tool_ramming_enabled(m_current_tool) || boundary_wipe_start_enabled(m_current_tool); auto new_tool_temp = is_first_layer() ? m_filpar[tool].first_layer_temperature : m_filpar[tool].temperature; toolchange_Unload(writer, cleaning_box, m_filpar[m_current_tool].material, (is_first_layer() ? m_filpar[m_current_tool].first_layer_temperature : m_filpar[m_current_tool].temperature), @@ -1763,11 +1427,10 @@ void WipeTower2::toolchange_Unload( float remaining = xr - xl ; // keeps track of distance to the next turnaround float e_done = 0; // measures E move done from each segment - // Orca: Do ramming when SEMM and ramming is enabled or when multi tool head when ramming is enabled on the multi tool. - const bool do_ramming = (m_semm && m_enable_filament_ramming) || m_filpar[m_current_tool].multitool_ramming; + const bool do_ramming = tool_ramming_enabled(m_current_tool); const bool cold_ramming = m_is_mk4mmu3; // Orca: see set_toolchange() — quantized ram band + wipe restart at the boundary. - const bool boundary_wipe_start = do_ramming && !m_semm && m_use_gap_wall; + const bool boundary_wipe_start = boundary_wipe_start_enabled(m_current_tool); float planned_ramming_depth = 0.f; if (boundary_wipe_start && m_layer_info != m_plan.end()) for (const auto& tch : m_layer_info->tool_changes) @@ -1994,12 +1657,9 @@ void WipeTower2::toolchange_Unload( // first wipe row so the purge row lattice continues across the block boundary // (previous box's last row top edge sits at its box top): with the planned depth // of rows * dy, the last row's top edge then lands exactly on this box's top and - // no blank band is left between adjacent purge blocks. Mirrors dy/line_width in - // toolchange_Wipe(). - const float wipe_dy = (is_first_layer() ? m_extra_flow : m_extra_spacing_wipe) * m_perimeter_width; - const float wipe_line_width = m_perimeter_width * m_extra_flow; + // no blank band is left between adjacent purge blocks. writer.set_position(Vec2f(end_of_ramming.x(), - cleaning_box.ld.y() + m_depth_traversed + wipe_dy - (m_perimeter_width + wipe_line_width) / 2.f + m_perimeter_width)); + cleaning_box.ld.y() + m_depth_traversed + wipe_start_offset_after_ram(0.f, is_first_layer()) + m_perimeter_width)); } writer.resume_preview() @@ -2090,7 +1750,7 @@ void WipeTower2::toolchange_Wipe( const float& xr = cleaning_box.rd.x(); writer.set_extrusion_flow(m_extrusion_flow * m_extra_flow); - const float line_width = m_perimeter_width * m_extra_flow; + const float line_width = wipe_line_width(); writer.change_analyzer_line_width(line_width); // Variables x_to_wipe and traversed_x are here to be able to make sure it always wipes at least @@ -2098,7 +1758,7 @@ void WipeTower2::toolchange_Wipe( // wipe until the end of the assigned area. float x_to_wipe = volume_to_length(wipe_volume, m_perimeter_width, m_layer_height) / m_extra_flow; - float dy = (is_first_layer() ? m_extra_flow : m_extra_spacing_wipe) * m_perimeter_width; // Don't use the extra spacing for the first layer, but do use the spacing resulting from increased flow. + float dy = wipe_row_spacing(is_first_layer()); // Don't use the extra spacing for the first layer, but do use the spacing resulting from increased flow. // All the calculations in all other places take the spacing into account for all the layers. // If spare layers are excluded->if 1 or less toolchange has been done, it must be sill the first layer, too.So slow down. @@ -2291,7 +1951,7 @@ WipeTower::ToolChangeResult WipeTower2::finish_layer() poly = generate_support_cone_wall(writer, wt_box, feedrate, infill_cone, spacing); } else { WipeTower::box_coordinates wt_box(Vec2f(0.f, 0.f), m_wipe_tower_width, m_layer_info->depth + m_perimeter_width); - poly = generate_support_rib_wall(writer, wt_box, feedrate, first_layer, m_wall_type == (int)wtwRib, true, m_use_gap_wall); + poly = generate_support_rib_wall(writer, wt_box, feedrate, first_layer, m_wall_type == (int)wtwRib, true); } // brim (first layer only) @@ -2419,16 +2079,14 @@ WipeTower2::WipeTowerInfo::ToolChange WipeTower2::set_toolchange(size_t old_tool float length_to_extrude = volume_to_length((m_semm ? 0.25f : m_filpar[old_tool].multitool_ramming_time) * std::accumulate(m_filpar[old_tool].ramming_speed.begin(), m_filpar[old_tool].ramming_speed.end(), 0.f), m_perimeter_width * m_filpar[old_tool].ramming_line_width_multiplicator, layer_height); - // Orca: Reserve ramming depth only when toolchange_Unload() will actually ram - // (same condition as its do_ramming), otherwise the unprinted reservation - // leaves blank bands between the purge boxes. - const bool do_ramming = (m_semm && m_enable_filament_ramming) || m_filpar[old_tool].multitool_ramming; + // Orca: Reserve ramming depth only when toolchange_Unload() will actually ram, + // otherwise the unprinted reservation leaves blank bands between the purge boxes. + const bool do_ramming = tool_ramming_enabled(old_tool); // Orca: with the gap wall on a multi-tool printer the ram band is quantized up to // the whole reserved rows and the wipe restarts at the left-edge boundary on a // fresh row below it (BBL parity: the old-tool purge is whole rows and the wipe - // always starts at the box corner, where the entry scrub runs). SEMM keeps the - // stock continue-from-ram-end behavior. Must match toolchange_Unload()/tool_change(). - const bool boundary_wipe_start = do_ramming && !m_semm && m_use_gap_wall; + // always starts at the box corner, where the entry scrub runs). + const bool boundary_wipe_start = boundary_wipe_start_enabled(old_tool); float ramming_depth = do_ramming ? ((int(length_to_extrude / width) + 1) * (m_perimeter_width * m_filpar[old_tool].ramming_line_width_multiplicator * m_filpar[old_tool].ramming_step_multiplicator) * m_extra_spacing_ramming) : 0; // first_wipe_line rides for free on the last (partially used) ramming row, which // is already covered by ramming_depth. Without ramming that row does not exist @@ -2588,30 +2246,26 @@ static WipeTower::ToolChangeResult merge_tcr(WipeTower::ToolChangeResult& first, } -// Processes vector m_plan and calls respective functions to generate G-code for the wipe tower -// Resulting ToolChangeResults are appended into vector "result" // Precompute, for every plan layer, the wall openings ("skip points") at each toolchange's -// entry, like WipeTower::get_all_wall_skip_points(). The entry is where tool_change() -// starts: cleaning_box.ld + (0, m_depth_traversed), with m_depth_traversed advancing by -// required_depth per toolchange — reproduced here from the finalized plan so each gap -// coincides with the entry travel's target (tcr.start_pos, pre-rotation frame). -// With a boundary wipe start the entry, the wipe and its scrub sit on the first wipe row -// below the quantized ram band, so the gap moves there with them (BBL cuts its gap at the -// CP_TOOLCHANGE_WIPE start row too, never at the ram band). +// entry position, like WipeTower::get_all_wall_skip_points(). toolchange_entry_pos() +// reproduces from the finalized plan where tool_change() will start, so each gap coincides +// with the entry travel's target (tcr.start_pos, pre-rotation frame). BBL parity: the gap +// sits at the CP_TOOLCHANGE_WIPE start row, never at the ram band. void WipeTower2::compute_wall_skip_points() { m_wall_skip_points.assign(m_plan.size(), std::vector()); for (size_t layer_id = 0; layer_id < m_plan.size(); ++layer_id) { float depth_traversed = 0.f; for (const auto& toolchange : m_plan[layer_id].tool_changes) { - const float ram_offset = (!m_semm && toolchange.ramming_depth > 0.f) ? - wipe_start_offset_after_ram(toolchange.ramming_depth, layer_id == m_first_layer_idx) : 0.f; - m_wall_skip_points[layer_id].emplace_back(m_perimeter_width / 2.f, m_perimeter_width / 2.f + depth_traversed + ram_offset); + m_wall_skip_points[layer_id].emplace_back( + toolchange_entry_pos(depth_traversed, toolchange.ramming_depth, layer_id == m_first_layer_idx)); depth_traversed += toolchange.required_depth; } } } +// Processes vector m_plan and calls respective functions to generate G-code for the wipe tower +// Resulting ToolChangeResults are appended into vector "result" void WipeTower2::generate(std::vector> &result) { if (m_plan.empty()) @@ -2779,8 +2433,7 @@ Polygon WipeTower2::generate_support_rib_wall(WipeTowerWriter2& double feedrate, bool first_layer, bool rib_wall, - bool extrude_perimeter, - bool skip_points) + bool extrude_perimeter) { float retract_length = m_filpar[m_current_tool].retract_length; @@ -2800,7 +2453,7 @@ Polygon WipeTower2::generate_support_rib_wall(WipeTowerWriter2& if (!extrude_perimeter) return wall_polygon; - if (skip_points) { + if (m_use_gap_wall) { // Cut the wall open at each toolchange's entry (see compute_wall_skip_points()). // The vector is empty during the save_on_last_wipe planning passes, which therefore // measure the un-gapped wall — same approximation as the BBL tower. diff --git a/src/libslic3r/GCode/WipeTower2.hpp b/src/libslic3r/GCode/WipeTower2.hpp index 439515cdd4..3efa884202 100644 --- a/src/libslic3r/GCode/WipeTower2.hpp +++ b/src/libslic3r/GCode/WipeTower2.hpp @@ -34,6 +34,10 @@ public: bool is_finish, bool is_contact = false) const; + // Whether this print cuts wall openings ("skip points") at the toolchange entries. + // Shared with the entry routing in GCode.cpp so the router and the tower agree. + static bool use_gap_wall(const PrintConfig& config); + // x -- x coordinates of wipe tower in mm ( left bottom corner ) // y -- y coordinates of wipe tower in mm ( left bottom corner ) // width -- width of wipe tower in mm ( default 60 mm - leave as it is ) @@ -284,13 +288,35 @@ private: bool is_first_layer() const { return size_t(m_layer_info - m_plan.begin()) == m_first_layer_idx; } - // With a boundary wipe start (multitool ram, non-SEMM, gap wall) the wipe begins on a - // fresh row below the quantized ram band. Y offset from the box start to that first - // wipe row; must stay in sync with the alignment travel in toolchange_Unload(). + // Purge row lattice of toolchange_Wipe(): row pitch and extrusion width. + float wipe_row_spacing(bool first_layer) const { return (first_layer ? m_extra_flow : m_extra_spacing_wipe) * m_perimeter_width; } + float wipe_line_width() const { return m_perimeter_width * m_extra_flow; } + + // Whether toolchange_Unload() rams this (old) tool out. + bool tool_ramming_enabled(size_t tool) const { return (m_semm && m_enable_filament_ramming) || m_filpar[tool].multitool_ramming; } + // Whether the wipe restarts at the box boundary on a fresh row below the quantized + // ram band after ramming this (old) tool out (multi-tool gap wall; SEMM keeps the + // stock continue-from-ram-end behavior). + bool boundary_wipe_start_enabled(size_t tool) const { return tool_ramming_enabled(tool) && !m_semm && m_use_gap_wall; } + + // With a boundary wipe start the wipe begins on a fresh row below the quantized ram + // band. Y offset from the box start to that first wipe row. float wipe_start_offset_after_ram(float ramming_depth, bool first_layer) const { - const float wipe_dy = (first_layer ? m_extra_flow : m_extra_spacing_wipe) * m_perimeter_width; - return ramming_depth + wipe_dy - (m_perimeter_width + m_perimeter_width * m_extra_flow) / 2.f; + return ramming_depth + wipe_row_spacing(first_layer) - (m_perimeter_width + wipe_line_width()) / 2.f; + } + + // Tower-local entry position of a toolchange whose box starts depth_traversed into + // the layer: the box corner, moved down to the first wipe row when the plan gives + // it a boundary wipe start (ramming_depth > 0 iff the unload rams). tool_change() + // enters here and compute_wall_skip_points() cuts the wall gap here, so the routed + // entry, the gap and the wipe scrub all share one opening. + Vec2f toolchange_entry_pos(float depth_traversed, float ramming_depth, bool first_layer) const + { + Vec2f pos(m_perimeter_width / 2.f, m_perimeter_width / 2.f + depth_traversed); + if (!m_semm && m_use_gap_wall && ramming_depth > 0.f) + pos.y() += wipe_start_offset_after_ram(ramming_depth, first_layer); + return pos; } // Calculates extrusion flow needed to produce required line width for given layer height @@ -379,8 +405,7 @@ private: double feedrate, bool first_layer, bool rib_wall, - bool extrude_perimeter, - bool skip_points); + bool extrude_perimeter); Polygon generate_support_cone_wall( WipeTowerWriter2& writer, From 4824a171f1bcebe644eafe70a45fc50a5c9e6347 Mon Sep 17 00:00:00 2001 From: SoftFever Date: Fri, 31 Jul 2026 15:34:04 +0800 Subject: [PATCH 15/16] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- tests/libslic3r/test_toolordering_nozzle_group.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/libslic3r/test_toolordering_nozzle_group.cpp b/tests/libslic3r/test_toolordering_nozzle_group.cpp index 5eee164eb0..dc54aae80a 100644 --- a/tests/libslic3r/test_toolordering_nozzle_group.cpp +++ b/tests/libslic3r/test_toolordering_nozzle_group.cpp @@ -509,8 +509,8 @@ TEST_CASE("A degenerate process variant map on a custom multi-extruder printer s // otherwise the failed per-extruder lookups collapse the per-extruder retract overrides // during slicing and the post-slice re-apply invalidates every fresh result, forever. DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); - config.option("nozzle_diameter", true)->values = {0.4, 0.4, 0.4, 0.4, 0.4}; config.set_num_extruders(5); + config.option("nozzle_diameter", true)->values = {0.4, 0.4, 0.4, 0.4, 0.4}; // per-extruder machine values that a first-slot collapse would destroy config.option("retract_before_wipe", true)->values = {100., 70., 70., 70., 100.}; config.option("z_hop_types", true)->values = {zhtSlope, zhtNormal, zhtNormal, zhtNormal, zhtSlope}; From c8aacea17690c2a86a13731c616b65675481e0bb Mon Sep 17 00:00:00 2001 From: SoftFever Date: Fri, 31 Jul 2026 16:58:26 +0800 Subject: [PATCH 16/16] fix typo --- src/libslic3r/GCode/WipeTower.cpp | 4 ++-- src/libslic3r/GCode/WipeTower.hpp | 2 +- src/libslic3r/GCode/WipeTower2.cpp | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/libslic3r/GCode/WipeTower.cpp b/src/libslic3r/GCode/WipeTower.cpp index 3fbc0cccaa..3d7353eadf 100644 --- a/src/libslic3r/GCode/WipeTower.cpp +++ b/src/libslic3r/GCode/WipeTower.cpp @@ -593,7 +593,7 @@ Polylines remove_points_from_polygon(const Polygon &polygon_ori, const std::vect return result; } -Polylines contrust_gap_for_skip_points(const Polygon &polygon, const std::vector & skip_points ,float wt_width,float gap_length,Polygon& insert_skip_polygon) +Polylines construct_gap_for_skip_points(const Polygon &polygon, const std::vector & skip_points ,float wt_width,float gap_length,Polygon& insert_skip_polygon) { if (skip_points.empty()) { insert_skip_polygon = polygon; @@ -5101,7 +5101,7 @@ Polygon WipeTower::generate_support_wall_new(WipeTowerWriter &writer, const box_ if (!extrude_perimeter) return wall_polygon; if (skip_points) { - result_wall = contrust_gap_for_skip_points(wall_polygon, m_wall_skip_points[m_cur_layer_id], m_wipe_tower_width, 2.5 * m_perimeter_width, insert_skip_polygon); + result_wall = construct_gap_for_skip_points(wall_polygon, m_wall_skip_points[m_cur_layer_id], m_wipe_tower_width, 2.5 * m_perimeter_width, insert_skip_polygon); } else { result_wall.push_back(to_polyline(wall_polygon)); diff --git a/src/libslic3r/GCode/WipeTower.hpp b/src/libslic3r/GCode/WipeTower.hpp index 3dbbf03ffb..66e8acf1c4 100644 --- a/src/libslic3r/GCode/WipeTower.hpp +++ b/src/libslic3r/GCode/WipeTower.hpp @@ -23,7 +23,7 @@ enum GCodeFlavor : unsigned char; // Cuts the tower wall polygon open at each skip point (a toolchange's entry position) // so the entry travel can pass through instead of crossing the printed wall. Defined in // WipeTower.cpp, shared by WipeTower and WipeTower2. -Polylines contrust_gap_for_skip_points( +Polylines construct_gap_for_skip_points( const Polygon& polygon, const std::vector& skip_points, float wt_width, float gap_length, Polygon& insert_skip_polygon); class WipeTower diff --git a/src/libslic3r/GCode/WipeTower2.cpp b/src/libslic3r/GCode/WipeTower2.cpp index e3e5f075c1..51ab155dd9 100644 --- a/src/libslic3r/GCode/WipeTower2.cpp +++ b/src/libslic3r/GCode/WipeTower2.cpp @@ -2461,7 +2461,7 @@ Polygon WipeTower2::generate_support_rib_wall(WipeTowerWriter2& const size_t layer_id = size_t(m_layer_info - m_plan.begin()); const std::vector& layer_skip_points = layer_id < m_wall_skip_points.size() ? m_wall_skip_points[layer_id] : no_skip_points; - result_wall = contrust_gap_for_skip_points(wall_polygon, layer_skip_points, m_wipe_tower_width, 2.5 * m_perimeter_width, + result_wall = construct_gap_for_skip_points(wall_polygon, layer_skip_points, m_wipe_tower_width, 2.5 * m_perimeter_width, insert_skip_polygon); } else { result_wall.push_back(to_polyline(wall_polygon));