From 97e9eb9c348478968231069de2ad0500d831e4a6 Mon Sep 17 00:00:00 2001 From: Hanif Koh Date: Thu, 3 Sep 2026 14:52:12 +0800 Subject: [PATCH] Verify WipeTower Footprint at Point of Generation The clamps and validation work from estimates. Once the tower is generated, _make_wipe_tower re-tests the exact first-layer footprint, brim and cone base included, against the printable area and the exclusion zone, so an off-plate tower fails with a clear error instead of exporting unprintable G-code. The rectangle-wall mesh footprint learns the Type2 cone base so that check and the post-generation validation see the real outline. Pre-generation, validation hard-checks the body plus an explicit brim and warns on the estimated auto brim and cone base with the existing "may collide" strings, so the user hears about a marginal position on the first slice rather than only at generation time. Two fff_print fixtures that print a tower at the default position move it onto the 200 mm test bed, as the multifilament fixtures already do: the shipped default y of 220 is off that bed, and the backstop now says so instead of exporting the tower. --- src/libslic3r/Print.cpp | 87 +++++++++++++++++++++++----- src/libslic3r/Print.hpp | 2 +- tests/fff_print/test_gcodewriter.cpp | 3 + tests/fff_print/test_wipe_tower.cpp | 2 + 4 files changed, 78 insertions(+), 16 deletions(-) diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index 271e410933..1b54a527ea 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -1068,33 +1068,57 @@ static StringObjectException layered_print_cleareance_valid(const Print &print, convex_hulls_temp.push_back(wipe_tower_polygon); } } + // Post-generation the mesh bottom already carries the brim. Pre-generation the body grows + // by the brim only when its width is explicit; the auto brim and a Type2 cone base depend on + // the tower height, exact only once generated, so they only warn here - the exact footprint + // is re-checked in _make_wipe_tower. + const bool exact_footprint = print.is_step_done(psWipeTower); + Polygons tower_polys_checked = (!exact_footprint && config.prime_tower_brim_width.value >= 0) ? + offset(convex_hulls_temp, float(scale_(brim_width))) : + convex_hulls_temp; + Polygons tower_polys_estimated; + if (!exact_footprint && !convex_hulls_temp.empty()) { + Polygon base = convex_hulls_temp.front(); + if (config.wipe_tower_wall_type.value == WipeTowerWallType::wtwCone && print.wipe_tower_type() == WipeTowerType::Type2) { + double max_height = 0.; + for (const PrintObject *object : print.objects()) + max_height = std::max(max_height, unscale_(object->size().z())); + base = WipeTower2::cone_base_polygon(width, depth, max_height, config.wipe_tower_cone_angle.value); + base.rotate(Geometry::deg2rad(a)); + base.translate(Point(scale_(x), scale_(y))); + } + tower_polys_estimated = offset(base, float(scale_(brim_width))); + } + // Object proximity stays a body-only warning: brim near-misses would newly warn on + // many setups that print fine. if (!intersection(convex_hulls_other, convex_hulls_temp).empty()) { if (warning) { warning->string += L("Prime Tower") + L(" is too close to others, and collisions may be caused.\n"); } } - if (!intersection(exclude_polys, convex_hulls_temp).empty()) { - /*if (warning) { - warning->string += L("Prime Tower is too close to exclusion area, there may be collisions when printing.\n"); - }*/ + if (!intersection(exclude_polys, tower_polys_checked).empty()) { return {L("Prime Tower") + L(" is too close to an exclusion area, and collisions will be caused.\n")}; } - if (print_config.enable_wrapping_detection.value && !intersection({wrapping_poly}, convex_hulls_temp).empty()) { + if (print_config.enable_wrapping_detection.value && !intersection({wrapping_poly}, tower_polys_checked).empty()) { return {L("Prime Tower") + L(" is too close to clumping detection area, and collisions will be caused.\n")}; } - // No gate on "is there a tower": one that is not printed estimates to zero, so the hull - // is degenerate and every check passes. Re-deriving it here missed the wrapping-detection + if (warning && !intersection(exclude_polys, tower_polys_estimated).empty()) { + warning->string += L("Prime Tower") + L(" is too close to exclusion area, there may be collisions when printing.") + "\n"; + } + if (warning && print_config.enable_wrapping_detection.value && !intersection({wrapping_poly}, tower_polys_estimated).empty()) { + warning->string += L("Prime Tower") + L(" is too close to clumping detection area, there may be collisions when printing.") + "\n"; + } + // No gate on "is there a tower": one that is not printed estimates to zero, so the hulls + // are degenerate and every check passes. Re-deriving it here missed the wrapping-detection // tower on a single-filament plate. - // Pre-generation, grow the body by the brim to match what the generator draws; - // post-generation the mesh already includes it. 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); - Polygons tower_polys_with_brim = print.is_step_done(psWipeTower) ? - convex_hulls_temp : offset(convex_hulls_temp, float(scale_(brim_width))); - if (!diff(tower_polys_with_brim, printable_polys).empty()) + if (!diff(tower_polys_checked, printable_polys).empty()) return {L("Prime Tower") + L(" is partially outside the printable area, and it cannot be printed.\n")}; + if (warning && !diff(tower_polys_estimated, printable_polys).empty()) + warning->string += L("Prime Tower") + L(" is partially outside the printable area, and it cannot be printed.\n"); return {}; } @@ -4390,7 +4414,9 @@ void Print::_make_wipe_tower() wipe_tower.get_wipe_tower_height(), wipe_tower.get_brim_width(), config().wipe_tower_wall_type.value == WipeTowerWallType::wtwRib, wipe_tower.get_rib_width(), wipe_tower.get_rib_length(), - config().wipe_tower_fillet_wall.value); + config().wipe_tower_fillet_wall.value, + config().wipe_tower_wall_type.value == WipeTowerWallType::wtwCone ? + (float) config().wipe_tower_cone_angle.value : 0.f); const Vec3d origin = Vec3d::Zero(); // FakeWipeTower::pos is a bed-frame translation applied after rotation // (getFakeExtrusionPathsFromWipeTower2 rotates about the local origin), so the @@ -4403,6 +4429,28 @@ void Print::_make_wipe_tower() config().wipe_tower_rotation_angle, config().wipe_tower_cone_angle, {scale_(origin.x()), scale_(origin.y())}); } + + // The clamps and checks above work from estimates; re-test the exact generated footprint + // so an off-plate tower fails with a clear error instead of exporting unprintable G-code + // (validate() only sees the mesh on its next run). + if (m_wipe_tower_data.wipe_tower_mesh_data) { + Polygon footprint = m_wipe_tower_data.wipe_tower_mesh_data->bottom; // includes brim and rib offset + footprint.rotate(Geometry::deg2rad(m_config.wipe_tower_rotation_angle.value)); + footprint.translate(Point(scale_(m_config.wipe_tower_x.get_at(m_plate_index)), + scale_(m_config.wipe_tower_y.get_at(m_plate_index)))); + const Polygons printable_polys = this->get_extruder_shared_printable_polygon(); + if (!printable_polys.empty() && !diff(Polygons{footprint}, printable_polys).empty()) { + const BoundingBox fp = get_extents(footprint); + const BoundingBox pr = get_extents(printable_polys); + BOOST_LOG_TRIVIAL(error) << boost::format("wipe tower footprint [%1%,%2%]-[%3%,%4%] leaves printable [%5%,%6%]-[%7%,%8%]") % + unscaled(fp.min.x()) % unscaled(fp.min.y()) % unscaled(fp.max.x()) % unscaled(fp.max.y()) % + unscaled(pr.min.x()) % unscaled(pr.min.y()) % unscaled(pr.max.x()) % unscaled(pr.max.y()); + throw Slic3r::SlicingError(L("Prime Tower") + L(" is partially outside the printable area, and it cannot be printed.\n")); + } + // The cutter/purge corner is a physical obstacle — the brim must stay out like the body. + if (!intersection(get_bed_excluded_area(m_config), Polygons{footprint}).empty()) + throw Slic3r::SlicingError(L("Prime Tower") + L(" is too close to an exclusion area, and collisions will be caused.\n")); + } } // Generate a recommended G-code output file name based on the format template, default extension, and template parameters @@ -5951,12 +5999,21 @@ ExtrusionLayers FakeWipeTower::getTrueExtrusionLayersFromWipeTower() const } return wtels; } -void WipeTowerData::construct_mesh(float width, float depth, float height, float brim_width, bool is_rib_wipe_tower, float rib_width, float rib_length,bool fillet_wall) +void WipeTowerData::construct_mesh(float width, float depth, float height, float brim_width, bool is_rib_wipe_tower, float rib_width, float rib_length,bool fillet_wall, float cone_angle) { wipe_tower_mesh_data = WipeTowerMeshData{}; float first_layer_height=0.08; //brim height if (width < EPSILON || depth < EPSILON || height < EPSILON) return; - if (!is_rib_wipe_tower || rib_length < EPSILON) { + if (cone_angle > EPSILON && (!is_rib_wipe_tower || rib_length < EPSILON)) { + // Cone tower: the base bulges past the body box; this bottom polygon feeds the + // containment checks, so it must carry the bulge and the brim (cone not lofted). + wipe_tower_mesh_data->real_wipe_tower_mesh = make_cube(width, depth, height); + wipe_tower_mesh_data->bottom = WipeTower2::cone_base_polygon(width, depth, height, cone_angle); + auto brim_bottom = offset(wipe_tower_mesh_data->bottom, scaled(brim_width)); + if (!brim_bottom.empty()) + wipe_tower_mesh_data->bottom = brim_bottom.front(); + wipe_tower_mesh_data->real_brim_mesh = WipeTower::its_make_rib_brim(wipe_tower_mesh_data->bottom, first_layer_height); + } else if (!is_rib_wipe_tower || rib_length < EPSILON) { wipe_tower_mesh_data->real_wipe_tower_mesh = make_cube(width, depth, height); wipe_tower_mesh_data->real_brim_mesh = make_cube(width + 2 * brim_width, depth + 2 * brim_width, first_layer_height); wipe_tower_mesh_data->real_brim_mesh.translate({-brim_width, -brim_width, 0}); diff --git a/src/libslic3r/Print.hpp b/src/libslic3r/Print.hpp index af1dc3af40..9822c5520c 100644 --- a/src/libslic3r/Print.hpp +++ b/src/libslic3r/Print.hpp @@ -804,7 +804,7 @@ struct WipeTowerData rib_offset = Vec2f::Zero(); wipe_tower_mesh_data = std::nullopt; } - void construct_mesh(float width, float depth, float height, float brim_width, bool is_rib_wipe_tower, float rib_width, float rib_length, bool fillet_wall); + void construct_mesh(float width, float depth, float height, float brim_width, bool is_rib_wipe_tower, float rib_width, float rib_length, bool fillet_wall, float cone_angle = 0.f); private: // Only allow the WipeTowerData to be instantiated internally by Print, diff --git a/tests/fff_print/test_gcodewriter.cpp b/tests/fff_print/test_gcodewriter.cpp index b09b38e794..c0c9e794b9 100644 --- a/tests/fff_print/test_gcodewriter.cpp +++ b/tests/fff_print/test_gcodewriter.cpp @@ -574,6 +574,9 @@ static DynamicPrintConfig dual_extruder_toolchange_config() config.set_key_value("nozzle_temperature_range_high", new ConfigOptionInts({240, 240})); config.set_key_value("flush_multiplier", new ConfigOptionFloats({1})); config.set_key_value("flush_volumes_matrix", new ConfigOptionFloats({0, 140, 140, 0})); + // Inside the 200x200 test bed; the default y, 220, is not, and generation rejects that. + config.set_key_value("wipe_tower_x", new ConfigOptionFloats({50.})); + config.set_key_value("wipe_tower_y", new ConfigOptionFloats({50.})); return config; } diff --git a/tests/fff_print/test_wipe_tower.cpp b/tests/fff_print/test_wipe_tower.cpp index 5a248075e4..24d50d6e66 100644 --- a/tests/fff_print/test_wipe_tower.cpp +++ b/tests/fff_print/test_wipe_tower.cpp @@ -152,6 +152,8 @@ static DynamicPrintConfig wipe_tower_toolchange_config(const std::string &gcode_ { "outer_wall_filament_id", 2 }, { "inner_wall_filament_id", 2 }, { "enable_prime_tower", true }, + { "wipe_tower_x", 50 }, // inside the 200x200 test bed + { "wipe_tower_y", 50 }, // (the default y, 220, is not) { "layer_height", 0.3 }, { "gcode_flavor", gcode_flavor }, });