From 5be5c90e59fa75433329f03b7b1012de3e6a7714 Mon Sep 17 00:00:00 2001 From: Kris Austin Date: Sat, 3 Oct 2026 18:29:54 -0500 Subject: [PATCH] perf: speed up G-code export by up to 8% via cheaper G-code text building (#16108) --- src/libslic3r/GCode.cpp | 82 +++++++++++----------- src/libslic3r/GCode.hpp | 6 +- src/libslic3r/GCodeReader.cpp | 17 ++++- src/libslic3r/GCodeReader.hpp | 10 ++- src/libslic3r/GCodeWriter.cpp | 94 +++++++++++++++++++------- src/libslic3r/GCodeWriter.hpp | 13 +++- src/libslic3r/LocalesUtils.cpp | 41 +++++++---- tests/fff_print/CMakeLists.txt | 1 + tests/fff_print/test_gcode.cpp | 40 +++++++++++ tests/fff_print/test_gcodewriter.cpp | 59 ++++++++++++++++ tests/libslic3r/CMakeLists.txt | 1 + tests/libslic3r/test_gcodereader.cpp | 54 +++++++++++++++ tests/libslic3r/test_locales_utils.cpp | 31 +++++++++ 13 files changed, 365 insertions(+), 84 deletions(-) create mode 100644 tests/fff_print/test_gcode.cpp create mode 100644 tests/libslic3r/test_gcodereader.cpp diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index 4599004858..c53dab15aa 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -5204,7 +5204,8 @@ namespace Skirt { // Orca: Klipper can't parse object names with spaces and other spetical characters std::string sanitize_instance_name(const std::string& name) { // Replace sequences of non-word characters with an underscore - std::string result = std::regex_replace(name, std::regex("[ !@#$%^&*()=+\\[\\]{};:\",']+"), "_"); + static const std::regex non_word_characters("[ !@#$%^&*()=+\\[\\]{};:\",']+"); + std::string result = std::regex_replace(name, non_word_characters, "_"); // Remove leading and trailing underscores if (!result.empty() && result.front() == '_') { result.erase(result.begin()); @@ -5218,12 +5219,16 @@ std::string sanitize_instance_name(const std::string& name) { inline std::string get_instance_name(const PrintObject *object, size_t inst_id) { auto obj_name = sanitize_instance_name(object->model_object()->name); - auto name = (boost::format("%1%_id_%2%_copy_%3%") % obj_name % object->get_id() % inst_id).str(); + auto name = obj_name + "_id_" + std::to_string(object->get_id()) + "_copy_" + std::to_string(inst_id); return sanitize_instance_name(name); } -inline std::string get_instance_name(const PrintObject *object, const PrintInstance &inst) { - return get_instance_name(object, inst.id); +const std::string& GCode::instance_name(const PrintInstance &instance) +{ + auto [it, inserted] = m_instance_names.try_emplace(&instance); + if (inserted) + it->second = get_instance_name(instance.print_object, instance.id); + return it->second; } std::string GCode::generate_skirt(const Print &print, @@ -6642,7 +6647,7 @@ LayerResult GCode::process_layer( const auto gflavor = print.config().gcode_flavor.value; if (gflavor == gcfKlipper) { m_writer.set_object_start_str(std::string("EXCLUDE_OBJECT_START NAME=") + - get_instance_name(&instance_to_print.print_object, inst.id) + "\n"); + instance_name(inst) + "\n"); } else if (gflavor == gcfMarlinLegacy || gflavor == gcfMarlinFirmware || gflavor == gcfRepRapFirmware) { std::string str = std::string("M486 S") + std::to_string(inst.unique_id) + "\n"; @@ -6791,7 +6796,7 @@ LayerResult GCode::process_layer( const auto gflavor = print.config().gcode_flavor.value; if (gflavor == gcfKlipper) { m_writer.set_object_end_str(std::string("EXCLUDE_OBJECT_END NAME=") + - get_instance_name(&instance_to_print.print_object, inst.id) + "\n"); + instance_name(inst) + "\n"); } else if (gflavor == gcfMarlinLegacy || gflavor == gcfMarlinFirmware || gflavor == gcfRepRapFirmware) { m_writer.set_object_end_str(std::string("M486 S-1\n")); } @@ -6866,7 +6871,7 @@ LayerResult GCode::process_layer( const auto gflavor = print.config().gcode_flavor.value; if (gflavor == gcfKlipper) { m_writer.set_object_start_str(std::string("EXCLUDE_OBJECT_START NAME=") + - get_instance_name(&instance_to_print.print_object, inst.id) + "\n"); + instance_name(inst) + "\n"); } else if (gflavor == gcfMarlinLegacy || gflavor == gcfMarlinFirmware || gflavor == gcfRepRapFirmware) { m_writer.set_object_start_str(std::string("M486 S") + std::to_string(inst.unique_id) + "\n"); } @@ -7092,7 +7097,7 @@ LayerResult GCode::process_layer( const auto gflavor = print.config().gcode_flavor.value; if (gflavor == gcfKlipper) { m_writer.set_object_end_str(std::string("EXCLUDE_OBJECT_END NAME=") + - get_instance_name(&instance_to_print.print_object, inst.id) + "\n"); + instance_name(inst) + "\n"); } else if (gflavor == gcfMarlinLegacy || gflavor == gcfMarlinFirmware || gflavor == gcfRepRapFirmware) { m_writer.set_object_end_str(std::string("M486 S-1\n")); } @@ -8084,12 +8089,12 @@ static float overhang_fan_overlap_threshold(int overhang_fan_threshold) } } -std::string GCode::_extrude(const ExtrusionPath &path, std::string description, double speed) +std::string GCode::_extrude(const ExtrusionPath &path, const std::string &path_description, double speed) { std::string gcode; - if (is_bridge(path.role())) - description += " (bridge)"; + const std::string bridge_description = is_bridge(path.role()) ? path_description + " (bridge)" : std::string(); + const std::string &description = bridge_description.empty() ? path_description : bridge_description; const ExtrusionPathSloped* sloped = dynamic_cast(&path); @@ -8760,7 +8765,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, // ORCA: End of adaptive PA code segment } - gcode += m_writer.set_speed(F, "", comment); + m_writer.set_speed(gcode, F, "", comment); { if (m_enable_cooling_markers) { if (enable_overhang_bridge_fan) { @@ -8784,7 +8789,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, double saved_z = m_writer.get_position().z(); for (const Line3& line : path.polyline.lines()) { - std::string tempDescription = description; + std::string flow_description; const double line_length = line.length() * SCALING_FACTOR; if (line_length < EPSILON) continue; @@ -8795,7 +8800,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, dE = m_small_area_infill_flow_compensator->modify_flow(line_length, dE, path.role()); if (m_config.gcode_comments && oldE > 0 && oldE != dE) { - tempDescription += Slic3r::format(" | Old Flow Value: %0.5f Length: %0.5f",oldE, line_length); + flow_description = description + Slic3r::format(" | Old Flow Value: %0.5f Length: %0.5f",oldE, line_length); } } if (path.z_contoured) { @@ -8814,24 +8819,24 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, if (z < 0.1) { throw RuntimeError("GCode: very low z"); } - gcode += m_writer.extrude_to_xyz(Vec3d(dest2d.x(), dest2d.y(), z), e, - GCodeWriter::full_gcode_comment ? tempDescription : ""); + m_writer.extrude_to_xyz(gcode, Vec3d(dest2d.x(), dest2d.y(), z), e, + flow_description.empty() ? description : flow_description); } else if (sloped == nullptr) { // Normal extrusion - gcode += m_writer.extrude_to_xy( + m_writer.extrude_to_xy(gcode, this->point_to_gcode(line.b.to_point()), dE, - GCodeWriter::full_gcode_comment ? tempDescription : "", path.is_force_no_extrusion()); + flow_description.empty() ? description : flow_description, path.is_force_no_extrusion()); } else { // Sloped extrusion const auto [z_ratio, e_ratio] = sloped->interpolate(path_length / total_length); Vec2d dest2d = this->point_to_gcode(line.b.to_point()); Vec3d dest3d(dest2d(0), dest2d(1), get_sloped_z(z_ratio)); - gcode += m_writer.extrude_to_xyz( + m_writer.extrude_to_xyz(gcode, dest3d, dE * e_ratio, - GCodeWriter::full_gcode_comment ? tempDescription : "", path.is_force_no_extrusion()); + flow_description.empty() ? description : flow_description, path.is_force_no_extrusion()); } check_and_insert_timelapse(line.b.to_point()); // Inline farthest-point snapshot } @@ -8839,13 +8844,13 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, // BBS: start to generate gcode from arc fitting data which includes line and arc const std::vector& fitting_result = path.polyline.fitting_result; for (size_t fitting_index = 0; fitting_index < fitting_result.size(); fitting_index++) { - std::string tempDescription = description; + std::string flow_description; switch (fitting_result[fitting_index].path_type) { case EMovePathType::Linear_move: { size_t start_index = fitting_result[fitting_index].start_point_index; size_t end_index = fitting_result[fitting_index].end_point_index; for (size_t point_index = start_index + 1; point_index < end_index + 1; point_index++) { - tempDescription = description; + flow_description.clear(); const Line line = Line(path.polyline.points[point_index - 1].to_point(), path.polyline.points[point_index].to_point()); const double line_length = line.length() * SCALING_FACTOR; if (line_length < EPSILON) @@ -8856,13 +8861,13 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, dE = m_small_area_infill_flow_compensator->modify_flow(line_length, dE, path.role()); if (m_config.gcode_comments && oldE > 0 && oldE != dE) { - tempDescription += Slic3r::format(" | Old Flow Value: %0.5f Length: %0.5f",oldE, line_length); + flow_description = description + Slic3r::format(" | Old Flow Value: %0.5f Length: %0.5f",oldE, line_length); } } - gcode += m_writer.extrude_to_xy( + m_writer.extrude_to_xy(gcode, this->point_to_gcode(line.b), dE, - GCodeWriter::full_gcode_comment ? tempDescription : "", path.is_force_no_extrusion()); + flow_description.empty() ? description : flow_description, path.is_force_no_extrusion()); check_and_insert_timelapse(line.b); // Inline farthest-point snapshot } break; @@ -8880,15 +8885,15 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, dE = m_small_area_infill_flow_compensator->modify_flow(arc_length, dE, path.role()); if (m_config.gcode_comments && oldE > 0 && oldE != dE) { - tempDescription += Slic3r::format(" | Old Flow Value: %0.5f Length: %0.5f",oldE, arc_length); + flow_description = description + Slic3r::format(" | Old Flow Value: %0.5f Length: %0.5f",oldE, arc_length); } } - gcode += m_writer.extrude_arc_to_xy( + m_writer.extrude_arc_to_xy(gcode, this->point_to_gcode(arc.end_point), center_offset, dE, arc.direction == ArcDirection::Arc_Dir_CCW, - GCodeWriter::full_gcode_comment ? tempDescription : "", path.is_force_no_extrusion()); + flow_description.empty() ? description : flow_description, path.is_force_no_extrusion()); check_and_insert_timelapse(arc.end_point); // Inline farthest-point snapshot break; } @@ -8912,7 +8917,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, Polyline3 l(p); total_length = l.length() * SCALING_FACTOR; } - gcode += m_writer.set_speed(last_set_speed, "", comment); + m_writer.set_speed(gcode, last_set_speed, "", comment); Vec3d prev = this->point_to_gcode_quantized(new_points[0].p); bool pre_fan_enabled = false; bool cur_fan_enabled = false; @@ -8924,7 +8929,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, double path_length = 0.; for (size_t i = 1; i < new_points.size(); i++) { - std::string tempDescription = description; + std::string flow_description; const ProcessedPoint &processed_point = new_points[i]; const ProcessedPoint &pre_processed_point = new_points[i-1]; Vec3d p = this->point_to_gcode_quantized(processed_point.p); @@ -8991,10 +8996,10 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, // Ignore small speed variations - emit speed change if the delta between current and new is greater than 60mm/min / 1mm/sec // Reset speed to F if delta to F is less than 1mm/sec if ((std::abs(last_set_speed - new_speed) > 60)) { - gcode += m_writer.set_speed(new_speed, "", comment); + m_writer.set_speed(gcode, new_speed, "", comment); last_set_speed = new_speed; } else if ((std::abs(F - new_speed) <= 60)) { - gcode += m_writer.set_speed(F, "", comment); + m_writer.set_speed(gcode, F, "", comment); last_set_speed = F; } auto dE = e_per_mm * line_length; @@ -9003,7 +9008,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, dE = m_small_area_infill_flow_compensator->modify_flow(line_length, dE, path.role()); if (m_config.gcode_comments && oldE > 0 && oldE != dE) { - tempDescription += Slic3r::format(" | Old Flow Value: %0.5f Length: %0.5f",oldE, line_length); + flow_description = description + Slic3r::format(" | Old Flow Value: %0.5f Length: %0.5f",oldE, line_length); } } if (path.z_contoured) { @@ -9021,16 +9026,16 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, if (z < 0.1) { throw RuntimeError("GCode: very low z"); } - gcode += m_writer.extrude_to_xyz(Vec3d(dest2d.x(), dest2d.y(), z), e, - GCodeWriter::full_gcode_comment ? tempDescription : ""); + m_writer.extrude_to_xyz(gcode, Vec3d(dest2d.x(), dest2d.y(), z), e, + flow_description.empty() ? description : flow_description); } else if (sloped == nullptr) { // Normal extrusion - gcode += m_writer.extrude_to_xy(p.head<2>(), dE, GCodeWriter::full_gcode_comment ? tempDescription : ""); + m_writer.extrude_to_xy(gcode, p.head<2>(), dE, flow_description.empty() ? description : flow_description); } else { // Sloped extrusion const auto [z_ratio, e_ratio] = sloped->interpolate(path_length / total_length); Vec3d dest3d(p(0), p(1), get_sloped_z(z_ratio)); - gcode += m_writer.extrude_to_xyz(dest3d, dE * e_ratio, GCodeWriter::full_gcode_comment ? tempDescription : ""); + m_writer.extrude_to_xyz(gcode, dest3d, dE * e_ratio, flow_description.empty() ? description : flow_description); } // Inline farthest-point snapshot on the variable-speed emission path. Inert unless the @@ -10074,6 +10079,7 @@ std::string GCode::set_object_info(Print *print) { // PA_Line has only one object, no EXCLUDE_OBJECT_DEFINE needed } else { size_t unique_id = 0; + m_instance_names.clear(); for (PrintObject* object : print->objects()) { object->set_id(object_id++); size_t inst_id = 0; @@ -10082,7 +10088,7 @@ std::string GCode::set_object_info(Print *print) { inst.id = inst_id++; auto bbox = inst.get_bounding_box(); auto center = print->translate_to_print_space(Vec2d(bbox.center().x(), bbox.center().y())); - auto inst_name = get_instance_name(object, inst); + const std::string &inst_name = instance_name(inst); if (gflavor == gcfKlipper) { gcode << "EXCLUDE_OBJECT_DEFINE NAME=" << inst_name << " CENTER=" << center.x() << "," << center.y() << " POLYGON=" << polygon_to_string(inst.get_convex_hull_2d(), print) << "\n"; diff --git a/src/libslic3r/GCode.hpp b/src/libslic3r/GCode.hpp index 6b050a4739..cc8c56000f 100644 --- a/src/libslic3r/GCode.hpp +++ b/src/libslic3r/GCode.hpp @@ -42,6 +42,7 @@ #include #include #include +#include #include #include #include @@ -715,6 +716,9 @@ private: bool m_enable_exclude_object; std::vector m_label_objects_ids; + // Object label names by instance, built on first use from the ids set_object_info() assigns. + std::unordered_map m_instance_names; + const std::string& instance_name(const PrintInstance &instance); std::string _encode_label_ids_to_base64(std::vector ids); // ORCA: Add support for role based fan speed control std::array m_is_role_based_fan_on; @@ -827,7 +831,7 @@ private: void update_layer_related_config(int layer_id); double calc_max_volumetric_speed(const double layer_height, const double line_width, const std::string co_str); - std::string _extrude(const ExtrusionPath &path, std::string description = "", double speed = -1); + std::string _extrude(const ExtrusionPath &path, const std::string &path_description = "", double speed = -1); bool _needSAFC(const ExtrusionPath &path); void print_machine_envelope(GCodeOutputStream& file, Print& print); void _print_first_layer_bed_temperature(GCodeOutputStream &file, Print &print, const std::string &gcode, unsigned int first_printing_extruder_id, bool wait); diff --git a/src/libslic3r/GCodeReader.cpp b/src/libslic3r/GCodeReader.cpp index 10c3be600f..20acecec76 100644 --- a/src/libslic3r/GCodeReader.cpp +++ b/src/libslic3r/GCodeReader.cpp @@ -21,6 +21,7 @@ #include #include +#include #include #include #include @@ -28,14 +29,24 @@ namespace Slic3r { +const GCodeConfig& GCodeReader::default_config() +{ + static const GCodeConfig config; + return config; +} + void GCodeReader::apply_config(const GCodeConfig &config) { - m_config = config; + m_config = std::make_shared(config); + m_use_relative_e_distances = config.use_relative_e_distances.value; } void GCodeReader::apply_config(const DynamicPrintConfig &config) { - m_config.apply(config, true); + auto applied = std::make_shared(m_config ? *m_config : default_config()); + applied->apply(config, true); + m_use_relative_e_distances = applied->use_relative_e_distances.value; + m_config = std::move(applied); } const char* GCodeReader::parse_line_internal(const char *ptr, const char *end, GCodeLine &gline, std::pair &command) @@ -95,7 +106,7 @@ const char* GCodeReader::parse_line_internal(const char *ptr, const char *end, G } } - if (gline.has(E) && m_config.use_relative_e_distances) + if (gline.has(E) && m_use_relative_e_distances) m_position[E] = 0; // Skip the rest of the line. diff --git a/src/libslic3r/GCodeReader.hpp b/src/libslic3r/GCodeReader.hpp index 591fe41e6b..e65dff193c 100644 --- a/src/libslic3r/GCodeReader.hpp +++ b/src/libslic3r/GCodeReader.hpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -111,7 +112,7 @@ public: void reset() { memset(m_position, 0, sizeof(m_position)); } void apply_config(const GCodeConfig &config); void apply_config(const DynamicPrintConfig &config); - const GCodeConfig& config() { return m_config; }; + const GCodeConfig& config() { return m_config ? *m_config : default_config(); }; template void parse_buffer(const std::string &buffer, Callback callback) @@ -172,7 +173,7 @@ public: GCodeConfig get_config() const { - return m_config; + return m_config ? *m_config : default_config(); } private: @@ -199,8 +200,11 @@ private: return c; } static const char* axis_pos(const char *raw_str, char axis); + static const GCodeConfig& default_config(); - GCodeConfig m_config; + // Set by apply_config() and shared by copies, so a reader without a config builds none. + std::shared_ptr m_config; + bool m_use_relative_e_distances{ default_config().use_relative_e_distances.value }; float m_position[NUM_AXES]; bool m_verbose; // To be set by the callback to stop parsing. diff --git a/src/libslic3r/GCodeWriter.cpp b/src/libslic3r/GCodeWriter.cpp index ca18ab791e..a862ebf2f1 100644 --- a/src/libslic3r/GCodeWriter.cpp +++ b/src/libslic3r/GCodeWriter.cpp @@ -9,6 +9,7 @@ #include "ClipperUtils.hpp" #include "Geometry/ArcWelder.hpp" #include "Line.hpp" +#include "LocalesUtils.hpp" #include "libslic3r.h" #include #include @@ -380,26 +381,35 @@ std::string GCodeWriter::set_acceleration_internal(Acceleration type, unsigned i last_value = acceleration; - std::ostringstream gcode; - if (FLAVOR_IS(gcfRepetier)) - gcode << (separate_travel ? "M202 X" : "M201 X") << acceleration << " Y" << acceleration; - else if (FLAVOR_IS(gcfRepRapFirmware) || FLAVOR_IS(gcfMarlinFirmware)) - gcode << (separate_travel ? "M204 T" : "M204 P") << acceleration; - else if (FLAVOR_IS(gcfKlipper)) { - gcode << "SET_VELOCITY_LIMIT ACCEL=" << acceleration; + const std::string value = std::to_string(acceleration); + std::string gcode; + if (FLAVOR_IS(gcfRepetier)) { + gcode += separate_travel ? "M202 X" : "M201 X"; + gcode += value; + gcode += " Y"; + gcode += value; + } else if (FLAVOR_IS(gcfRepRapFirmware) || FLAVOR_IS(gcfMarlinFirmware)) { + gcode += separate_travel ? "M204 T" : "M204 P"; + gcode += value; + } else if (FLAVOR_IS(gcfKlipper)) { + gcode.reserve(96); + gcode += "SET_VELOCITY_LIMIT ACCEL="; + gcode += value; if (this->config.accel_to_decel_enable) { - gcode << " ACCEL_TO_DECEL=" << acceleration * this->config.accel_to_decel_factor / 100; + gcode += " ACCEL_TO_DECEL="; + gcode += float_to_string_decimal_point(acceleration * this->config.accel_to_decel_factor / 100); if (GCodeWriter::full_gcode_comment) - gcode << " ; adjust ACCEL_TO_DECEL"; + gcode += " ; adjust ACCEL_TO_DECEL"; } + } else { + gcode += "M204 S"; + gcode += value; } - else - gcode << "M204 S" << acceleration; - if (GCodeWriter::full_gcode_comment) gcode << " ; adjust acceleration"; - gcode << "\n"; + if (GCodeWriter::full_gcode_comment) gcode += " ; adjust acceleration"; + gcode += "\n"; - return gcode.str(); + return gcode; } std::string GCodeWriter::set_jerk_xy(double jerk) @@ -476,25 +486,29 @@ std::string GCodeWriter::set_accel_and_jerk(unsigned int acceleration, double je if (!set_acceleration && !set_jerk) return std::string(); - std::ostringstream gcode; - gcode << "SET_VELOCITY_LIMIT"; + std::string gcode; + gcode.reserve(96); + gcode += "SET_VELOCITY_LIMIT"; if (set_acceleration) { - gcode << " ACCEL=" << acceleration; + gcode += " ACCEL="; + gcode += std::to_string(acceleration); if (this->config.accel_to_decel_enable) { - gcode << " ACCEL_TO_DECEL=" << acceleration * this->config.accel_to_decel_factor / 100; + gcode += " ACCEL_TO_DECEL="; + gcode += float_to_string_decimal_point(acceleration * this->config.accel_to_decel_factor / 100); } m_last_acceleration = acceleration; } if (set_jerk) { - gcode << " SQUARE_CORNER_VELOCITY=" << jerk; + gcode += " SQUARE_CORNER_VELOCITY="; + gcode += float_to_string_decimal_point(jerk); m_last_jerk = jerk; } if (GCodeWriter::full_gcode_comment) - gcode << " ; adjust VELOCITY_LIMIT(accel/jerk)"; - gcode << "\n"; + gcode += " ; adjust VELOCITY_LIMIT(accel/jerk)"; + gcode += "\n"; - return gcode.str(); + return gcode; } @@ -753,6 +767,13 @@ double GCodeWriter::get_extruder_retracted_length(const int filament_id) } std::string GCodeWriter::set_speed(double F, const std::string &comment, const std::string &cooling_marker) +{ + std::string gcode; + this->set_speed(gcode, F, comment, cooling_marker); + return gcode; +} + +void GCodeWriter::set_speed(std::string &out, double F, const std::string &comment, const std::string &cooling_marker) { assert(F > 0.); assert(F < 100000.); @@ -763,7 +784,7 @@ std::string GCodeWriter::set_speed(double F, const std::string &comment, const s //BBS w.emit_comment(GCodeWriter::full_gcode_comment, comment); w.emit_string(cooling_marker); - return w.string(); + w.append_to(out); } std::string GCodeWriter::travel_to_xy(const Vec2d &point, const std::string &comment) @@ -1115,6 +1136,13 @@ bool GCodeWriter::will_move_z(double z) const } std::string GCodeWriter::extrude_to_xy(const Vec2d &point, double dE, const std::string &comment, bool force_no_extrusion) +{ + std::string gcode; + this->extrude_to_xy(gcode, point, dE, comment, force_no_extrusion); + return gcode; +} + +void GCodeWriter::extrude_to_xy(std::string &out, const Vec2d &point, double dE, const std::string &comment, bool force_no_extrusion) { m_pos(0) = point(0); m_pos(1) = point(1); @@ -1133,13 +1161,20 @@ std::string GCodeWriter::extrude_to_xy(const Vec2d &point, double dE, const std: w.emit_e(filament()->E()); //BBS w.emit_comment(GCodeWriter::full_gcode_comment, comment); - return w.string(); + w.append_to(out); } //BBS: generate G2 or G3 extrude which moves by arc //point is end point which means X and Y axis //center_offset is I and J axis std::string GCodeWriter::extrude_arc_to_xy(const Vec2d& point, const Vec2d& center_offset, double dE, const bool is_ccw, const std::string& comment, bool force_no_extrusion) +{ + std::string gcode; + this->extrude_arc_to_xy(gcode, point, center_offset, dE, is_ccw, comment, force_no_extrusion); + return gcode; +} + +void GCodeWriter::extrude_arc_to_xy(std::string &out, const Vec2d& point, const Vec2d& center_offset, double dE, const bool is_ccw, const std::string& comment, bool force_no_extrusion) { m_pos(0) = point(0); m_pos(1) = point(1); @@ -1155,10 +1190,17 @@ std::string GCodeWriter::extrude_arc_to_xy(const Vec2d& point, const Vec2d& cent w.emit_e(filament()->E()); //BBS w.emit_comment(GCodeWriter::full_gcode_comment, comment); - return w.string(); + w.append_to(out); } std::string GCodeWriter::extrude_to_xyz(const Vec3d &point, double dE, const std::string &comment, bool force_no_extrusion) +{ + std::string gcode; + this->extrude_to_xyz(gcode, point, dE, comment, force_no_extrusion); + return gcode; +} + +void GCodeWriter::extrude_to_xyz(std::string &out, const Vec3d &point, double dE, const std::string &comment, bool force_no_extrusion) { // Check if Z actually changes (at export precision) before emitting it. // ZAA sloped extrusions call this for every segment, but many consecutive @@ -1182,7 +1224,7 @@ std::string GCodeWriter::extrude_to_xyz(const Vec3d &point, double dE, const std w.emit_e(filament()->E()); //BBS w.emit_comment(GCodeWriter::full_gcode_comment, comment); - return w.string(); + w.append_to(out); } std::string GCodeWriter::retract(bool before_wipe, double retract_length) diff --git a/src/libslic3r/GCodeWriter.hpp b/src/libslic3r/GCodeWriter.hpp index a07765a873..447d70b552 100644 --- a/src/libslic3r/GCodeWriter.hpp +++ b/src/libslic3r/GCodeWriter.hpp @@ -92,6 +92,11 @@ public: //BBS: generate G2 or G3 extrude which moves by arc std::string extrude_arc_to_xy(const Vec2d &point, const Vec2d ¢er_offset, double dE, const bool is_ccw, const std::string &comment = std::string(), bool force_no_extrusion = false); std::string extrude_to_xyz(const Vec3d &point, double dE, const std::string &comment = std::string(), bool force_no_extrusion = false); + // Each appends its line to `out`. + void set_speed(std::string &out, double F, const std::string &comment = std::string(), const std::string &cooling_marker = std::string()); + void extrude_to_xy(std::string &out, const Vec2d &point, double dE, const std::string &comment = std::string(), bool force_no_extrusion = false); + void extrude_arc_to_xy(std::string &out, const Vec2d &point, const Vec2d ¢er_offset, double dE, const bool is_ccw, const std::string &comment = std::string(), bool force_no_extrusion = false); + void extrude_to_xyz(std::string &out, const Vec3d &point, double dE, const std::string &comment = std::string(), bool force_no_extrusion = false); std::string retract(bool before_wipe = false, double retract_length = 0); std::string retract_for_toolchange(bool before_wipe = false, double retract_length = 0); // extra_retract adds a small over-extrusion to the deretract move (PETG pre-extrusion). @@ -299,8 +304,14 @@ public: } std::string string() { + std::string out; + this->append_to(out); + return out; + } + + void append_to(std::string &out) { *ptr_err.ptr ++ = '\n'; - return std::string(this->buf, ptr_err.ptr - buf); + out.append(this->buf, ptr_err.ptr - buf); } protected: diff --git a/src/libslic3r/LocalesUtils.cpp b/src/libslic3r/LocalesUtils.cpp index b73861c9db..d792692cd2 100644 --- a/src/libslic3r/LocalesUtils.cpp +++ b/src/libslic3r/LocalesUtils.cpp @@ -6,15 +6,22 @@ #include #include -#ifdef _WIN32 - #include -#endif +#include #include #include -#include +#include #include +// Defined where the floating point std::to_chars can be called, which with Apple's libc++ runtime is from macOS 13.3. +#if defined(_LIBCPP_VERSION) + #if defined(_LIBCPP_AVAILABILITY_HAS_TO_CHARS_FLOATING_POINT) && _LIBCPP_AVAILABILITY_HAS_TO_CHARS_FLOATING_POINT + #define SLIC3R_FLOAT_TO_CHARS + #endif +#elif defined(_WIN32) || defined(__cpp_lib_to_chars) + #define SLIC3R_FLOAT_TO_CHARS +#endif + namespace Slic3r { @@ -99,9 +106,9 @@ double atof_decimal_point(std::string_view str) 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. - // Other platforms are behind, fall back to slow stringstreams for now. -#ifdef _WIN32 + // Every branch prints the same digits in the classic locale as the stream at the end, which takes over when a branch + // is compiled out or the value is too long for the buffer. +#if defined(SLIC3R_FLOAT_TO_CHARS) constexpr size_t SIZE = 20; char out[SIZE] = ""; std::to_chars_result res; @@ -109,16 +116,26 @@ std::string float_to_string_decimal_point(double value, int precision/* = -1*/) res = std::to_chars(out, out+SIZE, value, std::chars_format::fixed, precision); else res = std::to_chars(out, out+SIZE, value, std::chars_format::general, 6); - if (res.ec == std::errc::value_too_large) - throw std::invalid_argument("float_to_string_decimal_point conversion failed."); - return std::string(out, res.ptr - out); -#else + if (res.ec == std::errc()) + return std::string(out, res.ptr - out); +#elif defined(__APPLE__) + // Formats in the C locale, as libc++'s stream does, switching only this thread's locale for the call. + static const locale_t c_locale = newlocale(LC_NUMERIC_MASK, "C", (locale_t) 0); + if (c_locale != (locale_t) 0) { + constexpr size_t SIZE = 20; + char out[SIZE]; + const locale_t previous = uselocale(c_locale); + const int length = precision >= 0 ? snprintf(out, SIZE, "%.*f", precision, value) : snprintf(out, SIZE, "%.*g", 6, value); + uselocale(previous); + if (length >= 0 && size_t(length) < SIZE) + return std::string(out, length); + } +#endif std::stringstream buf; if (precision >= 0) buf << std::fixed << std::setprecision(precision); buf << value; return buf.str(); -#endif } diff --git a/tests/fff_print/CMakeLists.txt b/tests/fff_print/CMakeLists.txt index 822844f50b..5c550931f8 100644 --- a/tests/fff_print/CMakeLists.txt +++ b/tests/fff_print/CMakeLists.txt @@ -9,6 +9,7 @@ add_executable(${_TEST_NAME}_tests test_fill.cpp test_flow.cpp test_gcode_timing.cpp + test_gcode.cpp test_gcodeprocessor.cpp test_gcodewriter.cpp test_model.cpp diff --git a/tests/fff_print/test_gcode.cpp b/tests/fff_print/test_gcode.cpp new file mode 100644 index 0000000000..45c50c9187 --- /dev/null +++ b/tests/fff_print/test_gcode.cpp @@ -0,0 +1,40 @@ +#include +#include + +#include "libslic3r/BoundingBox.hpp" +#include "libslic3r/Model.hpp" +#include "libslic3r/ModelArrange.hpp" +#include "libslic3r/Print.hpp" +#include "libslic3r/PrintConfig.hpp" + +#include "test_helpers.hpp" + +#include +#include + +using namespace Slic3r; + +TEST_CASE("Klipper object labels name each copy without the characters Klipper cannot parse", "[GCode]") +{ + const auto [name, label] = GENERATE(table({ + {"my part (2)", "my_part_2"}, + {"(cube)", "cube"}, + })); + DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); + config.set_deserialize_strict({{"gcode_flavor", "klipper"}, {"exclude_object", "1"}}); + Print print; + Model model; + Test::init_print(std::vector{Test::cube(20.)}, print, model, config, nullptr, false, 2); + model.objects.front()->name = name; + arrange_objects(model, BoundingBox{Point::new_scale(0., 0.), Point::new_scale(500., 500.)}, + ArrangeParams{scaled(min_object_distance(config))}); + print.apply(model, config); + + const std::string gcode = Test::gcode(print); + for (const char *copy : {"0", "1"}) { + const std::string instance_label = label + "_id_0_copy_" + copy; + INFO(instance_label); + CHECK(gcode.find("EXCLUDE_OBJECT_DEFINE NAME=" + instance_label + " ") != std::string::npos); + CHECK(gcode.find("EXCLUDE_OBJECT_START NAME=" + instance_label + "\n") != std::string::npos); + } +} diff --git a/tests/fff_print/test_gcodewriter.cpp b/tests/fff_print/test_gcodewriter.cpp index 992204d716..f8bb2fbe39 100644 --- a/tests/fff_print/test_gcodewriter.cpp +++ b/tests/fff_print/test_gcodewriter.cpp @@ -454,6 +454,65 @@ TEST_CASE("EXTRUDER_LIMIT per-extruder clamping and max fallback", "[GCodeWriter } } +TEST_CASE("Acceleration and velocity limit commands print their values in general notation", "[GCodeWriter]") +{ + enum class Command { Print, Travel, KlipperLimits }; + struct Case + { + GCodeFlavor flavor; + Command command; + unsigned int acceleration; + double jerk; + bool comments; + std::vector present; + std::vector absent; + }; + // accel_to_decel_factor is 50%, so ACCEL_TO_DECEL is half the acceleration. + const Case c = GENERATE(values({ + {gcfKlipper, Command::KlipperLimits, 2000000, 25. / 3., false, + {"SET_VELOCITY_LIMIT ACCEL=2000000 ", "ACCEL_TO_DECEL=1e+06 ", "SQUARE_CORNER_VELOCITY=8.33333\n"}, {}}, + {gcfKlipper, Command::KlipperLimits, 12345, 0., false, {"ACCEL=12345 ", "ACCEL_TO_DECEL=6172.5\n"}, {"SQUARE_CORNER_VELOCITY"}}, + {gcfKlipper, Command::KlipperLimits, 0, 0.25, true, {"SQUARE_CORNER_VELOCITY=0.25 ", "; adjust VELOCITY_LIMIT"}, {"ACCEL"}}, + {gcfKlipper, Command::Print, 3001, 0., true, {"ACCEL=3001 ", "ACCEL_TO_DECEL=1500.5 ", "; adjust ACCEL_TO_DECEL", "; adjust acceleration"}, {}}, + {gcfMarlinFirmware, Command::Print, 2500, 0., false, {"M204 P2500\n"}, {}}, + {gcfMarlinFirmware, Command::Travel, 7000, 0., false, {"M204 T7000\n"}, {}}, + {gcfRepRapFirmware, Command::Travel, 7000, 0., true, {"M204 T7000 ", "; adjust acceleration"}, {}}, + {gcfMarlinLegacy, Command::Print, 2500, 0., false, {"M204 S2500\n"}, {}}, + {gcfRepetier, Command::Print, 2500, 0., false, {"M201 X2500 Y2500\n"}, {}}, + {gcfRepetier, Command::Travel, 7000, 0., false, {"M202 X7000 Y7000\n"}, {}}, + })); + + struct CommentGuard + { + bool saved = GCodeWriter::full_gcode_comment; + ~CommentGuard() { GCodeWriter::full_gcode_comment = saved; } + } comment_guard; + GCodeWriter::full_gcode_comment = c.comments; + + DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); + config.set_key_value("gcode_flavor", new ConfigOptionEnum(c.flavor)); + config.set_key_value("accel_to_decel_enable", new ConfigOptionBool(true)); + config.set_key_value("accel_to_decel_factor", new ConfigOptionPercent(50)); + for (const char *limit : {"machine_max_acceleration_extruding", "machine_max_acceleration_travel", "machine_max_acceleration_x", + "machine_max_acceleration_y", "machine_max_jerk_x", "machine_max_jerk_y"}) { + std::vector &values = config.option(limit)->values; + std::fill(values.begin(), values.end(), 0.); + } + PrintConfig print_config; + print_config.apply(config, true); + GCodeWriter writer; + writer.apply_print_config(print_config); + + const std::string line = c.command == Command::Print ? writer.set_print_acceleration(c.acceleration) : + c.command == Command::Travel ? writer.set_travel_acceleration(c.acceleration) : + writer.set_accel_and_jerk(c.acceleration, c.jerk); + INFO(line); + for (const std::string &token : c.present) + CHECK_THAT(line, Catch::Matchers::ContainsSubstring(token)); + for (const std::string &token : c.absent) + CHECK_THAT(line, !Catch::Matchers::ContainsSubstring(token)); +} + SCENARIO("Extruder reads the injected config column", "[GCodeWriter][H2C]") { GIVEN("A writer whose per-variant arrays hold three columns for two filaments") { GCodeWriter writer; diff --git a/tests/libslic3r/CMakeLists.txt b/tests/libslic3r/CMakeLists.txt index 1d7b6e6adb..c1fbe4408d 100644 --- a/tests/libslic3r/CMakeLists.txt +++ b/tests/libslic3r/CMakeLists.txt @@ -19,6 +19,7 @@ add_executable(${_TEST_NAME}_tests test_config_variant_expansion.cpp test_locales_utils.cpp test_drc.cpp + test_gcodereader.cpp test_toolordering_nozzle_group.cpp test_parallel_resolve.cpp test_preset_bundle_loading.cpp diff --git a/tests/libslic3r/test_gcodereader.cpp b/tests/libslic3r/test_gcodereader.cpp new file mode 100644 index 0000000000..ef2f5e7fc6 --- /dev/null +++ b/tests/libslic3r/test_gcodereader.cpp @@ -0,0 +1,54 @@ +#include +#include +#include + +#include "libslic3r/Config.hpp" +#include "libslic3r/GCodeReader.hpp" +#include "libslic3r/PrintConfig.hpp" + +using namespace Slic3r; + +namespace { + +// The extrusion of the second of two moves that both give E1. +float second_move_extrusion(GCodeReader &reader) +{ + float extrusion = -1.f; + reader.parse_buffer("G1 X1 E1\nG1 X2 E1\n", [&extrusion](GCodeReader &reader, const GCodeReader::GCodeLine &line) { + extrusion = line.dist_E(reader); + }); + return extrusion; +} + +} // namespace + +TEST_CASE("A G-code reader measures extrusion as relative or absolute as its config says", "[GCodeReader]") +{ + const bool relative = GENERATE(false, true); + const float expected = relative ? 1.f : 0.f; + + GCodeConfig config; + config.use_relative_e_distances.value = relative; + GCodeReader applied; + applied.apply_config(config); + CHECK(applied.config().use_relative_e_distances.value == relative); + CHECK_THAT(second_move_extrusion(applied), Catch::Matchers::WithinAbs(expected, 1e-6)); + + DynamicPrintConfig dynamic; + dynamic.set_key_value("use_relative_e_distances", new ConfigOptionBool(relative)); + GCodeReader applied_dynamic; + applied_dynamic.apply_config(dynamic); + CHECK_THAT(second_move_extrusion(applied_dynamic), Catch::Matchers::WithinAbs(expected, 1e-6)); + + GCodeReader copy = applied; + CHECK(copy.config().use_relative_e_distances.value == relative); + CHECK_THAT(second_move_extrusion(copy), Catch::Matchers::WithinAbs(expected, 1e-6)); +} + +TEST_CASE("A G-code reader without a config uses the default one", "[GCodeReader]") +{ + const bool relative = GCodeConfig().use_relative_e_distances.value; + GCodeReader reader; + CHECK(reader.config().use_relative_e_distances.value == relative); + CHECK_THAT(second_move_extrusion(reader), Catch::Matchers::WithinAbs(relative ? 1.f : 0.f, 1e-6)); +} diff --git a/tests/libslic3r/test_locales_utils.cpp b/tests/libslic3r/test_locales_utils.cpp index 7de755e15c..b1e3a21cd1 100644 --- a/tests/libslic3r/test_locales_utils.cpp +++ b/tests/libslic3r/test_locales_utils.cpp @@ -4,8 +4,11 @@ #include #include +#include #include "libslic3r/LocalesUtils.hpp" +#include + using namespace Slic3r; namespace { @@ -44,6 +47,34 @@ struct CommaNumericLocale } // namespace +TEST_CASE("Floats print as printf prints them in the C locale", "[LocalesUtils]") +{ + const auto [value, precision, text] = GENERATE(table({ + {0.5, -1, "0.5"}, + {25. / 3., -1, "8.33333"}, + {1500.5, -1, "1500.5"}, + {1e6, -1, "1e+06"}, + {-0.000123, -1, "-0.000123"}, + {25. / 3., 3, "8.333"}, + {2., 0, "2"}, + // Longer than the to_chars buffer. + {1e21, 2, "1000000000000000000000.00"}, + })); + CHECK(float_to_string_decimal_point(value, precision) == text); +} + +TEST_CASE("Floats print with a decimal point in a locale whose decimal separator is a comma", "[LocalesUtils]") +{ + CommaNumericLocale comma; + { + CNumericLocalesSetter outer; + if (! comma.apply()) + SKIP("no locale with a comma decimal separator is installed"); + CHECK(float_to_string_decimal_point(1500.5) == "1500.5"); + CHECK(float_to_string_decimal_point(25. / 3., 3) == "8.333"); + } +} + TEST_CASE("a setter nested in another leaves the C locale in place for the outer one", "[LocalesUtils]") { CNumericLocalesSetter outer;