mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-16 13:32:44 +00:00
Keep the prime tower and its approach travel on non-rectangular beds
The placement clamps and the tower-approach router both stood in the bed's bounding box for the bed itself, so on a delta or hexagonal bed the prime tower could be parked in a corner that does not exist and the nozzle could be routed across it. Both now test the real printable outline, slicing reports a tower that does not fit instead of printing it off the bed, and a tower parked near an edge is routed along the clamped side rather than falling back to a straight line across the tower. Also fixes the placement validation rotating the tower hull by degrees read as radians about the plate origin, and never rotating the generated tower footprint at all.
This commit is contained in:
+54
-41
@@ -768,30 +768,31 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
|
|||||||
return changes;
|
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
|
// BBS
|
||||||
// start_pos refers to the last position before the wipe_tower.
|
// start_pos refers to the last position before the wipe_tower.
|
||||||
// end_pos refers to the wipe tower's start_pos.
|
// end_pos refers to the wipe tower's start_pos.
|
||||||
// using the print coordinate system
|
// 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;
|
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;
|
BoundingBox avoid_polygon_inner = avoid_polygon;
|
||||||
avoid_polygon_inner.offset(alpha);
|
avoid_polygon_inner.offset(alpha);
|
||||||
coord_t width = avoid_polygon_inner.max[0] - avoid_polygon_inner.min[0];
|
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.
|
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.
|
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.
|
// 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;
|
Points avoid_points = avoid_polygon_inner.polygon().points;
|
||||||
for (auto &wipe_tower_bbx_p : avoid_points) {
|
const bool is_bbx_in_bed = std::all_of(avoid_points.begin(), avoid_points.end(),
|
||||||
if (ClipperLib::PointInPolygon(wipe_tower_bbx_p, bed_polygon.points) != 1) {
|
[&bed_polygons](const Point &pt) { return contains(bed_polygons, pt, /*border_result=*/false); });
|
||||||
is_bbx_in_bed = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!is_bbx_in_bed) {
|
if (!is_bbx_in_bed) {
|
||||||
res.points.push_back(end_pos);
|
res.points.push_back(end_pos);
|
||||||
return res;
|
return res;
|
||||||
@@ -898,27 +899,17 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
|
|||||||
return Eigen::Rotation2Df(alpha) * (pt + m_rib_offset) + m_wipe_tower_pos;
|
return Eigen::Rotation2Df(alpha) * (pt + m_rib_offset) + m_wipe_tower_pos;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Printable-area bounds for tower-approach routing, in object coordinates (shared by
|
// Bed outline the tower-approach router plans against, in object coordinates. The real
|
||||||
// the BBL avoid-perimeter path in append_tcr and the Type2 skip-points router).
|
// outline is returned, not its bounding box, so the router's containment tests fail off
|
||||||
// Multi-nozzle: clamp the travel bounds to the region every extruder can reach
|
// the bed on circular/custom shapes; the multi-nozzle narrowing lives in the accessor.
|
||||||
// (get_extruder_shared_printable_polygon) instead of the full bed. Gated on the
|
Polygons WipeTowerIntegration::shared_printable_area(GCode &gcodegen) const
|
||||||
// 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));
|
// The frame change is a pure translation, so transform the origin once.
|
||||||
BoundingBox printer_bbx;
|
const Point offset = wipe_tower_point_to_object_point(gcodegen, Vec2f(m_plate_origin(0), m_plate_origin(1)));
|
||||||
if (is_multi_nozzle_printer(gcodegen.m_config)) {
|
Polygons bed_polygons = gcodegen.m_print->get_extruder_shared_printable_polygon();
|
||||||
printer_bbx = get_extents(gcodegen.m_print->get_extruder_shared_printable_polygon());
|
for (Polygon &poly : bed_polygons)
|
||||||
printer_bbx.min = wipe_tower_point_to_object_point(gcodegen, unscaled<float>(printer_bbx.min) + plate_origin_2d);
|
poly.translate(offset);
|
||||||
printer_bbx.max = wipe_tower_point_to_object_point(gcodegen, unscaled<float>(printer_bbx.max) + plate_origin_2d);
|
return bed_polygons;
|
||||||
} 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<float>() + 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
|
// With skip points enabled the Type2 tower wall has an opening at each toolchange's
|
||||||
@@ -933,15 +924,37 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
|
|||||||
if (!WipeTower2::use_gap_wall(gcodegen.m_config))
|
if (!WipeTower2::use_gap_wall(gcodegen.m_config))
|
||||||
return {};
|
return {};
|
||||||
const Vec2f plate_origin_2d(m_plate_origin(0), m_plate_origin(1));
|
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
|
// Transform tower-local corners exactly like the tcr points; a rotated tower gets a
|
||||||
// tower gets a conservative axis-aligned envelope.
|
// conservative axis-aligned envelope from the result.
|
||||||
Polygon avoid_points = scaled(m_wipe_tower_bbx).polygon();
|
auto tower_polygon = [&](const BoundingBoxf &bbx) {
|
||||||
for (auto& p : avoid_points.points)
|
Polygon poly = scaled(bbx).polygon();
|
||||||
p = wipe_tower_point_to_object_point(gcodegen, transform_wt2_pt(unscale(p).cast<float>()) + plate_origin_2d);
|
for (Point &p : poly.points)
|
||||||
BoundingBox avoid_bbx(avoid_points.points);
|
p = wipe_tower_point_to_object_point(gcodegen, transform_wt2_pt(unscale(p).cast<float>()) + plate_origin_2d);
|
||||||
if (avoid_bbx.contains(route_start))
|
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 {};
|
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;
|
std::string gcode;
|
||||||
// The polyline's last point is start_wipe_pos itself — emitted by the caller.
|
// 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)
|
for (size_t i = 0; i + 1 < travel_polyline.points.size(); ++i)
|
||||||
@@ -1322,7 +1335,7 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
|
|||||||
Vec2f gcode_last_pos2d{gcode_last_pos[0], gcode_last_pos[1]};
|
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<double>() + plate_origin_2d.cast<double>());
|
Point gcode_last_pos2d_object = gcodegen.gcode_to_point(gcode_last_pos2d.cast<double>() + plate_origin_2d.cast<double>());
|
||||||
Point start_wipe_pos = wipe_tower_point_to_object_point(gcodegen, tool_change_start_pos + plate_origin_2d);
|
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
|
// set avoid_bbx
|
||||||
avoid_bbx = scaled(m_wipe_tower_bbx);
|
avoid_bbx = scaled(m_wipe_tower_bbx);
|
||||||
@@ -1334,7 +1347,7 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
|
|||||||
avoid_bbx = BoundingBox(avoid_points.points);
|
avoid_bbx = BoundingBox(avoid_points.points);
|
||||||
}
|
}
|
||||||
std::string travel_to_wipe_tower_gcode;
|
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) {
|
for (size_t i = 0; i < travel_polyline.points.size(); ++i) {
|
||||||
const auto &p = travel_polyline.points[i];
|
const auto &p = travel_polyline.points[i];
|
||||||
|
|||||||
@@ -130,11 +130,11 @@ public:
|
|||||||
private:
|
private:
|
||||||
WipeTowerIntegration& operator=(const WipeTowerIntegration&);
|
WipeTowerIntegration& operator=(const WipeTowerIntegration&);
|
||||||
std::string append_tcr(GCode &gcodegen, const WipeTower::ToolChangeResult &tcr, int new_extruder_id, double z = -1.) const;
|
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 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;
|
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;
|
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
|
// 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;
|
std::string post_process_wipe_tower_moves(const WipeTower::ToolChangeResult& tcr, const Vec2f& translation, float angle) const;
|
||||||
|
|||||||
@@ -1630,25 +1630,56 @@ float WipeTower::get_auto_brim_by_height(float max_height) {
|
|||||||
return 8.f;
|
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 (polygons.empty()) return Vec2f{0.f, 0.f};
|
||||||
if (box1.size()[0] >= box2.size()[0]- 2*scaled_offset || box1.size()[1] >= box2.size()[1]-2*scaled_offset) return res;
|
|
||||||
|
|
||||||
if (box1.max[0] > box2.max[0] - scaled_offset) {
|
const BoundingBox bed = get_extents(polygons);
|
||||||
res[0] = unscaled<float>((box2.max[0] - scaled_offset) - box1.max[0]);
|
// No position fits the footprint.
|
||||||
}
|
if (box.size().x() >= bed.size().x() - 2 * offset || box.size().y() >= bed.size().y() - 2 * offset)
|
||||||
else if (box1.min[0] < box2.min[0] + scaled_offset) {
|
return Vec2f{0.f, 0.f};
|
||||||
res[0] = unscaled<float>((box2.min[0] + scaled_offset) - box1.min[0]);
|
|
||||||
|
// 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) {
|
// A bed that fills its own bounding box is fully clamped by that, so every rectangular bed — all
|
||||||
res[1] = unscaled<float>((box2.max[1] - scaled_offset) - box1.max[1]);
|
// 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<float>(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<coord_t>(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<float>(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<float>(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) {
|
return unscaled<float>(Point(shift + hi));
|
||||||
res[1] = unscaled<float>((box2.min[1] + scaled_offset) - box1.min[1]);
|
|
||||||
}
|
|
||||||
return res;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Polygon WipeTower::rib_section(float width, float depth, float rib_length, float rib_width,bool fillet_wall)
|
Polygon WipeTower::rib_section(float width, float depth, float rib_length, float rib_width,bool fillet_wall)
|
||||||
|
|||||||
@@ -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_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 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 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);
|
static Polygon rounding_polygon(Polygon &polygon, double rounding = 2., double angle_tol = 30. / 180. * PI);
|
||||||
struct Extrusion
|
struct Extrusion
|
||||||
{
|
{
|
||||||
|
|||||||
+18
-1
@@ -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));
|
||||||
wipe_tower_convex_hull.points.emplace_back(scale_(x + width), scale_(y + depth));
|
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.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);
|
convex_hulls_temp.push_back(wipe_tower_convex_hull);
|
||||||
} else {
|
} else {
|
||||||
//here, wipe_tower_polygon is not always convex.
|
//here, wipe_tower_polygon is not always convex.
|
||||||
Polygon wipe_tower_polygon;
|
Polygon wipe_tower_polygon;
|
||||||
if (print.wipe_tower_data().wipe_tower_mesh_data)
|
if (print.wipe_tower_data().wipe_tower_mesh_data)
|
||||||
wipe_tower_polygon = print.wipe_tower_data().wipe_tower_mesh_data->bottom;
|
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)));
|
wipe_tower_polygon.translate(Point(scale_(x), scale_(y)));
|
||||||
convex_hulls_temp.push_back(wipe_tower_polygon);
|
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()) {
|
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")};
|
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 {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
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<const ConfigOptionBool*>(dconfig.option("enable_wrapping_detection"))->value);
|
Vec3d wipe_tower_size = ppl.get_plate(plate_id)->estimate_wipe_tower_size(print_cfg, w, v, nozzle_nums, 0, false, dynamic_cast<const ConfigOptionBool*>(dconfig.option("enable_wrapping_detection"))->value);
|
||||||
|
|
||||||
{
|
// The stored position is already clamped onto the bed, by
|
||||||
const float margin = WIPE_TOWER_MARGIN + brim_width;
|
// set_default_wipe_tower_pos_for_plate and again on every drag.
|
||||||
BoundingBoxf3 plate_bbox = part_plate->get_bounding_box();
|
if (!current_print->is_step_done(psWipeTower) || !current_print->wipe_tower_data().wipe_tower_mesh_data) {
|
||||||
BoundingBoxf plate_bbox_2d(Vec2d(plate_bbox.min(0), plate_bbox.min(1)), Vec2d(plate_bbox.max(0), plate_bbox.max(1)));
|
// update for wipe tower position
|
||||||
const std::vector<Pointfs> &extruder_areas = part_plate->get_extruder_areas();
|
int volume_idx_wipe_tower_new = m_volumes.load_wipe_tower_preview(1000 + plate_id, x + plate_origin(0), y + plate_origin(1),
|
||||||
for (Pointfs points : extruder_areas) {
|
(float) wipe_tower_size(0), (float) wipe_tower_size(1), (float) wipe_tower_size(2),
|
||||||
BoundingBoxf bboxf(points);
|
a,
|
||||||
plate_bbox_2d.min = plate_bbox_2d.min(0) >= bboxf.min(0) ? plate_bbox_2d.min : bboxf.min;
|
/*!print->is_step_done(psWipeTower)*/ true, brim_width);
|
||||||
plate_bbox_2d.max = plate_bbox_2d.max(0) <= bboxf.max(0) ? plate_bbox_2d.max : bboxf.max;
|
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 {
|
||||||
coordf_t plate_bbox_x_min_local_coord = plate_bbox_2d.min(0) - plate_origin(0);
|
int volume_idx_wipe_tower_new = m_volumes.load_real_wipe_tower_preview(1000 + plate_id, x + plate_origin(0), y + plate_origin(1),
|
||||||
coordf_t plate_bbox_x_max_local_coord = plate_bbox_2d.max(0) - plate_origin(0);
|
current_print->wipe_tower_data().wipe_tower_mesh_data->real_wipe_tower_mesh,
|
||||||
coordf_t plate_bbox_y_max_local_coord = plate_bbox_2d.max(1) - plate_origin(1);
|
current_print->wipe_tower_data().wipe_tower_mesh_data->real_brim_mesh,
|
||||||
|
true,a,/*!print->is_step_done(psWipeTower)*/ true, m_initialized);
|
||||||
if (!current_print->is_step_done(psWipeTower) || !current_print->wipe_tower_data().wipe_tower_mesh_data) {
|
int volume_idx_wipe_tower_old = volume_idxs_wipe_tower_old[plate_id];
|
||||||
// update for wipe tower position
|
if (volume_idx_wipe_tower_old != -1) map_glvolume_old_to_new[volume_idx_wipe_tower_old] = volume_idx_wipe_tower_new;
|
||||||
{
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3337,6 +3337,11 @@ BoundingBoxf3 PartPlate::get_build_volume(bool use_share)
|
|||||||
return plate_box;
|
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
|
bool PartPlate::contains(const Vec3d& point) const
|
||||||
{
|
{
|
||||||
return m_bounding_box.contains(point);
|
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<coord_t>(margin));
|
||||||
|
x += move.x();
|
||||||
|
y += move.y();
|
||||||
|
}
|
||||||
|
|
||||||
ConfigOptionFloat wt_x_opt(x);
|
ConfigOptionFloat wt_x_opt(x);
|
||||||
ConfigOptionFloat wt_y_opt(y);
|
ConfigOptionFloat wt_y_opt(y);
|
||||||
dynamic_cast<ConfigOptionFloats *>(proj_cfg.option("wipe_tower_x"))->set_at(&wt_x_opt, plate_idx, 0);
|
dynamic_cast<ConfigOptionFloats *>(proj_cfg.option("wipe_tower_x"))->set_at(&wt_x_opt, plate_idx, 0);
|
||||||
|
|||||||
@@ -425,6 +425,9 @@ public:
|
|||||||
const BoundingBox get_bounding_box_crd();
|
const BoundingBox get_bounding_box_crd();
|
||||||
BoundingBoxf3 get_plate_box() {return get_build_volume();}
|
BoundingBoxf3 get_plate_box() {return get_build_volume();}
|
||||||
BoundingBoxf3 get_build_volume(bool use_share = false);
|
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<BoundingBoxf3>& get_exclude_areas() { return m_exclude_bounding_box; }
|
const std::vector<BoundingBoxf3>& get_exclude_areas() { return m_exclude_bounding_box; }
|
||||||
|
|
||||||
|
|||||||
@@ -1270,9 +1270,7 @@ void Selection::translate(const Vec3d &displacement, TransformationType transfor
|
|||||||
} else {
|
} else {
|
||||||
if (v.is_wipe_tower) {//in world cs
|
if (v.is_wipe_tower) {//in world cs
|
||||||
int plate_idx = v.object_idx() - 1000;
|
int plate_idx = v.object_idx() - 1000;
|
||||||
BoundingBoxf3 plate_bbox = wxGetApp().plater()->get_partplate_list().get_plate(plate_idx)->get_build_volume(true);
|
const Polygons bed_polys{wxGetApp().plater()->get_partplate_list().get_plate(plate_idx)->get_shared_printable_polygon()};
|
||||||
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();
|
|
||||||
Vec3d tower_origin = m_cache.volumes_data[i].get_volume_position();
|
Vec3d tower_origin = m_cache.volumes_data[i].get_volume_position();
|
||||||
Vec3d actual_displacement = displacement;
|
Vec3d actual_displacement = displacement;
|
||||||
bool show_read_wipe_tower = wxGetApp().plater()->get_partplate_list().get_plate(plate_idx)->fff_print()->is_step_done(psWipeTower);
|
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();
|
BoundingBoxf3 tower_bbox = v.bounding_box();
|
||||||
tower_bbox.translate(actual_displacement + tower_origin);
|
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])));
|
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));
|
const Vec2f offset = WipeTower::move_box_inside_polygon(tower_bbox2d, bed_polys, 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;
|
|
||||||
//}
|
|
||||||
actual_displacement += Vec3d(offset[0], offset[1],0);
|
actual_displacement += Vec3d(offset[0], offset[1],0);
|
||||||
v.set_volume_offset(m_cache.volumes_data[i].get_volume_position() + actual_displacement);
|
v.set_volume_offset(m_cache.volumes_data[i].get_volume_position() + actual_displacement);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
|
#include "libslic3r/BoundingBox.hpp"
|
||||||
|
#include "libslic3r/ClipperUtils.hpp"
|
||||||
#include "libslic3r/GCode/GCodeProcessor.hpp"
|
#include "libslic3r/GCode/GCodeProcessor.hpp"
|
||||||
#include "libslic3r/GCode/WipeTower.hpp"
|
#include "libslic3r/GCode/WipeTower.hpp"
|
||||||
#include "libslic3r/PrintConfig.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");
|
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<coord_t>(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<double>(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<coord_t>(-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
|
// 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
|
// 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.
|
// configured `gcode_flavor` reaches the wipe tower writer and lands in the exported G-code.
|
||||||
|
|||||||
Reference in New Issue
Block a user