Merge branch 'master' of https://github.com/Prusa-Development/PrusaSlicerPrivate into vb_et_instances_synch

This commit is contained in:
enricoturri1966 2023-01-26 10:00:58 +01:00
commit 92d26883a5
26 changed files with 806 additions and 399 deletions

View File

@ -4499,6 +4499,8 @@ filament_soluble = 1
filament_type = PVB
filament_colour = #FFFF6F
filament_spool_weight = 201
bed_temperature = 75
first_layer_bed_temperature = 75
slowdown_below_layer_time = 20
filament_ramming_parameters = "120 110 1.74194 1.90323 2.16129 2.48387 2.83871 3.25806 3.83871 4.6129 5.41935 5.96774| 0.05 1.69677 0.45 1.96128 0.95 2.63872 1.45 3.46129 1.95 4.99031 2.45 6.12908 2.95 8.30974 3.45 11.4065 3.95 7.6 4.45 7.6 4.95 7.6"
start_filament_gcode = "M900 K{if printer_notes=~/.*PRINTER_MODEL_MINI.*/ and nozzle_diameter[0]==0.6}0.12{elsif printer_notes=~/.*PRINTER_MODEL_MINI.*/ and nozzle_diameter[0]==0.8}0.06{elsif printer_notes=~/.*PRINTER_MODEL_MINI.*/}0.2{elsif nozzle_diameter[0]==0.8}0.02{elsif nozzle_diameter[0]==0.6}0.05{else}0.08{endif} ; Filament gcode LA 1.5\n{if printer_notes=~/.*PRINTER_MODEL_MINI.*/};{elsif printer_notes=~/.*PRINTER_HAS_BOWDEN.*/}M900 K200{elsif nozzle_diameter[0]==0.6}M900 K24{elsif nozzle_diameter[0]==0.8};{else}M900 K45{endif} ; Filament gcode LA 1.0"

View File

@ -776,7 +776,7 @@ public:
static int nil_value() { return std::numeric_limits<int>::max(); }
// A scalar is nil, or all values of a vector are nil.
bool is_nil() const override { for (auto v : this->values) if (v != nil_value()) return false; return true; }
bool is_nil(size_t idx) const override { return this->values[idx] == nil_value(); }
bool is_nil(size_t idx) const override { return values[idx < this->values.size() ? idx : 0] == nil_value(); }
std::string serialize() const override
{

View File

@ -32,8 +32,8 @@ public:
ExPolygon& operator=(const ExPolygon &other) = default;
ExPolygon& operator=(ExPolygon &&other) = default;
Polygon contour;
Polygons holes;
Polygon contour; //CCW
Polygons holes; //CW
void clear() { contour.points.clear(); holes.clear(); }
void scale(double factor);

View File

@ -113,26 +113,19 @@ namespace Slic3r {
{
std::string gcode;
// move to the nearest standby point
if (!this->standby_points.empty()) {
// get current position in print coordinates
Vec3d writer_pos = gcodegen.writer().get_position();
Point pos = Point::new_scale(writer_pos(0), writer_pos(1));
// find standby point
Point standby_point = nearest_point(this->standby_points, pos).first;
/* We don't call gcodegen.travel_to() because we don't need retraction (it was already
triggered by the caller) nor avoid_crossing_perimeters and also because the coordinates
of the destination point must not be transformed by origin nor current extruder offset. */
gcode += gcodegen.writer().travel_to_xy(unscale(standby_point),
"move to standby position");
}
unsigned int extruder_id = gcodegen.writer().extruder()->id();
const ConfigOptionIntsNullable& filament_idle_temp = gcodegen.config().idle_temperature;
if (filament_idle_temp.is_nil(extruder_id)) {
// There is no idle temperature defined in filament settings.
// Use the delta value from print config.
if (gcodegen.config().standby_temperature_delta.value != 0) {
// we assume that heating is always slower than cooling, so no need to block
gcode += gcodegen.writer().set_temperature
(this->_get_temp(gcodegen) + gcodegen.config().standby_temperature_delta.value, false, gcodegen.writer().extruder()->id());
(this->_get_temp(gcodegen) + gcodegen.config().standby_temperature_delta.value, false, extruder_id);
}
} else {
// Use the value from filament settings. That one is absolute, not delta.
gcode += gcodegen.writer().set_temperature(filament_idle_temp.get_at(extruder_id), false, extruder_id);
}
return gcode;
@ -145,8 +138,7 @@ namespace Slic3r {
std::string();
}
int
OozePrevention::_get_temp(GCode& gcodegen)
int OozePrevention::_get_temp(const GCode& gcodegen) const
{
return (gcodegen.layer() != nullptr && gcodegen.layer()->id() == 0)
? gcodegen.config().first_layer_temperature.get_at(gcodegen.writer().extruder()->id())
@ -238,8 +230,16 @@ namespace Slic3r {
std::string tcr_rotated_gcode = post_process_wipe_tower_moves(tcr, wipe_tower_offset, wipe_tower_rotation);
if (! tcr.priming) {
// Move over the wipe tower.
double current_z = gcodegen.writer().get_position().z();
if (z == -1.) // in case no specific z was provided, print at current_z pos
z = current_z;
const bool needs_toolchange = gcodegen.writer().need_toolchange(new_extruder_id);
const bool will_go_down = ! is_approx(z, current_z);
if (! needs_toolchange || (gcodegen.config().single_extruder_multi_material && ! tcr.priming)) {
// Move over the wipe tower. If this is not single-extruder MM, the first wipe tower move following the
// toolchange will travel there anyway (if there is a toolchange).
gcode += gcodegen.retract();
gcodegen.m_avoid_crossing_perimeters.use_external_mp_once();
gcode += gcodegen.travel_to(
@ -249,81 +249,35 @@ namespace Slic3r {
gcode += gcodegen.unretract();
}
double current_z = gcodegen.writer().get_position().z();
if (z == -1.) // in case no specific z was provided, print at current_z pos
z = current_z;
if (! is_approx(z, current_z)) {
if (will_go_down) {
gcode += gcodegen.writer().retract();
gcode += gcodegen.writer().travel_to_z(z, "Travel down to the last wipe tower layer.");
gcode += gcodegen.writer().unretract();
}
// Process the end filament gcode.
std::string end_filament_gcode_str;
if (gcodegen.writer().extruder() != nullptr) {
// Process the custom end_filament_gcode in case of single_extruder_multi_material.
unsigned int old_extruder_id = gcodegen.writer().extruder()->id();
const std::string& end_filament_gcode = gcodegen.config().end_filament_gcode.get_at(old_extruder_id);
if (gcodegen.writer().extruder() != nullptr && !end_filament_gcode.empty()) {
end_filament_gcode_str = gcodegen.placeholder_parser_process("end_filament_gcode", end_filament_gcode, old_extruder_id);
check_add_eol(end_filament_gcode_str);
}
}
// Process the custom toolchange_gcode. If it is empty, provide a simple Tn command to change the filament.
// Otherwise, leave control to the user completely.
std::string toolchange_gcode_str;
const std::string& toolchange_gcode = gcodegen.config().toolchange_gcode.value;
if (! toolchange_gcode.empty()) {
DynamicConfig config;
int previous_extruder_id = gcodegen.writer().extruder() ? (int)gcodegen.writer().extruder()->id() : -1;
config.set_key_value("previous_extruder", new ConfigOptionInt(previous_extruder_id));
config.set_key_value("next_extruder", new ConfigOptionInt((int)new_extruder_id));
config.set_key_value("layer_num", new ConfigOptionInt(gcodegen.m_layer_index));
config.set_key_value("layer_z", new ConfigOptionFloat(tcr.print_z));
config.set_key_value("toolchange_z", new ConfigOptionFloat(z));
config.set_key_value("max_layer_z", new ConfigOptionFloat(gcodegen.m_max_layer_z));
toolchange_gcode_str = gcodegen.placeholder_parser_process("toolchange_gcode", toolchange_gcode, new_extruder_id, &config);
check_add_eol(toolchange_gcode_str);
std::string deretraction_str;
if (tcr.priming || (new_extruder_id >= 0 && needs_toolchange)) {
if (gcodegen.config().single_extruder_multi_material)
gcodegen.m_wipe.reset_path(); // We don't want wiping on the ramming lines.
toolchange_gcode_str = gcodegen.set_extruder(new_extruder_id, tcr.print_z); // TODO: toolchange_z vs print_z
if (gcodegen.config().wipe_tower)
deretraction_str = gcodegen.unretract();
}
std::string toolchange_command;
if (tcr.priming || (new_extruder_id >= 0 && gcodegen.writer().need_toolchange(new_extruder_id)))
toolchange_command = gcodegen.writer().toolchange(new_extruder_id);
if (!custom_gcode_changes_tool(toolchange_gcode_str, gcodegen.writer().toolchange_prefix(), new_extruder_id))
toolchange_gcode_str += toolchange_command;
else {
// We have informed the m_writer about the current extruder_id, we can ignore the generated G-code.
}
gcodegen.placeholder_parser().set("current_extruder", new_extruder_id);
// Process the start filament gcode.
std::string start_filament_gcode_str;
const std::string& start_filament_gcode = gcodegen.config().start_filament_gcode.get_at(new_extruder_id);
if (!start_filament_gcode.empty()) {
// Process the start_filament_gcode for the active filament only.
// Insert the toolchange and deretraction gcode into the generated gcode.
DynamicConfig config;
config.set_key_value("layer_num", new ConfigOptionInt(gcodegen.m_layer_index));
config.set_key_value("layer_z", new ConfigOptionFloat(gcodegen.writer().get_position()(2) - gcodegen.m_config.z_offset.value));
config.set_key_value("max_layer_z", new ConfigOptionFloat(gcodegen.m_max_layer_z));
config.set_key_value("filament_extruder_id", new ConfigOptionInt(new_extruder_id));
start_filament_gcode_str = gcodegen.placeholder_parser_process("start_filament_gcode", start_filament_gcode, new_extruder_id, &config);
check_add_eol(start_filament_gcode_str);
}
// Insert the end filament, toolchange, and start filament gcode into the generated gcode.
DynamicConfig config;
config.set_key_value("end_filament_gcode", new ConfigOptionString(end_filament_gcode_str));
config.set_key_value("toolchange_gcode", new ConfigOptionString(toolchange_gcode_str));
config.set_key_value("start_filament_gcode", new ConfigOptionString(start_filament_gcode_str));
config.set_key_value("deretraction_from_wipe_tower_generator", new ConfigOptionString(deretraction_str));
std::string tcr_gcode, tcr_escaped_gcode = gcodegen.placeholder_parser_process("tcr_rotated_gcode", tcr_rotated_gcode, new_extruder_id, &config);
unescape_string_cstyle(tcr_escaped_gcode, tcr_gcode);
gcode += tcr_gcode;
check_add_eol(toolchange_gcode_str);
// A phony move to the end position at the wipe tower.
gcodegen.writer().travel_to_xy(end_pos.cast<double>());
gcodegen.set_last_pos(wipe_tower_point_to_object_point(gcodegen, end_pos));
@ -898,34 +852,7 @@ namespace DoExport {
static void init_ooze_prevention(const Print &print, OozePrevention &ooze_prevention)
{
// Calculate wiping points if needed
if (print.config().ooze_prevention.value && ! print.config().single_extruder_multi_material) {
Points skirt_points;
for (const ExtrusionEntity *ee : print.skirt().entities)
for (const ExtrusionPath &path : dynamic_cast<const ExtrusionLoop*>(ee)->paths)
append(skirt_points, path.polyline.points);
if (! skirt_points.empty()) {
Polygon outer_skirt = Slic3r::Geometry::convex_hull(skirt_points);
Polygons skirts;
for (unsigned int extruder_id : print.extruders()) {
const Vec2d &extruder_offset = print.config().extruder_offset.get_at(extruder_id);
Polygon s(outer_skirt);
s.translate(Point::new_scale(-extruder_offset(0), -extruder_offset(1)));
skirts.emplace_back(std::move(s));
}
ooze_prevention.enable = true;
ooze_prevention.standby_points = offset(Slic3r::Geometry::convex_hull(skirts), float(scale_(3.))).front().equally_spaced_points(float(scale_(10.)));
#if 0
require "Slic3r/SVG.pm";
Slic3r::SVG::output(
"ooze_prevention.svg",
red_polygons => \@skirts,
polygons => [$outer_skirt],
points => $gcodegen->ooze_prevention->standby_points,
);
#endif
}
}
ooze_prevention.enable = print.config().ooze_prevention.value && ! print.config().single_extruder_multi_material;
}
// Fill in print_statistics and return formatted string containing filament statistics to be inserted into G-code comment section.
@ -1263,6 +1190,11 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato
m_placeholder_parser.set("first_layer_print_min", new ConfigOptionFloats({ bbox.min.x(), bbox.min.y() }));
m_placeholder_parser.set("first_layer_print_max", new ConfigOptionFloats({ bbox.max.x(), bbox.max.y() }));
m_placeholder_parser.set("first_layer_print_size", new ConfigOptionFloats({ bbox.size().x(), bbox.size().y() }));
std::vector<unsigned char> is_extruder_used(print.config().nozzle_diameter.size(), 0);
for (unsigned int extruder_id : print.extruders())
is_extruder_used[extruder_id] = true;
m_placeholder_parser.set("is_extruder_used", new ConfigOptionBools(is_extruder_used));
}
std::string start_gcode = this->placeholder_parser_process("start_gcode", print.config().start_gcode.value, initial_extruder_id);
// Set bed temperature if the start G-code does not contain any bed temp control G-codes.
@ -1282,8 +1214,9 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato
// Set other general things.
file.write(this->preamble());
// Calculate wiping points if needed
// Enable ooze prevention if configured so.
DoExport::init_ooze_prevention(print, m_ooze_prevention);
print.throw_if_canceled();
// Collect custom seam data from all objects.
@ -1814,7 +1747,7 @@ void GCode::_print_first_layer_extruder_temperatures(GCodeOutputStream &file, Pr
m_writer.set_temperature(temp, wait, first_printing_extruder_id);
} else {
// Custom G-code does not set the extruder temperature. Do it now.
if (print.config().single_extruder_multi_material.value) {
if (print.config().single_extruder_multi_material.value || m_ooze_prevention.enable) {
// Set temperature of the first printing extruder only.
int temp = print.config().first_layer_temperature.get_at(first_printing_extruder_id);
if (temp > 0)
@ -2136,11 +2069,14 @@ LayerResult GCode::process_layer(
// Transition from 1st to 2nd layer. Adjust nozzle temperatures as prescribed by the nozzle dependent
// first_layer_temperature vs. temperature settings.
for (const Extruder &extruder : m_writer.extruders()) {
if (print.config().single_extruder_multi_material.value && extruder.id() != m_writer.extruder()->id())
if (print.config().single_extruder_multi_material.value || m_ooze_prevention.enable) {
// In single extruder multi material mode, set the temperature for the current extruder only.
// The same applies when ooze prevention is enabled.
if (extruder.id() != m_writer.extruder()->id())
continue;
}
int temperature = print.config().temperature.get_at(extruder.id());
if (temperature > 0 && temperature != print.config().first_layer_temperature.get_at(extruder.id()))
if (temperature > 0 && (temperature != print.config().first_layer_temperature.get_at(extruder.id())))
gcode += m_writer.set_temperature(temperature, false, extruder.id());
}
gcode += m_writer.set_bed_temperature(print.config().bed_temperature.get_at(first_extruder_id));
@ -3186,6 +3122,9 @@ std::string GCode::set_extruder(unsigned int extruder_id, double print_z)
if (! start_filament_gcode.empty()) {
// Process the start_filament_gcode for the filament.
DynamicConfig config;
config.set_key_value("layer_num", new ConfigOptionInt(m_layer_index));
config.set_key_value("layer_z", new ConfigOptionFloat(this->writer().get_position()(2) - m_config.z_offset.value));
config.set_key_value("max_layer_z", new ConfigOptionFloat(m_max_layer_z));
config.set_key_value("filament_extruder_id", new ConfigOptionInt(int(extruder_id)));
gcode += this->placeholder_parser_process("start_filament_gcode", start_filament_gcode, extruder_id, &config);
check_add_eol(gcode);
@ -3201,8 +3140,7 @@ std::string GCode::set_extruder(unsigned int extruder_id, double print_z)
m_wipe.reset_path();
if (m_writer.extruder() != nullptr) {
// Process the custom end_filament_gcode. set_extruder() is only called if there is no wipe tower
// so it should not be injected twice.
// Process the custom end_filament_gcode.
unsigned int old_extruder_id = m_writer.extruder()->id();
const std::string &end_filament_gcode = m_config.end_filament_gcode.get_at(old_extruder_id);
if (! end_filament_gcode.empty()) {
@ -3212,8 +3150,7 @@ std::string GCode::set_extruder(unsigned int extruder_id, double print_z)
}
// If ooze prevention is enabled, park current extruder in the nearest
// standby point and set it to the standby temperature.
// If ooze prevention is enabled, set current extruder to the standby temperature.
if (m_ooze_prevention.enable && m_writer.extruder() != nullptr)
gcode += m_ooze_prevention.pre_toolchange(*this);
@ -3257,6 +3194,9 @@ std::string GCode::set_extruder(unsigned int extruder_id, double print_z)
if (! start_filament_gcode.empty()) {
// Process the start_filament_gcode for the new filament.
DynamicConfig config;
config.set_key_value("layer_num", new ConfigOptionInt(m_layer_index));
config.set_key_value("layer_z", new ConfigOptionFloat(this->writer().get_position()(2) - m_config.z_offset.value));
config.set_key_value("max_layer_z", new ConfigOptionFloat(m_max_layer_z));
config.set_key_value("filament_extruder_id", new ConfigOptionInt(int(extruder_id)));
gcode += this->placeholder_parser_process("start_filament_gcode", start_filament_gcode, extruder_id, &config);
check_add_eol(gcode);

View File

@ -39,14 +39,13 @@ struct PrintInstance;
class OozePrevention {
public:
bool enable;
Points standby_points;
OozePrevention() : enable(false) {}
std::string pre_toolchange(GCode &gcodegen);
std::string post_toolchange(GCode &gcodegen);
private:
int _get_temp(GCode &gcodegen);
int _get_temp(const GCode &gcodegen) const;
};
class Wipe {

View File

@ -122,39 +122,18 @@ std::vector<ExtendedPoint> estimate_points_properties(const std::vector<P>
CurvatureEstimator cestim;
auto maybe_unscale = [](const P &p) { return SCALED_INPUT ? unscaled(p) : p.template cast<double>(); };
std::vector<P> extrusion_points;
{
if (max_line_length <= 0) {
extrusion_points = input_points;
} else {
extrusion_points.reserve(input_points.size() * 2);
for (size_t i = 0; i + 1 < input_points.size(); i++) {
const P &curr = input_points[i];
const P &next = input_points[i + 1];
extrusion_points.push_back(curr);
auto len = maybe_unscale(next - curr).squaredNorm();
double t = sqrt((max_line_length * max_line_length) / len);
size_t new_point_count = 1.0 / (t + EPSILON);
for (size_t j = 1; j < new_point_count + 1; j++) {
extrusion_points.push_back(curr * (1.0 - j * t) + next * (j * t));
}
}
extrusion_points.push_back(input_points.back());
}
}
std::vector<ExtendedPoint> points;
points.reserve(extrusion_points.size() * (ADD_INTERSECTIONS ? 1.5 : 1));
points.reserve(input_points.size() * (ADD_INTERSECTIONS ? 1.5 : 1));
{
ExtendedPoint start_point{maybe_unscale(extrusion_points.front())};
ExtendedPoint start_point{maybe_unscale(input_points.front())};
auto [distance, nearest_line, x] = unscaled_prev_layer.template distance_from_lines_extra<SIGNED_DISTANCE>(start_point.position.cast<AABBScalar>());
start_point.distance = distance + boundary_offset;
start_point.nearest_prev_layer_line = nearest_line;
points.push_back(start_point);
}
for (size_t i = 1; i < extrusion_points.size(); i++) {
ExtendedPoint next_point{maybe_unscale(extrusion_points[i])};
for (size_t i = 1; i < input_points.size(); i++) {
ExtendedPoint next_point{maybe_unscale(input_points[i])};
auto [distance, nearest_line, x] = unscaled_prev_layer.template distance_from_lines_extra<SIGNED_DISTANCE>(next_point.position.cast<AABBScalar>());
next_point.distance = distance + boundary_offset;
next_point.nearest_prev_layer_line = nearest_line;
@ -172,7 +151,7 @@ std::vector<ExtendedPoint> estimate_points_properties(const std::vector<P>
if (PREV_LAYER_BOUNDARY_OFFSET && ADD_INTERSECTIONS) {
std::vector<ExtendedPoint> new_points;
new_points.reserve(points.size() * 2);
new_points.reserve(points.size()*2);
new_points.push_back(points.front());
for (int point_idx = 0; point_idx < int(points.size()) - 1; ++point_idx) {
const ExtendedPoint &curr = points[point_idx];
@ -201,7 +180,30 @@ std::vector<ExtendedPoint> estimate_points_properties(const std::vector<P>
}
new_points.push_back(next);
}
points = std::move(new_points);
points = new_points;
}
if (max_line_length > 0) {
std::vector<ExtendedPoint> new_points;
new_points.reserve(points.size()*2);
{
for (size_t i = 0; i + 1 < points.size(); i++) {
const ExtendedPoint &curr = points[i];
const ExtendedPoint &next = points[i + 1];
new_points.push_back(curr);
double len = (next.position - curr.position).squaredNorm();
double t = sqrt((max_line_length * max_line_length) / len);
size_t new_point_count = 1.0 / t;
for (size_t j = 1; j < new_point_count + 1; j++) {
Vec2d pos = curr.position * (1.0 - j * t) + next.position * (j * t);
auto [p_dist, p_near_l,
p_x] = unscaled_prev_layer.template distance_from_lines_extra<SIGNED_DISTANCE>(pos.cast<AABBScalar>());
new_points.push_back(ExtendedPoint{pos, float(p_dist + boundary_offset), p_near_l});
}
}
new_points.push_back(points.back());
}
points = new_points;
}
for (int point_idx = 0; point_idx < int(points.size()); ++point_idx) {

View File

@ -71,6 +71,8 @@ public:
return *this;
}
WipeTowerWriter& set_position(const Vec2f &pos) { m_current_pos = pos; return *this; }
WipeTowerWriter& set_initial_tool(size_t tool) { m_current_tool = tool; return *this; }
WipeTowerWriter& set_z(float z)
@ -806,6 +808,8 @@ void WipeTower::toolchange_Unload(
const float line_width = m_perimeter_width * m_filpar[m_current_tool].ramming_line_width_multiplicator; // desired ramming line thickness
const float y_step = line_width * m_filpar[m_current_tool].ramming_step_multiplicator * m_extra_spacing; // spacing between lines in mm
const Vec2f ramming_start_pos = Vec2f(xl, cleaning_box.ld.y() + m_depth_traversed + y_step/2.f);
writer.append("; CP TOOLCHANGE UNLOAD\n")
.change_analyzer_line_width(line_width);
@ -814,10 +818,12 @@ void WipeTower::toolchange_Unload(
float remaining = xr - xl ; // keeps track of distance to the next turnaround
float e_done = 0; // measures E move done from each segment
writer.travel(xl, cleaning_box.ld.y() + m_depth_traversed + y_step/2.f ); // move to starting position
if (m_semm)
writer.travel(ramming_start_pos); // move to starting position
else
writer.set_position(ramming_start_pos);
// if the ending point of the ram would end up in mid air, align it with the end of the wipe tower:
if (m_layer_info > m_plan.begin() && m_layer_info < m_plan.end() && (m_layer_info-1!=m_plan.begin() || !m_adhesion )) {
if (m_semm && (m_layer_info > m_plan.begin() && m_layer_info < m_plan.end() && (m_layer_info-1!=m_plan.begin() || !m_adhesion ))) {
// this is y of the center of previous sparse infill border
float sparse_beginning_y = 0.f;
@ -849,7 +855,7 @@ void WipeTower::toolchange_Unload(
writer.disable_linear_advance();
// now the ramming itself:
while (i < m_filpar[m_current_tool].ramming_speed.size())
while (m_semm && i < m_filpar[m_current_tool].ramming_speed.size())
{
const float x = volume_to_length(m_filpar[m_current_tool].ramming_speed[i] * 0.25f, line_width, m_layer_height);
const float e = m_filpar[m_current_tool].ramming_speed[i] * 0.25f / filament_area(); // transform volume per sec to E move;
@ -898,7 +904,7 @@ void WipeTower::toolchange_Unload(
// Cooling:
const int& number_of_moves = m_filpar[m_current_tool].cooling_moves;
if (number_of_moves > 0) {
if (m_semm && number_of_moves > 0) {
const float& initial_speed = m_filpar[m_current_tool].cooling_initial_speed;
const float& final_speed = m_filpar[m_current_tool].cooling_final_speed;
@ -916,14 +922,20 @@ void WipeTower::toolchange_Unload(
}
}
if (m_semm) {
// let's wait is necessary:
writer.wait(m_filpar[m_current_tool].delay);
// we should be at the beginning of the cooling tube again - let's move to parking position:
writer.retract(-m_cooling_tube_length/2.f+m_parking_pos_retraction-m_cooling_tube_retraction, 2000);
}
// this is to align ramming and future wiping extrusions, so the future y-steps can be uniform from the start:
// the perimeter_width will later be subtracted, it is there to not load while moving over just extruded material
writer.travel(end_of_ramming.x(), end_of_ramming.y() + (y_step/m_extra_spacing-m_perimeter_width) / 2.f + m_perimeter_width, 2400.f);
Vec2f pos = Vec2f(end_of_ramming.x(), end_of_ramming.y() + (y_step/m_extra_spacing-m_perimeter_width) / 2.f + m_perimeter_width);
if (m_semm)
writer.travel(pos, 2400.f);
else
writer.set_position(pos);
writer.resume_preview()
.flush_planner_queue();
@ -941,7 +953,7 @@ void WipeTower::toolchange_Change(
// This is where we want to place the custom gcodes. We will use placeholders for this.
// These will be substituted by the actual gcodes when the gcode is generated.
writer.append("[end_filament_gcode]\n");
//writer.append("[end_filament_gcode]\n");
writer.append("[toolchange_gcode]\n");
// Travel to where we assume we are. Custom toolchange or some special T code handling (parking extruder etc)
@ -952,11 +964,12 @@ void WipeTower::toolchange_Change(
.append(std::string("G1 X") + Slic3r::float_to_string_decimal_point(current_pos.x())
+ " Y" + Slic3r::float_to_string_decimal_point(current_pos.y())
+ never_skip_tag() + "\n");
writer.append("[deretraction_from_wipe_tower_generator]");
// The toolchange Tn command will be inserted later, only in case that the user does
// not provide a custom toolchange gcode.
writer.set_tool(new_tool); // This outputs nothing, the writer just needs to know the tool has changed.
writer.append("[start_filament_gcode]\n");
//writer.append("[start_filament_gcode]\n");
writer.flush_planner_queue();
m_current_tool = new_tool;

View File

@ -290,7 +290,6 @@ private:
// Extruder specific parameters.
std::vector<FilamentParameters> m_filpar;
// State of the wipe tower generator.
unsigned int m_num_layer_changes = 0; // Layer change counter for the output statistics.
unsigned int m_num_tool_changes = 0; // Tool change change counter for the output statistics.

View File

@ -470,7 +470,7 @@ static std::vector<std::string> s_Preset_filament_options {
"extrusion_multiplier", "filament_density", "filament_cost", "filament_spool_weight", "filament_loading_speed", "filament_loading_speed_start", "filament_load_time",
"filament_unloading_speed", "filament_unloading_speed_start", "filament_unload_time", "filament_toolchange_delay", "filament_cooling_moves",
"filament_cooling_initial_speed", "filament_cooling_final_speed", "filament_ramming_parameters", "filament_minimal_purge_on_wipe_tower",
"temperature", "first_layer_temperature", "bed_temperature", "first_layer_bed_temperature", "fan_always_on", "cooling", "min_fan_speed",
"temperature", "idle_temperature", "first_layer_temperature", "bed_temperature", "first_layer_bed_temperature", "fan_always_on", "cooling", "min_fan_speed",
"max_fan_speed", "bridge_fan_speed", "disable_fan_first_layers", "full_fan_speed_layer", "fan_below_layer_time", "slowdown_below_layer_time", "min_print_speed",
"start_filament_gcode", "end_filament_gcode",
// Retract overrides

View File

@ -191,6 +191,7 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n
|| opt_key == "infill_first"
|| opt_key == "single_extruder_multi_material"
|| opt_key == "temperature"
|| opt_key == "idle_temperature"
|| opt_key == "wipe_tower"
|| opt_key == "wipe_tower_width"
|| opt_key == "wipe_tower_brim_width"
@ -347,7 +348,7 @@ std::vector<ObjectID> Print::print_object_ids() const
bool Print::has_infinite_skirt() const
{
return (m_config.draft_shield == dsEnabled && m_config.skirts > 0) || (m_config.ooze_prevention && this->extruders().size() > 1);
return (m_config.draft_shield == dsEnabled && m_config.skirts > 0)/* || (m_config.ooze_prevention && this->extruders().size() > 1)*/;
}
bool Print::has_skirt() const
@ -505,8 +506,8 @@ std::string Print::validate(std::string* warning) const
return L("The Wipe Tower is currently only supported for the Marlin, RepRap/Sprinter, RepRapFirmware and Repetier G-code flavors.");
if (! m_config.use_relative_e_distances)
return L("The Wipe Tower is currently only supported with the relative extruder addressing (use_relative_e_distances=1).");
if (m_config.ooze_prevention)
return L("Ooze prevention is currently not supported with the wipe tower enabled.");
if (m_config.ooze_prevention && m_config.single_extruder_multi_material)
return L("Ooze prevention is only supported with the wipe tower when 'single_extruder_multi_material' is off.");
if (m_config.use_volumetric_e)
return L("The Wipe Tower currently does not support volumetric E (use_volumetric_e=0).");
if (m_config.complete_objects && extruders.size() > 1)

View File

@ -229,6 +229,11 @@ static void assign_printer_technology_to_unknown(t_optiondef_map &options, Print
kvp.second.printer_technology = printer_technology;
}
// Maximum extruder temperature, bumped to 1500 to support printing of glass.
namespace {
const int max_temp = 1500;
};
PrintConfigDef::PrintConfigDef()
{
this->init_common_params();
@ -1972,9 +1977,7 @@ void PrintConfigDef::init_fff_params()
def = this->add("ooze_prevention", coBool);
def->label = L("Enable");
def->tooltip = L("This option will drop the temperature of the inactive extruders to prevent oozing. "
"It will enable a tall skirt automatically and move extruders outside such "
"skirt when changing temperatures.");
def->tooltip = L("This option will drop the temperature of the inactive extruders to prevent oozing. ");
def->mode = comExpert;
def->set_default_value(new ConfigOptionBool(false));
@ -2475,7 +2478,8 @@ void PrintConfigDef::init_fff_params()
def = this->add("standby_temperature_delta", coInt);
def->label = L("Temperature variation");
def->tooltip = L("Temperature difference to be applied when an extruder is not active. "
"Enables a full-height \"sacrificial\" skirt on which the nozzles are periodically wiped.");
"The value is not used when 'idle_temperature' in filament settings "
"is defined.");
def->sidetext = "∆°C";
def->min = -max_temp;
def->max = max_temp;
@ -3731,6 +3735,15 @@ void PrintConfigDef::init_sla_params()
def->min = 0;
def->set_default_value(new ConfigOptionFloat(0.3));
def = this->add_nullable("idle_temperature", coInts);
def->label = L("Idle temperature");
def->tooltip = L("Nozzle temperature when the tool is currently not used in multi-tool setups."
"This is only used when 'Ooze prevention is active in Print Settings.'");
def->sidetext = L("°C");
def->min = 0;
def->max = max_temp;
def->set_default_value(new ConfigOptionIntsNullable { ConfigOptionIntsNullable::nil_value() });
def = this->add("bottle_volume", coFloat);
def->label = L("Bottle volume");
def->tooltip = L("Bottle volume");
@ -4511,6 +4524,12 @@ std::string validate(const FullPrintConfig &cfg)
assert(opt != nullptr);
const ConfigOptionDef *optdef = print_config_def.get(opt_key);
assert(optdef != nullptr);
if (opt->nullable() && opt->is_nil()) {
// Do not check nil values
continue;
}
bool out_of_range = false;
switch (opt->type()) {
case coFloat:

View File

@ -769,6 +769,7 @@ PRINT_CONFIG_CLASS_DERIVED_DEFINE(
((ConfigOptionFloatOrPercent, first_layer_height))
((ConfigOptionFloatOrPercent, first_layer_speed))
((ConfigOptionInts, first_layer_temperature))
((ConfigOptionIntsNullable, idle_temperature))
((ConfigOptionInts, full_fan_speed_layer))
((ConfigOptionFloat, infill_acceleration))
((ConfigOptionBool, infill_first))

View File

@ -1,4 +1,6 @@
#include "Exception.hpp"
#include "KDTreeIndirect.hpp"
#include "Point.hpp"
#include "Print.hpp"
#include "BoundingBox.hpp"
#include "ClipperUtils.hpp"
@ -21,14 +23,19 @@
#include "SupportSpotsGenerator.hpp"
#include "TriangleSelectorWrapper.hpp"
#include "format.hpp"
#include "libslic3r.h"
#include <algorithm>
#include <cmath>
#include <float.h>
#include <string_view>
#include <unordered_map>
#include <utility>
#include <boost/log/trivial.hpp>
#include <tbb/parallel_for.h>
#include <vector>
using namespace std::literals;
@ -406,21 +413,6 @@ void PrintObject::ironing()
}
}
/*
std::vector<size_t> problematic_layers = SupportSpotsGenerator::quick_search(this);
if (!problematic_layers.empty()) {
std::cout << "Object needs supports" << std::endl;
this->active_step_add_warning(PrintStateBase::WarningLevel::CRITICAL,
L("Supportable issues found. Consider enabling supports for this object"));
this->active_step_add_warning(PrintStateBase::WarningLevel::CRITICAL,
L("Supportable issues found. Consider enabling supports for this object"));
for (size_t index = 0; index < std::min(problematic_layers.size(), size_t(4)); ++index) {
this->active_step_add_warning(PrintStateBase::WarningLevel::CRITICAL,
format(L("Layer with issues: %1%"), problematic_layers[index] + 1));
}
}
*/
void PrintObject::generate_support_spots()
{
if (this->set_started(posSupportSpotsSearch)) {
@ -428,10 +420,51 @@ void PrintObject::generate_support_spots()
m_print->set_status(75, L("Searching support spots"));
if (!this->shared_regions()->generated_support_points.has_value()) {
PrintTryCancel cancel_func = m_print->make_try_cancel();
SupportSpotsGenerator::Params params{this->print()->m_config.filament_type.values};
SupportSpotsGenerator::SupportPoints supp_points = SupportSpotsGenerator::full_search(this, cancel_func, params);
SupportSpotsGenerator::Params params{this->print()->m_config.filament_type.values,
float(this->print()->m_config.perimeter_acceleration.getFloat()),
this->config().raft_layers.getInt(), this->config().brim_type.value,
float(this->config().brim_width.getFloat())};
auto [supp_points, partial_objects] = SupportSpotsGenerator::full_search(this, cancel_func, params);
this->m_shared_regions->generated_support_points = {this->trafo_centered(), supp_points};
m_print->throw_if_canceled();
auto alert_fn = [&](PrintStateBase::WarningLevel level, SupportSpotsGenerator::SupportPointCause cause) {
switch (cause) {
case SupportSpotsGenerator::SupportPointCause::LongBridge:
this->active_step_add_warning(level, L("There are bridges longer than recommended length. Consider adding supports. ") +
(L("Object name")) + ": " + this->model_object()->name);
break;
case SupportSpotsGenerator::SupportPointCause::FloatingBridgeAnchor:
this->active_step_add_warning(level, L("Unsupported bridges will collapse. Supports are needed. ") + (L("Object name")) +
": " + this->model_object()->name);
break;
case SupportSpotsGenerator::SupportPointCause::FloatingExtrusion:
if (level == PrintStateBase::WarningLevel::CRITICAL) {
this->active_step_add_warning(level, L("Clusters of unsupported extrusions found. Supports are needed. ") +
(L("Object name")) + ": " + this->model_object()->name);
} else {
this->active_step_add_warning(level, L("Some unspported extrusions found. Consider adding supports. ") +
(L("Object name")) + ": " + this->model_object()->name);
}
break;
case SupportSpotsGenerator::SupportPointCause::SeparationFromBed:
this->active_step_add_warning(level, L("Object part may break from the bed. Consider adding brim and/or supports. ") +
(L("Object name")) + ": " + this->model_object()->name);
break;
case SupportSpotsGenerator::SupportPointCause::UnstableFloatingPart:
this->active_step_add_warning(level, L("Floating object parts detected. Supports are needed. ") + (L("Object name")) +
": " + this->model_object()->name);
break;
case SupportSpotsGenerator::SupportPointCause::WeakObjectPart:
this->active_step_add_warning(level, L("Thin parts of the object may break. Consider adding supports. ") +
(L("Object name")) + ": " + this->model_object()->name);
break;
}
};
if (!this->has_support()) {
SupportSpotsGenerator::raise_alerts_for_issues(supp_points, partial_objects, alert_fn);
}
}
BOOST_LOG_TRIVIAL(debug) << "Searching support spots - end";
this->set_done(posSupportSpotsSearch);
@ -468,7 +501,10 @@ void PrintObject::estimate_curled_extrusions()
// Estimate curling of support material and add it to the malformaition lines of each layer
float support_flow_width = support_material_flow(this, this->config().layer_height).width();
SupportSpotsGenerator::Params params{this->print()->m_config.filament_type.values};
SupportSpotsGenerator::Params params{this->print()->m_config.filament_type.values,
float(this->print()->m_config.perimeter_acceleration.getFloat()),
this->config().raft_layers.getInt(), this->config().brim_type.value,
float(this->config().brim_width.getFloat())};
SupportSpotsGenerator::estimate_supports_malformations(this->support_layers(), support_flow_width, params);
SupportSpotsGenerator::estimate_malformations(this->layers(), params);
m_print->throw_if_canceled();
@ -580,6 +616,7 @@ bool PrintObject::invalidate_state_by_config_options(
if ( opt_key == "brim_width"
|| opt_key == "brim_separation"
|| opt_key == "brim_type") {
steps.emplace_back(posSupportSpotsSearch);
// Brim is printed below supports, support invalidates brim and skirt.
steps.emplace_back(posSupportMaterial);
} else if (

View File

@ -13,7 +13,7 @@
#include <boost/filesystem/path.hpp>
#include <boost/log/trivial.hpp>
#define SLAPRINT_DO_BENCHMARK
// #define SLAPRINT_DO_BENCHMARK
#ifdef SLAPRINT_DO_BENCHMARK
#include <libnest2d/tools/benchmark.h>

View File

@ -11,6 +11,7 @@
#include "PrincipalComponents2D.hpp"
#include "Print.hpp"
#include "PrintBase.hpp"
#include "PrintConfig.hpp"
#include "Tesselate.hpp"
#include "libslic3r.h"
#include "tbb/parallel_for.h"
@ -23,6 +24,7 @@
#include <cstddef>
#include <cstdio>
#include <functional>
#include <optional>
#include <unordered_map>
#include <unordered_set>
#include <stack>
@ -68,7 +70,7 @@ public:
float len;
const ExtrusionEntity *origin_entity;
bool support_point_generated = false;
std::optional<SupportSpotsGenerator::SupportPointCause> support_point_generated = {};
float form_quality = 1.0f;
float curled_up_height = 0.0f;
@ -81,10 +83,6 @@ auto get_b(ExtrusionLine &&l) { return l.b; }
namespace SupportSpotsGenerator {
SupportPoint::SupportPoint(const Vec3f &position, float force, float spot_radius, const Vec2f &direction)
: position(position), force(force), spot_radius(spot_radius), direction(direction)
{}
using LD = AABBTreeLines::LinesDistancer<ExtrusionLine>;
struct SupportGridFilter
@ -240,13 +238,66 @@ std::vector<ExtrusionLine> check_extrusion_entity_stability(const ExtrusionEntit
checked_lines_out.insert(checked_lines_out.end(), tmp.begin(), tmp.end());
}
return checked_lines_out;
} else if (entity->role().is_bridge() && !entity->role().is_perimeter()) {
// pure bridges are handled separately, beacuse we need to align the forward and backward direction support points
if (entity->length() < scale_(params.min_distance_to_allow_local_supports)) {
return {};
}
const float flow_width = get_flow_width(layer_region, entity->role());
std::vector<ExtendedPoint> annotated_points = estimate_points_properties<true, true, true, true>(entity->as_polyline().points,
prev_layer_boundary, flow_width,
params.bridge_distance);
std::vector<ExtrusionLine> lines_out;
lines_out.reserve(annotated_points.size());
float bridged_distance = 0.0f;
std::optional<Vec2d> bridging_dir{};
for (size_t i = 0; i < annotated_points.size(); ++i) {
ExtendedPoint &curr_point = annotated_points[i];
ExtendedPoint &prev_point = i > 0 ? annotated_points[i - 1] : annotated_points[i - 1];
SupportPointCause potential_cause = std::abs(curr_point.curvature) > 0.1 ? SupportPointCause::FloatingBridgeAnchor :
SupportPointCause::LongBridge;
float line_len = i > 0 ? ((annotated_points[i - 1].position - curr_point.position).norm()) : 0.0f;
Vec2d line_dir = (curr_point.position - prev_point.position).normalized();
ExtrusionLine line_out{i > 0 ? annotated_points[i - 1].position.cast<float>() : curr_point.position.cast<float>(),
curr_point.position.cast<float>(), line_len, entity};
float max_bridge_len = std::max(params.support_points_interface_radius * 2.0f,
params.bridge_distance /
((1.0f + std::abs(curr_point.curvature)) * (1.0f + std::abs(curr_point.curvature)) *
(1.0f + std::abs(curr_point.curvature))));
if (!bridging_dir.has_value() && curr_point.distance > flow_width && line_len > params.bridge_distance * 0.6) {
bridging_dir = (prev_point.position - curr_point.position).normalized();
}
if (curr_point.distance > flow_width && potential_cause == SupportPointCause::LongBridge && bridging_dir.has_value() &&
bridging_dir->dot(line_dir) < 0.8) { // skip backward direction of bridge - supported by forward points enough
bridged_distance += line_len;
} else if (curr_point.distance > flow_width) {
bridged_distance += line_len;
if (bridged_distance > max_bridge_len) {
bridged_distance = 0.0f;
line_out.support_point_generated = potential_cause;
}
} else {
bridged_distance = 0.0f;
}
lines_out.push_back(line_out);
}
return lines_out;
} else { // single extrusion path, with possible varying parameters
if (entity->length() < scale_(params.min_distance_to_allow_local_supports)) {
return {};
}
const float flow_width = get_flow_width(layer_region, entity->role());
// Compute only unsigned distance - prev_layer_lines can contain unconnected paths, thus the sign of the distance is unreliable
std::vector<ExtendedPoint> annotated_points = estimate_points_properties<true, true, false, false>(entity->as_polyline().points,
prev_layer_lines, flow_width,
@ -270,21 +321,44 @@ std::vector<ExtrusionLine> check_extrusion_entity_stability(const ExtrusionEntit
float sign = (prev_layer_boundary.distance_from_lines<true>(curr_point.position) + 0.5f * flow_width) < 0.0f ? -1.0f : 1.0f;
curr_point.distance *= sign;
float max_bridge_len = params.bridge_distance /
((1.0f + std::abs(curr_point.curvature)) * (1.0f + std::abs(curr_point.curvature)));
SupportPointCause potential_cause = SupportPointCause::FloatingExtrusion;
if (bridged_distance + line_len > params.bridge_distance * 0.8 && std::abs(curr_point.curvature) < 0.1) {
potential_cause = SupportPointCause::FloatingExtrusion;
}
float max_bridge_len = std::max(params.support_points_interface_radius * 2.0f,
params.bridge_distance /
((1.0f + std::abs(curr_point.curvature)) * (1.0f + std::abs(curr_point.curvature)) *
(1.0f + std::abs(curr_point.curvature))));
if (curr_point.distance > 2.0f * flow_width) {
line_out.form_quality = 0.8f;
bridged_distance += line_len;
if (bridged_distance > max_bridge_len) {
line_out.support_point_generated = true;
std::cout << "Problem found A: " << std::endl;
std::cout << "bridged_distance: " << bridged_distance << std::endl;
std::cout << "max_bridge_len: " << max_bridge_len << std::endl;
std::cout << "line_out.form_quality: " << line_out.form_quality << std::endl;
std::cout << "curr_point.distance: " << curr_point.distance << std::endl;
std::cout << "curr_point.curvature: " << curr_point.curvature << std::endl;
std::cout << "flow_width: " << flow_width << std::endl;
line_out.support_point_generated = potential_cause;
bridged_distance = 0.0f;
}
} else if (curr_point.distance > flow_width * (1.0 + std::clamp(curr_point.curvature, -0.30f, 0.20f))) {
bridged_distance += line_len;
line_out.form_quality = nearest_prev_layer_line.form_quality - 0.3f;
if (line_out.form_quality < 0 && bridged_distance > max_bridge_len) {
line_out.support_point_generated = true;
std::cout << "Problem found B: " << std::endl;
std::cout << "bridged_distance: " << bridged_distance << std::endl;
std::cout << "max_bridge_len: " << max_bridge_len << std::endl;
std::cout << "line_out.form_quality: " << line_out.form_quality << std::endl;
std::cout << "curr_point.distance: " << curr_point.distance << std::endl;
std::cout << "curr_point.curvature: " << curr_point.curvature << std::endl;
std::cout << "flow_width: " << flow_width << std::endl;
line_out.support_point_generated = potential_cause;
line_out.form_quality = 0.5f;
bridged_distance = 0.0f;
}
@ -349,11 +423,13 @@ public:
Vec3f sticking_centroid_accumulator = Vec3f::Zero();
Vec2f sticking_second_moment_of_area_accumulator = Vec2f::Zero();
float sticking_second_moment_of_area_covariance_accumulator{};
bool connected_to_bed = false;
ObjectPart() = default;
void add(const ObjectPart &other)
{
this->connected_to_bed = this->connected_to_bed || other.connected_to_bed;
this->volume_centroid_accumulator += other.volume_centroid_accumulator;
this->volume += other.volume;
this->sticking_area += other.sticking_area;
@ -416,7 +492,7 @@ public:
return elastic_section_modulus;
}
float is_stable_while_extruding(const SliceConnection &connection,
std::tuple<float, SupportPointCause> is_stable_while_extruding(const SliceConnection &connection,
const ExtrusionLine &extruded_line,
const Vec3f &extreme_point,
float layer_z,
@ -434,7 +510,7 @@ public:
// section for bed calculations
{
if (this->sticking_area < EPSILON) return 1.0f;
if (this->sticking_area < EPSILON) return {1.0f, SupportPointCause::UnstableFloatingPart};
Vec3f bed_centroid = this->sticking_centroid_accumulator / this->sticking_area;
float bed_yield_torque = -compute_elastic_section_modulus(line_dir, extreme_point, this->sticking_centroid_accumulator,
@ -475,16 +551,19 @@ public:
BOOST_LOG_TRIVIAL(debug) << "SSG: total_torque: " << bed_total_torque << " layer_z: " << layer_z;
#endif
if (bed_total_torque > 0) return bed_total_torque / bed_conflict_torque_arm;
if (bed_total_torque > 0) {
return {bed_total_torque / bed_conflict_torque_arm,
(this->connected_to_bed ? SupportPointCause::SeparationFromBed : SupportPointCause::UnstableFloatingPart)};
}
}
// section for weak connection calculations
{
if (connection.area < EPSILON) return 1.0f;
if (connection.area < EPSILON) return {1.0f, SupportPointCause::UnstableFloatingPart};
Vec3f conn_centroid = connection.centroid_accumulator / connection.area;
if (layer_z - conn_centroid.z() < 3.0f) { return -1.0f; }
if (layer_z - conn_centroid.z() < 3.0f) { return {-1.0f, SupportPointCause::WeakObjectPart}; }
float conn_yield_torque = compute_elastic_section_modulus(line_dir, extreme_point, connection.centroid_accumulator,
connection.second_moment_of_area_accumulator,
connection.second_moment_of_area_covariance_accumulator,
@ -492,7 +571,7 @@ public:
params.material_yield_strength;
float conn_weight_arm = (conn_centroid.head<2>() - mass_centroid.head<2>()).norm();
float conn_weight_torque = conn_weight_arm * weight * (1.0f - conn_centroid.z() / layer_z);
float conn_weight_torque = conn_weight_arm * weight * (1.0f - conn_centroid.z() / layer_z) * (1.0f - conn_centroid.z() / layer_z);
float conn_movement_arm = std::max(0.0f, mass_centroid.z() - conn_centroid.z());
float conn_movement_torque = movement_force * conn_movement_arm;
@ -514,18 +593,20 @@ public:
BOOST_LOG_TRIVIAL(debug) << "SSG: total_torque: " << conn_total_torque << " layer_z: " << layer_z;
#endif
return conn_total_torque / conn_conflict_torque_arm;
return {conn_total_torque / conn_conflict_torque_arm, SupportPointCause::WeakObjectPart};
}
}
};
// return new object part and actual area covered by extrusions
std::tuple<ObjectPart, float> build_object_part_from_slice(const LayerSlice &slice, const Layer *layer)
std::tuple<ObjectPart, float> build_object_part_from_slice(const size_t &slice_idx, const Layer *layer, const Params& params)
{
ObjectPart new_object_part;
float area_covered_by_extrusions = 0;
const LayerSlice& slice = layer->lslices_ex.at(slice_idx);
auto add_extrusions_to_object = [&new_object_part, &area_covered_by_extrusions](const ExtrusionEntity *e, const LayerRegion *region) {
auto add_extrusions_to_object = [&new_object_part, &area_covered_by_extrusions, &params](const ExtrusionEntity *e,
const LayerRegion *region) {
float flow_width = get_flow_width(region, e->role());
const Layer *l = region->layer();
float slice_z = l->slice_z;
@ -537,7 +618,8 @@ std::tuple<ObjectPart, float> build_object_part_from_slice(const LayerSlice &sli
new_object_part.volume += volume;
new_object_part.volume_centroid_accumulator += to_3d(Vec2f((line.a + line.b) / 2.0f), slice_z) * volume;
if (l->bottom_z() < EPSILON) { // layer attached on bed
if (l->id() == params.raft_layers_count) { // layer attached on bed/raft
new_object_part.connected_to_bed = true;
float sticking_area = line.len * flow_width;
new_object_part.sticking_area += sticking_area;
Vec2f middle = Vec2f((line.a + line.b) / 2.0f);
@ -579,6 +661,49 @@ std::tuple<ObjectPart, float> build_object_part_from_slice(const LayerSlice &sli
}
}
// BRIM HANDLING
if (layer->id() == params.raft_layers_count && params.raft_layers_count == 0 && params.brim_type != BrimType::btNoBrim) {
// TODO: The algorithm here should take into account that multiple slices may have coliding Brim areas and the final brim area is
// smaller,
// thus has lower adhesion. For now this effect will be neglected.
ExPolygon slice_poly = layer->lslices[slice_idx];
ExPolygons brim;
if (params.brim_type == BrimType::btOuterAndInner || params.brim_type == BrimType::btOuterOnly) {
Polygon brim_hole = slice_poly.contour;
brim_hole.reverse();
brim.push_back(ExPolygon{expand(slice_poly.contour, scale_(params.brim_width)).front(), brim_hole});
}
if (params.brim_type == BrimType::btOuterAndInner || params.brim_type == BrimType::btInnerOnly) {
Polygons brim_contours = slice_poly.holes;
polygons_reverse(brim_contours);
for (const Polygon &brim_contour : brim_contours) {
Polygons brim_holes = shrink({brim_contour}, scale_(params.brim_width));
polygons_reverse(brim_holes);
ExPolygon inner_brim{brim_contour};
inner_brim.holes = brim_holes;
brim.push_back(inner_brim);
}
}
for (const Polygon &poly : to_polygons(brim)) {
Vec2f p0 = unscaled(poly.first_point()).cast<float>();
for (size_t i = 2; i < poly.points.size(); i++) {
Vec2f p1 = unscaled(poly.points[i - 1]).cast<float>();
Vec2f p2 = unscaled(poly.points[i]).cast<float>();
float sign = cross2(p1 - p0, p2 - p1) > 0 ? 1.0f : -1.0f;
auto [area, first_moment_of_area, second_moment_area,
second_moment_of_area_covariance] = compute_moments_of_area_of_triangle(p0, p1, p2);
new_object_part.sticking_area += sign * area;
new_object_part.sticking_centroid_accumulator += sign * Vec3f(first_moment_of_area.x(), first_moment_of_area.y(),
layer->print_z * area);
new_object_part.sticking_second_moment_of_area_accumulator += sign * second_moment_area;
new_object_part.sticking_second_moment_of_area_covariance_accumulator += sign * second_moment_of_area_covariance;
}
}
}
return {new_object_part, area_covered_by_extrusions};
}
@ -621,11 +746,12 @@ public:
}
};
SupportPoints check_stability(const PrintObject *po, const PrintTryCancel& cancel_func, const Params &params)
std::tuple<SupportPoints, PartialObjects> check_stability(const PrintObject *po, const PrintTryCancel &cancel_func, const Params &params)
{
SupportPoints supp_points{};
SupportGridFilter supports_presence_grid(po, params.min_distance_between_support_points);
ActiveObjectParts active_object_parts{};
PartialObjects partial_objects{};
LD prev_layer_ext_perim_lines;
std::unordered_map<size_t, size_t> prev_slice_idx_to_object_part_mapping;
@ -633,6 +759,14 @@ SupportPoints check_stability(const PrintObject *po, const PrintTryCancel& cance
std::unordered_map<size_t, SliceConnection> prev_slice_idx_to_weakest_connection;
std::unordered_map<size_t, SliceConnection> next_slice_idx_to_weakest_connection;
auto remember_partial_object = [&active_object_parts, &partial_objects](size_t object_part_id) {
auto object_part = active_object_parts.access(object_part_id);
if (object_part.volume > EPSILON) {
partial_objects.emplace_back(object_part.volume_centroid_accumulator / object_part.volume, object_part.volume,
object_part.connected_to_bed);
}
};
for (size_t layer_idx = 0; layer_idx < po->layer_count(); ++layer_idx) {
cancel_func();
const Layer *layer = po->get_layer(layer_idx);
@ -641,7 +775,7 @@ SupportPoints check_stability(const PrintObject *po, const PrintTryCancel& cance
for (size_t slice_idx = 0; slice_idx < layer->lslices_ex.size(); ++slice_idx) {
const LayerSlice &slice = layer->lslices_ex.at(slice_idx);
auto [new_part, covered_area] = build_object_part_from_slice(slice, layer);
auto [new_part, covered_area] = build_object_part_from_slice(slice_idx, layer, params);
SliceConnection connection_to_below = estimate_slice_connection(slice_idx, layer);
#ifdef DETAILED_DEBUG_LOGS
@ -670,7 +804,10 @@ SupportPoints check_stability(const PrintObject *po, const PrintTryCancel& cance
final_part_id = *parts_ids.begin();
for (size_t part_id : parts_ids) {
if (final_part_id != part_id) { active_object_parts.merge(part_id, final_part_id); }
if (final_part_id != part_id) {
remember_partial_object(part_id);
active_object_parts.merge(part_id, final_part_id);
}
}
}
auto estimate_conn_strength = [bottom_z](const SliceConnection &conn) {
@ -731,7 +868,8 @@ SupportPoints check_stability(const PrintObject *po, const PrintTryCancel& cance
// Function that is used when new support point is generated. It will update the ObjectPart stability, weakest conneciton info,
// and the support presence grid and add the point to the issues.
auto reckon_new_support_point = [&part, &weakest_conn, &supp_points, &supports_presence_grid, &params,
&layer_idx](const Vec3f &support_point, float force, const Vec2f &dir) {
&layer_idx](SupportPointCause cause, const Vec3f &support_point, float force,
const Vec2f &dir) {
// if position is taken and point is for global stability (force > 0) or we are too close to the bed, do not add
// This allows local support points (e.g. bridging) to be generated densely
if ((supports_presence_grid.position_taken(support_point) && force > 0) || layer_idx <= 1) {
@ -739,14 +877,14 @@ SupportPoints check_stability(const PrintObject *po, const PrintTryCancel& cance
}
float area = params.support_points_interface_radius * params.support_points_interface_radius * float(PI);
// add the stability effect of the point only if the spot is not taken, so that the densely created local support points do not add
// unrealistic amount of stability to the object (due to overlaping of local support points)
// add the stability effect of the point only if the spot is not taken, so that the densely created local support points do
// not add unrealistic amount of stability to the object (due to overlaping of local support points)
if (!(supports_presence_grid.position_taken(support_point))) {
part.add_support_point(support_point, area);
}
float radius = params.support_points_interface_radius;
supp_points.emplace_back(support_point, force, radius, dir);
supp_points.emplace_back(cause, support_point, force, radius, dir);
supports_presence_grid.take_position(support_point);
// The support point also increases the stability of the weakest connection of the object, which should be reflected
@ -768,9 +906,11 @@ SupportPoints check_stability(const PrintObject *po, const PrintTryCancel& cance
const ExtrusionEntity *entity = fill_region->fills().entities[fill_idx];
if (entity->role() == ExtrusionRole::BridgeInfill) {
for (const ExtrusionLine &bridge :
check_extrusion_entity_stability(entity, fill_region, prev_layer_ext_perim_lines,prev_layer_boundary, params)) {
if (bridge.support_point_generated) {
reckon_new_support_point(create_support_point_position(bridge.b), -EPSILON, Vec2f::Zero());
check_extrusion_entity_stability(entity, fill_region, prev_layer_ext_perim_lines, prev_layer_boundary,
params)) {
if (bridge.support_point_generated.has_value()) {
reckon_new_support_point(*bridge.support_point_generated, create_support_point_position(bridge.b),
-EPSILON, Vec2f::Zero());
}
}
}
@ -783,10 +923,13 @@ SupportPoints check_stability(const PrintObject *po, const PrintTryCancel& cance
std::vector<ExtrusionLine> perims = check_extrusion_entity_stability(entity, perimeter_region,
prev_layer_ext_perim_lines,prev_layer_boundary, params);
for (const ExtrusionLine &perim : perims) {
if (perim.support_point_generated) {
reckon_new_support_point(create_support_point_position(perim.b), -EPSILON, Vec2f::Zero());
if (perim.support_point_generated.has_value()) {
reckon_new_support_point(*perim.support_point_generated, create_support_point_position(perim.b), -EPSILON,
Vec2f::Zero());
}
if (perim.is_external_perimeter()) {
current_slice_ext_perims_lines.push_back(perim);
}
if (perim.is_external_perimeter()) { current_slice_ext_perims_lines.push_back(perim); }
}
}
}
@ -795,7 +938,8 @@ SupportPoints check_stability(const PrintObject *po, const PrintTryCancel& cance
float unchecked_dist = params.min_distance_between_support_points + 1.0f;
for (const ExtrusionLine &line : current_slice_ext_perims_lines) {
if ((unchecked_dist + line.len < params.min_distance_between_support_points && line.curled_up_height < 0.3f) || line.len < EPSILON) {
if ((unchecked_dist + line.len < params.min_distance_between_support_points && line.curled_up_height < 0.3f) ||
line.len < EPSILON) {
unchecked_dist += line.len;
} else {
unchecked_dist = line.len;
@ -803,8 +947,10 @@ SupportPoints check_stability(const PrintObject *po, const PrintTryCancel& cance
auto [dist, nidx,
nearest_point] = current_slice_lines_distancer.distance_from_lines_extra<false>(pivot_site_search_point);
Vec3f support_point = create_support_point_position(nearest_point);
auto force = part.is_stable_while_extruding(weakest_conn, line, support_point, bottom_z, params);
if (force > 0) { reckon_new_support_point(support_point, force, (line.b - line.a).normalized()); }
auto [force, cause] = part.is_stable_while_extruding(weakest_conn, line, support_point, bottom_z, params);
if (force > 0) {
reckon_new_support_point(cause, support_point, force, (line.b - line.a).normalized());
}
}
}
current_layer_ext_perims_lines.insert(current_layer_ext_perims_lines.end(), current_slice_ext_perims_lines.begin(),
@ -812,11 +958,16 @@ SupportPoints check_stability(const PrintObject *po, const PrintTryCancel& cance
} // slice iterations
prev_layer_ext_perim_lines = LD(current_layer_ext_perims_lines);
} // layer iterations
return supp_points;
for (const auto& active_obj_pair : prev_slice_idx_to_object_part_mapping) {
remember_partial_object(active_obj_pair.second);
}
return {supp_points, partial_objects};
}
#ifdef DEBUG_FILES
void debug_export(SupportPoints support_points, std::string file_name)
void debug_export(const SupportPoints& support_points,const PartialObjects& objects, std::string file_name)
{
Slic3r::CNumericLocalesSetter locales_setter;
{
@ -827,13 +978,27 @@ void debug_export(SupportPoints support_points, std::string file_name)
}
for (size_t i = 0; i < support_points.size(); ++i) {
if (support_points[i].force <= 0) {
fprintf(fp, "v %f %f %f %f %f %f\n", support_points[i].position(0), support_points[i].position(1),
support_points[i].position(2), 0.0, 1.0, 0.0);
} else {
fprintf(fp, "v %f %f %f %f %f %f\n", support_points[i].position(0), support_points[i].position(1),
support_points[i].position(2), 1.0, 0.0, 0.0);
Vec3f color{1.0f, 1.0f, 1.0f};
switch (support_points[i].cause) {
case SupportPointCause::FloatingBridgeAnchor: color = {0.863281f, 0.109375f, 0.113281f}; break; //RED
case SupportPointCause::LongBridge: color = {0.960938f, 0.90625f, 0.0625f}; break; // YELLOW
case SupportPointCause::FloatingExtrusion: color = {0.921875f, 0.515625f, 0.101563f}; break; // ORANGE
case SupportPointCause::SeparationFromBed: color = {0.0f, 1.0f, 0.0}; break; // GREEN
case SupportPointCause::UnstableFloatingPart: color = {0.105469f, 0.699219f, 0.84375f}; break; // BLUE
case SupportPointCause::WeakObjectPart: color = {0.609375f, 0.210938f, 0.621094f}; break; // PURPLE
}
fprintf(fp, "v %f %f %f %f %f %f\n", support_points[i].position(0), support_points[i].position(1),
support_points[i].position(2), color[0], color[1], color[2]);
}
for (size_t i = 0; i < objects.size(); ++i) {
Vec3f color{1.0f, 0.0f, 1.0f};
if (objects[i].connected_to_bed) {
color = {1.0f, 0.0f, 0.0f};
}
fprintf(fp, "v %f %f %f %f %f %f\n", objects[i].centroid(0), objects[i].centroid(1), objects[i].centroid(2), color[0],
color[1], color[2]);
}
fclose(fp);
@ -841,17 +1006,15 @@ void debug_export(SupportPoints support_points, std::string file_name)
}
#endif
// std::vector<size_t> quick_search(const PrintObject *po, const Params &params) {
// return {};
// }
SupportPoints full_search(const PrintObject *po, const PrintTryCancel& cancel_func, const Params &params)
std::tuple<SupportPoints, PartialObjects> full_search(const PrintObject *po, const PrintTryCancel& cancel_func, const Params &params)
{
SupportPoints supp_points = check_stability(po, cancel_func, params);
auto results = check_stability(po, cancel_func, params);
#ifdef DEBUG_FILES
debug_export(supp_points, "issues");
auto [supp_points, objects] = results;
debug_export(supp_points, objects, "issues");
#endif
return supp_points;
return results;
}
void estimate_supports_malformations(SupportLayerPtrs &layers, float flow_width, const Params &params)
@ -998,5 +1161,84 @@ void estimate_malformations(LayerPtrs &layers, const Params &params)
#endif
}
void raise_alerts_for_issues(const SupportPoints &support_points,
PartialObjects &partial_objects,
std::function<void(PrintStateBase::WarningLevel, SupportPointCause)> alert_fn)
{
for (const SupportPoint &sp : support_points) {
if (sp.cause == SupportPointCause::SeparationFromBed) {
alert_fn(PrintStateBase::WarningLevel::NON_CRITICAL, SupportPointCause::SeparationFromBed);
break;
}
}
std::reverse(partial_objects.begin(), partial_objects.end());
std::sort(partial_objects.begin(), partial_objects.end(),
[](const PartialObject &left, const PartialObject &right) { return left.volume > right.volume; });
float max_volume_part = partial_objects.front().volume;
for (const PartialObject &p : partial_objects) {
if (p.volume > max_volume_part / 500.0f && !p.connected_to_bed) {
alert_fn(PrintStateBase::WarningLevel::CRITICAL, SupportPointCause::UnstableFloatingPart);
return;
}
}
for (const SupportPoint &sp : support_points) {
if (sp.cause == SupportPointCause::UnstableFloatingPart) {
alert_fn(PrintStateBase::WarningLevel::CRITICAL, SupportPointCause::UnstableFloatingPart);
return;
}
}
std::vector<SupportPoint> ext_supp_points{};
ext_supp_points.reserve(support_points.size());
for (const SupportPoint &sp : support_points) {
switch (sp.cause) {
case SupportPointCause::FloatingBridgeAnchor:
case SupportPointCause::FloatingExtrusion: ext_supp_points.push_back(sp); break;
default: break;
}
}
auto coord_fn = [&ext_supp_points](size_t idx, size_t dim) { return ext_supp_points[idx].position[dim]; };
KDTreeIndirect<3, float, decltype(coord_fn)> ext_points_tree{coord_fn, ext_supp_points.size()};
for (const SupportPoint &sp : ext_supp_points) {
auto cluster = find_nearby_points(ext_points_tree, sp.position, 3.0);
int score = 0;
bool floating_bridge = false;
for (size_t idx : cluster) {
score += ext_supp_points[idx].cause == SupportPointCause::FloatingBridgeAnchor ? 3 : 1;
floating_bridge = floating_bridge || ext_supp_points[idx].cause == SupportPointCause::FloatingBridgeAnchor;
}
if (score > 5) {
if (floating_bridge) {
alert_fn(PrintStateBase::WarningLevel::CRITICAL, SupportPointCause::FloatingBridgeAnchor);
} else {
alert_fn(PrintStateBase::WarningLevel::CRITICAL, SupportPointCause::FloatingExtrusion);
}
return;
}
}
if (ext_supp_points.size() > 5) {
alert_fn(PrintStateBase::WarningLevel::NON_CRITICAL, SupportPointCause::FloatingExtrusion);
}
for (const SupportPoint &sp : support_points) {
if (sp.cause == SupportPointCause::LongBridge) {
alert_fn(PrintStateBase::WarningLevel::NON_CRITICAL, SupportPointCause::LongBridge);
break;
}
}
for (const SupportPoint &sp : support_points) {
if (sp.cause == SupportPointCause::WeakObjectPart) {
alert_fn(PrintStateBase::WarningLevel::NON_CRITICAL, SupportPointCause::WeakObjectPart);
break;
}
}
}
} // namespace SupportSpotsGenerator
} // namespace Slic3r

View File

@ -4,43 +4,52 @@
#include "Layer.hpp"
#include "Line.hpp"
#include "PrintBase.hpp"
#include "PrintConfig.hpp"
#include <boost/log/trivial.hpp>
#include <cstddef>
#include <vector>
namespace Slic3r {
namespace SupportSpotsGenerator {
struct Params {
Params(const std::vector<std::string> &filament_types) {
struct Params
{
Params(
const std::vector<std::string> &filament_types, float max_acceleration, int raft_layers_count, BrimType brim_type, float brim_width)
: max_acceleration(max_acceleration), raft_layers_count(raft_layers_count), brim_type(brim_type), brim_width(brim_width)
{
if (filament_types.size() > 1) {
BOOST_LOG_TRIVIAL(warning)
<< "SupportSpotsGenerator does not currently handle different materials properly, only first will be used";
}
if (filament_types.empty() || filament_types[0].empty()) {
BOOST_LOG_TRIVIAL(error)
<< "SupportSpotsGenerator error: empty filament_type";
BOOST_LOG_TRIVIAL(error) << "SupportSpotsGenerator error: empty filament_type";
filament_type = std::string("PLA");
} else {
filament_type = filament_types[0];
BOOST_LOG_TRIVIAL(debug)
<< "SupportSpotsGenerator: applying filament type: " << filament_type;
BOOST_LOG_TRIVIAL(debug) << "SupportSpotsGenerator: applying filament type: " << filament_type;
}
}
// the algorithm should use the following units for all computations: distance [mm], mass [g], time [s], force [g*mm/s^2]
const float bridge_distance = 12.0f; //mm
const float bridge_distance = 12.0f; // mm
const float max_acceleration; // mm/s^2 ; max acceleration of object in XY -- should be applicable only to printers with bed slinger,
// however we do not have such info yet. The force is usually small anyway, so not such a big deal to include it everytime
const int raft_layers_count;
std::string filament_type;
BrimType brim_type;
const float brim_width;
const std::pair<float,float> malformation_distance_factors = std::pair<float, float> { 0.4, 1.2 };
const float max_curled_height_factor = 10.0f;
const float min_distance_between_support_points = 3.0f; //mm
const float support_points_interface_radius = 1.5f; // mm
const float connections_min_considerable_area = 1.5f; //mm^2
const float min_distance_to_allow_local_supports = 1.0f; //mm
std::string filament_type;
const float gravity_constant = 9806.65f; // mm/s^2; gravity acceleration on Earth's surface, algorithm assumes that printer is in upwards position.
const float max_acceleration = 9 * 1000.0f; // mm/s^2 ; max acceleration of object (bed) in XY (NOTE: The max hit is received by the object in the jerk phase, so the usual machine limits are too low)
const double filament_density = 1.25e-3f; // g/mm^3 ; Common filaments are very lightweight, so precise number is not that important
const double material_yield_strength = 33.0f * 1e6f; // (g*mm/s^2)/mm^2; 33 MPa is yield strength of ABS, which has the lowest yield strength from common materials.
const float standard_extruder_conflict_force = 20.0f * gravity_constant; // force that can occasionally push the model due to various factors (filament leaks, small curling, ... );
@ -48,10 +57,16 @@ struct Params {
// MPa * 1e^6 = (g*mm/s^2)/mm^2 = g/(mm*s^2); yield strength of the bed surface
double get_bed_adhesion_yield_strength() const {
if (raft_layers_count > 0) {
return get_support_spots_adhesion_strength() * 2.0;
}
if (filament_type == "PLA") {
return 0.018 * 1e6;
} else if (filament_type == "PET" || filament_type == "PETG") {
return 0.3 * 1e6;
} else if (filament_type == "ABS" || filament_type == "ASA") {
return 0.1 * 1e6; //TODO do measurements
} else { //PLA default value - defensive approach, PLA has quite low adhesion
return 0.018 * 1e6;
}
@ -63,28 +78,46 @@ struct Params {
}
};
// The support points are generated for two reasons:
enum class SupportPointCause {
LongBridge, // point generated on bridge and straight perimeter extrusion longer than the allowed length
FloatingBridgeAnchor, // point generated on unsupported bridge endpoint
FloatingExtrusion, // point generated on extrusion that does not hold on its own
SeparationFromBed, // point generated for object parts that are connected to the bed, but the area is too small and there is a risk of separation (brim may help)
UnstableFloatingPart, // point generated for object parts not connected to the bed, holded only by the other support points (brim will not help here)
WeakObjectPart // point generated when some part of the object is too weak to hold the upper part and may break (imagine hourglass)
};
// The support points can be sorted into two groups
// 1. Local extrusion support for extrusions that are printed in the air and would not
// withstand on their own (too long bridges, sharp turns in large overhang, concave bridge holes, etc.)
// These points have negative force (-EPSILON) and Vec2f::Zero() direction
// The algorithm still expects that these points will be supported and accounts for them in the global stability check
// The algorithm still expects that these points will be supported and accounts for them in the global stability check.
// 2. Global stability support points are generated at each spot, where the algorithm detects that extruding the current line
// may cause separation of the object part from the bed and/or its support spots or crack in the weak connection of the object parts
// may cause separation of the object part from the bed and/or its support spots or crack in the weak connection of the object parts.
// The generated point's direction is the estimated falling direction of the object part, and the force is equal to te difference
// between forces that destabilize the object (extruder conflicts with curled filament, weight if instable center of mass, bed movements etc)
// and forces that stabilize the object (bed adhesion, other support spots adhesion, weight if stable center of mass)
// and forces that stabilize the object (bed adhesion, other support spots adhesion, weight if stable center of mass).
// Note that the force is only the difference - the amount needed to stabilize the object again.
struct SupportPoint {
SupportPoint(const Vec3f &position, float force, float spot_radius, const Vec2f &direction);
bool is_local_extrusion_support() const { return force < 0; }
struct SupportPoint
{
SupportPoint(SupportPointCause cause, const Vec3f &position, float force, float spot_radius, const Vec2f &direction)
: cause(cause), position(position), force(force), spot_radius(spot_radius), direction(direction)
{}
bool is_local_extrusion_support() const
{
return cause == SupportPointCause::LongBridge || cause == SupportPointCause::FloatingExtrusion;
}
bool is_global_object_support() const { return !is_local_extrusion_support(); }
//position is in unscaled coords. The z coordinate is aligned with the layers bottom_z coordiantes
SupportPointCause cause; // reason why this support point was generated. Used for the user alerts
// position is in unscaled coords. The z coordinate is aligned with the layers bottom_z coordiantes
Vec3f position;
// force that destabilizes the object to the point of falling/breaking. It is in g*mm/s^2 units
// values gathered from large XL print: Min : 0 | Max : 18713800 | Average : 1361186 | Median : 329103
// force that destabilizes the object to the point of falling/breaking. g*mm/s^2 units
// It is valid only for global_object_support. For local extrusion support points, the force is -EPSILON
// values gathered from large XL model: Min : 0 | Max : 18713800 | Average : 1361186 | Median : 329103
// For reference 18713800 is weight of 1.8 Kg object, 329103 is weight of 0.03 Kg
// The final printed object weight was approx 0.5 Kg
// The final sliced object weight was approx 0.5 Kg
float force;
// Expected spot size. The support point strength is calculated from the area defined by this value.
// Currently equal to the support_points_interface_radius parameter above
@ -99,13 +132,28 @@ struct Malformations {
std::vector<Lines> layers; //for each layer
};
// std::vector<size_t> quick_search(const PrintObject *po, const Params &params);
SupportPoints full_search(const PrintObject *po, const PrintTryCancel& cancel_func, const Params &params);
struct PartialObject
{
PartialObject(Vec3f centroid, float volume, bool connected_to_bed)
: centroid(centroid), volume(volume), connected_to_bed(connected_to_bed)
{}
void estimate_supports_malformations(std::vector<SupportLayer*> &layers, float supports_flow_width, const Params &params);
void estimate_malformations(std::vector<Layer*> &layers, const Params &params);
Vec3f centroid;
float volume;
bool connected_to_bed;
};
} // namespace SupportSpotsGenerator
}
using PartialObjects = std::vector<PartialObject>;
std::tuple<SupportPoints, PartialObjects> full_search(const PrintObject *po, const PrintTryCancel& cancel_func, const Params &params);
void estimate_supports_malformations(std::vector<SupportLayer *> &layers, float supports_flow_width, const Params &params);
void estimate_malformations(std::vector<Layer *> &layers, const Params &params);
void raise_alerts_for_issues(const SupportPoints &support_points,
PartialObjects &partial_objects,
std::function<void(PrintStateBase::WarningLevel, SupportPointCause)> alert_fn);
}} // namespace Slic3r::SupportSpotsGenerator
#endif /* SRC_LIBSLIC3R_SUPPORTABLEISSUESSEARCH_HPP_ */

View File

@ -1751,7 +1751,7 @@ PageBuildVolume::PageBuildVolume(ConfigWizard* parent)
: ConfigWizardPage(parent, _L("Build Volume"), _L("Build Volume"), 1)
, build_volume(new DiamTextCtrl(this))
{
append_text(_L("Set verctical size of your printer."));
append_text(_L("Set vertical size of your printer."));
wxString value = "200";
build_volume->SetValue(value);

View File

@ -119,8 +119,8 @@ void FileGet::priv::get_perform()
wxString temp_path_wstring(m_tmp_path.wstring());
std::cout << "dest_path: " << dest_path.string() << std::endl;
std::cout << "m_tmp_path: " << m_tmp_path.string() << std::endl;
//std::cout << "dest_path: " << dest_path.string() << std::endl;
//std::cout << "m_tmp_path: " << m_tmp_path.string() << std::endl;
BOOST_LOG_TRIVIAL(info) << GUI::format("Starting download from %1% to %2%. Temp path is %3%",m_url, dest_path, m_tmp_path);

View File

@ -762,28 +762,26 @@ void SpinCtrl::BUILD() {
if (m_opt.width >= 0) size.SetWidth(m_opt.width*m_em_unit);
wxString text_value = wxString("");
int default_value = 0;
int default_value = UNDEF_VALUE;
switch (m_opt.type) {
case coInt:
default_value = m_opt.default_value->getInt();
text_value = wxString::Format(_T("%i"), default_value);
break;
case coInts:
{
const ConfigOptionInts *vec = m_opt.get_default_value<ConfigOptionInts>();
if (vec == nullptr || vec->empty()) break;
for (size_t id = 0; id < vec->size(); ++id)
{
default_value = vec->get_at(id);
text_value += wxString::Format(_T("%i"), default_value);
}
default_value = m_opt.get_default_value<ConfigOptionInts>()->get_at(m_opt_idx);
if (m_opt.nullable)
m_last_meaningful_value = default_value == ConfigOptionIntsNullable::nil_value() ? static_cast<int>(m_opt.max) : default_value;
break;
}
default:
break;
}
if (default_value != UNDEF_VALUE)
text_value = wxString::Format(_T("%i"), default_value);
const int min_val = m_opt.min == -FLT_MAX
#ifdef __WXOSX__
// We will forcibly set the input value for SpinControl, since the value
@ -882,6 +880,50 @@ void SpinCtrl::BUILD() {
window = dynamic_cast<wxWindow*>(temp);
}
void SpinCtrl::set_value(const boost::any& value, bool change_event/* = false*/)
{
m_disable_change_event = !change_event;
tmp_value = boost::any_cast<int>(value);
m_value = value;
if (m_opt.nullable) {
const bool m_is_na_val = tmp_value == ConfigOptionIntsNullable::nil_value();
if (m_is_na_val)
dynamic_cast<wxSpinCtrl*>(window)->SetValue(na_value());
else {
m_last_meaningful_value = value;
dynamic_cast<wxSpinCtrl*>(window)->SetValue(tmp_value);
}
}
else
dynamic_cast<wxSpinCtrl*>(window)->SetValue(tmp_value);
m_disable_change_event = false;
}
void SpinCtrl::set_last_meaningful_value()
{
const int val = boost::any_cast<int>(m_last_meaningful_value);
dynamic_cast<wxSpinCtrl*>(window)->SetValue(val);
tmp_value = val;
propagate_value();
}
void SpinCtrl::set_na_value()
{
dynamic_cast<wxSpinCtrl*>(window)->SetValue(na_value());
m_value = ConfigOptionIntsNullable::nil_value();
propagate_value();
}
boost::any& SpinCtrl::get_value()
{
wxSpinCtrl* spin = static_cast<wxSpinCtrl*>(window);
if (spin->GetTextValue() == na_value())
return m_value;
int value = spin->GetValue();
return m_value = value;
}
void SpinCtrl::propagate_value()
{
// check if value was really changed

View File

@ -330,24 +330,18 @@ public:
void BUILD() override;
/// Propagate value from field to the OptionGroupe and Config after kill_focus/ENTER
void propagate_value() ;
/*
void set_value(const std::string& value, bool change_event = false) {
m_disable_change_event = !change_event;
dynamic_cast<wxSpinCtrl*>(window)->SetValue(value);
m_disable_change_event = false;
}
void set_value(const boost::any& value, bool change_event = false) override {
m_disable_change_event = !change_event;
tmp_value = boost::any_cast<int>(value);
m_value = value;
dynamic_cast<wxSpinCtrl*>(window)->SetValue(tmp_value);
m_disable_change_event = false;
}
*/
void set_value(const boost::any& value, bool change_event = false) override;
void set_last_meaningful_value() override;
void set_na_value() override;
boost::any& get_value() override {
int value = static_cast<wxSpinCtrl*>(window)->GetValue();
return m_value = value;
}
boost::any& get_value() override;
void msw_rescale() override;

View File

@ -43,8 +43,8 @@ bool GLGizmoFdmSupports::on_init()
{
m_shortcut_key = WXK_CONTROL_L;
m_desc["auto_generate"] = _L("Auto-generate supports");
m_desc["generating"] = _L("Generating supports...");
m_desc["autopaint"] = _L("Automatic painting");
m_desc["painting"] = _L("painting...");
m_desc["clipping_of_view"] = _L("Clipping of view") + ": ";
m_desc["reset_direction"] = _L("Reset direction");
m_desc["cursor_size"] = _L("Brush size") + ": ";
@ -160,9 +160,9 @@ void GLGizmoFdmSupports::on_render_input_window(float x, float y, float bottom_l
ImGui::Separator();
if (waiting_for_autogenerated_supports) {
m_imgui->text(m_desc.at("generating"));
m_imgui->text(m_desc.at("painting"));
} else {
bool generate = m_imgui->button(m_desc.at("auto_generate"));
bool generate = m_imgui->button(m_desc.at("autopaint"));
if (generate)
auto_generate();
}
@ -525,12 +525,12 @@ void GLGizmoFdmSupports::auto_generate()
});
MessageDialog dlg(GUI::wxGetApp().plater(),
_L("Autogeneration will erase all currently painted areas.") + "\n\n" +
_L("Automatic painting will erase all currently painted areas.") + "\n\n" +
_L("Are you sure you want to do it?") + "\n",
_L("Warning"), wxICON_WARNING | wxYES | wxNO);
if (not_painted || dlg.ShowModal() == wxID_YES) {
Plater::TakeSnapshot snapshot(wxGetApp().plater(), _L("Autogenerate support points"));
Plater::TakeSnapshot snapshot(wxGetApp().plater(), _L("Automatic painting support points"));
int mesh_id = -1.0f;
for (ModelVolume *mv : mo->volumes) {
if (mv->is_model_part()) {

View File

@ -1826,13 +1826,8 @@ static void validate_custom_gcode_cb(Tab* tab, const wxString& title, const t_co
tab->on_value_change(opt_key, value);
}
void TabFilament::add_filament_overrides_page()
void TabFilament::create_line_with_near_label_widget(ConfigOptionsGroupShp optgroup, const std::string& opt_key, int opt_index/* = 0*/)
{
PageShp page = add_options_page(L("Filament Overrides"), "wrench");
ConfigOptionsGroupShp optgroup = page->new_optgroup(L("Retraction"));
auto append_single_option_line = [optgroup, this](const std::string& opt_key, int opt_index)
{
Line line {"",""};
if (opt_key == "filament_retract_lift_above" || opt_key == "filament_retract_lift_below") {
Option opt = optgroup->get_option(opt_key);
@ -1863,7 +1858,26 @@ void TabFilament::add_filament_overrides_page()
};
optgroup->append_line(line);
};
}
void TabFilament::update_line_with_near_label_widget(ConfigOptionsGroupShp optgroup, const std::string& opt_key, int opt_index/* = 0*/, bool is_checked/* = true*/)
{
if (!m_overrides_options[opt_key])
return;
m_overrides_options[opt_key]->Enable(is_checked);
is_checked &= !m_config->option(opt_key)->is_nil();
m_overrides_options[opt_key]->SetValue(is_checked);
Field* field = optgroup->get_fieldc(opt_key, opt_index);
if (field != nullptr)
field->toggle(is_checked);
}
void TabFilament::add_filament_overrides_page()
{
PageShp page = add_options_page(L("Filament Overrides"), "wrench");
ConfigOptionsGroupShp optgroup = page->new_optgroup(L("Retraction"));
const int extruder_idx = 0; // #ys_FIXME
@ -1879,7 +1893,7 @@ void TabFilament::add_filament_overrides_page()
"filament_wipe",
"filament_retract_before_wipe"
})
append_single_option_line(opt_key, extruder_idx);
create_line_with_near_label_widget(optgroup, opt_key, extruder_idx);
}
void TabFilament::update_filament_overrides_page()
@ -1914,14 +1928,7 @@ void TabFilament::update_filament_overrides_page()
for (const std::string& opt_key : opt_keys)
{
bool is_checked = opt_key=="filament_retract_length" ? true : have_retract_length;
m_overrides_options[opt_key]->Enable(is_checked);
is_checked &= !m_config->option(opt_key)->is_nil();
m_overrides_options[opt_key]->SetValue(is_checked);
Field* field = optgroup->get_fieldc(opt_key, extruder_idx);
if (field != nullptr)
field->toggle(is_checked);
update_line_with_near_label_widget(optgroup, opt_key, extruder_idx, is_checked);
}
}
@ -1952,6 +1959,9 @@ void TabFilament::build()
};
optgroup = page->new_optgroup(L("Temperature"));
create_line_with_near_label_widget(optgroup, "idle_temperature");
Line line = { L("Nozzle"), "" };
line.append_option(optgroup->get_option("first_layer_temperature"));
line.append_option(optgroup->get_option("temperature"));
@ -2142,6 +2152,14 @@ void TabFilament::toggle_options()
if (m_active_page->title() == "Filament Overrides")
update_filament_overrides_page();
if (m_active_page->title() == "Filament") {
Page* page = m_active_page;
const auto og_it = std::find_if(page->m_optgroups.begin(), page->m_optgroups.end(), [](const ConfigOptionsGroupShp og) { return og->title == "Temperature"; });
if (og_it != page->m_optgroups.end())
update_line_with_near_label_widget(*og_it, "idle_temperature");
}
}
void TabFilament::update()

View File

@ -436,6 +436,8 @@ private:
ogStaticText* m_volumetric_speed_description_line {nullptr};
ogStaticText* m_cooling_description_line {nullptr};
void create_line_with_near_label_widget(ConfigOptionsGroupShp optgroup, const std::string &opt_key, int opt_index = 0);
void update_line_with_near_label_widget(ConfigOptionsGroupShp optgroup, const std::string &opt_key, int opt_index = 0, bool is_checked = true);
void add_filament_overrides_page();
void update_filament_overrides_page();
void update_volumetric_flow_preset_hints();

View File

@ -167,10 +167,14 @@ AppUpdateDownloadDialog::AppUpdateDownloadDialog( const Semver& ver_online, boos
wxString wxext = boost::nowide::widen(extension);
wildcard = GUI::format_wxstr("%1% Files (*.%2%)|*.%2%", wxext.Upper(), wxext);
}
boost::system::error_code ec;
boost::filesystem::path dir = boost::filesystem::absolute(boost::filesystem::path(GUI::format(txtctrl_path->GetValue())), ec);
if (ec)
dir = GUI::format(txtctrl_path->GetValue());
wxDirDialog save_dlg(
this
, _L("Select directory:")
, txtctrl_path->GetValue()
, GUI::format_wxstr(dir.string())
/*
, filename //boost::nowide::widen(AppUpdater::get_filename_from_url(txtctrl_path->GetValue().ToUTF8().data()))
, wildcard
@ -188,35 +192,46 @@ AppUpdateDownloadDialog::AppUpdateDownloadDialog( const Semver& ver_online, boos
if (auto* btn_ok = get_button(wxID_OK); btn_ok != NULL) {
btn_ok->SetLabel(_L("Download"));
btn_ok->Bind(wxEVT_BUTTON, ([this, path](wxCommandEvent& e){
boost::filesystem::path path = boost::filesystem::path(txtctrl_path->GetValue().ToUTF8().data()) / GUI::format(filename);
boost::system::error_code ec;
if (path.parent_path().string().empty()) {
std::string input = GUI::into_u8(txtctrl_path->GetValue());
boost::filesystem::path dir = boost::filesystem::absolute(boost::filesystem::path(input), ec);
if (ec)
dir = boost::filesystem::path(input);
bool show_change = (dir.string() != input);
boost::filesystem::path path = dir / GUI::format(filename);
ec.clear();
if (dir.string().empty()) {
MessageDialog msgdlg(nullptr, _L("Directory path is empty."), _L("Notice"), wxOK);
msgdlg.ShowModal();
return;
}
ec.clear();
if (!boost::filesystem::exists(path.parent_path(), ec) || !boost::filesystem::is_directory(path.parent_path(),ec) || ec) {
if (!boost::filesystem::exists(dir, ec) || !boost::filesystem::is_directory(dir,ec) || ec) {
ec.clear();
if (!boost::filesystem::exists(path.parent_path().parent_path(), ec) || !boost::filesystem::is_directory(path.parent_path().parent_path(), ec) || ec) {
if (!boost::filesystem::exists(dir.parent_path(), ec) || !boost::filesystem::is_directory(dir.parent_path(), ec) || ec) {
MessageDialog msgdlg(nullptr, _L("Directory path is incorrect."), _L("Notice"), wxOK);
msgdlg.ShowModal();
return;
}
MessageDialog msgdlg(nullptr, GUI::format_wxstr(_L("Directory %1% doesn't exists. Do you wish to create it?"), GUI::format_wxstr(path.parent_path().string())), _L("Notice"), wxYES_NO);
show_change = false;
MessageDialog msgdlg(nullptr, GUI::format_wxstr(_L("Directory %1% doesn't exists. Do you wish to create it?"), dir.string()), _L("Notice"), wxYES_NO);
if (msgdlg.ShowModal() != wxID_YES)
return;
ec.clear();
if(!boost::filesystem::create_directory(path.parent_path(), ec) || ec)
{
if(!boost::filesystem::create_directory(dir, ec) || ec) {
MessageDialog msgdlg(nullptr, _L("Failed to create directory."), _L("Notice"), wxOK);
msgdlg.ShowModal();
return;
}
}
if (boost::filesystem::exists(path)) {
MessageDialog msgdlg(nullptr, GUI::format_wxstr(_L("File %1% already exists. Do you wish to overwrite it?"), GUI::format_wxstr(path.string())),_L("Notice"), wxYES_NO);
show_change = false;
MessageDialog msgdlg(nullptr, GUI::format_wxstr(_L("File %1% already exists. Do you wish to overwrite it?"), path.string()),_L("Notice"), wxYES_NO);
if (msgdlg.ShowModal() != wxID_YES)
return;
}
if (show_change) {
MessageDialog msgdlg(nullptr, GUI::format_wxstr(_L("Download path is %1%. Do you wish to continue?"), path.string()), _L("Notice"), wxYES_NO);
if (msgdlg.ShowModal() != wxID_YES)
return;
}
@ -241,7 +256,12 @@ bool AppUpdateDownloadDialog::run_after_download() const
boost::filesystem::path AppUpdateDownloadDialog::get_download_path() const
{
return boost::filesystem::path(txtctrl_path->GetValue().ToUTF8().data()) / GUI::format(filename);
boost::system::error_code ec;
std::string input = GUI::format(txtctrl_path->GetValue());
boost::filesystem::path dir = boost::filesystem::absolute(boost::filesystem::path(input), ec);
if (ec)
dir = boost::filesystem::path(input);
return dir / GUI::format(filename);
}
// MsgUpdateConfig

View File

@ -13,6 +13,7 @@
#include "slic3r/GUI/format.hpp"
#include "slic3r/GUI/GUI_App.hpp"
#include "slic3r/GUI/GUI.hpp"
#include "slic3r/GUI/I18N.hpp"
#include "slic3r/Utils/Http.hpp"
#include "libslic3r/Utils.hpp"
@ -36,7 +37,7 @@ namespace {
std::string msg;
bool res = GUI::create_process(path, std::wstring(), msg);
if (!res) {
std::string full_message = GUI::format("Running downloaded instaler of %1% has failed:\n%2%", SLIC3R_APP_NAME, msg);
std::string full_message = GUI::format(_utf8("Running downloaded instaler of %1% has failed:\n%2%"), SLIC3R_APP_NAME, msg);
BOOST_LOG_TRIVIAL(error) << full_message; // lm: maybe UI error msg? // dk: bellow. (maybe some general show error evt would be better?)
wxCommandEvent* evt = new wxCommandEvent(EVT_SLIC3R_APP_DOWNLOAD_FAILED);
evt->SetString(full_message);
@ -170,8 +171,10 @@ bool AppUpdater::priv::http_get_file(const std::string& url, size_t size_limit,
// progress function returns true as success (to continue)
cancel = (m_cancel ? true : !progress_fn(std::move(progress)));
if (cancel) {
error_message = GUI::format("Error getting: `%1%`: Download was canceled.", //lm:typo //dk: am i blind? :)
url);
// Lets keep error_message empty here - if there is need to show error dialog, the message will be probably shown by whatever caused the cancel.
/*
error_message = GUI::format(_utf8("Error getting: `%1%`: Download was canceled."), url);
*/
BOOST_LOG_TRIVIAL(debug) << "AppUpdater::priv::http_get_file message: "<< error_message;
}
})
@ -200,7 +203,30 @@ boost::filesystem::path AppUpdater::priv::download_file(const DownloadAppData& d
assert(!dest_path.empty());
if (dest_path.empty())
{
BOOST_LOG_TRIVIAL(error) << "Download from " << data.url << " could not start. Destination path is empty.";
std::string line1 = GUI::format(_utf8("Internal download error for url %1%:"), data.url);
std::string line2 = _utf8("Destination path is empty.");
std::string message = GUI::format("%1%\n%2%", line1, line2);
BOOST_LOG_TRIVIAL(error) << message;
wxCommandEvent* evt = new wxCommandEvent(EVT_SLIC3R_APP_DOWNLOAD_FAILED);
evt->SetString(message);
GUI::wxGetApp().QueueEvent(evt);
return boost::filesystem::path();
}
boost::filesystem::path tmp_path = dest_path;
tmp_path += format(".%1%%2%", get_current_pid(), ".download");
FILE* file;
wxString temp_path_wstring(tmp_path.wstring());
file = fopen(temp_path_wstring.c_str(), "wb");
assert(file != NULL);
if (file == NULL) {
std::string line1 = GUI::format(_utf8("Download from %1% couldn't start:"), data.url);
std::string line2 = GUI::format(_utf8("Can't create file at %1%."), tmp_path.string());
std::string message = GUI::format("%1%\n%2%", line1, line2);
BOOST_LOG_TRIVIAL(error) << message;
wxCommandEvent* evt = new wxCommandEvent(EVT_SLIC3R_APP_DOWNLOAD_FAILED);
evt->SetString(message);
GUI::wxGetApp().QueueEvent(evt);
return boost::filesystem::path();
}
@ -217,7 +243,7 @@ boost::filesystem::path AppUpdater::priv::download_file(const DownloadAppData& d
GUI::wxGetApp().QueueEvent(evt);
return false;
} else if (progress.dltotal > 0 && progress.dltotal < expected_size) {
//lm:When will this happen? Is that not an error? // dk: It is possible error, but we cannot know until the download is finished. Somehow the total size can grow during the download.
// This is possible error, but we cannot know until the download is finished. Somehow the total size can grow during the download.
BOOST_LOG_TRIVIAL(info) << GUI::format("Downloading new %1% has incorrect size. The download will continue. \nExpected size: %2%\nDownload size: %3%", SLIC3R_APP_NAME, expected_size, progress.dltotal);
}
// progress event
@ -232,27 +258,26 @@ boost::filesystem::path AppUpdater::priv::download_file(const DownloadAppData& d
return true;
}
// on_complete
, [dest_path, expected_size](std::string body, std::string& error_message){
, [&file, dest_path, tmp_path, expected_size](std::string body, std::string& error_message){
// Size check. Does always 1 char == 1 byte?
size_t body_size = body.size();
if (body_size != expected_size) {
//lm:UI message? // dk: changed. Now it propagates to UI.
error_message = GUI::format("Downloaded file has wrong size. Expected size: %1% Downloaded size: %2%", expected_size, body_size);
error_message = GUI::format(_utf8("Downloaded file has wrong size. Expected size: %1% Downloaded size: %2%"), expected_size, body_size);
return false;
}
if (file == NULL) {
error_message = GUI::format(_utf8("Can't create file at %1%."), tmp_path.string());
return false;
}
boost::filesystem::path tmp_path = dest_path;
tmp_path += format(".%1%%2%", get_current_pid(), ".download");
try
{
FILE* file;
file = fopen(tmp_path.string().c_str(), "wb");
fwrite(body.c_str(), 1, body.size(), file);
fclose(file);
boost::filesystem::rename(tmp_path, dest_path);
}
catch (const std::exception& e)
{
error_message = GUI::format("Failed to write and move %1% to %2%:/n%3%", tmp_path, dest_path, e.what());
error_message = GUI::format(_utf8("Failed to write to file or to move %1% to %2%:\n%3%"), tmp_path, dest_path, e.what());
return false;
}
return true;
@ -261,16 +286,19 @@ boost::filesystem::path AppUpdater::priv::download_file(const DownloadAppData& d
);
if (!res)
{
if (m_cancel)
{
BOOST_LOG_TRIVIAL(info) << error_message; //lm:Is this an error? // dk: changed to info
if (m_cancel) {
BOOST_LOG_TRIVIAL(info) << error_message;
wxCommandEvent* evt = new wxCommandEvent(EVT_SLIC3R_APP_DOWNLOAD_FAILED); // FAILED with empty msg only closes progress notification
GUI::wxGetApp().QueueEvent(evt);
} else {
std::string message = GUI::format("Downloading new %1% has failed:\n%2%", SLIC3R_APP_NAME, error_message);
BOOST_LOG_TRIVIAL(error) << message;
std::string message = (error_message.empty()
? std::string()
: GUI::format(_utf8("Downloading new %1% has failed:\n%2%"), SLIC3R_APP_NAME, error_message));
wxCommandEvent* evt = new wxCommandEvent(EVT_SLIC3R_APP_DOWNLOAD_FAILED);
if (!message.empty()) {
BOOST_LOG_TRIVIAL(error) << message;
evt->SetString(message);
}
GUI::wxGetApp().QueueEvent(evt);
}
return boost::filesystem::path();

View File

@ -108,18 +108,18 @@ SCENARIO("Ooze prevention", "[Multi]")
Polygon convex_hull = Geometry::convex_hull(extrusion_points);
THEN("all nozzles are outside skirt at toolchange") {
Points t;
sort_remove_duplicates(toolchange_points);
size_t inside = 0;
for (const auto &point : toolchange_points)
for (const Vec2d &offset : print_config.extruder_offset.values) {
Point p = point + scaled<coord_t>(offset);
if (convex_hull.contains(p))
++ inside;
}
REQUIRE(inside == 0);
}
// THEN("all nozzles are outside skirt at toolchange") {
// Points t;
// sort_remove_duplicates(toolchange_points);
// size_t inside = 0;
// for (const auto &point : toolchange_points)
// for (const Vec2d &offset : print_config.extruder_offset.values) {
// Point p = point + scaled<coord_t>(offset);
// if (convex_hull.contains(p))
// ++ inside;
// }
// REQUIRE(inside == 0);
// }
#if 0
require "Slic3r/SVG.pm";