Size the Footprint Estimate from the Planners

The shared estimate reserved every tower with one volume-per-purge rule
and the stability floor. Both planners do more: WipeTower (Type1) wipes
each filament's own prime volume in whole lines, one block per
adhesiveness category sized by its worst layer, rams the leaving
filament at every nozzle change, and squares a rib tower from the
planned depth; WipeTower2 (Type2) spaces its lines by
wipe_tower_extra_spacing, not the Type1-only infill gap, and its extra
flow cancels out of the depth. Both extend the ribs rather than the body
below the stability minimum, size every layer including a thinner first
one, and lay the brim in whole loops, WipeTower reporting half a spacing
of line width on top.

All of that now lives in estimate_wipe_tower_footprint, fed the planner
(resolve_wipe_tower_type mirrors Print::wipe_tower_type and the CLI's
Bambu Lab detection) and the filament ids rather than a count. Print
passes its own tool set; the PartPlate adapter derives the plate's ids
from the passed config and treats an explicit count as a floor, so the
CLI's count-only callers size per filament too. The placement clamp also
reserves a Type2 cone's base bulge, which the body box does not cover.

The planner-mirroring helpers sit beside the planners in WipeTower and
WipeTower2 so the two stay in sync; the libslic3r cases pin them to
footprints measured from generated G-code.
This commit is contained in:
Hanif Koh
2026-09-09 15:45:16 +08:00
committed by HanifKoh
parent 98acd687f7
commit 99627c8e93
13 changed files with 559 additions and 126 deletions
+88
View File
@@ -1630,6 +1630,94 @@ float WipeTower::get_auto_brim_by_height(float max_height) {
return 8.f;
}
float WipeTower::estimate_brim_real_width(float brim_width, float nozzle_diameter, float first_layer_height, bool type2)
{
if (brim_width <= 0.f)
return brim_width;
const float spacing = nozzle_diameter * 1.25f - first_layer_height * float(1. - M_PI_4); // Width_To_Nozzle_Ratio
if (spacing <= EPSILON)
return brim_width;
const int loops_num = int((brim_width + spacing / 2.f) / spacing);
return loops_num * spacing + (type2 ? 0.f : spacing / 2.f);
}
float WipeTower::get_wrapping_detection_depth()
{
return float(wrapping_wipe_tower_depth);
}
float WipeTower::nozzle_change_perimeter_width(float nozzle_diameter)
{
auto it = nozzle_diameter_to_nozzle_change_width.find(nozzle_diameter);
return it != nozzle_diameter_to_nozzle_change_width.end() ? it->second : 2.f * nozzle_diameter * 1.25f;
}
float WipeTower::estimate_tower_blocks_depth(const std::vector<PurgeEstimate> &purges, float width, float layer_height, float nozzle_diameter, float extra_spacing)
{
if (purges.empty() || layer_height < EPSILON || nozzle_diameter < EPSILON)
return 0.f;
const float pw = nozzle_diameter * 1.25f; // Width_To_Nozzle_Ratio
const float ncpw = nozzle_change_perimeter_width(nozzle_diameter);
const float line_width = width - 2.f * pw;
if (line_width <= EPSILON)
return 0.f;
// Line cross-section as volume_to_length() sees it; the infill gap stretches the perimeter
// width by the configured ratio and nozzle-change lines keep their own width
// (calc_block_infill_gap).
auto line_area = [layer_height](float w) { return layer_height * (w - layer_height * float(1. - M_PI_4)); };
const float extra_width = (extra_spacing - 1.f) * pw;
const float gap = pw + extra_width;
const float nc_gap = ncpw + extra_width;
// A layer purges into at most (filaments - 1) targets, so a category holding every filament
// never sees its smallest purge (the layer's first filament) in its worst layer.
struct Block { float depth = 0.f; float min_purge = 0.f; size_t filaments = 0; };
std::map<int, Block> blocks;
for (const PurgeEstimate &purge : purges) {
Block &block = blocks[purge.category];
const float purge_depth = std::ceil(purge.prime_volume / line_area(pw) / line_width) * gap;
block.min_purge = block.filaments == 0 ? purge_depth : std::min(block.min_purge, purge_depth);
block.depth += purge_depth;
++block.filaments;
if (purge.filament_change_length > EPSILON) {
// The leaving filament is rammed over the nozzle-change flow, again in whole lines.
const float filament_area = float(M_PI) * purge.filament_diameter * purge.filament_diameter / 4.f;
const float nc_length = purge.filament_change_length * filament_area / line_area(ncpw);
block.depth += std::ceil(nc_length / (width - ncpw - pw)) * nc_gap;
}
}
float depth = pw; // plan_tower_new starts the first block one perimeter width in
for (const auto &[category, block] : blocks)
depth += block.filaments == purges.size() ? block.depth - block.min_purge : block.depth;
return depth;
}
float WipeTower::rib_footprint_side(float width, float depth, float rib_width, float extra_rib_length, float max_height)
{
if (width < EPSILON || depth < EPSILON)
return 0.f;
// Ribs run the diagonal; below the height-based minimum they are extended rather than the
// body, then by the extra length, never ending up shorter than the diagonal.
const float diagonal = std::sqrt(width * width + depth * depth);
float rib_length = diagonal;
if (depth + EPSILON < get_limit_depth_by_height(max_height))
rib_length = std::max(rib_length, get_limit_depth_by_height(max_height) * float(std::sqrt(2.)));
rib_length = std::max(diagonal, rib_length + extra_rib_length);
// Half the extension at each end of the diagonal plus half the rib width, projected onto the axes.
const float rib_w = std::min(rib_width, std::min(width, depth) / 2.f);
const float per_side = ((rib_length - diagonal) / 2.f + rib_w / 2.f) / float(std::sqrt(2.));
return std::max(width, depth) + 2.f * per_side;
}
float WipeTower::estimate_rib_tower_bbox_side(const std::vector<PurgeEstimate> &purges, float width, float layer_height, float nozzle_diameter, float extra_spacing, float rib_width, float extra_rib_length, float max_height)
{
if (purges.empty() || width < EPSILON || layer_height < EPSILON || nozzle_diameter < EPSILON)
return 0.f;
const float pw = nozzle_diameter * 1.25f; // Width_To_Nozzle_Ratio
const float square = align_ceil(std::sqrt(estimate_tower_blocks_depth(purges, width, layer_height, nozzle_diameter, extra_spacing) * width), pw);
const float depth = estimate_tower_blocks_depth(purges, square, layer_height, nozzle_diameter, extra_spacing);
return rib_footprint_side(square, depth, rib_width, extra_rib_length, max_height);
}
Vec2f WipeTower::move_box_inside_polygon(const BoundingBox &box, const Polygons &polygons, coord_t offset)
{
if (polygons.empty()) return Vec2f{0.f, 0.f};