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/GCode.cpp b/src/libslic3r/GCode.cpp index 5b0ce2f716..9c8959a05c 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 "GCode/OrderingStrategies.hpp" #include "Print.hpp" @@ -889,6 +890,65 @@ static std::vector get_path_of_change_filament(const Print& print) return res; } + // 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 + { + 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); + } + 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_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) + 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) @@ -999,6 +1059,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; @@ -1261,24 +1322,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); @@ -1308,20 +1352,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); @@ -1419,20 +1466,16 @@ 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); - auto transform_wt_pt = [&alpha, this](const Vec2f &pt) -> Vec2f { - Vec2f out = Eigen::Rotation2Df(alpha) * pt; - 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() : 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)); @@ -1462,16 +1505,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 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(); - gcode += gcodegen.travel_to(wipe_tower_point_to_object_point(gcodegen, start_pos + plate_origin_2d), 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) { @@ -1494,6 +1543,36 @@ 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 && 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 + // 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(); + 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()}; @@ -1679,7 +1758,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 fdb6f54861..acd5acb0a2 100644 --- a/src/libslic3r/GCode.hpp +++ b/src/libslic3r/GCode.hpp @@ -132,6 +132,9 @@ 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; + 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/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/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/WipeTower.cpp b/src/libslic3r/GCode/WipeTower.cpp index 06bca292d4..3d7353eadf 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 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; 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 = 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)); 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..66e8acf1c4 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 { @@ -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 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 { @@ -84,7 +89,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 +112,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 +127,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 +169,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 +188,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 +228,6 @@ public: } } - void set_wipe_volume(std::vector>& wiping_matrix) { - wipe_volumes = wiping_matrix; - } // Switch to a next layer. void set_layer( @@ -250,7 +256,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 +314,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 +325,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 +343,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 +397,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 +443,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 +460,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 +475,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 +511,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 +528,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 +586,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 +616,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 +636,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/GCode/WipeTower2.cpp b/src/libslic3r/GCode/WipeTower2.cpp index 34b385d4be..51ab155dd9 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; @@ -234,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; @@ -296,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; @@ -306,234 +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}); - } -} - -static Polylines remove_points_from_polygon( - const Polygon& polygon, const std::vector& skip_points, double range, bool is_left, Polygon& insert_skip_pg) -{ - assert(polygon.size() > 2); - 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 - 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++) { - 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)}; - } - 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); -}; - static Polygon generate_rectange_polygon(const Vec2f& wt_box_min, const Vec2f& wt_box_max) { Polygon res; @@ -1245,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), @@ -1272,7 +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), - m_flat_ironing(config.prime_tower_flat_ironing.value), + 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) { @@ -1342,6 +1102,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); @@ -1478,11 +1239,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); @@ -1525,8 +1286,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)) { @@ -1534,6 +1296,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; } } @@ -1571,7 +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); + // 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. @@ -1580,6 +1345,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 — 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). + 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), @@ -1602,7 +1372,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) @@ -1657,11 +1427,20 @@ 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 = 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) + 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(); @@ -1672,7 +1451,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; @@ -1731,6 +1511,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 @@ -1838,10 +1640,27 @@ 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. + writer.travel(Vec2f(ramming_start_pos.x(), + 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) 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. + writer.set_position(Vec2f(end_of_ramming.x(), + cleaning_box.ld.y() + m_depth_traversed + wipe_start_offset_after_ram(0.f, is_first_layer()) + m_perimeter_width)); + } writer.resume_preview() .flush_planner_queue(); @@ -1920,7 +1739,9 @@ void WipeTower2::toolchange_Wipe( WipeTowerWriter2 &writer, const WipeTower::box_coordinates &cleaning_box, float wipe_volume, - bool interface_layer) + bool interface_layer, + 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)) @@ -1929,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 @@ -1937,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. @@ -1950,9 +1771,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) { @@ -1963,22 +1781,45 @@ 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 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; + 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() + 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); + } + if (m_left_to_right) writer.extrude(xr - (i % 4 == 0 ? 0 : 1.5f*line_width), writer.y(), wipe_speed); 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 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; } @@ -2110,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, false); + poly = generate_support_rib_wall(writer, wt_box, feedrate, first_layer, m_wall_type == (int)wtwRib, true); } // brim (first layer only) @@ -2228,15 +2069,32 @@ 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; - 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); - // 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); + 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)); +} - float first_wipe_volume = length_to_volume(first_wipe_line, m_perimeter_width * m_extra_flow, layer_height_par); +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((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, + // 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). + 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 + // (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); // ORCA: Keep wipe-depth planning consistent with toolchange_Wipe(). // ORCA: On the first layer, toolchange_Wipe() advances purge rows using @@ -2245,12 +2103,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); } @@ -2288,49 +2145,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) { @@ -2339,8 +2211,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, @@ -2363,6 +2246,24 @@ static WipeTower::ToolChangeResult merge_tcr(WipeTower::ToolChangeResult& first, } +// Precompute, for every plan layer, the wall openings ("skip points") at each toolchange's +// 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) { + 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) @@ -2378,12 +2279,41 @@ 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. + if (m_use_gap_wall) + compute_wall_skip_points(); + m_layer_info = m_plan.begin(); m_current_height = 0.f; @@ -2410,7 +2340,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) { @@ -2503,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; @@ -2524,18 +2453,28 @@ Polygon WipeTower2::generate_support_rib_wall(WipeTowerWriter2& if (!extrude_perimeter) 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, + 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. + 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 = 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)); 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(). Measured on + // the un-gapped outline so a wall gap cannot shift the tower. + if (rib_wall && is_first_layer()) { + BoundingBox bbox = get_extents(insert_skip_polygon); + 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..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 ) @@ -69,9 +73,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; } @@ -149,6 +153,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; @@ -220,7 +225,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; @@ -231,6 +235,12 @@ 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_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; @@ -278,6 +288,37 @@ private: bool is_first_layer() const { return size_t(m_layer_info - m_plan.begin()) == m_first_layer_idx; } + // 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 + { + 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 float extrusion_flow(float layer_height = -1.f) const // negative layer_height - return current m_extrusion_flow { @@ -328,9 +369,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( @@ -353,7 +395,9 @@ private: WipeTowerWriter2 &writer, const WipeTower::box_coordinates &cleaning_box, float wipe_volume, - bool interface_layer); + bool interface_layer, + bool priming = false, + bool fill_box = false); Polygon generate_support_rib_wall(WipeTowerWriter2& writer, @@ -361,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, @@ -372,6 +415,12 @@ private: float spacing); 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); }; 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..371f1e68ea 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; @@ -4017,10 +4021,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 +4083,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 +4114,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 +4134,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 +4141,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); @@ -4338,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, 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 7510fcf17a..17dd195df9 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -10516,6 +10516,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) { @@ -10557,6 +10595,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/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"); diff --git a/tests/libslic3r/test_config_variant_expansion.cpp b/tests/libslic3r/test_config_variant_expansion.cpp index 14675fd4dc..dd7de2af44 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.})); } } @@ -272,6 +287,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..dc54aae80a 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.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}; + // 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") {