diff --git a/src/libslic3r/GCode/WipeTowerEstimate.cpp b/src/libslic3r/GCode/WipeTowerEstimate.cpp index f40f2899fd..d8cfe74527 100644 --- a/src/libslic3r/GCode/WipeTowerEstimate.cpp +++ b/src/libslic3r/GCode/WipeTowerEstimate.cpp @@ -37,6 +37,17 @@ WipeTowerType resolve_wipe_tower_type(const ConfigBase &config) return type != nullptr ? WipeTowerType(type->getInt()) : WipeTowerType::Type2; } +Polygon estimate_wipe_tower_first_layer_outline(const ConfigBase &config, WipeTowerType tower_type, double width, double depth, double height) +{ + // Type1 ignores the cone option. The wall type is read by value: a preset-shaped config + // holds it as ConfigOptionEnumGeneric, which a cast to ConfigOptionEnum cannot see. + const ConfigOption *wall_type = option_of(config, "wipe_tower_wall_type"); + const ConfigOption *cone_angle = option_of(config, "wipe_tower_cone_angle"); + const bool cone = tower_type == WipeTowerType::Type2 && wall_type != nullptr && + wall_type->getInt() == int(WipeTowerWallType::wtwCone) && cone_angle != nullptr; + return WipeTower2::cone_base_polygon(width, depth, height, cone ? cone_angle->getFloat() : 0.); +} + WipeTowerFootprint estimate_wipe_tower_footprint(const ConfigBase &config, WipeTowerType tower_type, const std::vector &filament_ids, double layer_height, double max_object_height) { WipeTowerFootprint footprint; diff --git a/src/libslic3r/GCode/WipeTowerEstimate.hpp b/src/libslic3r/GCode/WipeTowerEstimate.hpp index 5b333005e8..387028649b 100644 --- a/src/libslic3r/GCode/WipeTowerEstimate.hpp +++ b/src/libslic3r/GCode/WipeTowerEstimate.hpp @@ -2,6 +2,8 @@ #include +#include "../Polygon.hpp" + namespace Slic3r { class ConfigBase; @@ -23,6 +25,12 @@ struct WipeTowerFootprint // the GUI and CLI placement can resolve it without a Print. WipeTowerType resolve_wipe_tower_type(const ConfigBase &config); +// First-layer outline of an estimated tower in tower-local scaled coordinates, brim excluded: +// the body box, or for a Type2 cone wall the box unioned with the cone's base. The preview, +// the placement margin and validation all take the outline from here so they cannot disagree +// about whether a cone exists. +Polygon estimate_wipe_tower_first_layer_outline(const ConfigBase &config, WipeTowerType tower_type, double width, double depth, double height); + // filament_ids: 0-based filaments purged on the plate. The config cannot see custom G-code tool // changes, so ids derived from the model must include them // (Print::extruders(true)) or a real tower is sized as if it were never built. diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index 1b54a527ea..60f747ce04 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -1078,15 +1078,12 @@ static StringObjectException layered_print_cleareance_valid(const Print &print, 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))); - } + double max_height = 0.; + for (const PrintObject *object : print.objects()) + max_height = std::max(max_height, unscale_(object->size().z())); + Polygon base = estimate_wipe_tower_first_layer_outline(config, print.wipe_tower_type(), width, depth, max_height); + 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 diff --git a/src/slic3r/GUI/3DScene.cpp b/src/slic3r/GUI/3DScene.cpp index a21deaf209..6dbea1272a 100644 --- a/src/slic3r/GUI/3DScene.cpp +++ b/src/slic3r/GUI/3DScene.cpp @@ -21,7 +21,6 @@ #include "libslic3r/PresetBundle.hpp" #include "libslic3r/ClipperUtils.hpp" #include "libslic3r/GCode/WipeTower.hpp" -#include "libslic3r/GCode/WipeTower2.hpp" #include "libslic3r/GCode/WipeTowerEstimate.hpp" #include "libslic3r/Tesselate.hpp" #include "libslic3r/PrintConfig.hpp" @@ -928,25 +927,13 @@ int GLVolumeCollection::load_wipe_tower_preview( const float brim_height = 0.2f; // one first layer, visual only TriangleMesh brim_slab; if (show_brim) { - // A Type2 cone-wall tower's base bulges past the body box — follow the real base - // outline instead of the rectangle. Type1 ignores the cone option. - Polygon cone_base; - { - // Preset enums are ConfigOptionEnumGeneric, so read them by value; the planner is - // resolved as the estimate resolves it, off the printer preset. - const DynamicPrintConfig &print_cfg = GUI::wxGetApp().preset_bundle->prints.get_edited_preset().config; - const DynamicPrintConfig &printer_cfg = GUI::wxGetApp().preset_bundle->printers.get_edited_preset().config; - const ConfigOption *wall_opt = print_cfg.option("wipe_tower_wall_type"); - if (wall_opt != nullptr && wall_opt->getInt() == int(WipeTowerWallType::wtwCone) && resolve_wipe_tower_type(printer_cfg) == WipeTowerType::Type2) - cone_base = WipeTower2::cone_base_polygon(width, depth, height, print_cfg.opt_float("wipe_tower_cone_angle")); - } - if (!cone_base.empty()) { - Polygons brim_outline = offset(cone_base, scaled(brim_width)); - brim_slab = WipeTower::its_make_rib_brim(brim_outline.empty() ? cone_base : brim_outline.front(), brim_height); - } else { - brim_slab = make_cube(width + 2.f * brim_width, depth + 2.f * brim_width, brim_height); - brim_slab.translate({-brim_width, -brim_width, 0.f}); - } + // The brim follows the real first-layer outline: a Type2 cone-wall tower's base bulges + // past the body box. The wall type and angle are print settings, the planner a printer one. + const DynamicPrintConfig &print_cfg = GUI::wxGetApp().preset_bundle->prints.get_edited_preset().config; + const DynamicPrintConfig &printer_cfg = GUI::wxGetApp().preset_bundle->printers.get_edited_preset().config; + const Polygon outline = estimate_wipe_tower_first_layer_outline(print_cfg, resolve_wipe_tower_type(printer_cfg), width, depth, height); + const Polygons brim_outline = offset(outline, scaled(brim_width)); + brim_slab = WipeTower::its_make_rib_brim(brim_outline.empty() ? outline : brim_outline.front(), brim_height); wipe_tower_shell.merge(brim_slab); } for (int extruder_id : plate_extruders) { diff --git a/src/slic3r/GUI/PartPlate.cpp b/src/slic3r/GUI/PartPlate.cpp index e73d43b782..90e2c96ab6 100644 --- a/src/slic3r/GUI/PartPlate.cpp +++ b/src/slic3r/GUI/PartPlate.cpp @@ -22,7 +22,6 @@ #include "libslic3r/libslic3r.h" #include "libslic3r/Polygon.hpp" #include "libslic3r/GCode/WipeTowerEstimate.hpp" -#include "libslic3r/GCode/WipeTower2.hpp" #include "libslic3r/ClipperUtils.hpp" #include "libslic3r/BoundingBox.hpp" #include "libslic3r/Geometry.hpp" @@ -2398,14 +2397,9 @@ arrangement::ArrangePolygon PartPlate::estimate_wipe_tower_polygon(const Dynamic // clamp put the brim off the bed. Matches set_default_wipe_tower_pos_for_plate. float wp_brim_width = float(footprint.brim_width); // A Type2 stabilization cone bulges past the body box like a brim does - fold its worst-axis - // bulge into the same margin (Type1 ignores the cone option). - const auto *cone_wall_opt = config.option("wipe_tower_wall_type"); - const auto *cone_angle_opt = config.option("wipe_tower_cone_angle"); - if (cone_wall_opt != nullptr && cone_wall_opt->getInt() == int(WipeTowerWallType::wtwCone) && cone_angle_opt != nullptr && - cone_angle_opt->getFloat() > EPSILON && resolve_wipe_tower_type(config) == WipeTowerType::Type2) { - const BoundingBox cb = get_extents(WipeTower2::cone_base_polygon(w, depth, wt_size.z(), cone_angle_opt->getFloat())); - wp_brim_width += float(std::max({0., unscaled(cb.max.x()) - w, unscaled(cb.max.y()) - depth, -unscaled(cb.min.x()), -unscaled(cb.min.y())})); - } + // bulge into the same margin. + const BoundingBox outline = get_extents(estimate_wipe_tower_first_layer_outline(config, resolve_wipe_tower_type(config), w, depth, wt_size.z())); + wp_brim_width += float(std::max({0., unscaled(outline.max.x()) - w, unscaled(outline.max.y()) - depth, -unscaled(outline.min.x()), -unscaled(outline.min.y())})); // A position valid by WIPE_TOWER_MARGIN is the user's choice and stays untouched; an // invalid one is re-placed with the comfort margin (falling back to the validity bounds // on cramped plates). std::clamp is UB if lo > hi, so keep every hi >= lo. diff --git a/tests/libslic3r/test_wipe_tower_estimate.cpp b/tests/libslic3r/test_wipe_tower_estimate.cpp index fee5c1f152..20644200f3 100644 --- a/tests/libslic3r/test_wipe_tower_estimate.cpp +++ b/tests/libslic3r/test_wipe_tower_estimate.cpp @@ -1,5 +1,7 @@ #include +#include "libslic3r/BoundingBox.hpp" +#include "libslic3r/ClipperUtils.hpp" #include "libslic3r/GCode/WipeTower.hpp" #include "libslic3r/GCode/WipeTower2.hpp" #include "libslic3r/GCode/WipeTowerEstimate.hpp" @@ -272,6 +274,33 @@ TEST_CASE("Every wall and tower type is read the same from a preset and a static CHECK(estimate(static_config, 1, 0.2, 5., type).depth > 0.); } +TEST_CASE("The first-layer outline bulges only for a Type2 cone wall", "[WipeTowerEstimate]") { + // Read off a preset-shaped config, whose enums are ConfigOptionEnumGeneric: a cast to + // ConfigOptionEnum sees no wall type there and would never find the cone. + DynamicPrintConfig config = make_config("cone"); + config.set_key_value("wipe_tower_cone_angle", new ConfigOptionFloat(25.)); + REQUIRE(dynamic_cast(config.option("wipe_tower_wall_type")) != nullptr); + const Polygon box = Polygon::new_scale({{0., 0.}, {35., 0.}, {35., 20.}, {0., 20.}}); + auto is_box = [&box](const Polygon &outline) { return diff(Polygons{outline}, Polygons{box}).empty(); }; + + // A 25-degree cone on a 100 mm tower has a 22 mm base radius, past the 10 mm half-depth. + const Polygon cone = estimate_wipe_tower_first_layer_outline(config, WipeTowerType::Type2, 35., 20., 100.); + CHECK(unscaled(get_extents(cone).max.y()) > 20. + 1.); + CHECK(diff(Polygons{box}, Polygons{cone}).empty()); + // Type1 ignores the cone option, and the other wall types have no cone. + CHECK(is_box(estimate_wipe_tower_first_layer_outline(config, WipeTowerType::Type1, 35., 20., 100.))); + for (const char *wall_type : {"rectangle", "rib"}) { + config.set_deserialize_strict("wipe_tower_wall_type", wall_type); + CHECK(is_box(estimate_wipe_tower_first_layer_outline(config, WipeTowerType::Type2, 35., 20., 100.))); + } + // The static config Print holds gives the same outline. + config.set_deserialize_strict("wipe_tower_wall_type", "cone"); + FullPrintConfig static_config; + static_config.apply(config, true); + const Polygon from_static = estimate_wipe_tower_first_layer_outline(static_config, WipeTowerType::Type2, 35., 20., 100.); + CHECK(from_static.points == cone.points); +} + TEST_CASE("A Bambu Lab printer always gets the Type1 planner", "[WipeTowerEstimate]") { DynamicPrintConfig config = make_config(); config.set_deserialize_strict("wipe_tower_type", "type2");