diff --git a/src/libslic3r/CMakeLists.txt b/src/libslic3r/CMakeLists.txt index 72f741ae4d..609c666505 100644 --- a/src/libslic3r/CMakeLists.txt +++ b/src/libslic3r/CMakeLists.txt @@ -158,6 +158,8 @@ set(SLIC3R_SOURCES GCode/ExtrusionProcessor.hpp GCode/FindReplace.cpp GCode/FindReplace.hpp + GCode/LabelObjects.cpp + GCode/LabelObjects.hpp GCode/GCodeWriter.cpp GCode/GCodeWriter.hpp GCode/PostProcessor.cpp diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index 5afe6b34c4..dd62029769 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -28,6 +28,7 @@ #include "Exception.hpp" #include "ExtrusionEntity.hpp" #include "Geometry/ConvexHull.hpp" +#include "GCode/LabelObjects.hpp" #include "GCode/PrintExtents.hpp" #include "GCode/Thumbnails.hpp" #include "GCode/WipeTower.hpp" @@ -102,7 +103,6 @@ namespace Slic3r { gcode += '\n'; } - // Return true if tch_prefix is found in custom_gcode static bool custom_gcode_changes_tool(const std::string& custom_gcode, const std::string& tch_prefix, unsigned next_extruder) { @@ -1144,6 +1144,10 @@ void GCodeGenerator::_do_export(Print& print, GCodeOutputStream &file, Thumbnail // Emit machine envelope limits for the Marlin firmware. this->print_machine_envelope(file, print); + // Label all objects so printer knows about them since the start. + m_label_objects.init(print); + file.write(m_label_objects.all_objects_header()); + // Update output variables after the extruders were initialized. m_placeholder_parser_integration.init(m_writer); // Let the start-up script prime the 1st printing tool. @@ -2336,9 +2340,8 @@ void GCodeGenerator::process_layer_single_object( const bool print_wipe_extrusions) { bool first = true; - int object_id = 0; // Delay layer initialization as many layers may not print with all extruders. - auto init_layer_delayed = [this, &print_instance, &layer_to_print, &first, &object_id, &gcode]() { + auto init_layer_delayed = [this, &print_instance, &layer_to_print, &first, &gcode]() { if (first) { first = false; const PrintObject &print_object = print_instance.print_object; @@ -2354,14 +2357,7 @@ void GCodeGenerator::process_layer_single_object( m_avoid_crossing_perimeters.use_external_mp_once(); m_last_obj_copy = this_object_copy; this->set_origin(unscale(offset)); - if (this->config().gcode_label_objects) { - for (const PrintObject *po : print_object.print()->objects()) - if (po == &print_object) - break; - else - ++ object_id; - gcode += std::string("; printing object ") + print_object.model_object()->name + " id:" + std::to_string(object_id) + " copy " + std::to_string(print_instance.instance_id) + "\n"; - } + gcode += m_label_objects.start_object(print_instance.print_object.instances()[print_instance.instance_id], GCode::LabelObjects::IncludeName::No); } }; @@ -2538,8 +2534,8 @@ void GCodeGenerator::process_layer_single_object( } } } - if (! first && this->config().gcode_label_objects) - gcode += std::string("; stop printing object ") + print_object.model_object()->name + " id:" + std::to_string(object_id) + " copy " + std::to_string(print_instance.instance_id) + "\n"; + if (! first) + gcode += m_label_objects.stop_object(print_instance.print_object.instances()[print_instance.instance_id]); } void GCodeGenerator::apply_print_config(const PrintConfig &print_config) diff --git a/src/libslic3r/GCode.hpp b/src/libslic3r/GCode.hpp index ae1cc333b1..d25113b0d2 100644 --- a/src/libslic3r/GCode.hpp +++ b/src/libslic3r/GCode.hpp @@ -28,6 +28,7 @@ #include "GCode/CoolingBuffer.hpp" #include "GCode/FindReplace.hpp" #include "GCode/GCodeWriter.hpp" +#include "GCode/LabelObjects.hpp" #include "GCode/PressureEqualizer.hpp" #include "GCode/RetractWhenCrossingPerimeters.hpp" #include "GCode/SmoothPath.hpp" @@ -353,6 +354,7 @@ private: OozePrevention m_ooze_prevention; GCode::Wipe m_wipe; + GCode::LabelObjects m_label_objects; AvoidCrossingPerimeters m_avoid_crossing_perimeters; JPSPathFinder m_avoid_crossing_curled_overhangs; RetractWhenCrossingPerimeters m_retract_when_crossing_perimeters; diff --git a/src/libslic3r/GCode/LabelObjects.cpp b/src/libslic3r/GCode/LabelObjects.cpp new file mode 100644 index 0000000000..4c32122ad2 --- /dev/null +++ b/src/libslic3r/GCode/LabelObjects.cpp @@ -0,0 +1,190 @@ +#include "LabelObjects.hpp" + +#include "ClipperUtils.hpp" +#include "Model.hpp" +#include "Print.hpp" +#include "TriangleMeshSlicer.hpp" + + +namespace Slic3r::GCode { + + +namespace { + +Polygon instance_outline(const PrintInstance* pi) +{ + ExPolygons outline; + const ModelObject* mo = pi->model_instance->get_object(); + const ModelInstance* mi = pi->model_instance; + for (const ModelVolume *v : mo->volumes) { + Polygons vol_outline; + vol_outline = project_mesh(v->mesh().its, + mi->get_matrix() * v->get_matrix(), + [] {}); + switch (v->type()) { + case ModelVolumeType::MODEL_PART: outline = union_ex(outline, vol_outline); break; + case ModelVolumeType::NEGATIVE_VOLUME: outline = diff_ex(outline, vol_outline); break; + default:; + } + } + + // The projection may contain multiple polygons, which is not supported by Klipper. + // When that happens, calculate and use a 2d convex hull instead. + if (outline.size() == 1u) + return outline.front().contour; + else + return pi->model_instance->get_object()->convex_hull_2d(pi->model_instance->get_matrix()); +} + +}; // anonymous namespace + + +void LabelObjects::init(const Print& print) +{ + m_label_objects_style = print.config().gcode_label_objects; + m_flavor = print.config().gcode_flavor; + + if (m_label_objects_style == LabelObjectsStyle::Disabled) + return; + + std::map> model_object_to_print_instances; + + // Iterate over all PrintObjects and their PrintInstances, collect PrintInstances which + // belong to the same ModelObject. + for (const PrintObject* po : print.objects()) + for (const PrintInstance& pi : po->instances()) + model_object_to_print_instances[pi.model_instance->get_object()].emplace_back(&pi); + + // Now go through the map, assign a unique_id to each of the PrintInstances and get the indices of the + // respective ModelObject and ModelInstance so we can use them in the tags. This will maintain + // indices even in case that some instances are rotated (those end up in different PrintObjects) + // or when some are out of bed (these ModelInstances have no corresponding PrintInstances). + int unique_id = 0; + for (const auto& [model_object, print_instances] : model_object_to_print_instances) { + const ModelObjectPtrs& model_objects = model_object->get_model()->objects; + int object_id = int(std::find(model_objects.begin(), model_objects.end(), model_object) - model_objects.begin()); + for (const PrintInstance* const pi : print_instances) { + bool object_has_more_instances = print_instances.size() > 1u; + int instance_id = int(std::find(model_object->instances.begin(), model_object->instances.end(), pi->model_instance) - model_object->instances.begin()); + + // Now compose the name of the object and define whether indexing is 0 or 1-based. + std::string name = model_object->name; + if (m_label_objects_style == LabelObjectsStyle::Octoprint) { + // use zero-based indexing for objects and instances, as we always have done + name += " id:" + std::to_string(object_id) + " copy " + std::to_string(instance_id); + } + else if (m_label_objects_style == LabelObjectsStyle::Firmware) { + // use one-based indexing for objects and instances so indices match what we see in PrusaSlicer. + ++object_id; + ++instance_id; + + if (object_has_more_instances) + name += " (Instance " + std::to_string(instance_id) + ")"; + if (m_flavor == gcfKlipper) { + const std::string banned = "-. \r\n\v\t\f"; + std::replace_if(name.begin(), name.end(), [&banned](char c) { return banned.find(c) != std::string::npos; }, '_'); + } + } + + m_label_data.emplace(pi, LabelData{name, unique_id}); + ++unique_id; + } + } +} + + + +std::string LabelObjects::all_objects_header() const +{ + if (m_label_objects_style == LabelObjectsStyle::Disabled) + return std::string(); + + std::string out; + + // Let's sort the values according to unique_id so they are in the same order in which they were added. + std::vector> label_data_sorted; + for (const auto& pi_and_label : m_label_data) + label_data_sorted.emplace_back(pi_and_label); + std::sort(label_data_sorted.begin(), label_data_sorted.end(), [](const auto& ld1, const auto& ld2) { return ld1.second.unique_id < ld2.second.unique_id; }); + + out += "\n"; + for (const auto& [print_instance, label] : label_data_sorted) { + if (m_flavor == gcfKlipper) { + char buffer[64]; + out += "EXCLUDE_OBJECT_DEFINE NAME=" + label.name; + Polygon outline = instance_outline(print_instance); + assert(! outline.empty()); + outline.douglas_peucker(50000.f); + Point center = outline.centroid(); + std::snprintf(buffer, sizeof(buffer) - 1, " CENTER=%.3f,%.3f", unscale(center[0]), unscale(center[1])); + out += buffer + std::string(" POLYGON=["); + for (const Point& point : outline) { + std::snprintf(buffer, sizeof(buffer) - 1, "[%.3f,%.3f],", unscale(point[0]), unscale(point[1])); + out += buffer; + } + out.pop_back(); + out += "]\n"; + } else { + out += start_object(*print_instance, IncludeName::Yes); + out += stop_object(*print_instance); + } + } + out += "\n"; + return out; +} + + + +std::string LabelObjects::start_object(const PrintInstance& print_instance, IncludeName include_name) const +{ + if (m_label_objects_style == LabelObjectsStyle::Disabled) + return std::string(); + + const LabelData& label = m_label_data.at(&print_instance); + + std::string out; + if (m_label_objects_style == LabelObjectsStyle::Octoprint) + out += std::string("; printing object ") + label.name + "\n"; + else if (m_label_objects_style == LabelObjectsStyle::Firmware) { + if (m_flavor == GCodeFlavor::gcfMarlinFirmware || m_flavor == GCodeFlavor::gcfMarlinLegacy || m_flavor == GCodeFlavor::gcfRepRapFirmware) { + out += std::string("M486 S") + std::to_string(label.unique_id) + "\n"; + if (include_name == IncludeName::Yes) { + out += std::string("M486 A"); + out += (m_flavor == GCodeFlavor::gcfRepRapFirmware ? (std::string("\"") + label.name + "\"") : label.name) + "\n"; + } + } else if (m_flavor == gcfKlipper) + out += "EXCLUDE_OBJECT_START NAME=" + label.name + "\n"; + else { + // Not supported by / implemented for the other firmware flavors. + } + } + return out; +} + + + +std::string LabelObjects::stop_object(const PrintInstance& print_instance) const +{ + if (m_label_objects_style == LabelObjectsStyle::Disabled) + return std::string(); + + const LabelData& label = m_label_data.at(&print_instance); + + std::string out; + if (m_label_objects_style == LabelObjectsStyle::Octoprint) + out += std::string("; stop printing object ") + label.name + "\n"; + else if (m_label_objects_style == LabelObjectsStyle::Firmware) { + if (m_flavor == GCodeFlavor::gcfMarlinFirmware || m_flavor == GCodeFlavor::gcfMarlinLegacy || m_flavor == GCodeFlavor::gcfRepRapFirmware) + out += std::string("M486 S-1\n"); + else if (m_flavor ==gcfKlipper) + out += "EXCLUDE_OBJECT_END NAME=" + label.name + "\n"; + else { + // Not supported by / implemented for the other firmware flavors. + } + } + return out; +} + + + +} // namespace Slic3r::GCode diff --git a/src/libslic3r/GCode/LabelObjects.hpp b/src/libslic3r/GCode/LabelObjects.hpp new file mode 100644 index 0000000000..78add73009 --- /dev/null +++ b/src/libslic3r/GCode/LabelObjects.hpp @@ -0,0 +1,42 @@ +#ifndef slic3r_GCode_LabelObjects_hpp_ +#define slic3r_GCode_LabelObjects_hpp_ + +namespace Slic3r { + +enum GCodeFlavor : unsigned char; +enum class LabelObjectsStyle; +struct PrintInstance; +class Print; + + +namespace GCode { + + +class LabelObjects { +public: + enum class IncludeName { + No, + Yes + }; + void init(const Print& print); + std::string all_objects_header() const; + std::string start_object(const PrintInstance& print_instance, IncludeName include_name) const; + std::string stop_object(const PrintInstance& print_instance) const; + +private: + struct LabelData { + std::string name; + int unique_id; + }; + + LabelObjectsStyle m_label_objects_style; + GCodeFlavor m_flavor; + std::unordered_map m_label_data; + +}; + + +} // namespace GCode +} // namespace Slic3r + +#endif // slic3r_GCode_LabelObjects_hpp_ diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index 6d52484e9c..38767ea62e 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -229,6 +229,13 @@ static const t_config_enum_values s_keys_map_DraftShield = { }; CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(DraftShield) +static const t_config_enum_values s_keys_map_LabelObjectsStyle = { + { "disabled", int(LabelObjectsStyle::Disabled) }, + { "octoprint", int(LabelObjectsStyle::Octoprint) }, + { "firmware", int(LabelObjectsStyle::Firmware) } +}; +CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(LabelObjectsStyle) + static const t_config_enum_values s_keys_map_GCodeThumbnailsFormat = { { "PNG", int(GCodeThumbnailsFormat::PNG) }, { "JPG", int(GCodeThumbnailsFormat::JPG) }, @@ -1493,13 +1500,20 @@ void PrintConfigDef::init_fff_params() def->mode = comExpert; def->set_default_value(new ConfigOptionEnum(gcfRepRapSprinter)); - def = this->add("gcode_label_objects", coBool); + def = this->add("gcode_label_objects", coEnum); def->label = L("Label objects"); - def->tooltip = L("Enable this to add comments into the G-Code labeling print moves with what object they belong to," - " which is useful for the Octoprint CancelObject plugin. This settings is NOT compatible with " - "Single Extruder Multi Material setup and Wipe into Object / Wipe into Infill."); + def->tooltip = L("Selects whether labels should be exported at object boundaries and in what format.\n" + " OctoPrint = comments to be consumed by OctoPrint CancelObject plugin.\n" + " Firmware = firmware specific G-code (it will be chosen based on firmware flavor and it can end up to be empty).\n\n" + "This settings is NOT compatible with Single Extruder Multi Material setup and Wipe into Object / Wipe into Infill."); + + def->set_enum({ + { "disabled", L("Disabled") }, + { "octoprint", L("OctoPrint comments") }, + { "firmware", L("Firmware-specific") } + }); def->mode = comAdvanced; - def->set_default_value(new ConfigOptionBool(0)); + def->set_default_value(new ConfigOptionEnum(LabelObjectsStyle::Disabled)); def = this->add("gcode_substitutions", coStrings); def->label = L("G-code substitutions"); @@ -4335,6 +4349,10 @@ void PrintConfigDef::handle_legacy(t_config_option_key &opt_key, std::string &va } else if (opt_key == "draft_shield" && (value == "1" || value == "0")) { // draft_shield used to be a bool, it was turned into an enum in PrusaSlicer 2.4.0. value = value == "1" ? "enabled" : "disabled"; + } else if (opt_key == "gcode_label_objects" && (value == "1" || value == "0")) { + // gcode_label_objects used to be a bool (the behavior was nothing or "octoprint"), it is + // and enum since PrusaSlicer 2.6.2. + value = value == "1" ? "octoprint" : "disabled"; } else if (opt_key == "octoprint_host") { opt_key = "print_host"; } else if (opt_key == "octoprint_cafile") { diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index 390f91daf6..63ffcde0e3 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -147,6 +147,10 @@ enum DraftShield { dsDisabled, dsLimited, dsEnabled }; +enum class LabelObjectsStyle { + Disabled, Octoprint, Firmware +}; + enum class PerimeterGeneratorType { // Classic perimeter generator using Clipper offsets with constant extrusion width. @@ -183,6 +187,7 @@ CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(SLAPillarConnectionMode) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(SLASupportTreeType) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(BrimType) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(DraftShield) +CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(LabelObjectsStyle) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(GCodeThumbnailsFormat) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(ForwardCompatibilitySubstitutionRule) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(PerimeterGeneratorType) @@ -729,7 +734,7 @@ PRINT_CONFIG_CLASS_DEFINE( ((ConfigOptionFloats, filament_multitool_ramming_flow)) ((ConfigOptionBool, gcode_comments)) ((ConfigOptionEnum, gcode_flavor)) - ((ConfigOptionBool, gcode_label_objects)) + ((ConfigOptionEnum, gcode_label_objects)) // Triples of strings: "search pattern", "replace with pattern", "attribs" // where "attribs" are one of: // r - regular expression