Merge branch 'main' into weilun/speed_dial

This commit is contained in:
SoftFever
2026-09-10 11:54:13 +08:00
committed by GitHub
62 changed files with 1845 additions and 414 deletions
+2 -2
View File
@@ -162,7 +162,7 @@ static bool stl_read(stl_file *stl, FILE *fp, int first_facet, bool first, Impor
rewind(fp); rewind(fp);
try{ try{
char solid_name[256]; char solid_name[256];
int res_solid = fscanf(fp, " solid %[^\n]", solid_name); int res_solid = fscanf(fp, " solid %255[^\n]", solid_name);
if (res_solid == 1) { if (res_solid == 1) {
char* mw_position = strstr(solid_name, "MW"); char* mw_position = strstr(solid_name, "MW");
if (mw_position != NULL) { if (mw_position != NULL) {
@@ -170,7 +170,7 @@ static bool stl_read(stl_file *stl, FILE *fp, int first_facet, bool first, Impor
char version_str[16]; char version_str[16];
char model_id_str[128]; char model_id_str[128];
char country_code_str[16]; char country_code_str[16];
int num_values = sscanf(mw_position + 3, "%s %s %s", version_str, model_id_str, country_code_str); int num_values = sscanf(mw_position + 3, "%15s %127s %15s", version_str, model_id_str, country_code_str);
if (num_values == 3) { if (num_values == 3) {
if (strcmp(version_str, "1.0") == 0) { if (strcmp(version_str, "1.0") == 0) {
model_id = model_id_str; model_id = model_id_str;
+15 -9
View File
@@ -22,6 +22,7 @@
Match regexes; each must match at least one output line Match regexes; each must match at least one output line
NotMatch regexes; none may match any output line NotMatch regexes; none may match any output line
NotExists paths that must not exist after the case runs NotExists paths that must not exist after the case runs
DateStampedZip require a bundle date from during this case's invocation
.PARAMETER Name .PARAMETER Name
Run only the cases whose name matches this regex. Headings with no Run only the cases whose name matches this regex. Headings with no
@@ -109,12 +110,6 @@ $slnDir = Join-Path $fixtures 'sln'
New-Item -ItemType Directory -Force -Path $slnDir | Out-Null New-Item -ItemType Directory -Force -Path $slnDir | Out-Null
Set-Content -Path (Join-Path $slnDir 'OrcaSlicer.sln') -Value '' -Encoding ascii Set-Content -Path (Join-Path $slnDir 'OrcaSlicer.sln') -Value '' -Encoding ascii
# The pack stamp is checked against real dates, so a locale-dependent parse
# in the script cannot pass by looking date-shaped. Yesterday is accepted too,
# so a run that crosses midnight does not flake.
$dateStamps = @((Get-Date -Format 'yyyyMMdd'), (Get-Date).AddDays(-1).ToString('yyyyMMdd'))
$stampPattern = '_(' + ($dateStamps -join '|') + ')\.zip$'
$cases = @( $cases = @(
'argument handling' 'argument handling'
@{ Name = 'no arguments prints help'; Args = @(); DryRun = $false @{ Name = 'no arguments prints help'; Args = @(); DryRun = $false
@@ -326,12 +321,12 @@ $cases = @(
Contains = @('OrcaSlicer_dep_win-x64_') Contains = @('OrcaSlicer_dep_win-x64_')
NotContains = @('-clang', '-Release') } NotContains = @('-clang', '-Release') }
@{ Name = 'the bundle is stamped with today, not a shuffled date'; Args = @('-p') @{ Name = 'the bundle is stamped with today, not a shuffled date'; Args = @('-p')
Match = @($stampPattern) } DateStampedZip = $true }
# powershell.exe is not in System32 itself, so a trimmed PATH used to # powershell.exe is not in System32 itself, so a trimmed PATH used to
# leave the stamp empty and the bundle named OrcaSlicer_dep_win-x64_.zip. # leave the stamp empty and the bundle named OrcaSlicer_dep_win-x64_.zip.
@{ Name = 'the bundle is stamped even with a bare PATH'; Args = @('-p') @{ Name = 'the bundle is stamped even with a bare PATH'; Args = @('-p')
Env = @{ PATH = 'C:\Windows\system32;C:\Windows' } Env = @{ PATH = 'C:\Windows\system32;C:\Windows' }
Match = @($stampPattern) } DateStampedZip = $true }
@{ Name = '-p packs without rebuilding'; Args = @('-p') @{ Name = '-p packs without rebuilding'; Args = @('-p')
Match = @('^\+ .*(7z\.exe a|tar\.exe -a -c -f) ') Match = @('^\+ .*(7z\.exe a|tar\.exe -a -c -f) ')
NotContains = @('cmake -S deps') } NotContains = @('cmake -S deps') }
@@ -861,7 +856,7 @@ function Invoke-BuildScript {
$knownFields = @( $knownFields = @(
'Name', 'Args', 'ExpectExit', 'DryRun', 'First', 'Env', 'Name', 'Args', 'ExpectExit', 'DryRun', 'First', 'Env',
'Contains', 'NotContains', 'Match', 'NotMatch', 'NotExists' 'Contains', 'NotContains', 'Match', 'NotMatch', 'NotExists', 'DateStampedZip'
) )
function Test-Case { function Test-Case {
@@ -876,7 +871,9 @@ function Test-Case {
$expect = 0 $expect = 0
if ($Case.ContainsKey('ExpectExit')) { $expect = $Case['ExpectExit'] } if ($Case.ContainsKey('ExpectExit')) { $expect = $Case['ExpectExit'] }
$started = Get-Date
$result = Invoke-BuildScript -Arguments $argv -Environment $Case['Env'] $result = Invoke-BuildScript -Arguments $argv -Environment $Case['Env']
$finished = Get-Date
$problems = @() $problems = @()
@@ -902,6 +899,15 @@ function Test-Case {
$problems += "no line matching /$pattern/" $problems += "no line matching /$pattern/"
} }
} }
if ($Case['DateStampedZip']) {
# Bound the accepted dates to this invocation so crossing midnight is
# valid without allowing an unrelated past or future date.
$dateStamps = @($started.ToString('yyyyMMdd'), $finished.ToString('yyyyMMdd')) | Select-Object -Unique
$pattern = '_(' + ($dateStamps -join '|') + ')\.zip$'
if (@($lines | Where-Object { $_ -match $pattern }).Count -eq 0) {
$problems += "no line matching /$pattern/"
}
}
foreach ($pattern in $Case['NotMatch']) { foreach ($pattern in $Case['NotMatch']) {
foreach ($line in @($lines | Where-Object { $_ -match $pattern })) { foreach ($line in @($lines | Where-Object { $_ -match $pattern })) {
$problems += "line matches /$pattern/: $line" $problems += "line matches /$pattern/: $line"
+54 -35
View File
@@ -1925,7 +1925,7 @@ int CLI::run(int argc, char **argv)
} }
} }
catch (std::exception& e) { catch (std::exception& e) {
boost::nowide::cerr << construct_assemble_list << ": " << e.what() << std::endl; boost::nowide::cerr << "construct_assemble_list: " << e.what() << std::endl;
record_exit_reson(outfile_dir, CLI_DATA_FILE_ERROR, 0, cli_errors[CLI_DATA_FILE_ERROR], sliced_info); record_exit_reson(outfile_dir, CLI_DATA_FILE_ERROR, 0, cli_errors[CLI_DATA_FILE_ERROR], sliced_info);
flush_and_exit(CLI_DATA_FILE_ERROR); flush_and_exit(CLI_DATA_FILE_ERROR);
} }
@@ -1975,7 +1975,7 @@ int CLI::run(int argc, char **argv)
} }
std::unique_ptr<PresetBundle> cli_preset_bundle; std::unique_ptr<PresetBundle> cli_preset_bundle;
auto ensure_cli_preset_bundle = [&cli_preset_bundle, config_substitution_rule](std::string &error) -> PresetBundle * { auto ensure_cli_preset_bundle = [&cli_preset_bundle](std::string &error) -> PresetBundle * {
if (cli_preset_bundle) if (cli_preset_bundle)
return cli_preset_bundle.get(); return cli_preset_bundle.get();
try { try {
@@ -2002,7 +2002,7 @@ int CLI::run(int argc, char **argv)
} }
}; };
auto resolve_preset = [&ensure_cli_preset_bundle, config_substitution_rule](const std::string &file, DynamicPrintConfig &config, auto resolve_preset = [&ensure_cli_preset_bundle](const std::string &file, DynamicPrintConfig &config,
std::string &config_type, const std::string &config_from, std::string &config_type, const std::string &config_from,
bool probe_type, std::string &error) { bool probe_type, std::string &error) {
const auto *inherits = config.option<ConfigOptionString>(BBL_JSON_KEY_INHERITS); const auto *inherits = config.option<ConfigOptionString>(BBL_JSON_KEY_INHERITS);
@@ -2046,7 +2046,7 @@ int CLI::run(int argc, char **argv)
error, allow_source_manifest); error, allow_source_manifest);
}; };
auto load_config_file = [config_substitution_rule, &resolve_preset](const std::string& file, DynamicPrintConfig& config, std::string& config_type, auto load_config_file = [&resolve_preset](const std::string& file, DynamicPrintConfig& config, std::string& config_type,
std::string& config_name, std::string& filament_id, std::string& config_from) { std::string& config_name, std::string& filament_id, std::string& config_from) {
if (! boost::filesystem::exists(file)) { if (! boost::filesystem::exists(file)) {
boost::nowide::cerr << __FUNCTION__<< ": can not find setting file: " << file << std::endl; boost::nowide::cerr << __FUNCTION__<< ": can not find setting file: " << file << std::endl;
@@ -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(); 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%}") 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(); %(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_x = wipe_x_option->get_at(plate_index);
plate_obj_size_info.wipe_y = wipe_y_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); // Body and brim from one estimate: resolving an auto (-1) brim against a different
plate_obj_size_info.wipe_width = width_option->value; // 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); plate_obj_size_info.wipe_width = footprint.width;
float brim_width = brim_width_option->value; plate_obj_size_info.wipe_depth = footprint.depth;
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);
Vec3d origin = plate->get_origin(); 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); Vec3d start(origin(0) + plate_obj_size_info.wipe_x - brim_width, origin(1) + plate_obj_size_info.wipe_y, 0.f);
@@ -4835,13 +4826,16 @@ int CLI::run(int argc, char **argv)
} }
} }
if (!arrange_cfg.is_seq_print && (assemble_plate.filaments_count > 1)||(enable_wrapping_detect && !current_wrapping_exclude_area.empty())) if ((!arrange_cfg.is_seq_print && (assemble_plate.filaments_count > 1))||(enable_wrapping_detect && !current_wrapping_exclude_area.empty()))
{ {
//prepare the wipe tower //prepare the wipe tower
int plate_count = partplate_list.get_plate_count(); int plate_count = partplate_list.get_plate_count();
auto printer_structure_opt = m_print_config.option<ConfigOptionEnum<PrinterStructure>>("printer_structure"); 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; const float tower_margin = WIPE_TOWER_MARGIN + tower_brim_width;
// set the default position, the same with print config(left top) // 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); wipe_y_option->set_at(&wt_y_opt, i, 0);
Vec3d wipe_tower_size, wipe_tower_pos; 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 //update the new wp position
wt_x_opt.value = wipe_tower_pos(0); 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(); int extruder_size = used_filament_set.size();
auto printer_structure_opt = m_print_config.option<ConfigOptionEnum<PrinterStructure>>("printer_structure"); 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; const float tower_margin = WIPE_TOWER_MARGIN + tower_brim_width;
// set the default position, the same with print config(left top) // set the default position, the same with print config(left top)
float x = WIPE_TOWER_DEFAULT_X_POS; 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; 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 //update the new wp position
if (bedid < plate_count) { if (bedid < plate_count) {
@@ -5276,22 +5273,16 @@ int CLI::run(int argc, char **argv)
//float depth = v * (filaments_cnt - 1) / (layer_height * w); //float depth = v * (filaments_cnt - 1) / (layer_height * w);
const ConfigOptionBool *wrapping_detection = m_print_config.option<ConfigOptionBool>("enable_wrapping_detection"); const WipeTowerFootprint footprint = cur_plate->estimate_wipe_tower_footprint(m_print_config, filaments_cnt);
bool enable_wrapping = (wrapping_detection != nullptr) && wrapping_detection->value; Vec3d wipe_tower_size(footprint.width, footprint.depth, footprint.height);
Vec3d wipe_tower_size = cur_plate->estimate_wipe_tower_size(m_print_config, w, v, new_extruder_count, filaments_cnt, false, enable_wrapping);
Vec3d plate_origin = cur_plate->get_origin(); Vec3d plate_origin = cur_plate->get_origin();
int plate_width, plate_depth; int plate_width, plate_depth;
double plate_height; double plate_height;
partplate_list.get_plate_size(plate_width, plate_depth, plate_height); partplate_list.get_plate_size(plate_width, plate_depth, plate_height);
float depth = wipe_tower_size(1); float depth = wipe_tower_size(1);
float margin = 15.f, wp_brim_width = 0.f; // Brim already resolved against the height the body was sized from.
ConfigOption *wipe_tower_brim_width_opt = m_print_config.option("prime_tower_brim_width"); float margin = 15.f, wp_brim_width = float(footprint.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; BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format("arrange wipe_tower: wp_brim_width %1%")%wp_brim_width;
}
w = wipe_tower_size(0); 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%") 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; //Print fff_print;
std::vector<size_t> plate_triangle_counts(partplate_list.get_plate_count(), 0); 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) while(!finished)
{ {
//BBS: slice every partplate one by one //BBS: slice every partplate one by one
+1 -1
View File
@@ -297,7 +297,7 @@ int wmain(int argc, wchar_t **argv)
// printf("Loading Slic3r library: %S\n", path_to_slic3r); // printf("Loading Slic3r library: %S\n", path_to_slic3r);
HINSTANCE hInstance_Slic3r = LoadLibraryExW(path_to_slic3r, nullptr, 0); HINSTANCE hInstance_Slic3r = LoadLibraryExW(path_to_slic3r, nullptr, 0);
if (hInstance_Slic3r == nullptr) { if (hInstance_Slic3r == nullptr) {
printf("OrcaSlicer.dll was not loaded, error=%d\n", GetLastError()); printf("OrcaSlicer.dll was not loaded, error=%lu\n", GetLastError());
return -1; return -1;
} }
+35 -1
View File
@@ -8,7 +8,11 @@
#define NANOSVGRAST_IMPLEMENTATION #define NANOSVGRAST_IMPLEMENTATION
#include "nanosvg/nanosvgrast.h" #include "nanosvg/nanosvgrast.h"
#include "libslic3r/BoundingBox.hpp"
#include "libslic3r/GCode.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/Preset.hpp"
#include "libslic3r/Config.hpp" #include "libslic3r/Config.hpp"
#include "libslic3r/PresetBundle.hpp" #include "libslic3r/PresetBundle.hpp"
@@ -116,15 +120,45 @@ Vec2d printable_area_center(const DynamicPrintConfig &cfg)
return 0.5 * (lo + hi); 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 // 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 // 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 // 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 // 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 // topology, so one model covers both. An undefined placeholder in any shipped custom g-code throws
// Slic3r::PlaceholderParserError from export. // 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); const Vec2d center = printable_area_center(cfg);
place_wipe_tower(cfg, center);
TriangleMesh m = make_cube(10, 10, 10); TriangleMesh m = make_cube(10, 10, 10);
m.translate(float(center.x() - 5.), float(center.y() - 5.), 0.f); m.translate(float(center.x() - 5.), float(center.y() - 5.), 0.f);
+1 -1
View File
@@ -364,7 +364,7 @@ void CStackWalker::GetModuleInformation(LPMODULE_INFO pmi)
if (dwInfoSize > 0) if (dwInfoSize > 0)
{ {
LPVOID lpData = new byte[dwInfoSize]; byte *lpData = new byte[dwInfoSize];
ZeroMemory(lpData, dwInfoSize * sizeof(byte)); ZeroMemory(lpData, dwInfoSize * sizeof(byte));
if (GetFileVersionInfo(pmi->szModulePath, dwHandle, dwInfoSize, lpData) > 0 ) if (GetFileVersionInfo(pmi->szModulePath, dwHandle, dwInfoSize, lpData) > 0 )
+1 -1
View File
@@ -49,7 +49,7 @@ SplittedLine split_line(const PathType& path, const ExPolygons& clip, bool close
// Convert the input path into an open ZPath // Convert the input path into an open ZPath
ClipperZUtils::ZPath p; ClipperZUtils::ZPath p;
p.reserve(path.size() + closed ? 1 : 0); p.reserve(path.size() + (closed ? 1 : 0));
ClipperLib_Z::cInt z = 0; ClipperLib_Z::cInt z = 0;
for (const auto& point : path) { for (const auto& point : path) {
p.emplace_back(point.x(), point.y(), z); p.emplace_back(point.x(), point.y(), z);
+2
View File
@@ -272,6 +272,8 @@ set(lisbslic3r_sources
GCode/WipeTower2.hpp GCode/WipeTower2.hpp
GCode/WipeTower.cpp GCode/WipeTower.cpp
GCode/WipeTower.hpp GCode/WipeTower.hpp
GCode/WipeTowerEstimate.cpp
GCode/WipeTowerEstimate.hpp
GCodeWriter.cpp GCodeWriter.cpp
GCodeWriter.hpp GCodeWriter.hpp
Geometry/ArcWelder.hpp Geometry/ArcWelder.hpp
+11 -8
View File
@@ -3090,10 +3090,11 @@ bool FillRectilinear::fill_surface_trapezoidal(
case 0: // Grid / Trapezoidal case 0: // Grid / Trapezoidal
{ {
// Generate a non-crossing trapezoidal pattern to avoid overextrusion at intersections when `multiline > 1`. // Generate a non-crossing trapezoidal pattern to avoid overextrusion at intersections when `multiline > 1`.
// P2--P3 /*
// / \ * P2--P3
// P0_P1/ \P4_ * / \
// * P0_P1/ \P4_
*/
// P0xP1x=P4xP0x=d1/2 // P0xP1x=P4xP0x=d1/2
// P2xP3x=d1 // P2xP3x=d1
// P1yP2y=P2yP3y=d2 // P1yP2y=P2yP3y=d2
@@ -3171,10 +3172,12 @@ bool FillRectilinear::fill_surface_trapezoidal(
case 1: // Triangular case 1: // Triangular
{ {
// Generate a non-crossing trapezoidal pattern with a base line below. // Generate a non-crossing trapezoidal pattern with a base line below.
// P1-P2 /*
// / \ * P1-P2
// P0/ \P3_P4 * / \
// ---------------- * P0/ \P3_P4
* ----------------
*/
// P1xP2x=P3xP4x=d2 // P1xP2x=P3xP4x=d2
// P0yP1y=P2yP3y=h-2d1 // P0yP1y=P2yP3y=h-2d1
// //
-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 // VERSION NUMBERS
// 0 : .3mf, files saved by older slic3r or other applications. No version definition in them. // 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. // 1 : Introduction of 3mf versioning. No other change in data saved into 3mf files.
+7 -4
View File
@@ -1468,9 +1468,11 @@ void GCodeProcessor::run_post_process()
// Append a per-filament usage block at a filament change. // Append a per-filament usage block at a filament change.
auto handle_filament_change = [&](int filament_id, int cur_line_id, int nozzle_id) { auto handle_filament_change = [&](int filament_id, int cur_line_id, int nozzle_id) {
// skip filament changes emitted inside the machine start / end gcode // Skip filament changes emitted inside the machine start / end gcode. One forward pass assigns
if (m_machine_start_gcode_end_line_id == (unsigned int) (-1) && (unsigned int) (cur_line_id) < m_machine_start_gcode_end_line_id || // the tag ids and tests them in the same loop, so inside the start gcode the end tag is unseen
m_machine_end_gcode_start_line_id != (unsigned int) (-1) && (unsigned int) (cur_line_id) > m_machine_end_gcode_start_line_id) // and the id still holds the sentinel. That is why the first clause tests == and the second !=.
if ((m_machine_start_gcode_end_line_id == (unsigned int) (-1) && (unsigned int) (cur_line_id) < m_machine_start_gcode_end_line_id) ||
(m_machine_end_gcode_start_line_id != (unsigned int) (-1) && (unsigned int) (cur_line_id) > m_machine_end_gcode_start_line_id))
return; return;
if (!m_filament_blocks.empty()) if (!m_filament_blocks.empty())
m_filament_blocks.back().upper_gcode_id = cur_line_id; m_filament_blocks.back().upper_gcode_id = cur_line_id;
@@ -2777,7 +2779,7 @@ bool GCodeProcessor::check_multi_extruder_gcode_valid(const int
std::map<int, std::map<int, GCodePosInfo>> gcode_path_pos; // object_id, filament_id, pos std::map<int, std::map<int, GCodePosInfo>> gcode_path_pos; // object_id, filament_id, pos
for (const GCodeProcessorResult::MoveVertex &move : m_result.moves) { for (const GCodeProcessorResult::MoveVertex &move : m_result.moves) {
// sometimes, the start line extrude was outside the edge of plate a little, this is allowed, so do not include into the gcode_path_pos // sometimes, the start line extrude was outside the edge of plate a little, this is allowed, so do not include into the gcode_path_pos
if (move.type == EMoveType::Extrude /* && move.extrusion_role != ExtrusionRole::erFlush || move.type == EMoveType::Travel*/) if (move.type == EMoveType::Extrude /* && move.extrusion_role != ExtrusionRole::erFlush || move.type == EMoveType::Travel*/) {
if (move.extrusion_role == ExtrusionRole::erCustom) { if (move.extrusion_role == ExtrusionRole::erCustom) {
/*if (move.is_arc_move_with_interpolation_points()) { /*if (move.is_arc_move_with_interpolation_points()) {
for (int i = 0; i < move.interpolation_points.size(); i++) { for (int i = 0; i < move.interpolation_points.size(); i++) {
@@ -2800,6 +2802,7 @@ bool GCodeProcessor::check_multi_extruder_gcode_valid(const int
move.print_z); move.print_z);
} }
} }
}
bool valid = true; bool valid = true;
Point plate_offset = Point(scale_(m_x_offset), scale_(m_y_offset)); Point plate_offset = Point(scale_(m_x_offset), scale_(m_y_offset));
+1 -1
View File
@@ -3137,7 +3137,7 @@ void ToolOrdering::assign_custom_gcodes(const Print &print)
// Skip all custom G-codes above this layer and skip all extruder switches. // Skip all custom G-codes above this layer and skip all extruder switches.
for (; custom_gcode_it != custom_gcode_per_print_z.gcodes.rend() && ( for (; custom_gcode_it != custom_gcode_per_print_z.gcodes.rend() && (
(print_z_above > lt.print_z && custom_gcode_it->print_z > 0.5 * (lt.print_z + print_z_above)) (print_z_above > lt.print_z && custom_gcode_it->print_z > 0.5 * (lt.print_z + print_z_above))
|| custom_gcode_it->type == CustomGCode::ToolChange); ++ custom_gcode_it); || custom_gcode_it->type == CustomGCode::ToolChange); ++ custom_gcode_it) {}
print_z_above = lt.print_z; print_z_above = lt.print_z;
if (custom_gcode_it == custom_gcode_per_print_z.gcodes.rend()) if (custom_gcode_it == custom_gcode_per_print_z.gcodes.rend())
// Custom G-codes were processed. // Custom G-codes were processed.
+89 -5
View File
@@ -1630,6 +1630,94 @@ float WipeTower::get_auto_brim_by_height(float max_height) {
return 8.f; 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) Vec2f WipeTower::move_box_inside_polygon(const BoundingBox &box, const Polygons &polygons, coord_t offset)
{ {
if (polygons.empty()) return Vec2f{0.f, 0.f}; if (polygons.empty()) return Vec2f{0.f, 0.f};
@@ -4883,14 +4971,10 @@ void WipeTower::generate_new(std::vector<std::vector<WipeTower::ToolChangeResult
} }
} }
if (!has_inserted) { if (!has_inserted && !finish_block_tcr.gcode.empty())
if (finish_block_tcr.gcode.empty())
finish_block_tcr = finish_block_tcr;
else
finish_layer_tcr = merge_tcr(finish_layer_tcr, finish_block_tcr); finish_layer_tcr = merge_tcr(finish_layer_tcr, finish_block_tcr);
} }
} }
}
// record the contact layers of different categories // record the contact layers of different categories
if (layer_result.empty()) { if (layer_result.empty()) {
// there is nothing to merge finish_layer with // there is nothing to merge finish_layer with
+27
View File
@@ -42,9 +42,36 @@ public:
static const std::map<float, float> min_depth_per_height; static const std::map<float, float> min_depth_per_height;
static float get_limit_depth_by_height(float max_height); static float get_limit_depth_by_height(float max_height);
static float get_auto_brim_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_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 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); 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 // 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 // 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 // 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); 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. // 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 // 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. // 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. // in WipeTowerIntegration::append_tcr2 does not strip it.
static const std::string wait_for_temp_tag() { return ";_WAIT_FOR_TEMP_ON_WIPE_TOWER"; } 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); 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); 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, // 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. // 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
+1 -1
View File
@@ -57,7 +57,7 @@
#define HAS_INTRINSIC_128_TYPE #define HAS_INTRINSIC_128_TYPE
#endif #endif
#if defined(_MSC_VER) && defined(_WIN64) #if defined(_MSC_VER) && defined(_M_X64)
#include <intrin.h> #include <intrin.h>
#pragma intrinsic(_mul128) #pragma intrinsic(_mul128)
#endif #endif
+5
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)). //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. //However that requires an implementation of a heap that supports the decreaseKey operation, which is not in the std library.
//TODO: Implement this? //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>; using MapValue = std::pair<const Point*, coordf_t>;
const auto closest = std::min_element(smallest_distance.begin(), smallest_distance.end(), const auto closest = std::min_element(smallest_distance.begin(), smallest_distance.end(),
[](const MapValue& a, const MapValue& b) { [](const MapValue& a, const MapValue& b) {
if (a.second != b.second)
return 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. //Add this point to the graph and remove it from the candidates.
+4
View File
@@ -170,6 +170,10 @@ public:
this->m_check_sum = rhs.check_sum(); this->m_check_sum = rhs.check_sum();
this->m_connectors_cnt = rhs.connectors_cnt(); this->m_connectors_cnt = rhs.connectors_cnt();
} }
// A user-declared copy assignment or destructor deprecates the implicitly generated
// copy constructor, and this class has both, so declare it rather than rely on it.
CutObjectBase(const CutObjectBase &) = default;
CutObjectBase &operator=(const CutObjectBase &other) CutObjectBase &operator=(const CutObjectBase &other)
{ {
this->copy(other); this->copy(other);
+15 -1
View File
@@ -1614,6 +1614,12 @@ PresetsConfigSubstitutions PresetBundle::import_presets(std::vector<std::string>
metadata.id = to_string(uuid); metadata.id = to_string(uuid);
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " bundle_id was empty, so generating a UUID: " << metadata.id; 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 // Build bundle directory path based on whether bundle_structure.json was present
fs::path bundle_base_dir; fs::path bundle_base_dir;
@@ -1636,11 +1642,15 @@ PresetsConfigSubstitutions PresetBundle::import_presets(std::vector<std::string>
if (status) { if (status) {
std::string file_name = file_stat.m_filename; std::string file_name = file_stat.m_filename;
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " From zip file: " << file << ". Read 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) { if (std::string::npos != index) {
file_name = file_name.substr(index + 1); file_name = file_name.substr(index + 1);
} }
if (BUNDLE_STRUCTURE_JSON_NAME == file_name) continue; 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 // create target file path
std::string target_file_path = boost::filesystem::path(temp_folder / file_name).make_preferred().string(); std::string target_file_path = boost::filesystem::path(temp_folder / file_name).make_preferred().string();
@@ -1729,6 +1739,10 @@ bool PresetBundle::import_json_presets(PresetsConfigSubstitutions & s
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << " Preset type is unknown, not loading: " << name; BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << " Preset type is unknown, not loading: " << name;
return false; 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 PresetOrigin load_origin = detect_origin_from_path(boost::filesystem::path(bundle_dir));
const std::string preset_name = get_preset_canonical_name(name, load_origin); const std::string preset_name = get_preset_canonical_name(name, load_origin);
+99 -93
View File
@@ -20,6 +20,7 @@
#include "GCode.hpp" #include "GCode.hpp"
#include "GCode/WipeTower.hpp" #include "GCode/WipeTower.hpp"
#include "GCode/WipeTower2.hpp" #include "GCode/WipeTower2.hpp"
#include "GCode/WipeTowerEstimate.hpp"
#include "Utils.hpp" #include "Utils.hpp"
#include "PrintConfig.hpp" #include "PrintConfig.hpp"
#include "MaterialType.hpp" #include "MaterialType.hpp"
@@ -1031,20 +1032,21 @@ static StringObjectException layered_print_cleareance_valid(const Print &print,
//BBS: add the wipe tower check logic //BBS: add the wipe tower check logic
const PrintConfig & config = print.config(); 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(); int plate_index = print.get_plate_index();
const Vec3d plate_origin = print.get_plate_origin(); const Vec3d plate_origin = print.get_plate_origin();
float x = config.wipe_tower_x.get_at(plate_index) + plate_origin(0); 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 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 a = config.wipe_tower_rotation_angle.value;
//float v = config.wiping_volume.value; //float v = config.wiping_volume.value;
float depth = print.wipe_tower_data(filaments_count).depth; // The estimate resolves the effective width (a rib wall squares the tower).
//float brim_width = print.wipe_tower_data(filaments_count).brim_width; const WipeTowerData &wipe_tower_estimate = print.wipe_tower_data(filaments_count);
float width = wipe_tower_estimate.width;
if (config.wipe_tower_wall_type.value == WipeTowerWallType::wtwRib) float depth = wipe_tower_estimate.depth;
width = depth; float brim_width = wipe_tower_estimate.brim_width;
Polygons convex_hulls_temp; Polygons convex_hulls_temp;
if (print.has_wipe_tower()) { 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); 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 (!intersection(convex_hulls_other, convex_hulls_temp).empty()) {
if (warning) { if (warning) {
warning->string += L("Prime Tower") + L(" is too close to others, and collisions may be caused.\n"); warning->string += L("Prime Tower") + L(" is too close to others, and collisions may be caused.\n");
} }
} }
if (!intersection(exclude_polys, convex_hulls_temp).empty()) { if (!intersection(exclude_polys, tower_polys_checked).empty()) {
/*if (warning) {
warning->string += L("Prime Tower is too close to exclusion area, there may be collisions when printing.\n");
}*/
return {L("Prime Tower") + L(" is too close to an exclusion area, and collisions will be caused.\n")}; return {L("Prime Tower") + L(" is too close to an exclusion area, and collisions will be caused.\n")};
} }
if (print_config.enable_wrapping_detection.value && !intersection({wrapping_poly}, convex_hulls_temp).empty()) { if (print_config.enable_wrapping_detection.value && !intersection({wrapping_poly}, tower_polys_checked).empty()) {
return {L("Prime Tower") + L(" is too close to clumping detection area, and collisions will be caused.\n")}; return {L("Prime Tower") + L(" is too close to clumping detection area, and collisions will be caused.\n")};
} }
// Skip the containment check for towers that will never be printed (single-filament if (warning && !intersection(exclude_polys, tower_polys_estimated).empty()) {
// prints without smooth timelapse keep the config's tower position but emit nothing). warning->string += L("Prime Tower") + L(" is too close to exclusion area, there may be collisions when printing.") + "\n";
// 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. if (warning && print_config.enable_wrapping_detection.value && !intersection({wrapping_poly}, tower_polys_estimated).empty()) {
// Post-generation the mesh bottom already includes the real brim, so the exact warning->string += L("Prime Tower") + L(" is too close to clumping detection area, there may be collisions when printing.") + "\n";
// footprint is tested. }
if (filaments_count > 1 || print.enable_timelapse_print()) { // No gate on "is there a tower": one that is not printed estimates to zero, so the hulls
// The shared printable polygon is plate-local, while the tower polygons above are // are degenerate and every check passes. Re-deriving it here missed the wrapping-detection
// already shifted by the plate origin. // tower on a single-filament plate.
Polygons printable_polys = print.get_extruder_shared_printable_polygon(); Polygons printable_polys = print.get_extruder_shared_printable_polygon();
const Point plate_shift(scale_(plate_origin.x()), scale_(plate_origin.y())); const Point plate_shift(scale_(plate_origin.x()), scale_(plate_origin.y()));
for (Polygon &p : printable_polys) for (Polygon &p : printable_polys)
p.translate(plate_shift); p.translate(plate_shift);
if (!diff(convex_hulls_temp, printable_polys).empty()) 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")}; return {L("Prime Tower") + L(" is partially outside the printable area, and it cannot be printed.\n")};
} if (warning && !diff(tower_polys_estimated, printable_polys).empty())
warning->string += L("Prime Tower") + L(" is partially outside the printable area, and it cannot be printed.\n");
return {}; return {};
} }
@@ -3997,74 +4017,25 @@ bool Print::has_wipe_tower() const
const WipeTowerData &Print::wipe_tower_data(size_t filaments_cnt) 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. // Until the tower is generated, size it with the estimate the GUI/CLI placement uses, so
double max_height = 0; // validation cannot reject a position the clamp just accepted.
for (size_t obj_idx = 0; obj_idx < m_objects.size(); obj_idx++) { if (is_step_done(psWipeTower) || filaments_cnt == 0)
double object_z = (double) m_objects[obj_idx]->size().z(); return m_wipe_tower_data;
max_height = std::max(unscale_(object_z), max_height);
}
if (max_height < EPSILON) return m_wipe_tower_data;
double layer_height = 0.08f; // hard code layer height double max_height = 0.;
layer_height = m_objects.front()->config().layer_height.value; double layer_height = std::numeric_limits<double>::max();
for (const PrintObject *object : m_objects) {
auto timelapse_type = config().option<ConfigOptionEnum<TimelapseType>>("timelapse_type"); max_height = std::max(max_height, unscale_(double(object->size().z())));
bool need_wipe_tower = (timelapse_type ? (timelapse_type->value == TimelapseType::tlSmooth) : false) | (m_config.wipe_tower_wall_type.value == WipeTowerWallType::wtwRib); layer_height = std::min(layer_height, object->config().layer_height.value);
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 (max_height < EPSILON)
return m_wipe_tower_data;
const WipeTowerFootprint footprint = estimate_wipe_tower_footprint(m_config, this->wipe_tower_type(), this->extruders(true), layer_height, max_height);
if (! is_step_done(psWipeTower) && filaments_cnt !=0) { WipeTowerData &data = const_cast<Print *>(this)->m_wipe_tower_data;
double wipe_volume = m_config.prime_volume; data.depth = float(footprint.depth);
int filament_depth_count = m_config.nozzle_diameter.values.size() == 2 ? filaments_cnt : filaments_cnt - 1; data.width = float(footprint.width);
if (filaments_cnt == 1 && enable_timelapse_print()) filament_depth_count = 1; data.brim_width = float(footprint.brim_width);
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);
}
return m_wipe_tower_data; 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()); 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); wipe_tower.generate_new(m_wipe_tower_data.tool_changes);
m_wipe_tower_data.depth = wipe_tower.get_depth(); 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.brim_width = wipe_tower.get_brim_width();
m_wipe_tower_data.bbx = wipe_tower.get_bbx(); m_wipe_tower_data.bbx = wipe_tower.get_bbx();
m_wipe_tower_data.rib_offset = wipe_tower.get_rib_offset(); 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()); 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); wipe_tower.generate(m_wipe_tower_data.tool_changes);
m_wipe_tower_data.depth = wipe_tower.get_depth(); 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.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.brim_width = wipe_tower.get_brim_width();
m_wipe_tower_data.height = wipe_tower.get_wipe_tower_height(); 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(), wipe_tower.get_wipe_tower_height(), wipe_tower.get_brim_width(),
config().wipe_tower_wall_type.value == WipeTowerWallType::wtwRib, config().wipe_tower_wall_type.value == WipeTowerWallType::wtwRib,
wipe_tower.get_rib_width(), wipe_tower.get_rib_length(), wipe_tower.get_rib_width(), wipe_tower.get_rib_length(),
config().wipe_tower_fillet_wall.value); config().wipe_tower_fillet_wall.value,
config().wipe_tower_wall_type.value == WipeTowerWallType::wtwCone ?
(float) config().wipe_tower_cone_angle.value : 0.f);
const Vec3d origin = Vec3d::Zero(); const Vec3d origin = Vec3d::Zero();
// FakeWipeTower::pos is a bed-frame translation applied after rotation // FakeWipeTower::pos is a bed-frame translation applied after rotation
// (getFakeExtrusionPathsFromWipeTower2 rotates about the local origin), so the // (getFakeExtrusionPathsFromWipeTower2 rotates about the local origin), so the
@@ -4451,6 +4426,28 @@ void Print::_make_wipe_tower()
config().wipe_tower_rotation_angle, config().wipe_tower_cone_angle, config().wipe_tower_rotation_angle, config().wipe_tower_cone_angle,
{scale_(origin.x()), scale_(origin.y())}); {scale_(origin.x()), scale_(origin.y())});
} }
// The clamps and checks above work from estimates; re-test the exact generated footprint
// so an off-plate tower fails with a clear error instead of exporting unprintable G-code
// (validate() only sees the mesh on its next run).
if (m_wipe_tower_data.wipe_tower_mesh_data) {
Polygon footprint = m_wipe_tower_data.wipe_tower_mesh_data->bottom; // includes brim and rib offset
footprint.rotate(Geometry::deg2rad(m_config.wipe_tower_rotation_angle.value));
footprint.translate(Point(scale_(m_config.wipe_tower_x.get_at(m_plate_index)),
scale_(m_config.wipe_tower_y.get_at(m_plate_index))));
const Polygons printable_polys = this->get_extruder_shared_printable_polygon();
if (!printable_polys.empty() && !diff(Polygons{footprint}, printable_polys).empty()) {
const BoundingBox fp = get_extents(footprint);
const BoundingBox pr = get_extents(printable_polys);
BOOST_LOG_TRIVIAL(error) << boost::format("wipe tower footprint [%1%,%2%]-[%3%,%4%] leaves printable [%5%,%6%]-[%7%,%8%]") %
unscaled(fp.min.x()) % unscaled(fp.min.y()) % unscaled(fp.max.x()) % unscaled(fp.max.y()) %
unscaled(pr.min.x()) % unscaled(pr.min.y()) % unscaled(pr.max.x()) % unscaled(pr.max.y());
throw Slic3r::SlicingError(L("Prime Tower") + L(" is partially outside the printable area, and it cannot be printed.\n"));
}
// The cutter/purge corner is a physical obstacle — the brim must stay out like the body.
if (!intersection(get_bed_excluded_area(m_config), Polygons{footprint}).empty())
throw Slic3r::SlicingError(L("Prime Tower") + L(" is too close to an exclusion area, and collisions will be caused.\n"));
}
} }
// Generate a recommended G-code output file name based on the format template, default extension, and template parameters // Generate a recommended G-code output file name based on the format template, default extension, and template parameters
@@ -5999,17 +5996,26 @@ ExtrusionLayers FakeWipeTower::getTrueExtrusionLayersFromWipeTower() const
} }
return wtels; return wtels;
} }
void WipeTowerData::construct_mesh(float width, float depth, float height, float brim_width, bool is_rib_wipe_tower, float rib_width, float rib_length,bool fillet_wall) void WipeTowerData::construct_mesh(float width, float depth, float height, float brim_width, bool is_rib_wipe_tower, float rib_width, float rib_length,bool fillet_wall, float cone_angle)
{ {
wipe_tower_mesh_data = WipeTowerMeshData{}; wipe_tower_mesh_data = WipeTowerMeshData{};
float first_layer_height=0.08; //brim height float first_layer_height=0.08; //brim height
if (width < EPSILON || depth < EPSILON || height < EPSILON) return; if (width < EPSILON || depth < EPSILON || height < EPSILON) return;
if (!is_rib_wipe_tower || rib_length < EPSILON) { if (cone_angle > EPSILON && (!is_rib_wipe_tower || rib_length < EPSILON)) {
// Cone tower: the base bulges past the body box; this bottom polygon feeds the
// containment checks, so it must carry the bulge and the brim (cone not lofted).
wipe_tower_mesh_data->real_wipe_tower_mesh = make_cube(width, depth, height);
wipe_tower_mesh_data->bottom = WipeTower2::cone_base_polygon(width, depth, height, cone_angle);
auto brim_bottom = offset(wipe_tower_mesh_data->bottom, scaled(brim_width));
if (!brim_bottom.empty())
wipe_tower_mesh_data->bottom = brim_bottom.front();
wipe_tower_mesh_data->real_brim_mesh = WipeTower::its_make_rib_brim(wipe_tower_mesh_data->bottom, first_layer_height);
} else if (!is_rib_wipe_tower || rib_length < EPSILON) {
wipe_tower_mesh_data->real_wipe_tower_mesh = make_cube(width, depth, height); wipe_tower_mesh_data->real_wipe_tower_mesh = make_cube(width, depth, height);
wipe_tower_mesh_data->real_brim_mesh = make_cube(width + 2 * brim_width, depth + 2 * brim_width, first_layer_height); wipe_tower_mesh_data->real_brim_mesh = make_cube(width + 2 * brim_width, depth + 2 * brim_width, first_layer_height);
wipe_tower_mesh_data->real_brim_mesh.translate({-brim_width, -brim_width, 0}); wipe_tower_mesh_data->real_brim_mesh.translate({-brim_width, -brim_width, 0});
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}), wipe_tower_mesh_data->bottom = {scaled(Vec2f{-brim_width, -brim_width}), scaled(Vec2f{width + brim_width, -brim_width}),
scaled(Vec2f{0, depth})}; scaled(Vec2f{width + brim_width, depth + brim_width}), scaled(Vec2f{-brim_width, depth + brim_width})};
} else { } 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->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); 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: // Depth of the wipe tower to pass to GLCanvas3D for exact bounding box:
float depth; 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; std::vector<std::pair<float, float>> z_and_depth_pairs;
float brim_width; float brim_width;
float height; float height;
@@ -795,12 +798,13 @@ struct WipeTowerData
used_filament.clear(); used_filament.clear();
number_of_toolchanges = -1; number_of_toolchanges = -1;
depth = 0.f; depth = 0.f;
width = 0.f;
brim_width = 0.f; brim_width = 0.f;
height = 0.f; height = 0.f;
rib_offset = Vec2f::Zero(); rib_offset = Vec2f::Zero();
wipe_tower_mesh_data = std::nullopt; wipe_tower_mesh_data = std::nullopt;
} }
void construct_mesh(float width, float depth, float height, float brim_width, bool is_rib_wipe_tower, float rib_width, float rib_length, bool fillet_wall); void construct_mesh(float width, float depth, float height, float brim_width, bool is_rib_wipe_tower, float rib_width, float rib_length, bool fillet_wall, float cone_angle = 0.f);
private: private:
// Only allow the WipeTowerData to be instantiated internally by Print, // Only allow the WipeTowerData to be instantiated internally by Print,
+78 -22
View File
@@ -2846,7 +2846,9 @@ void TreeSupport::drop_nodes()
const MinimumSpanningTree& mst = spanning_trees[group_index]; const MinimumSpanningTree& mst = spanning_trees[group_index];
//In the first pass, merge all nodes that are close together. //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()); 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* p_node = entry.second;
SupportNode& node = *p_node; SupportNode& node = *p_node;
if (!p_node->valid) if (!p_node->valid)
@@ -2934,7 +2936,32 @@ void TreeSupport::drop_nodes()
); );
//In the second pass, move all middle 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; SupportNode* p_node = entry.second;
const SupportNode& node = *p_node; 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)); ExPolygons overhangs_next = diff_clipped({ node.overhang }, get_collision(0, obj_layer_nr_next));
for(auto& overhang:overhangs_next) { for(auto& overhang:overhangs_next) {
Point next_pt = overhang.contour.centroid(); 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, PendingNode pending;
p_node->support_roof_layers_below - (p_node->distance_to_top >= 0 ? 1 : 0), pending.position = next_pt;
to_buildplate, p_node, print_z_next, height_next); pending.distance_to_top = p_node->distance_to_top + 1;
next_node->max_move_dist = 0; pending.support_roof_layers_below = p_node->support_roof_layers_below - (p_node->distance_to_top >= 0 ? 1 : 0);
next_node->overhang = std::move(overhang); pending.to_buildplate = to_buildplate;
m_ts_data->m_mutex.lock(); pending.parent = p_node;
contact_nodes[layer_nr_next].emplace_back(next_node); pending.zero_max_move = true;
m_ts_data->m_mutex.unlock(); pending.has_overhang = true;
pending.overhang = std::move(overhang);
pass2_out.pending.emplace_back(std::move(pending));
} }
return; return;
@@ -2973,17 +3002,17 @@ void TreeSupport::drop_nodes()
{ {
if (support_on_buildplate_only) if (support_on_buildplate_only)
{ {
unsupported_branch_leaves.push_front({ layer_nr, p_node }); pass2_out.unsupported_leaf = true;
} }
else { else {
p_node->valid = false; pass2_out.invalidate = true;
} }
return; return;
} }
// if the link between parent and current is cut by contours, mark current as bottom contact node // 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) 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; return;
} }
} }
@@ -3096,20 +3125,47 @@ void TreeSupport::drop_nodes()
} }
auto next_collision = get_collision(0, obj_layer_nr_next); 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); 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) // 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; direction_to_outer = to_outside - node.position;
double dist_to_outer = unscale_(direction_to_outer.cast<double>().norm()); 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)); 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); 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();
} }
); 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 #ifdef SUPPORT_TREE_DEBUG_TO_SVG
+4 -7
View File
@@ -2382,13 +2382,10 @@ static void merge_influence_areas(
size_t num_buckets_initial; size_t num_buckets_initial;
{ {
// How many buckets per first merge iteration? // How many buckets per first merge iteration?
const size_t num_threads = tbb::this_task_arena::max_concurrency(); // Fixed at 4: merging is not associative, so sizing buckets off max_concurrency() made
// 4 buckets per thread if possible, // results depend on the core count of the slicing machine.
const size_t num_buckets_min = (input_size + 2) / 4; const size_t bucket_size = 4;
// 2 buckets per thread otherwise. num_buckets_initial = (input_size + 2) / 4;
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;
// Fill in the buckets. // Fill in the buckets.
SupportElementMerging *it = influence_areas.data(); SupportElementMerging *it = influence_areas.data();
// Reserve one more bucket to keep a single influence area which will not be merged in the first iteration. // 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); extern bool is_shapes_dir(const std::string& dir);
//BBS: add json support //BBS: add json support
extern bool is_json_file(const std::string& path); 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 // Orca: custom protocal support utils
inline bool is_orca_open(const std::string& url) { return boost::starts_with(url, "orcaslicer://open"); } 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 EXTERNAL_INFILL_MARGIN = 3;
static constexpr double BRIDGE_INFILL_MARGIN = 1; static constexpr double BRIDGE_INFILL_MARGIN = 1;
static constexpr double WIPE_TOWER_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. //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)); } //inline coord_t scale_(coordf_t v) { return coord_t(floor(v / SCALING_FACTOR + 0.5f)); }
#define scale_(val) ((val) / SCALING_FACTOR) #define scale_(val) ((val) / SCALING_FACTOR)
+25 -1
View File
@@ -961,7 +961,7 @@ CopyFileResult copy_file(const std::string &from, const std::string &to, std::st
BOOL result = CopyFileW(src_wstr, dst_wstr, FALSE); BOOL result = CopyFileW(src_wstr, dst_wstr, FALSE);
if (!result) { if (!result) {
DWORD errCode = GetLastError(); DWORD errCode = GetLastError();
error_message = "Error: " + errCode; error_message = "Error: " + std::to_string(errCode);
ret = FAIL_COPY_FILE; ret = FAIL_COPY_FILE;
goto __finished; goto __finished;
} }
@@ -1088,6 +1088,30 @@ bool is_json_file(const std::string& path)
return boost::iends_with(path, ".json"); 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) bool is_img_file(const std::string &path)
{ {
return boost::iends_with(path, ".png") || boost::iends_with(path, ".svg"); 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/AppConfig.hpp"
#include "libslic3r/PresetBundle.hpp" #include "libslic3r/PresetBundle.hpp"
#include "libslic3r/ClipperUtils.hpp" #include "libslic3r/ClipperUtils.hpp"
#include "libslic3r/GCode/WipeTower.hpp"
#include "libslic3r/GCode/WipeTowerEstimate.hpp"
#include "libslic3r/Tesselate.hpp" #include "libslic3r/Tesselate.hpp"
#include "libslic3r/PrintConfig.hpp" #include "libslic3r/PrintConfig.hpp"
@@ -919,6 +921,21 @@ int GLVolumeCollection::load_wipe_tower_preview(
GUI::PartPlateList& ppl = GUI::wxGetApp().plater()->get_partplate_list(); GUI::PartPlateList& ppl = GUI::wxGetApp().plater()->get_partplate_list();
std::vector<int> plate_extruders = ppl.get_plate(plate_idx)->get_extruders(true); std::vector<int> plate_extruders = ppl.get_plate(plate_idx)->get_extruders(true);
TriangleMesh wipe_tower_shell = make_cube(width, depth, height); 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) { for (int extruder_id : plate_extruders) {
if (extruder_id <= extruder_colors.size()) if (extruder_id <= extruder_colors.size())
colors.push_back(extruder_colors[extruder_id - 1]); colors.push_back(extruder_colors[extruder_id - 1]);
@@ -929,14 +946,19 @@ int GLVolumeCollection::load_wipe_tower_preview(
// Orca: make it transparent // Orca: make it transparent
for(auto& color : colors) for(auto& color : colors)
color.a(0.66f); 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)); volumes.emplace_back(new GLWipeTowerVolume(colors));
GLWipeTowerVolume& v = *dynamic_cast<GLWipeTowerVolume*>(volumes.back()); GLWipeTowerVolume& v = *dynamic_cast<GLWipeTowerVolume*>(volumes.back());
v.model_per_colors.resize(colors.size()); v.model_per_colors.resize(colors.size());
for (int i = 0; i < colors.size(); i++) { for (size_t i = 0; i < slab_count; i++) {
TriangleMesh color_part = make_cube(width, depth / colors.size(), height); TriangleMesh color_part = make_cube(width, depth / slab_count, height);
color_part.translate({ 0.f, depth * i / colors.size(), 0. }); color_part.translate({ 0.f, depth * i / slab_count, 0. });
v.model_per_colors[i].init_from(color_part); 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.model.init_from(wipe_tower_shell);
v.mesh_raycaster = std::make_unique<GUI::MeshRaycaster>(std::make_shared<const TriangleMesh>(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); v.set_convex_hull(wipe_tower_shell);
+1 -1
View File
@@ -406,7 +406,7 @@ void AmsMapingPopup::update_ams_data_multi_machines()
int ams_type = 1; int ams_type = 1;
int nozzle_id = 0; int nozzle_id = 0;
if (ams_type >= 1 || ams_type <= 3) { // 1:ams 2:ams-lite 3:n3f if (ams_type >= 1 && ams_type <= 3) { // 1:ams 2:ams-lite 3:n3f
auto sizer_mapping_list = new wxBoxSizer(wxHORIZONTAL); auto sizer_mapping_list = new wxBoxSizer(wxHORIZONTAL);
auto ams_mapping_item_container = new MappingContainer(nozzle_id == 0 ? m_right_marea_panel : m_left_marea_panel, "AMS-1", 4); auto ams_mapping_item_container = new MappingContainer(nozzle_id == 0 ? m_right_marea_panel : m_left_marea_panel, "AMS-1", 4);
+3 -1
View File
@@ -848,7 +848,9 @@ void BackgroundSlicingProcess::finalize_gcode()
case CopyFileResult::SUCCESS: break; // no error case CopyFileResult::SUCCESS: break; // no error
case CopyFileResult::FAIL_COPY_FILE: case CopyFileResult::FAIL_COPY_FILE:
throw Slic3r::ExportError(GUI::format( throw Slic3r::ExportError(GUI::format(
_L("Copying of the temporary G-code to the output G-code failed. Maybe the SD card is write locked?\nError message: %1%"), m_export_path_on_removable_media ?
_L("Copying of the temporary G-code to the output G-code failed. Maybe the SD card is write locked?\nError message: %1%") :
_L("Copying of the temporary G-code to the output G-code failed.\nError message: %1%"),
error_message)); error_message));
break; break;
case CopyFileResult::FAIL_FILES_DIFFERENT: case CopyFileResult::FAIL_FILES_DIFFERENT:
@@ -360,7 +360,7 @@ void CaliPresetCustomRangePanel::create_panel(wxWindow* parent)
int max_decimal_length; int max_decimal_length;
if (i <= 1) if (i <= 1)
max_decimal_length = 3; max_decimal_length = 3;
else if (i >= 2) else
max_decimal_length = 4; max_decimal_length = 4;
if (decimal_number > max_decimal_length) { if (decimal_number > max_decimal_length) {
int allowed_length = number.length() - decimal_number + max_decimal_length; int allowed_length = number.length() - decimal_number + max_decimal_length;
+1 -1
View File
@@ -872,7 +872,7 @@ namespace Slic3r
obj->m_is_online = elem["dev_online"].get<bool>(); obj->m_is_online = elem["dev_online"].get<bool>();
if (elem.contains("dev_model_name") && !elem["dev_model_name"].is_null()) { if (elem.contains("dev_model_name") && !elem["dev_model_name"].is_null()) {
auto printer_type = elem["dev_model_name"].get<std::string>(); auto printer_type = elem["dev_model_name"].get<std::string>();
for (const std::pair<std::string, std::vector<std::string>> &pair : device_subseries) { for (const auto &pair : device_subseries) {
auto it = std::find(pair.second.begin(), pair.second.end(), printer_type); auto it = std::find(pair.second.begin(), pair.second.end(), printer_type);
if (it != pair.second.end()) if (it != pair.second.end())
{ {
+2 -1
View File
@@ -2711,7 +2711,8 @@ void ColourPicker::set_value(const boost::any& value, bool change_event)
auto field = dynamic_cast<wxColourPickerCtrl*>(window); auto field = dynamic_cast<wxColourPickerCtrl*>(window);
#ifdef __WXMSW__ #ifdef __WXMSW__
wxColour clr = (clr_str.IsEmpty() || !clr.IsOk()) ? wxTransparentColour : clr_str; const wxColour parsed_clr(clr_str);
wxColour clr = (clr_str.IsEmpty() || !parsed_clr.IsOk()) ? wxTransparentColour : parsed_clr;
field->SetColour(clr); field->SetColour(clr);
draw_bmp_btn(field, clr); draw_bmp_btn(field, clr);
#else #else
+30 -16
View File
@@ -2190,7 +2190,7 @@ void GLCanvas3D::render(bool only_init)
// Negative coordinate means out of the window, likely because the window was deactivated. // Negative coordinate means out of the window, likely because the window was deactivated.
// In that case the tooltip should be hidden. // In that case the tooltip should be hidden.
if (m_mouse.position.x() >= 0. && m_mouse.position.y() >= 0. || has_mouse_capture()) { // ORCA continue to capture mouse pos mid drag if ((m_mouse.position.x() >= 0. && m_mouse.position.y() >= 0.) || has_mouse_capture()) { // ORCA continue to capture mouse pos mid drag
if (tooltip.empty()) if (tooltip.empty())
tooltip = m_layers_editing.get_tooltip(*this); tooltip = m_layers_editing.get_tooltip(*this);
@@ -2890,23 +2890,37 @@ void GLCanvas3D::reload_scene(bool refresh_immediately, bool force_full_scene_re
DynamicPrintConfig& proj_cfg = wxGetApp().preset_bundle->project_config; 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 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 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; 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(); 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(); 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; if (part_plate->get_objects_on_this_plate().empty()) continue;
float brim_width = print->wipe_tower_data(filaments_count).brim_width; // Body and brim from this plate's own estimate: m_process->fff_print() is the
int nozzle_nums = wxGetApp().preset_bundle->get_printer_extruder_count(); // selected plate's, so an auto brim drew every tower with that plate's brim.
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); 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) { if (!current_print->is_step_done(psWipeTower) || !current_print->wipe_tower_data().wipe_tower_mesh_data) {
// update for wipe tower position // 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), int volume_idx_wipe_tower_new = m_volumes.load_wipe_tower_preview(1000 + plate_id, x + plate_origin(0), y + plate_origin(1),
@@ -9760,18 +9774,18 @@ void GLCanvas3D::_render_paint_toolbar() const
ImVec2 number_label_size = ImGui::CalcTextSize(std::to_string(i + 1).c_str()); ImVec2 number_label_size = ImGui::CalcTextSize(std::to_string(i + 1).c_str());
ImGui::SetCursorPosY(cursor_y + text_offset_y); ImGui::SetCursorPosY(cursor_y + text_offset_y);
ImGui::SetCursorPosX(spacing + i * (spacing + button_size.x) + (button_size.x - number_label_size.x) / 2); ImGui::SetCursorPosX(spacing + i * (spacing + button_size.x) + (button_size.x - number_label_size.x) / 2);
ImGui::TextColored(text_color, std::to_string(i + 1).c_str()); ImGui::TextColored(text_color, "%s", std::to_string(i + 1).c_str());
imgui.pop_bold_font(); imgui.pop_bold_font();
ImVec2 filament_first_line_label_size = ImGui::CalcTextSize(filament_text_first_line[i].c_str()); ImVec2 filament_first_line_label_size = ImGui::CalcTextSize(filament_text_first_line[i].c_str());
ImGui::SetCursorPosY(cursor_y + text_offset_y + number_label_size.y); ImGui::SetCursorPosY(cursor_y + text_offset_y + number_label_size.y);
ImGui::SetCursorPosX(spacing + i * (spacing + button_size.x) + (button_size.x - filament_first_line_label_size.x) / 2); ImGui::SetCursorPosX(spacing + i * (spacing + button_size.x) + (button_size.x - filament_first_line_label_size.x) / 2);
ImGui::TextColored(text_color, filament_text_first_line[i].c_str()); ImGui::TextColored(text_color, "%s", filament_text_first_line[i].c_str());
ImVec2 filament_second_line_label_size = ImGui::CalcTextSize(filament_text_second_line[i].c_str()); ImVec2 filament_second_line_label_size = ImGui::CalcTextSize(filament_text_second_line[i].c_str());
ImGui::SetCursorPosY(cursor_y + text_offset_y + number_label_size.y + filament_first_line_label_size.y); ImGui::SetCursorPosY(cursor_y + text_offset_y + number_label_size.y + filament_first_line_label_size.y);
ImGui::SetCursorPosX(spacing + i * (spacing + button_size.x) + (button_size.x - filament_second_line_label_size.x) / 2); ImGui::SetCursorPosX(spacing + i * (spacing + button_size.x) + (button_size.x - filament_second_line_label_size.x) / 2);
ImGui::TextColored(text_color, filament_text_second_line[i].c_str()); ImGui::TextColored(text_color, "%s", filament_text_second_line[i].c_str());
} }
if (ImGui::GetWindowWidth() == constraint_window_width) { if (ImGui::GetWindowWidth() == constraint_window_width) {
@@ -9998,9 +10012,9 @@ void GLCanvas3D::_render_assemble_info() const
double size1 = m_selection.get_bounding_box().size()(1); double size1 = m_selection.get_bounding_box().size()(1);
double size2 = m_selection.get_bounding_box().size()(2); double size2 = m_selection.get_bounding_box().size()(2);
if (!m_selection.is_empty()) { if (!m_selection.is_empty()) {
ImGui::Text(_L("Volume:").ToUTF8()); ImGui::SameLine(caption_max); ImGui::Text("%s", _L("Volume:").ToUTF8().data()); ImGui::SameLine(caption_max);
ImGui::Text("%.2f", size0 * size1 * size2); ImGui::Text("%.2f", size0 * size1 * size2);
ImGui::Text(_L("Size:").ToUTF8()); ImGui::SameLine(caption_max); ImGui::Text("%s", _L("Size:").ToUTF8().data()); ImGui::SameLine(caption_max);
ImGui::Text("%.2f x %.2f x %.2f", size0, size1, size2); ImGui::Text("%.2f x %.2f x %.2f", size0, size1, size2);
} }
imgui->end(); imgui->end();
+1 -1
View File
@@ -6808,7 +6808,7 @@ bool GUI_App::check_preset_parent_available(const std::pair<std::string, std::ma
void GUI_App::add_pending_vendor_preset(const std::pair<std::string, std::map<std::string, std::string>>& preset_data) void GUI_App::add_pending_vendor_preset(const std::pair<std::string, std::map<std::string, std::string>>& preset_data)
{ {
Preset::Type type; Preset::Type type = Preset::Type::TYPE_INVALID;
if (preset_data.second.at(BBL_JSON_KEY_TYPE) == PRESET_IOT_PRINT_TYPE) if (preset_data.second.at(BBL_JSON_KEY_TYPE) == PRESET_IOT_PRINT_TYPE)
type = Preset::Type::TYPE_PRINT; type = Preset::Type::TYPE_PRINT;
else if (preset_data.second.at(BBL_JSON_KEY_TYPE) == PRESET_IOT_PRINTER_TYPE) else if (preset_data.second.at(BBL_JSON_KEY_TYPE) == PRESET_IOT_PRINTER_TYPE)
+2 -2
View File
@@ -102,7 +102,7 @@ CopyFileResult copy_file_gui(const std::string &from, const std::string &to, std
result = ReadFile(handlesrc, buff, size, &dwRead, NULL); result = ReadFile(handlesrc, buff, size, &dwRead, NULL);
if (!result) { if (!result) {
DWORD errCode = GetLastError(); DWORD errCode = GetLastError();
error_message = "Error: " + errCode; error_message = "Error: " + std::to_string(errCode);
ret = FAIL_COPY_FILE; ret = FAIL_COPY_FILE;
goto __finished; goto __finished;
} }
@@ -110,7 +110,7 @@ CopyFileResult copy_file_gui(const std::string &from, const std::string &to, std
result = WriteFile(handledst,buff,size,&dwWrite,NULL); result = WriteFile(handledst,buff,size,&dwWrite,NULL);
if (!result) { if (!result) {
DWORD errCode = GetLastError(); DWORD errCode = GetLastError();
error_message = "Error: " + errCode; error_message = "Error: " + std::to_string(errCode);
ret = FAIL_COPY_FILE; ret = FAIL_COPY_FILE;
goto __finished; goto __finished;
} }
+1 -1
View File
@@ -342,7 +342,7 @@ bool GLGizmoBrimEars::on_mouse(const wxMouseEvent& mouse_event)
// concludes that the event was not intended for it, it should return false. // concludes that the event was not intended for it, it should return false.
bool GLGizmoBrimEars::gizmo_event(SLAGizmoEventType action, const Vec2d &mouse_position, bool shift_down, bool alt_down, bool control_down) bool GLGizmoBrimEars::gizmo_event(SLAGizmoEventType action, const Vec2d &mouse_position, bool shift_down, bool alt_down, bool control_down)
{ {
if (action != SLAGizmoEventType::MouseWheelDown || action != SLAGizmoEventType::MouseWheelUp || action != SLAGizmoEventType::Moving) { if (action != SLAGizmoEventType::MouseWheelDown && action != SLAGizmoEventType::MouseWheelUp && action != SLAGizmoEventType::Moving) {
apply_radius_change(); apply_radius_change();
} }
+1 -1
View File
@@ -122,7 +122,7 @@ bool GLGizmoFdmSupports::on_init()
{ctrl + _L("Mouse wheel"), _L("Gap area")} {ctrl + _L("Mouse wheel"), _L("Gap area")}
}; };
memset(&m_print_instance, 0, sizeof(m_print_instance)); m_print_instance = PrintInstance();
return true; return true;
} }
+2 -2
View File
@@ -343,12 +343,12 @@ void GLGizmoSimplify::on_render_input_window(float x, float y, float bottom_limi
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing,ImVec2(10,20)); ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing,ImVec2(10,20));
if (is_worker_running) { // apply or preview if (is_worker_running) { // apply or preview
// draw progress bar // draw progress bar
std::string progress_text = GUI::format("%1%", std::to_string(progress)) + "%%"; std::string progress_text = GUI::format("%1%", std::to_string(progress)) + "%";
ImVec2 progress_size(bottom_left_width - space_size, 0.0f); ImVec2 progress_size(bottom_left_width - space_size, 0.0f);
ImGui::BBLProgressBar2(progress / 100., progress_size); ImGui::BBLProgressBar2(progress / 100., progress_size);
ImGui::SameLine(); ImGui::SameLine();
ImGui::AlignTextToFramePadding(); ImGui::AlignTextToFramePadding();
ImGui::TextColored(ImVec4(0.42f, 0.42f, 0.42f, 1.00f), progress_text.c_str()); ImGui::TextColored(ImVec4(0.42f, 0.42f, 0.42f, 1.00f), "%s", progress_text.c_str());
ImGui::SameLine(bottom_left_width + slider_width + m_imgui->scaled(1.0f)); ImGui::SameLine(bottom_left_width + slider_width + m_imgui->scaled(1.0f));
} else { } else {
ImGui::Dummy(ImVec2(bottom_left_width - space_size, -1)); ImGui::Dummy(ImVec2(bottom_left_width - space_size, -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(); int extruder_size = extruder_ids.size();
Vec3d wipe_tower_size, wipe_tower_pos; 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, 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, nozzle_nums, extruder_size);
arrange_poly.bed_idx = plate_index; arrange_poly.bed_idx = plate_index;
return arrange_poly; return arrange_poly;
} }
+1 -1
View File
@@ -71,7 +71,7 @@ MediaPlayCtrl::MediaPlayCtrl(wxWindow *parent, wxMediaCtrl3 *media_ctrl, const w
auto ip = str.find(' ', ik); auto ip = str.find(' ', ik);
if (ip == wxString::npos) ip = str.Length(); if (ip == wxString::npos) ip = str.Length();
auto v = str.Mid(ik, ip - ik); auto v = str.Mid(ik, ip - ik);
if (k == "T:" && v.Length() == 8) { if (strcmp(k, "T:") == 0 && v.Length() == 8) {
long h = 0,m = 0,s = 0; long h = 0,m = 0,s = 0;
v.Left(2).ToLong(&h); v.Left(2).ToLong(&h);
v.Mid(3, 2).ToLong(&m); v.Mid(3, 2).ToLong(&m);
+1 -1
View File
@@ -498,7 +498,7 @@ void Mouse3DController::render_settings_dialog(GLCanvas3D& canvas) const
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(20.0f, 20.0f)); ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(20.0f, 20.0f));
static ImVec2 last_win_size(0.0f, 0.0f); static ImVec2 last_win_size(0.0f, 0.0f);
bool shown = true; bool shown = true;
if (imgui.begin(_L("3Dconnexion settings"), &shown, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoCollapse || ImGuiWindowFlags_NoTitleBar)) { if (imgui.begin(_L("3Dconnexion settings"), &shown, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar)) {
if (shown) { if (shown) {
ImVec2 win_size = ImGui::GetWindowSize(); ImVec2 win_size = ImGui::GetWindowSize();
if (last_win_size.x != win_size.x || last_win_size.y != win_size.y) { if (last_win_size.x != win_size.x || last_win_size.y != win_size.y) {
+123 -117
View File
@@ -1,5 +1,6 @@
#include <cstddef> #include <cstddef>
#include <algorithm> #include <algorithm>
#include <limits>
#include <numeric> #include <numeric>
#include <vector> #include <vector>
#include <string> #include <string>
@@ -20,6 +21,7 @@
#include "libslic3r/libslic3r.h" #include "libslic3r/libslic3r.h"
#include "libslic3r/Polygon.hpp" #include "libslic3r/Polygon.hpp"
#include "libslic3r/GCode/WipeTowerEstimate.hpp"
#include "libslic3r/ClipperUtils.hpp" #include "libslic3r/ClipperUtils.hpp"
#include "libslic3r/BoundingBox.hpp" #include "libslic3r/BoundingBox.hpp"
#include "libslic3r/Geometry.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)) { if (check_objects_empty_and_gcode3mf(plate_extruders)) {
return plate_extruders; return plate_extruders;
} }
// if 3mf file return get_extruders(conside_custom_gcode, wxGetApp().preset_bundle->prints.get_edited_preset().config, wxGetApp().preset_bundle->project_config);
const DynamicPrintConfig& glb_config = wxGetApp().preset_bundle->prints.get_edited_preset().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_intf_extr = glb_config.opt_int("support_interface_filament");
int glb_support_extr = glb_config.opt_int("support_filament"); int glb_support_extr = glb_config.opt_int("support_filament");
int glb_outer_wall_extr = glb_config.opt_int("outer_wall_filament_id"); 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; glb_support |= glb_config.opt_int("raft_layers") > 0;
for (int obj_idx = 0; obj_idx < m_model->objects.size(); obj_idx++) { 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; continue;
ModelObject* mo = m_model->objects[obj_idx]; 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) { if (conside_custom_gcode) {
//BBS //BBS
int nums_extruders = 0; 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(); nums_extruders = color_option->values.size();
if (m_model->plates_custom_gcodes.find(m_plate_index) != m_model->plates_custom_gcodes.end()) { 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) { 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 // is never loaded into a tray, so callers (AMS mapping, filament checks) must see the
// physical filaments it resolves to instead. // physical filaments it resolves to instead.
{ {
auto& project_config = wxGetApp().preset_bundle->project_config; const auto* is_mixed_opt = project_config.option<ConfigOptionBools>("filament_is_mixed");
auto* is_mixed_opt = project_config.option<ConfigOptionBools>("filament_is_mixed"); const auto* comp_strs_opt = project_config.option<ConfigOptionStrings>("filament_mixed_components");
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)) { if (is_mixed_opt && comp_strs_opt && has_any_mixed_filament(is_mixed_opt->values)) {
std::vector<unsigned int> ext_0based; std::vector<unsigned int> ext_0based;
for (int e : plate_extruders) for (int e : plate_extruders)
@@ -2311,113 +2329,97 @@ bool PartPlate::check_compatible_of_nozzle_and_filament(const DynamicPrintConfig
return wipe_tower_size; 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; // The CLI calls this too, so the plate's filaments are derived from the passed config:
double layer_height = 0.08f; // hard code layer height // get_extruders(bool) reads the same keys off wxGetApp()'s presets, which the CLI has none of.
double max_height = 0.f; // An explicit count is a floor: init-time and arrange estimates size an empty plate for that
wipe_tower_size.setZero(); // 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();
// 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 ConfigOption *layer_height_opt = config.option("layer_height");
if (layer_height_opt) const double global_layer_height = layer_height_opt != nullptr ? layer_height_opt->getFloat() : 0.08;
layer_height = layer_height_opt->getFloat(); double max_height = 0.;
double layer_height = std::numeric_limits<double>::max();
// empty plate for (int obj_idx = 0; obj_idx < int(m_model->objects.size()); ++obj_idx) {
if (plate_extruder_size == 0) const ModelObject *object = m_model->objects[obj_idx];
{ if (!use_global_objects && !contain_any_instance_totally(obj_idx))
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))
continue; continue;
// 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);
}
if (layer_height == std::numeric_limits<double>::max())
layer_height = global_layer_height;
BoundingBoxf3 bbox = m_model->objects[obj_idx]->bounding_box_exact(); std::vector<unsigned int> filament_ids;
max_height = std::max(bbox.size().z(), max_height); for (int id : plate_extruders)
} if (id > 0)
wipe_tower_size(2) = max_height; filament_ids.push_back(static_cast<unsigned int>(id - 1));
//const DynamicPrintConfig &dconfig = wxGetApp().preset_bundle->prints.get_edited_preset().config; return Slic3r::estimate_wipe_tower_footprint(config, resolve_wipe_tower_type(config), filament_ids, layer_height, max_height);
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;
}
}
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;
} }
return wipe_tower_size; 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
}
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
{ {
float x = dynamic_cast<const ConfigOptionFloats*>(config.option("wipe_tower_x"))->get_at(plate_index); 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 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 a = dynamic_cast<const ConfigOptionFloat*>(config.option("wipe_tower_rotation_angle"))->value;
float v = dynamic_cast<const ConfigOptionFloat*>(config.option("prime_volume"))->value; const WipeTowerFootprint footprint = estimate_wipe_tower_footprint(config, plate_extruder_size, use_global_objects);
float tower_brim_width = dynamic_cast<const ConfigOptionFloat*>(config.option("prime_tower_brim_width"))->value; wt_size = Vec3d(footprint.width, footprint.depth, footprint.height);
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);
int plate_width=m_width, plate_depth=m_depth; 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 depth = wt_size(1);
float margin = WIPE_TOWER_MARGIN + tower_brim_width, wp_brim_width = 0.f; // Resolved brim, not the raw option: "Auto" (-1) would yield a margin of 0 and let the
const ConfigOption* wipe_tower_brim_width_opt = config.option("prime_tower_brim_width"); // clamp put the brim off the bed. Matches set_default_wipe_tower_pos_for_plate.
if (wipe_tower_brim_width_opt) { float wp_brim_width = float(footprint.brim_width);
wp_brim_width = wipe_tower_brim_width_opt->getFloat(); // A Type2 stabilization cone bulges past the body box like a brim does - fold its worst-axis
if (wp_brim_width < 0) wp_brim_width = WipeTower::get_auto_brim_by_height((float) wt_size.z()); // 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; 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);
x = std::clamp(x, margin, (float)plate_width - w - margin - wp_brim_width); const float margin_c = (float) WIPE_TOWER_AUTO_MARGIN + wp_brim_width;
y = std::clamp(y, margin, (float)plate_depth - depth - 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(0) = x;
wt_pos(1) = y; wt_pos(1) = y;
wt_pos(2) = 0.f; wt_pos(2) = 0.f;
@@ -2755,6 +2757,20 @@ bool PartPlate::contain_instance_totally(int obj_id, int instance_id) const
return result; 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 //check whether instance is outside the plate or not
bool PartPlate::check_outside(int obj_id, int instance_id, BoundingBoxf3* bounding_box) 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); 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); 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; WipeTowerFootprint footprint = part_plate->estimate_wipe_tower_footprint(full_config, init_pos ? 2 : 0);
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);
if (!init_pos && (is_approx(wipe_tower_size(0), 0.0) || is_approx(wipe_tower_size(1), 0.0))) { if (!init_pos && (is_approx(footprint.width, 0.0) || is_approx(footprint.depth, 0.0))) {
wipe_tower_size = part_plate->estimate_wipe_tower_size(full_config, w, v, nozzle_nums, 2, false, enable_wrapping); 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 // Brim-aware margin: the brim extends outward from the tower position.
float brim_width = 0.f; const float brim_width = float(footprint.brim_width);
const ConfigOptionFloat *brim_opt = full_config.option<ConfigOptionFloat>("prime_tower_brim_width"); const float margin = WIPE_TOWER_AUTO_MARGIN + 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;
// clamp wipe tower position within plate boundaries // clamp wipe tower position within plate boundaries
{ {
+10 -2
View File
@@ -11,6 +11,7 @@
#include "libslic3r/GCode/GCodeProcessor.hpp" #include "libslic3r/GCode/GCodeProcessor.hpp"
#include "libslic3r/Format/bbs_3mf.hpp" #include "libslic3r/Format/bbs_3mf.hpp"
#include "libslic3r/Slicing.hpp" #include "libslic3r/Slicing.hpp"
#include "libslic3r/GCode/WipeTowerEstimate.hpp"
#include "libslic3r/Arrange.hpp" #include "libslic3r/Arrange.hpp"
#include "Plater.hpp" #include "Plater.hpp"
#include "libslic3r/Model.hpp" #include "libslic3r/Model.hpp"
@@ -339,11 +340,16 @@ public:
Vec3d get_origin() { return m_origin; } 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 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; // plate_extruder_size: a floor on the filaments purged on the plate; its own are always
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; // 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; bool check_objects_empty_and_gcode3mf(std::vector<int> &result) const;
// get used filaments from config, 1 based idx // 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 = 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_under_cli(bool conside_custom_gcode, DynamicPrintConfig& full_config) const;
std::vector<int> get_extruders_without_support(bool conside_custom_gcode = false) const; std::vector<int> get_extruders_without_support(bool conside_custom_gcode = false) const;
// get used filaments from gcode result, 1 based idx // get used filaments from gcode result, 1 based idx
@@ -366,6 +372,8 @@ public:
bool contain_instance_totally(ModelObject* object, int instance_id) const; bool contain_instance_totally(ModelObject* object, int instance_id) const;
//judge whether instance is totally included in plate or not //judge whether instance is totally included in plate or not
bool contain_instance_totally(int obj_id, int instance_id) const; 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 //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); bool is_left_top_of(int obj_id, int instance_id);
+2 -2
View File
@@ -3667,8 +3667,8 @@ void Sidebar::update_presets(Preset::Type preset_type)
// so extruders without an explicit sub-nozzle count never offer Hybrid. A nullable-int // so extruders without an explicit sub-nozzle count never offer Hybrid. A nullable-int
// nil is INT_MAX (> 1) and would otherwise falsely pass the gate, so exclude it too. // nil is INT_MAX (> 1) and would otherwise falsely pass the gate, so exclude it too.
if (boost::algorithm::contains(extruder_variants->values[index], type + " " + nozzle_volumes_def->enum_labels[i]) || if (boost::algorithm::contains(extruder_variants->values[index], type + " " + nozzle_volumes_def->enum_labels[i]) ||
extruder_max_nozzle_count->get_at(index) > 1 && extruder_max_nozzle_count->get_at(index) != ConfigOptionIntsNullable::nil_value() && (extruder_max_nozzle_count->get_at(index) > 1 && extruder_max_nozzle_count->get_at(index) != ConfigOptionIntsNullable::nil_value() &&
nozzle_volumes_def->enum_keys_map->at(nozzle_volumes_def->enum_values[i]) == nvtHybrid) { nozzle_volumes_def->enum_keys_map->at(nozzle_volumes_def->enum_values[i]) == nvtHybrid)) {
if (nozzle_volumes_def->enum_keys_map->at(nozzle_volumes_def->enum_values[i]) == NozzleVolumeType::nvtHighFlow &&(diameter == "0.2" || if (nozzle_volumes_def->enum_keys_map->at(nozzle_volumes_def->enum_values[i]) == NozzleVolumeType::nvtHighFlow &&(diameter == "0.2" ||
is_skip_high_flow_printer(printer_model))) is_skip_high_flow_printer(printer_model)))
continue; continue;
+1 -1
View File
@@ -3073,7 +3073,7 @@ static bool _HasExt(const std::vector<FilamentInfo> &ams_mapping_result) {
}; };
for (const auto &info : ams_mapping_result) { for (const auto &info : ams_mapping_result) {
if (info.ams_id == VIRTUAL_AMS_MAIN_ID_STR || info.ams_id == VIRTUAL_AMS_DEPUTY_ID_STR && !info.ams_id.empty()) { if (info.ams_id == VIRTUAL_AMS_MAIN_ID_STR || (info.ams_id == VIRTUAL_AMS_DEPUTY_ID_STR && !info.ams_id.empty())) {
return true; return true;
} }
} }
+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()}; 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 tower_origin = m_cache.volumes_data[i].get_volume_position();
Vec3d actual_displacement = displacement; Vec3d actual_displacement = displacement;
bool show_read_wipe_tower = wxGetApp().plater()->get_partplate_list().get_plate(plate_idx)->fff_print()->is_step_done(psWipeTower); // Both preview volumes carry the brim in their bounding box, and the release
float brim_width = wxGetApp().preset_bundle->prints.get_edited_preset().config.opt_float("prime_tower_brim_width"); // clamp holds it WIPE_TOWER_MARGIN inside — same margin, so drops don't snap.
const double margin = WIPE_TOWER_MARGIN;
const double margin = show_read_wipe_tower ? WIPE_TOWER_MARGIN : brim_width + 0.5; // 0.5 is the line width of wipe tower
actual_displacement = (m_cache.volumes_data[i].get_instance_rotation_matrix() * m_cache.volumes_data[i].get_instance_scale_matrix() * 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()) m_cache.volumes_data[i].get_instance_mirror_matrix())
+2 -2
View File
@@ -678,7 +678,7 @@ SyncAmsInfoDialog::SyncAmsInfoDialog(wxWindow *parent, SyncInfo &info) :
wxBoxSizer *loading_Sizer = new wxBoxSizer(wxHORIZONTAL); wxBoxSizer *loading_Sizer = new wxBoxSizer(wxHORIZONTAL);
m_gif_ctrl = new wxAnimationCtrl(m_loading_page, wxID_ANY, wxNullAnimation, wxDefaultPosition, wxDefaultSize, wxAC_DEFAULT_STYLE); m_gif_ctrl = new wxAnimationCtrl(m_loading_page, wxID_ANY, wxNullAnimation, wxDefaultPosition, wxDefaultSize, wxAC_DEFAULT_STYLE);
auto gif_path = Slic3r::var("loading.gif").c_str(); const wxString gif_path = from_u8(Slic3r::var("loading.gif"));
if (m_gif_ctrl->LoadFile(gif_path)){ if (m_gif_ctrl->LoadFile(gif_path)){
m_gif_ctrl->SetSize(m_gif_ctrl->GetAnimation().GetSize()); m_gif_ctrl->SetSize(m_gif_ctrl->GetAnimation().GetSize());
m_gif_ctrl->Play(); m_gif_ctrl->Play();
@@ -1547,7 +1547,7 @@ bool SyncAmsInfoDialog::is_nozzle_type_match(DevExtderSystem data, wxString &err
auto sai_nz_pt = wxGetApp().preset_bundle->printers.get_edited_preset().get_printer_type(wxGetApp().preset_bundle); auto sai_nz_pt = wxGetApp().preset_bundle->printers.get_edited_preset().get_printer_type(wxGetApp().preset_bundle);
if (target_machine_nozzle_id == DEPUTY_EXTRUDER_ID) { if (target_machine_nozzle_id == DEPUTY_EXTRUDER_ID) {
pos = _L(DevPrinterConfigUtil::get_toolhead_display_name(sai_nz_pt, DEPUTY_EXTRUDER_ID, ToolHeadComponent::Nozzle, ToolHeadNameCase::LowerCase)); pos = _L(DevPrinterConfigUtil::get_toolhead_display_name(sai_nz_pt, DEPUTY_EXTRUDER_ID, ToolHeadComponent::Nozzle, ToolHeadNameCase::LowerCase));
} else if ((target_machine_nozzle_id == MAIN_EXTRUDER_ID)) { } else if (target_machine_nozzle_id == MAIN_EXTRUDER_ID) {
pos = _L(DevPrinterConfigUtil::get_toolhead_display_name(sai_nz_pt, MAIN_EXTRUDER_ID, ToolHeadComponent::Nozzle, ToolHeadNameCase::LowerCase)); pos = _L(DevPrinterConfigUtil::get_toolhead_display_name(sai_nz_pt, MAIN_EXTRUDER_ID, ToolHeadComponent::Nozzle, ToolHeadNameCase::LowerCase));
} }
+2 -2
View File
@@ -1281,8 +1281,8 @@ static wxString get_string_value(std::string opt_key, const DynamicPrintConfig&
} }
auto opt_vector = dynamic_cast<const ConfigOptionVectorBase *>(option); auto opt_vector = dynamic_cast<const ConfigOptionVectorBase *>(option);
if (option->is_scalar() && config.option(opt_key)->is_nil() || if ((option->is_scalar() && config.option(opt_key)->is_nil()) ||
option->is_vector() && opt_vector && opt_idx >= 0 && opt_idx < opt_vector->size() && opt_vector->is_nil(opt_idx)) (option->is_vector() && opt_vector && opt_idx >= 0 && opt_idx < opt_vector->size() && opt_vector->is_nil(opt_idx)))
return _L("N/A"); return _L("N/A");
wxString out; wxString out;
+4 -4
View File
@@ -254,10 +254,8 @@ int Http::priv::xfercb(void *userp, curl_off_t dltotal, curl_off_t dlnow, curl_o
bool cb_cancel = false; bool cb_cancel = false;
if (self->progressfn) { if (self->progressfn) {
double speed; double speed = 0.;
curl_easy_getinfo(self->curl, CURLINFO_SPEED_UPLOAD, &speed); curl_easy_getinfo(self->curl, CURLINFO_SPEED_UPLOAD, &speed);
if (speed > 0.01)
speed = speed;
Progress progress(dltotal, dlnow, ultotal, ulnow, self->buffer, speed); Progress progress(dltotal, dlnow, ultotal, ulnow, self->buffer, speed);
self->progressfn(progress, cb_cancel); self->progressfn(progress, cb_cancel);
} }
@@ -323,8 +321,10 @@ void Http::priv::form_add_file(const char *name, const fs::path &path, const cha
// We can't use CURLFORM_FILECONTENT, because curl doesn't support Unicode filenames on Windows // We can't use CURLFORM_FILECONTENT, because curl doesn't support Unicode filenames on Windows
// and so we use CURLFORM_STREAM with boost ifstream to read the file. // and so we use CURLFORM_STREAM with boost ifstream to read the file.
std::string filename_str;
if (filename == nullptr) { if (filename == nullptr) {
filename = path.string().c_str(); filename_str = path.string();
filename = filename_str.c_str();
} }
form_files.emplace_back(path, offset, length); form_files.emplace_back(path, offset, length);
+1 -1
View File
@@ -39,7 +39,7 @@ std::string find_closest_color_preset_by_vendor_and_type(const PresetCollection&
std::string p_color = p.config.opt_string("default_filament_colour", 0u); std::string p_color = p.config.opt_string("default_filament_colour", 0u);
unsigned int p_color_value; unsigned int p_color_value;
if (!p_color.empty()) { if (!p_color.empty()) {
unsigned int hash_pos = p_color.find("#"); size_t hash_pos = p_color.find("#");
p_color_value = std::stoul(p_color.substr(hash_pos != std::string::npos ? hash_pos + 1 : 0), nullptr, 16); p_color_value = std::stoul(p_color.substr(hash_pos != std::string::npos ? hash_pos + 1 : 0), nullptr, 16);
} else { } else {
// Default to black if no color specified in profile. Assume other profiles might be a closer color match. // Default to black if no color specified in profile. Assume other profiles might be a closer color match.
+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("nozzle_temperature_range_high", new ConfigOptionInts({240, 240}));
config.set_key_value("flush_multiplier", new ConfigOptionFloats({1})); config.set_key_value("flush_multiplier", new ConfigOptionFloats({1}));
config.set_key_value("flush_volumes_matrix", new ConfigOptionFloats({0, 140, 140, 0})); config.set_key_value("flush_volumes_matrix", new ConfigOptionFloats({0, 140, 140, 0}));
// Inside the 200x200 test bed; the default y, 220, is not, and generation rejects that.
config.set_key_value("wipe_tower_x", new ConfigOptionFloats({50.}));
config.set_key_value("wipe_tower_y", new ConfigOptionFloats({50.}));
return config; return config;
} }
+67 -2
View File
@@ -1,5 +1,7 @@
#include <catch2/catch_all.hpp> #include <catch2/catch_all.hpp>
#include <algorithm>
#include "libslic3r/Layer.hpp" #include "libslic3r/Layer.hpp"
#include "libslic3r/TriangleMesh.hpp" #include "libslic3r/TriangleMesh.hpp"
@@ -33,10 +35,13 @@ TriangleMesh scaled(TestMesh id, float scale)
return mesh; 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, 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 }, { "enable_support", 1 },
{ "support_type", "tree(auto)" }, { "support_type", "tree(auto)" },
{ "support_style", style }, { "support_style", style },
@@ -45,6 +50,8 @@ void slice_with_tree_support(const TriangleMesh &mesh, Slic3r::Print &print, con
{ "raft_layers", raft_layers }, { "raft_layers", raft_layers },
{ "layer_height", 0.2 }, { "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) 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(); 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 } // namespace
TEST_CASE("Tree support is generated for an overhang and not for a plain cube", "[TreeSupport]") 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. // The raft goes under the object.
REQUIRE(rafted_object->layers().front()->print_z > unrafted_object->layers().front()->print_z); 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 }, { "outer_wall_filament_id", 2 },
{ "inner_wall_filament_id", 2 }, { "inner_wall_filament_id", 2 },
{ "enable_prime_tower", true }, { "enable_prime_tower", true },
{ "wipe_tower_x", 50 }, // inside the 200x200 test bed
{ "wipe_tower_y", 50 }, // (the default y, 220, is not)
{ "layer_height", 0.3 }, { "layer_height", 0.3 },
{ "gcode_flavor", gcode_flavor }, { "gcode_flavor", gcode_flavor },
}); });
@@ -182,3 +184,148 @@ TEST_CASE("The wipe tower's toolchange planner flush follows the gcode flavor",
CHECK_THAT(tower, !Catch::Matchers::ContainsSubstring(unexpected)); 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_polygon.cpp
test_mutable_polygon.cpp test_mutable_polygon.cpp
test_mutable_priority_queue.cpp test_mutable_priority_queue.cpp
test_minimum_spanning_tree.cpp
test_nozzle_volume_type.cpp test_nozzle_volume_type.cpp
test_step.cpp test_step.cpp
test_stl.cpp test_stl.cpp
@@ -39,6 +40,8 @@ add_executable(${_TEST_NAME}_tests
test_utils.cpp test_utils.cpp
test_timeutils.cpp test_timeutils.cpp
test_voronoi.cpp test_voronoi.cpp
test_wipe_tower_estimate.cpp
test_wipe_tower.cpp
test_optimizers.cpp test_optimizers.cpp
test_ordering_strategies.cpp test_ordering_strategies.cpp
# test_png_io.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));
}
}
}
@@ -6,6 +6,8 @@
#include "libslic3r/PresetBundle.hpp" #include "libslic3r/PresetBundle.hpp"
#include "libslic3r/AppConfig.hpp" #include "libslic3r/AppConfig.hpp"
#include "libslic3r/Utils.hpp"
#include "libslic3r/miniz_extension.hpp"
#include "test_utils.hpp" #include "test_utils.hpp"
@@ -1406,3 +1408,91 @@ TEST_CASE("Sizing down to the nozzle count plus mixes is what eats the mixed tai
CHECK(bundle.project_config.option<ConfigOptionStrings>("filament_mixed_components")->values[5] == "1,2"); CHECK(bundle.project_config.option<ConfigOptionStrings>("filament_mixed_components")->values[5] == "1,2");
} }
} }
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"));
}
}
+36
View File
@@ -2,6 +2,13 @@
#include "libslic3r/Utils.hpp" #include "libslic3r/Utils.hpp"
#include "test_utils.hpp"
#include <algorithm>
#include <cctype>
#include <fstream>
#include <string>
#ifndef _WIN32 #ifndef _WIN32
#include <unistd.h> // getuid #include <unistd.h> // getuid
#endif #endif
@@ -52,3 +59,32 @@ TEST_CASE("per-user temp root is unchanged on Windows, isolated elsewhere", "[ut
REQUIRE_THAT(root, Catch::Matchers::StartsWith(base + "/orcaslicer_")); REQUIRE_THAT(root, Catch::Matchers::StartsWith(base + "/orcaslicer_"));
#endif #endif
} }
TEST_CASE("copy_file reports the OS error when the destination cannot be written", "[utils]") {
ScopedTemporaryFile source(".txt");
{
std::ofstream ofs(source.string(), std::ios::binary);
ofs << "orca";
}
REQUIRE(boost::filesystem::exists(source.path()));
// A directory that was never created, so the copy fails on every platform.
const boost::filesystem::path destination = source.path().parent_path() / "orca-missing-dir" / "copy.txt";
REQUIRE_FALSE(boost::filesystem::exists(destination.parent_path()));
std::string error_message;
REQUIRE(copy_file(source.string(), destination.string(), error_message) == FAIL_COPY_FILE);
REQUIRE_FALSE(error_message.empty());
#ifdef _WIN32
// The Windows branch formats GetLastError() itself. Writing that as
// "Error: " + errCode adds an integer to a string literal, which indexes into the
// literal instead of appending and runs off its end for any code above 7.
const std::string prefix = "Error: ";
REQUIRE(error_message.rfind(prefix, 0) == 0);
const std::string code = error_message.substr(prefix.size());
REQUIRE_FALSE(code.empty());
REQUIRE(std::all_of(code.begin(), code.end(), [](unsigned char c) { return std::isdigit(c) != 0; }));
#endif // _WIN32
}
+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));
}