Merge main

This commit is contained in:
Lam Wei Lun
2026-09-09 19:12:23 +08:00
33 changed files with 1722 additions and 342 deletions
+50 -31
View File
@@ -4015,7 +4015,7 @@ int CLI::run(int argc, char **argv)
}
};
auto check_plate_wipe_tower = [get_print_sequence, is_smooth_timelapse, new_extruder_count](Slic3r::GUI::PartPlate* plate, int plate_index, DynamicPrintConfig& print_config, plate_obj_size_info_t &plate_obj_size_info) {
auto check_plate_wipe_tower = [get_print_sequence, is_smooth_timelapse](Slic3r::GUI::PartPlate* plate, int plate_index, DynamicPrintConfig& print_config, plate_obj_size_info_t &plate_obj_size_info) {
plate_obj_size_info.obj_bbox= plate->get_objects_bounding_box();
BOOST_LOG_TRIVIAL(info) << boost::format("plate %1%, object bbox: min {%2%, %3%, %4%} - max {%5%, %6%, %7%}")
%(plate_index+1) %plate_obj_size_info.obj_bbox.min.x() % plate_obj_size_info.obj_bbox.min.y() % plate_obj_size_info.obj_bbox.min.z() %plate_obj_size_info.obj_bbox.max.x() % plate_obj_size_info.obj_bbox.max.y() % plate_obj_size_info.obj_bbox.max.z();
@@ -4059,22 +4059,13 @@ int CLI::run(int argc, char **argv)
plate_obj_size_info.wipe_x = wipe_x_option->get_at(plate_index);
plate_obj_size_info.wipe_y = wipe_y_option->get_at(plate_index);
ConfigOptionFloat* width_option = print_config.option<ConfigOptionFloat>("prime_tower_width", true);
plate_obj_size_info.wipe_width = width_option->value;
// Body and brim from one estimate: resolving an auto (-1) brim against a different
// height would size the two halves of the same tower from two different objects.
const WipeTowerFootprint footprint = plate->estimate_wipe_tower_footprint(print_config, filaments_cnt);
float brim_width = float(footprint.brim_width);
ConfigOptionFloat* brim_width_option = print_config.option<ConfigOptionFloat>("prime_tower_brim_width", true);
float brim_width = brim_width_option->value;
if (brim_width < 0) brim_width = WipeTower::get_auto_brim_by_height((float)plate_obj_size_info.obj_bbox.max.z());
ConfigOptionFloat* volume_option = print_config.option<ConfigOptionFloat>("prime_volume", true);
float wipe_volume = volume_option->value;
const ConfigOptionBool * wrapping_detection = print_config.option<ConfigOptionBool>("enable_wrapping_detection");
bool enable_wrapping = (wrapping_detection != nullptr) && wrapping_detection->value;
Vec3d wipe_tower_size = plate->estimate_wipe_tower_size(print_config, plate_obj_size_info.wipe_width, wipe_volume, new_extruder_count, filaments_cnt, false, enable_wrapping);
plate_obj_size_info.wipe_width = wipe_tower_size(0);
plate_obj_size_info.wipe_depth = wipe_tower_size(1);
plate_obj_size_info.wipe_width = footprint.width;
plate_obj_size_info.wipe_depth = footprint.depth;
Vec3d origin = plate->get_origin();
Vec3d start(origin(0) + plate_obj_size_info.wipe_x - brim_width, origin(1) + plate_obj_size_info.wipe_y, 0.f);
@@ -4841,7 +4832,10 @@ int CLI::run(int argc, char **argv)
int plate_count = partplate_list.get_plate_count();
auto printer_structure_opt = m_print_config.option<ConfigOptionEnum<PrinterStructure>>("printer_structure");
const float tower_brim_width = m_print_config.option<ConfigOptionFloat>("prime_tower_width", true)->value;
// This margin only pre-adjusts the default away from the near edges;
// estimate_wipe_tower_polygon below computes the real clamped position.
float tower_brim_width = m_print_config.option<ConfigOptionFloat>("prime_tower_brim_width", true)->value;
if (tower_brim_width < 0.f) tower_brim_width = 8.f; // auto: object heights unknown here, 8 mm is the auto cap
const float tower_margin = WIPE_TOWER_MARGIN + tower_brim_width;
// set the default position, the same with print config(left top)
@@ -4875,7 +4869,7 @@ int CLI::run(int argc, char **argv)
wipe_y_option->set_at(&wt_y_opt, i, 0);
Vec3d wipe_tower_size, wipe_tower_pos;
ArrangePolygon wipe_tower_ap = cur_plate->estimate_wipe_tower_polygon(m_print_config, i, wipe_tower_pos, wipe_tower_size, new_extruder_count, assemble_plate.filaments_count, true);
ArrangePolygon wipe_tower_ap = cur_plate->estimate_wipe_tower_polygon(m_print_config, i, wipe_tower_pos, wipe_tower_size, assemble_plate.filaments_count, true);
//update the new wp position
wt_x_opt.value = wipe_tower_pos(0);
@@ -5138,7 +5132,10 @@ int CLI::run(int argc, char **argv)
int extruder_size = used_filament_set.size();
auto printer_structure_opt = m_print_config.option<ConfigOptionEnum<PrinterStructure>>("printer_structure");
const float tower_brim_width = m_print_config.option<ConfigOptionFloat>("prime_tower_width", true)->value;
// This margin only pre-adjusts the default away from the near edges;
// estimate_wipe_tower_polygon below computes the real clamped position.
float tower_brim_width = m_print_config.option<ConfigOptionFloat>("prime_tower_brim_width", true)->value;
if (tower_brim_width < 0.f) tower_brim_width = 8.f; // auto: object heights unknown here, 8 mm is the auto cap
const float tower_margin = WIPE_TOWER_MARGIN + tower_brim_width;
// set the default position, the same with print config(left top)
float x = WIPE_TOWER_DEFAULT_X_POS;
@@ -5175,7 +5172,7 @@ int CLI::run(int argc, char **argv)
}
Vec3d wipe_tower_size, wipe_tower_pos;
ArrangePolygon wipe_tower_ap = partplate_list.get_plate(plate_index_valid)->estimate_wipe_tower_polygon(m_print_config, plate_index_valid, wipe_tower_pos, wipe_tower_size, new_extruder_count, extruder_size, true);
ArrangePolygon wipe_tower_ap = partplate_list.get_plate(plate_index_valid)->estimate_wipe_tower_polygon(m_print_config, plate_index_valid, wipe_tower_pos, wipe_tower_size, extruder_size, true);
//update the new wp position
if (bedid < plate_count) {
@@ -5276,22 +5273,16 @@ int CLI::run(int argc, char **argv)
//float depth = v * (filaments_cnt - 1) / (layer_height * w);
const ConfigOptionBool *wrapping_detection = m_print_config.option<ConfigOptionBool>("enable_wrapping_detection");
bool enable_wrapping = (wrapping_detection != nullptr) && wrapping_detection->value;
Vec3d wipe_tower_size = cur_plate->estimate_wipe_tower_size(m_print_config, w, v, new_extruder_count, filaments_cnt, false, enable_wrapping);
const WipeTowerFootprint footprint = cur_plate->estimate_wipe_tower_footprint(m_print_config, filaments_cnt);
Vec3d wipe_tower_size(footprint.width, footprint.depth, footprint.height);
Vec3d plate_origin = cur_plate->get_origin();
int plate_width, plate_depth;
double plate_height;
partplate_list.get_plate_size(plate_width, plate_depth, plate_height);
float depth = wipe_tower_size(1);
float margin = 15.f, wp_brim_width = 0.f;
ConfigOption *wipe_tower_brim_width_opt = m_print_config.option("prime_tower_brim_width");
if (wipe_tower_brim_width_opt ) {
wp_brim_width = wipe_tower_brim_width_opt->getFloat();
if (wp_brim_width < 0) wp_brim_width = WipeTower::get_auto_brim_by_height((float) wipe_tower_size.z());
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format("arrange wipe_tower: wp_brim_width %1%")%wp_brim_width;
}
// Brim already resolved against the height the body was sized from.
float margin = 15.f, wp_brim_width = float(footprint.brim_width);
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format("arrange wipe_tower: wp_brim_width %1%")%wp_brim_width;
w = wipe_tower_size(0);
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format("arrange wipe_tower: x=%1%, y=%2%, width=%3%, depth=%4%, angle=%5%, prime_volume=%6%, filaments_cnt=%7%, layer_height=%8%, plate_width=%9%, plate_depth=%10%")
@@ -5807,6 +5798,34 @@ int CLI::run(int argc, char **argv)
//Print fff_print;
std::vector<size_t> plate_triangle_counts(partplate_list.get_plate_count(), 0);
// The stored (or default) tower position may not fit the tower these plates
// need, and no CLI placement site runs on a plain slice - mirror the GUI's
// reload clamp and fit every plate's tower into the printable area first.
if (m_print_config.option<ConfigOptionBool>("enable_prime_tower", true)->value) {
for (int index = 0; index < partplate_list.get_plate_count(); index++) {
if ((plate_to_slice != 0) && (plate_to_slice != (index + 1)))
continue;
Slic3r::GUI::PartPlate *plate = partplate_list.get_plate(index);
// Printing by object disables the tower only with more than one instance.
bool is_seq_print = false;
get_print_sequence(plate, m_print_config, is_seq_print);
if (is_seq_print && plate->printable_instance_size() > 1)
continue;
// An empty estimate is a plate that prints no tower (one filament and
// neither smooth timelapse, wrapping detection nor a raft).
Vec3d wt_pos, wt_size;
plate->estimate_wipe_tower_polygon(m_print_config, index, wt_pos, wt_size);
if (wt_size(0) < EPSILON || wt_size(1) < EPSILON)
continue;
ConfigOptionFloat wt_x_opt((float) wt_pos(0));
ConfigOptionFloat wt_y_opt((float) wt_pos(1));
m_print_config.option<ConfigOptionFloats>("wipe_tower_x", true)->set_at(&wt_x_opt, index, 0);
m_print_config.option<ConfigOptionFloats>("wipe_tower_y", true)->set_at(&wt_y_opt, index, 0);
BOOST_LOG_TRIVIAL(info) << boost::format("plate %1%: wipe tower clamped to {%2%, %3%}, size {%4%, %5%}")
% (index + 1) % wt_pos(0) % wt_pos(1) % wt_size(0) % wt_size(1);
}
}
while(!finished)
{
//BBS: slice every partplate one by one
+35 -1
View File
@@ -8,7 +8,11 @@
#define NANOSVGRAST_IMPLEMENTATION
#include "nanosvg/nanosvgrast.h"
#include "libslic3r/BoundingBox.hpp"
#include "libslic3r/GCode.hpp"
#include "libslic3r/GCode/WipeTower.hpp"
#include "libslic3r/GCode/WipeTowerEstimate.hpp"
#include "libslic3r/Geometry.hpp"
#include "libslic3r/Preset.hpp"
#include "libslic3r/Config.hpp"
#include "libslic3r/PresetBundle.hpp"
@@ -116,15 +120,45 @@ Vec2d printable_area_center(const DynamicPrintConfig &cfg)
return 0.5 * (lo + hi);
}
// Put the prime tower where the GUI and CLI would before slicing. The config default (x 15, y 220)
// lies off any bed shallower than the tower, and generation rejects an off-plate tower instead of
// exporting it. Beside the centred cube, clear of the edge exclusion strips some beds carry, then
// pulled inside the printable outline by the tower's own estimated footprint, with a few mm of
// clearance so the conflict checker never sees the two touch.
void place_wipe_tower(DynamicPrintConfig &cfg, const Vec2d &center)
{
const auto *area = cfg.option<ConfigOptionPoints>("printable_area");
if (area == nullptr || area->values.size() < 3)
return;
const WipeTowerFootprint footprint = estimate_wipe_tower_footprint(cfg, resolve_wipe_tower_type(cfg), {0, 1}, cfg.opt_float("layer_height"), 10.);
if (footprint.depth < EPSILON)
return;
const double margin = WIPE_TOWER_MARGIN + footprint.brim_width;
// The position is the tower's own origin; a rotated tower extends from it in another
// direction, so place the rotated box's extents rather than the origin.
Slic3r::Polygon box({Point::new_scale(0., 0.), Point::new_scale(footprint.width, 0.), Point::new_scale(footprint.width, footprint.depth), Point::new_scale(0., footprint.depth)});
box.rotate(Geometry::deg2rad(cfg.opt_float("wipe_tower_rotation_angle")));
const BoundingBox local = get_extents(box);
const Vec2d lo = unscale(local.min);
const Vec2d size = unscale(local.max) - lo;
Vec2d pos(center.x() + 5. + margin + 5. - lo.x(), center.y() - size.y() / 2. - lo.y());
box.translate(Point::new_scale(pos.x(), pos.y()));
const Vec2f move = WipeTower::move_box_inside_polygon(get_extents(box), Polygons{Polygon::new_scale(area->values)}, scaled<coord_t>(margin));
pos += move.cast<double>();
cfg.option<ConfigOptionFloats>("wipe_tower_x", true)->values = {pos.x()};
cfg.option<ConfigOptionFloats>("wipe_tower_y", true)->values = {pos.y()};
}
// Slice one centered cube that switches from filament 1 to filament 2 partway up, so exactly one
// filament change fires, then export. The change drives the printer's own change_filament_gcode: on a
// single-nozzle machine it rides the AMS prime tower (append_tcr), on a multi-nozzle machine it routes
// through the nozzle swap (set_extruder / append_tcr2) - the engine picks the path from the printer's
// topology, so one model covers both. An undefined placeholder in any shipped custom g-code throws
// Slic3r::PlaceholderParserError from export.
std::string slice_two_color_cube_and_export(const DynamicPrintConfig &cfg, bool is_bbl)
std::string slice_two_color_cube_and_export(DynamicPrintConfig cfg, bool is_bbl)
{
const Vec2d center = printable_area_center(cfg);
place_wipe_tower(cfg, center);
TriangleMesh m = make_cube(10, 10, 10);
m.translate(float(center.x() - 5.), float(center.y() - 5.), 0.f);
+2
View File
@@ -272,6 +272,8 @@ set(lisbslic3r_sources
GCode/WipeTower2.hpp
GCode/WipeTower.cpp
GCode/WipeTower.hpp
GCode/WipeTowerEstimate.cpp
GCode/WipeTowerEstimate.hpp
GCodeWriter.cpp
GCodeWriter.hpp
Geometry/ArcWelder.hpp
-39
View File
@@ -102,45 +102,6 @@ struct ZipUnicodePathExtraField
}
};
// Validate that a relative file path does not escape the root directory via path traversal.
static bool is_path_within_root(const std::string& file_path, const boost::filesystem::path& root)
{
if (file_path.empty())
return false;
boost::filesystem::path p(file_path);
if (p.is_absolute())
return false;
// Reject any path component that is ".."
for (const auto& component : p) {
if (component == "..")
return false;
}
// Resolve the full path and verify it starts with the canonical root (also catches symlink escapes)
try {
boost::filesystem::path full_path = root / p;
boost::filesystem::path canonical_root = boost::filesystem::weakly_canonical(root);
boost::filesystem::path canonical_full = boost::filesystem::weakly_canonical(full_path);
auto root_str = canonical_root.string();
auto full_str = canonical_full.string();
if (full_str.length() < root_str.length())
return false;
if (full_str.compare(0, root_str.length(), root_str) != 0)
return false;
// Ensure it's a proper prefix (not just a substring of a longer directory name)
if (full_str.length() > root_str.length() &&
full_str[root_str.length()] != boost::filesystem::path::preferred_separator)
return false;
} catch (const boost::filesystem::filesystem_error&) {
return false;
}
return true;
}
// VERSION NUMBERS
// 0 : .3mf, files saved by older slic3r or other applications. No version definition in them.
// 1 : Introduction of 3mf versioning. No other change in data saved into 3mf files.
+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};
+27
View File
@@ -42,9 +42,36 @@ public:
static const std::map<float, float> min_depth_per_height;
static float get_limit_depth_by_height(float max_height);
static float get_auto_brim_by_height(float max_height);
// Both generators lay the brim in whole loops one line spacing apart, so the printed width
// differs from the configured one. WipeTower reports it with half a spacing of line width
// added, WipeTower2 reports the loops alone; an estimate has to round like the generator
// whose G-code it stands in for.
static float estimate_brim_real_width(float brim_width, float nozzle_diameter, float first_layer_height, bool type2);
// Depth a Type1 tower reserves once nothing but wrapping detection asks for one.
static float get_wrapping_detection_depth();
// Line width of the nozzle-change purge lines at this nozzle diameter.
static float nozzle_change_perimeter_width(float nozzle_diameter);
static TriangleMesh its_make_rib_tower(float width, float depth, float height, float rib_length, float rib_width, bool fillet_wall);
static TriangleMesh its_make_rib_brim(const Polygon& brim, float layer_height);
static Polygon rib_section(float width, float depth, float rib_length, float rib_width, bool fillet_wall);
// One filament's share of a Type1 tower layer, as plan_tower_new() reserves it.
struct PurgeEstimate
{
float prime_volume = 0.f; // mm3 wiped after changing to this filament
int category = 0; // filament_adhesiveness_category; one purge block per category
float filament_change_length = 0.f; // mm of filament rammed when it leaves its nozzle; 0 when no nozzle change is planned
float filament_diameter = 1.75f;
};
// Depth of the Type1 purge stack at the given width (also the rectangle-wall depth): each
// purge is whole lines at the block infill gap, one block per adhesiveness category sized by
// its worst layer, stacked behind one perimeter width.
static float estimate_tower_blocks_depth(const std::vector<PurgeEstimate> &purges, float width, float layer_height, float nozzle_diameter, float extra_spacing);
// Side of the square bounding a rib-wall tower's first layer, brim excluded: the body plus the
// rib bulge, with the ribs extended to the height-based minimum as both generators do.
static float rib_footprint_side(float width, float depth, float rib_width, float extra_rib_length, float max_height);
// Type1 rib tower: plan_tower_new() squares the tower from the depth at the configured width,
// then re-plans the depth at the squared width.
static float 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);
// Translation that brings a footprint inside the printable outline, padded by offset. The prime
// tower is validated against the real outline (see layered_print_cleareance_valid), so clamping
// against the bounding box alone would leave it off a delta or hexagonal bed. box and polygons
+17
View File
@@ -2129,6 +2129,23 @@ std::pair<double, double> WipeTower2::get_wipe_tower_cone_base(double width, dou
return std::make_pair(R, support_scale);
}
Polygon WipeTower2::cone_base_polygon(double width, double depth, double height, double angle_deg)
{
Polygon box({Point::new_scale(Vec2d(0., 0.)), Point::new_scale(Vec2d(width, 0.)),
Point::new_scale(Vec2d(width, depth)), Point::new_scale(Vec2d(0., depth))});
if (angle_deg <= EPSILON || height <= EPSILON || width <= EPSILON || depth <= EPSILON)
return box;
const auto [R, x_scale] = get_wipe_tower_cone_base(width, height, depth, angle_deg);
if (R <= EPSILON)
return box;
const Vec2d center(width / 2., depth / 2.);
Polygon ellipse;
for (double alpha = 0.; alpha < 2. * M_PI; alpha += M_PI / 20.)
ellipse.points.push_back(Point::new_scale(center + R * Vec2d(std::cos(alpha) / x_scale, std::sin(alpha))));
Polygons u = union_({box, ellipse});
return u.empty() ? box : u.front();
}
// Static method to extract wipe_volumes[from][to] from the configuration.
// Takes a ConfigBase so the GUI's wipe tower size estimate can pass the plate's
// DynamicPrintConfig directly instead of materializing a full PrintConfig per call.
+4
View File
@@ -27,6 +27,10 @@ public:
// in WipeTowerIntegration::append_tcr2 does not strip it.
static const std::string wait_for_temp_tag() { return ";_WAIT_FOR_TEMP_ON_WIPE_TOWER"; }
static std::pair<double, double> get_wipe_tower_cone_base(double width, double height, double depth, double angle_deg);
// First-layer outline of a cone-wall tower in tower-local (scaled) coordinates: body box
// unioned with the cone's base ellipse — the model first_layer_wipe_tower_corners uses,
// and generate_support_cone_wall stays within it. Brim not included.
static Polygon cone_base_polygon(double width, double depth, double height, double angle_deg);
static std::vector<std::vector<float>> extract_wipe_volumes(const ConfigBase& config);
// Estimated total flush volume of a SEMM print with the given number of filaments,
// used to reserve wipe tower space before the tower is generated.
+202
View File
@@ -0,0 +1,202 @@
#include "WipeTowerEstimate.hpp"
#include "WipeTower.hpp"
#include "WipeTower2.hpp"
#include "../Config.hpp"
#include "../PrintConfig.hpp"
#include "../libslic3r.h"
#include <algorithm>
#include <cmath>
#include <set>
namespace Slic3r {
// Every caller today declares all these keys, but the signature accepts any ConfigBase: fall
// back to the key's declared default, never to a hand-copied constant.
static const ConfigOption *option_of(const ConfigBase &config, const char *key)
{
if (const ConfigOption *opt = config.option(key); opt != nullptr)
return opt;
if (const ConfigDef *def = config.def(); def != nullptr)
if (const ConfigOptionDef *opt_def = def->get(key); opt_def != nullptr)
return opt_def->default_value.get();
return nullptr;
}
WipeTowerType resolve_wipe_tower_type(const ConfigBase &config)
{
// printer_model is what the CLI keys its Bambu Lab detection on; the GUI's vendor flag
// agrees for every shipped profile.
if (const auto *model = dynamic_cast<const ConfigOptionString *>(config.option("printer_model"));
model != nullptr && model->value.compare(0, 9, "Bambu Lab") == 0)
return WipeTowerType::Type1;
// By value, not by concrete type: a static PrintConfig holds ConfigOptionEnum<T>, a
// DynamicConfig built from presets holds ConfigOptionEnumGeneric, and both answer getInt().
const ConfigOption *type = option_of(config, "wipe_tower_type");
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<T> 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<unsigned int> &filament_ids, double layer_height, double max_object_height)
{
WipeTowerFootprint footprint;
footprint.height = max_object_height;
const size_t filaments_cnt = filament_ids.size();
if (filaments_cnt == 0 || layer_height < EPSILON)
return footprint;
auto opt_float = [&config](const char *key) {
const ConfigOption *opt = option_of(config, key);
return opt != nullptr ? opt->getFloat() : 0.;
};
auto opt_bool = [&config](const char *key) {
const ConfigOption *opt = option_of(config, key);
return opt != nullptr && opt->getBool();
};
auto opt_enum = [&config](const char *key, int fallback) {
const ConfigOption *opt = option_of(config, key);
return opt != nullptr ? opt->getInt() : fallback;
};
auto floats_of = [&config](const char *key) { return dynamic_cast<const ConfigOptionFloats *>(option_of(config, key)); };
auto max_of = [&floats_of](const char *key, double fallback) {
const auto *opt = floats_of(key);
return (opt != nullptr && !opt->values.empty()) ? *std::max_element(opt->values.begin(), opt->values.end()) : fallback;
};
auto float_at = [&floats_of](const char *key, unsigned int id, double fallback) {
const auto *opt = floats_of(key);
return (opt != nullptr && !opt->values.empty()) ? opt->get_at(id) : fallback;
};
auto int_at = [&config](const char *key, unsigned int id, int fallback) {
const auto *opt = dynamic_cast<const ConfigOptionInts *>(option_of(config, key));
return (opt != nullptr && !opt->values.empty()) ? opt->get_at(id) : fallback;
};
// Both planners size every layer, so the tower has to fit its thinnest one: the first layer
// when it is printed thinner than the rest.
const double first_layer_height = opt_float("initial_layer_print_height");
if (first_layer_height > EPSILON)
layer_height = std::min(layer_height, first_layer_height);
const bool type1 = tower_type == WipeTowerType::Type1;
const double width = opt_float("prime_tower_width");
const double prime_volume = opt_float("prime_volume");
// Type1 spaces its purge lines by prime_tower_infill_gap, Type2 by wipe_tower_extra_spacing.
// Type2's extra flow cancels out of the depth: the line length is divided by it and the row
// pitch multiplied by it (WipeTower2::get_wipe_depth).
const double extra_spacing = opt_float(type1 ? "prime_tower_infill_gap" : "wipe_tower_extra_spacing") / 100.;
const double rib_width = opt_float("wipe_tower_rib_width");
const double extra_rib_length = opt_float("wipe_tower_extra_rib_length");
const auto *nozzle_opt = floats_of("nozzle_diameter");
const double nozzle_diameter = (nozzle_opt != nullptr && !nozzle_opt->values.empty()) ? nozzle_opt->values.front() : 0.4;
const bool dual_nozzle = nozzle_opt != nullptr && nozzle_opt->values.size() == 2;
const bool rib_wall = opt_enum("wipe_tower_wall_type", int(WipeTowerWallType::wtwRectangle)) == int(WipeTowerWallType::wtwRib);
const bool smooth_timelapse = opt_enum("timelapse_type", int(TimelapseType::tlTraditional)) == int(TimelapseType::tlSmooth);
const bool wrapping = opt_bool("enable_wrapping_detection");
// Reasons a tower is printed with no tool change to purge for: the ones that stop
// normalize_fdm_2 clearing enable_prime_tower. Its mixed-filament case is not modelled.
const bool need_wipe_tower = smooth_timelapse || wrapping;
// A tower printed for one of the reasons above has no tool change to purge for; both
// planners give it the idle depth below and nothing more.
const size_t purge_count = filaments_cnt > 1 ? (dual_nozzle ? filaments_cnt : filaments_cnt - 1) : 0;
// Type2 purges one volume per tool change. Type1 plans per filament below; here the volume
// only decides whether a tower exists.
double volume = prime_volume * double(purge_count);
if (dual_nozzle) {
// Dual-nozzle printers also purge the filament change length on the tower.
const double length = max_of("filament_change_length", 0.);
const double diameter = max_of("filament_diameter", 1.75);
volume += length * PI * diameter * diameter / 4. * double(filaments_cnt / 2);
}
// Single-extruder multi-material purges the flush matrix instead of the prime volume.
const bool semm_flush = opt_bool("purge_in_prime_tower") && opt_bool("single_extruder_multi_material");
if (semm_flush)
volume = WipeTower2::estimate_semm_flush_volume(config, filaments_cnt);
// The Type1 planner wipes each filament's own prime volume after changing to it, in a block
// per adhesiveness category. On a two-nozzle printer the leaving filament is also rammed at
// every nozzle change; the tool order groups filaments by nozzle, so a layer crosses
// (nozzles used - 1) times, charged here to the longest ramming.
std::vector<WipeTower::PurgeEstimate> purges;
if (type1 && filaments_cnt > 1) {
const bool saving_mode = opt_enum("prime_volume_mode", int(PrimeVolumeMode::pvmDefault)) == int(PrimeVolumeMode::pvmSaving);
std::set<int> nozzles;
size_t longest_ramming = 0;
for (size_t i = 0; i < filaments_cnt; ++i) {
const unsigned int id = filament_ids[i];
WipeTower::PurgeEstimate purge;
purge.prime_volume = saving_mode ? 15.f : float(float_at("filament_prime_volume", id, prime_volume));
purge.category = int_at("filament_adhesiveness_category", id, 0);
purge.filament_diameter = float(float_at("filament_diameter", id, 1.75));
purges.push_back(purge);
if (dual_nozzle) {
nozzles.insert(int_at("filament_map", id, 1));
if (float_at("filament_change_length", id, 0.) > float_at("filament_change_length", filament_ids[longest_ramming], 0.))
longest_ramming = i;
}
}
if (nozzles.size() > 1)
purges[longest_ramming].filament_change_length = float(float_at("filament_change_length", filament_ids[longest_ramming], 0.) * double(nozzles.size() - 1));
}
// Both wall types decide this together: over-reserving only wastes bed area, but reporting
// no tower for one that is built collapses the validation hull to a point.
// A tool change is a reason on its own (see the base commit); Type1 already reserves
// per filament, Type2 has only the volume, which can resolve to zero.
const bool has_purge = type1 ? !purges.empty() : volume > EPSILON;
if (!has_purge && filaments_cnt < 2 && !need_wipe_tower)
return footprint;
const double min_depth = WipeTower::get_limit_depth_by_height(float(max_object_height));
const float perimeter_width = float(nozzle_diameter) * 1.25f; // Width_To_Nozzle_Ratio
// With nothing to purge, plan_tower_new sizes the tower for wrapping detection or the
// stability minimum; WipeTower2 only knows the latter.
const double idle_depth = (type1 && wrapping && !smooth_timelapse) ? WipeTower::get_wrapping_detection_depth() : min_depth;
if (rib_wall) {
// Both planners square the tower to the purge area and extend the ribs, not the body,
// below the stability minimum.
double side;
if (!purges.empty())
side = WipeTower::estimate_rib_tower_bbox_side(purges, float(width), float(layer_height), float(nozzle_diameter), float(extra_spacing), float(rib_width), float(extra_rib_length), float(max_object_height));
else {
const double square = has_purge ? std::sqrt(volume / layer_height * extra_spacing) : idle_depth;
side = WipeTower::rib_footprint_side(float(square), float(square), float(rib_width), float(extra_rib_length), float(max_object_height));
}
footprint.width = footprint.depth = side;
} else {
double depth;
if (type1) {
// plan_tower_new stretches a short purge stack to the stability minimum behind its
// leading perimeter width.
depth = purges.empty() ? idle_depth : std::max(min_depth + perimeter_width, double(WipeTower::estimate_tower_blocks_depth(purges, float(width), float(layer_height), float(nozzle_diameter), float(extra_spacing))));
} else {
depth = volume / (layer_height * width);
// The flush volumes already hold the spacing between wipes.
if (!semm_flush)
depth *= extra_spacing;
depth = std::max(min_depth, depth);
}
footprint.width = width;
footprint.depth = depth;
}
footprint.brim_width = opt_float("prime_tower_brim_width");
if (footprint.brim_width < 0)
footprint.brim_width = WipeTower::get_auto_brim_by_height(float(max_object_height));
footprint.brim_width = WipeTower::estimate_brim_real_width(float(footprint.brim_width), float(nozzle_diameter), float(first_layer_height > EPSILON ? first_layer_height : layer_height), !type1);
return footprint;
}
} // namespace Slic3r
+47
View File
@@ -0,0 +1,47 @@
#pragma once
#include <vector>
#include "../Polygon.hpp"
namespace Slic3r {
class ConfigBase;
enum class WipeTowerType;
// Pre-slice footprint of the wipe tower, shared by validation (Print), the GUI's placement
// clamp/preview/arrange and the CLI placement. The arithmetic is shared; the inputs below are
// not, so a change to how one caller derives them has to be mirrored in the others.
struct WipeTowerFootprint
{
double width = 0.; // effective width: equals depth for a rib wall, which squares the tower
double depth = 0.; // 0 when these inputs imply no tower
double height = 0.; // tallest object; drives the stability floor and the auto brim
double brim_width = 0.; // printed width: auto (-1) resolved by height, laid in whole loops
};
// Which planner builds the tower: Bambu Lab printers always get Type1, the rest follow
// wipe_tower_type. The rule Print::wipe_tower_type() and the CLI apply, read off the config so
// 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.
// layer_height: thinnest layer the objects are sliced at. The first layer is folded in here.
//
// A raft is deliberately not a reason: normalize_fdm_2 clears enable_prime_tower for a plate
// purging one filament unless smooth timelapse or wrapping detection is on.
WipeTowerFootprint estimate_wipe_tower_footprint(const ConfigBase &config,
WipeTowerType tower_type,
const std::vector<unsigned int> &filament_ids,
double layer_height,
double max_object_height);
} // namespace Slic3r
+6 -1
View File
@@ -60,10 +60,15 @@ auto MinimumSpanningTree::prim(std::vector<Point> vertices) const -> AdjacencyGr
//This search is O(V) right now, which can be made down to O(log(V)). This reduces the overall time complexity from O(V*V) to O(V*log(E)).
//However that requires an implementation of a heap that supports the decreaseKey operation, which is not in the std library.
//TODO: Implement this?
// Break equal-distance ties on coordinates: the map is keyed by address, so its
// iteration order (and therefore the first minimum) would otherwise depend on where
// the vertices were allocated.
using MapValue = std::pair<const Point*, coordf_t>;
const auto closest = std::min_element(smallest_distance.begin(), smallest_distance.end(),
[](const MapValue& a, const MapValue& b) {
return a.second < b.second;
if (a.second != b.second)
return a.second < b.second;
return *a.first < *b.first;
});
//Add this point to the graph and remove it from the candidates.
+15 -1
View File
@@ -1635,6 +1635,12 @@ PresetsConfigSubstitutions PresetBundle::import_presets(std::vector<std::string>
metadata.id = to_string(uuid);
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " bundle_id was empty, so generating a UUID: " << metadata.id;
}
if (has_bundle_structure && !is_path_within_root(metadata.id, user_folder / user_id / PRESET_LOCAL_DIR)) {
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " bundle id escapes the bundle directory, not importing: " << metadata.id;
fclose(zipFile);
fs::remove_all(temp_folder, ec);
continue;
}
// Build bundle directory path based on whether bundle_structure.json was present
fs::path bundle_base_dir;
@@ -1657,11 +1663,15 @@ PresetsConfigSubstitutions PresetBundle::import_presets(std::vector<std::string>
if (status) {
std::string file_name = file_stat.m_filename;
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " From zip file: " << file << ". Read file name: " << file_stat.m_filename;
size_t index = file_name.find_last_of('/');
size_t index = file_name.find_last_of("/\\");
if (std::string::npos != index) {
file_name = file_name.substr(index + 1);
}
if (BUNDLE_STRUCTURE_JSON_NAME == file_name) continue;
if (!is_path_within_root(file_name, temp_folder)) {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << " zip entry escapes the temp directory, skipping: " << file_stat.m_filename;
continue;
}
// create target file path
std::string target_file_path = boost::filesystem::path(temp_folder / file_name).make_preferred().string();
@@ -1750,6 +1760,10 @@ bool PresetBundle::import_json_presets(PresetsConfigSubstitutions & s
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << " Preset type is unknown, not loading: " << name;
return false;
}
if (!is_path_within_root(name, fs::path(collection->m_dir_path))) {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << " Preset name escapes the preset directory, not loading: " << name;
return false;
}
const PresetOrigin load_origin = detect_origin_from_path(boost::filesystem::path(bundle_dir));
const std::string preset_name = get_preset_canonical_name(name, load_origin);
+104 -98
View File
@@ -20,6 +20,7 @@
#include "GCode.hpp"
#include "GCode/WipeTower.hpp"
#include "GCode/WipeTower2.hpp"
#include "GCode/WipeTowerEstimate.hpp"
#include "Utils.hpp"
#include "PrintConfig.hpp"
#include "MaterialType.hpp"
@@ -1031,20 +1032,21 @@ static StringObjectException layered_print_cleareance_valid(const Print &print,
//BBS: add the wipe tower check logic
const PrintConfig & config = print.config();
int filaments_count = print.extruders().size();
// Custom G-code tool changes (MultiAsSingle) build a real tower on a plate whose objects
// all use one filament, so they have to be counted or the hull below collapses to a point.
int filaments_count = print.extruders(true).size();
int plate_index = print.get_plate_index();
const Vec3d plate_origin = print.get_plate_origin();
float x = config.wipe_tower_x.get_at(plate_index) + plate_origin(0);
float y = config.wipe_tower_y.get_at(plate_index) + plate_origin(1);
float width = config.prime_tower_width.value;
float a = config.wipe_tower_rotation_angle.value;
//float v = config.wiping_volume.value;
float depth = print.wipe_tower_data(filaments_count).depth;
//float brim_width = print.wipe_tower_data(filaments_count).brim_width;
if (config.wipe_tower_wall_type.value == WipeTowerWallType::wtwRib)
width = depth;
// The estimate resolves the effective width (a rib wall squares the tower).
const WipeTowerData &wipe_tower_estimate = print.wipe_tower_data(filaments_count);
float width = wipe_tower_estimate.width;
float depth = wipe_tower_estimate.depth;
float brim_width = wipe_tower_estimate.brim_width;
Polygons convex_hulls_temp;
if (print.has_wipe_tower()) {
@@ -1066,36 +1068,54 @@ 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()) {
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
// 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")};
}
// Skip the containment check for towers that will never be printed (single-filament
// prints without smooth timelapse keep the config's tower position but emit nothing).
// Pre-generation only the body square is tested — the auto-brim estimate can overshoot
// the generated brim by several mm and must not hard-fail a print that physically fits.
// Post-generation the mesh bottom already includes the real brim, so the exact
// footprint is tested.
if (filaments_count > 1 || print.enable_timelapse_print()) {
// The shared printable polygon is plate-local, while the tower polygons above are
// already shifted by the plate origin.
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);
if (!diff(convex_hulls_temp, printable_polys).empty())
return {L("Prime Tower") + L(" is partially outside the printable area, and it cannot be printed.\n")};
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.
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);
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 {};
}
@@ -3997,74 +4017,25 @@ bool Print::has_wipe_tower() const
const WipeTowerData &Print::wipe_tower_data(size_t filaments_cnt) const
{
// If the wipe tower wasn't created yet, make sure the depth and brim_width members are set to default.
double max_height = 0;
for (size_t obj_idx = 0; obj_idx < m_objects.size(); obj_idx++) {
double object_z = (double) m_objects[obj_idx]->size().z();
max_height = std::max(unscale_(object_z), max_height);
// Until the tower is generated, size it with the estimate the GUI/CLI placement uses, so
// validation cannot reject a position the clamp just accepted.
if (is_step_done(psWipeTower) || filaments_cnt == 0)
return m_wipe_tower_data;
double max_height = 0.;
double layer_height = std::numeric_limits<double>::max();
for (const PrintObject *object : m_objects) {
max_height = std::max(max_height, unscale_(double(object->size().z())));
layer_height = std::min(layer_height, object->config().layer_height.value);
}
if (max_height < EPSILON) return m_wipe_tower_data;
if (max_height < EPSILON)
return m_wipe_tower_data;
double layer_height = 0.08f; // hard code layer height
layer_height = m_objects.front()->config().layer_height.value;
auto timelapse_type = config().option<ConfigOptionEnum<TimelapseType>>("timelapse_type");
bool need_wipe_tower = (timelapse_type ? (timelapse_type->value == TimelapseType::tlSmooth) : false) | (m_config.wipe_tower_wall_type.value == WipeTowerWallType::wtwRib);
double extra_spacing = config().option("prime_tower_infill_gap")->getFloat() / 100.;
double rib_width = config().option("wipe_tower_rib_width")->getFloat();
double filament_change_volume = 0.;
{
std::vector<double> filament_change_lengths;
auto filament_change_lengths_opt = config().option<ConfigOptionFloats>("filament_change_length");
if (filament_change_lengths_opt) filament_change_lengths = filament_change_lengths_opt->values;
double length = filament_change_lengths.empty() ? 0 : *std::max_element(filament_change_lengths.begin(), filament_change_lengths.end());
double diameter = 1.75;
std::vector<double> diameters;
auto filament_diameter_opt = config().option<ConfigOptionFloats>("filament_diameter");
if (filament_diameter_opt) diameters = filament_diameter_opt->values;
diameter = diameters.empty() ? diameter : *std::max_element(diameters.begin(), diameters.end());
filament_change_volume = length * PI * diameter * diameter / 4.;
}
if (! is_step_done(psWipeTower) && filaments_cnt !=0) {
double wipe_volume = m_config.prime_volume;
int filament_depth_count = m_config.nozzle_diameter.values.size() == 2 ? filaments_cnt : filaments_cnt - 1;
if (filaments_cnt == 1 && enable_timelapse_print()) filament_depth_count = 1;
double volume = wipe_volume * filament_depth_count;
if (m_config.nozzle_diameter.values.size() == 2) volume += filament_change_volume * (int) (filaments_cnt / 2);
// Sizing should take into account currently set wiping volumes.
// For a long time, the initial preview would just use 900/width per toolchange (15mm on a 60mm wide tower)
// and it worked well enough. Let's try to do slightly better by accounting for the purging volumes.
const bool semm_flush = m_config.purge_in_prime_tower && m_config.single_extruder_multi_material;
if (semm_flush) volume = WipeTower2::estimate_semm_flush_volume(m_config, filaments_cnt);
if (m_config.wipe_tower_wall_type.value == WipeTowerWallType::wtwRib) {
double depth = std::sqrt(volume / layer_height * extra_spacing);
if (need_wipe_tower || filaments_cnt > 1) {
float min_wipe_tower_depth = WipeTower::get_limit_depth_by_height(max_height);
depth = std::max((double) min_wipe_tower_depth, depth);
depth += rib_width / std::sqrt(2) + config().wipe_tower_extra_rib_length.value;
const_cast<Print *>(this)->m_wipe_tower_data.depth = depth;
const_cast<Print *>(this)->m_wipe_tower_data.brim_width = m_config.prime_tower_brim_width;
}
}
else {
double width = m_config.prime_tower_width;
double depth = volume / (layer_height * width);
// The flush volumes already hold the spacing between wipes.
if (!semm_flush) depth *= extra_spacing;
if (need_wipe_tower || depth > EPSILON) {
float min_wipe_tower_depth = WipeTower::get_limit_depth_by_height(max_height);
depth = std::max((double) min_wipe_tower_depth, depth);
}
const_cast<Print *>(this)->m_wipe_tower_data.depth = depth;
const_cast<Print *>(this)->m_wipe_tower_data.brim_width = m_config.prime_tower_brim_width;
}
if (m_config.prime_tower_brim_width < 0) const_cast<Print *>(this)->m_wipe_tower_data.brim_width = WipeTower::get_auto_brim_by_height(max_height);
}
const WipeTowerFootprint footprint = estimate_wipe_tower_footprint(m_config, this->wipe_tower_type(), this->extruders(true), layer_height, max_height);
WipeTowerData &data = const_cast<Print *>(this)->m_wipe_tower_data;
data.depth = float(footprint.depth);
data.width = float(footprint.width);
data.brim_width = float(footprint.brim_width);
return m_wipe_tower_data;
}
@@ -4290,6 +4261,7 @@ void Print::_make_wipe_tower()
m_wipe_tower_data.tool_changes.reserve(m_wipe_tower_data.tool_ordering.layer_tools().size());
wipe_tower.generate_new(m_wipe_tower_data.tool_changes);
m_wipe_tower_data.depth = wipe_tower.get_depth();
m_wipe_tower_data.width = wipe_tower.width();
m_wipe_tower_data.brim_width = wipe_tower.get_brim_width();
m_wipe_tower_data.bbx = wipe_tower.get_bbx();
m_wipe_tower_data.rib_offset = wipe_tower.get_rib_offset();
@@ -4403,6 +4375,7 @@ void Print::_make_wipe_tower()
m_wipe_tower_data.tool_changes.reserve(m_wipe_tower_data.tool_ordering.layer_tools().size());
wipe_tower.generate(m_wipe_tower_data.tool_changes);
m_wipe_tower_data.depth = wipe_tower.get_depth();
m_wipe_tower_data.width = wipe_tower.width();
m_wipe_tower_data.z_and_depth_pairs = wipe_tower.get_z_and_depth_pairs();
m_wipe_tower_data.brim_width = wipe_tower.get_brim_width();
m_wipe_tower_data.height = wipe_tower.get_wipe_tower_height();
@@ -4438,7 +4411,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
@@ -4451,6 +4426,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
@@ -5999,17 +5996,26 @@ 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});
wipe_tower_mesh_data->bottom = {scaled(Vec2f{-brim_width, -brim_width}), scaled(Vec2f{width + brim_width, 0}), scaled(Vec2f{width + brim_width, depth + brim_width}),
scaled(Vec2f{0, depth})};
wipe_tower_mesh_data->bottom = {scaled(Vec2f{-brim_width, -brim_width}), scaled(Vec2f{width + brim_width, -brim_width}),
scaled(Vec2f{width + brim_width, depth + brim_width}), scaled(Vec2f{-brim_width, depth + brim_width})};
} else {
wipe_tower_mesh_data->real_wipe_tower_mesh = WipeTower::its_make_rib_tower(width, depth, height, rib_length, rib_width, fillet_wall);
wipe_tower_mesh_data->bottom = WipeTower::rib_section(width, depth, rib_length, rib_width, fillet_wall);
+5 -1
View File
@@ -782,6 +782,9 @@ struct WipeTowerData
// Depth of the wipe tower to pass to GLCanvas3D for exact bounding box:
float depth;
// Effective width (a rib wall squares the tower): the estimate until generation, then the
// generated width, so it never disagrees with depth.
float width;
std::vector<std::pair<float, float>> z_and_depth_pairs;
float brim_width;
float height;
@@ -795,12 +798,13 @@ struct WipeTowerData
used_filament.clear();
number_of_toolchanges = -1;
depth = 0.f;
width = 0.f;
brim_width = 0.f;
height = 0.f;
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,
+79 -23
View File
@@ -2846,7 +2846,9 @@ void TreeSupport::drop_nodes()
const MinimumSpanningTree& mst = spanning_trees[group_index];
//In the first pass, merge all nodes that are close together.
std::vector<std::pair<const Point, SupportNode*>> nodes_vec(nodes_this_part.begin(), nodes_this_part.end());
tbb::parallel_for_each(nodes_vec.begin(), nodes_vec.end(), [&](const std::pair<const Point, SupportNode*>& entry) {
// Sequential: nodes merge into and invalidate each other in place, so parallel execution
// makes the merge order (and thus the result) depend on thread scheduling.
std::for_each(nodes_vec.begin(), nodes_vec.end(), [&](const std::pair<const Point, SupportNode*>& entry) {
SupportNode* p_node = entry.second;
SupportNode& node = *p_node;
if (!p_node->valid)
@@ -2934,7 +2936,32 @@ void TreeSupport::drop_nodes()
);
//In the second pass, move all middle nodes.
tbb::parallel_for_each(nodes_vec.begin(), nodes_vec.end(), [&](const std::pair<const Point, SupportNode*>& entry) {
// Still parallel: this pass only reads other nodes. Side effects (invalidation, new
// nodes, contact_nodes/unsupported_branch_leaves updates) are recorded per node and
// applied afterwards in node order. Node creation must be deferred too, since
// SupportNode's constructor writes `parent->child = this` on other nodes.
struct PendingNode {
Point position;
int distance_to_top = 0;
int support_roof_layers_below = 0;
bool to_buildplate = false;
SupportNode *parent = nullptr;
bool zero_max_move = false;
bool has_overhang = false;
ExPolygon overhang;
bool clamp_radius = false;
coordf_t parent_radius = 0;
double dist_to_outer = 0;
};
struct PassTwoResult {
bool invalidate = false;
bool unsupported_leaf = false;
std::vector<PendingNode> pending;
};
std::vector<PassTwoResult> pass2_results(nodes_vec.size());
auto pass2_body = [&](size_t node_idx) {
const std::pair<const Point, SupportNode*>& entry = nodes_vec[node_idx];
PassTwoResult& pass2_out = pass2_results[node_idx];
SupportNode* p_node = entry.second;
const SupportNode& node = *p_node;
@@ -2949,14 +2976,16 @@ void TreeSupport::drop_nodes()
ExPolygons overhangs_next = diff_clipped({ node.overhang }, get_collision(0, obj_layer_nr_next));
for(auto& overhang:overhangs_next) {
Point next_pt = overhang.contour.centroid();
SupportNode *next_node = m_ts_data->create_node(next_pt, p_node->distance_to_top + 1, obj_layer_nr_next,
p_node->support_roof_layers_below - (p_node->distance_to_top >= 0 ? 1 : 0),
to_buildplate, p_node, print_z_next, height_next);
next_node->max_move_dist = 0;
next_node->overhang = std::move(overhang);
m_ts_data->m_mutex.lock();
contact_nodes[layer_nr_next].emplace_back(next_node);
m_ts_data->m_mutex.unlock();
PendingNode pending;
pending.position = next_pt;
pending.distance_to_top = p_node->distance_to_top + 1;
pending.support_roof_layers_below = p_node->support_roof_layers_below - (p_node->distance_to_top >= 0 ? 1 : 0);
pending.to_buildplate = to_buildplate;
pending.parent = p_node;
pending.zero_max_move = true;
pending.has_overhang = true;
pending.overhang = std::move(overhang);
pass2_out.pending.emplace_back(std::move(pending));
}
return;
@@ -2973,17 +3002,17 @@ void TreeSupport::drop_nodes()
{
if (support_on_buildplate_only)
{
unsupported_branch_leaves.push_front({ layer_nr, p_node });
pass2_out.unsupported_leaf = true;
}
else {
p_node->valid = false;
pass2_out.invalidate = true;
}
return;
}
// if the link between parent and current is cut by contours, mark current as bottom contact node
if (p_node->parent && intersection_ln({p_node->position, p_node->parent->position}, layer_contours).empty()==false)
{
p_node->valid = false;
pass2_out.invalidate = true;
return;
}
}
@@ -3096,20 +3125,47 @@ void TreeSupport::drop_nodes()
}
auto next_collision = get_collision(0, obj_layer_nr_next);
const bool to_buildplate = !is_inside_ex(m_ts_data->m_layer_outlines[obj_layer_nr_next], next_layer_vertex);
SupportNode * next_node = m_ts_data->create_node(next_layer_vertex, node.distance_to_top + 1, obj_layer_nr_next,
node.support_roof_layers_below - (node.distance_to_top >= 0 ? 1 : 0),
to_buildplate, p_node, print_z_next, height_next);
// don't increase radius if next node will collide partially with the object (STUDIO-7883)
to_outside = projection_onto(next_collision, next_node->position);
to_outside = projection_onto(next_collision, next_layer_vertex);
direction_to_outer = to_outside - node.position;
double dist_to_outer = unscale_(direction_to_outer.cast<double>().norm());
next_node->radius = std::max(node.radius, std::min(next_node->radius, dist_to_outer));
get_max_move_dist(next_node);
m_ts_data->m_mutex.lock();
contact_nodes[layer_nr_next].push_back(next_node);
m_ts_data->m_mutex.unlock();
PendingNode pending;
pending.position = next_layer_vertex;
pending.distance_to_top = node.distance_to_top + 1;
pending.support_roof_layers_below = node.support_roof_layers_below - (node.distance_to_top >= 0 ? 1 : 0);
pending.to_buildplate = to_buildplate;
pending.parent = p_node;
pending.clamp_radius = true;
pending.parent_radius = node.radius;
pending.dist_to_outer = dist_to_outer;
pass2_out.pending.emplace_back(std::move(pending));
};
tbb::parallel_for(tbb::blocked_range<size_t>(0, nodes_vec.size()),
[&pass2_body](const tbb::blocked_range<size_t>& node_range) {
for (size_t node_idx = node_range.begin(); node_idx < node_range.end(); ++ node_idx)
pass2_body(node_idx);
});
// Apply the recorded side effects in node order.
for (size_t node_idx = 0; node_idx < nodes_vec.size(); ++ node_idx) {
PassTwoResult& pass2_out = pass2_results[node_idx];
for (PendingNode& pending : pass2_out.pending) {
SupportNode* next_node = m_ts_data->create_node(pending.position, pending.distance_to_top, obj_layer_nr_next,
pending.support_roof_layers_below, pending.to_buildplate, pending.parent, print_z_next, height_next);
if (pending.zero_max_move)
next_node->max_move_dist = 0;
if (pending.has_overhang)
next_node->overhang = std::move(pending.overhang);
if (pending.clamp_radius) {
next_node->radius = std::max(pending.parent_radius, std::min(next_node->radius, pending.dist_to_outer));
get_max_move_dist(next_node);
}
contact_nodes[layer_nr_next].push_back(next_node);
}
if (pass2_out.unsupported_leaf)
unsupported_branch_leaves.push_front({ layer_nr, nodes_vec[node_idx].second });
if (pass2_out.invalidate)
nodes_vec[node_idx].second->valid = false;
}
);
}
#ifdef SUPPORT_TREE_DEBUG_TO_SVG
+4 -7
View File
@@ -2382,13 +2382,10 @@ static void merge_influence_areas(
size_t num_buckets_initial;
{
// How many buckets per first merge iteration?
const size_t num_threads = tbb::this_task_arena::max_concurrency();
// 4 buckets per thread if possible,
const size_t num_buckets_min = (input_size + 2) / 4;
// 2 buckets per thread otherwise.
const size_t num_buckets_max = input_size / 2;
num_buckets_initial = num_buckets_min >= num_threads ? num_buckets_min : num_buckets_max;
const size_t bucket_size = num_buckets_min >= num_threads ? 4 : 2;
// Fixed at 4: merging is not associative, so sizing buckets off max_concurrency() made
// results depend on the core count of the slicing machine.
const size_t bucket_size = 4;
num_buckets_initial = (input_size + 2) / 4;
// Fill in the buckets.
SupportElementMerging *it = influence_areas.data();
// Reserve one more bucket to keep a single influence area which will not be merged in the first iteration.
+4
View File
@@ -255,6 +255,10 @@ extern bool is_gallery_file(const std::string& path, char const* type);
extern bool is_shapes_dir(const std::string& dir);
//BBS: add json support
extern bool is_json_file(const std::string& path);
// True if rel_path is relative, has no ".." component and, joined to root, still resolves inside it.
// Both '/' and '\\' are treated as separators on every platform, so an archive rejected on one OS
// is rejected on all of them.
extern bool is_path_within_root(const std::string &rel_path, const boost::filesystem::path &root);
// Orca: custom protocal support utils
inline bool is_orca_open(const std::string& url) { return boost::starts_with(url, "orcaslicer://open"); }
+3
View File
@@ -93,6 +93,9 @@ static constexpr double INSET_OVERLAP_TOLERANCE = 0.4;
static constexpr double EXTERNAL_INFILL_MARGIN = 3;
static constexpr double BRIDGE_INFILL_MARGIN = 1;
static constexpr double WIPE_TOWER_MARGIN = 1.;
// Margin for system placement of the wipe tower (defaults, re-placement, CLI). Positions
// within WIPE_TOWER_MARGIN stay valid: a user drag down to that limit is respected.
static constexpr double WIPE_TOWER_AUTO_MARGIN = 15.;
//FIXME Better to use an inline function with an explicit return type.
//inline coord_t scale_(coordf_t v) { return coord_t(floor(v / SCALING_FACTOR + 0.5f)); }
#define scale_(val) ((val) / SCALING_FACTOR)
+24
View File
@@ -1088,6 +1088,30 @@ bool is_json_file(const std::string& path)
return boost::iends_with(path, ".json");
}
bool is_path_within_root(const std::string &rel_path, const boost::filesystem::path &root)
{
auto is_separator = [](char c) { return c == '/' || c == '\\'; };
if (rel_path.empty() || is_separator(rel_path.front()) || (rel_path.size() > 1 && rel_path[1] == ':'))
return false;
for (size_t start = 0; start <= rel_path.size();) {
size_t end = start;
while (end < rel_path.size() && !is_separator(rel_path[end]))
++end;
if (rel_path.compare(start, end - start, "..") == 0)
return false;
start = end + 1;
}
// Resolve against the canonical root so a symlink inside it cannot lead back out.
try {
const std::string root_str = boost::filesystem::weakly_canonical(root).string();
const std::string full_str = boost::filesystem::weakly_canonical(root / rel_path).string();
return full_str.compare(0, root_str.size(), root_str) == 0 &&
(full_str.size() == root_str.size() || full_str[root_str.size()] == boost::filesystem::path::preferred_separator);
} catch (const boost::filesystem::filesystem_error &) {
return false;
}
}
bool is_img_file(const std::string &path)
{
return boost::iends_with(path, ".png") || boost::iends_with(path, ".svg");
+25 -3
View File
@@ -20,6 +20,8 @@
#include "libslic3r/AppConfig.hpp"
#include "libslic3r/PresetBundle.hpp"
#include "libslic3r/ClipperUtils.hpp"
#include "libslic3r/GCode/WipeTower.hpp"
#include "libslic3r/GCode/WipeTowerEstimate.hpp"
#include "libslic3r/Tesselate.hpp"
#include "libslic3r/PrintConfig.hpp"
@@ -919,6 +921,21 @@ int GLVolumeCollection::load_wipe_tower_preview(
GUI::PartPlateList& ppl = GUI::wxGetApp().plater()->get_partplate_list();
std::vector<int> plate_extruders = ppl.get_plate(plate_idx)->get_extruders(true);
TriangleMesh wipe_tower_shell = make_cube(width, depth, height);
// The brim is part of the printed footprint: draw it and fold it into the shell so the
// outside-bed shader and the drag clamp react to the true first-layer extent.
const bool show_brim = brim_width > 0.f;
const float brim_height = 0.2f; // one first layer, visual only
TriangleMesh brim_slab;
if (show_brim) {
// 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) {
if (extruder_id <= extruder_colors.size())
colors.push_back(extruder_colors[extruder_id - 1]);
@@ -929,14 +946,19 @@ int GLVolumeCollection::load_wipe_tower_preview(
// Orca: make it transparent
for(auto& color : colors)
color.a(0.66f);
const size_t slab_count = colors.size(); // per-filament body slabs; the brim part comes after
if (show_brim && !colors.empty())
colors.push_back(colors.front());
volumes.emplace_back(new GLWipeTowerVolume(colors));
GLWipeTowerVolume& v = *dynamic_cast<GLWipeTowerVolume*>(volumes.back());
v.model_per_colors.resize(colors.size());
for (int i = 0; i < colors.size(); i++) {
TriangleMesh color_part = make_cube(width, depth / colors.size(), height);
color_part.translate({ 0.f, depth * i / colors.size(), 0. });
for (size_t i = 0; i < slab_count; i++) {
TriangleMesh color_part = make_cube(width, depth / slab_count, height);
color_part.translate({ 0.f, depth * i / slab_count, 0. });
v.model_per_colors[i].init_from(color_part);
}
if (show_brim && !colors.empty())
v.model_per_colors[slab_count].init_from(brim_slab);
v.model.init_from(wipe_tower_shell);
v.mesh_raycaster = std::make_unique<GUI::MeshRaycaster>(std::make_shared<const TriangleMesh>(wipe_tower_shell));
v.set_convex_hull(wipe_tower_shell);
+24 -10
View File
@@ -2891,23 +2891,37 @@ void GLCanvas3D::reload_scene(bool refresh_immediately, bool force_full_scene_re
DynamicPrintConfig& proj_cfg = wxGetApp().preset_bundle->project_config;
float x = dynamic_cast<const ConfigOptionFloats*>(proj_cfg.option("wipe_tower_x"))->get_at(plate_id);
float y = dynamic_cast<const ConfigOptionFloats*>(proj_cfg.option("wipe_tower_y"))->get_at(plate_id);
float w = dynamic_cast<const ConfigOptionFloat*>(m_config->option("prime_tower_width"))->value;
float a = dynamic_cast<const ConfigOptionFloat*>(m_config->option("wipe_tower_rotation_angle"))->value;
// BBS
float v = dynamic_cast<const ConfigOptionFloat*>(m_config->option("prime_volume"))->value;
Vec3d plate_origin = ppl.get_plate(plate_id)->get_origin();
const Print* print = m_process->fff_print();
const Print* current_print = part_plate->fff_print();
if (!need_wipe_tower && part_plate->get_extruders(true).size() < 2) continue;
if (part_plate->get_objects_on_this_plate().empty()) continue;
float brim_width = print->wipe_tower_data(filaments_count).brim_width;
int nozzle_nums = wxGetApp().preset_bundle->get_printer_extruder_count();
Vec3d wipe_tower_size = ppl.get_plate(plate_id)->estimate_wipe_tower_size(full_config, w, v, nozzle_nums, 0, false, dynamic_cast<const ConfigOptionBool*>(dconfig.option("enable_wrapping_detection"))->value);
// Body and brim from this plate's own estimate: m_process->fff_print() is the
// selected plate's, so an auto brim drew every tower with that plate's brim.
const WipeTowerFootprint footprint = part_plate->estimate_wipe_tower_footprint(full_config);
// The estimate is also the answer to whether this plate prints a tower;
// deciding it here as well only gave the two room to drift.
if (footprint.depth <= 0.) continue;
float brim_width = float(footprint.brim_width);
Vec3d wipe_tower_size(footprint.width, footprint.depth, footprint.height);
// set_default_wipe_tower_pos_for_plate doesn't rerun when painting changes the
// filament count, so redo its clamp here on every reload — unconditionally: a
// paint-triggered reload can arrive before the background process invalidates
// psWipeTower, so gating on it would skip the clamp exactly when it is needed.
{
Vec3d clamped_pos, clamped_size;
part_plate->estimate_wipe_tower_polygon(full_config, plate_id, clamped_pos, clamped_size);
if (std::abs(x - (float) clamped_pos(0)) > EPSILON || std::abs(y - (float) clamped_pos(1)) > EPSILON) {
x = (float) clamped_pos(0);
y = (float) clamped_pos(1);
ConfigOptionFloat wt_x_opt(x), wt_y_opt(y);
dynamic_cast<ConfigOptionFloats*>(proj_cfg.option("wipe_tower_x"))->set_at(&wt_x_opt, plate_id, 0);
dynamic_cast<ConfigOptionFloats*>(proj_cfg.option("wipe_tower_y"))->set_at(&wt_y_opt, plate_id, 0);
}
}
// The stored position is already clamped onto the bed, by
// set_default_wipe_tower_pos_for_plate and again on every drag.
if (!current_print->is_step_done(psWipeTower) || !current_print->wipe_tower_data().wipe_tower_mesh_data) {
// update for wipe tower position
int volume_idx_wipe_tower_new = m_volumes.load_wipe_tower_preview(1000 + plate_id, x + plate_origin(0), y + plate_origin(1),
+1 -2
View File
@@ -265,8 +265,7 @@ arrangement::ArrangePolygon estimate_wipe_tower_info(int plate_index, std::set<i
int extruder_size = extruder_ids.size();
Vec3d wipe_tower_size, wipe_tower_pos;
int nozzle_nums = wxGetApp().preset_bundle->get_printer_extruder_count();
auto arrange_poly = ppl.get_plate(plate_index_valid)->estimate_wipe_tower_polygon(full_config, plate_index, wipe_tower_pos, wipe_tower_size, nozzle_nums, extruder_size);
auto arrange_poly = ppl.get_plate(plate_index_valid)->estimate_wipe_tower_polygon(full_config, plate_index, wipe_tower_pos, wipe_tower_size, extruder_size);
arrange_poly.bed_idx = plate_index;
return arrange_poly;
}
+123 -117
View File
@@ -1,5 +1,6 @@
#include <cstddef>
#include <algorithm>
#include <limits>
#include <numeric>
#include <vector>
#include <string>
@@ -20,6 +21,7 @@
#include "libslic3r/libslic3r.h"
#include "libslic3r/Polygon.hpp"
#include "libslic3r/GCode/WipeTowerEstimate.hpp"
#include "libslic3r/ClipperUtils.hpp"
#include "libslic3r/BoundingBox.hpp"
#include "libslic3r/Geometry.hpp"
@@ -1531,8 +1533,23 @@ std::vector<int> PartPlate::get_extruders(bool conside_custom_gcode) const
if (check_objects_empty_and_gcode3mf(plate_extruders)) {
return plate_extruders;
}
// if 3mf file
const DynamicPrintConfig& glb_config = wxGetApp().preset_bundle->prints.get_edited_preset().config;
return get_extruders(conside_custom_gcode, wxGetApp().preset_bundle->prints.get_edited_preset().config, wxGetApp().preset_bundle->project_config);
}
// The plate's filaments, with the global keys read from the given configs rather than the
// application's presets: the wipe tower estimate is also called under the CLI, which has no
// application object. get_extruders(bool) passes the edited presets; a full config serves both.
std::vector<int> PartPlate::get_extruders(bool conside_custom_gcode, const DynamicPrintConfig& glb_config, const DynamicPrintConfig& project_config) const
{
std::vector<int> plate_extruders;
// A plate from a sliced .gcode.3mf holds no objects, so report the filaments the G-code
// used. check_objects_empty_and_gcode3mf does this for get_extruders(bool), but reaches
// the plater, which the CLI has none of; slice_filaments_info is only filled for such a plate.
if (m_model->objects.empty()) {
for (const FilamentInfo &info : slice_filaments_info)
plate_extruders.push_back(info.id + 1);
return plate_extruders;
}
int glb_support_intf_extr = glb_config.opt_int("support_interface_filament");
int glb_support_extr = glb_config.opt_int("support_filament");
int glb_outer_wall_extr = glb_config.opt_int("outer_wall_filament_id");
@@ -1549,7 +1566,9 @@ std::vector<int> PartPlate::get_extruders(bool conside_custom_gcode) const
glb_support |= glb_config.opt_int("raft_layers") > 0;
for (int obj_idx = 0; obj_idx < m_model->objects.size(); obj_idx++) {
if (!contain_instance_totally(obj_idx, 0))
// Any instance on the plate counts, as PrintApply does: after an arrange, instance 0
// can sit on a different plate.
if (!contain_any_instance_totally(obj_idx))
continue;
ModelObject* mo = m_model->objects[obj_idx];
@@ -1662,7 +1681,7 @@ std::vector<int> PartPlate::get_extruders(bool conside_custom_gcode) const
if (conside_custom_gcode) {
//BBS
int nums_extruders = 0;
if (const ConfigOptionStrings *color_option = dynamic_cast<const ConfigOptionStrings *>(wxGetApp().preset_bundle->project_config.option("filament_colour"))) {
if (const ConfigOptionStrings *color_option = dynamic_cast<const ConfigOptionStrings *>(project_config.option("filament_colour"))) {
nums_extruders = color_option->values.size();
if (m_model->plates_custom_gcodes.find(m_plate_index) != m_model->plates_custom_gcodes.end()) {
for (auto item : m_model->plates_custom_gcodes.at(m_plate_index).gcodes) {
@@ -1681,9 +1700,8 @@ std::vector<int> PartPlate::get_extruders(bool conside_custom_gcode) const
// is never loaded into a tray, so callers (AMS mapping, filament checks) must see the
// physical filaments it resolves to instead.
{
auto& project_config = wxGetApp().preset_bundle->project_config;
auto* is_mixed_opt = project_config.option<ConfigOptionBools>("filament_is_mixed");
auto* comp_strs_opt = project_config.option<ConfigOptionStrings>("filament_mixed_components");
const auto* is_mixed_opt = project_config.option<ConfigOptionBools>("filament_is_mixed");
const auto* comp_strs_opt = project_config.option<ConfigOptionStrings>("filament_mixed_components");
if (is_mixed_opt && comp_strs_opt && has_any_mixed_filament(is_mixed_opt->values)) {
std::vector<unsigned int> ext_0based;
for (int e : plate_extruders)
@@ -2311,113 +2329,97 @@ bool PartPlate::check_compatible_of_nozzle_and_filament(const DynamicPrintConfig
return wipe_tower_size;
}*/
Vec3d PartPlate::estimate_wipe_tower_size(const DynamicPrintConfig & config, const double w, const double wipe_volume, int extruder_count, int plate_extruder_size, bool use_global_objects, bool enable_wrapping_detection) const
WipeTowerFootprint PartPlate::estimate_wipe_tower_footprint(const DynamicPrintConfig &config, int plate_extruder_size, bool use_global_objects) const
{
Vec3d wipe_tower_size;
double layer_height = 0.08f; // hard code layer height
double max_height = 0.f;
wipe_tower_size.setZero();
// The CLI calls this too, so the plate's filaments are derived from the passed config:
// get_extruders(bool) reads the same keys off wxGetApp()'s presets, which the CLI has none of.
// An explicit count is a floor: init-time and arrange estimates size an empty plate for that
// many generic filaments, the lowest ids not already on the plate.
std::vector<int> plate_extruders = get_extruders(true, config, config);
for (int id = 1; int(plate_extruders.size()) < plate_extruder_size; ++id)
if (std::find(plate_extruders.begin(), plate_extruders.end(), id) == plate_extruders.end())
plate_extruders.push_back(id);
// The wipe tower filament joins the tool ordering even when unused (Print::extruders), so
// validation counts it - but only where there is a tower to join, which is the
// has_wipe_tower() half of that guard.
const ConfigOption *wipe_tower_filament_opt = config.option("wipe_tower_filament");
const ConfigOption *enable_prime_tower_opt = config.option("enable_prime_tower");
const int wipe_tower_filament = wipe_tower_filament_opt != nullptr ? wipe_tower_filament_opt->getInt() : 0;
if (enable_prime_tower_opt != nullptr && enable_prime_tower_opt->getBool() && plate_extruders.size() > 1 && wipe_tower_filament > 0 &&
std::find(plate_extruders.begin(), plate_extruders.end(), wipe_tower_filament) == plate_extruders.end())
plate_extruders.push_back(wipe_tower_filament);
if (plate_extruders.empty())
return WipeTowerFootprint();
const ConfigOption* layer_height_opt = config.option("layer_height");
if (layer_height_opt)
layer_height = layer_height_opt->getFloat();
// empty plate
if (plate_extruder_size == 0)
{
std::vector<int> plate_extruders = get_extruders(true);
plate_extruder_size = plate_extruders.size();
}
if (plate_extruder_size == 0)
return wipe_tower_size;
for (int obj_idx = 0; obj_idx < m_model->objects.size(); obj_idx++) {
if (!use_global_objects && !contain_instance_totally(obj_idx, 0))
// Tallest object on this plate and the thinnest layer it is sliced at, resolved per object
// as PrintObject resolves them (override, else preset) and over this plate's objects only -
// seeding from the global value, or folding in an off-plate override, diverges from Print.
const ConfigOption *layer_height_opt = config.option("layer_height");
const double global_layer_height = layer_height_opt != nullptr ? layer_height_opt->getFloat() : 0.08;
double max_height = 0.;
double layer_height = std::numeric_limits<double>::max();
for (int obj_idx = 0; obj_idx < int(m_model->objects.size()); ++obj_idx) {
const ModelObject *object = m_model->objects[obj_idx];
if (!use_global_objects && !contain_any_instance_totally(obj_idx))
continue;
BoundingBoxf3 bbox = m_model->objects[obj_idx]->bounding_box_exact();
max_height = std::max(bbox.size().z(), max_height);
}
wipe_tower_size(2) = max_height;
//const DynamicPrintConfig &dconfig = wxGetApp().preset_bundle->prints.get_edited_preset().config;
auto timelapse_type = config.option<ConfigOptionEnum<TimelapseType>>("timelapse_type");
bool need_wipe_tower = (timelapse_type ? (timelapse_type->value == TimelapseType::tlSmooth) : false) | enable_wrapping_detection;
double extra_spacing = config.option("prime_tower_infill_gap")->getFloat() / 100.;
const ConfigOptionEnum<WipeTowerWallType>* use_rib_wall_opt = config.option<ConfigOptionEnum<WipeTowerWallType>>("wipe_tower_wall_type");
bool use_rib_wall = use_rib_wall_opt ? use_rib_wall_opt->value == WipeTowerWallType::wtwRib: false;
double rib_width = config.option("wipe_tower_rib_width")->getFloat();
double depth;
double filament_change_volume=0.;
{
std::vector<double> filament_change_lengths;
auto filament_change_lengths_opt = m_print->config().option<ConfigOptionFloats>("filament_change_length");
if (filament_change_lengths_opt) filament_change_lengths = filament_change_lengths_opt->values;
double length = filament_change_lengths.empty() ? 0 : *std::max_element(filament_change_lengths.begin(), filament_change_lengths.end());
double diameter = 1.75;
std::vector<double> diameters;
auto filament_diameter_opt = m_print->config().option<ConfigOptionFloats>("filament_diameter");
if (filament_diameter_opt) diameters = filament_diameter_opt->values;
diameter = diameters.empty() ? diameter : *std::max_element(diameters.begin(), diameters.end());
filament_change_volume = length * PI * diameter * diameter / 4.;
}
double volume = wipe_volume * (extruder_count == 2 ? plate_extruder_size : (plate_extruder_size - 1));
if (extruder_count == 2) volume += filament_change_volume * (int) (plate_extruder_size / 2);
// Read from the passed plate config — m_print may not have been applied yet
// (fresh plates, CLI), in which case its PrintConfig still holds defaults.
const auto *purge_opt = config.option<ConfigOptionBool>("purge_in_prime_tower");
const auto *semm_opt = config.option<ConfigOptionBool>("single_extruder_multi_material");
const bool semm_flush = purge_opt && purge_opt->value && semm_opt && semm_opt->value;
if (semm_flush) volume = WipeTower2::estimate_semm_flush_volume(config, plate_extruder_size);
if (use_rib_wall) {
depth = std::sqrt(volume / layer_height * extra_spacing);
if (need_wipe_tower || plate_extruder_size > 1) {
float min_wipe_tower_depth = WipeTower::get_limit_depth_by_height(max_height);
double volume_depth = depth;
depth = std::max((double) min_wipe_tower_depth, depth);
rib_width = std::min(rib_width, depth / 2);
depth = rib_width / std::sqrt(2) + std::max(depth + m_print->config().wipe_tower_extra_rib_length.value, volume_depth);
wipe_tower_size(0) = wipe_tower_size(1) = depth;
// Per instance, to match PrintObject::size(); the union over instances differs once
// they are rotated apart. The cached convex hull has the mesh's z extent and is cheap
// enough for every scene reload.
for (int inst_idx = 0; inst_idx < int(object->instances.size()); ++inst_idx) {
if (!use_global_objects && !contain_instance_totally(obj_idx, inst_idx))
continue;
max_height = std::max(max_height, object->instance_convex_hull_bounding_box(inst_idx, true).size().z());
}
const ConfigOption *object_layer_height = object->config.option("layer_height");
layer_height = std::min(layer_height, object_layer_height != nullptr ? object_layer_height->getFloat() : global_layer_height);
}
else {
depth = volume / (layer_height * w);
// The flush volumes already hold the spacing between wipes.
if (!semm_flush) depth *= extra_spacing;
if (need_wipe_tower || depth > EPSILON) {
float min_wipe_tower_depth = WipeTower::get_limit_depth_by_height(max_height);
depth = std::max((double)min_wipe_tower_depth, depth);
}
wipe_tower_size(0) = w;
wipe_tower_size(1) = depth;
}
if (layer_height == std::numeric_limits<double>::max())
layer_height = global_layer_height;
return wipe_tower_size;
std::vector<unsigned int> filament_ids;
for (int id : plate_extruders)
if (id > 0)
filament_ids.push_back(static_cast<unsigned int>(id - 1));
return Slic3r::estimate_wipe_tower_footprint(config, resolve_wipe_tower_type(config), filament_ids, layer_height, max_height);
}
arrangement::ArrangePolygon PartPlate::estimate_wipe_tower_polygon(const DynamicPrintConfig& config, int plate_index, Vec3d& wt_pos, Vec3d& wt_size, int extruder_count, int plate_extruder_size, bool use_global_objects) const
arrangement::ArrangePolygon PartPlate::estimate_wipe_tower_polygon(const DynamicPrintConfig& config, int plate_index, Vec3d& wt_pos, Vec3d& wt_size, int plate_extruder_size, bool use_global_objects) const
{
float x = dynamic_cast<const ConfigOptionFloats*>(config.option("wipe_tower_x"))->get_at(plate_index);
float y = dynamic_cast<const ConfigOptionFloats*>(config.option("wipe_tower_y"))->get_at(plate_index);
float w = dynamic_cast<const ConfigOptionFloat*>(config.option("prime_tower_width"))->value;
//float a = dynamic_cast<const ConfigOptionFloat*>(config.option("wipe_tower_rotation_angle"))->value;
float v = dynamic_cast<const ConfigOptionFloat*>(config.option("prime_volume"))->value;
float tower_brim_width = dynamic_cast<const ConfigOptionFloat*>(config.option("prime_tower_brim_width"))->value;
const ConfigOptionBool * wrapping_opt = dynamic_cast<const ConfigOptionBool *>(config.option("enable_wrapping_detection"));
bool enable_wrapping = (wrapping_opt != nullptr) && wrapping_opt->value;
wt_size = estimate_wipe_tower_size(config, w, v, extruder_count, plate_extruder_size, use_global_objects, enable_wrapping);
const WipeTowerFootprint footprint = estimate_wipe_tower_footprint(config, plate_extruder_size, use_global_objects);
wt_size = Vec3d(footprint.width, footprint.depth, footprint.height);
int plate_width=m_width, plate_depth=m_depth;
w = wt_size(0); // effective width; differs from prime_tower_width when the rib wall squares the tower
float w = wt_size(0); // effective width; differs from prime_tower_width when the rib wall squares the tower
float depth = wt_size(1);
float margin = WIPE_TOWER_MARGIN + tower_brim_width, wp_brim_width = 0.f;
const ConfigOption* wipe_tower_brim_width_opt = config.option("prime_tower_brim_width");
if (wipe_tower_brim_width_opt) {
wp_brim_width = wipe_tower_brim_width_opt->getFloat();
if (wp_brim_width < 0) wp_brim_width = WipeTower::get_auto_brim_by_height((float) wt_size.z());
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format("arrange wipe_tower: wp_brim_width %1%") % wp_brim_width;
}
x = std::clamp(x, margin, (float)plate_width - w - margin - wp_brim_width);
y = std::clamp(y, margin, (float)plate_depth - depth - margin - wp_brim_width);
// Resolved brim, not the raw option: "Auto" (-1) would yield a margin of 0 and let the
// 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.
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.
const float margin = WIPE_TOWER_MARGIN + wp_brim_width;
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format("arrange wipe_tower: wp_brim_width %1%") % wp_brim_width;
const float x_hi = std::max(margin, (float) plate_width - w - margin);
const float y_hi = std::max(margin, (float) plate_depth - depth - margin);
const float margin_c = (float) WIPE_TOWER_AUTO_MARGIN + wp_brim_width;
float x_lo_c = margin_c, x_hi_c = (float) plate_width - w - margin_c;
if (x_lo_c > x_hi_c) { x_lo_c = margin; x_hi_c = x_hi; }
float y_lo_c = margin_c, y_hi_c = (float) plate_depth - depth - margin_c;
if (y_lo_c > y_hi_c) { y_lo_c = margin; y_hi_c = y_hi; }
// Drag clamps reach this limit through the volume's bounding box (post-slice: the real
// mesh, a couple of mm inside this reserved estimate), so a drop can land slightly out
// of bounds — snap it onto the bound; only far-out positions get the comfort re-place.
const float tol = 5.f;
if (x < margin - tol || x > x_hi + tol) x = std::clamp(x, x_lo_c, x_hi_c);
else x = std::clamp(x, margin, x_hi);
if (y < margin - tol || y > y_hi + tol) y = std::clamp(y, y_lo_c, y_hi_c);
else y = std::clamp(y, margin, y_hi);
wt_pos(0) = x;
wt_pos(1) = y;
wt_pos(2) = 0.f;
@@ -2755,6 +2757,20 @@ bool PartPlate::contain_instance_totally(int obj_id, int instance_id) const
return result;
}
//judge whether any of the object's instances is totally included in plate or not
bool PartPlate::contain_any_instance_totally(int obj_id) const
{
if (obj_id < 0 || obj_id >= int(m_model->objects.size()))
return false;
const ModelObject *object = m_model->objects[obj_id];
for (int instance_id = 0; instance_id < int(object->instances.size()); ++instance_id)
if (contain_instance_totally(obj_id, instance_id))
return true;
return false;
}
//check whether instance is outside the plate or not
bool PartPlate::check_outside(int obj_id, int instance_id, BoundingBoxf3* bounding_box)
{
@@ -4488,26 +4504,16 @@ void PartPlateList::set_default_wipe_tower_pos_for_plate(int plate_idx, bool ini
f_volume_maps = wxGetApp().preset_bundle->get_default_nozzle_volume_types_for_filaments(filament_maps);
}
DynamicPrintConfig full_config = wxGetApp().preset_bundle->full_config(false, filament_maps, f_volume_maps);
float w = dynamic_cast<const ConfigOptionFloat *>(full_config.option("prime_tower_width"))->value;
float v = dynamic_cast<const ConfigOptionFloat *>(full_config.option("prime_volume"))->value;
bool enable_wrapping = false;
const ConfigOptionBool *wrapping_opt = dynamic_cast<const ConfigOptionBool *>(full_config.option("enable_wrapping_detection"));
if (wrapping_opt) enable_wrapping = wrapping_opt->value;
int nozzle_nums = wxGetApp().preset_bundle->get_printer_extruder_count();
Vec3d wipe_tower_size = part_plate->estimate_wipe_tower_size(full_config, w, v, nozzle_nums, init_pos ? 2 : 0, false, enable_wrapping);
WipeTowerFootprint footprint = part_plate->estimate_wipe_tower_footprint(full_config, init_pos ? 2 : 0);
if (!init_pos && (is_approx(wipe_tower_size(0), 0.0) || is_approx(wipe_tower_size(1), 0.0))) {
wipe_tower_size = part_plate->estimate_wipe_tower_size(full_config, w, v, nozzle_nums, 2, false, enable_wrapping);
if (!init_pos && (is_approx(footprint.width, 0.0) || is_approx(footprint.depth, 0.0))) {
footprint = part_plate->estimate_wipe_tower_footprint(full_config, 2);
}
Vec3d wipe_tower_size(footprint.width, footprint.depth, footprint.height);
// Compute brim-aware margin: brim extends outward from tower position
float brim_width = 0.f;
const ConfigOptionFloat *brim_opt = full_config.option<ConfigOptionFloat>("prime_tower_brim_width");
if (brim_opt) {
brim_width = brim_opt->value;
if (brim_width < 0) brim_width = WipeTower::get_auto_brim_by_height((float) wipe_tower_size.z());
}
const float margin = WIPE_TOWER_MARGIN + brim_width;
// Brim-aware margin: the brim extends outward from the tower position.
const float brim_width = float(footprint.brim_width);
const float margin = WIPE_TOWER_AUTO_MARGIN + brim_width;
// clamp wipe tower position within plate boundaries
{
+10 -2
View File
@@ -11,6 +11,7 @@
#include "libslic3r/GCode/GCodeProcessor.hpp"
#include "libslic3r/Format/bbs_3mf.hpp"
#include "libslic3r/Slicing.hpp"
#include "libslic3r/GCode/WipeTowerEstimate.hpp"
#include "libslic3r/Arrange.hpp"
#include "Plater.hpp"
#include "libslic3r/Model.hpp"
@@ -339,11 +340,16 @@ public:
Vec3d get_origin() { return m_origin; }
//Vec3d calculate_wipe_tower_size(const DynamicPrintConfig &config, const double w, const double wipe_volume, int plate_extruder_size = 0, bool use_global_objects = false) const;
Vec3d estimate_wipe_tower_size(const DynamicPrintConfig & config, const double w, const double wipe_volume, int extruder_count = 1, int plate_extruder_size = 0, bool use_global_objects = false, bool enable_wrapping_detection = false) const;
arrangement::ArrangePolygon estimate_wipe_tower_polygon(const DynamicPrintConfig & config, int plate_index, Vec3d& wt_pos, Vec3d& wt_size, int extruder_count = 1, int plate_extruder_size = 0, bool use_global_objects = false) const;
// plate_extruder_size: a floor on the filaments purged on the plate; its own are always
// counted, so 0 sizes for exactly those.
// use_global_objects skips the containment test, which the CLI needs before objects are
// assigned to plates - the layer height is then the project's thinnest, which over-reserves.
WipeTowerFootprint estimate_wipe_tower_footprint(const DynamicPrintConfig & config, int plate_extruder_size = 0, bool use_global_objects = false) const;
arrangement::ArrangePolygon estimate_wipe_tower_polygon(const DynamicPrintConfig & config, int plate_index, Vec3d& wt_pos, Vec3d& wt_size, int plate_extruder_size = 0, bool use_global_objects = false) const;
bool check_objects_empty_and_gcode3mf(std::vector<int> &result) const;
// get used filaments from config, 1 based idx
std::vector<int> get_extruders(bool conside_custom_gcode = false) const;
std::vector<int> get_extruders(bool conside_custom_gcode, const DynamicPrintConfig& glb_config, const DynamicPrintConfig& project_config) const;
std::vector<int> get_extruders_under_cli(bool conside_custom_gcode, DynamicPrintConfig& full_config) const;
std::vector<int> get_extruders_without_support(bool conside_custom_gcode = false) const;
// get used filaments from gcode result, 1 based idx
@@ -366,6 +372,8 @@ public:
bool contain_instance_totally(ModelObject* object, int instance_id) const;
//judge whether instance is totally included in plate or not
bool contain_instance_totally(int obj_id, int instance_id) const;
//judge whether any of the object's instances is totally included in plate or not
bool contain_any_instance_totally(int obj_id) const;
//judge whether the plate's origin is at the left of instance or not
bool is_left_top_of(int obj_id, int instance_id);
+3 -4
View File
@@ -1273,10 +1273,9 @@ void Selection::translate(const Vec3d &displacement, TransformationType transfor
const Polygons bed_polys{wxGetApp().plater()->get_partplate_list().get_plate(plate_idx)->get_shared_printable_polygon()};
Vec3d tower_origin = m_cache.volumes_data[i].get_volume_position();
Vec3d actual_displacement = displacement;
bool show_read_wipe_tower = wxGetApp().plater()->get_partplate_list().get_plate(plate_idx)->fff_print()->is_step_done(psWipeTower);
float brim_width = wxGetApp().preset_bundle->prints.get_edited_preset().config.opt_float("prime_tower_brim_width");
const double margin = show_read_wipe_tower ? WIPE_TOWER_MARGIN : brim_width + 0.5; // 0.5 is the line width of wipe tower
// Both preview volumes carry the brim in their bounding box, and the release
// clamp holds it WIPE_TOWER_MARGIN inside — same margin, so drops don't snap.
const double margin = WIPE_TOWER_MARGIN;
actual_displacement = (m_cache.volumes_data[i].get_instance_rotation_matrix() * m_cache.volumes_data[i].get_instance_scale_matrix() *
m_cache.volumes_data[i].get_instance_mirror_matrix())
+3
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("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;
}
+67 -2
View File
@@ -1,5 +1,7 @@
#include <catch2/catch_all.hpp>
#include <algorithm>
#include "libslic3r/Layer.hpp"
#include "libslic3r/TriangleMesh.hpp"
@@ -33,10 +35,13 @@ TriangleMesh scaled(TestMesh id, float scale)
return mesh;
}
// `extra` is applied last, so a caller can add or override any key.
void slice_with_tree_support(const TriangleMesh &mesh, Slic3r::Print &print, const char *style,
int threshold_angle = 30, int build_plate_only = 0, int raft_layers = 0)
int threshold_angle = 30, int build_plate_only = 0, int raft_layers = 0,
std::initializer_list<Slic3r::ConfigBase::SetDeserializeItem> extra = {})
{
Slic3r::Test::init_and_process_print({ mesh }, print, {
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
config.set_deserialize_strict({
{ "enable_support", 1 },
{ "support_type", "tree(auto)" },
{ "support_style", style },
@@ -45,6 +50,8 @@ void slice_with_tree_support(const TriangleMesh &mesh, Slic3r::Print &print, con
{ "raft_layers", raft_layers },
{ "layer_height", 0.2 },
});
config.set_deserialize_strict(extra);
Slic3r::Test::init_and_process_print({ mesh }, print, config);
}
Points support_points(const Slic3r::Print &print)
@@ -63,6 +70,32 @@ size_t support_point_count(const TriangleMesh &mesh, const char *style, int thre
return support_points(print).size();
}
// Index of the first differing point, or the common length when they match. An index keeps a
// failure readable; comparing the vectors themselves dumps thousands of points.
size_t first_difference(const Points &a, const Points &b)
{
const size_t common = std::min(a.size(), b.size());
for (size_t i = 0; i < common; ++i)
if (a[i] != b[i])
return i;
return common;
}
// Slice `mesh` twice and require an identical support point sequence. Point counts and total
// length are order insensitive, so the sequence is what a reordering shows up in.
void sliced_twice_matches(const TriangleMesh &mesh, int build_plate_only, const char *style = "tree_slim",
std::initializer_list<Slic3r::ConfigBase::SetDeserializeItem> extra = {})
{
Slic3r::Print first_print, second_print;
slice_with_tree_support(mesh, first_print, style, 30, build_plate_only, 0, extra);
slice_with_tree_support(mesh, second_print, style, 30, build_plate_only, 0, extra);
const Points first = support_points(first_print);
const Points second = support_points(second_print);
REQUIRE(first.size() > 1000); // without support the comparison below passes vacuously
REQUIRE(second.size() == first.size());
REQUIRE(first_difference(first, second) == first.size());
}
} // namespace
TEST_CASE("Tree support is generated for an overhang and not for a plain cube", "[TreeSupport]")
@@ -123,3 +156,35 @@ TEST_CASE("A raft is still generated under tree support", "[TreeSupport]")
// The raft goes under the object.
REQUIRE(rafted_object->layers().front()->print_z > unrafted_object->layers().front()->print_z);
}
// drop_nodes() decides the node merges and spawns the next layer's nodes in parallel. Every one of
// those decisions has to be applied in a fixed order, or the same model gives different branches on
// each slice.
TEST_CASE("Tree support toolpaths do not depend on thread scheduling", "[TreeSupport][Regression]")
{
// Scaled up so that a layer holds enough nodes for the parallel range to be split. At stock
// size it stays in one chunk and the order never varies.
SECTION("overhang") { sliced_twice_matches(scaled(TestMesh::overhang, 2.f), 0); }
SECTION("bridge with hole") { sliced_twice_matches(scaled(TestMesh::bridge_with_hole, 3.f), 0); }
// Dropping every branch that cannot reach the bed leaves the survivors dense enough that the
// neighbour merge fires in bulk.
SECTION("on the build plate") { sliced_twice_matches(scaled(TestMesh::overhang, 4.f), 1); }
// Branches resting on the model are what put nodes in a part group other than 0, which is the
// only way to reach the prune in the second pass. tree_hybrid additionally builds polygon
// nodes, so it is the only style that exercises the overhang merge.
SECTION("resting on the model") { sliced_twice_matches(two_tier_mesh(), 0); }
SECTION("hybrid on the model") { sliced_twice_matches(two_tier_mesh(), 0, "tree_hybrid"); }
}
// Prim breaks equal-distance ties by heap address. A 1 mm branch diameter puts neighbours close
// enough to tie, and an explicit line width pins max_move_dist, so the moved tie winner reaches
// the support toolpaths.
TEST_CASE("Tree support toolpaths do not depend on the MST tie order", "[TreeSupport][Regression]")
{
sliced_twice_matches(two_tier_mesh(), 0, "tree_hybrid", {
{ "tree_support_branch_diameter", 1.0 },
{ "tree_support_branch_distance", 5.0 },
{ "tree_support_branch_angle", 40 },
{ "support_line_width", 0.4 },
});
}
+147
View File
@@ -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 },
});
@@ -182,3 +184,148 @@ TEST_CASE("The wipe tower's toolchange planner flush follows the gcode flavor",
CHECK_THAT(tower, !Catch::Matchers::ContainsSubstring(unexpected));
}
}
// What Print feeds the shared estimate. The libslic3r WipeTowerEstimate cases cannot see this:
// they call the estimator directly. The estimate counts the filaments the print really uses,
// so the two-filament shape gives the outer wall the second one.
static DynamicPrintConfig tower_estimate_config(const char *wall_type, unsigned int filaments = 2)
{
// 100 mm3 per purge on a 50 mm wide tower: one purge is 100/(layer_height * 50) of depth.
return multifilament_config(filaments, {
{ "outer_wall_filament_id", filaments == 2 ? "2" : "1" },
{ "enable_prime_tower", "1" },
{ "wipe_tower_wall_type", wall_type },
{ "prime_tower_width", "50" },
{ "prime_volume", "100" },
{ "prime_tower_infill_gap", "100%" },
{ "prime_tower_brim_width", "3" },
{ "purge_in_prime_tower", "0" },
{ "single_extruder_multi_material", "0" },
{ "timelapse_type", "0" },
{ "layer_height", "0.2" },
{ "enable_wrapping_detection", "0" },
{ "raft_layers", "0" } });
}
TEST_CASE("The tower is sized for the thinnest layer any object on the plate is sliced at", "[WipeTower]")
{
// The tower has to survive its thinnest layer, so an override finer than the preset drives
// the estimate even on the second object. Two 20 mm cubes, the second at 0.1 mm.
const DynamicPrintConfig config = tower_estimate_config("rectangle");
const std::vector<std::vector<ConfigBase::SetDeserializeItem>> overrides = {
{}, { { "layer_height", "0.1" } } };
Print print;
Model model;
init_print({ cube(20), cube(20) }, print, model, config, &overrides);
// One purge at 0.1 mm: 100 / (0.1 * 50) = 20 mm, above the 20 mm-tall tower's stability
// floor. At the preset's 0.2 mm it would be half that, so the two are easy to tell apart.
const float floor_20mm = WipeTower::get_limit_depth_by_height(20.f);
REQUIRE(floor_20mm < 10.f);
CHECK_THAT(print.wipe_tower_data(2).depth, Catch::Matchers::WithinAbs(20., 1e-4));
}
TEST_CASE("Validation is given the tower's effective width, not the configured one", "[WipeTower]")
{
// A rib wall squares the tower, so its width is its depth. Validation reads this rather
// than re-deriving the rule from the wall type.
Print print;
Model model;
SECTION("a rectangle wall keeps the configured width") {
const DynamicPrintConfig config = tower_estimate_config("rectangle");
init_print({ cube(20) }, print, model, config);
const WipeTowerData &data = print.wipe_tower_data(2);
CHECK_THAT(data.width, Catch::Matchers::WithinAbs(50., 1e-4));
CHECK(data.depth < data.width);
}
SECTION("a rib wall reports the squared footprint") {
const DynamicPrintConfig config = tower_estimate_config("rib");
init_print({ cube(20) }, print, model, config);
const WipeTowerData &data = print.wipe_tower_data(2);
CHECK_THAT(data.width, Catch::Matchers::WithinAbs(data.depth, 1e-4));
CHECK(data.width > 0.f);
}
}
TEST_CASE("Generating the tower keeps its reported width current", "[WipeTower]")
{
// width is handed out after the slice, so leaving it at the estimate reports a zero-width
// tower to every post-generation consumer.
const DynamicPrintConfig config = wipe_tower_toolchange_config("marlin");
Print print;
Model model;
init_print({ cube(10) }, print, model, config);
print.apply(model, config);
REQUIRE(print.wipe_tower_data(2).width > 0.f);
print.process();
REQUIRE(print.is_step_done(psWipeTower));
const WipeTowerData &data = print.wipe_tower_data();
// A width the generator never wrote reads as zero. A rib wall squares the tower, so the
// generated width is the body square: under the configured 50 mm, and inside the depth.
CHECK(data.width > 0.f);
CHECK(data.width < 50.f);
CHECK(data.width <= data.depth + EPSILON);
}
TEST_CASE("A single-filament plate reserves a tower only when one is actually printed", "[WipeTower]")
{
// The estimate has to answer this the way Print::apply does: reporting no tower for one
// that is built collapses the validation hull to a point, and reporting one for a tower
// that is not built takes that bed area away from the arranger and draws a preview box
// over nothing.
Print print;
Model model;
SECTION("no tool change and nothing else that prints one") {
const DynamicPrintConfig config = tower_estimate_config("rib", 1);
init_print({ cube(20) }, print, model, config);
REQUIRE_FALSE(print.has_wipe_tower());
CHECK_THAT(print.wipe_tower_data(1).depth, Catch::Matchers::WithinAbs(0., 1e-6));
}
// A raft puts the tower on every layer below the object, but only where there is a tower:
// Print::apply runs normalize_fdm_2, which clears enable_prime_tower for a plate that
// purges one filament and has neither smooth timelapse nor wrapping detection on.
SECTION("a raft alone does not print one") {
DynamicPrintConfig config = tower_estimate_config("rib", 1);
config.set_deserialize_strict({ { "raft_layers", "3" } });
init_print({ cube(20) }, print, model, config);
REQUIRE_FALSE(print.config().enable_prime_tower.value);
REQUIRE_FALSE(print.has_wipe_tower());
CHECK_THAT(print.wipe_tower_data(1).depth, Catch::Matchers::WithinAbs(0., 1e-6));
}
SECTION("smooth timelapse prints one, and keeps enable_prime_tower on") {
DynamicPrintConfig config = tower_estimate_config("rib", 1);
config.set_deserialize_strict({ { "timelapse_type", "1" } });
init_print({ cube(20) }, print, model, config);
REQUIRE(print.has_wipe_tower());
CHECK(print.wipe_tower_data(1).depth > 0.f);
}
}
TEST_CASE("A tower printed without a tool change is still validated against the bed", "[WipeTower]")
{
// Wrapping detection prints a tower on a plate that purges one filament. Neither the old
// estimate (which read the wall type and smooth timelapse) nor the old containment gate (the
// filament count or smooth timelapse) knew about it, so between them that tower was never
// checked against the bed.
Print print;
Model model;
DynamicPrintConfig config = tower_estimate_config("rectangle", 1);
// Relative E without a per-layer G92 is rejected before the tower is ever looked at, and
// has_wipe_tower() wants a real exclusion polygon before it honours wrapping detection.
config.set_deserialize_strict({ { "enable_wrapping_detection", "1" },
{ "wrapping_exclude_area", "180x180,190x180,190x190,180x190" },
{ "wipe_tower_x", "500" }, { "wipe_tower_y", "500" }, { "use_relative_e_distances", "0" } });
init_print({ cube(20) }, print, model, config);
REQUIRE(print.extruders(true).size() == 1);
REQUIRE(print.has_wipe_tower());
CHECK(print.wipe_tower_data(1).depth > 0.f);
CHECK_THAT(print.validate().string, Catch::Matchers::ContainsSubstring("printable area"));
}
+3
View File
@@ -29,6 +29,7 @@ add_executable(${_TEST_NAME}_tests
test_polygon.cpp
test_mutable_polygon.cpp
test_mutable_priority_queue.cpp
test_minimum_spanning_tree.cpp
test_nozzle_volume_type.cpp
test_step.cpp
test_stl.cpp
@@ -39,6 +40,8 @@ add_executable(${_TEST_NAME}_tests
test_utils.cpp
test_timeutils.cpp
test_voronoi.cpp
test_wipe_tower_estimate.cpp
test_wipe_tower.cpp
test_optimizers.cpp
test_ordering_strategies.cpp
# test_png_io.cpp
@@ -0,0 +1,66 @@
#include <catch2/catch_all.hpp>
#include <algorithm>
#include "libslic3r/MinimumSpanningTree.hpp"
#include "libslic3r/Point.hpp"
using namespace Slic3r;
// A 5x5 lattice: at every step of Prim's algorithm several candidates sit at the same
// distance from the tree, so the tie-break decides the tree's shape.
static std::vector<Point> lattice()
{
std::vector<Point> vertices;
for (int y = 0; y < 5; ++y)
for (int x = 0; x < 5; ++x)
vertices.emplace_back(Point::new_scale(x, y));
return vertices;
}
static std::vector<Point> sorted_neighbours(const MinimumSpanningTree &mst, const Point &vertex)
{
std::vector<Point> neighbours = mst.adjacent_nodes(vertex);
std::sort(neighbours.begin(), neighbours.end());
return neighbours;
}
TEST_CASE("Minimum spanning tree connects every vertex", "[MinimumSpanningTree]")
{
const std::vector<Point> vertices = lattice();
const MinimumSpanningTree mst(vertices);
REQUIRE(mst.vertices().size() == vertices.size());
size_t adjacency_entries = 0;
for (const Point &vertex : vertices) {
const std::vector<Point> neighbours = mst.adjacent_nodes(vertex);
REQUIRE(! neighbours.empty());
adjacency_entries += neighbours.size();
}
// A tree on n vertices has n - 1 edges, each listed from both ends.
REQUIRE(adjacency_entries == 2 * (vertices.size() - 1));
}
TEST_CASE("Minimum spanning tree does not depend on the order of the non-root vertices", "[MinimumSpanningTree][Regression]")
{
const std::vector<Point> vertices = lattice();
const MinimumSpanningTree reference(vertices);
// The root stays first: Prim's tree legitimately depends on where it starts.
// Every other order of the remaining vertices must give the same tree.
std::vector<std::vector<Point>> orders;
orders.emplace_back(vertices);
std::reverse(orders.back().begin() + 1, orders.back().end());
for (size_t shift = 1; shift + 1 < vertices.size(); ++shift) {
orders.emplace_back(vertices);
std::rotate(orders.back().begin() + 1, orders.back().begin() + 1 + shift, orders.back().end());
}
for (const std::vector<Point> &order : orders) {
const MinimumSpanningTree mst(order);
for (const Point &vertex : vertices) {
INFO("vertex " << vertex.x() << "," << vertex.y());
REQUIRE(sorted_neighbours(mst, vertex) == sorted_neighbours(reference, vertex));
}
}
}
@@ -8,6 +8,8 @@
#include "libslic3r/AppConfig.hpp"
#include "libslic3r/Model.hpp"
#include "libslic3r/TriangleMesh.hpp"
#include "libslic3r/Utils.hpp"
#include "libslic3r/miniz_extension.hpp"
#include "test_utils.hpp"
@@ -5104,3 +5106,91 @@ TEST_CASE("Published 3MF denylist and mixed-key sets match the import/export con
for (const std::string &key : mixed)
CHECK(structural.count(key) == 0);
}
namespace {
// data_dir() is a process-wide global that import_presets extracts into; scope it to the test.
struct ScopedDataDir
{
std::string previous = data_dir();
explicit ScopedDataDir(const fs::path &dir) { set_data_dir(dir.string()); }
~ScopedDataDir() { set_data_dir(previous); }
};
std::string read_file(const fs::path &file)
{
std::ifstream in(file.string(), std::ios::binary);
return std::string(std::istreambuf_iterator<char>(in), std::istreambuf_iterator<char>());
}
void write_zip(const fs::path &zip_file, const std::vector<std::pair<std::string, std::string>> &entries)
{
mz_zip_archive zip;
mz_zip_zero_struct(&zip);
REQUIRE(open_zip_writer(&zip, zip_file.string()));
for (const auto &[name, content] : entries)
REQUIRE(mz_zip_writer_add_mem(&zip, name.c_str(), content.data(), content.size(), MZ_DEFAULT_COMPRESSION));
REQUIRE(mz_zip_writer_finalize_archive(&zip));
REQUIRE(close_zip_writer(&zip));
}
bool any_filename_contains(const fs::path &root, const std::string &needle)
{
for (fs::recursive_directory_iterator it(root), end; it != end; ++it)
if (it->path().filename().string().find(needle) != std::string::npos)
return true;
return false;
}
} // namespace
TEST_CASE("Config import confines zip entries, preset names and bundle ids to the preset directory", "[Preset][Bundle][Regression]")
{
ScopedTemporaryDir temp_dir;
const fs::path data_root = temp_dir.path() / "datadir";
const fs::path src_dir = temp_dir.path() / "src";
fs::create_directories(src_dir);
ScopedDataDir scoped_data_dir(data_root);
PresetBundle bundle;
AppConfig app_config;
const auto confirm = [](std::string const &) { return 1; };
const auto import = [&](const fs::path &file) {
std::vector<std::string> files{file.string()};
bundle.import_presets(files, confirm, ForwardCompatibilitySubstitutionRule::Disable, app_config);
return files;
};
const fs::path good_file = src_dir / "Good.json";
write_print_preset(bundle.prints.default_preset().config, good_file, "Good");
const std::string good_json = read_file(good_file);
// Four levels up from where import_presets writes (<datadir>/user/default/temp) is temp_dir
// itself, so anything that escapes lands where the scan below can see it.
const std::string up = "../../../../";
const std::string up_win = "..\\..\\..\\..\\";
SECTION("zip entry names with either separator are reduced to a basename") {
const fs::path zip = src_dir / "bundle.zip";
write_zip(zip, {{up + "zip-escape.json", "{}"}, {up_win + "zip-escape.json", "{}"}, {"presets/Good.json", good_json}});
import(zip);
CHECK(bundle.prints.find_preset("Good") != nullptr);
CHECK_FALSE(any_filename_contains(temp_dir.path(), "zip-escape"));
}
SECTION("a preset name that walks out of the preset directory is rejected") {
for (const std::string &name : {up + "name-escape", up_win + "name-escape"}) {
const fs::path file = src_dir / "escape.json";
write_print_preset(bundle.prints.default_preset().config, file, name);
CHECK(import(file).empty());
CHECK_FALSE(any_filename_contains(temp_dir.path(), "name-escape"));
}
}
SECTION("a bundle id that walks out of the bundle directory is rejected") {
const fs::path zip = src_dir / "bundle.zip";
write_zip(zip, {{BUNDLE_STRUCTURE_JSON_NAME, "{\"id\": \"" + up + "bundle-escape\"}"}, {"Good.json", good_json}});
CHECK(import(zip).empty());
CHECK_FALSE(any_filename_contains(temp_dir.path(), "bundle-escape"));
}
}
+93
View File
@@ -0,0 +1,93 @@
#include <catch2/catch_all.hpp>
#include <cmath>
#include "libslic3r/BoundingBox.hpp"
#include "libslic3r/ClipperUtils.hpp"
#include "libslic3r/GCode/WipeTower.hpp"
#include "libslic3r/GCode/WipeTower2.hpp"
using namespace Slic3r;
using Catch::Matchers::WithinAbs;
// A Bambu P1S project that reproduced the off-plate brim: two PLAs priming 30 and 45 mm3 in
// separate adhesiveness categories on a 35 mm tower, 0.21 mm layers, 0.4 nozzle (0.5 mm lines),
// 150 % infill gap (0.75 mm line pitch), rib width 8, 16 mm tall.
static std::vector<WipeTower::PurgeEstimate> cube_purges(int first_category = 100)
{
return {{30.f, first_category}, {45.f, 0}};
}
TEST_CASE("Cone base polygon bulges past the body box", "[WipeTower]") {
// Zero angle: plain body box.
const Polygon box = WipeTower2::cone_base_polygon(35., 20., 100., 0.);
CHECK(box.points.size() == 4);
CHECK(get_extents(box).size() == Point::new_scale(Vec2d(35., 20.)));
// A 25-degree cone on a 100 mm tower: base radius R = tan(12.5deg)*100 = 22.2 mm,
// which exceeds the body half-depth, so the footprint bulges to center +- R in y
// (support_scale keeps the x extent compressed near the body).
const Polygon base = WipeTower2::cone_base_polygon(35., 20., 100., 25.);
const BoundingBox bb = get_extents(base);
const double R = std::tan(25. / 2. * M_PI / 180.) * 100.;
CHECK_THAT(unscaled(bb.min.y()), WithinAbs(10. - R, 0.1));
CHECK_THAT(unscaled(bb.max.y()), WithinAbs(10. + R, 0.1));
// The footprint always contains the body box.
CHECK(diff(Polygons{box}, Polygons{base}).empty());
}
TEST_CASE("Type1 block-stack depth quantizes each purge to whole lines", "[WipeTower]") {
// A 0.5 mm line at 0.21 mm carries 0.0955 mm3 per mm, so across the 34 mm between the
// perimeters 30 mm3 is 10 lines and 45 mm3 is 14: 7.5 + 10.5 at the 0.75 mm pitch behind
// one perimeter width. The generated mesh of the project measured exactly this.
CHECK_THAT(WipeTower::estimate_tower_blocks_depth(cube_purges(), 35.f, 0.21f, 0.4f, 1.5f), WithinAbs(18.5f, 0.01f));
// Sharing one category, a layer can never purge into every filament (one of them starts
// the layer), so the block is sized by its worst layer and the 10-line purge drops out.
CHECK_THAT(WipeTower::estimate_tower_blocks_depth(cube_purges(0), 35.f, 0.21f, 0.4f, 1.5f), WithinAbs(11.0f, 0.01f));
CHECK_THAT(WipeTower::estimate_tower_blocks_depth({}, 35.f, 0.2f, 0.4f, 1.f), WithinAbs(0.f, 1e-6f));
// A width narrower than two perimeter widths cannot hold purge lines.
CHECK_THAT(WipeTower::estimate_tower_blocks_depth({{45.f, 0}}, 0.9f, 0.2f, 0.4f, 1.f), WithinAbs(0.f, 1e-6f));
}
TEST_CASE("A nozzle change adds its ramming lines to the block", "[WipeTower]") {
// 10 mm of 1.75 mm filament (24.05 mm3) laid as 1.0 mm nozzle-change lines at 0.2 mm
// (0.1914 mm2 each) is 125.7 mm; across the 48.5 mm available that is 3 lines of 1.0 mm.
std::vector<WipeTower::PurgeEstimate> purges{{100.f, 0}, {100.f, 0}};
const float without_change = WipeTower::estimate_tower_blocks_depth(purges, 50.f, 0.2f, 0.4f, 1.f);
purges.front().filament_change_length = 10.f;
CHECK_THAT(WipeTower::estimate_tower_blocks_depth(purges, 50.f, 0.2f, 0.4f, 1.f) - without_change, WithinAbs(3.f, 1e-4f));
}
TEST_CASE("Rib tower footprint estimate covers the generated footprint", "[WipeTower]") {
// The generated first-layer wall bbox of the project measured 29.56 mm from the sliced
// G-code; the volume-only estimate said 23.585 mm.
const float side = WipeTower::estimate_rib_tower_bbox_side(cube_purges(), 35.f, 0.21f, 0.4f, 1.5f, 8.f, 0.f, 16.f);
CHECK(side >= 29.56f);
CHECK(side <= 29.56f + 4.f); // without grossly over-reserving plate space
// Separate categories stack their blocks, so the footprint must not shrink when they differ.
CHECK(side >= WipeTower::estimate_rib_tower_bbox_side(cube_purges(0), 35.f, 0.21f, 0.4f, 1.5f, 8.f, 0.f, 16.f));
CHECK_THAT(WipeTower::estimate_rib_tower_bbox_side({}, 35.f, 0.2f, 0.4f, 1.f, 8.f, 0.f, 16.f), WithinAbs(0.f, 1e-6f));
}
TEST_CASE("Rib footprint extends the ribs, not the body, below the stability minimum", "[WipeTower]") {
// A 10 mm body under a 90 mm print: the ribs stretch to the minimum depth's diagonal, and
// the rib width is capped at half the body, so the square grows to minimum + 5 / sqrt(2).
const float min_depth = WipeTower::get_limit_depth_by_height(90.f);
REQUIRE(min_depth > 10.f);
CHECK_THAT(WipeTower::rib_footprint_side(10.f, 10.f, 8.f, 0.f, 90.f), WithinAbs(min_depth + 5.f / std::sqrt(2.f), 1e-4f));
// The extra rib length runs along the diagonal, so it shows as its projection on each axis.
const float plain = WipeTower::rib_footprint_side(30.f, 30.f, 8.f, 0.f, 5.f);
CHECK_THAT(plain, WithinAbs(30.f + 8.f / std::sqrt(2.f), 1e-4f));
CHECK_THAT(WipeTower::rib_footprint_side(30.f, 30.f, 8.f, 4.f, 5.f) - plain, WithinAbs(4.f / std::sqrt(2.f), 1e-4f));
// A negative extra length cannot pull the ribs inside the diagonal.
CHECK_THAT(WipeTower::rib_footprint_side(30.f, 30.f, 8.f, -4.f, 5.f), WithinAbs(plain, 1e-4f));
CHECK_THAT(WipeTower::rib_footprint_side(0.f, 30.f, 8.f, 0.f, 5.f), WithinAbs(0.f, 1e-6f));
}
TEST_CASE("Brim width estimate matches each generator's loop quantization", "[WipeTower]") {
// 3 mm configured, 0.4 nozzle, 0.2 first layer: 0.4571 mm spacing, 7 loops. WipeTower2
// prints and reports the 7 loops; WipeTower reports half a spacing of line width on top.
const float spacing = 0.5f - 0.2f * float(1. - M_PI_4);
CHECK_THAT(WipeTower::estimate_brim_real_width(3.f, 0.4f, 0.2f, true), WithinAbs(7.f * spacing, 1e-4f));
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));
}
@@ -0,0 +1,351 @@
#include <catch2/catch_all.hpp>
#include "libslic3r/BoundingBox.hpp"
#include "libslic3r/ClipperUtils.hpp"
#include "libslic3r/GCode/WipeTower.hpp"
#include "libslic3r/GCode/WipeTower2.hpp"
#include "libslic3r/GCode/WipeTowerEstimate.hpp"
#include "libslic3r/PrintConfig.hpp"
#include <cmath>
#include <numeric>
#include <string>
using namespace Slic3r;
using Catch::Matchers::WithinAbs;
// Rectangle wall, one nozzle, 100 mm3 prime volume on a 50 mm wide tower at 0.2 mm layers: one
// purge is 10 mm of depth. The flush matrix is off here; the shipped-default case covers it.
// Built as PresetBundle::full_config builds the GUI's: apply() creates each enum as a
// ConfigOptionEnumGeneric, where full_print_config() would clone the static defaults'
// ConfigOptionEnum<T>. The estimate has to read either.
static DynamicPrintConfig preset_shaped_defaults()
{
DynamicPrintConfig config;
config.apply(FullPrintConfig::defaults());
return config;
}
static DynamicPrintConfig make_config(const char *wall_type = "rectangle")
{
DynamicPrintConfig config = preset_shaped_defaults();
config.set_key_value("prime_tower_width", new ConfigOptionFloat(50.));
config.set_key_value("prime_volume", new ConfigOptionFloat(100.));
config.set_key_value("filament_prime_volume", new ConfigOptionFloats({100.}));
config.set_key_value("filament_adhesiveness_category", new ConfigOptionInts({0}));
config.set_key_value("prime_tower_infill_gap", new ConfigOptionPercent(100.));
config.set_key_value("wipe_tower_extra_spacing", new ConfigOptionPercent(100.));
config.set_key_value("prime_tower_brim_width", new ConfigOptionFloat(3.));
config.set_deserialize_strict("wipe_tower_wall_type", wall_type);
config.set_key_value("wipe_tower_rib_width", new ConfigOptionFloat(8.));
config.set_key_value("wipe_tower_extra_rib_length", new ConfigOptionFloat(0.));
config.set_key_value("nozzle_diameter", new ConfigOptionFloats({0.4}));
config.set_key_value("initial_layer_print_height", new ConfigOptionFloat(0.2));
config.set_deserialize_strict("timelapse_type", "0");
config.set_key_value("enable_wrapping_detection", new ConfigOptionBool(false));
config.set_key_value("raft_layers", new ConfigOptionInt(0));
config.set_key_value("purge_in_prime_tower", new ConfigOptionBool(false));
config.set_key_value("single_extruder_multi_material", new ConfigOptionBool(false));
return config;
}
static std::vector<unsigned int> filaments(size_t count)
{
std::vector<unsigned int> ids(count);
std::iota(ids.begin(), ids.end(), 0u);
return ids;
}
// The first `count` filaments on the given planner; Type2 unless a case says otherwise.
static WipeTowerFootprint estimate(const ConfigBase &config, size_t count, double layer_height, double height, WipeTowerType type = WipeTowerType::Type2)
{
return estimate_wipe_tower_footprint(config, type, filaments(count), layer_height, height);
}
// What both planners print for a 3 mm brim at 0.4 nozzle and 0.2 first layer (0.4571 mm loops).
static double printed_brim(double configured, WipeTowerType type)
{
return WipeTower::estimate_brim_real_width(float(configured), 0.4f, 0.2f, type == WipeTowerType::Type2);
}
TEST_CASE("A rectangle wall tower is sized by the purge volume", "[WipeTowerEstimate]") {
const DynamicPrintConfig config = make_config();
// Three filaments purge twice per layer; a 5 mm object keeps the stability floor at 5 mm.
const WipeTowerFootprint fp = estimate(config, 3, 0.2, 5.);
CHECK_THAT(fp.width, WithinAbs(50., 1e-9));
CHECK_THAT(fp.depth, WithinAbs(20., 1e-9));
CHECK_THAT(fp.height, WithinAbs(5., 1e-9));
CHECK_THAT(fp.brim_width, WithinAbs(printed_brim(3., WipeTowerType::Type2), 1e-6));
// Thinner layers need more depth for the same volume.
CHECK_THAT(estimate(config, 3, 0.1, 5.).depth, WithinAbs(40., 1e-9));
}
TEST_CASE("Each planner spaces its purge lines by its own option", "[WipeTowerEstimate]") {
// Type2 reads wipe_tower_extra_spacing and Type1 prime_tower_infill_gap; neither sees the
// other's key. Type2's extra flow cancels out of its depth.
DynamicPrintConfig config = make_config();
config.set_key_value("wipe_tower_extra_flow", new ConfigOptionPercent(250.));
CHECK_THAT(estimate(config, 3, 0.2, 5.).depth, WithinAbs(20., 1e-9));
config.set_key_value("wipe_tower_extra_spacing", new ConfigOptionPercent(150.));
CHECK_THAT(estimate(config, 3, 0.2, 5.).depth, WithinAbs(30., 1e-9));
const double type1_spaced = estimate(config, 3, 0.2, 5., WipeTowerType::Type1).depth;
config.set_key_value("prime_tower_infill_gap", new ConfigOptionPercent(150.));
CHECK_THAT(estimate(config, 3, 0.2, 5.).depth, WithinAbs(30., 1e-9));
// Type1 stacks whole lines behind one 0.5 mm perimeter width, so only the stack scales.
CHECK_THAT(estimate(config, 3, 0.2, 5., WipeTowerType::Type1).depth - 0.5, WithinAbs(1.5 * (type1_spaced - 0.5), 1e-6));
}
TEST_CASE("Type1 sizes the tower from each filament's own prime volume", "[WipeTowerEstimate]") {
// The Bambu P1S project of the WipeTower cases: 30 and 45 mm3 in two categories on a 35 mm
// tower at 0.21 mm, 150 % gap, is 18.5 mm of stacked blocks (11 mm sharing one category).
DynamicPrintConfig config = make_config();
config.set_key_value("prime_tower_width", new ConfigOptionFloat(35.));
config.set_key_value("prime_tower_infill_gap", new ConfigOptionPercent(150.));
config.set_key_value("initial_layer_print_height", new ConfigOptionFloat(0.21));
config.set_key_value("filament_prime_volume", new ConfigOptionFloats({30., 45.}));
config.set_key_value("filament_adhesiveness_category", new ConfigOptionInts({100, 0}));
const std::vector<WipeTower::PurgeEstimate> purges{{30.f, 100}, {45.f, 0}};
const double blocks = WipeTower::estimate_tower_blocks_depth(purges, 35.f, 0.21f, 0.4f, 1.5f);
REQUIRE_THAT(blocks, WithinAbs(18.5, 0.01));
CHECK_THAT(estimate(config, 2, 0.21, 5., WipeTowerType::Type1).depth, WithinAbs(blocks, 1e-4));
// The ids pick the volumes, so their order does not matter and a lone filament has no purge.
CHECK_THAT(estimate_wipe_tower_footprint(config, WipeTowerType::Type1, {1, 0}, 0.21, 5.).depth, WithinAbs(blocks, 1e-4));
CHECK_THAT(estimate(config, 1, 0.21, 5., WipeTowerType::Type1).depth, WithinAbs(0., 1e-9));
config.set_key_value("filament_adhesiveness_category", new ConfigOptionInts({0, 0}));
CHECK_THAT(estimate(config, 2, 0.21, 5., WipeTowerType::Type1).depth, WithinAbs(11., 0.01));
// A rib wall squares the same stack.
config.set_deserialize_strict("wipe_tower_wall_type", "rib");
const WipeTowerFootprint rib = estimate(config, 2, 0.21, 5., WipeTowerType::Type1);
CHECK_THAT(rib.width, WithinAbs(rib.depth, 1e-9));
CHECK_THAT(rib.depth, WithinAbs(WipeTower::estimate_rib_tower_bbox_side({{30.f, 0}, {45.f, 0}}, 35.f, 0.21f, 0.4f, 1.5f, 8.f, 0.f, 5.f), 1e-4));
}
TEST_CASE("A second nozzle adds the ramming of one nozzle change per layer", "[WipeTowerEstimate]") {
// Two filaments on two nozzles: the tool order crosses once per layer, and Type1 rams 10 mm
// of filament as three 1.0 mm nozzle-change lines (see the WipeTower case).
DynamicPrintConfig config = make_config();
config.set_key_value("nozzle_diameter", new ConfigOptionFloats({0.4, 0.4}));
config.set_key_value("filament_change_length", new ConfigOptionFloats({10., 10.}));
config.set_key_value("filament_diameter", new ConfigOptionFloats({1.75, 1.75}));
config.set_key_value("filament_map", new ConfigOptionInts({1, 1}));
const double same_nozzle = estimate(config, 2, 0.2, 5., WipeTowerType::Type1).depth;
config.set_key_value("filament_map", new ConfigOptionInts({1, 2}));
CHECK_THAT(estimate(config, 2, 0.2, 5., WipeTowerType::Type1).depth - same_nozzle, WithinAbs(3., 1e-4));
}
TEST_CASE("The tower is sized for the first layer when it is the thinnest", "[WipeTowerEstimate]") {
// Both planners reserve the worst layer: a 0.28 mm print with a 0.2 mm first layer needs
// the 0.2 mm depth, while a thicker first layer changes nothing.
DynamicPrintConfig config = make_config();
const double at_thinnest = estimate(config, 3, 0.2, 5.).depth;
CHECK_THAT(estimate(config, 3, 0.28, 5.).depth, WithinAbs(at_thinnest, 1e-9));
config.set_key_value("initial_layer_print_height", new ConfigOptionFloat(0.3));
CHECK(estimate(config, 3, 0.28, 5.).depth < at_thinnest);
}
TEST_CASE("Object height sets the stability floor and the auto brim", "[WipeTowerEstimate]") {
DynamicPrintConfig config = make_config();
// Two filaments purge once: 10 mm, lifted to the 20 mm floor of a 100 mm tower.
CHECK_THAT(estimate(config, 2, 0.2, 100.).depth, WithinAbs(20., 1e-9));
config.set_key_value("prime_tower_brim_width", new ConfigOptionFloat(-1.));
const double auto_brim = WipeTower::get_auto_brim_by_height(50.f);
CHECK_THAT(estimate(config, 2, 0.2, 50.).brim_width, WithinAbs(printed_brim(auto_brim, WipeTowerType::Type2), 1e-6));
CHECK_THAT(estimate(config, 2, 0.2, 50., WipeTowerType::Type1).brim_width, WithinAbs(printed_brim(auto_brim, WipeTowerType::Type1), 1e-6));
}
TEST_CASE("A single filament only gets a tower when one is printed anyway", "[WipeTowerEstimate]") {
DynamicPrintConfig config = make_config();
CHECK_THAT(estimate(config, 1, 0.2, 100.).depth, WithinAbs(0., 1e-9));
CHECK_THAT(estimate(config, 0, 0.2, 100.).width, WithinAbs(0., 1e-9));
// Wrapping detection prints a tower on the first layers whatever the filament count: the
// Type1 planner's fixed 10 mm, the stability floor otherwise.
config.set_key_value("enable_wrapping_detection", new ConfigOptionBool(true));
CHECK_THAT(estimate(config, 1, 0.2, 100.).depth, WithinAbs(20., 1e-9));
CHECK_THAT(estimate(config, 1, 0.2, 100., WipeTowerType::Type1).depth, WithinAbs(WipeTower::get_wrapping_detection_depth(), 1e-9));
config.set_key_value("enable_wrapping_detection", new ConfigOptionBool(false));
// A raft is not one of them: normalize_fdm_2 clears enable_prime_tower for a plate that
// purges one filament unless smooth timelapse or wrapping detection is on, so a raft
// alone leaves no tower to reserve for.
config.set_key_value("raft_layers", new ConfigOptionInt(3));
CHECK_THAT(estimate(config, 1, 0.2, 100.).depth, WithinAbs(0., 1e-9));
config.set_key_value("raft_layers", new ConfigOptionInt(0));
config.set_deserialize_strict("timelapse_type", "1");
// A tower printed with no tool change is exactly the planner's idle depth: there is
// nothing to purge, and WipeTower2 sizes it at the stability floor.
CHECK_THAT(estimate(config, 1, 0.2, 100.).depth, WithinAbs(20., 1e-9));
CHECK_THAT(estimate(config, 1, 0.2, 5.).depth, WithinAbs(WipeTower::get_limit_depth_by_height(5.f), 1e-9));
}
TEST_CASE("A tool change reserves a tower even with nothing to purge", "[WipeTowerEstimate]") {
// The purge volumes are configurable down to zero, but the tool changes are still printed
// on the tower and both planners still floor it - so the estimate has to floor it too.
// Type1 plans per filament and already reserves one; Type2 has only the volume to go on.
const double height = GENERATE(5., 100.);
const float floor = WipeTower::get_limit_depth_by_height(float(height));
const char *wall = GENERATE("rectangle", "rib");
DynamicPrintConfig config = make_config(wall);
config.set_key_value("prime_volume", new ConfigOptionFloat(0.));
config.set_key_value("filament_prime_volume", new ConfigOptionFloats({0.}));
CHECK(estimate(config, 3, 0.2, height, WipeTowerType::Type2).depth >= floor);
CHECK(estimate(config, 3, 0.2, height, WipeTowerType::Type1).depth >= floor);
// Still nothing for a lone filament with no other reason.
CHECK_THAT(estimate(config, 1, 0.2, height, WipeTowerType::Type2).depth, WithinAbs(0., 1e-9));
CHECK_THAT(estimate(config, 1, 0.2, height, WipeTowerType::Type1).depth, WithinAbs(0., 1e-9));
}
TEST_CASE("Both wall types agree on whether there is a tower at all", "[WipeTowerEstimate]") {
// A wall type may only change the shape of the tower, never whether one is reserved:
// reporting no tower for one that is built collapses the validation hull to a point.
const double height = GENERATE(5., 100.);
DynamicPrintConfig rect = make_config();
DynamicPrintConfig rib = make_config("rib");
// No tool change and nothing else that prints a tower - neither wall type reserves one.
CHECK_THAT(estimate(rect, 1, 0.2, height).depth, WithinAbs(0., 1e-9));
CHECK_THAT(estimate(rib, 1, 0.2, height).depth, WithinAbs(0., 1e-9));
// Not even on a dual-nozzle printer, where a lone filament still needs no purge.
rect.set_key_value("nozzle_diameter", new ConfigOptionFloats({0.4, 0.4}));
rib.set_key_value("nozzle_diameter", new ConfigOptionFloats({0.4, 0.4}));
CHECK_THAT(estimate(rect, 1, 0.2, height).depth, WithinAbs(0., 1e-9));
CHECK_THAT(estimate(rib, 1, 0.2, height).depth, WithinAbs(0., 1e-9));
// With a tool change both reserve one, and both respect the stability floor.
CHECK(estimate(rect, 2, 0.2, height).depth >= WipeTower::get_limit_depth_by_height(float(height)));
CHECK(estimate(rib, 2, 0.2, height).depth >= WipeTower::get_limit_depth_by_height(float(height)));
}
TEST_CASE("A rib wall squares the tower and caps the rib width", "[WipeTowerEstimate]") {
DynamicPrintConfig config = make_config("rib");
// sqrt(200 / 0.2) = 31.62 mm square, plus the 8 mm rib bulge along the diagonal.
const double body = std::sqrt(1000.);
WipeTowerFootprint fp = estimate(config, 3, 0.2, 5.);
CHECK_THAT(fp.depth, WithinAbs(8. / std::sqrt(2.) + body, 1e-5));
CHECK_THAT(fp.width, WithinAbs(fp.depth, 1e-9));
// The extra rib length runs along the diagonal and grows the footprint by its projection.
config.set_key_value("wipe_tower_extra_rib_length", new ConfigOptionFloat(4.));
CHECK_THAT(estimate(config, 3, 0.2, 5.).depth, WithinAbs((8. + 4.) / std::sqrt(2.) + body, 1e-5));
// A tiny tower caps the rib width at half its depth: 5 mm body, 2.5 mm rib.
config.set_key_value("wipe_tower_extra_rib_length", new ConfigOptionFloat(0.));
config.set_key_value("prime_volume", new ConfigOptionFloat(5.));
CHECK_THAT(estimate(config, 2, 0.2, 5.).depth, WithinAbs(2.5 / std::sqrt(2.) + 5., 1e-5));
}
TEST_CASE("Every wall and tower type is read the same from a preset and a static config", "[WipeTowerEstimate]") {
// The GUI, arrange and the CLI pass a DynamicPrintConfig whose enums are
// ConfigOptionEnumGeneric; Print passes a static config whose enums are ConfigOptionEnum<T>.
// Both the wall type and the planner selection are read by value, so both give the same shape.
const char *wall_type = GENERATE("rectangle", "cone", "rib");
const char *tower_type = GENERATE("type1", "type2");
DynamicPrintConfig preset = make_config(wall_type);
preset.set_deserialize_strict("wipe_tower_type", tower_type);
REQUIRE(dynamic_cast<const ConfigOptionEnumGeneric *>(preset.option("wipe_tower_wall_type")) != nullptr);
FullPrintConfig static_config;
static_config.apply(preset, true);
REQUIRE(static_config.wipe_tower_wall_type.serialize() == wall_type);
REQUIRE(static_config.wipe_tower_type.serialize() == tower_type);
const WipeTowerType type = resolve_wipe_tower_type(preset);
CHECK(type == (std::string(tower_type) == "type1" ? WipeTowerType::Type1 : WipeTowerType::Type2));
CHECK(resolve_wipe_tower_type(static_config) == type);
// Three filaments purge twice per layer on a 5 mm object.
const WipeTowerFootprint fp = estimate(preset, 3, 0.2, 5., type);
const WipeTowerFootprint from_static = estimate(static_config, 3, 0.2, 5., type);
CHECK(fp.depth > 0.);
if (std::string(wall_type) == "rib")
CHECK_THAT(fp.width, WithinAbs(fp.depth, 1e-9));
else
CHECK_THAT(fp.width, WithinAbs(50., 1e-9));
CHECK_THAT(from_static.width, WithinAbs(fp.width, 1e-9));
CHECK_THAT(from_static.depth, WithinAbs(fp.depth, 1e-9));
CHECK_THAT(from_static.brim_width, WithinAbs(fp.brim_width, 1e-9));
// Smooth timelapse is the other enum the estimate reads: a lone filament gets a tower
// through both storages too.
preset.set_deserialize_strict("timelapse_type", "1");
static_config.apply(preset, true);
CHECK(estimate(preset, 1, 0.2, 5., type).depth > 0.);
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<T> 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<const ConfigOptionEnumGeneric *>(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");
config.set_key_value("printer_model", new ConfigOptionString("Bambu Lab X1 Carbon"));
CHECK(resolve_wipe_tower_type(config) == WipeTowerType::Type1);
config.set_key_value("printer_model", new ConfigOptionString("Voron 2.4"));
CHECK(resolve_wipe_tower_type(config) == WipeTowerType::Type2);
config.erase("wipe_tower_type");
CHECK(resolve_wipe_tower_type(config) == WipeTowerType::Type2);
}
TEST_CASE("A dual nozzle purges every filament plus the filament change", "[WipeTowerEstimate]") {
DynamicPrintConfig config = make_config();
config.set_key_value("nozzle_diameter", new ConfigOptionFloats({0.4, 0.4}));
config.set_key_value("filament_change_length", new ConfigOptionFloats({10., 10.}));
config.set_key_value("filament_diameter", new ConfigOptionFloats({1.75, 1.75}));
// Two purges of 100 mm3 plus one 10 mm filament change: (200 + 10 * pi * 1.75^2 / 4) / (0.2 * 50).
const double change_volume = 10. * PI * 1.75 * 1.75 / 4.;
CHECK_THAT(estimate(config, 2, 0.2, 5.).depth, WithinAbs((200. + change_volume) / 10., 1e-9));
}
TEST_CASE("The shipped defaults size the tower from the flush matrix", "[WipeTowerEstimate]") {
// Both keys default to true, so the shipped configuration purges the flush volumes rather
// than the prime volume, with no infill gap on top - the flush volumes already hold it.
DynamicPrintConfig config = preset_shaped_defaults();
REQUIRE(config.opt_bool("purge_in_prime_tower"));
REQUIRE(config.opt_bool("single_extruder_multi_material"));
config.set_key_value("prime_tower_width", new ConfigOptionFloat(50.));
config.set_deserialize_strict("wipe_tower_wall_type", "rectangle");
config.set_key_value("nozzle_diameter", new ConfigOptionFloats({0.4}));
const double flush_volume = WipeTower2::estimate_semm_flush_volume(config, 2);
const double expected = std::max(double(WipeTower::get_limit_depth_by_height(5.f)), flush_volume / (0.2 * 50.));
CHECK_THAT(estimate(config, 2, 0.2, 5.).depth, WithinAbs(expected, 1e-6));
}
TEST_CASE("A config missing a tower key falls back to that key's default", "[WipeTowerEstimate]") {
// The signature takes any ConfigBase: an absent key must read as its declared default.
const DynamicPrintConfig full = make_config();
DynamicPrintConfig partial = full;
partial.erase("wipe_tower_extra_spacing");
REQUIRE(partial.option("wipe_tower_extra_spacing") == nullptr);
DynamicPrintConfig defaulted = full;
defaulted.set_key_value("wipe_tower_extra_spacing",
print_config_def.get("wipe_tower_extra_spacing")->default_value->clone());
CHECK_THAT(estimate(partial, 3, 0.2, 5.).depth, WithinAbs(estimate(defaulted, 3, 0.2, 5.).depth, 1e-9));
}