diff --git a/src/libslic3r/Arachne/WallToolPaths.cpp b/src/libslic3r/Arachne/WallToolPaths.cpp index a116674daa..9fd933480e 100644 --- a/src/libslic3r/Arachne/WallToolPaths.cpp +++ b/src/libslic3r/Arachne/WallToolPaths.cpp @@ -812,7 +812,8 @@ bool WallToolPaths::removeEmptyToolPaths(std::vector &toolpa * Get the order constraints of the insets when printing walls per region / hole. * Each returned pair consists of adjacent wall lines where the left has an inset_idx one lower than the right. * - * Odd walls should always go after their enclosing wall polygons. + * Odd walls follow the same order as even ones, so that the centre line of a narrow wall is an inner wall + * like any other when printing inner to outer. * * \param outer_to_inner Whether the wall polygons with a lower inset_idx should go before those with a higher one. */ @@ -886,12 +887,7 @@ WallToolPaths::ExtrusionLineSet WallToolPaths::getRegionOrder(const std::vector< continue; // not directly adjacent if (!shorter_then(loc_here - lineloc_nearby.j.p, (lineloc_here.j.w + lineloc_nearby.j.w) / 2 * diagonal_extension)) continue; // points are too far away from each other - if (here->is_odd || nearby->is_odd) { - if (here->is_odd && !nearby->is_odd && nearby->inset_idx < here->inset_idx) - order_requirements.emplace(std::make_pair(nearby, here)); - if (nearby->is_odd && !here->is_odd && here->inset_idx < nearby->inset_idx) - order_requirements.emplace(std::make_pair(here, nearby)); - } else if ((nearby->inset_idx < here->inset_idx) == outer_to_inner) { + if ((nearby->inset_idx < here->inset_idx) == outer_to_inner) { order_requirements.emplace(std::make_pair(nearby, here)); } else { assert((nearby->inset_idx > here->inset_idx) == outer_to_inner); diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index 508ab5b421..1500483f6e 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -3985,6 +3985,16 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato this->placeholder_parser().set("print_time_minute", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Print_Time_Minute_Placeholder))); this->placeholder_parser().set("print_time_sec", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Print_Time_Sec_Placeholder))); this->placeholder_parser().set("used_filament_length", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Used_Filament_Length_Placeholder))); + // The final print statistics are known only after the whole G-code is generated, so they are filled in during post-processing too. + // The running extruded_* totals are left alone; profiles use them as numbers. + this->placeholder_parser().set("used_filament", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Used_Filament_Length_Placeholder))); + this->placeholder_parser().set("print_time", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Normal_Print_Time_Placeholder))); + this->placeholder_parser().set("normal_print_time", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Normal_Print_Time_Placeholder))); + this->placeholder_parser().set("silent_print_time", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Silent_Print_Time_Placeholder))); + this->placeholder_parser().set("total_weight", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Total_Weight_Placeholder))); + this->placeholder_parser().set("total_cost", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Total_Cost_Placeholder))); + this->placeholder_parser().set("total_wipe_tower_cost", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Total_Wipe_Tower_Cost_Placeholder))); + this->placeholder_parser().set("total_wipe_tower_filament", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Total_Wipe_Tower_Filament_Placeholder))); // Sync variant-mapped params into placeholder_parser before processing start gcode update_placeholder_parser_with_variant_params(); diff --git a/src/libslic3r/GCode/GCodeProcessor.cpp b/src/libslic3r/GCode/GCodeProcessor.cpp index ac047be180..6346651448 100644 --- a/src/libslic3r/GCode/GCodeProcessor.cpp +++ b/src/libslic3r/GCode/GCodeProcessor.cpp @@ -120,7 +120,13 @@ const std::vector GCodeProcessor::Reserved_Tags = { "@PRINT_TIME_HOUR@", "@PRINT_TIME_MINUTE@", "@PRINT_TIME_SEC@", - "@USED_FILAMENT_LENGTH@" + "@USED_FILAMENT_LENGTH@", + "@NORMAL_PRINT_TIME@", + "@SILENT_PRINT_TIME@", + "@TOTAL_WEIGHT@", + "@TOTAL_COST@", + "@TOTAL_WIPE_TOWER_COST@", + "@TOTAL_WIPE_TOWER_FILAMENT@" }; const std::vector GCodeProcessor::Reserved_Tags_compatible = { @@ -147,7 +153,13 @@ const std::vector GCodeProcessor::Reserved_Tags_compatible = { "@PRINT_TIME_HOUR@", "@PRINT_TIME_MINUTE@", "@PRINT_TIME_SEC@", - "@USED_FILAMENT_LENGTH@" + "@USED_FILAMENT_LENGTH@", + "@NORMAL_PRINT_TIME@", + "@SILENT_PRINT_TIME@", + "@TOTAL_WEIGHT@", + "@TOTAL_COST@", + "@TOTAL_WIPE_TOWER_COST@", + "@TOTAL_WIPE_TOWER_FILAMENT@" }; @@ -1264,19 +1276,22 @@ void GCodeProcessor::run_post_process() return ret; }; - // Process inline placeholders (print_time_total_sec, print_time_day, print_time_hour, print_time_minute, print_time_sec and used_filament_length) + // Process inline placeholders (print_time_*, used_filament_length and the print statistics known only after export) auto process_inline_placeholders = [&](std::string& gcode_line) { bool processed = false; // Every inline placeholder contains '@', so a line without one has nothing to replace. if (gcode_line.find('@') == std::string::npos) return processed; - const std::string& print_time_total_placeholder = reserved_tag(ETags::Print_Time_Total_Sec_Placeholder); - const std::string& print_time_day_placeholder = reserved_tag(ETags::Print_Time_Day_Placeholder); - const std::string& print_time_hour_placeholder = reserved_tag(ETags::Print_Time_Hour_Placeholder); - const std::string& print_time_minute_placeholder = reserved_tag(ETags::Print_Time_Minute_Placeholder); - const std::string& print_time_sec_placeholder = reserved_tag(ETags::Print_Time_Sec_Placeholder); - const std::string& used_filament_placeholder = reserved_tag(ETags::Used_Filament_Length_Placeholder); + auto replace_tag = [&gcode_line, &processed](ETags tag, const std::string& value) { + const std::string& placeholder = reserved_tag(tag); + size_t pos = gcode_line.find(placeholder); + while (pos != std::string::npos) { + gcode_line.replace(pos, placeholder.length(), value); + processed = true; + pos = gcode_line.find(placeholder, pos + value.length()); + } + }; double print_time_total_sec = m_time_processor.machines[static_cast(PrintEstimatedStatistics::ETimeMode::Normal)].time; if (print_time_total_sec < 0.0) @@ -1289,70 +1304,31 @@ void GCodeProcessor::run_post_process() int print_time_minute = (day_remainder_seconds % 3600) / 60; int print_time_sec = day_remainder_seconds % 60; - // Replace print_time_total_sec - size_t pos = gcode_line.find(print_time_total_placeholder); - while (pos != std::string::npos) { - char buf[64]; - sprintf(buf, "%.2f", print_time_total_sec); - gcode_line.replace(pos, print_time_total_placeholder.length(), buf); - processed = true; - pos = gcode_line.find(print_time_total_placeholder, pos + strlen(buf)); + double total_filament_mm = 0.0; + for (const auto& mm : filament_mm) { + total_filament_mm += mm; } - // Replace print_time_day - pos = gcode_line.find(print_time_day_placeholder); - while (pos != std::string::npos) { - char buf[64]; - sprintf(buf, "%d", print_time_day); - gcode_line.replace(pos, print_time_day_placeholder.length(), buf); - processed = true; - pos = gcode_line.find(print_time_day_placeholder, pos + strlen(buf)); - } - - // Replace print_time_hour - pos = gcode_line.find(print_time_hour_placeholder); - while (pos != std::string::npos) { - char buf[64]; - sprintf(buf, "%d", print_time_hour); - gcode_line.replace(pos, print_time_hour_placeholder.length(), buf); - processed = true; - pos = gcode_line.find(print_time_hour_placeholder, pos + strlen(buf)); - } - - // Replace print_time_minute - pos = gcode_line.find(print_time_minute_placeholder); - while (pos != std::string::npos) { - char buf[64]; - sprintf(buf, "%d", print_time_minute); - gcode_line.replace(pos, print_time_minute_placeholder.length(), buf); - processed = true; - pos = gcode_line.find(print_time_minute_placeholder, pos + strlen(buf)); - } - - // Replace print_time_sec - pos = gcode_line.find(print_time_sec_placeholder); - while (pos != std::string::npos) { - char buf[64]; - sprintf(buf, "%d", print_time_sec); - gcode_line.replace(pos, print_time_sec_placeholder.length(), buf); - processed = true; - pos = gcode_line.find(print_time_sec_placeholder, pos + strlen(buf)); - } - - // Replace used_filament_length - pos = gcode_line.find(used_filament_placeholder); - while (pos != std::string::npos) { - double total_filament_mm = 0.0; - for (const auto& mm : filament_mm) { - total_filament_mm += mm; - } - double used_filament_length = total_filament_mm / 1000.0; // Convert mm to m - char buf[64]; - sprintf(buf, "%.2f", used_filament_length); - gcode_line.replace(pos, used_filament_placeholder.length(), buf); - processed = true; - pos = gcode_line.find(used_filament_placeholder, pos + strlen(buf)); - } + replace_tag(ETags::Print_Time_Total_Sec_Placeholder, Slic3r::float_to_string_decimal_point(print_time_total_sec, 2)); + replace_tag(ETags::Print_Time_Day_Placeholder, std::to_string(print_time_day)); + replace_tag(ETags::Print_Time_Hour_Placeholder, std::to_string(print_time_hour)); + replace_tag(ETags::Print_Time_Minute_Placeholder, std::to_string(print_time_minute)); + replace_tag(ETags::Print_Time_Sec_Placeholder, std::to_string(print_time_sec)); + replace_tag(ETags::Used_Filament_Length_Placeholder, + Slic3r::float_to_string_decimal_point(total_filament_mm / 1000.0, 2)); // Convert mm to m + // Same values as the print statistics that filename_format gets (PrintStatistics::config(), filled in by + // update_print_estimated_stats() in GCode.cpp), which runs only after this post-processing. + replace_tag(ETags::Normal_Print_Time_Placeholder, get_time_dhm(PrintEstimatedStatistics::ETimeMode::Normal)); + replace_tag(ETags::Silent_Print_Time_Placeholder, is_stealth_time_estimator_enabled() ? + get_time_dhm(PrintEstimatedStatistics::ETimeMode::Stealth) : + short_time("N/A")); + const double total_cost = filament_total_cost + m_print->config().time_cost.value * print_time_total_sec / 3600.0; + replace_tag(ETags::Total_Weight_Placeholder, Slic3r::float_to_string_decimal_point(filament_total_g, 2)); + replace_tag(ETags::Total_Cost_Placeholder, Slic3r::float_to_string_decimal_point(total_cost, 2)); + replace_tag(ETags::Total_Wipe_Tower_Cost_Placeholder, + Slic3r::float_to_string_decimal_point(m_print->print_statistics().total_wipe_tower_cost, 2)); + replace_tag(ETags::Total_Wipe_Tower_Filament_Placeholder, + Slic3r::float_to_string_decimal_point(m_print->print_statistics().total_wipe_tower_filament, 2)); return processed; }; diff --git a/src/libslic3r/GCode/GCodeProcessor.hpp b/src/libslic3r/GCode/GCodeProcessor.hpp index 87c08617d1..72fb84bad0 100644 --- a/src/libslic3r/GCode/GCodeProcessor.hpp +++ b/src/libslic3r/GCode/GCodeProcessor.hpp @@ -601,6 +601,12 @@ inline constexpr float DEFAULT_FILAMENT_DENSITY = 1.245f; Print_Time_Minute_Placeholder, Print_Time_Sec_Placeholder, Used_Filament_Length_Placeholder, + Normal_Print_Time_Placeholder, + Silent_Print_Time_Placeholder, + Total_Weight_Placeholder, + Total_Cost_Placeholder, + Total_Wipe_Tower_Cost_Placeholder, + Total_Wipe_Tower_Filament_Placeholder, }; static const std::string& reserved_tag(ETags tag) { return s_IsBBLPrinter ? Reserved_Tags[static_cast(tag)] : Reserved_Tags_compatible[static_cast(tag)]; } diff --git a/src/libslic3r/PerimeterGenerator.cpp b/src/libslic3r/PerimeterGenerator.cpp index b1c6ed21fa..eed1adcf78 100644 --- a/src/libslic3r/PerimeterGenerator.cpp +++ b/src/libslic3r/PerimeterGenerator.cpp @@ -62,7 +62,12 @@ public: // Depth in the hierarchy. External perimeter has depth = 0. An external perimeter could be both a contour and a hole. unsigned short depth; // ORCA: an external hole perimeter touching the next outer wall all around, with nothing between them. + // E.g. a 1mm tube wall with 0.5mm walls: 2 lines, the contour's outer wall and the hole's. bool is_thin_wall_hole = false; + // ORCA: an external hole perimeter with no room for an inner wall beside it, at most gap fill. Every thin wall + // hole is one, and so is e.g. a 1.5mm tube wall with 0.5mm walls: 3 lines, the two outer walls and gap fill + // between them. A 2mm tube wall has room for an inner wall a side and is not. Only set for Inner/Outer/Inner. + bool is_narrow_wall_hole = false; // Children contour, may be both CCW and CW oriented (outer contours or holes). std::vector children; @@ -572,7 +577,8 @@ static ExtrusionEntityCollection traverse_extrusions(const PerimeterGenerator& p extrusion_coll.append(std::move(extrusion_loop)); // Orca: Reverse the order of paths for thin wall holes. We define thin wall hole as a hole with only one perimeter. const bool thin_wall_hole = !pg_extrusion.is_contour && pg_extrusions.size() == 2; - if (thin_wall_hole && perimeter_generator.config->wall_sequence != WallSequence::OuterInner) + if (thin_wall_hole && perimeter_generator.config->wall_sequence != WallSequence::OuterInner && + !perimeter_generator.outer_walls_first_for_brim()) std::reverse(extrusion_coll.entities.begin(), extrusion_coll.entities.end()); } else { @@ -1690,13 +1696,24 @@ void PerimeterGenerator::process_classic() last = std::move(offsets); - // ORCA: a thin wall hole has no room for gap fill or an inner wall anywhere beside its outer wall. + // ORCA: a thin wall hole has no room for gap fill or an inner wall anywhere beside its outer wall, a + // narrow wall hole none for an inner wall, the room the next loop is offset by below. if (i == 0 && ! holes[0].empty()) { - const float room = float(0.5 * (ext_perimeter_spacing2 + gap_fill_min_width)); - const Polygons reach = to_polygons(offset2_ex(last, -room, room + float(SCALED_EPSILON))); - for (PerimeterGeneratorLoop &hole : holes[0]) - hole.is_thin_wall_hole = intersection_pl(Polylines{ hole.polygon.split_at_first_point() }, - ClipperUtils::clip_clipper_polygons_with_subject_bbox(reach, get_extents(hole.polygon).inflated(SCALED_EPSILON))).empty(); + auto reach = [&last](double room) { + return to_polygons(offset2_ex(last, -float(room), float(room) + float(SCALED_EPSILON))); + }; + auto out_of_reach = [](const Polygon &hole, const Polygons &reach) { + return intersection_pl(Polylines{ hole.split_at_first_point() }, + ClipperUtils::clip_clipper_polygons_with_subject_bbox(reach, get_extents(hole).inflated(SCALED_EPSILON))).empty(); + }; + // Only Inner/Outer/Inner orders narrow wall holes apart. + const bool narrow_holes_ordered = this->config->wall_sequence == WallSequence::InnerOuterInner; + const Polygons gap_fill_reach = reach(0.5 * (ext_perimeter_spacing2 + gap_fill_min_width)); + const Polygons inner_wall_reach = narrow_holes_ordered ? reach(ext_perimeter_spacing2 + min_spacing / 2.) : Polygons(); + for (PerimeterGeneratorLoop &hole : holes[0]) { + hole.is_thin_wall_hole = out_of_reach(hole.polygon, gap_fill_reach); + hole.is_narrow_wall_hole = narrow_holes_ordered && out_of_reach(hole.polygon, inner_wall_reach); + } } //BBS: refer to superslicer @@ -1835,18 +1852,16 @@ void PerimeterGenerator::process_classic() // if brim will be printed, reverse the order of perimeters so that // we continue inwards after having finished the brim - // TODO: add test for perimeter order bool is_outer_wall_first = this->config->wall_sequence == WallSequence::OuterInner; - if (is_outer_wall_first || - //BBS: always print outer wall first when there indeed has brim. - // btLeadingEdgeOnly is an outer brim too (a belt brim at the part's first contact). - (this->layer_id == 0 && - (this->object_config->brim_type == BrimType::btOuterOnly || - this->object_config->brim_type == BrimType::btLeadingEdgeOnly) && - this->object_config->brim_width.value > 0)) + // Orca: a contour with only a narrow wall hole beside it has nothing to sandwich, and prints the hole first, + // then the contour, then the gap fill between them, as Arachne does. + const PerimeterGeneratorLoops &island_loops = contours.front(); + const bool narrow_wall_hole_island = island_loops.size() == 1 && island_loops.front().children.size() == 1 && + island_loops.front().children.front().is_narrow_wall_hole; + if (is_outer_wall_first || this->outer_walls_first_for_brim()) entities.reverse(); // Orca: sandwich mode. Apply after 1st layer. - else if ((this->config->wall_sequence == WallSequence::InnerOuterInner) && layer_id > 0){ + else if ((this->config->wall_sequence == WallSequence::InnerOuterInner) && layer_id > 0 && !narrow_wall_hole_island){ entities.reverse(); // reverse all entities - order them from external to internal if(entities.entities.size()>2){ // 3 walls minimum needed to do inner outer inner ordering int position = 0; // index to run the re-ordering for multiple external perimeters in a single island. @@ -2412,11 +2427,25 @@ std::vector reorderPerimetersByProximity(std // Function to reorder perimeters starting from a given reference index auto reorderFromReference = [&](int referenceIndex) { - std::unordered_set firstLevelIndices; - firstLevelIndices.insert(referenceIndex); - - // Find first level touching perimeters - std::vector firstLevelTouchingIndices = findAllTouchingPerimeters(entities, firstLevelIndices, threshold_external, threshold_internal, 1); + // Find first level touching perimeters. The odd centre line of a wall too narrow for two inner walls is shared + // by the outer walls on both sides, so those join this island and are printed before it too, holes first. + std::vector outerIndices{referenceIndex}; + std::vector firstLevelTouchingIndices; + for (size_t next = 0; next < outerIndices.size(); ++next) + for (int idx : findAllTouchingPerimeters(entities, {outerIndices[next]}, threshold_external, threshold_internal, 1)) { + if (std::find(firstLevelTouchingIndices.begin(), firstLevelTouchingIndices.end(), idx) != firstLevelTouchingIndices.end()) + continue; + firstLevelTouchingIndices.push_back(idx); + if (!entities[idx].extrusion->is_odd) + continue; + for (size_t i = 0; i < entities.size(); ++i) + if (entities[i].extrusion->inset_idx == 0 && includedIndices.count(int(i)) == 0 && + std::find(outerIndices.begin(), outerIndices.end(), int(i)) == outerIndices.end() && + arachne_lines_touch(*entities[i].extrusion, *entities[idx].extrusion, double(threshold_external))) + outerIndices.push_back(int(i)); + } + std::stable_partition(outerIndices.begin(), outerIndices.end(), + [&entities](int idx) { return entities[idx].extrusion->is_closed && !entities[idx].is_contour; }); // Bring the largest first level perimeter to the front // The longest first neighbour is most likely the dominant proximate perimeter // hence printing it immediately after the external perimeter should speed things up @@ -2427,8 +2456,10 @@ std::vector reorderPerimetersByProximity(std std::iter_swap(maxIt, firstLevelTouchingIndices.end() - 1); } // Insert first level perimeters into reordered list - reordered.push_back(entities[referenceIndex]); - includedIndices.insert(referenceIndex); + for (int idx : outerIndices) { + reordered.push_back(entities[idx]); + includedIndices.insert(idx); + } for (int idx : firstLevelTouchingIndices) { if (includedIndices.count(idx) == 0) { @@ -2757,7 +2788,7 @@ void PerimeterGenerator::process_arachne() if (layer_id == 0){ // disable inner outer inner algorithm after the first layer is_outer_wall_first = - this->config->wall_sequence == WallSequence::OuterInner; + this->config->wall_sequence == WallSequence::OuterInner || this->outer_walls_first_for_brim(); } if (is_outer_wall_first) { start_perimeter = 0; @@ -3042,6 +3073,16 @@ ExPolygons PerimeterGenerator::printable_slices(const ExPolygons &slices) const return min_width > EPSILON ? opening_ex(slices, float(scale_(min_width / 2.))) : slices; } +bool PerimeterGenerator::outer_walls_first_for_brim() const +{ + return layer_id == 0 && brim_puts_outer_walls_first(object_config->brim_type.value, object_config->brim_width.value); +} + +bool brim_puts_outer_walls_first(BrimType brim_type, double brim_width) +{ + return brim_width > 0 && (brim_type == btOuterOnly || brim_type == btOuterAndInner || brim_type == btLeadingEdgeOnly); +} + std::vector PerimeterGenerator::generate_lower_polygons_series(float width) { float nozzle_diameter = print_config->nozzle_diameter.get_at(config->outer_wall_filament_id - 1); diff --git a/src/libslic3r/PerimeterGenerator.hpp b/src/libslic3r/PerimeterGenerator.hpp index ceaa2252d1..0dcc510e42 100644 --- a/src/libslic3r/PerimeterGenerator.hpp +++ b/src/libslic3r/PerimeterGenerator.hpp @@ -164,6 +164,8 @@ public: Polygons lower_slices_polygons() const { return m_lower_slices_polygons; } // ORCA: the slices less the slivers the wall generator prints nothing for, so they never count as support. ExPolygons printable_slices(const ExPolygons &slices) const; + // ORCA: whether this layer prints its outer walls first for the brim, see brim_puts_outer_walls_first(). + bool outer_walls_first_for_brim() const; private: std::vector generate_lower_polygons_series(float width); @@ -183,6 +185,10 @@ private: Polygons m_lower_slices_polygons; }; +// ORCA: a brim against the outer walls makes the first layer print them first, whatever the wall sequence, so the +// brim runs straight on into the outer wall. Auto, ear and painted brims may leave the outer walls bare. +bool brim_puts_outer_walls_first(BrimType brim_type, double brim_width); + } #endif diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index f557348998..4ae928cc95 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -2293,6 +2293,7 @@ void PresetCollection::load_project_embedded_presets(std::vector& proje BOOST_LOG_TRIVIAL(error) << boost::format("can not find parent for config %1%!")%preset->file; continue; } + extend_default_config_length(config, false, {}); preset->config.update_diff_values_to_child_config(config, extruder_id_name, extruder_variant_name, *key_set1, *key_set2); //preset->config.apply(std::move(config)); Preset::normalize(preset->config); diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index 9e76d6690a..7e9b21ccdf 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -17,6 +17,7 @@ #include "I18N.hpp" #include "Layer.hpp" #include "MutablePolygon.hpp" +#include "PerimeterGenerator.hpp" #include "PrintBase.hpp" #include "PrintConfig.hpp" #include "SLA/IndexedMesh.hpp" @@ -1464,15 +1465,13 @@ bool PrintObject::invalidate_state_by_config_options( || opt_key == "top_surface_speed") { // Brim is printed below supports, support invalidates brim and skirt. steps.emplace_back(posSupportMaterial); - if (opt_key == "brim_type") { - const auto* old_brim_type = old_config.option>(opt_key); - const auto* new_brim_type = new_config.option>(opt_key); - //BBS: When switch to manual brim, the object must have brim, then re-generate perimeter - //to make the wall order of first layer to be outer-first - // btLeadingEdgeOnly is printed as an outer brim (Brim.cpp, BeltBrim.cpp), so it - // takes part in the same first-layer wall order rule. - if (old_brim_type->value == btOuterOnly || new_brim_type->value == btOuterOnly || - old_brim_type->value == btLeadingEdgeOnly || new_brim_type->value == btLeadingEdgeOnly) + if (opt_key == "brim_type" || opt_key == "brim_width") { + // Regenerate the walls when the brim starts or stops putting the first layer's outer walls first. + auto outer_walls_first = [](const ConfigOptionResolver &config) { + return brim_puts_outer_walls_first(config.option>("brim_type")->value, + config.option("brim_width")->value); + }; + if (outer_walls_first(old_config) != outer_walls_first(new_config)) steps.emplace_back(posPerimeters); } } else if ( diff --git a/src/libslic3r/Support/SupportCommon.cpp b/src/libslic3r/Support/SupportCommon.cpp index 481213fe48..dddbfcf189 100644 --- a/src/libslic3r/Support/SupportCommon.cpp +++ b/src/libslic3r/Support/SupportCommon.cpp @@ -707,8 +707,10 @@ void tree_supports_generate_paths( false); } if (eec) { - std::reverse(eec->entities.begin(), eec->entities.end()); - dst.emplace_back(eec.release()); + if (!eec->empty()) { + std::reverse(eec->entities.begin(), eec->entities.end()); + dst.emplace_back(eec.release()); + } } } } @@ -1825,12 +1827,7 @@ void generate_support_toolpaths( no_sort = true; } else if (support_params.support_style == SupportMaterialStyle::smsTreeOrganic && (config.support_base_pattern == smpNone || config.support_base_pattern == smpDefault)) { - // Orca: A special case for the hollow Organic supports - // Orca: If the tree supports are too tall, use a double wall to make it stronger - SupportParameters support_params2 = support_params; - if (support_layer.print_z > 100.0) - support_params2.tree_branch_diameter_double_wall_area_scaled = 0.1; - tree_supports_generate_paths(base_layer.extrusions, base_layer.polygons_to_extrude(), flow, support_params2); + tree_supports_generate_paths(base_layer.extrusions, base_layer.polygons_to_extrude(), flow, support_params); done = true; } if (! done) diff --git a/src/libslic3r/Support/TreeSupport.cpp b/src/libslic3r/Support/TreeSupport.cpp index 5187e73a25..70ab0b06f1 100644 --- a/src/libslic3r/Support/TreeSupport.cpp +++ b/src/libslic3r/Support/TreeSupport.cpp @@ -2149,7 +2149,9 @@ void TreeSupport::draw_circles() const bool with_lightning_infill = m_support_params.base_fill_pattern == ipLightning; coordf_t support_extrusion_width = m_support_params.support_extrusion_width; const float tree_brim_width = config.tree_support_brim_width.value; - + // Reinforce branches moving more than half a support-line width between layers. + // Square the threshold for comparison with the squared XY movement below. + const double max_move_sq = sqr(double(scale_(m_object_config->support_line_width / 2))); // Belt floor: the first object layer is not on a flat bed — it rests on the // tilted, moving belt. So the first-object-layer adhesion features (the tree // support brim, the hybrid first-layer base expansion) must be suppressed: @@ -2201,7 +2203,7 @@ void TreeSupport::draw_circles() size_t first_base_roof_area = 0; bool floor_interface_as_base = false; bool has_circle_node = false; - bool need_extra_wall = false; + ExPolygons extra_wall_areas; ExPolygons collision_sharp_tails; ExPolygons collision_base; auto get_collision = [&](bool sharp_tail) -> ExPolygons &{ @@ -2279,7 +2281,6 @@ void TreeSupport::draw_circles() // area = diff_clipped({ ExPolygon(circle) }, get_collision(node.is_sharp_tail && node.distance_to_top <= 0)); if (!area.empty()) has_circle_node = true; - if (node.need_extra_wall) need_extra_wall = true; // merge overhang to get a smoother interface surface // Do not merge when buildplate_only is on, because some underneath nodes may have been deleted. @@ -2294,6 +2295,10 @@ void TreeSupport::draw_circles() } } + // Use the Pythagorean theorem to compare movement magnitude (dx² + dy²) without a square root. + if (node.is_processed && node.movement.cast().squaredNorm() > max_move_sq) + append(extra_wall_areas, area); + if (obj_layer_nr>0 && node.distance_to_top < 0) append(roof_gap_areas, area); else if (obj_layer_nr > 0 && node.support_roof_layers_below == 1 && @@ -2530,7 +2535,8 @@ void TreeSupport::draw_circles() //if (area(expoly) < SQ(scale_(1))) continue; area_groups.emplace_back(&expoly, SupportLayer::BaseType, max_layers_above_base); area_groups.back().need_infill = overlaps({ expoly }, area_poly); - area_groups.back().need_extra_wall = need_extra_wall && !area_groups.back().need_infill; + area_groups.back().need_extra_wall = !area_groups.back().need_infill && + !extra_wall_areas.empty() && overlaps({expoly}, extra_wall_areas); } for (size_t roof_idx = 0; roof_idx < ts_layer->roof_areas.size(); ++roof_idx) { auto &expoly = ts_layer->roof_areas[roof_idx]; @@ -3433,11 +3439,6 @@ void TreeSupport::smooth_nodes() } } - float max_move = scale_(m_object_config->support_line_width / 2); - // if the branch is very tall, the tip also needs extra wall - float thresh_tall_branch = 100; - float thresh_dist_to_top = 30; - for (int layer_nr = 0; layer_nr< contact_nodes.size(); layer_nr++) { std::vector &curr_layer_nodes = contact_nodes[layer_nr]; if (curr_layer_nodes.empty()) continue; @@ -3447,20 +3448,17 @@ void TreeSupport::smooth_nodes() std::vector radii; std::vector branch; SupportNode * p_node = node; - float total_height = 0; // add a fixed head if it's not a polygon node, see STUDIO-4403 // Polygon node can't be added because the move distance might be huge, making the nodes in between jump and dangling if (node->child && node->child->type!=ePolygon) { pts.push_back(p_node->child->position); radii.push_back(p_node->child->radius); branch.push_back(p_node->child); - total_height += p_node->child->height; } do { pts.push_back(p_node->position); radii.push_back(p_node->radius); branch.push_back(p_node); - total_height += p_node->height; p_node = p_node->parent; } while (p_node && !p_node->is_processed); if (pts.size() < 3) continue; @@ -3479,9 +3477,6 @@ void TreeSupport::smooth_nodes() branch[i]->radius = radii1[i]; branch[i]->movement = (pts[i + 1] - pts[i - 1]) / 2; branch[i]->is_processed = true; - if (branch[i]->parents.size() > 1 || (branch[i]->movement.x() > max_move || branch[i]->movement.y() > max_move) || - (total_height > thresh_tall_branch && branch[i]->dist_mm_to_top < thresh_dist_to_top)) - branch[i]->need_extra_wall = true; BOOST_LOG_TRIVIAL(info) << "smooth_nodes: layer_nr=" << layer_nr << ", i=" << i << ", pt=" << pt << ", movement=" << branch[i]->movement << ", radius=" << branch[i]->radius; } } @@ -3489,13 +3484,6 @@ void TreeSupport::smooth_nodes() std::swap(pts, pts1); std::swap(radii, radii1); } - else { - // interpolate need_extra_wall in the end - for (size_t i = 1; i < branch.size() - 1; i++) { - if (branch[i - 1]->need_extra_wall && branch[i + 1]->need_extra_wall) - branch[i]->need_extra_wall = true; - } - } } } } diff --git a/src/libslic3r/Support/TreeSupport.hpp b/src/libslic3r/Support/TreeSupport.hpp index d8bb241fee..e6f651077f 100644 --- a/src/libslic3r/Support/TreeSupport.hpp +++ b/src/libslic3r/Support/TreeSupport.hpp @@ -135,7 +135,6 @@ struct SupportNode TreeNodeType type = eCircle; bool is_corner = false; bool is_processed = false; - bool need_extra_wall = false; bool is_sharp_tail = false; bool valid = true; ExPolygon overhang; // when type==ePolygon, set this value to get original overhang area diff --git a/src/slic3r/GUI/Selection.cpp b/src/slic3r/GUI/Selection.cpp index 58d5e15fe3..4d7f1591ee 100644 --- a/src/slic3r/GUI/Selection.cpp +++ b/src/slic3r/GUI/Selection.cpp @@ -474,17 +474,14 @@ void Selection::add_curr_plate() if (!m_valid) return; + std::vector volume_idxs = get_volume_idxs_from_plate(wxGetApp().plater()->get_partplate_list().get_curr_plate_index()); + if (volume_idxs.empty()) + return; + wxGetApp().plater()->take_snapshot(std::string("Selection-Add Curr Plate All!")); m_mode = Instance; clear(); - - PartPlate* plate = wxGetApp().plater()->get_partplate_list().get_curr_plate(); - for (int obj_idx = 0; obj_idx < m_model->objects.size(); obj_idx++) { - if (plate && plate->contain_instance_totally(obj_idx, 0)) { - std::vector volume_idxs = get_volume_idxs_from_object(obj_idx); - do_add_volumes(volume_idxs); - } - } + do_add_volumes(volume_idxs); update_type(); this->set_bounding_boxes_dirty(); @@ -511,20 +508,14 @@ void Selection::remove_curr_plate() if (!m_valid) return; - PartPlate* plate = wxGetApp().plater()->get_partplate_list().get_curr_plate(); - if (plate->empty()) + std::vector volume_idxs = get_volume_idxs_from_plate(wxGetApp().plater()->get_partplate_list().get_curr_plate_index()); + if (volume_idxs.empty()) return; wxGetApp().plater()->take_snapshot(std::string("Selection-Delete Curr Plate All")); m_mode = Instance; clear(); - - for (int obj_idx = 0; obj_idx < m_model->objects.size(); obj_idx++) { - if (plate && plate->contain_instance(obj_idx, 0)) { - std::vector volume_idxs = get_volume_idxs_from_object(obj_idx); - do_add_volumes(volume_idxs); - } - } + do_add_volumes(volume_idxs); update_type(); this->set_bounding_boxes_dirty(); @@ -2270,6 +2261,27 @@ std::vector Selection::get_volume_idxs_from_volume(unsigned int ob return idxs; } +std::vector Selection::get_volume_idxs_from_plate(const int plate_idx) const +{ + PartPlate* plate = wxGetApp().plater()->get_partplate_list().get_plate(plate_idx); + std::vector idxs; + if (!plate) + return idxs; + + for (int obj_idx = 0; obj_idx < (int)m_model->objects.size(); obj_idx++) { + const ModelObject* obj = m_model->objects[obj_idx]; + for (int inst_idx = 0; inst_idx < (int)obj->instances.size(); inst_idx++) { + // on this plate or overlapping it in some way (fully inside, oversized or partly outside) + if (plate->contain_instance(obj_idx, inst_idx) || plate->intersect_instance(obj_idx, inst_idx)) { + std::vector inst_idxs = get_volume_idxs_from_instance(obj_idx, inst_idx); + idxs.insert(idxs.end(), inst_idxs.begin(), inst_idxs.end()); + } + } + } + + return idxs; +} + std::vector Selection::get_missing_volume_idxs_from(const std::vector& volume_idxs) const { std::vector idxs; diff --git a/src/slic3r/GUI/Selection.hpp b/src/slic3r/GUI/Selection.hpp index dcee38a813..a02f01da00 100644 --- a/src/slic3r/GUI/Selection.hpp +++ b/src/slic3r/GUI/Selection.hpp @@ -409,6 +409,8 @@ public: std::vector get_volume_idxs_from_instance(unsigned int object_idx, unsigned int instance_idx) const; // returns the idx of the volume corresponding to the volume with the given idxs std::vector get_volume_idxs_from_volume(unsigned int object_idx, unsigned int instance_idx, unsigned int volume_idx) const; + // returns the list of idxs of the volumes of all instances on the given plate or overlapping it. (fully inside, oversized or partly outside) + std::vector get_volume_idxs_from_plate(const int plate_idx) const; // returns the list of idxs of the volumes contained in the selection but not in the given list std::vector get_missing_volume_idxs_from(const std::vector& volume_idxs) const; // returns the list of idxs of the volumes contained in the given list but not in the selection diff --git a/tests/fff_print/test_perimeters.cpp b/tests/fff_print/test_perimeters.cpp index 070912c307..b105cfd832 100644 --- a/tests/fff_print/test_perimeters.cpp +++ b/tests/fff_print/test_perimeters.cpp @@ -22,6 +22,7 @@ #include "libslic3r/ClipperUtils.hpp" #include "libslic3r/ExPolygon.hpp" #include "libslic3r/Point.hpp" +#include "libslic3r/Polygon.hpp" #include "libslic3r/Polyline.hpp" #include #include @@ -994,3 +995,384 @@ TEST_CASE("A hole whose outer wall touches the contour's runs with it", "[Perime CHECK(layers[i].holes_ccw.front() == ccw); } } + +namespace { + +// The outer radius of round_tube(). +const double tube_radius = 11.5; + +// Every setting the tube walls below are measured against. With 0.5mm walls a 1mm tube wall holds only the two +// outer walls, touching, and every further 0.5mm makes room for one more inner wall. +DynamicPrintConfig tube_config(const char *wall_generator, int wall_loops) +{ + DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); + config.set_deserialize_strict({ + { "wall_generator", wall_generator }, + { "wall_loops", wall_loops }, + { "wall_sequence", "inner wall/outer wall" }, + { "wall_direction", "ccw" }, + { "layer_height", 0.2 }, + { "initial_layer_print_height", 0.2 }, + { "nozzle_diameter", "0.4" }, + { "outer_wall_line_width", 0.5 }, + { "inner_wall_line_width", 0.5 }, + { "precise_outer_wall", true }, + { "detect_thin_wall", false }, + { "only_one_wall_top", false }, + { "only_one_wall_first_layer", false }, + { "brim_type", "no_brim" }, + { "detect_overhang_wall", true }, + { "overhang_reverse", false }, + { "overhang_reverse_threshold", "0%" }, + }); + return config; +} + +// A 2mm tall round tube whose wall is `wall` mm thick. +Print &round_tube(Print &print, Model &model, const DynamicPrintConfig &config, double wall) +{ + ModelObject *object = model.add_object(); + object->name = "round_tube.stl"; + object->add_volume(make_cylinder(tube_radius, 2.), ModelVolumeType::MODEL_PART, false); + object->add_volume(make_cylinder(tube_radius - wall, 2.), ModelVolumeType::NEGATIVE_VOLUME, false); + object->add_instance(); + object->ensure_on_bed(); + + print.auto_assign_extruders(object); + print.apply(model, config); + print.validate(); + print.set_status_silent(); + print.process(); + return print; +} + +// The side of the tube wall a wall loop lies on. The centre is the middle of the tube wall, where Arachne lays the +// inner wall both sides share when the tube wall is too narrow for a pair of them. +enum class TubeSide { Contour, Hole, Centre }; + +struct TubeLoop { + TubeSide side; + int inset; + bool ccw; +}; + +// The wall loops of every layer of round_tube() in extrusion order. +std::vector> tube_loops(const Print &print, double wall) +{ + const double centre = tube_radius - 0.5 * wall; + std::vector> out; + for (const Layer *layer : print.objects().front()->layers()) { + const Vec2d axis = get_extents(layer->lslices).center().cast(); + std::vector &loops = out.emplace_back(); + for (const LayerRegion *region : layer->regions()) + for (const ExtrusionEntity *entity : region->perimeters.flatten().entities) { + if (! entity->is_loop()) + continue; + const ExtrusionLoop *loop = static_cast(entity); + const Polygon polygon = loop->polygon(); + double radius = 0.; + for (const Point &p : polygon.points) + radius += unscaled((p.cast() - axis).norm()); + radius /= double(polygon.points.size()); + const TubeSide side = radius > centre + 0.1 ? TubeSide::Contour : radius < centre - 0.1 ? TubeSide::Hole : TubeSide::Centre; + loops.push_back({ side, loop->inset_idx, polygon.is_counter_clockwise() }); + } + } + return out; +} + +// The insets one side of a wall is printed in, given its deepest inset: inside out for Inner/Outer, outside in for +// Outer/Inner, and for Inner/Outer/Inner the third wall onwards inside out, then the outer wall, then the first inner +// wall. Inner/Outer/Inner prints its first layer Inner/Outer, and outside in when there is no third wall. +std::vector wall_sequence_insets(const std::string &wall_sequence, bool first_layer, int deepest) +{ + std::vector insets; + if (wall_sequence == "inner wall/outer wall" || (wall_sequence == "inner-outer-inner wall" && first_layer)) { + for (int inset = deepest; inset >= 0; -- inset) + insets.push_back(inset); + } else if (wall_sequence == "outer wall/inner wall" || deepest < 2) { + for (int inset = 0; inset <= deepest; ++ inset) + insets.push_back(inset); + } else { + for (int inset = deepest; inset >= 2; -- inset) + insets.push_back(inset); + insets.push_back(0); + insets.push_back(1); + } + return insets; +} + +// Whether a wall of round_tube() runs with the contour: all but the walls of the hole, and those too when the hole +// is a thin wall hole, its outer wall touching the contour's with nothing between them. +bool runs_with_contour(const TubeLoop &loop, bool thin_wall_hole) +{ + return loop.side != TubeSide::Hole || thin_wall_hole; +} + +// The insets of the walls on one side of a round_tube() layer, the shared centre line included, in extrusion order. +std::vector side_insets(const std::vector &loops, TubeSide side) +{ + std::vector insets; + for (const TubeLoop &loop : loops) + if (loop.side == side || loop.side == TubeSide::Centre) + insets.push_back(loop.inset); + return insets; +} + +// The walls of a round_tube() layer in extrusion order, as side and inset: "c0 c1 h0 h1" is the contour's outer wall, +// then its first inner wall, then the same for the hole, and "m" marks the wall in the middle both sides share. +std::string wall_order(const std::vector &loops) +{ + std::string out; + for (const TubeLoop &loop : loops) + out += std::string(out.empty() ? "" : " ") + (loop.side == TubeSide::Contour ? "c" : loop.side == TubeSide::Hole ? "h" : "m") + + std::to_string(loop.inset); + return out; +} + +} // namespace + +// Each side of a tube wall prints its walls in the order the wall sequence asks for, from two walls a side up to +// four. The inner wall Arachne lays in the middle of a wall too narrow for a pair of them is an inner wall of both +// sides; Classic fills that middle with gap fill instead, so it has one wall less a side there. +TEST_CASE("Each side of a tube wall prints its walls in the wall sequence order", "[Perimeters]") +{ + struct TubeWall { + double wall; + int wall_loops; + // Walls a side, a shared inner wall included. + int arachne_walls; + int classic_walls; + }; + const TubeWall tube = GENERATE(values({ + { 1.5, 3, 2, 1 }, { 2.0, 2, 2, 2 }, { 2.5, 3, 3, 2 }, { 3.0, 3, 3, 3 }, { 4.0, 4, 4, 4 } })); + const char *wall_generator = GENERATE("classic", "arachne"); + const std::string wall_sequence = GENERATE("inner wall/outer wall", "outer wall/inner wall", "inner-outer-inner wall"); + CAPTURE(tube.wall, tube.wall_loops, wall_generator, wall_sequence); + + DynamicPrintConfig config = tube_config(wall_generator, tube.wall_loops); + config.set_deserialize_strict("wall_sequence", wall_sequence); + Print print; + Model model; + round_tube(print, model, config, tube.wall); + const std::vector> layers = tube_loops(print, tube.wall); + // 2mm at 0.2mm layers. + REQUIRE(layers.size() == 10); + + const int walls = std::string(wall_generator) == "arachne" ? tube.arachne_walls : tube.classic_walls; + for (size_t i = 0; i < layers.size(); ++ i) { + for (const TubeSide side : { TubeSide::Contour, TubeSide::Hole }) { + const bool contour_side = side == TubeSide::Contour; + CAPTURE(i, contour_side); + const std::vector insets = side_insets(layers[i], side); + REQUIRE(int(insets.size()) == walls); + CHECK(insets == wall_sequence_insets(wall_sequence, i == 0, walls - 1)); + } + } +} + +// The outer wall of a thin wall hole touches the contour's with nothing between them. The hole is printed first, so +// the contour leans on it, unless the wall sequence puts the outer walls first. +TEST_CASE("A thin wall hole is printed before the contour unless the outer walls go first", "[Perimeters]") +{ + const char *wall_generator = GENERATE("classic", "arachne"); + const std::string wall_sequence = GENERATE("inner wall/outer wall", "outer wall/inner wall", "inner-outer-inner wall"); + CAPTURE(wall_generator, wall_sequence); + + DynamicPrintConfig config = tube_config(wall_generator, 3); + config.set_deserialize_strict("wall_sequence", wall_sequence); + Print print; + Model model; + round_tube(print, model, config, 1.); + const std::vector> layers = tube_loops(print, 1.); + REQUIRE(layers.size() == 10); + + const TubeSide first = wall_sequence == "outer wall/inner wall" ? TubeSide::Contour : TubeSide::Hole; + for (size_t i = 0; i < layers.size(); ++ i) { + CAPTURE(i); + REQUIRE(layers[i].size() == 2); + REQUIRE(layers[i][0].side != layers[i][1].side); + CHECK(layers[i].front().side == first); + } +} + +// A 1.5mm tube wall leaves Inner/Outer/Inner no third wall to sandwich, only the inner wall Arachne lays in the middle +// for both sides, or Classic's gap fill. Past the first layer it prints the hole first, then the contour, then the +// middle. +TEST_CASE("Inner/Outer/Inner prints the hole of a narrow tube wall first and the middle last", "[Perimeters]") +{ + const char *wall_generator = GENERATE("classic", "arachne"); + CAPTURE(wall_generator); + + DynamicPrintConfig config = tube_config(wall_generator, 3); + config.set_deserialize_strict("wall_sequence", "inner-outer-inner wall"); + Print print; + Model model; + round_tube(print, model, config, 1.5); + const std::vector> layers = tube_loops(print, 1.5); + REQUIRE(layers.size() == 10); + + // Classic's gap fill is not a wall loop, and goes after the walls. + const std::string expected = std::string(wall_generator) == "arachne" ? "h0 c0 m1" : "h0 c0"; + for (size_t i = 1; i < layers.size(); ++ i) { + CAPTURE(i); + CHECK(wall_order(layers[i]) == expected); + } +} + +// The contour's walls run in the wall direction and the hole's against it, except that the walls of a thin wall +// hole run with the contour, as does the inner wall Arachne shares between both sides of a 1.5mm tube wall. +TEST_CASE("Tube walls run in the wall direction and hole walls against it", "[Perimeters]") +{ + const double wall = GENERATE(1.0, 1.5, 2.0, 3.0); + const char *wall_generator = GENERATE("classic", "arachne"); + const std::string wall_direction = GENERATE("ccw", "cw"); + CAPTURE(wall, wall_generator, wall_direction); + + DynamicPrintConfig config = tube_config(wall_generator, 3); + config.set_deserialize_strict("wall_direction", wall_direction); + Print print; + Model model; + round_tube(print, model, config, wall); + const std::vector> layers = tube_loops(print, wall); + REQUIRE(layers.size() == 10); + + const bool thin_wall_hole = wall == 1.; + for (size_t i = 0; i < layers.size(); ++ i) { + CAPTURE(i); + REQUIRE(std::any_of(layers[i].begin(), layers[i].end(), [](const TubeLoop &loop) { return loop.side == TubeSide::Contour; })); + REQUIRE(std::any_of(layers[i].begin(), layers[i].end(), [](const TubeLoop &loop) { return loop.side == TubeSide::Hole; })); + for (const TubeLoop &loop : layers[i]) { + CAPTURE(int(loop.side), loop.inset); + CHECK(loop.ccw == (runs_with_contour(loop, thin_wall_hole) == (wall_direction == "ccw"))); + } + } +} + +// Reverse on even with no threshold reverses every wall of every other layer, or only the inner walls when it is +// restricted to them, and leaves the order of the walls alone. +TEST_CASE("Reverse on even reverses the tube walls of every other layer", "[Perimeters]") +{ + const double wall = GENERATE(1.0, 1.5, 3.0); + const char *wall_generator = GENERATE("classic", "arachne"); + const bool reverse_internal_only = GENERATE(false, true); + CAPTURE(wall, wall_generator, reverse_internal_only); + + auto slice = [&](bool overhang_reverse) { + DynamicPrintConfig config = tube_config(wall_generator, 3); + config.set_deserialize_strict({ + { "overhang_reverse", overhang_reverse }, + { "overhang_reverse_internal_only", reverse_internal_only }, + }); + Print print; + Model model; + round_tube(print, model, config, wall); + return tube_loops(print, wall); + }; + const std::vector> plain = slice(false); + const std::vector> reversed = slice(true); + REQUIRE(plain.size() == 10); + REQUIRE(reversed.size() == plain.size()); + + for (size_t i = 0; i < plain.size(); ++ i) { + CAPTURE(i); + REQUIRE(reversed[i].size() == plain[i].size()); + for (size_t j = 0; j < plain[i].size(); ++ j) { + CAPTURE(j); + CHECK(reversed[i][j].side == plain[i][j].side); + CHECK(reversed[i][j].inset == plain[i][j].inset); + const bool flips = i % 2 == 1 && (! reverse_internal_only || plain[i][j].inset > 0); + CHECK(reversed[i][j].ccw == (plain[i][j].ccw != flips)); + } + } +} + +namespace { + +DynamicPrintConfig brim_tube_config(const char *wall_generator, const std::string &wall_sequence, const char *brim_type, double brim_width) +{ + DynamicPrintConfig config = tube_config(wall_generator, 3); + config.set_deserialize_strict({ + { "wall_sequence", wall_sequence }, + { "brim_type", brim_type }, + { "brim_width", brim_width }, + }); + return config; +} + +} // namespace + +// A brim against the outer walls makes the first layer print them first, whatever the wall sequence, so the brim runs +// straight on into the outer wall: each side of the tube wall outside in, and the contour before a thin wall hole. +// Brims that may leave the outer walls bare keep the wall sequence, and so do the layers above. +TEST_CASE("A brim against the outer walls makes the first layer print them first", "[Perimeters]") +{ + struct Brim { + const char *type; + double width; + bool outer_walls_first; + }; + const Brim brim = GENERATE(values({ + { "no_brim", 5., false }, { "inner_only", 5., false }, { "auto_brim", 5., false }, { "outer_only", 0., false }, + { "outer_only", 5., true }, { "outer_and_inner", 5., true }, { "leading_edge_only", 5., true } })); + const double wall = GENERATE(1.0, 3.0); + const char *wall_generator = GENERATE("classic", "arachne"); + const std::string wall_sequence = GENERATE("inner wall/outer wall", "inner-outer-inner wall"); + CAPTURE(brim.type, brim.width, wall, wall_generator, wall_sequence); + + Print print; + Model model; + round_tube(print, model, brim_tube_config(wall_generator, wall_sequence, brim.type, brim.width), wall); + const std::vector> layers = tube_loops(print, wall); + REQUIRE(layers.size() == 10); + + // The thin wall hole has its outer wall a side, the 3mm tube wall three walls a side. + const size_t walls = wall == 1. ? 1 : 3; + for (const size_t i : { size_t(0), size_t(1) }) { + const bool outer_walls_first = brim.outer_walls_first && i == 0; + for (const TubeSide side : { TubeSide::Contour, TubeSide::Hole }) { + const bool contour_side = side == TubeSide::Contour; + CAPTURE(i, contour_side); + const std::vector insets = side_insets(layers[i], side); + REQUIRE(insets.size() == walls); + CHECK(insets == wall_sequence_insets(outer_walls_first ? "outer wall/inner wall" : wall_sequence, i == 0, int(walls) - 1)); + } + if (wall == 1.) + CHECK(layers[i].front().side == (outer_walls_first ? TubeSide::Contour : TubeSide::Hole)); + } +} + +// Changing the brim of a sliced print regenerates the walls when the first layer's wall order changes with it, so +// the print gets the walls a slice from scratch with the new brim gets. +TEST_CASE("Changing the brim updates the wall order of the first layer", "[Perimeters]") +{ + struct BrimChange { + const char *from_type; + double from_width; + const char *to_type; + double to_width; + }; + const BrimChange change = GENERATE(values({ + { "outer_only", 0., "outer_only", 5. }, { "outer_only", 5., "outer_only", 0. }, + { "no_brim", 5., "outer_and_inner", 5. }, { "outer_and_inner", 5., "inner_only", 5. } })); + const char *wall_generator = GENERATE("classic", "arachne"); + CAPTURE(change.from_type, change.from_width, change.to_type, change.to_width, wall_generator); + + const std::string wall_sequence = "inner wall/outer wall"; + Print print; + Model model; + round_tube(print, model, brim_tube_config(wall_generator, wall_sequence, change.from_type, change.from_width), 3.); + const std::string before = wall_order(tube_loops(print, 3.).front()); + print.apply(model, brim_tube_config(wall_generator, wall_sequence, change.to_type, change.to_width)); + print.process(); + const std::string after = wall_order(tube_loops(print, 3.).front()); + + Print fresh_print; + Model fresh_model; + round_tube(fresh_print, fresh_model, brim_tube_config(wall_generator, wall_sequence, change.to_type, change.to_width), 3.); + const std::string fresh = wall_order(tube_loops(fresh_print, 3.).front()); + + // The change does turn the first layer around. + REQUIRE(before != fresh); + CHECK(after == fresh); +} diff --git a/tests/fff_print/test_print.cpp b/tests/fff_print/test_print.cpp index dd78ef5f5f..670c2aa7c1 100644 --- a/tests/fff_print/test_print.cpp +++ b/tests/fff_print/test_print.cpp @@ -735,6 +735,65 @@ TEST_CASE("Custom G-code placeholders are substituted", "[Print]") CHECK(per_layer.find(";Layer:399 ") != std::string::npos); } +// The final print statistics are known only after the whole G-code is generated, yet every custom G-code, +// the file header included, gets the same values as filename_format. +TEST_CASE("Print statistics in custom G-code match the final print statistics", "[Print][Regression]") +{ + const std::string gcode_key = GENERATE(as{}, "file_start_gcode", "machine_start_gcode", "machine_end_gcode"); + // Bambu printers use another tag table and G-code layout. + const bool is_bbl = GENERATE(false, true); + CAPTURE(gcode_key, is_bbl); + + const std::vector number_keys = { "total_weight", "used_filament", "total_cost", "total_wipe_tower_cost", + "total_wipe_tower_filament" }; + const std::vector time_keys = { "print_time", "normal_print_time", "silent_print_time" }; + std::string templ; + for (const std::vector *keys : { &number_keys, &time_keys }) + for (const std::string &key : *keys) + templ += "; stat " + key + "={" + key + "}\n"; + + DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); + config.set_deserialize_strict({ + { gcode_key, templ }, + { "filament_density", "1.25" }, + { "filament_cost", "20" }, + { "time_cost", "2" }, + }); + Print print; + Model model; + init_print({ cube(20) }, print, model, config); + print.is_BBL_printer() = is_bbl; + + std::string gcode; + REQUIRE_NOTHROW(gcode = Slic3r::Test::gcode(print)); + + // Match whole lines only: the config block repeats the template on a single line, and Bambu printers + // write that block before the start G-code. + const std::string lines = "\n" + gcode; + const auto value_of = [&lines](const std::string &key) { + const std::string prefix = "\n; stat " + key + "="; + const size_t start = lines.find(prefix); + REQUIRE(start != std::string::npos); + const size_t begin = start + prefix.size(); + return lines.substr(begin, lines.find('\n', begin) - begin); + }; + // The same statistics filename_format is expanded with. + DynamicConfig stats = print.print_statistics().config(); + // A 20mm cube with filament_density 1.25 weighs something, unlike the running extruded_weight_total in the header. + CHECK(stats.opt_float("total_weight") > 0.); + for (const std::string &key : number_keys) { + CAPTURE(key); + const std::string value = value_of(key); + REQUIRE(value.find('@') == std::string::npos); + // Written with two decimals. + CHECK_THAT(std::stod(value), Catch::Matchers::WithinAbs(stats.opt_float(key), 0.006)); + } + for (const std::string &key : time_keys) { + CAPTURE(key); + CHECK(value_of(key) == stats.opt_string(key)); + } +} + TEST_CASE("export_gcode writes G-code without a result pointer", "[Print][export_gcode]") { Print print; diff --git a/tests/libslic3r/test_preset_bundle_loading.cpp b/tests/libslic3r/test_preset_bundle_loading.cpp index 47f5c80cca..7ea1452042 100644 --- a/tests/libslic3r/test_preset_bundle_loading.cpp +++ b/tests/libslic3r/test_preset_bundle_loading.cpp @@ -985,6 +985,37 @@ TEST_CASE("A user filament synced to the cloud and back keeps its values and uns } } +TEST_CASE("A project filament storing one value for a per-variant key applies it to every variant", "[Preset][Bundle][Regression]") +{ + PresetCollection filaments(Preset::TYPE_FILAMENT, Preset::filament_options(), + static_cast(FullPrintConfig::defaults())); + Preset &parent = add_inmemory_preset(filaments, "Parent Filament"); + parent.config.set_key_value("filament_extruder_variant", new ConfigOptionStrings({"Direct Drive Standard", "Direct Drive High Flow"})); + parent.config.set_key_value("adaptive_pressure_advance_model", new ConfigOptionStrings({"parent standard", "parent high flow"})); + parent.config.set_deserialize_strict("pressure_advance", "0.02,0.03"); + parent.is_system = true; + + // A filament saved before these keys became per-variant lists both variants but stores each key once. + const std::string model = "0.025,15.6,5000"; + DynamicPrintConfig config; + config.set_key_value(BBL_JSON_KEY_INHERITS, new ConfigOptionString("Parent Filament")); + config.set_key_value("filament_extruder_variant", new ConfigOptionStrings({"Direct Drive Standard", "Direct Drive High Flow"})); + config.set_key_value("adaptive_pressure_advance_model", new ConfigOptionStrings({model})); + config.set_deserialize_strict("pressure_advance", "0.016"); + auto embedded = std::make_unique(Preset::TYPE_FILAMENT, "Project Filament", false); + embedded->config = std::move(config); + embedded->is_project_embedded = true; + + std::vector project_presets{embedded.get()}; + PresetsConfigSubstitutions substitutions; + filaments.load_project_embedded_presets(project_presets, PRESET_FILAMENT_NAME, substitutions, ForwardCompatibilitySubstitutionRule::Disable); + + const Preset *loaded = filaments.find_preset("Project Filament"); + REQUIRE(loaded != nullptr); + CHECK(loaded->config.option("adaptive_pressure_advance_model")->values == std::vector{model, model}); + check_double_vector(loaded->config.option("pressure_advance")->values, {0.016, 0.016}); +} + TEST_CASE("Repeated loads of a user preset directory produce the same presets", "[Preset][Bundle]") { ScopedTemporaryDir temp_dir;