mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-27 19:01:02 +00:00
Port wipe tower BBS improvements (#15485)
This commit is contained in:
@@ -6,6 +6,8 @@
|
||||
#include "libslic3r/ClipperUtils.hpp"
|
||||
#include "libslic3r/GCode/WipeTower.hpp"
|
||||
#include "libslic3r/GCode/WipeTower2.hpp"
|
||||
#include "libslic3r/Print.hpp"
|
||||
#include "libslic3r/PrintConfig.hpp"
|
||||
|
||||
using namespace Slic3r;
|
||||
using Catch::Matchers::WithinAbs;
|
||||
@@ -91,3 +93,197 @@ TEST_CASE("Brim width estimate matches each generator's loop quantization", "[Wi
|
||||
CHECK_THAT(WipeTower::estimate_brim_real_width(3.f, 0.4f, 0.2f, false), WithinAbs(7.5f * spacing, 1e-4f));
|
||||
CHECK_THAT(WipeTower::estimate_brim_real_width(0.f, 0.4f, 0.2f, true), WithinAbs(0.f, 1e-6f));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// "No sparse layers": the compaction rule and the clearance it demands of the plate.
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
|
||||
// A square of side mm centred on (cx, cy), in bed coordinates.
|
||||
static Polygon centered_square(double cx, double cy, double side)
|
||||
{
|
||||
const double h = 0.5 * side;
|
||||
Polygon poly;
|
||||
poly.points = {Point::new_scale(cx - h, cy - h), Point::new_scale(cx + h, cy - h),
|
||||
Point::new_scale(cx + h, cy + h), Point::new_scale(cx - h, cy + h)};
|
||||
return poly;
|
||||
}
|
||||
|
||||
static WipeTower::ToolChangeResult make_tcr(int initial_tool, int new_tool, float layer_height)
|
||||
{
|
||||
WipeTower::ToolChangeResult tcr{};
|
||||
tcr.initial_tool = initial_tool;
|
||||
tcr.new_tool = new_tool;
|
||||
tcr.layer_height = layer_height;
|
||||
return tcr;
|
||||
}
|
||||
|
||||
// A 20 mm square tower at the bed origin, no spiral z-hop, so the keep-out zone is the bare
|
||||
// footprint and every distance below is one the test sets.
|
||||
static PrintConfig clearance_config()
|
||||
{
|
||||
PrintConfig cfg;
|
||||
cfg.extruder_clearance_radius.value = 40.;
|
||||
cfg.extruder_clearance_dist_to_rod.value = 20.;
|
||||
cfg.extruder_clearance_height_to_rod.value = 25.;
|
||||
cfg.extruder_clearance_height_to_lid.value = 120.;
|
||||
cfg.nozzle_height.value = 5.;
|
||||
cfg.nozzle_diameter.values = {0.4};
|
||||
cfg.z_hop.values = {0.};
|
||||
cfg.travel_slope.values = {3.};
|
||||
return cfg;
|
||||
}
|
||||
|
||||
TEST_CASE("Sparse layers are skipped only when nothing else needs a tower on every layer", "[WipeTower][NoSparseLayers]") {
|
||||
PrintConfig cfg;
|
||||
cfg.timelapse_type.value = TimelapseType::tlTraditional;
|
||||
cfg.enable_wrapping_detection.value = false;
|
||||
|
||||
cfg.wipe_tower_no_sparse_layers.value = false;
|
||||
CHECK_FALSE(wipe_tower_sparse_layers_skipped(cfg));
|
||||
cfg.wipe_tower_no_sparse_layers.value = true;
|
||||
CHECK(wipe_tower_sparse_layers_skipped(cfg));
|
||||
|
||||
// Both park the nozzle on the tower every layer, so no layer is ever dropped and the option
|
||||
// must read as off everywhere rather than compact in one place and not another.
|
||||
cfg.timelapse_type.value = TimelapseType::tlSmooth;
|
||||
CHECK_FALSE(wipe_tower_sparse_layers_skipped(cfg));
|
||||
cfg.timelapse_type.value = TimelapseType::tlTraditional;
|
||||
cfg.enable_wrapping_detection.value = true;
|
||||
CHECK_FALSE(wipe_tower_sparse_layers_skipped(cfg));
|
||||
}
|
||||
|
||||
TEST_CASE("A planned layer is sparse only when its single tool change keeps the filament", "[WipeTower][NoSparseLayers]") {
|
||||
CHECK(wipe_tower_layer_is_sparse({make_tcr(1, 1, 0.2f)}));
|
||||
CHECK_FALSE(wipe_tower_layer_is_sparse({make_tcr(0, 1, 0.2f)}));
|
||||
// A second entry means the layer carries real work whatever the tools are.
|
||||
CHECK_FALSE(wipe_tower_layer_is_sparse({make_tcr(1, 1, 0.2f), make_tcr(1, 1, 0.2f)}));
|
||||
CHECK_FALSE(wipe_tower_layer_is_sparse({}));
|
||||
}
|
||||
|
||||
TEST_CASE("The compacted tower falls one layer height behind the object per sparse layer", "[WipeTower][NoSparseLayers]") {
|
||||
// Five 0.2 mm layers off a 0.1 mm z offset, the middle two sparse. The object reaches
|
||||
// 0.1 + 5 * 0.2 = 1.1; the tower only grows on the three printed layers, so it ends at
|
||||
// 0.1 + 3 * 0.2 = 0.7 and a sparse layer carries the previous value rather than its own.
|
||||
const std::vector<std::vector<WipeTower::ToolChangeResult>> tool_changes{
|
||||
{make_tcr(0, 1, 0.2f)}, {make_tcr(1, 1, 0.2f)}, {make_tcr(1, 1, 0.2f)},
|
||||
{make_tcr(1, 0, 0.2f)}, {make_tcr(0, 1, 0.2f)}};
|
||||
|
||||
const std::vector<float> tower_z = compute_compacted_wipe_tower_z(tool_changes, 0.1f);
|
||||
REQUIRE(tower_z.size() == tool_changes.size());
|
||||
CHECK_THAT(tower_z[0], WithinAbs(0.3f, 1e-5f));
|
||||
CHECK_THAT(tower_z[1], WithinAbs(0.3f, 1e-5f));
|
||||
CHECK_THAT(tower_z[2], WithinAbs(0.3f, 1e-5f));
|
||||
CHECK_THAT(tower_z[3], WithinAbs(0.5f, 1e-5f));
|
||||
CHECK_THAT(tower_z[4], WithinAbs(0.7f, 1e-5f));
|
||||
CHECK_THAT(1.1f - tower_z.back(), WithinAbs(2 * 0.2f, 1e-5f));
|
||||
|
||||
// Without a base the tower starts at the bed, and an empty layer carries over like a sparse one.
|
||||
const std::vector<float> no_offset = compute_compacted_wipe_tower_z({{make_tcr(0, 1, 0.2f)}, {}}, 0.f);
|
||||
CHECK_THAT(no_offset[0], WithinAbs(0.2f, 1e-5f));
|
||||
CHECK_THAT(no_offset[1], WithinAbs(0.2f, 1e-5f));
|
||||
}
|
||||
|
||||
TEST_CASE("The tower keep-out zone grows by the spiral z-hop envelope", "[WipeTower][NoSparseLayers]") {
|
||||
PrintConfig cfg = clearance_config();
|
||||
const Polygon footprint = centered_square(0., 0., 20.);
|
||||
|
||||
// No lift, no envelope: the zone works on the bare footprint.
|
||||
CHECK_THAT(unscaled(compacted_wipe_tower_zone(cfg, footprint).hull.bounding_box().max.x()), WithinAbs(10., 1e-6));
|
||||
|
||||
// A spiral lift leaves the outline at low z, so it counts as tower. The circle reaches
|
||||
// 2 * lift / (2*pi*atan(slope)) past the outline, matching GCodeWriter: 2*2/(2*pi*atan(3)) = 0.51 mm.
|
||||
cfg.z_hop.values = {2.};
|
||||
const CompactedTowerZone lifted = compacted_wipe_tower_zone(cfg, footprint);
|
||||
CHECK_THAT(unscaled(lifted.hull.bounding_box().max.x()), WithinAbs(10.51, 0.02));
|
||||
CHECK_THAT(unscaled(lifted.hull.bounding_box().min.y()), WithinAbs(-10.51, 0.02));
|
||||
CHECK(diff(Polygons{footprint}, Polygons{lifted.hull}).empty());
|
||||
|
||||
// z_hop is capped at 5 mm by the option, so a taller lift cannot widen the zone further.
|
||||
cfg.z_hop.values = {10.};
|
||||
const double capped = unscaled(compacted_wipe_tower_zone(cfg, footprint).hull.bounding_box().max.x());
|
||||
CHECK_THAT(capped, WithinAbs(10. + 2. * 5. / (2. * M_PI * std::atan(3.)), 0.02));
|
||||
|
||||
// The rod sweeps the whole X axis, so its band is the tower's y span plus half the rod offset.
|
||||
CHECK_THAT(unscaled(lifted.bbox_rod.max.y()), WithinAbs(10.51 + 10., 0.02));
|
||||
}
|
||||
|
||||
TEST_CASE("An object beside a compacted tower is limited by the nearest part of the toolhead", "[WipeTower][NoSparseLayers]") {
|
||||
const PrintConfig cfg = clearance_config();
|
||||
const CompactedTowerZone zone = compacted_wipe_tower_zone(cfg, centered_square(0., 0., 20.));
|
||||
|
||||
// Each side carries half its clearance less 0.1 mm slack, so the two outlines meet when the
|
||||
// objects are a full clearance apart: 2 * (4 - 0.2) / 2 = 3.8 mm for the bare nozzle cone,
|
||||
// 2 * (40 - 0.2) / 2 = 39.8 mm for the head body. A 10 mm object at x leaves a gap of x - 15.
|
||||
const double tall = 50., shortish = 3.;
|
||||
|
||||
// Gap 1 mm, inside the nozzle cone: the object may not rise above the tower at all.
|
||||
const CompactedTowerClearance touching = compacted_wipe_tower_clearance(cfg, zone, centered_square(16., 0., 10.), tall);
|
||||
CHECK_THAT(touching.allowed_rise, WithinAbs(0., 1e-9));
|
||||
|
||||
// Gap 10 mm: clear of the cone but inside the head body, which starts at nozzle_height.
|
||||
const CompactedTowerClearance near_body = compacted_wipe_tower_clearance(cfg, zone, centered_square(25., 0., 10.), tall);
|
||||
CHECK(near_body.near_body);
|
||||
CHECK_THAT(near_body.allowed_rise, WithinAbs(5., 1e-9));
|
||||
CHECK_THAT(near_body.body_clearance, WithinAbs(40., 1e-9));
|
||||
|
||||
// The same spot, but an object that never rises past the cone. The body sits above the cone, so
|
||||
// it cannot reach this object however close it stands, and only the narrow tier applies.
|
||||
const CompactedTowerClearance low = compacted_wipe_tower_clearance(cfg, zone, centered_square(25., 0., 10.), shortish);
|
||||
CHECK_FALSE(low.near_body);
|
||||
CHECK_THAT(low.body_clearance, WithinAbs(4., 1e-9));
|
||||
CHECK_THAT(low.allowed_rise, WithinAbs(25., 1e-9));
|
||||
|
||||
// Gap 55 mm, clear of the head entirely: the rod is the obstacle, since the object shares the
|
||||
// tower's y band and the rod spans the whole x axis however far apart the two stand.
|
||||
const CompactedTowerClearance far_in_band = compacted_wipe_tower_clearance(cfg, zone, centered_square(70., 0., 10.), tall);
|
||||
CHECK_FALSE(far_in_band.near_body);
|
||||
CHECK_THAT(far_in_band.far_clearance, WithinAbs(25., 1e-9));
|
||||
CHECK_THAT(far_in_band.allowed_rise, WithinAbs(25., 1e-9));
|
||||
|
||||
// Out of the band the rod passes over it and only the lid is left.
|
||||
const CompactedTowerClearance out_of_band = compacted_wipe_tower_clearance(cfg, zone, centered_square(70., 60., 10.), tall);
|
||||
CHECK_THAT(out_of_band.allowed_rise, WithinAbs(120., 1e-9));
|
||||
}
|
||||
|
||||
TEST_CASE("The ring drawn around the tower meets the outline drawn around an offender", "[WipeTower][NoSparseLayers]") {
|
||||
const PrintConfig cfg = clearance_config();
|
||||
const CompactedTowerZone zone = compacted_wipe_tower_zone(cfg, centered_square(0., 0., 20.));
|
||||
|
||||
// What the plater draws has to be what the check tested, otherwise a user moves an object until
|
||||
// the outlines part and slicing still refuses the plate. Both halves of the 3.8 mm nozzle
|
||||
// clearance: at a 3 mm gap the rings overlap and the rise limit is zero, at 5 mm neither holds.
|
||||
for (const auto &c : {std::make_pair(18., true), std::make_pair(20., false)}) {
|
||||
DYNAMIC_SECTION("object at x = " << c.first) {
|
||||
const Polygon hull = centered_square(c.first, 0., 10.);
|
||||
const CompactedTowerClearance clearance = compacted_wipe_tower_clearance(cfg, zone, hull, 3.);
|
||||
const Polygons rings = compacted_wipe_tower_rings(zone, compacted_tower_body_tier(clearance));
|
||||
const Polygon outline = compacted_wipe_tower_offender_outline(hull, clearance.body_clearance);
|
||||
const bool outlines_meet = ! intersection(rings, Polygons{outline}).empty();
|
||||
const bool rise_denied = clearance.allowed_rise < EPSILON;
|
||||
CHECK(outlines_meet == c.second);
|
||||
CHECK(rise_denied == c.second);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Only the keep-out ring an object is measured against is drawn", "[WipeTower][NoSparseLayers]") {
|
||||
const PrintConfig cfg = clearance_config();
|
||||
const CompactedTowerZone zone = compacted_wipe_tower_zone(cfg, centered_square(0., 0., 20.));
|
||||
|
||||
// Drawing the wide ring when no object is judged on it would show a keep-out zone the check can
|
||||
// never trip, so it is added only once some object reaches past the nozzle cone.
|
||||
CHECK(compacted_wipe_tower_rings(zone, false).size() == zone.grown_nozzle.size());
|
||||
CHECK(compacted_wipe_tower_rings(zone, true).size() == zone.grown_nozzle.size() + zone.grown_body.size());
|
||||
CHECK_THAT(unscaled(get_extents(zone.grown_nozzle).max.x()), WithinAbs(10. + 0.5 * (4. - 0.2), 0.02));
|
||||
CHECK_THAT(unscaled(get_extents(zone.grown_body).max.x()), WithinAbs(10. + 0.5 * (40. - 0.2), 0.02));
|
||||
}
|
||||
|
||||
TEST_CASE("Footprint padding covers the brim and the extrusion half width on each side", "[WipeTower][NoSparseLayers]") {
|
||||
// A nominal outline hulls extrusion centre lines and is re-centred once the real wall is known,
|
||||
// so a line width per side on top of the brim is what keeps an estimate enclosing the real tower.
|
||||
const PrintConfig cfg = clearance_config();
|
||||
CHECK_THAT(compacted_tower_footprint_padding(cfg, 2.), WithinAbs(2. + 2. * 0.4, 1e-9));
|
||||
CHECK_THAT(compacted_tower_footprint_padding(cfg, 0.), WithinAbs(2. * 0.4, 1e-9));
|
||||
// Callers whose outline already carries the brim pass zero, and a negative one cannot shrink it.
|
||||
CHECK_THAT(compacted_tower_footprint_padding(cfg, -5.), WithinAbs(2. * 0.4, 1e-9));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user