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,