diff --git a/resources/profiles/PrusaResearch.idx b/resources/profiles/PrusaResearch.idx index cf33c536b1..b68100ac8e 100644 --- a/resources/profiles/PrusaResearch.idx +++ b/resources/profiles/PrusaResearch.idx @@ -1,4 +1,5 @@ min_slic3r_version = 2.6.2-alpha0 +1.11.0-alpha4 Updated compatible printer conditions for specific filament profiles. 1.11.0-alpha3 Added new print profiles for Prusa MINI Input Shaper (Alpha). Updated MK4 IS profiles. 1.11.0-alpha2 Added MK3.9 and Prusa MINI Input Shaper (alpha). Enabled binary g-code, arc fitting and QOI/PNG for MINI and MINI IS. 1.11.0-alpha1 Updated ramming parameters. Updated start-gcode for XL Multi-Tool. Updated output filename format. diff --git a/resources/profiles/PrusaResearch.ini b/resources/profiles/PrusaResearch.ini index 9c908fb88b..b208e92ea5 100644 --- a/resources/profiles/PrusaResearch.ini +++ b/resources/profiles/PrusaResearch.ini @@ -5,7 +5,7 @@ name = Prusa Research # Configuration version of this file. Config file will only be installed, if the config_version differs. # This means, the server may force the PrusaSlicer configuration to be downgraded. -config_version = 1.11.0-alpha3 +config_version = 1.11.0-alpha4 # Where to get the updates from? config_update_url = https://files.prusa3d.com/wp-content/uploads/repository/PrusaSlicer-settings-master/live/PrusaResearch/ changelog_url = https://files.prusa3d.com/?latest=slicer-profiles&lng=%1% @@ -6399,18 +6399,18 @@ compatible_printers_condition = nozzle_diameter[0]!=0.8 and printer_model!="MK2S [filament:Generic PETG @PG] inherits = Generic PETG; *PETPG* -compatible_printers_condition = printer_notes=~/.*MK4.*/ and printer_model!="MK4IS" and nozzle_diameter[0]!=0.8 and nozzle_diameter[0]!=0.6 +compatible_printers_condition = printer_notes=~/.*MK4.*/ and printer_notes!~/.*MK4IS.*/ and nozzle_diameter[0]!=0.8 and nozzle_diameter[0]!=0.6 [filament:Generic PETG @PG 0.6] inherits = Generic PETG; *PET06PG* filament_max_volumetric_speed = 17 -compatible_printers_condition = printer_notes=~/.*MK4.*/ and printer_model!="MK4IS" and nozzle_diameter[0]==0.6 +compatible_printers_condition = printer_notes=~/.*MK4.*/ and printer_notes!~/.*MK4IS.*/ and nozzle_diameter[0]==0.6 [filament:Generic PETG @PG 0.8] inherits = Generic PETG; *PET08PG* first_layer_temperature = 240 temperature = 250 -compatible_printers_condition = printer_notes=~/.*MK4.*/ and printer_model!="MK4IS" and nozzle_diameter[0]==0.8 +compatible_printers_condition = printer_notes=~/.*MK4.*/ and printer_notes!~/.*MK4IS.*/ and nozzle_diameter[0]==0.8 [filament:Generic PETG @XL] inherits = Generic PETG @PG; *PETXL* @@ -7563,18 +7563,18 @@ compatible_printers_condition = nozzle_diameter[0]!=0.8 and printer_notes!~/.*PG [filament:Generic PLA @PG] inherits = Generic PLA; *PLAPG* -compatible_printers_condition = printer_notes=~/.*MK4.*/ and printer_model!="MK4IS" and nozzle_diameter[0]!=0.8 and nozzle_diameter[0]!=0.6 +compatible_printers_condition = printer_notes=~/.*MK4.*/ and printer_notes!~/.*MK4IS.*/ and nozzle_diameter[0]!=0.8 and nozzle_diameter[0]!=0.6 [filament:Generic PLA @PG 0.6] inherits = Generic PLA; *PLA06PG* filament_max_volumetric_speed = 15 -compatible_printers_condition = printer_notes=~/.*MK4.*/ and printer_model!="MK4IS" and nozzle_diameter[0]==0.6 +compatible_printers_condition = printer_notes=~/.*MK4.*/ and printer_notes!~/.*MK4IS.*/ and nozzle_diameter[0]==0.6 [filament:Generic PLA @PG 0.8] inherits = Generic PLA; *PLA08PG* first_layer_temperature = 220 temperature = 220 -compatible_printers_condition = printer_notes=~/.*MK4.*/ and printer_model!="MK4IS" and nozzle_diameter[0]==0.8 +compatible_printers_condition = printer_notes=~/.*MK4.*/ and printer_notes!~/.*MK4IS.*/ and nozzle_diameter[0]==0.8 [filament:Generic PLA @XL] inherits = Generic PLA @PG; *PLAXL* @@ -10376,16 +10376,16 @@ compatible_printers_condition = nozzle_diameter[0]!=0.6 and nozzle_diameter[0]!= [filament:Prusa PETG @PG] inherits = Prusa PETG; *PETPG* -compatible_printers_condition = printer_notes=~/.*MK4.*/ and printer_model!="MK4IS" and nozzle_diameter[0]!=0.8 and nozzle_diameter[0]!=0.6 +compatible_printers_condition = printer_notes=~/.*MK4.*/ and printer_notes!~/.*MK4IS.*/ and nozzle_diameter[0]!=0.8 and nozzle_diameter[0]!=0.6 [filament:Prusa PETG @PG 0.6] inherits = Prusa PETG; *PET06PG* -compatible_printers_condition = printer_notes=~/.*MK4.*/ and printer_model!="MK4IS" and nozzle_diameter[0]==0.6 +compatible_printers_condition = printer_notes=~/.*MK4.*/ and printer_notes!~/.*MK4IS.*/ and nozzle_diameter[0]==0.6 [filament:Prusa PETG @PG 0.8] inherits = Prusa PETG; *PET08PG* temperature = 250 -compatible_printers_condition = printer_notes=~/.*MK4.*/ and printer_model!="MK4IS" and nozzle_diameter[0]==0.8 +compatible_printers_condition = printer_notes=~/.*MK4.*/ and printer_notes!~/.*MK4IS.*/ and nozzle_diameter[0]==0.8 [filament:Prusa PETG @XL] inherits = Prusa PETG @PG; *PETXL* @@ -10469,17 +10469,17 @@ compatible_printers_condition = nozzle_diameter[0]!=0.8 and nozzle_diameter[0]!= [filament:Prusament PETG @PG] inherits = Prusament PETG; *PETPG* -compatible_printers_condition = printer_notes=~/.*MK4.*/ and printer_model!="MK4IS" and nozzle_diameter[0]!=0.8 and nozzle_diameter[0]!=0.6 +compatible_printers_condition = printer_notes=~/.*MK4.*/ and printer_notes!~/.*MK4IS.*/ and nozzle_diameter[0]!=0.8 and nozzle_diameter[0]!=0.6 [filament:Prusament PETG @PG 0.6] inherits = Prusament PETG; *PET06PG* -compatible_printers_condition = printer_notes=~/.*MK4.*/ and printer_model!="MK4IS" and nozzle_diameter[0]==0.6 +compatible_printers_condition = printer_notes=~/.*MK4.*/ and printer_notes!~/.*MK4IS.*/ and nozzle_diameter[0]==0.6 [filament:Prusament PETG @PG 0.8] inherits = Prusament PETG; *PET08PG* first_layer_temperature = 250 temperature = 260 -compatible_printers_condition = printer_notes=~/.*MK4.*/ and printer_model!="MK4IS" and nozzle_diameter[0]==0.8 +compatible_printers_condition = printer_notes=~/.*MK4.*/ and printer_notes!~/.*MK4IS.*/ and nozzle_diameter[0]==0.8 [filament:Prusament PETG @XL] inherits = Prusament PETG @PG; *PETXL* @@ -10790,16 +10790,16 @@ compatible_printers_condition = nozzle_diameter[0]!=0.8 and printer_notes!~/.*PG [filament:Prusa PLA @PG] inherits = Prusa PLA; *PLAPG* -compatible_printers_condition = printer_notes=~/.*MK4.*/ and printer_model!="MK4IS" and nozzle_diameter[0]!=0.8 and nozzle_diameter[0]!=0.6 +compatible_printers_condition = printer_notes=~/.*MK4.*/ and printer_notes!~/.*MK4IS.*/ and nozzle_diameter[0]!=0.8 and nozzle_diameter[0]!=0.6 [filament:Prusa PLA @PG 0.6] inherits = Prusa PLA; *PLA06PG* filament_max_volumetric_speed = 15.5 -compatible_printers_condition = printer_notes=~/.*MK4.*/ and printer_model!="MK4IS" and nozzle_diameter[0]==0.6 +compatible_printers_condition = printer_notes=~/.*MK4.*/ and printer_notes!~/.*MK4IS.*/ and nozzle_diameter[0]==0.6 [filament:Prusa PLA @PG 0.8] inherits = Prusa PLA; *PLA08PG* -compatible_printers_condition = printer_notes=~/.*MK4.*/ and printer_model!="MK4IS" and nozzle_diameter[0]==0.8 +compatible_printers_condition = printer_notes=~/.*MK4.*/ and printer_notes!~/.*MK4IS.*/ and nozzle_diameter[0]==0.8 [filament:Prusa PLA @XL] inherits = Prusa PLA @PG; *PLAXL* @@ -12247,12 +12247,12 @@ compatible_printers_condition = nozzle_diameter[0]!=0.8 and printer_notes!~/.*PG [filament:Prusament PLA @PG] inherits = Prusament PLA; *PLAPG* -compatible_printers_condition = printer_notes=~/.*MK4.*/ and printer_model!="MK4IS" and nozzle_diameter[0]!=0.8 and nozzle_diameter[0]!=0.6 +compatible_printers_condition = printer_notes=~/.*MK4.*/ and printer_notes!~/.*MK4IS.*/ and nozzle_diameter[0]!=0.8 and nozzle_diameter[0]!=0.6 [filament:Prusament PLA @PG 0.6] inherits = Prusament PLA; *PLA06PG* filament_max_volumetric_speed = 16 -compatible_printers_condition = printer_notes=~/.*MK4.*/ and printer_model!="MK4IS" and nozzle_diameter[0]==0.6 +compatible_printers_condition = printer_notes=~/.*MK4.*/ and printer_notes!~/.*MK4IS.*/ and nozzle_diameter[0]==0.6 [filament:Prusament PLA @PG 0.8] inherits = Prusament PLA; *PLA08PG* diff --git a/src/libslic3r/CMakeLists.txt b/src/libslic3r/CMakeLists.txt index c4f2a5e64c..0f3c3b5da7 100644 --- a/src/libslic3r/CMakeLists.txt +++ b/src/libslic3r/CMakeLists.txt @@ -160,6 +160,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/ExtrusionProcessor.hpp b/src/libslic3r/GCode/ExtrusionProcessor.hpp index 3e2bbf080d..774413bd53 100644 --- a/src/libslic3r/GCode/ExtrusionProcessor.hpp +++ b/src/libslic3r/GCode/ExtrusionProcessor.hpp @@ -49,6 +49,34 @@ std::vector estimate_points_properties(const POINTS float flow_width, float max_line_length = -1.0f) { + bool looped = input_points.front() == input_points.back(); + std::function get_prev_index = [](int idx, size_t count) { + if (idx > 0) { + return idx - 1; + } else + return idx; + }; + if (looped) { + get_prev_index = [](int idx, size_t count) { + if (idx == 0) + idx = count; + return --idx; + }; + }; + std::function get_next_index = [](int idx, size_t size) { + if (idx + 1 < size) { + return idx + 1; + } else + return idx; + }; + if (looped) { + get_next_index = [](int idx, size_t count) { + if (++idx == count) + idx = 0; + return idx; + }; + }; + using P = typename POINTS::value_type; using AABBScalar = typename AABBTreeLines::LinesDistancer::Scalar; @@ -156,69 +184,65 @@ std::vector estimate_points_properties(const POINTS points = std::move(new_points); } - std::vector angles_for_curvature(points.size()); + float accumulated_distance = 0; std::vector distances_for_curvature(points.size()); + for (int point_idx = 0; point_idx < int(points.size()); ++point_idx) { + const ExtendedPoint &a = points[point_idx]; + const ExtendedPoint &b = points[get_prev_index(point_idx, points.size())]; - for (size_t point_idx = 0; point_idx < points.size(); ++point_idx) { - ExtendedPoint &a = points[point_idx]; - size_t prev = prev_idx_modulo(point_idx, points.size()); - size_t next = next_idx_modulo(point_idx, points.size()); - - int iter_limit = points.size(); - while ((a.position - points[prev].position).squaredNorm() < 1 && iter_limit > 0) { - prev = prev_idx_modulo(prev, points.size()); - iter_limit--; - } - - while ((a.position - points[next].position).squaredNorm() < 1 && iter_limit > 0) { - next = next_idx_modulo(next, points.size()); - iter_limit--; - } - - distances_for_curvature[point_idx] = (points[prev].position - a.position).norm(); - float alfa = angle(a.position - points[prev].position, points[next].position - a.position); - angles_for_curvature[point_idx] = alfa; + distances_for_curvature[point_idx] = (b.position - a.position).norm(); + accumulated_distance += distances_for_curvature[point_idx]; } - if (std::accumulate(distances_for_curvature.begin(), distances_for_curvature.end(), 0) > EPSILON) + if (accumulated_distance > EPSILON) for (float window_size : {3.0f, 9.0f, 16.0f}) { - size_t tail_point = 0; - float tail_window_acc = 0; - float tail_angle_acc = 0; + for (int point_idx = 0; point_idx < int(points.size()); ++point_idx) { + ExtendedPoint ¤t = points[point_idx]; - size_t head_point = 0; - float head_window_acc = 0; - float head_angle_acc = 0; - - for (size_t point_idx = 0; point_idx < points.size(); ++point_idx) { - if (point_idx == 0) { - while (tail_window_acc < window_size * 0.5) { - tail_window_acc += distances_for_curvature[tail_point]; - tail_angle_acc += angles_for_curvature[tail_point]; - tail_point = prev_idx_modulo(tail_point, points.size()); + Vec2d back_position = current.position; + { + size_t back_point_index = point_idx; + float dist_backwards = 0; + while (dist_backwards < window_size * 0.5 && back_point_index != get_prev_index(back_point_index, points.size())) { + float line_dist = distances_for_curvature[get_prev_index(back_point_index, points.size())]; + if (dist_backwards + line_dist > window_size * 0.5) { + back_position = points[back_point_index].position + + (window_size * 0.5 - dist_backwards) * + (points[get_prev_index(back_point_index, points.size())].position - + points[back_point_index].position) + .normalized(); + dist_backwards += window_size * 0.5 - dist_backwards + EPSILON; + } else { + dist_backwards += line_dist; + back_point_index = get_prev_index(back_point_index, points.size()); + } } } - while (tail_window_acc - distances_for_curvature[next_idx_modulo(tail_point, points.size())] > window_size * 0.5) { - tail_point = next_idx_modulo(tail_point, points.size()); - tail_window_acc -= distances_for_curvature[tail_point]; - tail_angle_acc -= angles_for_curvature[tail_point]; + + Vec2d front_position = current.position; + { + size_t front_point_index = point_idx; + float dist_forwards = 0; + while (dist_forwards < window_size * 0.5 && front_point_index != get_next_index(front_point_index, points.size())) { + float line_dist = distances_for_curvature[front_point_index]; + if (dist_forwards + line_dist > window_size * 0.5) { + front_position = points[front_point_index].position + + (window_size * 0.5 - dist_forwards) * + (points[get_next_index(front_point_index, points.size())].position - + points[front_point_index].position) + .normalized(); + dist_forwards += window_size * 0.5 - dist_forwards + EPSILON; + } else { + dist_forwards += line_dist; + front_point_index = get_next_index(front_point_index, points.size()); + } + } } - while (head_window_acc < window_size * 0.5) { - head_point = next_idx_modulo(head_point, points.size()); - head_window_acc += distances_for_curvature[head_point]; - head_angle_acc += angles_for_curvature[head_point]; + float new_curvature = angle(current.position - back_position, front_position - current.position) / window_size; + if (abs(current.curvature) < abs(new_curvature)) { + current.curvature = new_curvature; } - - float curvature = (tail_angle_acc + head_angle_acc) / window_size; - if (std::abs(curvature) > std::abs(points[point_idx].curvature)) { - points[point_idx].curvature = curvature; - } - - tail_window_acc += distances_for_curvature[point_idx]; - tail_angle_acc += angles_for_curvature[point_idx]; - head_window_acc -= distances_for_curvature[point_idx]; - head_angle_acc -= angles_for_curvature[point_idx]; } } diff --git a/src/libslic3r/GCode/GCodeProcessor.cpp b/src/libslic3r/GCode/GCodeProcessor.cpp index 6e7e8b8944..9504860944 100644 --- a/src/libslic3r/GCode/GCodeProcessor.cpp +++ b/src/libslic3r/GCode/GCodeProcessor.cpp @@ -595,8 +595,12 @@ void GCodeProcessor::apply_config(const PrintConfig& config) for (size_t i = 0; i < extruders_count; ++ i) { m_extruder_offsets[i] = to_3d(config.extruder_offset.get_at(i).cast().eval(), 0.f); m_extruder_colors[i] = static_cast(i); - m_extruder_temps_config[i] = static_cast(config.temperature.get_at(i)); m_extruder_temps_first_layer_config[i] = static_cast(config.first_layer_temperature.get_at(i)); + m_extruder_temps_config[i] = static_cast(config.temperature.get_at(i)); + if (m_extruder_temps_config[i] == 0) { + // This means the value should be ignored and first layer temp should be used. + m_extruder_temps_config[i] = m_extruder_temps_first_layer_config[i]; + } m_result.filament_diameters[i] = static_cast(config.filament_diameter.get_at(i)); m_result.filament_densities[i] = static_cast(config.filament_density.get_at(i)); m_result.filament_cost[i] = static_cast(config.filament_cost.get_at(i)); 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/Geometry/Circle.cpp b/src/libslic3r/Geometry/Circle.cpp index cdaf72d6a8..24d408c6a2 100644 --- a/src/libslic3r/Geometry/Circle.cpp +++ b/src/libslic3r/Geometry/Circle.cpp @@ -146,4 +146,83 @@ Circled circle_ransac(const Vec2ds& input, size_t iterations, double* min_error) return circle_best; } +template +Circled circle_least_squares_by_solver(const Vec2ds &input, Solver solver) +{ + Circled out; + if (input.size() < 3) { + out = Circled::make_invalid(); + } else { + Eigen::Matrix A(input.size(), 3); + Eigen::VectorXd b(input.size()); + for (size_t r = 0; r < input.size(); ++ r) { + const Vec2d &p = input[r]; + A.row(r) = Vec3d(2. * p.x(), 2. * p.y(), - 1.); + b(r) = p.squaredNorm(); + } + auto result = solver(A, b); + out.center = result.template head<2>(); + double r2 = out.center.squaredNorm() - result(2); + if (r2 <= EPSILON) + out.make_invalid(); + else + out.radius = sqrt(r2); + } + + return out; +} + +Circled circle_least_squares_svd(const Vec2ds &input) +{ + return circle_least_squares_by_solver(input, + [](const Eigen::Matrix &A, const Eigen::VectorXd &b) + { return A.bdcSvd(Eigen::ComputeThinU | Eigen::ComputeThinV).solve(b).eval(); }); +} + +Circled circle_least_squares_qr(const Vec2ds &input) +{ + return circle_least_squares_by_solver(input, + [](const Eigen::Matrix &A, const Eigen::VectorXd &b) + { return A.colPivHouseholderQr().solve(b).eval(); }); +} + +Circled circle_least_squares_normal(const Vec2ds &input) +{ + Circled out; + if (input.size() < 3) { + out = Circled::make_invalid(); + } else { + Eigen::Matrix A = Eigen::Matrix::Zero(); + Eigen::Matrix b = Eigen::Matrix::Zero(); + for (size_t i = 0; i < input.size(); ++ i) { + const Vec2d &p = input[i]; + // Calculate right hand side of a normal equation. + b += p.squaredNorm() * Vec3d(2. * p.x(), 2. * p.y(), -1.); + // Calculate normal matrix (correlation matrix). + // Diagonal: + A(0, 0) += 4. * p.x() * p.x(); + A(1, 1) += 4. * p.y() * p.y(); + A(2, 2) += 1.; + // Off diagonal elements: + const double a = 4. * p.x() * p.y(); + A(0, 1) += a; + A(1, 0) += a; + const double b = -2. * p.x(); + A(0, 2) += b; + A(2, 0) += b; + const double c = -2. * p.y(); + A(1, 2) += c; + A(2, 1) += c; + } + auto result = A.ldlt().solve(b).eval(); + out.center = result.head<2>(); + double r2 = out.center.squaredNorm() - result(2); + if (r2 <= EPSILON) + out.make_invalid(); + else + out.radius = sqrt(r2); + } + return out; +} + } } // namespace Slic3r::Geometry diff --git a/src/libslic3r/Geometry/Circle.hpp b/src/libslic3r/Geometry/Circle.hpp index f4bca148d7..ca32d32b50 100644 --- a/src/libslic3r/Geometry/Circle.hpp +++ b/src/libslic3r/Geometry/Circle.hpp @@ -141,6 +141,13 @@ Circled circle_taubin_newton(const Vec2ds& input, size_t cycles = 20); // Find circle using RANSAC randomized algorithm. Circled circle_ransac(const Vec2ds& input, size_t iterations = 20, double* min_error = nullptr); +// Least squares fitting with SVD. Most accurate, but slowest. +Circled circle_least_squares_svd(const Vec2ds &input); +// Least squares fitting with QR decomposition. Medium accuracy, medium speed. +Circled circle_least_squares_qr(const Vec2ds &input); +// Least squares fitting solving normal equations. Low accuracy, high speed. +Circled circle_least_squares_normal(const Vec2ds &input); + // Randomized algorithm by Emo Welzl, working with squared radii for efficiency. The returned circle radius is inflated by epsilon. template CircleSq smallest_enclosing_circle2_welzl(const Points &points, const typename Vector::Scalar epsilon) 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 diff --git a/src/libslic3r/SupportSpotsGenerator.cpp b/src/libslic3r/SupportSpotsGenerator.cpp index fd5bd7d5e8..eb3f1f07a1 100644 --- a/src/libslic3r/SupportSpotsGenerator.cpp +++ b/src/libslic3r/SupportSpotsGenerator.cpp @@ -166,7 +166,7 @@ struct SliceConnection this->second_moment_of_area_covariance_accumulator += other.second_moment_of_area_covariance_accumulator; } - void print_info(const std::string &tag) + void print_info(const std::string &tag) const { Vec3f centroid = centroid_accumulator / area; Vec2f variance = (second_moment_of_area_accumulator / area - centroid.head<2>().cwiseProduct(centroid.head<2>())); @@ -179,6 +179,27 @@ struct SliceConnection } }; +Integrals::Integrals (const Polygons& polygons) { + for (const Polygon &polygon : polygons) { + Vec2f p0 = unscaled(polygon.first_point()).cast(); + for (size_t i = 2; i < polygon.points.size(); i++) { + Vec2f p1 = unscaled(polygon.points[i - 1]).cast(); + Vec2f p2 = unscaled(polygon.points[i]).cast(); + + float sign = cross2(p1 - p0, p2 - p1) > 0 ? 1.0f : -1.0f; + + auto [area, first_moment_of_area, second_moment_area, + second_moment_of_area_covariance] = compute_moments_of_area_of_triangle(p0, p1, p2); + + this->area += sign * area; + this->x_i += sign * first_moment_of_area; + this->x_i_squared += sign * second_moment_area; + this->xy += sign * second_moment_of_area_covariance; + } + } +} + + SliceConnection estimate_slice_connection(size_t slice_idx, const Layer *layer) { SliceConnection connection; @@ -200,22 +221,11 @@ SliceConnection estimate_slice_connection(size_t slice_idx, const Layer *layer) Polygons overlap = intersection(ClipperUtils::clip_clipper_polygons_with_subject_bbox(slice_polys, below_bb), ClipperUtils::clip_clipper_polygons_with_subject_bbox(below_polys, slice_bb)); - for (const Polygon &poly : overlap) { - Vec2f p0 = unscaled(poly.first_point()).cast(); - for (size_t i = 2; i < poly.points.size(); i++) { - Vec2f p1 = unscaled(poly.points[i - 1]).cast(); - Vec2f p2 = unscaled(poly.points[i]).cast(); - - float sign = cross2(p1 - p0, p2 - p1) > 0 ? 1.0f : -1.0f; - - auto [area, first_moment_of_area, second_moment_area, - second_moment_of_area_covariance] = compute_moments_of_area_of_triangle(p0, p1, p2); - connection.area += sign * area; - connection.centroid_accumulator += sign * Vec3f(first_moment_of_area.x(), first_moment_of_area.y(), layer->print_z * area); - connection.second_moment_of_area_accumulator += sign * second_moment_area; - connection.second_moment_of_area_covariance_accumulator += sign * second_moment_of_area_covariance; - } - } + const Integrals integrals{overlap}; + connection.area += integrals.area; + connection.centroid_accumulator += Vec3f(integrals.x_i.x(), integrals.x_i.y(), layer->print_z * integrals.area); + connection.second_moment_of_area_accumulator += integrals.x_i_squared; + connection.second_moment_of_area_covariance_accumulator += integrals.xy; return connection; }; @@ -450,6 +460,48 @@ std::vector check_extrusion_entity_stability(const ExtrusionEntit } } +/** + * Calculates the second moment of area over an arbitrary polygon. + * + * Important note: The calculated moment is for an axis with origin at + * the polygon centroid! + * + * @param integrals Integrals over the polygon area. + * @param axis_direction Direction of the rotation axis going through centroid. + */ +float compute_second_moment( + const Integrals& integrals, + const Vec2f& axis_direction +) { + // Second moment of area for any axis intersecting coordinate system origin + // can be evaluated using the second moments of area calculated for the coordinate + // system axis and the moment product (int xy). + // The equation is derived appling known formulas for the moment of inertia + // to a plannar problem. One can reason about second moment + // of area by by setting density to 1 in the moment of inertia formulas. + const auto area = integrals.area; + const auto I_xx = integrals.x_i_squared.y(); + const auto I_yy = integrals.x_i_squared.x(); + const auto I_xy = -integrals.xy; + + const Vec2f centroid = integrals.x_i / area; + + Matrix2f moment_tensor{}; + moment_tensor << + I_xx, I_xy, + I_xy, I_yy; + + const float moment_at_0_0 = axis_direction.transpose() * moment_tensor * axis_direction; + + // Apply parallel axis theorem to move the moment to centroid + using line_alg::distance_to_infinite_squared; + + const Linef axis_at_0_0 = {{0, 0}, axis_direction.cast()}; + + const double distance = distance_to_infinite_squared(axis_at_0_0, centroid.cast()); + return moment_at_0_0 - area * distance; +} + class ObjectPart { public: @@ -482,43 +534,22 @@ public: this->sticking_second_moment_of_area_covariance_accumulator += sticking_area * position.x() * position.y(); } - float compute_directional_xy_variance(const Vec2f &line_dir, - const Vec3f ¢roid_accumulator, - const Vec2f &second_moment_of_area_accumulator, - const float &second_moment_of_area_covariance_accumulator, - const float &area) const - { - assert(area > 0); - Vec3f centroid = centroid_accumulator / area; - Vec2f variance = (second_moment_of_area_accumulator / area - centroid.head<2>().cwiseProduct(centroid.head<2>())); - float covariance = second_moment_of_area_covariance_accumulator / area - centroid.x() * centroid.y(); - // Var(aX+bY)=a^2*Var(X)+b^2*Var(Y)+2*a*b*Cov(X,Y) - float directional_xy_variance = line_dir.x() * line_dir.x() * variance.x() + line_dir.y() * line_dir.y() * variance.y() + - 2.0f * line_dir.x() * line_dir.y() * covariance; -#ifdef DETAILED_DEBUG_LOGS - BOOST_LOG_TRIVIAL(debug) << "centroid: " << centroid.x() << " " << centroid.y() << " " << centroid.z(); - BOOST_LOG_TRIVIAL(debug) << "variance: " << variance.x() << " " << variance.y(); - BOOST_LOG_TRIVIAL(debug) << "covariance: " << covariance; - BOOST_LOG_TRIVIAL(debug) << "directional_xy_variance: " << directional_xy_variance; -#endif - return directional_xy_variance; - } - float compute_elastic_section_modulus(const Vec2f &line_dir, - const Vec3f &extreme_point, - const Vec3f ¢roid_accumulator, - const Vec2f &second_moment_of_area_accumulator, - const float &second_moment_of_area_covariance_accumulator, - const float &area) const - { - float directional_xy_variance = compute_directional_xy_variance(line_dir, centroid_accumulator, second_moment_of_area_accumulator, - second_moment_of_area_covariance_accumulator, area); - if (directional_xy_variance < EPSILON) { return 0.0f; } - Vec3f centroid = centroid_accumulator / area; + float compute_elastic_section_modulus( + const Vec2f &line_dir, + const Vec3f &extreme_point, + const Integrals& integrals + ) const { + float second_moment_of_area = compute_second_moment(integrals, Vec2f{-line_dir.y(), line_dir.x()}); + + if (second_moment_of_area < EPSILON) { return 0.0f; } + + Vec2f centroid = integrals.x_i / integrals.area; float extreme_fiber_dist = line_alg::distance_to(Linef(centroid.head<2>().cast(), (centroid.head<2>() + Vec2f(line_dir.y(), -line_dir.x())).cast()), extreme_point.head<2>().cast()); - float elastic_section_modulus = area * directional_xy_variance / extreme_fiber_dist; + + float elastic_section_modulus = second_moment_of_area / extreme_fiber_dist; #ifdef DETAILED_DEBUG_LOGS BOOST_LOG_TRIVIAL(debug) << "extreme_fiber_dist: " << extreme_fiber_dist; @@ -534,6 +565,12 @@ public: float layer_z, const Params ¶ms) const { + // Note that exteme point is calculated for the current layer, while it should + // be computed for the first layer. The shape of the first layer however changes a lot, + // during support points additions (for organic supports it is not even clear how) + // and during merging. Using the current layer is heuristics and also small optimization, + // as the AABB tree for it is calculated anyways. This heuristic should usually be + // on the safe side. Vec2f line_dir = (extruded_line.b - extruded_line.a).normalized(); const Vec3f &mass_centroid = this->volume_centroid_accumulator / this->volume; float mass = this->volume * params.filament_density; @@ -548,19 +585,19 @@ public: { if (this->sticking_area < EPSILON) return {1.0f, SupportPointCause::UnstableFloatingPart}; + Integrals integrals; + integrals.area = this->sticking_area; + integrals.x_i = this->sticking_centroid_accumulator.head<2>(); + integrals.x_i_squared = this->sticking_second_moment_of_area_accumulator; + integrals.xy = this->sticking_second_moment_of_area_covariance_accumulator; + Vec3f bed_centroid = this->sticking_centroid_accumulator / this->sticking_area; - float bed_yield_torque = -compute_elastic_section_modulus(line_dir, extreme_point, this->sticking_centroid_accumulator, - this->sticking_second_moment_of_area_accumulator, - this->sticking_second_moment_of_area_covariance_accumulator, - this->sticking_area) * - params.get_bed_adhesion_yield_strength(); + float bed_yield_torque = -compute_elastic_section_modulus(line_dir, extreme_point, integrals) * params.get_bed_adhesion_yield_strength(); Vec2f bed_weight_arm = (mass_centroid.head<2>() - bed_centroid.head<2>()); float bed_weight_arm_len = bed_weight_arm.norm(); - float bed_weight_dir_xy_variance = compute_directional_xy_variance(bed_weight_arm, this->sticking_centroid_accumulator, - this->sticking_second_moment_of_area_accumulator, - this->sticking_second_moment_of_area_covariance_accumulator, - this->sticking_area); + + float bed_weight_dir_xy_variance = compute_second_moment(integrals, {-bed_weight_arm.y(), bed_weight_arm.x()}) / this->sticking_area; float bed_weight_sign = bed_weight_arm_len < 2.0f * sqrt(bed_weight_dir_xy_variance) ? -1.0f : 1.0f; float bed_weight_torque = bed_weight_sign * bed_weight_arm_len * weight; @@ -600,11 +637,14 @@ public: Vec3f conn_centroid = connection.centroid_accumulator / connection.area; if (layer_z - conn_centroid.z() < 3.0f) { return {-1.0f, SupportPointCause::WeakObjectPart}; } - float conn_yield_torque = compute_elastic_section_modulus(line_dir, extreme_point, connection.centroid_accumulator, - connection.second_moment_of_area_accumulator, - connection.second_moment_of_area_covariance_accumulator, - connection.area) * - params.material_yield_strength; + + Integrals integrals; + integrals.area = connection.area; + integrals.x_i = connection.centroid_accumulator.head<2>(); + integrals.x_i_squared = connection.second_moment_of_area_accumulator; + integrals.xy = connection.second_moment_of_area_covariance_accumulator; + + float conn_yield_torque = compute_elastic_section_modulus(line_dir, extreme_point, integrals) * params.material_yield_strength; float conn_weight_arm = (conn_centroid.head<2>() - mass_centroid.head<2>()).norm(); if (layer_z - conn_centroid.z() < 30.0) { diff --git a/src/libslic3r/SupportSpotsGenerator.hpp b/src/libslic3r/SupportSpotsGenerator.hpp index 2e07ef0290..ddb87f0f94 100644 --- a/src/libslic3r/SupportSpotsGenerator.hpp +++ b/src/libslic3r/SupportSpotsGenerator.hpp @@ -147,6 +147,33 @@ struct PartialObject bool connected_to_bed; }; + +/** + * Unsacled values of integrals over a polygonal domain. + */ +class Integrals{ + public: + /** + * Construct integral x_i int x_i^2 (i=1,2), xy and integral 1 (area). + * + * @param polygons List of polygons specifing the domain. + */ + explicit Integrals(const Polygons& polygons); + + // TODO refactor and delete the default constructor + Integrals() = default; + + float area{}; + Vec2f x_i{Vec2f::Zero()}; + Vec2f x_i_squared{Vec2f::Zero()}; + float xy{}; +}; + +float compute_second_moment( + const Integrals& integrals, + const Vec2f& axis_direction +); + using PartialObjects = std::vector; // Both support points and partial objects are sorted from the lowest z to the highest diff --git a/src/slic3r/GUI/EditGCodeDialog.cpp b/src/slic3r/GUI/EditGCodeDialog.cpp index 834930e08f..93c8d59c26 100644 --- a/src/slic3r/GUI/EditGCodeDialog.cpp +++ b/src/slic3r/GUI/EditGCodeDialog.cpp @@ -277,7 +277,6 @@ void EditGCodeDialog::add_selected_value_to_gcode() if (val.IsEmpty()) return; - const long pos = m_gcode_editor->GetInsertionPoint(); m_gcode_editor->WriteText(m_gcode_editor->GetInsertionPoint() == m_gcode_editor->GetLastPosition() ? "\n" + val : val); if (val.Last() == ']') { diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index ee4ebdf1ba..92d7bbfc04 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -4735,6 +4735,7 @@ void SubstitutionManager::update_from_config() if (m_substitutions == subst && m_grid_sizer->IsShown(1)) { // just update visibility for chb_match_single_lines int subst_id = 0; + assert(m_chb_match_single_lines.size() == size_t(subst.size()/4)); for (size_t i = 0; i < subst.size(); i += 4) { const std::string& params = subst[i + 2]; const bool regexp = strchr(params.c_str(), 'r') != nullptr || strchr(params.c_str(), 'R') != nullptr; @@ -4772,8 +4773,10 @@ void SubstitutionManager::delete_all() m_config->option("gcode_substitutions")->values.clear(); call_ui_update(); - if (!m_grid_sizer->IsEmpty()) + if (!m_grid_sizer->IsEmpty()) { m_grid_sizer->Clear(true); + m_chb_match_single_lines.clear(); + } m_parent->GetParent()->Layout(); } @@ -4839,6 +4842,7 @@ wxSizer* TabPrint::create_substitutions_widget(wxWindow* parent) m_subst_manager.init(m_config, parent, grid_sizer); m_subst_manager.set_cb_edited_substitution([this]() { update_dirty(); + Layout(); wxGetApp().mainframe->on_config_changed(m_config); // invalidate print }); m_subst_manager.set_cb_hide_delete_all_btn([this]() { diff --git a/tests/libslic3r/CMakeLists.txt b/tests/libslic3r/CMakeLists.txt index 90693c8a04..7b0d200c7d 100644 --- a/tests/libslic3r/CMakeLists.txt +++ b/tests/libslic3r/CMakeLists.txt @@ -39,6 +39,7 @@ add_executable(${_TEST_NAME}_tests test_astar.cpp test_anyptr.cpp test_jump_point_search.cpp + test_support_spots_generator.cpp ../data/prusaparts.cpp ../data/prusaparts.hpp ) diff --git a/tests/libslic3r/test_support_spots_generator.cpp b/tests/libslic3r/test_support_spots_generator.cpp new file mode 100644 index 0000000000..3ea8dcd070 --- /dev/null +++ b/tests/libslic3r/test_support_spots_generator.cpp @@ -0,0 +1,97 @@ +#include "libslic3r/Point.hpp" +#include +#include + +using namespace Slic3r; +using namespace SupportSpotsGenerator; + + +TEST_CASE("Numerical integral calculation compared with exact solution.", "[SupportSpotsGenerator]") { + const float width = 10; + const float height = 20; + const Polygon polygon = { + scaled(Vec2f{-width / 2, -height / 2}), + scaled(Vec2f{width / 2, -height / 2}), + scaled(Vec2f{width / 2, height / 2}), + scaled(Vec2f{-width / 2, height / 2}) + }; + + const Integrals integrals{{polygon}}; + CHECK(integrals.area == Approx(width * height)); + CHECK(integrals.x_i.x() == Approx(0)); + CHECK(integrals.x_i.y() == Approx(0)); + CHECK(integrals.x_i_squared.x() == Approx(std::pow(width, 3) * height / 12)); + CHECK(integrals.x_i_squared.y() == Approx(width * std::pow(height, 3) / 12)); +} + +TEST_CASE("Moment values and ratio check.", "[SupportSpotsGenerator]") { + const float width = 40; + const float height = 2; + + // Moments are calculated at centroid. + // Polygon centroid must not be (0, 0). + const Polygon polygon = { + scaled(Vec2f{0, 0}), + scaled(Vec2f{width, 0}), + scaled(Vec2f{width, height}), + scaled(Vec2f{0, height}) + }; + + const Integrals integrals{{polygon}}; + + const Vec2f x_axis{1, 0}; + const float x_axis_moment = compute_second_moment(integrals, x_axis); + + const Vec2f y_axis{0, 1}; + const float y_axis_moment = compute_second_moment(integrals, y_axis); + + const float moment_ratio = std::pow(width / height, 2); + + // Ensure the object transaltion has no effect. + CHECK(x_axis_moment == Approx(width * std::pow(height, 3) / 12)); + CHECK(y_axis_moment == Approx(std::pow(width, 3) * height / 12)); + // If the object is "wide" the y axis moments should be large compared to x axis moment. + CHECK(y_axis_moment / x_axis_moment == Approx(moment_ratio)); +} + +TEST_CASE("Moments calculation for rotated axis.", "[SupportSpotsGenerator]") { + + Polygon polygon = { + scaled(Vec2f{6.362284076172198, 138.9674202217155}), + scaled(Vec2f{97.48779843751677, 106.08136606617076}), + scaled(Vec2f{135.75221821532384, 66.84428834668765}), + scaled(Vec2f{191.5308049852741, 45.77905628725614}), + scaled(Vec2f{182.7525148049201, 74.01799041087513}), + scaled(Vec2f{296.83210979283473, 196.80022572637228}), + scaled(Vec2f{215.16434429179148, 187.45715418834143}), + scaled(Vec2f{64.64574271229334, 284.293883209721}), + scaled(Vec2f{110.76507036894843, 174.35633141113783}), + scaled(Vec2f{77.56229640885199, 189.33057746591336}) + }; + + Integrals integrals{{polygon}}; + + std::mt19937 generator{std::random_device{}()}; + std::uniform_real_distribution angle_distribution{0, 2*M_PI}; + + // Meassured counterclockwise from (1, 0) + const float angle = angle_distribution(generator); + Vec2f axis{std::cos(angle), std::sin(angle)}; + + float moment_calculated_then_rotated = compute_second_moment( + integrals, + axis + ); + + // We want to rotate the object clockwise by angle to align the axis with (1, 0) + // Method .rotate is counterclockwise for positive angle + polygon.rotate(-angle); + + Integrals integrals_rotated{{polygon}}; + float moment_rotated_polygon = compute_second_moment( + integrals_rotated, + Vec2f{1, 0} + ); + + CHECK(moment_calculated_then_rotated == Approx(moment_rotated_polygon)); +}