diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index 85903cb779..c20405781b 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -768,30 +768,31 @@ static std::vector get_path_of_change_filament(const Print& print) return changes; } + // Clearance the tower-approach router keeps around the tower: the avoid box is + // inflated by this much before routing, and the inflated corners must stay on the + // bed for a route to be generated at all. + static constexpr float wipe_tower_routing_clearance = 2.f; + // BBS // start_pos refers to the last position before the wipe_tower. // end_pos refers to the wipe tower's start_pos. // using the print coordinate system - Polyline WipeTowerIntegration::generate_path_to_wipe_tower(const Point& start_pos,const Point &end_pos , const BoundingBox& avoid_polygon , const BoundingBox& printer_bbx) const + Polyline WipeTowerIntegration::generate_path_to_wipe_tower(const Point& start_pos,const Point &end_pos , const BoundingBox& avoid_polygon , const Polygons& bed_polygons) const { Polyline res; - coord_t alpha = scaled(2.f); // offset distance + coord_t alpha = scaled(wipe_tower_routing_clearance); // offset distance BoundingBox avoid_polygon_inner = avoid_polygon; avoid_polygon_inner.offset(alpha); coord_t width = avoid_polygon_inner.max[0] - avoid_polygon_inner.min[0]; - Polygon bed_polygon = printer_bbx.polygon(); Vec2f v(1, 0); // the first print direction of end_pos. if (abs(end_pos[0] - avoid_polygon_inner.min[0]) < width / 2) v = -v; // judge whether the wipe tower's infill goes to the left or right. - // Judge whether the avoid_polygon_inner is outside the printer_bbx. + // Judge whether the avoid_polygon_inner is outside the bed. The real printable + // outline is tested (not its bounding box), so on circular/custom beds corners + // hanging off the bed are rejected. // If so, do nothing and just go directly to the end_pos. - bool is_bbx_in_bed = true; Points avoid_points = avoid_polygon_inner.polygon().points; - for (auto &wipe_tower_bbx_p : avoid_points) { - if (ClipperLib::PointInPolygon(wipe_tower_bbx_p, bed_polygon.points) != 1) { - is_bbx_in_bed = false; - break; - } - } + const bool is_bbx_in_bed = std::all_of(avoid_points.begin(), avoid_points.end(), + [&bed_polygons](const Point &pt) { return contains(bed_polygons, pt, /*border_result=*/false); }); if (!is_bbx_in_bed) { res.points.push_back(end_pos); return res; @@ -898,27 +899,17 @@ static std::vector get_path_of_change_filament(const Print& print) 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 + // Bed outline the tower-approach router plans against, in object coordinates. The real + // outline is returned, not its bounding box, so the router's containment tests fail off + // the bed on circular/custom shapes; the multi-nozzle narrowing lives in the accessor. + Polygons WipeTowerIntegration::shared_printable_area(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; + // The frame change is a pure translation, so transform the origin once. + const Point offset = wipe_tower_point_to_object_point(gcodegen, Vec2f(m_plate_origin(0), m_plate_origin(1))); + Polygons bed_polygons = gcodegen.m_print->get_extruder_shared_printable_polygon(); + for (Polygon &poly : bed_polygons) + poly.translate(offset); + return bed_polygons; } // With skip points enabled the Type2 tower wall has an opening at each toolchange's @@ -933,15 +924,37 @@ static std::vector get_path_of_change_filament(const Print& print) 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)) + // Transform tower-local corners exactly like the tcr points; a rotated tower gets a + // conservative axis-aligned envelope from the result. + auto tower_polygon = [&](const BoundingBoxf &bbx) { + Polygon poly = scaled(bbx).polygon(); + for (Point &p : poly.points) + p = wipe_tower_point_to_object_point(gcodegen, transform_wt2_pt(unscale(p).cast()) + plate_origin_2d); + return poly; + }; + // The avoid envelope covers the first-layer brim (and rib flare), which a travel may + // cross freely: early-out only when the approach already starts over the tower body + // itself, so a start between the wall and the brim edge still gets routed in through + // the wall opening. Test the rotated polygon, not its bounding box — at angles off the + // axes the box's corner triangles cover most of the brim ring. + const float body_width = gcodegen.m_config.wipe_tower_wall_type.value == WipeTowerWallType::wtwRib ? m_wipe_tower_depth : m_right; + if (tower_polygon(BoundingBoxf(Vec2d(0., 0.), Vec2d(body_width, m_wipe_tower_depth))).contains(route_start)) return {}; - Polyline travel_polyline = generate_path_to_wipe_tower(route_start, start_wipe_pos, avoid_bbx, printer_travel_bounds(gcodegen)); + + const Polygons bed = shared_printable_area(gcodegen); + BoundingBox avoid_bbx = get_extents(tower_polygon(m_wipe_tower_bbx)); + // The inflated corners must stay on the bed for the router to generate a route at all: + // clamp the box against the bed shrunk by the clearance the router adds, so a tower + // parked near the bed edge is still routed along the clamped side instead of always + // travelling straight across the tower. + BoundingBox clamp_bbx = get_extents(bed); + clamp_bbx.offset(-(scaled(wipe_tower_routing_clearance) + SCALED_EPSILON)); + avoid_bbx.min = avoid_bbx.min.cwiseMax(clamp_bbx.min); + avoid_bbx.max = avoid_bbx.max.cwiseMin(clamp_bbx.max); + if (avoid_bbx.min.x() >= avoid_bbx.max.x() || avoid_bbx.min.y() >= avoid_bbx.max.y()) + return {}; + + Polyline travel_polyline = generate_path_to_wipe_tower(route_start, start_wipe_pos, avoid_bbx, bed); 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) @@ -1322,7 +1335,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 = printer_travel_bounds(gcodegen); + BoundingBox avoid_bbx; { // set avoid_bbx avoid_bbx = scaled(m_wipe_tower_bbx); @@ -1334,7 +1347,7 @@ static std::vector get_path_of_change_filament(const Print& print) avoid_bbx = BoundingBox(avoid_points.points); } std::string travel_to_wipe_tower_gcode; - Polyline travel_polyline = generate_path_to_wipe_tower(gcode_last_pos2d_object, start_wipe_pos, avoid_bbx, printer_bbx); + Polyline travel_polyline = generate_path_to_wipe_tower(gcode_last_pos2d_object, start_wipe_pos, avoid_bbx, shared_printable_area(gcodegen)); for (size_t i = 0; i < travel_polyline.points.size(); ++i) { const auto &p = travel_polyline.points[i]; diff --git a/src/libslic3r/GCode.hpp b/src/libslic3r/GCode.hpp index 6346334889..6bdb04a8a9 100644 --- a/src/libslic3r/GCode.hpp +++ b/src/libslic3r/GCode.hpp @@ -130,11 +130,11 @@ public: private: WipeTowerIntegration& operator=(const WipeTowerIntegration&); 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; + Polyline generate_path_to_wipe_tower(const Point &start_pos, const Point &end_pos, const BoundingBox &avoid_polygon, const Polygons &bed_polygons) 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; + Polygons shared_printable_area(GCode &gcodegen) const; // Postprocesses gcode: rotates and moves G1 extrusions and returns result std::string post_process_wipe_tower_moves(const WipeTower::ToolChangeResult& tcr, const Vec2f& translation, float angle) const; diff --git a/src/libslic3r/GCode/WipeTower.cpp b/src/libslic3r/GCode/WipeTower.cpp index b97e773e63..5834937169 100644 --- a/src/libslic3r/GCode/WipeTower.cpp +++ b/src/libslic3r/GCode/WipeTower.cpp @@ -1630,25 +1630,56 @@ float WipeTower::get_auto_brim_by_height(float max_height) { return 8.f; } -Vec2f WipeTower::move_box_inside_box(const BoundingBox &box1, const BoundingBox &box2,int scaled_offset) +Vec2f WipeTower::move_box_inside_polygon(const BoundingBox &box, const Polygons &polygons, coord_t offset) { - Vec2f res{0, 0}; - if (box1.size()[0] >= box2.size()[0]- 2*scaled_offset || box1.size()[1] >= box2.size()[1]-2*scaled_offset) return res; + if (polygons.empty()) return Vec2f{0.f, 0.f}; - if (box1.max[0] > box2.max[0] - scaled_offset) { - res[0] = unscaled((box2.max[0] - scaled_offset) - box1.max[0]); - } - else if (box1.min[0] < box2.min[0] + scaled_offset) { - res[0] = unscaled((box2.min[0] + scaled_offset) - box1.min[0]); + const BoundingBox bed = get_extents(polygons); + // No position fits the footprint. + if (box.size().x() >= bed.size().x() - 2 * offset || box.size().y() >= bed.size().y() - 2 * offset) + return Vec2f{0.f, 0.f}; + + // Clamp against the bounding box first, moving only along the axis that is violated so a dragged + // prime tower slides along the bed edge instead of jumping inwards. + Point shift(0, 0); + for (int axis = 0; axis < 2; ++axis) { + if (box.max[axis] > bed.max[axis] - offset) + shift[axis] = (bed.max[axis] - offset) - box.max[axis]; + else if (box.min[axis] < bed.min[axis] + offset) + shift[axis] = (bed.min[axis] + offset) - box.min[axis]; } - if (box1.max[1] > box2.max[1] - scaled_offset) { - res[1] = unscaled((box2.max[1] - scaled_offset) - box1.max[1]); + // A bed that fills its own bounding box is fully clamped by that, so every rectangular bed — all + // but the delta-style profiles — stops here and keeps its historic placement, including when a + // negative margin lets the footprint hang over the edge. The tolerance is relative because an + // exact rectangle loses a few ulps once the areas are squared world coordinates. + double area = 0.; + for (const Polygon &poly : polygons) area += std::abs(poly.area()); + const double bed_area = double(bed.size().x()) * double(bed.size().y()); + if (area >= bed_area * (1. - EPSILON)) return unscaled(shift); + + // Clamp a negative margin (an auto brim width that has not been resolved yet) to zero: padding by + // it would shrink the footprint and hand back a position the validation still rejects. The + // epsilon lets the move's round trip through millimeters land on the outline without counting as + // a violation. + BoundingBox padded = box.inflated(std::max(offset, 0) - SCALED_EPSILON); + padded.translate(shift); + auto fits = [&padded, &polygons](const Point &move) { + BoundingBox moved = padded; + moved.translate(move); + return diff(Polygons{moved.polygon()}, polygons).empty(); + }; + if (fits(Point(0, 0))) return unscaled(shift); + + // Walk towards the middle of the bed. On every non-rectangular bed we ship, the fitting positions + // form a convex region around it, so bisecting stops just inside the outline. + Point lo(0, 0), hi = bed.center() - padded.center(); + if (!fits(hi)) return unscaled(shift); + for (int i = 0; i < 12; ++i) { + const Point mid = (lo + hi) / 2; + if (fits(mid)) hi = mid; else lo = mid; } - else if (box1.min[1] < box2.min[1] + scaled_offset) { - res[1] = unscaled((box2.min[1] + scaled_offset) - box1.min[1]); - } - return res; + return unscaled(Point(shift + hi)); } Polygon WipeTower::rib_section(float width, float depth, float rib_length, float rib_width,bool fillet_wall) diff --git a/src/libslic3r/GCode/WipeTower.hpp b/src/libslic3r/GCode/WipeTower.hpp index 0819a04f10..045c82cbf3 100644 --- a/src/libslic3r/GCode/WipeTower.hpp +++ b/src/libslic3r/GCode/WipeTower.hpp @@ -45,7 +45,11 @@ public: static TriangleMesh its_make_rib_tower(float width, float depth, float height, float rib_length, float rib_width, bool fillet_wall); static TriangleMesh its_make_rib_brim(const Polygon& brim, float layer_height); static Polygon rib_section(float width, float depth, float rib_length, float rib_width, bool fillet_wall); - static Vec2f move_box_inside_box(const BoundingBox &box1, const BoundingBox &box2, int offset = 0); + // Translation that brings a footprint inside the printable outline, padded by offset. The prime + // tower is validated against the real outline (see layered_print_cleareance_valid), so clamping + // against the bounding box alone would leave it off a delta or hexagonal bed. box and polygons + // must share one scaled coordinate frame; the translation comes back in millimeters. + static Vec2f move_box_inside_polygon(const BoundingBox &box, const Polygons &polygons, coord_t offset = 0); static Polygon rounding_polygon(Polygon &polygon, double rounding = 2., double angle_tol = 30. / 180. * PI); struct Extrusion { diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index 61025c89d7..6a34cc31ff 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -1048,13 +1048,14 @@ static StringObjectException layered_print_cleareance_valid(const Print &print, wipe_tower_convex_hull.points.emplace_back(scale_(x + width), scale_(y)); wipe_tower_convex_hull.points.emplace_back(scale_(x + width), scale_(y + depth)); wipe_tower_convex_hull.points.emplace_back(scale_(x), scale_(y + depth)); - wipe_tower_convex_hull.rotate(a); + wipe_tower_convex_hull.rotate(Geometry::deg2rad(a), Point(scale_(x), scale_(y))); convex_hulls_temp.push_back(wipe_tower_convex_hull); } else { //here, wipe_tower_polygon is not always convex. Polygon wipe_tower_polygon; if (print.wipe_tower_data().wipe_tower_mesh_data) wipe_tower_polygon = print.wipe_tower_data().wipe_tower_mesh_data->bottom; + wipe_tower_polygon.rotate(Geometry::deg2rad(a)); wipe_tower_polygon.translate(Point(scale_(x), scale_(y))); convex_hulls_temp.push_back(wipe_tower_polygon); } @@ -1073,6 +1074,22 @@ static StringObjectException layered_print_cleareance_valid(const Print &print, if (print_config.enable_wrapping_detection.value && !intersection({wrapping_poly}, convex_hulls_temp).empty()) { return {L("Prime Tower") + L(" is too close to clumping detection area, and collisions will be caused.\n")}; } + // Skip the containment check for towers that will never be printed (single-filament + // prints without smooth timelapse keep the config's tower position but emit nothing). + // Pre-generation only the body square is tested — the auto-brim estimate can overshoot + // the generated brim by several mm and must not hard-fail a print that physically fits. + // Post-generation the mesh bottom already includes the real brim, so the exact + // footprint is tested. + if (filaments_count > 1 || print.enable_timelapse_print()) { + // The shared printable polygon is plate-local, while the tower polygons above are + // already shifted by the plate origin. + Polygons printable_polys = print.get_extruder_shared_printable_polygon(); + const Point plate_shift(scale_(plate_origin.x()), scale_(plate_origin.y())); + for (Polygon &p : printable_polys) + p.translate(plate_shift); + if (!diff(convex_hulls_temp, printable_polys).empty()) + return {L("Prime Tower") + L(" is partially outside the printable area, and it cannot be printed.\n")}; + } return {}; } diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp index 110b6697ae..45c0d87791 100644 --- a/src/slic3r/GUI/GLCanvas3D.cpp +++ b/src/slic3r/GUI/GLCanvas3D.cpp @@ -2899,47 +2899,23 @@ void GLCanvas3D::reload_scene(bool refresh_immediately, bool force_full_scene_re int nozzle_nums = wxGetApp().preset_bundle->get_printer_extruder_count(); Vec3d wipe_tower_size = ppl.get_plate(plate_id)->estimate_wipe_tower_size(print_cfg, w, v, nozzle_nums, 0, false, dynamic_cast(dconfig.option("enable_wrapping_detection"))->value); - { - const float margin = WIPE_TOWER_MARGIN + brim_width; - BoundingBoxf3 plate_bbox = part_plate->get_bounding_box(); - BoundingBoxf plate_bbox_2d(Vec2d(plate_bbox.min(0), plate_bbox.min(1)), Vec2d(plate_bbox.max(0), plate_bbox.max(1))); - const std::vector &extruder_areas = part_plate->get_extruder_areas(); - for (Pointfs points : extruder_areas) { - BoundingBoxf bboxf(points); - plate_bbox_2d.min = plate_bbox_2d.min(0) >= bboxf.min(0) ? plate_bbox_2d.min : bboxf.min; - plate_bbox_2d.max = plate_bbox_2d.max(0) <= bboxf.max(0) ? plate_bbox_2d.max : bboxf.max; - } - - coordf_t plate_bbox_x_min_local_coord = plate_bbox_2d.min(0) - plate_origin(0); - coordf_t plate_bbox_x_max_local_coord = plate_bbox_2d.max(0) - plate_origin(0); - coordf_t plate_bbox_y_max_local_coord = plate_bbox_2d.max(1) - plate_origin(1); - - if (!current_print->is_step_done(psWipeTower) || !current_print->wipe_tower_data().wipe_tower_mesh_data) { - // update for wipe tower position - { - int volume_idx_wipe_tower_new = m_volumes.load_wipe_tower_preview(1000 + plate_id, x + plate_origin(0), y + plate_origin(1), - (float) wipe_tower_size(0), (float) wipe_tower_size(1), (float) wipe_tower_size(2), - a, - /*!print->is_step_done(psWipeTower)*/ true, brim_width); - int volume_idx_wipe_tower_old = volume_idxs_wipe_tower_old[plate_id]; - if (volume_idx_wipe_tower_old != -1) map_glvolume_old_to_new[volume_idx_wipe_tower_old] = volume_idx_wipe_tower_new; - } - } else { - const float margin = 2.f; - auto tower_bottom = current_print->wipe_tower_data().wipe_tower_mesh_data->bottom; - tower_bottom.translate(scaled(Vec2d{x, y})); - tower_bottom.translate(scaled(Vec2d{plate_origin[0], plate_origin[1]})); - auto tower_bottom_bbox = get_extents(tower_bottom); - BoundingBoxf3 plate_bbox = wxGetApp().plater()->get_partplate_list().get_plate(plate_id)->get_build_volume(true); - BoundingBox plate_bbox2d = BoundingBox(scaled(Vec2f(plate_bbox.min[0], plate_bbox.min[1])), scaled(Vec2f(plate_bbox.max[0], plate_bbox.max[1]))); - Vec2f offset = WipeTower::move_box_inside_box(tower_bottom_bbox, plate_bbox2d, scaled(margin)); - int volume_idx_wipe_tower_new = m_volumes.load_real_wipe_tower_preview(1000 + plate_id, x + plate_origin(0), y + plate_origin(1), - current_print->wipe_tower_data().wipe_tower_mesh_data->real_wipe_tower_mesh, - current_print->wipe_tower_data().wipe_tower_mesh_data->real_brim_mesh, - true,a,/*!print->is_step_done(psWipeTower)*/ true, m_initialized); - int volume_idx_wipe_tower_old = volume_idxs_wipe_tower_old[plate_id]; - if (volume_idx_wipe_tower_old != -1) map_glvolume_old_to_new[volume_idx_wipe_tower_old] = volume_idx_wipe_tower_new; - } + // The stored position is already clamped onto the bed, by + // set_default_wipe_tower_pos_for_plate and again on every drag. + if (!current_print->is_step_done(psWipeTower) || !current_print->wipe_tower_data().wipe_tower_mesh_data) { + // update for wipe tower position + int volume_idx_wipe_tower_new = m_volumes.load_wipe_tower_preview(1000 + plate_id, x + plate_origin(0), y + plate_origin(1), + (float) wipe_tower_size(0), (float) wipe_tower_size(1), (float) wipe_tower_size(2), + a, + /*!print->is_step_done(psWipeTower)*/ true, brim_width); + int volume_idx_wipe_tower_old = volume_idxs_wipe_tower_old[plate_id]; + if (volume_idx_wipe_tower_old != -1) map_glvolume_old_to_new[volume_idx_wipe_tower_old] = volume_idx_wipe_tower_new; + } else { + int volume_idx_wipe_tower_new = m_volumes.load_real_wipe_tower_preview(1000 + plate_id, x + plate_origin(0), y + plate_origin(1), + current_print->wipe_tower_data().wipe_tower_mesh_data->real_wipe_tower_mesh, + current_print->wipe_tower_data().wipe_tower_mesh_data->real_brim_mesh, + true,a,/*!print->is_step_done(psWipeTower)*/ true, m_initialized); + int volume_idx_wipe_tower_old = volume_idxs_wipe_tower_old[plate_id]; + if (volume_idx_wipe_tower_old != -1) map_glvolume_old_to_new[volume_idx_wipe_tower_old] = volume_idx_wipe_tower_new; } } } diff --git a/src/slic3r/GUI/PartPlate.cpp b/src/slic3r/GUI/PartPlate.cpp index 4f57c2577d..3ab764aaf3 100644 --- a/src/slic3r/GUI/PartPlate.cpp +++ b/src/slic3r/GUI/PartPlate.cpp @@ -3337,6 +3337,11 @@ BoundingBoxf3 PartPlate::get_build_volume(bool use_share) return plate_box; } +Polygon PartPlate::get_shared_printable_polygon() const +{ + return m_extruder_areas.empty() ? Polygon::new_scale(m_shape) : get_shared_poly(m_extruder_areas); +} + bool PartPlate::contains(const Vec3d& point) const { return m_bounding_box.contains(point); @@ -4412,6 +4417,18 @@ void PartPlateList::set_default_wipe_tower_pos_for_plate(int plate_idx, bool ini } } + // The bounding box above still allows a corner a delta or hexagonal bed does not have, and the + // prime tower is validated against the real outline — pull it onto the bed before storing. + { + Polygons bed{part_plate->get_shared_printable_polygon()}; + bed.front().translate(Point(-scaled(plate_origin.x()), -scaled(plate_origin.y()))); // into the frame x/y live in + const BoundingBox tower(Point::new_scale(x, y), + Point::new_scale(x + wipe_tower_size(0), y + wipe_tower_size(1))); + const Vec2f move = WipeTower::move_box_inside_polygon(tower, bed, scaled(margin)); + x += move.x(); + y += move.y(); + } + ConfigOptionFloat wt_x_opt(x); ConfigOptionFloat wt_y_opt(y); dynamic_cast(proj_cfg.option("wipe_tower_x"))->set_at(&wt_x_opt, plate_idx, 0); diff --git a/src/slic3r/GUI/PartPlate.hpp b/src/slic3r/GUI/PartPlate.hpp index 8f4055706f..47481dcad4 100644 --- a/src/slic3r/GUI/PartPlate.hpp +++ b/src/slic3r/GUI/PartPlate.hpp @@ -425,6 +425,9 @@ public: const BoundingBox get_bounding_box_crd(); BoundingBoxf3 get_plate_box() {return get_build_volume();} BoundingBoxf3 get_build_volume(bool use_share = false); + // Polygon counterpart of get_build_volume(true), in scaled world coordinates. The bounding box + // that one returns hides the corners a non-rectangular bed does not have. + Polygon get_shared_printable_polygon() const; const std::vector& get_exclude_areas() { return m_exclude_bounding_box; } diff --git a/src/slic3r/GUI/Selection.cpp b/src/slic3r/GUI/Selection.cpp index 5d6a545873..b6d7abde21 100644 --- a/src/slic3r/GUI/Selection.cpp +++ b/src/slic3r/GUI/Selection.cpp @@ -1270,9 +1270,7 @@ void Selection::translate(const Vec3d &displacement, TransformationType transfor } else { if (v.is_wipe_tower) {//in world cs int plate_idx = v.object_idx() - 1000; - BoundingBoxf3 plate_bbox = wxGetApp().plater()->get_partplate_list().get_plate(plate_idx)->get_build_volume(true); - BoundingBox plate_bbox2d = BoundingBox(scaled(Vec2f(plate_bbox.min[0], plate_bbox.min[1])), scaled(Vec2f(plate_bbox.max[0], plate_bbox.max[1]))); - Vec3d tower_size = v.bounding_box().size(); + const Polygons bed_polys{wxGetApp().plater()->get_partplate_list().get_plate(plate_idx)->get_shared_printable_polygon()}; Vec3d tower_origin = m_cache.volumes_data[i].get_volume_position(); Vec3d actual_displacement = displacement; bool show_read_wipe_tower = wxGetApp().plater()->get_partplate_list().get_plate(plate_idx)->fff_print()->is_step_done(psWipeTower); @@ -1287,18 +1285,7 @@ void Selection::translate(const Vec3d &displacement, TransformationType transfor BoundingBoxf3 tower_bbox = v.bounding_box(); tower_bbox.translate(actual_displacement + tower_origin); BoundingBox tower_bbox2d = BoundingBox(scaled(Vec2f(tower_bbox.min[0], tower_bbox.min[1])), scaled(Vec2f(tower_bbox.max[0], tower_bbox.max[1]))); - Vec2f offset = WipeTower::move_box_inside_box(tower_bbox2d, plate_bbox2d,scaled(margin)); - //if (tower_origin(0) + actual_displacement(0) - margin < plate_bbox.min(0)) { - // actual_displacement(0) = plate_bbox.min(0) - tower_origin(0) + margin; - //} else if (tower_origin(0) + actual_displacement(0) + tower_size(0) + margin > plate_bbox.max(0)) { - // actual_displacement(0) = plate_bbox.max(0) - tower_origin(0) - tower_size(0) - margin; - //} - - //if (tower_origin(1) + actual_displacement(1) - margin < plate_bbox.min(1)) { - // actual_displacement(1) = plate_bbox.min(1) - tower_origin(1) + margin; - //} else if (tower_origin(1) + actual_displacement(1) + tower_size(1) + margin > plate_bbox.max(1)) { - // actual_displacement(1) = plate_bbox.max(1) - tower_origin(1) - tower_size(1) - margin; - //} + const Vec2f offset = WipeTower::move_box_inside_polygon(tower_bbox2d, bed_polys, scaled(margin)); actual_displacement += Vec3d(offset[0], offset[1],0); v.set_volume_offset(m_cache.volumes_data[i].get_volume_position() + actual_displacement); } diff --git a/tests/fff_print/test_wipe_tower.cpp b/tests/fff_print/test_wipe_tower.cpp index bb9e4781d1..2bd7ac4189 100644 --- a/tests/fff_print/test_wipe_tower.cpp +++ b/tests/fff_print/test_wipe_tower.cpp @@ -3,6 +3,8 @@ #include #include +#include "libslic3r/BoundingBox.hpp" +#include "libslic3r/ClipperUtils.hpp" #include "libslic3r/GCode/GCodeProcessor.hpp" #include "libslic3r/GCode/WipeTower.hpp" #include "libslic3r/PrintConfig.hpp" @@ -53,6 +55,65 @@ TEST_CASE("Other flavors wait in the wipe tower with a seconds dwell", "[WipeTow CHECK(wait_command(flavor, 1.5f) == "G4 S1.500\n"); } +// The prime tower is validated against the real printable outline, so the placement clamps have to +// agree with it wherever that outline is not a rectangle. A regular hexagon inscribed in a 200mm +// circle stands in for the shipped delta beds. +TEST_CASE("The wipe tower placement clamp follows a non-rectangular bed outline", "[WipeTower]") +{ + const coord_t margin = scaled(1.); + auto square_at = [](double x, double y, double side) { + return BoundingBox(Point::new_scale(x, y), Point::new_scale(x + side, y + side)); + }; + // Does the footprint, padded by pad, sit inside the outline once the returned move is applied? + auto lands_inside = [](BoundingBox box, const Polygons &bed, const Vec2f &move, coord_t pad) { + box.translate(Point::new_scale(move.x(), move.y())); + return diff(Polygons{box.inflated(pad).polygon()}, bed).empty(); + }; + + const Polygons hex_bed{make_circle_num_segments(scaled(100.), 6)}; + const Polygons square_bed{Polygon::new_scale(Pointfs{{0., 0.}, {200., 0.}, {200., 200.}, {0., 200.}})}; + + SECTION("a rectangular bed is left to the bounding box clamp") { + const Vec2f move = WipeTower::move_box_inside_polygon(square_at(50., 50., 30.), square_bed, margin); + CHECK_THAT(move.x(), Catch::Matchers::WithinAbs(0., 1e-6)); + CHECK_THAT(move.y(), Catch::Matchers::WithinAbs(0., 1e-6)); + } + + // Dragging the tower off one edge may not pull it away from the other, or it would jump out from + // under the cursor instead of sliding along the edge. + SECTION("only the violated axis is clamped") { + const Vec2f move = WipeTower::move_box_inside_polygon(square_at(185., 50., 30.), square_bed, margin); + CHECK_THAT(move.x(), Catch::Matchers::WithinAbs(-16., 1e-6)); + CHECK_THAT(move.y(), Catch::Matchers::WithinAbs(0., 1e-6)); + } + + SECTION("a footprint already inside the outline is left alone") { + const Vec2f move = WipeTower::move_box_inside_polygon(square_at(-15., -15., 30.), hex_bed, margin); + CHECK_THAT(move.x(), Catch::Matchers::WithinAbs(0., 1e-6)); + CHECK_THAT(move.y(), Catch::Matchers::WithinAbs(0., 1e-6)); + } + + SECTION("a footprint in the bounding box corner is pulled onto the bed") { + const BoundingBox box = square_at(55., 50., 30.); + REQUIRE_FALSE(lands_inside(box, hex_bed, Vec2f::Zero(), margin)); // in the bbox, off the hexagon + CHECK(lands_inside(box, hex_bed, WipeTower::move_box_inside_polygon(box, hex_bed, margin), margin)); + } + + // An unresolved auto brim width reaches the drag clamp as a negative margin. Padding by it would + // shrink the footprint and hand back a position the slice validation still rejects. + SECTION("a negative margin still lands the footprint inside the outline") { + const BoundingBox box = square_at(55., 50., 30.); + const coord_t brim = scaled(-0.5); + CHECK(lands_inside(box, hex_bed, WipeTower::move_box_inside_polygon(box, hex_bed, brim), 0)); + } + + SECTION("a footprint too large for the bed is left alone") { + const Vec2f move = WipeTower::move_box_inside_polygon(square_at(-200., -200., 400.), hex_bed, margin); + CHECK_THAT(move.x(), Catch::Matchers::WithinAbs(0., 1e-6)); + CHECK_THAT(move.y(), Catch::Matchers::WithinAbs(0., 1e-6)); + } +} + // The cases above only exercise the helpers in isolation. The one below slices a real // two-filament print, so it also covers the binding constraint of both changes: that the // configured `gcode_flavor` reaches the wipe tower writer and lands in the exported G-code.