Refactored cooling logic for readibility and maintainability.
This commit is contained in:
parent
269770bbbc
commit
cbaf0ccc51
@ -79,6 +79,7 @@ $config->set('disable_fan_first_layers', [ 0 ]);
|
|||||||
"G1 X50 F2500\n" .
|
"G1 X50 F2500\n" .
|
||||||
"G1 F3000;_EXTRUDE_SET_SPEED\n" .
|
"G1 F3000;_EXTRUDE_SET_SPEED\n" .
|
||||||
"G1 X100 E1\n" .
|
"G1 X100 E1\n" .
|
||||||
|
";_EXTRUDE_END\n" .
|
||||||
"G1 E4 F400",
|
"G1 E4 F400",
|
||||||
# Print time of $gcode.
|
# Print time of $gcode.
|
||||||
my $print_time = 50 / (2500 / 60) + 100 / (3000 / 60) + 4 / (400 / 60);
|
my $print_time = 50 / (2500 / 60) + 100 / (3000 / 60) + 4 / (400 / 60);
|
||||||
|
@ -89,8 +89,9 @@ struct PerExtruderAdjustments
|
|||||||
time_total += line.time;
|
time_total += line.time;
|
||||||
return time_total;
|
return time_total;
|
||||||
}
|
}
|
||||||
// Calculate the maximum time when slowing down.
|
// Calculate the total elapsed time when slowing down
|
||||||
float maximum_time(bool slowdown_external_perimeters) {
|
// to the minimum extrusion feed rate defined for the current material.
|
||||||
|
float maximum_time_after_slowdown(bool slowdown_external_perimeters) {
|
||||||
float time_total = 0.f;
|
float time_total = 0.f;
|
||||||
for (const CoolingLine &line : lines)
|
for (const CoolingLine &line : lines)
|
||||||
if (line.adjustable(slowdown_external_perimeters)) {
|
if (line.adjustable(slowdown_external_perimeters)) {
|
||||||
@ -102,6 +103,7 @@ struct PerExtruderAdjustments
|
|||||||
time_total += line.time;
|
time_total += line.time;
|
||||||
return time_total;
|
return time_total;
|
||||||
}
|
}
|
||||||
|
// Calculate the adjustable part of the total time.
|
||||||
float adjustable_time(bool slowdown_external_perimeters) {
|
float adjustable_time(bool slowdown_external_perimeters) {
|
||||||
float time_total = 0.f;
|
float time_total = 0.f;
|
||||||
for (const CoolingLine &line : lines)
|
for (const CoolingLine &line : lines)
|
||||||
@ -117,7 +119,9 @@ struct PerExtruderAdjustments
|
|||||||
time_total += line.time;
|
time_total += line.time;
|
||||||
return time_total;
|
return time_total;
|
||||||
}
|
}
|
||||||
float slow_down_maximum(bool slowdown_external_perimeters) {
|
// Slow down the adjustable extrusions to the minimum feedrate allowed for the current extruder material.
|
||||||
|
// Used by both proportional and non-proportional slow down.
|
||||||
|
float slowdown_to_minimum_feedrate(bool slowdown_external_perimeters) {
|
||||||
float time_total = 0.f;
|
float time_total = 0.f;
|
||||||
for (CoolingLine &line : lines) {
|
for (CoolingLine &line : lines) {
|
||||||
if (line.adjustable(slowdown_external_perimeters)) {
|
if (line.adjustable(slowdown_external_perimeters)) {
|
||||||
@ -130,6 +134,8 @@ struct PerExtruderAdjustments
|
|||||||
}
|
}
|
||||||
return time_total;
|
return time_total;
|
||||||
}
|
}
|
||||||
|
// Slow down each adjustable G-code line proportionally by a factor.
|
||||||
|
// Used by the proportional slow down.
|
||||||
float slow_down_proportional(float factor, bool slowdown_external_perimeters) {
|
float slow_down_proportional(float factor, bool slowdown_external_perimeters) {
|
||||||
assert(factor >= 1.f);
|
assert(factor >= 1.f);
|
||||||
float time_total = 0.f;
|
float time_total = 0.f;
|
||||||
@ -144,9 +150,8 @@ struct PerExtruderAdjustments
|
|||||||
return time_total;
|
return time_total;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool operator<(const PerExtruderAdjustments &rhs) const { return this->extruder_id < rhs.extruder_id; }
|
|
||||||
|
|
||||||
// Sort the lines, adjustable first, higher feedrate first.
|
// Sort the lines, adjustable first, higher feedrate first.
|
||||||
|
// Used by non-proportional slow down.
|
||||||
void sort_lines_by_decreasing_feedrate() {
|
void sort_lines_by_decreasing_feedrate() {
|
||||||
std::sort(lines.begin(), lines.end(), [](const CoolingLine &l1, const CoolingLine &l2) {
|
std::sort(lines.begin(), lines.end(), [](const CoolingLine &l1, const CoolingLine &l2) {
|
||||||
bool adj1 = l1.adjustable();
|
bool adj1 = l1.adjustable();
|
||||||
@ -161,21 +166,25 @@ struct PerExtruderAdjustments
|
|||||||
time_non_adjustable += lines[i].time;
|
time_non_adjustable += lines[i].time;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate the maximum time when slowing down.
|
// Calculate the maximum time stretch when slowing down to min_feedrate.
|
||||||
float time_stretch_when_slowing_down_to(float min_feedrate) {
|
// Slowdown to min_feedrate shall be allowed for this extruder's material.
|
||||||
|
// Used by non-proportional slow down.
|
||||||
|
float time_stretch_when_slowing_down_to_feedrate(float min_feedrate) {
|
||||||
float time_stretch = 0.f;
|
float time_stretch = 0.f;
|
||||||
if (this->min_print_speed < min_feedrate + EPSILON) {
|
assert(this->min_print_speed < min_feedrate + EPSILON);
|
||||||
for (size_t i = 0; i < n_lines_adjustable; ++ i) {
|
for (size_t i = 0; i < n_lines_adjustable; ++ i) {
|
||||||
const CoolingLine &line = lines[i];
|
const CoolingLine &line = lines[i];
|
||||||
if (line.feedrate > min_feedrate)
|
if (line.feedrate > min_feedrate)
|
||||||
time_stretch += line.time * (line.feedrate / min_feedrate - 1.f);
|
time_stretch += line.time * (line.feedrate / min_feedrate - 1.f);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return time_stretch;
|
return time_stretch;
|
||||||
}
|
}
|
||||||
|
|
||||||
void slow_down_to(float min_feedrate) {
|
// Slow down all adjustable lines down to min_feedrate.
|
||||||
if (this->min_print_speed < min_feedrate + EPSILON) {
|
// Slowdown to min_feedrate shall be allowed for this extruder's material.
|
||||||
|
// Used by non-proportional slow down.
|
||||||
|
void slow_down_to_feedrate(float min_feedrate) {
|
||||||
|
assert(this->min_print_speed < min_feedrate + EPSILON);
|
||||||
for (size_t i = 0; i < n_lines_adjustable; ++ i) {
|
for (size_t i = 0; i < n_lines_adjustable; ++ i) {
|
||||||
CoolingLine &line = lines[i];
|
CoolingLine &line = lines[i];
|
||||||
if (line.feedrate > min_feedrate) {
|
if (line.feedrate > min_feedrate) {
|
||||||
@ -185,10 +194,13 @@ struct PerExtruderAdjustments
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Extruder, for which the G-code will be adjusted.
|
// Extruder, for which the G-code will be adjusted.
|
||||||
unsigned int extruder_id = 0;
|
unsigned int extruder_id = 0;
|
||||||
|
// Is the cooling slow down logic enabled for this extruder's material?
|
||||||
|
bool cooling_slow_down_enabled = false;
|
||||||
|
// Slow down the print down to min_print_speed if the total layer time is below slowdown_below_layer_time.
|
||||||
|
float slowdown_below_layer_time = 0.f;
|
||||||
// Minimum print speed allowed for this extruder.
|
// Minimum print speed allowed for this extruder.
|
||||||
float min_print_speed = 0.f;
|
float min_print_speed = 0.f;
|
||||||
|
|
||||||
@ -199,43 +211,57 @@ struct PerExtruderAdjustments
|
|||||||
size_t n_lines_adjustable = 0;
|
size_t n_lines_adjustable = 0;
|
||||||
// Non-adjustable time of lines starting with n_lines_adjustable.
|
// Non-adjustable time of lines starting with n_lines_adjustable.
|
||||||
float time_non_adjustable = 0;
|
float time_non_adjustable = 0;
|
||||||
|
// Current total time for this extruder.
|
||||||
|
float time_total = 0;
|
||||||
|
// Maximum time for this extruder, when the maximum slow down is applied.
|
||||||
|
float time_maximum = 0;
|
||||||
|
|
||||||
// Temporaries for processing the slow down. Both thresholds go from 0 to n_lines_adjustable.
|
// Temporaries for processing the slow down. Both thresholds go from 0 to n_lines_adjustable.
|
||||||
size_t idx_line_begin = 0;
|
size_t idx_line_begin = 0;
|
||||||
size_t idx_line_end = 0;
|
size_t idx_line_end = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
#define EXTRUDER_CONFIG(OPT) config.OPT.get_at(m_current_extruder)
|
|
||||||
|
|
||||||
std::string CoolingBuffer::process_layer(const std::string &gcode, size_t layer_id)
|
std::string CoolingBuffer::process_layer(const std::string &gcode, size_t layer_id)
|
||||||
|
{
|
||||||
|
std::vector<PerExtruderAdjustments> per_extruder_adjustments = this->parse_layer_gcode(gcode, m_current_pos);
|
||||||
|
float layer_time_stretched = this->calculate_layer_slowdown(per_extruder_adjustments);
|
||||||
|
return this->apply_layer_cooldown(gcode, layer_id, layer_time_stretched, per_extruder_adjustments);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the layer G-code for the moves, which could be adjusted.
|
||||||
|
// Return the list of parsed lines, bucketed by an extruder.
|
||||||
|
std::vector<PerExtruderAdjustments> CoolingBuffer::parse_layer_gcode(const std::string &gcode, std::vector<float> ¤t_pos) const
|
||||||
{
|
{
|
||||||
const FullPrintConfig &config = m_gcodegen.config();
|
const FullPrintConfig &config = m_gcodegen.config();
|
||||||
const std::vector<Extruder> &extruders = m_gcodegen.writer().extruders();
|
const std::vector<Extruder> &extruders = m_gcodegen.writer().extruders();
|
||||||
const size_t num_extruders = extruders.size();
|
unsigned int num_extruders = 0;
|
||||||
|
|
||||||
std::vector<PerExtruderAdjustments> per_extruder_adjustments(num_extruders);
|
|
||||||
unsigned int id_extruder_max = 0;
|
|
||||||
for (const Extruder &ex : extruders)
|
for (const Extruder &ex : extruders)
|
||||||
id_extruder_max = std::max(ex.id(), id_extruder_max);
|
num_extruders = std::max(ex.id() + 1, num_extruders);
|
||||||
std::vector<size_t> map_extruder_to_per_extruder_adjustment(id_extruder_max + 1, 0);
|
|
||||||
for (size_t i = 0; i < num_extruders; ++ i) {
|
std::vector<PerExtruderAdjustments> per_extruder_adjustments(extruders.size());
|
||||||
|
std::vector<size_t> map_extruder_to_per_extruder_adjustment(num_extruders, 0);
|
||||||
|
for (size_t i = 0; i < extruders.size(); ++ i) {
|
||||||
|
PerExtruderAdjustments &adj = per_extruder_adjustments[i];
|
||||||
unsigned int extruder_id = extruders[i].id();
|
unsigned int extruder_id = extruders[i].id();
|
||||||
per_extruder_adjustments[i].extruder_id = extruder_id;
|
adj.extruder_id = extruder_id;
|
||||||
per_extruder_adjustments[i].min_print_speed = config.min_print_speed.get_at(extruder_id);
|
adj.cooling_slow_down_enabled = config.cooling.get_at(extruder_id);
|
||||||
|
adj.slowdown_below_layer_time = config.slowdown_below_layer_time.get_at(extruder_id);
|
||||||
|
adj.min_print_speed = config.min_print_speed.get_at(extruder_id);
|
||||||
map_extruder_to_per_extruder_adjustment[extruder_id] = i;
|
map_extruder_to_per_extruder_adjustment[extruder_id] = i;
|
||||||
}
|
}
|
||||||
|
|
||||||
const std::string toolchange_prefix = m_gcodegen.writer().toolchange_prefix();
|
const std::string toolchange_prefix = m_gcodegen.writer().toolchange_prefix();
|
||||||
// Parse the layer G-code for the moves, which could be adjusted.
|
unsigned int current_extruder = m_current_extruder;
|
||||||
{
|
PerExtruderAdjustments *adjustment = &per_extruder_adjustments[map_extruder_to_per_extruder_adjustment[current_extruder]];
|
||||||
PerExtruderAdjustments *adjustment = &per_extruder_adjustments[map_extruder_to_per_extruder_adjustment[m_current_extruder]];
|
|
||||||
unsigned int initial_extruder = m_current_extruder;
|
|
||||||
const char *line_start = gcode.c_str();
|
const char *line_start = gcode.c_str();
|
||||||
const char *line_end = line_start;
|
const char *line_end = line_start;
|
||||||
const char extrusion_axis = config.get_extrusion_axis()[0];
|
const char extrusion_axis = config.get_extrusion_axis()[0];
|
||||||
// Index of an existing CoolingLine of the current adjustment, which holds the feedrate setting command
|
// Index of an existing CoolingLine of the current adjustment, which holds the feedrate setting command
|
||||||
// for a sequence of extrusion moves.
|
// for a sequence of extrusion moves.
|
||||||
size_t active_speed_modifier = size_t(-1);
|
size_t active_speed_modifier = size_t(-1);
|
||||||
for (; *line_start != 0; line_start = line_end) {
|
|
||||||
|
for (; *line_start != 0; line_start = line_end)
|
||||||
|
{
|
||||||
while (*line_end != '\n' && *line_end != 0)
|
while (*line_end != '\n' && *line_end != 0)
|
||||||
++ line_end;
|
++ line_end;
|
||||||
// sline will not contain the trailing '\n'.
|
// sline will not contain the trailing '\n'.
|
||||||
@ -253,7 +279,7 @@ std::string CoolingBuffer::process_layer(const std::string &gcode, size_t layer_
|
|||||||
if (line.type) {
|
if (line.type) {
|
||||||
// G0, G1 or G92
|
// G0, G1 or G92
|
||||||
// Parse the G-code line.
|
// Parse the G-code line.
|
||||||
std::vector<float> new_pos(m_current_pos);
|
std::vector<float> new_pos(current_pos);
|
||||||
const char *c = sline.data() + 3;
|
const char *c = sline.data() + 3;
|
||||||
for (;;) {
|
for (;;) {
|
||||||
// Skip whitespaces.
|
// Skip whitespaces.
|
||||||
@ -290,10 +316,10 @@ std::string CoolingBuffer::process_layer(const std::string &gcode, size_t layer_
|
|||||||
// G0 or G1. Calculate the duration.
|
// G0 or G1. Calculate the duration.
|
||||||
if (config.use_relative_e_distances.value)
|
if (config.use_relative_e_distances.value)
|
||||||
// Reset extruder accumulator.
|
// Reset extruder accumulator.
|
||||||
m_current_pos[3] = 0.f;
|
current_pos[3] = 0.f;
|
||||||
float dif[4];
|
float dif[4];
|
||||||
for (size_t i = 0; i < 4; ++ i)
|
for (size_t i = 0; i < 4; ++ i)
|
||||||
dif[i] = new_pos[i] - m_current_pos[i];
|
dif[i] = new_pos[i] - current_pos[i];
|
||||||
float dxy2 = dif[0] * dif[0] + dif[1] * dif[1];
|
float dxy2 = dif[0] * dif[0] + dif[1] * dif[1];
|
||||||
float dxyz2 = dxy2 + dif[2] * dif[2];
|
float dxyz2 = dxy2 + dif[2] * dif[2];
|
||||||
if (dxyz2 > 0.f) {
|
if (dxyz2 > 0.f) {
|
||||||
@ -326,7 +352,7 @@ std::string CoolingBuffer::process_layer(const std::string &gcode, size_t layer_
|
|||||||
line.type = 0;
|
line.type = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
m_current_pos = std::move(new_pos);
|
current_pos = std::move(new_pos);
|
||||||
} else if (boost::starts_with(sline, ";_EXTRUDE_END")) {
|
} else if (boost::starts_with(sline, ";_EXTRUDE_END")) {
|
||||||
line.type = CoolingLine::TYPE_EXTRUDE_END;
|
line.type = CoolingLine::TYPE_EXTRUDE_END;
|
||||||
active_speed_modifier = size_t(-1);
|
active_speed_modifier = size_t(-1);
|
||||||
@ -334,9 +360,9 @@ std::string CoolingBuffer::process_layer(const std::string &gcode, size_t layer_
|
|||||||
// Switch the tool.
|
// Switch the tool.
|
||||||
line.type = CoolingLine::TYPE_SET_TOOL;
|
line.type = CoolingLine::TYPE_SET_TOOL;
|
||||||
unsigned int new_extruder = (unsigned int)atoi(sline.c_str() + toolchange_prefix.size());
|
unsigned int new_extruder = (unsigned int)atoi(sline.c_str() + toolchange_prefix.size());
|
||||||
if (new_extruder != m_current_extruder) {
|
if (new_extruder != current_extruder) {
|
||||||
m_current_extruder = new_extruder;
|
current_extruder = new_extruder;
|
||||||
adjustment = &per_extruder_adjustments[map_extruder_to_per_extruder_adjustment[m_current_extruder]];
|
adjustment = &per_extruder_adjustments[map_extruder_to_per_extruder_adjustment[current_extruder]];
|
||||||
}
|
}
|
||||||
} else if (boost::starts_with(sline, ";_BRIDGE_FAN_START")) {
|
} else if (boost::starts_with(sline, ";_BRIDGE_FAN_START")) {
|
||||||
line.type = CoolingLine::TYPE_BRIDGE_FAN_START;
|
line.type = CoolingLine::TYPE_BRIDGE_FAN_START;
|
||||||
@ -354,105 +380,89 @@ std::string CoolingBuffer::process_layer(const std::string &gcode, size_t layer_
|
|||||||
if (line.type != 0)
|
if (line.type != 0)
|
||||||
adjustment->lines.emplace_back(std::move(line));
|
adjustment->lines.emplace_back(std::move(line));
|
||||||
}
|
}
|
||||||
m_current_extruder = initial_extruder;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort the extruders by the increasing slowdown_below_layer_time.
|
return per_extruder_adjustments;
|
||||||
std::vector<size_t> extruder_by_slowdown_time;
|
}
|
||||||
extruder_by_slowdown_time.reserve(num_extruders);
|
|
||||||
// Only insert entries, which are adjustable (have cooling enabled and non-zero stretchable time).
|
|
||||||
// Collect total print time of non-adjustable extruders.
|
|
||||||
float elapsed_time_total_non_adjustable = 0.f;
|
|
||||||
for (size_t i = 0; i < num_extruders; ++ i) {
|
|
||||||
if (config.cooling.get_at(extruders[i].id())) {
|
|
||||||
extruder_by_slowdown_time.emplace_back(i);
|
|
||||||
per_extruder_adjustments[i].sort_lines_by_decreasing_feedrate();
|
|
||||||
} else
|
|
||||||
elapsed_time_total_non_adjustable += per_extruder_adjustments[i].elapsed_time_total();
|
|
||||||
}
|
|
||||||
std::sort(extruder_by_slowdown_time.begin(), extruder_by_slowdown_time.end(),
|
|
||||||
[&config, &extruders](const size_t idx1, const size_t idx2){
|
|
||||||
return config.slowdown_below_layer_time.get_at(extruders[idx1].id()) <
|
|
||||||
config.slowdown_below_layer_time.get_at(extruders[idx2].id());
|
|
||||||
});
|
|
||||||
|
|
||||||
// Elapsed time after adjustment.
|
// Slow down an extruder range proportionally down to slowdown_below_layer_time.
|
||||||
float elapsed_time_total = 0.f;
|
// Return the total time for the complete layer.
|
||||||
{
|
static inline float extruder_range_slow_down_proportional(
|
||||||
// Elapsed time for the already adjusted extruders.
|
std::vector<PerExtruderAdjustments*>::iterator it_begin,
|
||||||
float elapsed_time_total0 = elapsed_time_total_non_adjustable;
|
std::vector<PerExtruderAdjustments*>::iterator it_end,
|
||||||
for (size_t i_extruder_by_slowdown_time = 0; i_extruder_by_slowdown_time < extruder_by_slowdown_time.size(); ++ i_extruder_by_slowdown_time) {
|
// Elapsed time for the extruders already processed.
|
||||||
// Idx in per_extruder_adjustments.
|
float elapsed_time_total0,
|
||||||
size_t idx = extruder_by_slowdown_time[i_extruder_by_slowdown_time];
|
// Initial total elapsed time before slow down.
|
||||||
// Macro to sum or adjust all sections starting with i_extruder_by_slowdown_time.
|
float elapsed_time_before_slowdown,
|
||||||
#define FORALL_UNPROCESSED(ACCUMULATOR, ACTION) \
|
// Target time for the complete layer (all extruders applied).
|
||||||
ACCUMULATOR = elapsed_time_total0;\
|
float slowdown_below_layer_time)
|
||||||
for (size_t j = i_extruder_by_slowdown_time; j < extruder_by_slowdown_time.size(); ++ j) \
|
{
|
||||||
ACCUMULATOR += per_extruder_adjustments[extruder_by_slowdown_time[j]].ACTION
|
// Total layer time after the slow down has been applied.
|
||||||
// Calculate the current adjusted elapsed_time_total over the non-finalized extruders.
|
float total_after_slowdown = elapsed_time_before_slowdown;
|
||||||
float total;
|
|
||||||
FORALL_UNPROCESSED(total, elapsed_time_total());
|
|
||||||
float slowdown_below_layer_time = float(config.slowdown_below_layer_time.get_at(per_extruder_adjustments[idx].extruder_id)) * 1.001f;
|
|
||||||
if (total > slowdown_below_layer_time) {
|
|
||||||
// The current total time is above the minimum threshold of the rest of the extruders, don't adjust anything.
|
|
||||||
} else {
|
|
||||||
// Adjust this and all the following (higher config.slowdown_below_layer_time) extruders.
|
|
||||||
// Sum maximum slow down time as if everything was slowed down including the external perimeters.
|
|
||||||
float max_time;
|
|
||||||
FORALL_UNPROCESSED(max_time, maximum_time(true));
|
|
||||||
if (max_time > slowdown_below_layer_time) {
|
|
||||||
// By slowing every possible movement, the layer time could be reached.
|
|
||||||
#if 0
|
|
||||||
// Now decide, whether the external perimeters shall be slowed down as well.
|
// Now decide, whether the external perimeters shall be slowed down as well.
|
||||||
float max_time_nep;
|
float max_time_nep = elapsed_time_total0;
|
||||||
FORALL_UNPROCESSED(max_time_nep, maximum_time(false));
|
for (auto it = it_begin; it != it_end; ++ it)
|
||||||
|
max_time_nep += (*it)->maximum_time_after_slowdown(false);
|
||||||
if (max_time_nep > slowdown_below_layer_time) {
|
if (max_time_nep > slowdown_below_layer_time) {
|
||||||
// It is sufficient to slow down the non-external perimeter moves to reach the target layer time.
|
// It is sufficient to slow down the non-external perimeter moves to reach the target layer time.
|
||||||
// Slow down the non-external perimeters proportionally.
|
// Slow down the non-external perimeters proportionally.
|
||||||
float non_adjustable_time;
|
float non_adjustable_time = elapsed_time_total0;
|
||||||
FORALL_UNPROCESSED(non_adjustable_time, non_adjustable_time(false));
|
for (auto it = it_begin; it != it_end; ++ it)
|
||||||
|
non_adjustable_time += (*it)->non_adjustable_time(false);
|
||||||
// The following step is a linear programming task due to the minimum movement speeds of the print moves.
|
// The following step is a linear programming task due to the minimum movement speeds of the print moves.
|
||||||
// Run maximum 5 iterations until a good enough approximation is reached.
|
// Run maximum 5 iterations until a good enough approximation is reached.
|
||||||
for (size_t iter = 0; iter < 5; ++ iter) {
|
for (size_t iter = 0; iter < 5; ++ iter) {
|
||||||
float factor = (slowdown_below_layer_time - non_adjustable_time) / (total - non_adjustable_time);
|
float factor = (slowdown_below_layer_time - non_adjustable_time) / (total_after_slowdown - non_adjustable_time);
|
||||||
assert(factor > 1.f);
|
assert(factor > 1.f);
|
||||||
FORALL_UNPROCESSED(total, slow_down_proportional(factor, false));
|
total_after_slowdown = elapsed_time_total0;
|
||||||
if (total > 0.95f * slowdown_below_layer_time)
|
for (auto it = it_begin; it != it_end; ++ it)
|
||||||
|
total_after_slowdown += (*it)->slow_down_proportional(factor, false);
|
||||||
|
if (total_after_slowdown > 0.95f * slowdown_below_layer_time)
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Slow down everything. First slow down the non-external perimeters to maximum.
|
// Slow down everything. First slow down the non-external perimeters to maximum.
|
||||||
FORALL_UNPROCESSED(total, slow_down_maximum(false));
|
for (auto it = it_begin; it != it_end; ++ it)
|
||||||
|
(*it)->slowdown_to_minimum_feedrate(false);
|
||||||
// Slow down the external perimeters proportionally.
|
// Slow down the external perimeters proportionally.
|
||||||
float non_adjustable_time;
|
float non_adjustable_time = elapsed_time_total0;
|
||||||
FORALL_UNPROCESSED(non_adjustable_time, non_adjustable_time(true));
|
for (auto it = it_begin; it != it_end; ++ it)
|
||||||
|
non_adjustable_time += (*it)->non_adjustable_time(true);
|
||||||
for (size_t iter = 0; iter < 5; ++ iter) {
|
for (size_t iter = 0; iter < 5; ++ iter) {
|
||||||
float factor = (slowdown_below_layer_time - non_adjustable_time) / (total - non_adjustable_time);
|
float factor = (slowdown_below_layer_time - non_adjustable_time) / (total_after_slowdown - non_adjustable_time);
|
||||||
assert(factor > 1.f);
|
assert(factor > 1.f);
|
||||||
FORALL_UNPROCESSED(total, slow_down_proportional(factor, true));
|
total_after_slowdown = elapsed_time_total0;
|
||||||
if (total > 0.95f * slowdown_below_layer_time)
|
for (auto it = it_begin; it != it_end; ++ it)
|
||||||
|
total_after_slowdown += (*it)->slow_down_proportional(factor, true);
|
||||||
|
if (total_after_slowdown > 0.95f * slowdown_below_layer_time)
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#else
|
return total_after_slowdown;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Slow down an extruder range to slowdown_below_layer_time.
|
||||||
|
// Return the total time for the complete layer.
|
||||||
|
static inline void extruder_range_slow_down_non_proportional(
|
||||||
|
std::vector<PerExtruderAdjustments*>::iterator it_begin,
|
||||||
|
std::vector<PerExtruderAdjustments*>::iterator it_end,
|
||||||
|
float time_stretch)
|
||||||
|
{
|
||||||
// Slow down. Try to equalize the feedrates.
|
// Slow down. Try to equalize the feedrates.
|
||||||
std::vector<PerExtruderAdjustments*> by_min_print_speed;
|
std::vector<PerExtruderAdjustments*> by_min_print_speed(it_begin, it_end);
|
||||||
by_min_print_speed.reserve(extruder_by_slowdown_time.size() - i_extruder_by_slowdown_time);
|
|
||||||
for (size_t j = i_extruder_by_slowdown_time; j < extruder_by_slowdown_time.size(); ++ j)
|
|
||||||
by_min_print_speed.emplace_back(&per_extruder_adjustments[extruder_by_slowdown_time[j]]);
|
|
||||||
// Find the next highest adjustable feedrate among the extruders.
|
// Find the next highest adjustable feedrate among the extruders.
|
||||||
float feedrate = 0;
|
float feedrate = 0;
|
||||||
for (PerExtruderAdjustments *adj : by_min_print_speed)
|
for (PerExtruderAdjustments *adj : by_min_print_speed) {
|
||||||
if (adj->idx_line_begin < adj->n_lines_adjustable && adj->lines[adj->idx_line_begin].feedrate > feedrate)
|
adj->idx_line_begin = 0;
|
||||||
|
adj->idx_line_end = 0;
|
||||||
|
assert(adj->idx_line_begin < adj->n_lines_adjustable);
|
||||||
|
if (adj->lines[adj->idx_line_begin].feedrate > feedrate)
|
||||||
feedrate = adj->lines[adj->idx_line_begin].feedrate;
|
feedrate = adj->lines[adj->idx_line_begin].feedrate;
|
||||||
if (feedrate == 0)
|
}
|
||||||
// No adjustable line is left.
|
assert(feedrate > 0.f);
|
||||||
break;
|
|
||||||
// Sort by min_print_speed, maximum speed first.
|
// Sort by min_print_speed, maximum speed first.
|
||||||
std::sort(by_min_print_speed.begin(), by_min_print_speed.end(),
|
std::sort(by_min_print_speed.begin(), by_min_print_speed.end(),
|
||||||
[](const PerExtruderAdjustments *p1, const PerExtruderAdjustments *p2){ return p1->min_print_speed > p2->min_print_speed; });
|
[](const PerExtruderAdjustments *p1, const PerExtruderAdjustments *p2){ return p1->min_print_speed > p2->min_print_speed; });
|
||||||
// Slow down, fast moves first.
|
// Slow down, fast moves first.
|
||||||
float time_stretch = slowdown_below_layer_time - total;
|
|
||||||
for (;;) {
|
for (;;) {
|
||||||
// For each extruder, find the span of lines with a feedrate close to feedrate.
|
// For each extruder, find the span of lines with a feedrate close to feedrate.
|
||||||
for (PerExtruderAdjustments *adj : by_min_print_speed) {
|
for (PerExtruderAdjustments *adj : by_min_print_speed) {
|
||||||
@ -467,34 +477,32 @@ std::string CoolingBuffer::process_layer(const std::string &gcode, size_t layer_
|
|||||||
feedrate_next = adj->lines[adj->idx_line_end].feedrate;
|
feedrate_next = adj->lines[adj->idx_line_end].feedrate;
|
||||||
// Slow down, limited by max(feedrate_next, min_print_speed).
|
// Slow down, limited by max(feedrate_next, min_print_speed).
|
||||||
for (auto adj = by_min_print_speed.begin(); adj != by_min_print_speed.end();) {
|
for (auto adj = by_min_print_speed.begin(); adj != by_min_print_speed.end();) {
|
||||||
float feedrate_limit = std::max(feedrate_next, (*adj)->min_print_speed);
|
// Slow down at most by time_stretch.
|
||||||
if (feedrate_limit == 0.f) {
|
if ((*adj)->min_print_speed == 0.f) {
|
||||||
float adjustable_time = 0.f;
|
// All the adjustable speeds are now lowered to the same speed,
|
||||||
|
// and the minimum speed is set to zero.
|
||||||
|
float time_adjustable = 0.f;
|
||||||
for (auto it = adj; it != by_min_print_speed.end(); ++ it)
|
for (auto it = adj; it != by_min_print_speed.end(); ++ it)
|
||||||
adjustable_time += (*it)->adjustable_time(true);
|
time_adjustable += (*it)->adjustable_time(true);
|
||||||
float ratio = (adjustable_time + time_stretch) / adjustable_time;
|
float rate = (time_adjustable + time_stretch) / time_adjustable;
|
||||||
for (auto it = adj; it != by_min_print_speed.end(); ++ it)
|
for (auto it = adj; it != by_min_print_speed.end(); ++ it)
|
||||||
(*it)->slow_down_proportional(ratio, true);
|
(*it)->slow_down_proportional(rate, true);
|
||||||
// Break from two levels of loops.
|
return;
|
||||||
feedrate_next = 0.f;
|
|
||||||
break;
|
|
||||||
} else {
|
} else {
|
||||||
|
float feedrate_limit = std::max(feedrate_next, (*adj)->min_print_speed);
|
||||||
|
bool done = false;
|
||||||
float time_stretch_max = 0.f;
|
float time_stretch_max = 0.f;
|
||||||
for (auto it = adj; it != by_min_print_speed.end(); ++ it)
|
for (auto it = adj; it != by_min_print_speed.end(); ++ it)
|
||||||
time_stretch_max += (*it)->time_stretch_when_slowing_down_to(feedrate_limit);
|
time_stretch_max += (*it)->time_stretch_when_slowing_down_to_feedrate(feedrate_limit);
|
||||||
bool done = false;
|
if (time_stretch_max >= time_stretch) {
|
||||||
if (time_stretch_max > time_stretch) {
|
|
||||||
feedrate_limit = feedrate - (feedrate - feedrate_limit) * time_stretch / time_stretch_max;
|
feedrate_limit = feedrate - (feedrate - feedrate_limit) * time_stretch / time_stretch_max;
|
||||||
done = true;
|
done = true;
|
||||||
}
|
} else
|
||||||
for (auto it = adj; it != by_min_print_speed.end(); ++ it)
|
|
||||||
(*it)->slow_down_to(feedrate_limit);
|
|
||||||
if (done) {
|
|
||||||
// Break from two levels of loops.
|
|
||||||
feedrate_next = 0.f;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
time_stretch -= time_stretch_max;
|
time_stretch -= time_stretch_max;
|
||||||
|
for (auto it = adj; it != by_min_print_speed.end(); ++ it)
|
||||||
|
(*it)->slow_down_to_feedrate(feedrate_limit);
|
||||||
|
if (done)
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
// Skip the other extruders with nearly the same min_print_speed, as they have been processed already.
|
// Skip the other extruders with nearly the same min_print_speed, as they have been processed already.
|
||||||
auto next = adj;
|
auto next = adj;
|
||||||
@ -509,25 +517,81 @@ std::string CoolingBuffer::process_layer(const std::string &gcode, size_t layer_
|
|||||||
feedrate = feedrate_next;
|
feedrate = feedrate_next;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
}
|
||||||
|
|
||||||
|
// Calculate slow down for all the extruders.
|
||||||
|
float CoolingBuffer::calculate_layer_slowdown(std::vector<PerExtruderAdjustments> &per_extruder_adjustments)
|
||||||
|
{
|
||||||
|
// Sort the extruders by an increasing slowdown_below_layer_time.
|
||||||
|
// The layers with a lower slowdown_below_layer_time are slowed down
|
||||||
|
// together with all the other layers with slowdown_below_layer_time above.
|
||||||
|
std::vector<PerExtruderAdjustments*> by_slowdown_time;
|
||||||
|
by_slowdown_time.reserve(per_extruder_adjustments.size());
|
||||||
|
// Only insert entries, which are adjustable (have cooling enabled and non-zero stretchable time).
|
||||||
|
// Collect total print time of non-adjustable extruders.
|
||||||
|
float elapsed_time_total0 = 0.f;
|
||||||
|
for (PerExtruderAdjustments &adj : per_extruder_adjustments) {
|
||||||
|
// Curren total time for this extruder.
|
||||||
|
adj.time_total = adj.elapsed_time_total();
|
||||||
|
// Maximum time for this extruder, when all extrusion moves are slowed down to min_extrusion_speed.
|
||||||
|
adj.time_maximum = adj.maximum_time_after_slowdown(true);
|
||||||
|
if (adj.cooling_slow_down_enabled) {
|
||||||
|
by_slowdown_time.emplace_back(&adj);
|
||||||
|
if (! m_cooling_logic_proportional)
|
||||||
|
// sorts the lines, also sets adj.time_non_adjustable
|
||||||
|
adj.sort_lines_by_decreasing_feedrate();
|
||||||
|
} else
|
||||||
|
elapsed_time_total0 += adj.elapsed_time_total();
|
||||||
|
}
|
||||||
|
std::sort(by_slowdown_time.begin(), by_slowdown_time.end(),
|
||||||
|
[](const PerExtruderAdjustments *adj1, const PerExtruderAdjustments *adj2)
|
||||||
|
{ return adj1->slowdown_below_layer_time < adj2->slowdown_below_layer_time; });
|
||||||
|
|
||||||
|
for (auto cur_begin = by_slowdown_time.begin(); cur_begin != by_slowdown_time.end(); ++ cur_begin) {
|
||||||
|
PerExtruderAdjustments &adj = *(*cur_begin);
|
||||||
|
// Calculate the current adjusted elapsed_time_total over the non-finalized extruders.
|
||||||
|
float total = elapsed_time_total0;
|
||||||
|
for (auto it = cur_begin; it != by_slowdown_time.end(); ++ it)
|
||||||
|
total += (*it)->time_total;
|
||||||
|
float slowdown_below_layer_time = adj.slowdown_below_layer_time * 1.001f;
|
||||||
|
if (total > slowdown_below_layer_time) {
|
||||||
|
// The current total time is above the minimum threshold of the rest of the extruders, don't adjust anything.
|
||||||
|
} else {
|
||||||
|
// Adjust this and all the following (higher config.slowdown_below_layer_time) extruders.
|
||||||
|
// Sum maximum slow down time as if everything was slowed down including the external perimeters.
|
||||||
|
float max_time = elapsed_time_total0;
|
||||||
|
for (auto it = cur_begin; it != by_slowdown_time.end(); ++ it)
|
||||||
|
max_time += (*it)->time_maximum;
|
||||||
|
if (max_time > slowdown_below_layer_time) {
|
||||||
|
if (m_cooling_logic_proportional)
|
||||||
|
extruder_range_slow_down_proportional(cur_begin, by_slowdown_time.end(), elapsed_time_total0, total, slowdown_below_layer_time);
|
||||||
|
else
|
||||||
|
extruder_range_slow_down_non_proportional(cur_begin, by_slowdown_time.end(), slowdown_below_layer_time - total);
|
||||||
} else {
|
} else {
|
||||||
// Slow down to maximum possible.
|
// Slow down to maximum possible.
|
||||||
FORALL_UNPROCESSED(total, slow_down_maximum(true));
|
for (auto it = cur_begin; it != by_slowdown_time.end(); ++ it)
|
||||||
|
(*it)->slowdown_to_minimum_feedrate(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#undef FORALL_UNPROCESSED
|
elapsed_time_total0 += adj.elapsed_time_total();
|
||||||
// Sum the final elapsed time for all extruders up to i_extruder_by_slowdown_time.
|
|
||||||
if (i_extruder_by_slowdown_time + 1 == extruder_by_slowdown_time.size())
|
|
||||||
// Optimization for single extruder prints.
|
|
||||||
elapsed_time_total0 = total;
|
|
||||||
else
|
|
||||||
elapsed_time_total0 += per_extruder_adjustments[idx].elapsed_time_total();
|
|
||||||
}
|
|
||||||
elapsed_time_total = elapsed_time_total0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Transform the G-code.
|
return elapsed_time_total0;
|
||||||
// First sort the adjustment lines by their position in the source G-code.
|
}
|
||||||
|
|
||||||
|
// Apply slow down over G-code lines stored in per_extruder_adjustments, enable fan if needed.
|
||||||
|
// Returns the adjusted G-code.
|
||||||
|
std::string CoolingBuffer::apply_layer_cooldown(
|
||||||
|
// Source G-code for the current layer.
|
||||||
|
const std::string &gcode,
|
||||||
|
// ID of the current layer, used to disable fan for the first n layers.
|
||||||
|
size_t layer_id,
|
||||||
|
// Total time of this layer after slow down, used to control the fan.
|
||||||
|
float layer_time,
|
||||||
|
// Per extruder list of G-code lines and their cool down attributes.
|
||||||
|
std::vector<PerExtruderAdjustments> &per_extruder_adjustments)
|
||||||
|
{
|
||||||
|
// First sort the adjustment lines by of multiple extruders by their position in the source G-code.
|
||||||
std::vector<const CoolingLine*> lines;
|
std::vector<const CoolingLine*> lines;
|
||||||
{
|
{
|
||||||
size_t n_lines = 0;
|
size_t n_lines = 0;
|
||||||
@ -545,8 +609,9 @@ std::string CoolingBuffer::process_layer(const std::string &gcode, size_t layer_
|
|||||||
int fan_speed = -1;
|
int fan_speed = -1;
|
||||||
bool bridge_fan_control = false;
|
bool bridge_fan_control = false;
|
||||||
int bridge_fan_speed = 0;
|
int bridge_fan_speed = 0;
|
||||||
auto change_extruder_set_fan = [ this, layer_id, elapsed_time_total, &new_gcode, &fan_speed, &bridge_fan_control, &bridge_fan_speed ]() {
|
auto change_extruder_set_fan = [ this, layer_id, layer_time, &new_gcode, &fan_speed, &bridge_fan_control, &bridge_fan_speed ]() {
|
||||||
const FullPrintConfig &config = m_gcodegen.config();
|
const FullPrintConfig &config = m_gcodegen.config();
|
||||||
|
#define EXTRUDER_CONFIG(OPT) config.OPT.get_at(m_current_extruder)
|
||||||
int min_fan_speed = EXTRUDER_CONFIG(min_fan_speed);
|
int min_fan_speed = EXTRUDER_CONFIG(min_fan_speed);
|
||||||
int fan_speed_new = EXTRUDER_CONFIG(fan_always_on) ? min_fan_speed : 0;
|
int fan_speed_new = EXTRUDER_CONFIG(fan_always_on) ? min_fan_speed : 0;
|
||||||
if (layer_id >= EXTRUDER_CONFIG(disable_fan_first_layers)) {
|
if (layer_id >= EXTRUDER_CONFIG(disable_fan_first_layers)) {
|
||||||
@ -554,17 +619,18 @@ std::string CoolingBuffer::process_layer(const std::string &gcode, size_t layer_
|
|||||||
float slowdown_below_layer_time = float(EXTRUDER_CONFIG(slowdown_below_layer_time));
|
float slowdown_below_layer_time = float(EXTRUDER_CONFIG(slowdown_below_layer_time));
|
||||||
float fan_below_layer_time = float(EXTRUDER_CONFIG(fan_below_layer_time));
|
float fan_below_layer_time = float(EXTRUDER_CONFIG(fan_below_layer_time));
|
||||||
if (EXTRUDER_CONFIG(cooling)) {
|
if (EXTRUDER_CONFIG(cooling)) {
|
||||||
if (elapsed_time_total < slowdown_below_layer_time) {
|
if (layer_time < slowdown_below_layer_time) {
|
||||||
// Layer time very short. Enable the fan to a full throttle.
|
// Layer time very short. Enable the fan to a full throttle.
|
||||||
fan_speed_new = max_fan_speed;
|
fan_speed_new = max_fan_speed;
|
||||||
} else if (elapsed_time_total < fan_below_layer_time) {
|
} else if (layer_time < fan_below_layer_time) {
|
||||||
// Layer time quite short. Enable the fan proportionally according to the current layer time.
|
// Layer time quite short. Enable the fan proportionally according to the current layer time.
|
||||||
assert(elapsed_time_total >= slowdown_below_layer_time);
|
assert(layer_time >= slowdown_below_layer_time);
|
||||||
double t = (elapsed_time_total - slowdown_below_layer_time) / (fan_below_layer_time - slowdown_below_layer_time);
|
double t = (layer_time - slowdown_below_layer_time) / (fan_below_layer_time - slowdown_below_layer_time);
|
||||||
fan_speed_new = int(floor(t * min_fan_speed + (1. - t) * max_fan_speed) + 0.5);
|
fan_speed_new = int(floor(t * min_fan_speed + (1. - t) * max_fan_speed) + 0.5);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
bridge_fan_speed = EXTRUDER_CONFIG(bridge_fan_speed);
|
bridge_fan_speed = EXTRUDER_CONFIG(bridge_fan_speed);
|
||||||
|
#undef EXTRUDER_CONFIG
|
||||||
bridge_fan_control = bridge_fan_speed > fan_speed_new;
|
bridge_fan_control = bridge_fan_speed > fan_speed_new;
|
||||||
} else {
|
} else {
|
||||||
bridge_fan_control = false;
|
bridge_fan_control = false;
|
||||||
@ -576,10 +642,11 @@ std::string CoolingBuffer::process_layer(const std::string &gcode, size_t layer_
|
|||||||
new_gcode += m_gcodegen.writer().set_fan(fan_speed);
|
new_gcode += m_gcodegen.writer().set_fan(fan_speed);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
change_extruder_set_fan();
|
|
||||||
|
|
||||||
const char *pos = gcode.c_str();
|
const char *pos = gcode.c_str();
|
||||||
int current_feedrate = 0;
|
int current_feedrate = 0;
|
||||||
|
const std::string toolchange_prefix = m_gcodegen.writer().toolchange_prefix();
|
||||||
|
change_extruder_set_fan();
|
||||||
for (const CoolingLine *line : lines) {
|
for (const CoolingLine *line : lines) {
|
||||||
const char *line_start = gcode.c_str() + line->line_start;
|
const char *line_start = gcode.c_str() + line->line_start;
|
||||||
const char *line_end = gcode.c_str() + line->line_end;
|
const char *line_end = gcode.c_str() + line->line_end;
|
||||||
|
@ -9,13 +9,17 @@ namespace Slic3r {
|
|||||||
|
|
||||||
class GCode;
|
class GCode;
|
||||||
class Layer;
|
class Layer;
|
||||||
|
class PerExtruderAdjustments;
|
||||||
|
|
||||||
/*
|
// A standalone G-code filter, to control cooling of the print.
|
||||||
A standalone G-code filter, to control cooling of the print.
|
// The G-code is processed per layer. Once a layer is collected, fan start / stop commands are edited
|
||||||
The G-code is processed per layer. Once a layer is collected, fan start / stop commands are edited
|
// and the print is modified to stretch over a minimum layer time.
|
||||||
and the print is modified to stretch over a minimum layer time.
|
//
|
||||||
*/
|
// The simple it sounds, the actual implementation is significantly more complex.
|
||||||
|
// Namely, for a multi-extruder print, each material may require a different cooling logic.
|
||||||
|
// For example, some materials may not like to print too slowly, while with some materials
|
||||||
|
// we may slow down significantly.
|
||||||
|
//
|
||||||
class CoolingBuffer {
|
class CoolingBuffer {
|
||||||
public:
|
public:
|
||||||
CoolingBuffer(GCode &gcodegen);
|
CoolingBuffer(GCode &gcodegen);
|
||||||
@ -25,7 +29,12 @@ public:
|
|||||||
GCode* gcodegen() { return &m_gcodegen; }
|
GCode* gcodegen() { return &m_gcodegen; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
CoolingBuffer& operator=(const CoolingBuffer&);
|
CoolingBuffer& operator=(const CoolingBuffer&) = delete;
|
||||||
|
std::vector<PerExtruderAdjustments> parse_layer_gcode(const std::string &gcode, std::vector<float> ¤t_pos) const;
|
||||||
|
float calculate_layer_slowdown(std::vector<PerExtruderAdjustments> &per_extruder_adjustments);
|
||||||
|
// Apply slow down over G-code lines stored in per_extruder_adjustments, enable fan if needed.
|
||||||
|
// Returns the adjusted G-code.
|
||||||
|
std::string apply_layer_cooldown(const std::string &gcode, size_t layer_id, float layer_time, std::vector<PerExtruderAdjustments> &per_extruder_adjustments);
|
||||||
|
|
||||||
GCode& m_gcodegen;
|
GCode& m_gcodegen;
|
||||||
std::string m_gcode;
|
std::string m_gcode;
|
||||||
@ -34,6 +43,9 @@ private:
|
|||||||
std::vector<char> m_axis;
|
std::vector<char> m_axis;
|
||||||
std::vector<float> m_current_pos;
|
std::vector<float> m_current_pos;
|
||||||
unsigned int m_current_extruder;
|
unsigned int m_current_extruder;
|
||||||
|
|
||||||
|
// Old logic: proportional.
|
||||||
|
bool m_cooling_logic_proportional = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user