mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-16 21:42:43 +00:00
Merge main
This commit is contained in:
@@ -272,6 +272,8 @@ set(lisbslic3r_sources
|
||||
GCode/WipeTower2.hpp
|
||||
GCode/WipeTower.cpp
|
||||
GCode/WipeTower.hpp
|
||||
GCode/WipeTowerEstimate.cpp
|
||||
GCode/WipeTowerEstimate.hpp
|
||||
GCodeWriter.cpp
|
||||
GCodeWriter.hpp
|
||||
Geometry/ArcWelder.hpp
|
||||
|
||||
@@ -102,45 +102,6 @@ struct ZipUnicodePathExtraField
|
||||
}
|
||||
};
|
||||
|
||||
// Validate that a relative file path does not escape the root directory via path traversal.
|
||||
static bool is_path_within_root(const std::string& file_path, const boost::filesystem::path& root)
|
||||
{
|
||||
if (file_path.empty())
|
||||
return false;
|
||||
|
||||
boost::filesystem::path p(file_path);
|
||||
if (p.is_absolute())
|
||||
return false;
|
||||
|
||||
// Reject any path component that is ".."
|
||||
for (const auto& component : p) {
|
||||
if (component == "..")
|
||||
return false;
|
||||
}
|
||||
|
||||
// Resolve the full path and verify it starts with the canonical root (also catches symlink escapes)
|
||||
try {
|
||||
boost::filesystem::path full_path = root / p;
|
||||
boost::filesystem::path canonical_root = boost::filesystem::weakly_canonical(root);
|
||||
boost::filesystem::path canonical_full = boost::filesystem::weakly_canonical(full_path);
|
||||
|
||||
auto root_str = canonical_root.string();
|
||||
auto full_str = canonical_full.string();
|
||||
if (full_str.length() < root_str.length())
|
||||
return false;
|
||||
if (full_str.compare(0, root_str.length(), root_str) != 0)
|
||||
return false;
|
||||
// Ensure it's a proper prefix (not just a substring of a longer directory name)
|
||||
if (full_str.length() > root_str.length() &&
|
||||
full_str[root_str.length()] != boost::filesystem::path::preferred_separator)
|
||||
return false;
|
||||
} catch (const boost::filesystem::filesystem_error&) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// VERSION NUMBERS
|
||||
// 0 : .3mf, files saved by older slic3r or other applications. No version definition in them.
|
||||
// 1 : Introduction of 3mf versioning. No other change in data saved into 3mf files.
|
||||
|
||||
@@ -1630,6 +1630,94 @@ float WipeTower::get_auto_brim_by_height(float max_height) {
|
||||
return 8.f;
|
||||
}
|
||||
|
||||
float WipeTower::estimate_brim_real_width(float brim_width, float nozzle_diameter, float first_layer_height, bool type2)
|
||||
{
|
||||
if (brim_width <= 0.f)
|
||||
return brim_width;
|
||||
const float spacing = nozzle_diameter * 1.25f - first_layer_height * float(1. - M_PI_4); // Width_To_Nozzle_Ratio
|
||||
if (spacing <= EPSILON)
|
||||
return brim_width;
|
||||
const int loops_num = int((brim_width + spacing / 2.f) / spacing);
|
||||
return loops_num * spacing + (type2 ? 0.f : spacing / 2.f);
|
||||
}
|
||||
|
||||
float WipeTower::get_wrapping_detection_depth()
|
||||
{
|
||||
return float(wrapping_wipe_tower_depth);
|
||||
}
|
||||
|
||||
float WipeTower::nozzle_change_perimeter_width(float nozzle_diameter)
|
||||
{
|
||||
auto it = nozzle_diameter_to_nozzle_change_width.find(nozzle_diameter);
|
||||
return it != nozzle_diameter_to_nozzle_change_width.end() ? it->second : 2.f * nozzle_diameter * 1.25f;
|
||||
}
|
||||
|
||||
float WipeTower::estimate_tower_blocks_depth(const std::vector<PurgeEstimate> &purges, float width, float layer_height, float nozzle_diameter, float extra_spacing)
|
||||
{
|
||||
if (purges.empty() || layer_height < EPSILON || nozzle_diameter < EPSILON)
|
||||
return 0.f;
|
||||
const float pw = nozzle_diameter * 1.25f; // Width_To_Nozzle_Ratio
|
||||
const float ncpw = nozzle_change_perimeter_width(nozzle_diameter);
|
||||
const float line_width = width - 2.f * pw;
|
||||
if (line_width <= EPSILON)
|
||||
return 0.f;
|
||||
// Line cross-section as volume_to_length() sees it; the infill gap stretches the perimeter
|
||||
// width by the configured ratio and nozzle-change lines keep their own width
|
||||
// (calc_block_infill_gap).
|
||||
auto line_area = [layer_height](float w) { return layer_height * (w - layer_height * float(1. - M_PI_4)); };
|
||||
const float extra_width = (extra_spacing - 1.f) * pw;
|
||||
const float gap = pw + extra_width;
|
||||
const float nc_gap = ncpw + extra_width;
|
||||
// A layer purges into at most (filaments - 1) targets, so a category holding every filament
|
||||
// never sees its smallest purge (the layer's first filament) in its worst layer.
|
||||
struct Block { float depth = 0.f; float min_purge = 0.f; size_t filaments = 0; };
|
||||
std::map<int, Block> blocks;
|
||||
for (const PurgeEstimate &purge : purges) {
|
||||
Block &block = blocks[purge.category];
|
||||
const float purge_depth = std::ceil(purge.prime_volume / line_area(pw) / line_width) * gap;
|
||||
block.min_purge = block.filaments == 0 ? purge_depth : std::min(block.min_purge, purge_depth);
|
||||
block.depth += purge_depth;
|
||||
++block.filaments;
|
||||
if (purge.filament_change_length > EPSILON) {
|
||||
// The leaving filament is rammed over the nozzle-change flow, again in whole lines.
|
||||
const float filament_area = float(M_PI) * purge.filament_diameter * purge.filament_diameter / 4.f;
|
||||
const float nc_length = purge.filament_change_length * filament_area / line_area(ncpw);
|
||||
block.depth += std::ceil(nc_length / (width - ncpw - pw)) * nc_gap;
|
||||
}
|
||||
}
|
||||
float depth = pw; // plan_tower_new starts the first block one perimeter width in
|
||||
for (const auto &[category, block] : blocks)
|
||||
depth += block.filaments == purges.size() ? block.depth - block.min_purge : block.depth;
|
||||
return depth;
|
||||
}
|
||||
|
||||
float WipeTower::rib_footprint_side(float width, float depth, float rib_width, float extra_rib_length, float max_height)
|
||||
{
|
||||
if (width < EPSILON || depth < EPSILON)
|
||||
return 0.f;
|
||||
// Ribs run the diagonal; below the height-based minimum they are extended rather than the
|
||||
// body, then by the extra length, never ending up shorter than the diagonal.
|
||||
const float diagonal = std::sqrt(width * width + depth * depth);
|
||||
float rib_length = diagonal;
|
||||
if (depth + EPSILON < get_limit_depth_by_height(max_height))
|
||||
rib_length = std::max(rib_length, get_limit_depth_by_height(max_height) * float(std::sqrt(2.)));
|
||||
rib_length = std::max(diagonal, rib_length + extra_rib_length);
|
||||
// Half the extension at each end of the diagonal plus half the rib width, projected onto the axes.
|
||||
const float rib_w = std::min(rib_width, std::min(width, depth) / 2.f);
|
||||
const float per_side = ((rib_length - diagonal) / 2.f + rib_w / 2.f) / float(std::sqrt(2.));
|
||||
return std::max(width, depth) + 2.f * per_side;
|
||||
}
|
||||
|
||||
float WipeTower::estimate_rib_tower_bbox_side(const std::vector<PurgeEstimate> &purges, float width, float layer_height, float nozzle_diameter, float extra_spacing, float rib_width, float extra_rib_length, float max_height)
|
||||
{
|
||||
if (purges.empty() || width < EPSILON || layer_height < EPSILON || nozzle_diameter < EPSILON)
|
||||
return 0.f;
|
||||
const float pw = nozzle_diameter * 1.25f; // Width_To_Nozzle_Ratio
|
||||
const float square = align_ceil(std::sqrt(estimate_tower_blocks_depth(purges, width, layer_height, nozzle_diameter, extra_spacing) * width), pw);
|
||||
const float depth = estimate_tower_blocks_depth(purges, square, layer_height, nozzle_diameter, extra_spacing);
|
||||
return rib_footprint_side(square, depth, rib_width, extra_rib_length, max_height);
|
||||
}
|
||||
|
||||
Vec2f WipeTower::move_box_inside_polygon(const BoundingBox &box, const Polygons &polygons, coord_t offset)
|
||||
{
|
||||
if (polygons.empty()) return Vec2f{0.f, 0.f};
|
||||
|
||||
@@ -42,9 +42,36 @@ public:
|
||||
static const std::map<float, float> min_depth_per_height;
|
||||
static float get_limit_depth_by_height(float max_height);
|
||||
static float get_auto_brim_by_height(float max_height);
|
||||
// Both generators lay the brim in whole loops one line spacing apart, so the printed width
|
||||
// differs from the configured one. WipeTower reports it with half a spacing of line width
|
||||
// added, WipeTower2 reports the loops alone; an estimate has to round like the generator
|
||||
// whose G-code it stands in for.
|
||||
static float estimate_brim_real_width(float brim_width, float nozzle_diameter, float first_layer_height, bool type2);
|
||||
// Depth a Type1 tower reserves once nothing but wrapping detection asks for one.
|
||||
static float get_wrapping_detection_depth();
|
||||
// Line width of the nozzle-change purge lines at this nozzle diameter.
|
||||
static float nozzle_change_perimeter_width(float nozzle_diameter);
|
||||
static TriangleMesh its_make_rib_tower(float width, float depth, float height, float rib_length, float rib_width, bool fillet_wall);
|
||||
static TriangleMesh its_make_rib_brim(const Polygon& brim, float layer_height);
|
||||
static Polygon rib_section(float width, float depth, float rib_length, float rib_width, bool fillet_wall);
|
||||
// One filament's share of a Type1 tower layer, as plan_tower_new() reserves it.
|
||||
struct PurgeEstimate
|
||||
{
|
||||
float prime_volume = 0.f; // mm3 wiped after changing to this filament
|
||||
int category = 0; // filament_adhesiveness_category; one purge block per category
|
||||
float filament_change_length = 0.f; // mm of filament rammed when it leaves its nozzle; 0 when no nozzle change is planned
|
||||
float filament_diameter = 1.75f;
|
||||
};
|
||||
// Depth of the Type1 purge stack at the given width (also the rectangle-wall depth): each
|
||||
// purge is whole lines at the block infill gap, one block per adhesiveness category sized by
|
||||
// its worst layer, stacked behind one perimeter width.
|
||||
static float estimate_tower_blocks_depth(const std::vector<PurgeEstimate> &purges, float width, float layer_height, float nozzle_diameter, float extra_spacing);
|
||||
// Side of the square bounding a rib-wall tower's first layer, brim excluded: the body plus the
|
||||
// rib bulge, with the ribs extended to the height-based minimum as both generators do.
|
||||
static float rib_footprint_side(float width, float depth, float rib_width, float extra_rib_length, float max_height);
|
||||
// Type1 rib tower: plan_tower_new() squares the tower from the depth at the configured width,
|
||||
// then re-plans the depth at the squared width.
|
||||
static float estimate_rib_tower_bbox_side(const std::vector<PurgeEstimate> &purges, float width, float layer_height, float nozzle_diameter, float extra_spacing, float rib_width, float extra_rib_length, float max_height);
|
||||
// Translation that brings a footprint inside the printable outline, padded by offset. The prime
|
||||
// tower is validated against the real outline (see layered_print_cleareance_valid), so clamping
|
||||
// against the bounding box alone would leave it off a delta or hexagonal bed. box and polygons
|
||||
|
||||
@@ -2129,6 +2129,23 @@ std::pair<double, double> WipeTower2::get_wipe_tower_cone_base(double width, dou
|
||||
return std::make_pair(R, support_scale);
|
||||
}
|
||||
|
||||
Polygon WipeTower2::cone_base_polygon(double width, double depth, double height, double angle_deg)
|
||||
{
|
||||
Polygon box({Point::new_scale(Vec2d(0., 0.)), Point::new_scale(Vec2d(width, 0.)),
|
||||
Point::new_scale(Vec2d(width, depth)), Point::new_scale(Vec2d(0., depth))});
|
||||
if (angle_deg <= EPSILON || height <= EPSILON || width <= EPSILON || depth <= EPSILON)
|
||||
return box;
|
||||
const auto [R, x_scale] = get_wipe_tower_cone_base(width, height, depth, angle_deg);
|
||||
if (R <= EPSILON)
|
||||
return box;
|
||||
const Vec2d center(width / 2., depth / 2.);
|
||||
Polygon ellipse;
|
||||
for (double alpha = 0.; alpha < 2. * M_PI; alpha += M_PI / 20.)
|
||||
ellipse.points.push_back(Point::new_scale(center + R * Vec2d(std::cos(alpha) / x_scale, std::sin(alpha))));
|
||||
Polygons u = union_({box, ellipse});
|
||||
return u.empty() ? box : u.front();
|
||||
}
|
||||
|
||||
// Static method to extract wipe_volumes[from][to] from the configuration.
|
||||
// Takes a ConfigBase so the GUI's wipe tower size estimate can pass the plate's
|
||||
// DynamicPrintConfig directly instead of materializing a full PrintConfig per call.
|
||||
|
||||
@@ -27,6 +27,10 @@ public:
|
||||
// in WipeTowerIntegration::append_tcr2 does not strip it.
|
||||
static const std::string wait_for_temp_tag() { return ";_WAIT_FOR_TEMP_ON_WIPE_TOWER"; }
|
||||
static std::pair<double, double> get_wipe_tower_cone_base(double width, double height, double depth, double angle_deg);
|
||||
// First-layer outline of a cone-wall tower in tower-local (scaled) coordinates: body box
|
||||
// unioned with the cone's base ellipse — the model first_layer_wipe_tower_corners uses,
|
||||
// and generate_support_cone_wall stays within it. Brim not included.
|
||||
static Polygon cone_base_polygon(double width, double depth, double height, double angle_deg);
|
||||
static std::vector<std::vector<float>> extract_wipe_volumes(const ConfigBase& config);
|
||||
// Estimated total flush volume of a SEMM print with the given number of filaments,
|
||||
// used to reserve wipe tower space before the tower is generated.
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -60,10 +60,15 @@ auto MinimumSpanningTree::prim(std::vector<Point> vertices) const -> AdjacencyGr
|
||||
//This search is O(V) right now, which can be made down to O(log(V)). This reduces the overall time complexity from O(V*V) to O(V*log(E)).
|
||||
//However that requires an implementation of a heap that supports the decreaseKey operation, which is not in the std library.
|
||||
//TODO: Implement this?
|
||||
// Break equal-distance ties on coordinates: the map is keyed by address, so its
|
||||
// iteration order (and therefore the first minimum) would otherwise depend on where
|
||||
// the vertices were allocated.
|
||||
using MapValue = std::pair<const Point*, coordf_t>;
|
||||
const auto closest = std::min_element(smallest_distance.begin(), smallest_distance.end(),
|
||||
[](const MapValue& a, const MapValue& b) {
|
||||
return a.second < b.second;
|
||||
if (a.second != b.second)
|
||||
return a.second < b.second;
|
||||
return *a.first < *b.first;
|
||||
});
|
||||
|
||||
//Add this point to the graph and remove it from the candidates.
|
||||
|
||||
@@ -1635,6 +1635,12 @@ PresetsConfigSubstitutions PresetBundle::import_presets(std::vector<std::string>
|
||||
metadata.id = to_string(uuid);
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " bundle_id was empty, so generating a UUID: " << metadata.id;
|
||||
}
|
||||
if (has_bundle_structure && !is_path_within_root(metadata.id, user_folder / user_id / PRESET_LOCAL_DIR)) {
|
||||
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " bundle id escapes the bundle directory, not importing: " << metadata.id;
|
||||
fclose(zipFile);
|
||||
fs::remove_all(temp_folder, ec);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Build bundle directory path based on whether bundle_structure.json was present
|
||||
fs::path bundle_base_dir;
|
||||
@@ -1657,11 +1663,15 @@ PresetsConfigSubstitutions PresetBundle::import_presets(std::vector<std::string>
|
||||
if (status) {
|
||||
std::string file_name = file_stat.m_filename;
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " From zip file: " << file << ". Read file name: " << file_stat.m_filename;
|
||||
size_t index = file_name.find_last_of('/');
|
||||
size_t index = file_name.find_last_of("/\\");
|
||||
if (std::string::npos != index) {
|
||||
file_name = file_name.substr(index + 1);
|
||||
}
|
||||
if (BUNDLE_STRUCTURE_JSON_NAME == file_name) continue;
|
||||
if (!is_path_within_root(file_name, temp_folder)) {
|
||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << " zip entry escapes the temp directory, skipping: " << file_stat.m_filename;
|
||||
continue;
|
||||
}
|
||||
// create target file path
|
||||
std::string target_file_path = boost::filesystem::path(temp_folder / file_name).make_preferred().string();
|
||||
|
||||
@@ -1750,6 +1760,10 @@ bool PresetBundle::import_json_presets(PresetsConfigSubstitutions & s
|
||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << " Preset type is unknown, not loading: " << name;
|
||||
return false;
|
||||
}
|
||||
if (!is_path_within_root(name, fs::path(collection->m_dir_path))) {
|
||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << " Preset name escapes the preset directory, not loading: " << name;
|
||||
return false;
|
||||
}
|
||||
const PresetOrigin load_origin = detect_origin_from_path(boost::filesystem::path(bundle_dir));
|
||||
const std::string preset_name = get_preset_canonical_name(name, load_origin);
|
||||
|
||||
|
||||
+104
-98
@@ -20,6 +20,7 @@
|
||||
#include "GCode.hpp"
|
||||
#include "GCode/WipeTower.hpp"
|
||||
#include "GCode/WipeTower2.hpp"
|
||||
#include "GCode/WipeTowerEstimate.hpp"
|
||||
#include "Utils.hpp"
|
||||
#include "PrintConfig.hpp"
|
||||
#include "MaterialType.hpp"
|
||||
@@ -1031,20 +1032,21 @@ static StringObjectException layered_print_cleareance_valid(const Print &print,
|
||||
|
||||
//BBS: add the wipe tower check logic
|
||||
const PrintConfig & config = print.config();
|
||||
int filaments_count = print.extruders().size();
|
||||
// Custom G-code tool changes (MultiAsSingle) build a real tower on a plate whose objects
|
||||
// all use one filament, so they have to be counted or the hull below collapses to a point.
|
||||
int filaments_count = print.extruders(true).size();
|
||||
int plate_index = print.get_plate_index();
|
||||
const Vec3d plate_origin = print.get_plate_origin();
|
||||
float x = config.wipe_tower_x.get_at(plate_index) + plate_origin(0);
|
||||
float y = config.wipe_tower_y.get_at(plate_index) + plate_origin(1);
|
||||
float width = config.prime_tower_width.value;
|
||||
float a = config.wipe_tower_rotation_angle.value;
|
||||
//float v = config.wiping_volume.value;
|
||||
|
||||
float depth = print.wipe_tower_data(filaments_count).depth;
|
||||
//float brim_width = print.wipe_tower_data(filaments_count).brim_width;
|
||||
|
||||
if (config.wipe_tower_wall_type.value == WipeTowerWallType::wtwRib)
|
||||
width = depth;
|
||||
// The estimate resolves the effective width (a rib wall squares the tower).
|
||||
const WipeTowerData &wipe_tower_estimate = print.wipe_tower_data(filaments_count);
|
||||
float width = wipe_tower_estimate.width;
|
||||
float depth = wipe_tower_estimate.depth;
|
||||
float brim_width = wipe_tower_estimate.brim_width;
|
||||
|
||||
Polygons convex_hulls_temp;
|
||||
if (print.has_wipe_tower()) {
|
||||
@@ -1066,36 +1068,54 @@ static StringObjectException layered_print_cleareance_valid(const Print &print,
|
||||
convex_hulls_temp.push_back(wipe_tower_polygon);
|
||||
}
|
||||
}
|
||||
// Post-generation the mesh bottom already carries the brim. Pre-generation the body grows
|
||||
// by the brim only when its width is explicit; the auto brim and a Type2 cone base depend on
|
||||
// the tower height, exact only once generated, so they only warn here - the exact footprint
|
||||
// is re-checked in _make_wipe_tower.
|
||||
const bool exact_footprint = print.is_step_done(psWipeTower);
|
||||
Polygons tower_polys_checked = (!exact_footprint && config.prime_tower_brim_width.value >= 0) ?
|
||||
offset(convex_hulls_temp, float(scale_(brim_width))) :
|
||||
convex_hulls_temp;
|
||||
Polygons tower_polys_estimated;
|
||||
if (!exact_footprint && !convex_hulls_temp.empty()) {
|
||||
double max_height = 0.;
|
||||
for (const PrintObject *object : print.objects())
|
||||
max_height = std::max(max_height, unscale_(object->size().z()));
|
||||
Polygon base = estimate_wipe_tower_first_layer_outline(config, print.wipe_tower_type(), width, depth, max_height);
|
||||
base.rotate(Geometry::deg2rad(a));
|
||||
base.translate(Point(scale_(x), scale_(y)));
|
||||
tower_polys_estimated = offset(base, float(scale_(brim_width)));
|
||||
}
|
||||
// Object proximity stays a body-only warning: brim near-misses would newly warn on
|
||||
// many setups that print fine.
|
||||
if (!intersection(convex_hulls_other, convex_hulls_temp).empty()) {
|
||||
if (warning) {
|
||||
warning->string += L("Prime Tower") + L(" is too close to others, and collisions may be caused.\n");
|
||||
}
|
||||
}
|
||||
if (!intersection(exclude_polys, convex_hulls_temp).empty()) {
|
||||
/*if (warning) {
|
||||
warning->string += L("Prime Tower is too close to exclusion area, there may be collisions when printing.\n");
|
||||
}*/
|
||||
if (!intersection(exclude_polys, tower_polys_checked).empty()) {
|
||||
return {L("Prime Tower") + L(" is too close to an exclusion area, and collisions will be caused.\n")};
|
||||
}
|
||||
if (print_config.enable_wrapping_detection.value && !intersection({wrapping_poly}, convex_hulls_temp).empty()) {
|
||||
if (print_config.enable_wrapping_detection.value && !intersection({wrapping_poly}, tower_polys_checked).empty()) {
|
||||
return {L("Prime Tower") + L(" is too close to clumping detection area, and collisions will be caused.\n")};
|
||||
}
|
||||
// Skip the containment check for towers that will never be printed (single-filament
|
||||
// prints without smooth timelapse keep the config's tower position but emit nothing).
|
||||
// Pre-generation only the body square is tested — the auto-brim estimate can overshoot
|
||||
// the generated brim by several mm and must not hard-fail a print that physically fits.
|
||||
// Post-generation the mesh bottom already includes the real brim, so the exact
|
||||
// footprint is tested.
|
||||
if (filaments_count > 1 || print.enable_timelapse_print()) {
|
||||
// The shared printable polygon is plate-local, while the tower polygons above are
|
||||
// already shifted by the plate origin.
|
||||
Polygons printable_polys = print.get_extruder_shared_printable_polygon();
|
||||
const Point plate_shift(scale_(plate_origin.x()), scale_(plate_origin.y()));
|
||||
for (Polygon &p : printable_polys)
|
||||
p.translate(plate_shift);
|
||||
if (!diff(convex_hulls_temp, printable_polys).empty())
|
||||
return {L("Prime Tower") + L(" is partially outside the printable area, and it cannot be printed.\n")};
|
||||
if (warning && !intersection(exclude_polys, tower_polys_estimated).empty()) {
|
||||
warning->string += L("Prime Tower") + L(" is too close to exclusion area, there may be collisions when printing.") + "\n";
|
||||
}
|
||||
if (warning && print_config.enable_wrapping_detection.value && !intersection({wrapping_poly}, tower_polys_estimated).empty()) {
|
||||
warning->string += L("Prime Tower") + L(" is too close to clumping detection area, there may be collisions when printing.") + "\n";
|
||||
}
|
||||
// No gate on "is there a tower": one that is not printed estimates to zero, so the hulls
|
||||
// are degenerate and every check passes. Re-deriving it here missed the wrapping-detection
|
||||
// tower on a single-filament plate.
|
||||
Polygons printable_polys = print.get_extruder_shared_printable_polygon();
|
||||
const Point plate_shift(scale_(plate_origin.x()), scale_(plate_origin.y()));
|
||||
for (Polygon &p : printable_polys)
|
||||
p.translate(plate_shift);
|
||||
if (!diff(tower_polys_checked, printable_polys).empty())
|
||||
return {L("Prime Tower") + L(" is partially outside the printable area, and it cannot be printed.\n")};
|
||||
if (warning && !diff(tower_polys_estimated, printable_polys).empty())
|
||||
warning->string += L("Prime Tower") + L(" is partially outside the printable area, and it cannot be printed.\n");
|
||||
return {};
|
||||
}
|
||||
|
||||
@@ -3997,74 +4017,25 @@ bool Print::has_wipe_tower() const
|
||||
|
||||
const WipeTowerData &Print::wipe_tower_data(size_t filaments_cnt) const
|
||||
{
|
||||
// If the wipe tower wasn't created yet, make sure the depth and brim_width members are set to default.
|
||||
double max_height = 0;
|
||||
for (size_t obj_idx = 0; obj_idx < m_objects.size(); obj_idx++) {
|
||||
double object_z = (double) m_objects[obj_idx]->size().z();
|
||||
max_height = std::max(unscale_(object_z), max_height);
|
||||
// Until the tower is generated, size it with the estimate the GUI/CLI placement uses, so
|
||||
// validation cannot reject a position the clamp just accepted.
|
||||
if (is_step_done(psWipeTower) || filaments_cnt == 0)
|
||||
return m_wipe_tower_data;
|
||||
|
||||
double max_height = 0.;
|
||||
double layer_height = std::numeric_limits<double>::max();
|
||||
for (const PrintObject *object : m_objects) {
|
||||
max_height = std::max(max_height, unscale_(double(object->size().z())));
|
||||
layer_height = std::min(layer_height, object->config().layer_height.value);
|
||||
}
|
||||
if (max_height < EPSILON) return m_wipe_tower_data;
|
||||
if (max_height < EPSILON)
|
||||
return m_wipe_tower_data;
|
||||
|
||||
double layer_height = 0.08f; // hard code layer height
|
||||
layer_height = m_objects.front()->config().layer_height.value;
|
||||
|
||||
auto timelapse_type = config().option<ConfigOptionEnum<TimelapseType>>("timelapse_type");
|
||||
bool need_wipe_tower = (timelapse_type ? (timelapse_type->value == TimelapseType::tlSmooth) : false) | (m_config.wipe_tower_wall_type.value == WipeTowerWallType::wtwRib);
|
||||
double extra_spacing = config().option("prime_tower_infill_gap")->getFloat() / 100.;
|
||||
double rib_width = config().option("wipe_tower_rib_width")->getFloat();
|
||||
|
||||
double filament_change_volume = 0.;
|
||||
{
|
||||
std::vector<double> filament_change_lengths;
|
||||
auto filament_change_lengths_opt = config().option<ConfigOptionFloats>("filament_change_length");
|
||||
if (filament_change_lengths_opt) filament_change_lengths = filament_change_lengths_opt->values;
|
||||
double length = filament_change_lengths.empty() ? 0 : *std::max_element(filament_change_lengths.begin(), filament_change_lengths.end());
|
||||
double diameter = 1.75;
|
||||
std::vector<double> diameters;
|
||||
auto filament_diameter_opt = config().option<ConfigOptionFloats>("filament_diameter");
|
||||
if (filament_diameter_opt) diameters = filament_diameter_opt->values;
|
||||
diameter = diameters.empty() ? diameter : *std::max_element(diameters.begin(), diameters.end());
|
||||
filament_change_volume = length * PI * diameter * diameter / 4.;
|
||||
}
|
||||
|
||||
|
||||
if (! is_step_done(psWipeTower) && filaments_cnt !=0) {
|
||||
double wipe_volume = m_config.prime_volume;
|
||||
int filament_depth_count = m_config.nozzle_diameter.values.size() == 2 ? filaments_cnt : filaments_cnt - 1;
|
||||
if (filaments_cnt == 1 && enable_timelapse_print()) filament_depth_count = 1;
|
||||
double volume = wipe_volume * filament_depth_count;
|
||||
if (m_config.nozzle_diameter.values.size() == 2) volume += filament_change_volume * (int) (filaments_cnt / 2);
|
||||
|
||||
// Sizing should take into account currently set wiping volumes.
|
||||
// For a long time, the initial preview would just use 900/width per toolchange (15mm on a 60mm wide tower)
|
||||
// and it worked well enough. Let's try to do slightly better by accounting for the purging volumes.
|
||||
const bool semm_flush = m_config.purge_in_prime_tower && m_config.single_extruder_multi_material;
|
||||
if (semm_flush) volume = WipeTower2::estimate_semm_flush_volume(m_config, filaments_cnt);
|
||||
|
||||
if (m_config.wipe_tower_wall_type.value == WipeTowerWallType::wtwRib) {
|
||||
double depth = std::sqrt(volume / layer_height * extra_spacing);
|
||||
if (need_wipe_tower || filaments_cnt > 1) {
|
||||
float min_wipe_tower_depth = WipeTower::get_limit_depth_by_height(max_height);
|
||||
depth = std::max((double) min_wipe_tower_depth, depth);
|
||||
depth += rib_width / std::sqrt(2) + config().wipe_tower_extra_rib_length.value;
|
||||
const_cast<Print *>(this)->m_wipe_tower_data.depth = depth;
|
||||
const_cast<Print *>(this)->m_wipe_tower_data.brim_width = m_config.prime_tower_brim_width;
|
||||
}
|
||||
}
|
||||
else {
|
||||
double width = m_config.prime_tower_width;
|
||||
double depth = volume / (layer_height * width);
|
||||
// The flush volumes already hold the spacing between wipes.
|
||||
if (!semm_flush) depth *= extra_spacing;
|
||||
if (need_wipe_tower || depth > EPSILON) {
|
||||
float min_wipe_tower_depth = WipeTower::get_limit_depth_by_height(max_height);
|
||||
depth = std::max((double) min_wipe_tower_depth, depth);
|
||||
}
|
||||
const_cast<Print *>(this)->m_wipe_tower_data.depth = depth;
|
||||
const_cast<Print *>(this)->m_wipe_tower_data.brim_width = m_config.prime_tower_brim_width;
|
||||
}
|
||||
if (m_config.prime_tower_brim_width < 0) const_cast<Print *>(this)->m_wipe_tower_data.brim_width = WipeTower::get_auto_brim_by_height(max_height);
|
||||
}
|
||||
const WipeTowerFootprint footprint = estimate_wipe_tower_footprint(m_config, this->wipe_tower_type(), this->extruders(true), layer_height, max_height);
|
||||
WipeTowerData &data = const_cast<Print *>(this)->m_wipe_tower_data;
|
||||
data.depth = float(footprint.depth);
|
||||
data.width = float(footprint.width);
|
||||
data.brim_width = float(footprint.brim_width);
|
||||
return m_wipe_tower_data;
|
||||
}
|
||||
|
||||
@@ -4290,6 +4261,7 @@ void Print::_make_wipe_tower()
|
||||
m_wipe_tower_data.tool_changes.reserve(m_wipe_tower_data.tool_ordering.layer_tools().size());
|
||||
wipe_tower.generate_new(m_wipe_tower_data.tool_changes);
|
||||
m_wipe_tower_data.depth = wipe_tower.get_depth();
|
||||
m_wipe_tower_data.width = wipe_tower.width();
|
||||
m_wipe_tower_data.brim_width = wipe_tower.get_brim_width();
|
||||
m_wipe_tower_data.bbx = wipe_tower.get_bbx();
|
||||
m_wipe_tower_data.rib_offset = wipe_tower.get_rib_offset();
|
||||
@@ -4403,6 +4375,7 @@ void Print::_make_wipe_tower()
|
||||
m_wipe_tower_data.tool_changes.reserve(m_wipe_tower_data.tool_ordering.layer_tools().size());
|
||||
wipe_tower.generate(m_wipe_tower_data.tool_changes);
|
||||
m_wipe_tower_data.depth = wipe_tower.get_depth();
|
||||
m_wipe_tower_data.width = wipe_tower.width();
|
||||
m_wipe_tower_data.z_and_depth_pairs = wipe_tower.get_z_and_depth_pairs();
|
||||
m_wipe_tower_data.brim_width = wipe_tower.get_brim_width();
|
||||
m_wipe_tower_data.height = wipe_tower.get_wipe_tower_height();
|
||||
@@ -4438,7 +4411,9 @@ void Print::_make_wipe_tower()
|
||||
wipe_tower.get_wipe_tower_height(), wipe_tower.get_brim_width(),
|
||||
config().wipe_tower_wall_type.value == WipeTowerWallType::wtwRib,
|
||||
wipe_tower.get_rib_width(), wipe_tower.get_rib_length(),
|
||||
config().wipe_tower_fillet_wall.value);
|
||||
config().wipe_tower_fillet_wall.value,
|
||||
config().wipe_tower_wall_type.value == WipeTowerWallType::wtwCone ?
|
||||
(float) config().wipe_tower_cone_angle.value : 0.f);
|
||||
const Vec3d origin = Vec3d::Zero();
|
||||
// FakeWipeTower::pos is a bed-frame translation applied after rotation
|
||||
// (getFakeExtrusionPathsFromWipeTower2 rotates about the local origin), so the
|
||||
@@ -4451,6 +4426,28 @@ void Print::_make_wipe_tower()
|
||||
config().wipe_tower_rotation_angle, config().wipe_tower_cone_angle,
|
||||
{scale_(origin.x()), scale_(origin.y())});
|
||||
}
|
||||
|
||||
// The clamps and checks above work from estimates; re-test the exact generated footprint
|
||||
// so an off-plate tower fails with a clear error instead of exporting unprintable G-code
|
||||
// (validate() only sees the mesh on its next run).
|
||||
if (m_wipe_tower_data.wipe_tower_mesh_data) {
|
||||
Polygon footprint = m_wipe_tower_data.wipe_tower_mesh_data->bottom; // includes brim and rib offset
|
||||
footprint.rotate(Geometry::deg2rad(m_config.wipe_tower_rotation_angle.value));
|
||||
footprint.translate(Point(scale_(m_config.wipe_tower_x.get_at(m_plate_index)),
|
||||
scale_(m_config.wipe_tower_y.get_at(m_plate_index))));
|
||||
const Polygons printable_polys = this->get_extruder_shared_printable_polygon();
|
||||
if (!printable_polys.empty() && !diff(Polygons{footprint}, printable_polys).empty()) {
|
||||
const BoundingBox fp = get_extents(footprint);
|
||||
const BoundingBox pr = get_extents(printable_polys);
|
||||
BOOST_LOG_TRIVIAL(error) << boost::format("wipe tower footprint [%1%,%2%]-[%3%,%4%] leaves printable [%5%,%6%]-[%7%,%8%]") %
|
||||
unscaled(fp.min.x()) % unscaled(fp.min.y()) % unscaled(fp.max.x()) % unscaled(fp.max.y()) %
|
||||
unscaled(pr.min.x()) % unscaled(pr.min.y()) % unscaled(pr.max.x()) % unscaled(pr.max.y());
|
||||
throw Slic3r::SlicingError(L("Prime Tower") + L(" is partially outside the printable area, and it cannot be printed.\n"));
|
||||
}
|
||||
// The cutter/purge corner is a physical obstacle — the brim must stay out like the body.
|
||||
if (!intersection(get_bed_excluded_area(m_config), Polygons{footprint}).empty())
|
||||
throw Slic3r::SlicingError(L("Prime Tower") + L(" is too close to an exclusion area, and collisions will be caused.\n"));
|
||||
}
|
||||
}
|
||||
|
||||
// Generate a recommended G-code output file name based on the format template, default extension, and template parameters
|
||||
@@ -5999,17 +5996,26 @@ ExtrusionLayers FakeWipeTower::getTrueExtrusionLayersFromWipeTower() const
|
||||
}
|
||||
return wtels;
|
||||
}
|
||||
void WipeTowerData::construct_mesh(float width, float depth, float height, float brim_width, bool is_rib_wipe_tower, float rib_width, float rib_length,bool fillet_wall)
|
||||
void WipeTowerData::construct_mesh(float width, float depth, float height, float brim_width, bool is_rib_wipe_tower, float rib_width, float rib_length,bool fillet_wall, float cone_angle)
|
||||
{
|
||||
wipe_tower_mesh_data = WipeTowerMeshData{};
|
||||
float first_layer_height=0.08; //brim height
|
||||
if (width < EPSILON || depth < EPSILON || height < EPSILON) return;
|
||||
if (!is_rib_wipe_tower || rib_length < EPSILON) {
|
||||
if (cone_angle > EPSILON && (!is_rib_wipe_tower || rib_length < EPSILON)) {
|
||||
// Cone tower: the base bulges past the body box; this bottom polygon feeds the
|
||||
// containment checks, so it must carry the bulge and the brim (cone not lofted).
|
||||
wipe_tower_mesh_data->real_wipe_tower_mesh = make_cube(width, depth, height);
|
||||
wipe_tower_mesh_data->bottom = WipeTower2::cone_base_polygon(width, depth, height, cone_angle);
|
||||
auto brim_bottom = offset(wipe_tower_mesh_data->bottom, scaled(brim_width));
|
||||
if (!brim_bottom.empty())
|
||||
wipe_tower_mesh_data->bottom = brim_bottom.front();
|
||||
wipe_tower_mesh_data->real_brim_mesh = WipeTower::its_make_rib_brim(wipe_tower_mesh_data->bottom, first_layer_height);
|
||||
} else if (!is_rib_wipe_tower || rib_length < EPSILON) {
|
||||
wipe_tower_mesh_data->real_wipe_tower_mesh = make_cube(width, depth, height);
|
||||
wipe_tower_mesh_data->real_brim_mesh = make_cube(width + 2 * brim_width, depth + 2 * brim_width, first_layer_height);
|
||||
wipe_tower_mesh_data->real_brim_mesh.translate({-brim_width, -brim_width, 0});
|
||||
wipe_tower_mesh_data->bottom = {scaled(Vec2f{-brim_width, -brim_width}), scaled(Vec2f{width + brim_width, 0}), scaled(Vec2f{width + brim_width, depth + brim_width}),
|
||||
scaled(Vec2f{0, depth})};
|
||||
wipe_tower_mesh_data->bottom = {scaled(Vec2f{-brim_width, -brim_width}), scaled(Vec2f{width + brim_width, -brim_width}),
|
||||
scaled(Vec2f{width + brim_width, depth + brim_width}), scaled(Vec2f{-brim_width, depth + brim_width})};
|
||||
} else {
|
||||
wipe_tower_mesh_data->real_wipe_tower_mesh = WipeTower::its_make_rib_tower(width, depth, height, rib_length, rib_width, fillet_wall);
|
||||
wipe_tower_mesh_data->bottom = WipeTower::rib_section(width, depth, rib_length, rib_width, fillet_wall);
|
||||
|
||||
@@ -782,6 +782,9 @@ struct WipeTowerData
|
||||
|
||||
// Depth of the wipe tower to pass to GLCanvas3D for exact bounding box:
|
||||
float depth;
|
||||
// Effective width (a rib wall squares the tower): the estimate until generation, then the
|
||||
// generated width, so it never disagrees with depth.
|
||||
float width;
|
||||
std::vector<std::pair<float, float>> z_and_depth_pairs;
|
||||
float brim_width;
|
||||
float height;
|
||||
@@ -795,12 +798,13 @@ struct WipeTowerData
|
||||
used_filament.clear();
|
||||
number_of_toolchanges = -1;
|
||||
depth = 0.f;
|
||||
width = 0.f;
|
||||
brim_width = 0.f;
|
||||
height = 0.f;
|
||||
rib_offset = Vec2f::Zero();
|
||||
wipe_tower_mesh_data = std::nullopt;
|
||||
}
|
||||
void construct_mesh(float width, float depth, float height, float brim_width, bool is_rib_wipe_tower, float rib_width, float rib_length, bool fillet_wall);
|
||||
void construct_mesh(float width, float depth, float height, float brim_width, bool is_rib_wipe_tower, float rib_width, float rib_length, bool fillet_wall, float cone_angle = 0.f);
|
||||
|
||||
private:
|
||||
// Only allow the WipeTowerData to be instantiated internally by Print,
|
||||
|
||||
@@ -2846,7 +2846,9 @@ void TreeSupport::drop_nodes()
|
||||
const MinimumSpanningTree& mst = spanning_trees[group_index];
|
||||
//In the first pass, merge all nodes that are close together.
|
||||
std::vector<std::pair<const Point, SupportNode*>> nodes_vec(nodes_this_part.begin(), nodes_this_part.end());
|
||||
tbb::parallel_for_each(nodes_vec.begin(), nodes_vec.end(), [&](const std::pair<const Point, SupportNode*>& entry) {
|
||||
// Sequential: nodes merge into and invalidate each other in place, so parallel execution
|
||||
// makes the merge order (and thus the result) depend on thread scheduling.
|
||||
std::for_each(nodes_vec.begin(), nodes_vec.end(), [&](const std::pair<const Point, SupportNode*>& entry) {
|
||||
SupportNode* p_node = entry.second;
|
||||
SupportNode& node = *p_node;
|
||||
if (!p_node->valid)
|
||||
@@ -2934,7 +2936,32 @@ void TreeSupport::drop_nodes()
|
||||
);
|
||||
|
||||
//In the second pass, move all middle nodes.
|
||||
tbb::parallel_for_each(nodes_vec.begin(), nodes_vec.end(), [&](const std::pair<const Point, SupportNode*>& entry) {
|
||||
// Still parallel: this pass only reads other nodes. Side effects (invalidation, new
|
||||
// nodes, contact_nodes/unsupported_branch_leaves updates) are recorded per node and
|
||||
// applied afterwards in node order. Node creation must be deferred too, since
|
||||
// SupportNode's constructor writes `parent->child = this` on other nodes.
|
||||
struct PendingNode {
|
||||
Point position;
|
||||
int distance_to_top = 0;
|
||||
int support_roof_layers_below = 0;
|
||||
bool to_buildplate = false;
|
||||
SupportNode *parent = nullptr;
|
||||
bool zero_max_move = false;
|
||||
bool has_overhang = false;
|
||||
ExPolygon overhang;
|
||||
bool clamp_radius = false;
|
||||
coordf_t parent_radius = 0;
|
||||
double dist_to_outer = 0;
|
||||
};
|
||||
struct PassTwoResult {
|
||||
bool invalidate = false;
|
||||
bool unsupported_leaf = false;
|
||||
std::vector<PendingNode> pending;
|
||||
};
|
||||
std::vector<PassTwoResult> pass2_results(nodes_vec.size());
|
||||
auto pass2_body = [&](size_t node_idx) {
|
||||
const std::pair<const Point, SupportNode*>& entry = nodes_vec[node_idx];
|
||||
PassTwoResult& pass2_out = pass2_results[node_idx];
|
||||
|
||||
SupportNode* p_node = entry.second;
|
||||
const SupportNode& node = *p_node;
|
||||
@@ -2949,14 +2976,16 @@ void TreeSupport::drop_nodes()
|
||||
ExPolygons overhangs_next = diff_clipped({ node.overhang }, get_collision(0, obj_layer_nr_next));
|
||||
for(auto& overhang:overhangs_next) {
|
||||
Point next_pt = overhang.contour.centroid();
|
||||
SupportNode *next_node = m_ts_data->create_node(next_pt, p_node->distance_to_top + 1, obj_layer_nr_next,
|
||||
p_node->support_roof_layers_below - (p_node->distance_to_top >= 0 ? 1 : 0),
|
||||
to_buildplate, p_node, print_z_next, height_next);
|
||||
next_node->max_move_dist = 0;
|
||||
next_node->overhang = std::move(overhang);
|
||||
m_ts_data->m_mutex.lock();
|
||||
contact_nodes[layer_nr_next].emplace_back(next_node);
|
||||
m_ts_data->m_mutex.unlock();
|
||||
PendingNode pending;
|
||||
pending.position = next_pt;
|
||||
pending.distance_to_top = p_node->distance_to_top + 1;
|
||||
pending.support_roof_layers_below = p_node->support_roof_layers_below - (p_node->distance_to_top >= 0 ? 1 : 0);
|
||||
pending.to_buildplate = to_buildplate;
|
||||
pending.parent = p_node;
|
||||
pending.zero_max_move = true;
|
||||
pending.has_overhang = true;
|
||||
pending.overhang = std::move(overhang);
|
||||
pass2_out.pending.emplace_back(std::move(pending));
|
||||
|
||||
}
|
||||
return;
|
||||
@@ -2973,17 +3002,17 @@ void TreeSupport::drop_nodes()
|
||||
{
|
||||
if (support_on_buildplate_only)
|
||||
{
|
||||
unsupported_branch_leaves.push_front({ layer_nr, p_node });
|
||||
pass2_out.unsupported_leaf = true;
|
||||
}
|
||||
else {
|
||||
p_node->valid = false;
|
||||
pass2_out.invalidate = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
// if the link between parent and current is cut by contours, mark current as bottom contact node
|
||||
if (p_node->parent && intersection_ln({p_node->position, p_node->parent->position}, layer_contours).empty()==false)
|
||||
{
|
||||
p_node->valid = false;
|
||||
pass2_out.invalidate = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -3096,20 +3125,47 @@ void TreeSupport::drop_nodes()
|
||||
}
|
||||
auto next_collision = get_collision(0, obj_layer_nr_next);
|
||||
const bool to_buildplate = !is_inside_ex(m_ts_data->m_layer_outlines[obj_layer_nr_next], next_layer_vertex);
|
||||
SupportNode * next_node = m_ts_data->create_node(next_layer_vertex, node.distance_to_top + 1, obj_layer_nr_next,
|
||||
node.support_roof_layers_below - (node.distance_to_top >= 0 ? 1 : 0),
|
||||
to_buildplate, p_node, print_z_next, height_next);
|
||||
// don't increase radius if next node will collide partially with the object (STUDIO-7883)
|
||||
to_outside = projection_onto(next_collision, next_node->position);
|
||||
to_outside = projection_onto(next_collision, next_layer_vertex);
|
||||
direction_to_outer = to_outside - node.position;
|
||||
double dist_to_outer = unscale_(direction_to_outer.cast<double>().norm());
|
||||
next_node->radius = std::max(node.radius, std::min(next_node->radius, dist_to_outer));
|
||||
get_max_move_dist(next_node);
|
||||
m_ts_data->m_mutex.lock();
|
||||
contact_nodes[layer_nr_next].push_back(next_node);
|
||||
m_ts_data->m_mutex.unlock();
|
||||
PendingNode pending;
|
||||
pending.position = next_layer_vertex;
|
||||
pending.distance_to_top = node.distance_to_top + 1;
|
||||
pending.support_roof_layers_below = node.support_roof_layers_below - (node.distance_to_top >= 0 ? 1 : 0);
|
||||
pending.to_buildplate = to_buildplate;
|
||||
pending.parent = p_node;
|
||||
pending.clamp_radius = true;
|
||||
pending.parent_radius = node.radius;
|
||||
pending.dist_to_outer = dist_to_outer;
|
||||
pass2_out.pending.emplace_back(std::move(pending));
|
||||
};
|
||||
tbb::parallel_for(tbb::blocked_range<size_t>(0, nodes_vec.size()),
|
||||
[&pass2_body](const tbb::blocked_range<size_t>& node_range) {
|
||||
for (size_t node_idx = node_range.begin(); node_idx < node_range.end(); ++ node_idx)
|
||||
pass2_body(node_idx);
|
||||
});
|
||||
// Apply the recorded side effects in node order.
|
||||
for (size_t node_idx = 0; node_idx < nodes_vec.size(); ++ node_idx) {
|
||||
PassTwoResult& pass2_out = pass2_results[node_idx];
|
||||
for (PendingNode& pending : pass2_out.pending) {
|
||||
SupportNode* next_node = m_ts_data->create_node(pending.position, pending.distance_to_top, obj_layer_nr_next,
|
||||
pending.support_roof_layers_below, pending.to_buildplate, pending.parent, print_z_next, height_next);
|
||||
if (pending.zero_max_move)
|
||||
next_node->max_move_dist = 0;
|
||||
if (pending.has_overhang)
|
||||
next_node->overhang = std::move(pending.overhang);
|
||||
if (pending.clamp_radius) {
|
||||
next_node->radius = std::max(pending.parent_radius, std::min(next_node->radius, pending.dist_to_outer));
|
||||
get_max_move_dist(next_node);
|
||||
}
|
||||
contact_nodes[layer_nr_next].push_back(next_node);
|
||||
}
|
||||
if (pass2_out.unsupported_leaf)
|
||||
unsupported_branch_leaves.push_front({ layer_nr, nodes_vec[node_idx].second });
|
||||
if (pass2_out.invalidate)
|
||||
nodes_vec[node_idx].second->valid = false;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#ifdef SUPPORT_TREE_DEBUG_TO_SVG
|
||||
|
||||
@@ -2382,13 +2382,10 @@ static void merge_influence_areas(
|
||||
size_t num_buckets_initial;
|
||||
{
|
||||
// How many buckets per first merge iteration?
|
||||
const size_t num_threads = tbb::this_task_arena::max_concurrency();
|
||||
// 4 buckets per thread if possible,
|
||||
const size_t num_buckets_min = (input_size + 2) / 4;
|
||||
// 2 buckets per thread otherwise.
|
||||
const size_t num_buckets_max = input_size / 2;
|
||||
num_buckets_initial = num_buckets_min >= num_threads ? num_buckets_min : num_buckets_max;
|
||||
const size_t bucket_size = num_buckets_min >= num_threads ? 4 : 2;
|
||||
// Fixed at 4: merging is not associative, so sizing buckets off max_concurrency() made
|
||||
// results depend on the core count of the slicing machine.
|
||||
const size_t bucket_size = 4;
|
||||
num_buckets_initial = (input_size + 2) / 4;
|
||||
// Fill in the buckets.
|
||||
SupportElementMerging *it = influence_areas.data();
|
||||
// Reserve one more bucket to keep a single influence area which will not be merged in the first iteration.
|
||||
|
||||
@@ -255,6 +255,10 @@ extern bool is_gallery_file(const std::string& path, char const* type);
|
||||
extern bool is_shapes_dir(const std::string& dir);
|
||||
//BBS: add json support
|
||||
extern bool is_json_file(const std::string& path);
|
||||
// True if rel_path is relative, has no ".." component and, joined to root, still resolves inside it.
|
||||
// Both '/' and '\\' are treated as separators on every platform, so an archive rejected on one OS
|
||||
// is rejected on all of them.
|
||||
extern bool is_path_within_root(const std::string &rel_path, const boost::filesystem::path &root);
|
||||
|
||||
// Orca: custom protocal support utils
|
||||
inline bool is_orca_open(const std::string& url) { return boost::starts_with(url, "orcaslicer://open"); }
|
||||
|
||||
@@ -93,6 +93,9 @@ static constexpr double INSET_OVERLAP_TOLERANCE = 0.4;
|
||||
static constexpr double EXTERNAL_INFILL_MARGIN = 3;
|
||||
static constexpr double BRIDGE_INFILL_MARGIN = 1;
|
||||
static constexpr double WIPE_TOWER_MARGIN = 1.;
|
||||
// Margin for system placement of the wipe tower (defaults, re-placement, CLI). Positions
|
||||
// within WIPE_TOWER_MARGIN stay valid: a user drag down to that limit is respected.
|
||||
static constexpr double WIPE_TOWER_AUTO_MARGIN = 15.;
|
||||
//FIXME Better to use an inline function with an explicit return type.
|
||||
//inline coord_t scale_(coordf_t v) { return coord_t(floor(v / SCALING_FACTOR + 0.5f)); }
|
||||
#define scale_(val) ((val) / SCALING_FACTOR)
|
||||
|
||||
@@ -1088,6 +1088,30 @@ bool is_json_file(const std::string& path)
|
||||
return boost::iends_with(path, ".json");
|
||||
}
|
||||
|
||||
bool is_path_within_root(const std::string &rel_path, const boost::filesystem::path &root)
|
||||
{
|
||||
auto is_separator = [](char c) { return c == '/' || c == '\\'; };
|
||||
if (rel_path.empty() || is_separator(rel_path.front()) || (rel_path.size() > 1 && rel_path[1] == ':'))
|
||||
return false;
|
||||
for (size_t start = 0; start <= rel_path.size();) {
|
||||
size_t end = start;
|
||||
while (end < rel_path.size() && !is_separator(rel_path[end]))
|
||||
++end;
|
||||
if (rel_path.compare(start, end - start, "..") == 0)
|
||||
return false;
|
||||
start = end + 1;
|
||||
}
|
||||
// Resolve against the canonical root so a symlink inside it cannot lead back out.
|
||||
try {
|
||||
const std::string root_str = boost::filesystem::weakly_canonical(root).string();
|
||||
const std::string full_str = boost::filesystem::weakly_canonical(root / rel_path).string();
|
||||
return full_str.compare(0, root_str.size(), root_str) == 0 &&
|
||||
(full_str.size() == root_str.size() || full_str[root_str.size()] == boost::filesystem::path::preferred_separator);
|
||||
} catch (const boost::filesystem::filesystem_error &) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool is_img_file(const std::string &path)
|
||||
{
|
||||
return boost::iends_with(path, ".png") || boost::iends_with(path, ".svg");
|
||||
|
||||
Reference in New Issue
Block a user