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.
This commit is contained in:
Hanif Koh
2026-09-03 14:52:12 +08:00
parent 3c1bb21620
commit 97e9eb9c34
4 changed files with 78 additions and 16 deletions

View File

@@ -1068,33 +1068,57 @@ static StringObjectException layered_print_cleareance_valid(const Print &print,
convex_hulls_temp.push_back(wipe_tower_polygon); 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 (!intersection(convex_hulls_other, convex_hulls_temp).empty()) {
if (warning) { if (warning) {
warning->string += L("Prime Tower") + L(" is too close to others, and collisions may be caused.\n"); 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 (!intersection(exclude_polys, tower_polys_checked).empty()) {
/*if (warning) {
warning->string += L("Prime Tower is too close to exclusion area, there may be collisions when printing.\n");
}*/
return {L("Prime Tower") + L(" is too close to an exclusion area, and collisions will be caused.\n")}; 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")}; 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 if (warning && !intersection(exclude_polys, tower_polys_estimated).empty()) {
// is degenerate and every check passes. Re-deriving it here missed the wrapping-detection 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. // 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(); Polygons printable_polys = print.get_extruder_shared_printable_polygon();
const Point plate_shift(scale_(plate_origin.x()), scale_(plate_origin.y())); const Point plate_shift(scale_(plate_origin.x()), scale_(plate_origin.y()));
for (Polygon &p : printable_polys) for (Polygon &p : printable_polys)
p.translate(plate_shift); p.translate(plate_shift);
Polygons tower_polys_with_brim = print.is_step_done(psWipeTower) ? if (!diff(tower_polys_checked, printable_polys).empty())
convex_hulls_temp : offset(convex_hulls_temp, float(scale_(brim_width)));
if (!diff(tower_polys_with_brim, printable_polys).empty())
return {L("Prime Tower") + L(" is partially outside the printable area, and it cannot be printed.\n")}; 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 {}; return {};
} }
@@ -4390,7 +4414,9 @@ void Print::_make_wipe_tower()
wipe_tower.get_wipe_tower_height(), wipe_tower.get_brim_width(), wipe_tower.get_wipe_tower_height(), wipe_tower.get_brim_width(),
config().wipe_tower_wall_type.value == WipeTowerWallType::wtwRib, config().wipe_tower_wall_type.value == WipeTowerWallType::wtwRib,
wipe_tower.get_rib_width(), wipe_tower.get_rib_length(), 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(); const Vec3d origin = Vec3d::Zero();
// FakeWipeTower::pos is a bed-frame translation applied after rotation // FakeWipeTower::pos is a bed-frame translation applied after rotation
// (getFakeExtrusionPathsFromWipeTower2 rotates about the local origin), so the // (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, config().wipe_tower_rotation_angle, config().wipe_tower_cone_angle,
{scale_(origin.x()), scale_(origin.y())}); {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 // 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; 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{}; wipe_tower_mesh_data = WipeTowerMeshData{};
float first_layer_height=0.08; //brim height float first_layer_height=0.08; //brim height
if (width < EPSILON || depth < EPSILON || height < EPSILON) return; 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_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 = 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}); wipe_tower_mesh_data->real_brim_mesh.translate({-brim_width, -brim_width, 0});

View File

@@ -804,7 +804,7 @@ struct WipeTowerData
rib_offset = Vec2f::Zero(); rib_offset = Vec2f::Zero();
wipe_tower_mesh_data = std::nullopt; 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: private:
// Only allow the WipeTowerData to be instantiated internally by Print, // Only allow the WipeTowerData to be instantiated internally by Print,

View File

@@ -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("nozzle_temperature_range_high", new ConfigOptionInts({240, 240}));
config.set_key_value("flush_multiplier", new ConfigOptionFloats({1})); config.set_key_value("flush_multiplier", new ConfigOptionFloats({1}));
config.set_key_value("flush_volumes_matrix", new ConfigOptionFloats({0, 140, 140, 0})); 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; return config;
} }

View File

@@ -152,6 +152,8 @@ static DynamicPrintConfig wipe_tower_toolchange_config(const std::string &gcode_
{ "outer_wall_filament_id", 2 }, { "outer_wall_filament_id", 2 },
{ "inner_wall_filament_id", 2 }, { "inner_wall_filament_id", 2 },
{ "enable_prime_tower", true }, { "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 }, { "layer_height", 0.3 },
{ "gcode_flavor", gcode_flavor }, { "gcode_flavor", gcode_flavor },
}); });