diff --git a/src/libslic3r/Config.cpp b/src/libslic3r/Config.cpp index 38c5b30118..cac46d7108 100644 --- a/src/libslic3r/Config.cpp +++ b/src/libslic3r/Config.cpp @@ -871,19 +871,6 @@ ConfigSubstitutions ConfigBase::load_from_json(const std::string &file, ForwardC return std::move(substitutions_ctxt.substitutions); } -// Case-insensitive compare of a JSON key against a fixed ASCII one, without -// boost::iequals, whose std::locale() takes a lock the whole process shares in the -// MSVC runtime. -static bool ascii_iequals(const std::string &key, const char *literal) -{ - auto lower = [](char c) { return (c >= 'A' && c <= 'Z') ? char(c - 'A' + 'a') : c; }; - size_t i = 0; - for (; i < key.size() && literal[i] != '\0'; ++ i) - if (lower(key[i]) != lower(literal[i])) - return false; - return i == key.size() && literal[i] == '\0'; -} - int ConfigBase::load_from_json(const std::string &file, ConfigSubstitutionContext& substitution_context, bool load_inherits_to_config, std::map& key_values, std::string& reason) { json j; diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index 7aed4b808b..4599004858 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -806,13 +806,13 @@ static std::vector get_path_of_change_filament(const Print& print) GCodeReader parser; parser.parse_buffer(gcode, [&changes](GCodeReader &parser, const GCodeReader::GCodeLine &line) { const std::string_view cmd = line.cmd(); - if (boost::iequals(cmd, "M204") || boost::iequals(cmd, "M201") || - boost::iequals(cmd, "M202")) + if (ascii_iequals(cmd, "M204") || ascii_iequals(cmd, "M201") || + ascii_iequals(cmd, "M202")) changes.acceleration = true; - else if ((boost::iequals(cmd, "M205") || boost::iequals(cmd, "M207") || boost::iequals(cmd, "M566")) && + else if ((ascii_iequals(cmd, "M205") || ascii_iequals(cmd, "M207") || ascii_iequals(cmd, "M566")) && custom_gcode_line_has_xy_parameter(line.raw())) changes.jerk = true; - else if (boost::iequals(cmd, "SET_VELOCITY_LIMIT")) { + else if (ascii_iequals(cmd, "SET_VELOCITY_LIMIT")) { changes.acceleration |= boost::icontains(line.raw(), "ACCEL="); changes.jerk |= boost::icontains(line.raw(), "SQUARE_CORNER_VELOCITY="); } diff --git a/src/libslic3r/GCode/AdaptivePAProcessor.cpp b/src/libslic3r/GCode/AdaptivePAProcessor.cpp index 21bd308853..2adc03bc34 100644 --- a/src/libslic3r/GCode/AdaptivePAProcessor.cpp +++ b/src/libslic3r/GCode/AdaptivePAProcessor.cpp @@ -41,6 +41,9 @@ AdaptivePAProcessor::AdaptivePAProcessor(GCode &gcodegen) m_pa_change_pattern(R"(; PA_CHANGE:T(\d+) MM3MM:([0-9]*\.[0-9]+) ACCEL:(\d+) BR:(\d+) RC:(\d+) OV:(\d+))"), m_g1_f_pattern(R"(G1 F([0-9]+))") { + const size_t indices = std::max(m_config.adaptive_pressure_advance.size(), m_config.enable_pressure_advance.size()); + for (size_t i = 0; i < indices && !m_enabled; ++i) + m_enabled = m_config.adaptive_pressure_advance.get_at(i) && m_config.enable_pressure_advance.get_at(i); } // Method to get the interpolator for a specific filament config index. @@ -66,6 +69,12 @@ AdaptivePAInterpolator* AdaptivePAProcessor::getInterpolator(unsigned int config * @return A string containing the processed G-code with adaptive pressure advance applied. */ std::string AdaptivePAProcessor::process_layer(std::string &&gcode) { + // Without PA_CHANGE tags the loop below would only terminate the layer's last line. + if (!m_enabled && gcode.find("; PA_CHANGE") == std::string::npos) { + if (!gcode.empty() && gcode.back() != '\n') + gcode += '\n'; + return std::move(gcode); + } std::istringstream stream(gcode); std::string line; std::ostringstream output; diff --git a/src/libslic3r/GCode/AdaptivePAProcessor.hpp b/src/libslic3r/GCode/AdaptivePAProcessor.hpp index c662191536..f3aef6ce82 100644 --- a/src/libslic3r/GCode/AdaptivePAProcessor.hpp +++ b/src/libslic3r/GCode/AdaptivePAProcessor.hpp @@ -78,6 +78,7 @@ private: double m_next_feedrate; ///< First feed rate (speed) for the upcomming island. double m_current_feedrate; ///< Current, latest feedrate. int m_last_config_index; ///< Filament config index of the last PA_CHANGE tag. + bool m_enabled{false}; ///< Whether any filament config index has both PA and adaptive PA on, the only ones that emit PA_CHANGE tags. std::regex m_pa_change_pattern; ///< Regular expression to detect PA_CHANGE pattern. std::regex m_g1_f_pattern; ///< Regular expression to detect G1 F pattern. diff --git a/src/libslic3r/GCode/CoolingBuffer.cpp b/src/libslic3r/GCode/CoolingBuffer.cpp index 6524b3ce38..28a561369b 100644 --- a/src/libslic3r/GCode/CoolingBuffer.cpp +++ b/src/libslic3r/GCode/CoolingBuffer.cpp @@ -1,4 +1,5 @@ #include "../GCode.hpp" +#include "../LocalesUtils.hpp" #include "libslic3r/Extruder.hpp" #include "libslic3r/Point.hpp" #include "libslic3r/libslic3r.h" @@ -408,13 +409,13 @@ std::vector CoolingBuffer::parse_layer_gcode(const std:: if (*c == 0 || *c == ';') break; - assert(is_decimal_separator_point()); // for atof //BBS: Parse the axis. size_t axis = (*c >= 'X' && *c <= 'Z') ? (*c - 'X') : (*c == 'E') ? 3 : (*c == 'F') ? 4 : (*c == 'I') ? 5 : (*c == 'J') ? 6 : size_t(-1); if (axis != size_t(-1)) { - new_pos[axis] = float(atof(++c)); + ++ c; + new_pos[axis] = float(atof_decimal_point(std::string_view(c, sline.data() + sline.size() - c))); if (axis == 4) { // Convert mm/min to mm/sec. new_pos[4] /= 60.f; @@ -550,10 +551,9 @@ std::vector CoolingBuffer::parse_layer_gcode(const std:: line.type = CoolingLine::TYPE_G4; size_t pos_S = sline.find('S', 3); size_t pos_P = sline.find('P', 3); - assert(is_decimal_separator_point()); // for atof line.time = line.time_max = float( - (pos_S > 0) ? atof(sline.c_str() + pos_S + 1) : - (pos_P > 0) ? atof(sline.c_str() + pos_P + 1) * 0.001 : 0.); + (pos_S > 0) ? atof_decimal_point(sline.c_str() + pos_S + 1) : + (pos_P > 0) ? atof_decimal_point(sline.c_str() + pos_P + 1) * 0.001 : 0.); } else if (boost::starts_with(sline, ";_FORCE_RESUME_FAN_SPEED")) { line.type = CoolingLine::TYPE_FORCE_RESUME_FAN; } diff --git a/src/libslic3r/GCode/GCodeProcessor.cpp b/src/libslic3r/GCode/GCodeProcessor.cpp index 760a04eb4d..33b9119116 100644 --- a/src/libslic3r/GCode/GCodeProcessor.cpp +++ b/src/libslic3r/GCode/GCodeProcessor.cpp @@ -909,8 +909,8 @@ public: { // Orca: find start pos by seaching G28/G29/PRINT_START/START_PRINT commands auto is_start_pos = [](const std::string& curr_cmd) { - return boost::iequals(curr_cmd, "G28") || boost::iequals(curr_cmd, "G29") || boost::iequals(curr_cmd, "PRINT_START") || - boost::iequals(curr_cmd, "START_PRINT"); + return ascii_iequals(curr_cmd, "G28") || ascii_iequals(curr_cmd, "G29") || ascii_iequals(curr_cmd, "PRINT_START") || + ascii_iequals(curr_cmd, "START_PRINT"); }; assert(!m_lines.empty()); const float time_step = backtrace.time_step(); @@ -1265,6 +1265,9 @@ void GCodeProcessor::run_post_process() // Process inline placeholders (print_time_total_sec, print_time_day, print_time_hour, print_time_minute, print_time_sec and used_filament_length) auto process_inline_placeholders = [&](std::string& gcode_line) { bool processed = false; + // Every inline placeholder contains '@', so a line without one has nothing to replace. + if (gcode_line.find('@') == std::string::npos) + return processed; const std::string& print_time_total_placeholder = reserved_tag(ETags::Print_Time_Total_Sec_Placeholder); const std::string& print_time_day_placeholder = reserved_tag(ETags::Print_Time_Day_Placeholder); @@ -4007,13 +4010,13 @@ void GCodeProcessor::process_gcode_line(const GCodeReader::GCodeLine& line, bool const std::string_view cmd = line.cmd(); if (m_flavor == gcfKlipper) { - if (boost::iequals(cmd, "SET_VELOCITY_LIMIT")) + if (ascii_iequals(cmd, "SET_VELOCITY_LIMIT")) { process_SET_VELOCITY_LIMIT(line); return; } // ORCA: Add Pressure Advance visualization support - if (boost::iequals(cmd, "SET_PRESSURE_ADVANCE")) + if (ascii_iequals(cmd, "SET_PRESSURE_ADVANCE")) { process_SET_PRESSURE_ADVANCE(line); return; @@ -7551,51 +7554,72 @@ void GCodeProcessor::calculate_time(GCodeProcessorResult& result, size_t keep_la actual_speed_moves = std::move(machine.actual_speed_moves); } - // insert actual speed moves into the move list - unsigned int inserted_actual_speed_moves_count = 0; - std::vector new_moves; - std::map id_map; - for (auto it = actual_speed_moves.begin(); it != actual_speed_moves.end(); ++it) { - const unsigned int base_id = it->move_id + inserted_actual_speed_moves_count; - if (it->position.has_value()) { - // insert actual speed move into the move list - // clone from existing move - GCodeProcessorResult::MoveVertex new_move = result.moves[base_id]; - // override modified parameters - new_move.time = { 0.0f, 0.0f }; - new_move.position = *it->position; - new_move.actual_feedrate = it->actual_feedrate; - new_move.delta_extruder = *it->delta_extruder; - new_move.feedrate = *it->feedrate; - new_move.width = *it->width; - new_move.height = *it->height; - new_move.mm3_per_mm = *it->mm3_per_mm; - new_move.fan_speed = *it->fan_speed; - new_move.temperature = *it->temperature; - new_move.internal_only = true; - new_moves.push_back(new_move); + // actual_speed_moves holds, per block in move order, the moves to insert before the block's move and then an + // entry without a position for that move; positioned entries after the last such entry are dropped. + std::vector& moves = result.moves; + size_t inserted_actual_speed_moves_count = 0; + size_t kept = 0; + size_t group_start = 0; + for (size_t i = 0; i < actual_speed_moves.size(); ++i) { + if (actual_speed_moves[i].position.has_value()) + continue; + const unsigned int move_id = actual_speed_moves[i].move_id; + // A VG1 block has no move of its own, so its id can fall behind the previous block's or point past the list. + if (move_id < moves.size() && (kept == 0 || move_id > actual_speed_moves[kept - 1].move_id)) { + inserted_actual_speed_moves_count += i - group_start; + moves[move_id].actual_feedrate = actual_speed_moves[i].actual_feedrate; + // A seam vertex right after a block's move shares its actual speed. + if (move_id + 1 < moves.size() && moves[move_id + 1].type == EMoveType::Seam) + moves[move_id + 1].actual_feedrate = actual_speed_moves[i].actual_feedrate; + for (size_t j = group_start; j <= i; ++j, ++kept) + if (kept != j) + actual_speed_moves[kept] = std::move(actual_speed_moves[j]); } - else { - result.moves.insert(result.moves.begin() + base_id, new_moves.begin(), new_moves.end()); - id_map[it->move_id] = base_id + new_moves.size(); - // update move actual speed - result.moves[base_id + new_moves.size()].actual_feedrate = it->actual_feedrate; - inserted_actual_speed_moves_count += new_moves.size(); - // synchronize seams actual speed - if (base_id + new_moves.size() + 1 < result.moves.size()) { - GCodeProcessorResult::MoveVertex& move = result.moves[base_id + new_moves.size() + 1]; - if (move.type == EMoveType::Seam) - move.actual_feedrate = it->actual_feedrate; - } - new_moves.clear(); + group_start = i + 1; + } + actual_speed_moves.erase(actual_speed_moves.begin() + kept, actual_speed_moves.end()); + + // Walks the blocks back to front, so each shifted move is moved once, into its final slot. + size_t read = moves.size(); // one past the last move not yet placed + moves.resize(moves.size() + inserted_actual_speed_moves_count); + size_t write = moves.size(); // one past the last free slot + m_actual_speed_id_map.clear(); + size_t entry = actual_speed_moves.size(); + while (entry > 0) { + const unsigned int block_id = actual_speed_moves[--entry].move_id; + assert(block_id < read); + while (read > block_id + 1) + moves[--write] = moves[--read]; + const GCodeProcessorResult::MoveVertex block_move = moves[--read]; + moves[--write] = block_move; + m_actual_speed_id_map.emplace_back(block_id, (unsigned int)write); + for (; entry > 0 && actual_speed_moves[entry - 1].position.has_value(); --entry) { + const TimeMachine::ActualSpeedMove& it = actual_speed_moves[entry - 1]; + GCodeProcessorResult::MoveVertex new_move = block_move; + new_move.time = { 0.0f, 0.0f }; + new_move.position = *it.position; + new_move.actual_feedrate = it.actual_feedrate; + new_move.delta_extruder = *it.delta_extruder; + new_move.feedrate = *it.feedrate; + new_move.width = *it.width; + new_move.height = *it.height; + new_move.mm3_per_mm = *it.mm3_per_mm; + new_move.fan_speed = *it.fan_speed; + new_move.temperature = *it.temperature; + new_move.internal_only = true; + moves[--write] = new_move; } } + assert(read == write); // synchronize blocks' move_ids with after moves for actual speed insertion + std::reverse(m_actual_speed_id_map.begin(), m_actual_speed_id_map.end()); for (size_t i = 0; i < static_cast(PrintEstimatedStatistics::ETimeMode::Count); ++i) { for (GCodeProcessor::TimeBlock& block : m_time_processor.machines[i].blocks) { - auto it = id_map.find(block.move_id); - block.move_id = (it != id_map.end()) ? it->second : block.move_id + inserted_actual_speed_moves_count; + auto it = std::lower_bound(m_actual_speed_id_map.begin(), m_actual_speed_id_map.end(), block.move_id, + [](const std::pair& entry, unsigned int id) { return entry.first < id; }); + block.move_id = (it != m_actual_speed_id_map.end() && it->first == block.move_id) ? + it->second : block.move_id + (unsigned int)inserted_actual_speed_moves_count; } } } diff --git a/src/libslic3r/GCode/GCodeProcessor.hpp b/src/libslic3r/GCode/GCodeProcessor.hpp index cde789cb6c..819b019703 100644 --- a/src/libslic3r/GCode/GCodeProcessor.hpp +++ b/src/libslic3r/GCode/GCodeProcessor.hpp @@ -337,54 +337,57 @@ class Print; //BBS: add mutex for protection of gcode result mutable std::mutex result_mutex; - GCodeProcessorResult& operator=(const GCodeProcessorResult &other) + GCodeProcessorResult& operator=(const GCodeProcessorResult &other) { assign(other); return *this; } + // Declared because the user-declared copy assignment suppresses the implicit move. + GCodeProcessorResult& operator=(GCodeProcessorResult &&other) { assign(std::move(other)); return *this; } + // Add a new member here, or neither assignment transfers it. + template void assign(Other &&other) { - filename = other.filename; - id = other.id; - moves = other.moves; - lines_ends = other.lines_ends; - printable_area = other.printable_area; - bed_exclude_area = other.bed_exclude_area; - wrapping_exclude_area = other.wrapping_exclude_area; - toolpath_outside = other.toolpath_outside; - label_object_enabled = other.label_object_enabled; - long_retraction_when_cut = other.long_retraction_when_cut; - timelapse_warning_code = other.timelapse_warning_code; - printable_height = other.printable_height; - settings_ids = other.settings_ids; - filaments_count = other.filaments_count; - extruder_colors = other.extruder_colors; - filament_diameters = other.filament_diameters; - filament_densities = other.filament_densities; - filament_costs = other.filament_costs; - print_statistics = other.print_statistics; - custom_gcode_per_print_z = other.custom_gcode_per_print_z; - spiral_vase_mode = other.spiral_vase_mode; - warnings = other.warnings; - bed_type = other.bed_type; - gcode_check_result = other.gcode_check_result; - limit_filament_maps = other.limit_filament_maps; - filament_printable_reuslt = other.filament_printable_reuslt; + filename = std::forward(other).filename; + id = std::forward(other).id; + moves = std::forward(other).moves; + lines_ends = std::forward(other).lines_ends; + printable_area = std::forward(other).printable_area; + bed_exclude_area = std::forward(other).bed_exclude_area; + wrapping_exclude_area = std::forward(other).wrapping_exclude_area; + toolpath_outside = std::forward(other).toolpath_outside; + label_object_enabled = std::forward(other).label_object_enabled; + long_retraction_when_cut = std::forward(other).long_retraction_when_cut; + timelapse_warning_code = std::forward(other).timelapse_warning_code; + printable_height = std::forward(other).printable_height; + settings_ids = std::forward(other).settings_ids; + filaments_count = std::forward(other).filaments_count; + extruder_colors = std::forward(other).extruder_colors; + filament_diameters = std::forward(other).filament_diameters; + filament_densities = std::forward(other).filament_densities; + filament_costs = std::forward(other).filament_costs; + print_statistics = std::forward(other).print_statistics; + custom_gcode_per_print_z = std::forward(other).custom_gcode_per_print_z; + spiral_vase_mode = std::forward(other).spiral_vase_mode; + warnings = std::forward(other).warnings; + bed_type = std::forward(other).bed_type; + gcode_check_result = std::forward(other).gcode_check_result; + limit_filament_maps = std::forward(other).limit_filament_maps; + filament_printable_reuslt = std::forward(other).filament_printable_reuslt; // Orca: copy the shared grouping result so a copied result keeps it (shared_ptr => // memory-safe), rather than leaving a stale pointer on the target. No g-code effect either way. - nozzle_group_result = other.nozzle_group_result; + nozzle_group_result = std::forward(other).nozzle_group_result; // Keep the per-extruder hotend types on a copied result (injector input). - extruder_types = other.extruder_types; - printer_extruder_variant = other.printer_extruder_variant; - printer_extruder_id = other.printer_extruder_id; - layer_filaments = other.layer_filaments; - filament_change_sequence = other.filament_change_sequence; - used_mixed_filaments = other.used_mixed_filaments; - nozzle_change_sequence = other.nozzle_change_sequence; - optimal_assignment = other.optimal_assignment; - filament_change_count_map = other.filament_change_count_map; + extruder_types = std::forward(other).extruder_types; + printer_extruder_variant = std::forward(other).printer_extruder_variant; + printer_extruder_id = std::forward(other).printer_extruder_id; + layer_filaments = std::forward(other).layer_filaments; + filament_change_sequence = std::forward(other).filament_change_sequence; + used_mixed_filaments = std::forward(other).used_mixed_filaments; + nozzle_change_sequence = std::forward(other).nozzle_change_sequence; + optimal_assignment = std::forward(other).optimal_assignment; + filament_change_count_map = std::forward(other).filament_change_count_map; // Keep the SKIPPABLE per-type time on a copied result. - skippable_part_time = other.skippable_part_time; - initial_layer_time = other.initial_layer_time; + skippable_part_time = std::forward(other).skippable_part_time; + initial_layer_time = std::forward(other).initial_layer_time; #if ENABLE_GCODE_VIEWER_STATISTICS - time = other.time; + time = std::forward(other).time; #endif - return *this; } void lock() const { result_mutex.lock(); } void unlock() const { result_mutex.unlock(); } @@ -1216,6 +1219,9 @@ class Print; EProducer m_producer; TimeProcessor m_time_processor; + // calculate_time()'s map from each block's move id to its index after the actual speed moves are inserted, + // a member to reuse its capacity. + std::vector> m_actual_speed_id_map; UsedFilaments m_used_filaments; Print* m_print{ nullptr }; diff --git a/src/libslic3r/GCodeWriter.cpp b/src/libslic3r/GCodeWriter.cpp index 667271bcd1..ca18ab791e 100644 --- a/src/libslic3r/GCodeWriter.cpp +++ b/src/libslic3r/GCodeWriter.cpp @@ -465,32 +465,31 @@ std::string GCodeWriter::set_accel_and_jerk(unsigned int acceleration, double je if (EXTRUDER_LIMIT(m_max_acceleration) > 0 && acceleration > EXTRUDER_LIMIT(m_max_acceleration)) acceleration = EXTRUDER_LIMIT(m_max_acceleration); - bool is_empty = true; - std::ostringstream gcode; - gcode << "SET_VELOCITY_LIMIT"; - if (acceleration != 0 && acceleration != m_last_acceleration) { - gcode << " ACCEL=" << acceleration; - if (this->config.accel_to_decel_enable) { - gcode << " ACCEL_TO_DECEL=" << acceleration * this->config.accel_to_decel_factor / 100; - } - m_last_acceleration = acceleration; - is_empty = false; - } // Clamp the jerk to the allowed maximum. if (EXTRUDER_LIMIT(m_max_jerk_x) > 0 && jerk > EXTRUDER_LIMIT(m_max_jerk_x)) jerk = EXTRUDER_LIMIT(m_max_jerk_x); if (EXTRUDER_LIMIT(m_max_jerk_y) > 0 && jerk > EXTRUDER_LIMIT(m_max_jerk_y)) jerk = EXTRUDER_LIMIT(m_max_jerk_y); - if (jerk > 0.01 && !is_approx(jerk, m_last_jerk)) { + const bool set_acceleration = acceleration != 0 && acceleration != m_last_acceleration; + const bool set_jerk = jerk > 0.01 && !is_approx(jerk, m_last_jerk); + if (!set_acceleration && !set_jerk) + return std::string(); + + std::ostringstream gcode; + gcode << "SET_VELOCITY_LIMIT"; + if (set_acceleration) { + gcode << " ACCEL=" << acceleration; + if (this->config.accel_to_decel_enable) { + gcode << " ACCEL_TO_DECEL=" << acceleration * this->config.accel_to_decel_factor / 100; + } + m_last_acceleration = acceleration; + } + if (set_jerk) { gcode << " SQUARE_CORNER_VELOCITY=" << jerk; m_last_jerk = jerk; - is_empty = false; } - if(is_empty) - return std::string(); - if (GCodeWriter::full_gcode_comment) gcode << " ; adjust VELOCITY_LIMIT(accel/jerk)"; gcode << "\n"; diff --git a/src/libslic3r/LocalesUtils.cpp b/src/libslic3r/LocalesUtils.cpp index ead94faeb4..b73861c9db 100644 --- a/src/libslic3r/LocalesUtils.cpp +++ b/src/libslic3r/LocalesUtils.cpp @@ -84,6 +84,19 @@ double string_to_double_decimal_point(const std::string_view str, size_t* pos /* return out; } +double atof_decimal_point(std::string_view str) +{ + size_t i = 0; + while (i < str.size() && (str[i] == ' ' || (str[i] >= '\t' && str[i] <= '\r'))) + ++i; + if (i < str.size() && str[i] == '+') { + ++i; + if (i < str.size() && str[i] == '-') + return 0.; + } + return string_to_double_decimal_point(str.substr(i)); +} + std::string float_to_string_decimal_point(double value, int precision/* = -1*/) { // Our Windows build server fully supports C++17 std::to_chars. Let's use it. diff --git a/src/libslic3r/LocalesUtils.hpp b/src/libslic3r/LocalesUtils.hpp index c212a72e69..c2e020044a 100644 --- a/src/libslic3r/LocalesUtils.hpp +++ b/src/libslic3r/LocalesUtils.hpp @@ -48,6 +48,9 @@ bool is_decimal_separator_point(); std::string float_to_string_decimal_point(double value, int precision = -1); //std::string float_to_string_decimal_point(float value, int precision = -1); double string_to_double_decimal_point(const std::string_view str, size_t* pos = nullptr); +// Parses like atof in the C locale, skipping leading whitespace and a '+', +// without the C runtime's per-call locale lookup. +double atof_decimal_point(std::string_view str); } // namespace Slic3r diff --git a/src/libslic3r/Utils.hpp b/src/libslic3r/Utils.hpp index cc84b93939..29adae1b74 100644 --- a/src/libslic3r/Utils.hpp +++ b/src/libslic3r/Utils.hpp @@ -25,6 +25,8 @@ #include #include #include +#include +#include #include #include @@ -302,6 +304,14 @@ extern bool is_absolute_path_within_root(const boost::filesystem::path &path, co // Anything unknown is not safe. extern bool is_safe_to_open_file_name(const std::string &file_name); +// Case-insensitive compare against a fixed ASCII keyword, without boost::iequals, whose +// std::locale() takes a lock the whole process shares in the MSVC runtime. +inline bool ascii_iequals(std::string_view a, std::string_view b) +{ + auto lower = [](char c) { return (c >= 'A' && c <= 'Z') ? char(c - 'A' + 'a') : c; }; + return a.size() == b.size() && std::equal(a.begin(), a.end(), b.begin(), [&lower](char x, char y) { return lower(x) == lower(y); }); +} + // Orca: custom protocal support utils inline bool is_orca_open(const std::string& url) { return boost::starts_with(url, "orcaslicer://open"); } inline bool is_prusaslicer_open(const std::string& url) { return boost::starts_with(url, "prusaslicer://open"); } diff --git a/tests/fff_print/test_gcodeprocessor.cpp b/tests/fff_print/test_gcodeprocessor.cpp index 6914ec4df9..eeca017220 100644 --- a/tests/fff_print/test_gcodeprocessor.cpp +++ b/tests/fff_print/test_gcodeprocessor.cpp @@ -4,7 +4,12 @@ #include #include "libslic3r/libslic3r.h" #include "libslic3r/GCode/GCodeProcessor.hpp" +#include "libslic3r/Utils.hpp" +#include "test_utils.hpp" + +#include +#include #include #include @@ -79,3 +84,90 @@ TEST_CASE("Reserved keyword detection reports every offending line", "[GCodeProc CHECK(tags.empty()); } } + +namespace { + +// Closed outer-wall squares, each after a fast travel and before an inner-wall move, so the processor +// records seams and inserts actual speed moves. virtual_moves adds a VG1 move after each square. +void process_squares(int squares, GCodeProcessorResult &result, bool virtual_moves = false) +{ + std::ostringstream gcode; + gcode << "M83\nG90\n"; + for (int i = 0; i < squares; ++i) { + gcode << "G1 X10 Y10 Z" << 0.2 * (i + 1) << " F12000\n" + << "; FEATURE: Outer wall\n" + << "G1 X50 Y10 E2 F3000\nG1 X50 Y50 E2\nG1 X10 Y50 E2\nG1 X10 Y10 E2\n" + << "; FEATURE: Inner wall\n" + << "G1 X12 Y12 E0.1\nG1 X30 Y12 E1\n"; + if (virtual_moves) + gcode << "VG1 X20 Y30 F12000\n"; + } + FullPrintConfig config; + config.gcode_flavor.value = gcfMarlinFirmware; + // s_IsBBLPrinter selects the "; FEATURE: " role tags this G-code uses. + const bool was_bbl_printer = GCodeProcessor::s_IsBBLPrinter; + const ScopeGuard restore_bbl_printer([was_bbl_printer] { GCodeProcessor::s_IsBBLPrinter = was_bbl_printer; }); + GCodeProcessor::s_IsBBLPrinter = true; + ScopedTemporaryFile temp(".gcode"); + std::ofstream(temp.string()) << gcode.str(); + GCodeProcessor processor; + processor.apply_config(config); + processor.process_file(temp.string()); + result = std::move(processor.extract_result()); +} + +bool is_block_move(const GCodeProcessorResult::MoveVertex &move) +{ + return !move.internal_only && (move.type == EMoveType::Extrude || move.type == EMoveType::Travel); +} + +} // namespace + +TEST_CASE("Actual speed moves are inserted on their block's segment just before its move", "[GCodeProcessor]") +{ + // 60 squares take several planner passes, which remap the blocks kept between passes. + const int squares = GENERATE(10, 60); + const bool virtual_moves = GENERATE(false, true); + GCodeProcessorResult result; + process_squares(squares, result, virtual_moves); + const auto &moves = result.moves; + constexpr size_t normal = size_t(PrintEstimatedStatistics::ETimeMode::Normal); + + size_t inserted = 0; + for (size_t i = 1; i < moves.size(); ++i) { + if (!moves[i].internal_only) + continue; + ++inserted; + // Inserted moves have zero time, but a VG1 block's time is written to whatever move its move_id names. + if (!virtual_moves) + CHECK(moves[i].time[normal] == 0.f); + size_t block = i + 1; + while (block < moves.size() && moves[block].internal_only) + ++block; + size_t previous = i - 1; + while (previous > 0 && moves[previous].internal_only) + --previous; + REQUIRE(block < moves.size()); + CHECK(moves[block].gcode_id == moves[i].gcode_id); + const Vec3f segment = moves[block].position - moves[previous].position; + const Vec3f offset = moves[i].position - moves[previous].position; + CHECK(segment.cross(offset).norm() / segment.norm() < 1e-3f); + } + REQUIRE(inserted > 0); +} + +TEST_CASE("A seam takes the actual speed of the move it follows", "[GCodeProcessor]") +{ + GCodeProcessorResult result; + // 10 squares fit in one planner pass, so the seam's move and the block after it are timed together. + process_squares(10, result); + const auto &moves = result.moves; + + size_t seams = 0; + for (size_t i = 1; i < moves.size(); ++i) + if (moves[i].type == EMoveType::Seam && is_block_move(moves[i - 1])) { + ++seams; + CHECK_THAT(moves[i].actual_feedrate, Catch::Matchers::WithinAbs(moves[i - 1].actual_feedrate, 1e-4)); + } + REQUIRE(seams > 0); +} diff --git a/tests/fff_print/test_multifilament.cpp b/tests/fff_print/test_multifilament.cpp index 9b9cae23db..d3df8807e3 100644 --- a/tests/fff_print/test_multifilament.cpp +++ b/tests/fff_print/test_multifilament.cpp @@ -22,6 +22,7 @@ #include "libslic3r/Print.hpp" #include #include +#include #include #include #include @@ -842,6 +843,47 @@ TEST_CASE("Each filament sets the pressure advance of its extruder variant", "[M } } +// A two-extruder printer, Standard nozzle on extruder 1 and High Flow on extruder 2, whose per-variant arrays +// hold filament 1 Standard, filament 1 High Flow, filament 2 Standard and filament 2 High Flow. +static DynamicPrintConfig two_extruder_pressure_advance_config(const std::string &filament_map, const std::string &adaptive_pressure_advance, + int wall_filament, int infill_filament) +{ + DynamicPrintConfig config = multifilament_config(2, { + { "gcode_flavor", "klipper" }, + { "single_extruder_multi_material", 0 }, + { "nozzle_diameter", "0.4,0.4" }, + { "extruder_printable_height", "0,0" }, + { "printer_extruder_id", "1,2" }, + { "printer_extruder_variant", "Direct Drive Standard;Direct Drive High Flow" }, + { "extruder_variant_list", "Direct Drive Standard;Direct Drive High Flow" }, + { "filament_map", filament_map }, + { "filament_extruder_variant", "Direct Drive Standard;Direct Drive High Flow;Direct Drive Standard;Direct Drive High Flow" }, + { "filament_self_index", "1,1,2,2" }, + { "enable_pressure_advance", "1,1,1,1" }, + { "pressure_advance", "0.021,0.037,0.043,0.049" }, + { "adaptive_pressure_advance", adaptive_pressure_advance }, + { "sparse_infill_filament_id", infill_filament }, + { "internal_solid_filament_id", infill_filament }, + { "top_surface_filament_id", infill_filament }, + { "bottom_surface_filament_id", infill_filament }, + { "outer_wall_filament_id", wall_filament }, + { "inner_wall_filament_id", wall_filament }, + { "enable_prime_tower", 0 }, + { "skirt_loops", 0 }, + { "brim_type", "no_brim" }, + // custom G-code indexes the per-filament arrays by filament + { "machine_start_gcode", "; start pressure advance {pressure_advance[initial_extruder]}" }, + }); + config.option("adaptive_pressure_advance_model")->values = { + constant_pressure_advance_model("0.012"), constant_pressure_advance_model("0.034"), + constant_pressure_advance_model("0.056"), constant_pressure_advance_model("0.078") }; + config.option("extruder_type", true)->values = { etDirectDrive, etDirectDrive }; + config.option("nozzle_volume_type", true)->values = { nvtStandard, nvtHighFlow }; + // print each filament on the extruder filament_map gives it + config.option>("filament_map_mode", true)->value = fmmManual; + return config; +} + // On a printer with two extruders, a filament takes the pressure advance of the variant of the extruder // it is mapped to, whichever filament and extruder that is. TEST_CASE("Each filament sets the pressure advance of its extruder variant on a two-extruder printer", "[MultiFilament]") @@ -857,42 +899,8 @@ TEST_CASE("Each filament sets the pressure advance of its extruder variant on a // the other filament goes on the other extruder const std::string filament_map = filament == 1 ? std::to_string(extruder) + "," + std::to_string(3 - extruder) : std::to_string(3 - extruder) + "," + std::to_string(extruder); - DynamicPrintConfig config = multifilament_config(2, { - { "gcode_flavor", "klipper" }, - { "single_extruder_multi_material", 0 }, - { "nozzle_diameter", "0.4,0.4" }, - { "extruder_printable_height", "0,0" }, - // extruder 1 has a Standard nozzle, extruder 2 a High Flow one - { "printer_extruder_id", "1,2" }, - { "printer_extruder_variant", "Direct Drive Standard;Direct Drive High Flow" }, - { "extruder_variant_list", "Direct Drive Standard;Direct Drive High Flow" }, - { "filament_map", filament_map }, - // both filaments define Standard and High Flow - { "filament_extruder_variant", "Direct Drive Standard;Direct Drive High Flow;Direct Drive Standard;Direct Drive High Flow" }, - { "filament_self_index", "1,1,2,2" }, - { "enable_pressure_advance", "1,1,1,1" }, - { "pressure_advance", "0.021,0.037,0.043,0.049" }, - { "adaptive_pressure_advance", adaptive ? "1,1,1,1" : "0,0,0,0" }, - { "sparse_infill_filament_id", filament }, - { "internal_solid_filament_id", filament }, - { "top_surface_filament_id", filament }, - { "bottom_surface_filament_id", filament }, - { "outer_wall_filament_id", filament }, - { "inner_wall_filament_id", filament }, - { "enable_prime_tower", 0 }, - { "skirt_loops", 0 }, - { "brim_type", "no_brim" }, - // custom G-code indexes the per-filament arrays by filament - { "machine_start_gcode", "; start pressure advance {pressure_advance[initial_extruder]}" }, - }); - config.option("adaptive_pressure_advance_model")->values = { - constant_pressure_advance_model("0.012"), constant_pressure_advance_model("0.034"), - constant_pressure_advance_model("0.056"), constant_pressure_advance_model("0.078") }; - config.option("extruder_type", true)->values = { etDirectDrive, etDirectDrive }; - config.option("nozzle_volume_type", true)->values = { nvtStandard, nvtHighFlow }; - // keep the mapping above rather than grouping the filaments automatically - config.option>("filament_map_mode", true)->value = fmmManual; - const std::string gcode = slice({ cube(20) }, config); + const std::string gcode = slice({ cube(20) }, + two_extruder_pressure_advance_config(filament_map, adaptive ? "1,1,1,1" : "0,0,0,0", filament, filament)); std::set expected{ pressure_advance }; if (adaptive) @@ -902,6 +910,82 @@ TEST_CASE("Each filament sets the pressure advance of its extruder variant on a } } +// Filament 1 prints the walls on extruder 1 (variant index 0), filament 2 the infill on extruder 2 (variant index 3). +TEST_CASE("Adaptive pressure advance on one extruder leaves the other extruder's pressure advance alone", "[MultiFilament]") +{ + auto [adaptive, expected] = GENERATE(table>({ + { "1,0,0,0", { "0.021", "0.049", "0.012" } }, + { "0,0,0,1", { "0.021", "0.049", "0.078" } }, + })); + DYNAMIC_SECTION("adaptive " << adaptive) { + const std::string gcode = slice({ cube(20) }, two_extruder_pressure_advance_config("1,2", adaptive, 1, 2)); + CHECK(pressure_advance_values(gcode) == expected); + } +} + +// The pressure advance values a Klipper G-code sets while `tool` is active, in order, without repeating the value already set. +static std::vector pressure_advance_sequence(const std::string &gcode, int tool) +{ + const std::string token = "SET_PRESSURE_ADVANCE ADVANCE="; + std::vector values; + int current_tool = 0; + std::istringstream stream(gcode); + for (std::string line; std::getline(stream, line);) { + if (line.size() > 1 && line[0] == 'T' && std::isdigit((unsigned char) line[1])) + current_tool = std::stoi(line.substr(1)); + else if (current_tool == tool && line.rfind(token, 0) == 0) { + std::string value = line.substr(token.size(), line.find(';') - token.size()); + if (values.empty() || values.back() != value) + values.push_back(std::move(value)); + } + } + return values; +} + +TEST_CASE("Adaptive pressure advance predicts the same values after layers only a non-adaptive extruder prints", "[MultiFilament]") +{ + auto slice_with = [](const std::string &adaptive) { + DynamicPrintConfig config = two_extruder_pressure_advance_config("1,2", adaptive, 1, 1); + config.set_deserialize_strict({ + { "print_sequence", "by object" }, + // extruder 2 moves at one speed on its first layer, so no G1 F follows its first PA_CHANGE tag + { "filament_max_volumetric_speed", "100" }, + { "slow_down_for_layer_cooling", "0" }, + { "travel_speed", "120,40" }, + { "retraction_speed", "30,40" }, + { "deretraction_speed", "30,40" }, + { "initial_layer_speed", "30,40" }, + { "initial_layer_infill_speed", "60,40" }, + }); + auto &models = config.option("adaptive_pressure_advance_model")->values; + // with adaptive pressure advance on, filament 1 gets PA_CHANGE tags on every layer but keeps its pressure advance + models[0] = constant_pressure_advance_model("0.021"); + // a prediction that rises with flow, so it depends on the print speed + models[3] = "0.01,1,1000\n0.09,40,1000\n0.01,1,100000\n0.09,40,100000"; + return slice_with_object_overrides({ cube(20), cube(20) }, config, + { {}, { { "outer_wall_filament_id", 2 }, { "inner_wall_filament_id", 2 }, { "sparse_infill_filament_id", 2 }, + { "internal_solid_filament_id", 2 }, { "top_surface_filament_id", 2 }, { "bottom_surface_filament_id", 2 } } }); + }; + const std::vector expected = pressure_advance_sequence(slice_with("1,0,0,1"), 1); + REQUIRE(expected.size() > 2); + CHECK(pressure_advance_sequence(slice_with("0,0,0,1"), 1) == expected); +} + +TEST_CASE("Adaptive pressure advance on an unused extruder variant leaves the G-code unchanged", "[MultiFilament]") +{ + const std::string adaptive = GENERATE("0,1,0,0", "0,0,1,0"); + DYNAMIC_SECTION("adaptive " << adaptive) { + // the time and object ids differ between any two slices, and the config block lists the setting itself + auto masked = [](const std::string &gcode) { + return std::regex_replace(gcode.substr(0, gcode.find("; CONFIG_BLOCK_START")), std::regex("; generated by .*| id:\\d+"), ""); + }; + const std::string reference = masked(slice({ cube(20) }, two_extruder_pressure_advance_config("1,2", "0,0,0,0", 1, 2))); + const std::string gcode = masked(slice({ cube(20) }, two_extruder_pressure_advance_config("1,2", adaptive, 1, 2))); + REQUIRE(reference.find("SET_PRESSURE_ADVANCE") != std::string::npos); + CHECK(gcode == reference); + } +} + // The speeds, in percent, a G-code turns a fan on at: the part cooling fan for `M106 S`, the auxiliary // fan for `M106 P2 S`. static std::set fan_speeds(const std::string &gcode, const std::string &command) diff --git a/tests/libslic3r/test_locales_utils.cpp b/tests/libslic3r/test_locales_utils.cpp index 2bb134af6b..7de755e15c 100644 --- a/tests/libslic3r/test_locales_utils.cpp +++ b/tests/libslic3r/test_locales_utils.cpp @@ -69,3 +69,22 @@ TEST_CASE("a setter nested in another sets C again when the locale changed betwe CHECK_FALSE(is_decimal_separator_point()); } } + +TEST_CASE("atof_decimal_point parses what atof parses in the C locale", "[LocalesUtils]") +{ + const auto [text, value] = GENERATE(table({ + { "5", 5. }, + { " 12.5", 12.5 }, + { "\t+3", 3. }, + { "\r\n7", 7. }, + { "-1.25", -1.25 }, + { "1e2", 100. }, + { ".5", 0.5 }, + { "12.5;comment", 12.5 }, + { "+-5", 0. }, + { "", 0. }, + { "abc", 0. }, + })); + INFO(text); + CHECK_THAT(atof_decimal_point(text), Catch::Matchers::WithinAbs(value, 1e-12)); +} diff --git a/tests/libslic3r/test_utils.cpp b/tests/libslic3r/test_utils.cpp index 35094f056a..49161d5d6c 100644 --- a/tests/libslic3r/test_utils.cpp +++ b/tests/libslic3r/test_utils.cpp @@ -509,3 +509,15 @@ TEST_CASE("is_safe_to_open_file_name rejects programs and anything it does not k INFO(unsafe); CHECK_FALSE(is_safe_to_open_file_name(unsafe)); } + +TEST_CASE("ascii_iequals compares ASCII letters regardless of case", "[Utils]") { + CHECK(ascii_iequals("set_velocity_limit", "SET_VELOCITY_LIMIT")); + CHECK(ascii_iequals("G28", "g28")); + CHECK(ascii_iequals("", "")); + CHECK_FALSE(ascii_iequals("G28", "G29")); + CHECK_FALSE(ascii_iequals("G2", "G28")); + CHECK_FALSE(ascii_iequals("G28", "G2")); + // Non-letters 0x20 apart are not equal. + CHECK_FALSE(ascii_iequals("[", "{")); + CHECK_FALSE(ascii_iequals("@", "`")); +}