SPE-1804: Single perimeter for top and bottom layers working with both perimeter generators.

Co-authored-by: supermerill <merill@free.fr>
Co-authored-by: Morton Jonuschat <mjonuschat@gmail.com>
Co-authored-by: Vovodroid <vovodroid@users.noreply.github.com>
Co-authored-by: qing.zhang <qing.zhang@bambulab.com>
Co-authored-by: lane.wei <lane.wei@bambulab.com>
This commit is contained in:
Lukáš Hejl 2024-02-26 16:01:21 +01:00 committed by Lukas Matena
parent d383e59438
commit a3190d94d0
11 changed files with 224 additions and 15 deletions

View File

@ -183,6 +183,18 @@ namespace ClipperUtils {
out.end());
return out;
}
[[nodiscard]] Polygons clip_clipper_polygons_with_subject_bbox(const ExPolygons &src, const BoundingBox &bbox)
{
Polygons out;
out.reserve(number_polygons(src));
for (const ExPolygon &p : src) {
Polygons temp = clip_clipper_polygons_with_subject_bbox(p, bbox);
out.insert(out.end(), temp.begin(), temp.end());
}
out.erase(std::remove_if(out.begin(), out.end(), [](const Polygon &polygon) {return polygon.empty(); }), out.end());
return out;
}
}
static ExPolygons PolyTreeToExPolygons(ClipperLib::PolyTree &&polytree)

View File

@ -335,6 +335,7 @@ namespace ClipperUtils {
[[nodiscard]] Polygon clip_clipper_polygon_with_subject_bbox(const Polygon &src, const BoundingBox &bbox);
[[nodiscard]] Polygons clip_clipper_polygons_with_subject_bbox(const Polygons &src, const BoundingBox &bbox);
[[nodiscard]] Polygons clip_clipper_polygons_with_subject_bbox(const ExPolygon &src, const BoundingBox &bbox);
[[nodiscard]] Polygons clip_clipper_polygons_with_subject_bbox(const ExPolygons &src, const BoundingBox &bbox);
}
// offset Polygons

View File

@ -111,6 +111,7 @@ void LayerRegion::make_perimeters(
// Cummulative sum of polygons over all the regions.
const ExPolygons *lower_slices = this->layer()->lower_layer ? &this->layer()->lower_layer->lslices : nullptr;
const ExPolygons *upper_slices = this->layer()->upper_layer ? &this->layer()->upper_layer->lslices : nullptr;
// Cache for offsetted lower_slices
Polygons lower_layer_polygons_cache;
@ -124,6 +125,7 @@ void LayerRegion::make_perimeters(
params,
surface,
lower_slices,
upper_slices,
lower_layer_polygons_cache,
// output:
m_perimeters,
@ -135,6 +137,7 @@ void LayerRegion::make_perimeters(
params,
surface,
lower_slices,
upper_slices,
lower_layer_polygons_cache,
// output:
m_perimeters,

View File

@ -1083,6 +1083,7 @@ void PerimeterGenerator::process_arachne(
const Parameters &params,
const Surface &surface,
const ExPolygons *lower_slices,
const ExPolygons *upper_slices,
// Cache:
Polygons &lower_slices_polygons_cache,
// Output:
@ -1094,7 +1095,8 @@ void PerimeterGenerator::process_arachne(
ExPolygons &out_fill_expolygons)
{
// other perimeters
coord_t perimeter_spacing = params.perimeter_flow.scaled_spacing();
coord_t perimeter_width = params.perimeter_flow.scaled_width();
coord_t perimeter_spacing = params.perimeter_flow.scaled_spacing();
// external perimeters
coord_t ext_perimeter_width = params.ext_perimeter_flow.scaled_width();
coord_t ext_perimeter_spacing = params.ext_perimeter_flow.scaled_spacing();
@ -1114,12 +1116,82 @@ void PerimeterGenerator::process_arachne(
// we need to process each island separately because we might have different
// extra perimeters for each one
// detect how many perimeters must be generated for this island
int loop_number = params.config.perimeters + surface.extra_perimeters - 1; // 0-indexed loops
ExPolygons last = offset_ex(surface.expolygon.simplify_p(params.scaled_resolution), - float(ext_perimeter_width / 2. - ext_perimeter_spacing / 2.));
Polygons last_p = to_polygons(last);
int loop_number = params.config.perimeters + surface.extra_perimeters - 1; // 0-indexed loops
if (loop_number > 0 && ((params.config.top_one_perimeter_type == TopOnePerimeterType::TopmostOnly && upper_slices == nullptr) || (params.config.only_one_perimeter_first_layer && params.layer_id == 0)))
loop_number = 0;
// Calculate how many inner loops remain when TopSurfaces is selected.
const int inner_loop_number = (params.config.top_one_perimeter_type == TopOnePerimeterType::TopSurfaces && upper_slices != nullptr) ? loop_number - 1 : -1;
// Set one perimeter when TopSurfaces is selected.
if (params.config.top_one_perimeter_type == TopOnePerimeterType::TopSurfaces)
loop_number = 0;
ExPolygons last = offset_ex(surface.expolygon.simplify_p(params.scaled_resolution), - float(ext_perimeter_width / 2. - ext_perimeter_spacing / 2.));
Polygons last_p = to_polygons(last);
Arachne::WallToolPaths wall_tool_paths(last_p, ext_perimeter_spacing, perimeter_spacing, coord_t(loop_number + 1), 0, params.layer_height, params.object_config, params.print_config);
std::vector<Arachne::VariableWidthLines> perimeters = wall_tool_paths.getToolPaths();
ExPolygons infill_contour = union_ex(wall_tool_paths.getInnerContour());
// Check if there are some remaining perimeters to generate (the number of perimeters
// is greater than one together with enabled the single perimeter on top surface feature).
if (inner_loop_number >= 0) {
assert(upper_slices != nullptr);
// Infill contour bounding box.
BoundingBox infill_contour_bbox = get_extents(infill_contour);
infill_contour_bbox.offset(SCALED_EPSILON);
// Get top ExPolygons from current infill contour.
const Polygons upper_slices_clipped = ClipperUtils::clip_clipper_polygons_with_subject_bbox(*upper_slices, infill_contour_bbox);
ExPolygons top_expolygons = diff_ex(infill_contour, upper_slices_clipped);
if (!top_expolygons.empty()) {
if (lower_slices != nullptr) {
const float bridge_offset = float(std::max<coord_t>(ext_perimeter_spacing, perimeter_width));
const Polygons lower_slices_clipped = ClipperUtils::clip_clipper_polygons_with_subject_bbox(*lower_slices, infill_contour_bbox);
const ExPolygons current_slices_bridges = offset_ex(diff_ex(top_expolygons, lower_slices_clipped), bridge_offset);
// Remove bridges from top surface polygons.
top_expolygons = diff_ex(top_expolygons, current_slices_bridges);
}
// Filter out areas that are too thin and expand top surface polygons a bit to hide the wall line.
const float top_surface_min_width = std::max<float>(float(ext_perimeter_spacing) / 4.f + scaled<float>(0.00001), float(perimeter_width) / 4.f);
top_expolygons = offset2_ex(top_expolygons, -top_surface_min_width, top_surface_min_width + float(perimeter_width));
// Get the not-top ExPolygons (including bridges) from current slices and expanded real top ExPolygons (without bridges).
const ExPolygons not_top_expolygons = diff_ex(infill_contour, top_expolygons);
// Get final top ExPolygons.
top_expolygons = intersection_ex(top_expolygons, infill_contour);
const Polygons not_top_polygons = to_polygons(not_top_expolygons);
Arachne::WallToolPaths inner_wall_tool_paths(not_top_polygons, perimeter_spacing, perimeter_spacing, coord_t(inner_loop_number + 1), 0, params.layer_height, params.object_config, params.print_config);
std::vector<Arachne::VariableWidthLines> inner_perimeters = inner_wall_tool_paths.getToolPaths();
// Recalculate indexes of inner perimeters before merging them.
if (!perimeters.empty()) {
for (Arachne::VariableWidthLines &inner_perimeter : inner_perimeters) {
if (inner_perimeter.empty())
continue;
for (Arachne::ExtrusionLine &el : inner_perimeter)
++el.inset_idx;
}
}
perimeters.insert(perimeters.end(), inner_perimeters.begin(), inner_perimeters.end());
infill_contour = union_ex(top_expolygons, inner_wall_tool_paths.getInnerContour());
} else {
// There is no top surface ExPolygon, so we call Arachne again with parameters
// like when the single perimeter feature is disabled.
Arachne::WallToolPaths no_single_perimeter_tool_paths(last_p, ext_perimeter_spacing, perimeter_spacing, coord_t(inner_loop_number + 2), 0, params.layer_height, params.object_config, params.print_config);
perimeters = no_single_perimeter_tool_paths.getToolPaths();
infill_contour = union_ex(no_single_perimeter_tool_paths.getInnerContour());
}
}
Arachne::WallToolPaths wallToolPaths(last_p, ext_perimeter_spacing, perimeter_spacing, coord_t(loop_number + 1), 0, params.layer_height, params.object_config, params.print_config);
std::vector<Arachne::VariableWidthLines> perimeters = wallToolPaths.getToolPaths();
loop_number = int(perimeters.size()) - 1;
#ifdef ARACHNE_DEBUG
@ -1272,8 +1344,7 @@ void PerimeterGenerator::process_arachne(
if (ExtrusionEntityCollection extrusion_coll = traverse_extrusions(params, lower_slices_polygons_cache, ordered_extrusions); !extrusion_coll.empty())
out_loops.append(extrusion_coll);
ExPolygons infill_contour = union_ex(wallToolPaths.getInnerContour());
const coord_t spacing = (perimeters.size() == 1) ? ext_perimeter_spacing2 : perimeter_spacing;
const coord_t spacing = (perimeters.size() == 1) ? ext_perimeter_spacing2 : perimeter_spacing;
if (offset_ex(infill_contour, -float(spacing / 2.)).empty())
infill_contour.clear(); // Infill region is too small, so let's filter it out.
@ -1329,6 +1400,7 @@ void PerimeterGenerator::process_classic(
const Parameters &params,
const Surface &surface,
const ExPolygons *lower_slices,
const ExPolygons *upper_slices,
// Cache:
Polygons &lower_slices_polygons_cache,
// Output:
@ -1376,8 +1448,15 @@ void PerimeterGenerator::process_classic(
// extra perimeters for each one
// detect how many perimeters must be generated for this island
int loop_number = params.config.perimeters + surface.extra_perimeters - 1; // 0-indexed loops
ExPolygons last = union_ex(surface.expolygon.simplify_p(params.scaled_resolution));
// Set the topmost layer to be one perimeter.
if (loop_number > 0 && ((params.config.top_one_perimeter_type != TopOnePerimeterType::None && upper_slices == nullptr) || (params.config.only_one_perimeter_first_layer && params.layer_id == 0)))
loop_number = 0;
ExPolygons last = union_ex(surface.expolygon.simplify_p(params.scaled_resolution));
ExPolygons gaps;
ExPolygons top_fills;
ExPolygons fill_clip;
if (loop_number >= 0) {
// In case no perimeters are to be generated, loop_number will equal to -1.
std::vector<PerimeterGeneratorLoops> contours(loop_number+1); // depth => loops
@ -1468,6 +1547,65 @@ void PerimeterGenerator::process_classic(
}
}
last = std::move(offsets);
// Store surface for top infill if top_one_perimeter_type is set to TopSurfaces.
if (i == 0 && i != loop_number && params.config.top_one_perimeter_type == TopOnePerimeterType::TopSurfaces && upper_slices != nullptr) {
// Split the polygons with top/not_top.
// Get the offset from solid surface anchor.
const coordf_t total_perimeter_spacing = coordf_t(perimeter_spacing * (params.config.perimeters.value - 1));
const coordf_t top_surface_offset_threshold = params.config.perimeters.value <= 1 ? 0. : 0.9 * total_perimeter_spacing;
coordf_t top_surface_offset = params.config.perimeters.value == 0 ? 0. : 1.5 * coordf_t(ext_perimeter_width + total_perimeter_spacing);
// If possible, try to not push the extra perimeters inside the sparse infill.
if (top_surface_offset > top_surface_offset_threshold) {
top_surface_offset -= top_surface_offset_threshold;
} else
top_surface_offset = 0.;
// Don't take into account too thin areas.
const float top_surface_min_width = std::max<float>(float(ext_perimeter_spacing) / 2.f + scaled<float>(0.00001), float(perimeter_width));
// Current slices bounding box.
BoundingBox current_perimeters_bbox = get_extents(last);
current_perimeters_bbox.offset(SCALED_EPSILON);
ExPolygons current_slices_without_bridges;
if (lower_slices != nullptr) {
const float bridge_offset = 1.5f * float(std::max<coord_t>(ext_perimeter_spacing, perimeter_width));
const Polygons lower_slices_clipped = ClipperUtils::clip_clipper_polygons_with_subject_bbox(*lower_slices, current_perimeters_bbox);
const ExPolygons current_slices_bridges = offset_ex(diff_ex(last, lower_slices_clipped, ApplySafetyOffset::Yes), bridge_offset);
current_slices_without_bridges = diff_ex(last, current_slices_bridges, ApplySafetyOffset::Yes);
} else {
current_slices_without_bridges = last;
}
// Get top ExPolygons (including external perimeters) from current slices without bridges.
const Polygons upper_slices_clipped = expand(ClipperUtils::clip_clipper_polygons_with_subject_bbox(*upper_slices, current_perimeters_bbox), top_surface_min_width);
const ExPolygons top_polygons = diff_ex(current_slices_without_bridges, upper_slices_clipped, ApplySafetyOffset::Yes);
if (!top_polygons.empty()) {
// Set the clip to a virtual second perimeter.
fill_clip = offset_ex(last, -float(ext_perimeter_spacing));
// Get the not-top ExPolygons (including bridges) from current slices and expanded real top ExPolygons (without bridges).
const ExPolygons not_top_polygons = diff_ex(last, offset_ex(top_polygons, float(top_surface_offset) + top_surface_min_width - (float(ext_perimeter_spacing) / 2.f)), ApplySafetyOffset::Yes);
// Get difference between top ExPolygons without bridges and the area defined by the virtual second perimeter.
const ExPolygons top_gap = diff_ex(top_polygons, fill_clip);
// Get top infill surface ExPolygons (without bridges) using the difference between the area defined by the virtual second perimeter and non-top ExPolygons.
top_fills = diff_ex(fill_clip, not_top_polygons, ApplySafetyOffset::Yes);
// Set the clip to the external perimeter but go back inside by infill_extrusion_width/2 to ensure the extrusion won't go outside even with a 100% overlap.
fill_clip = offset_ex(last, float((coordf_t(ext_perimeter_spacing) / 2.) - params.config.infill_extrusion_width.get_abs_value(params.solid_infill_flow.nozzle_diameter()) / 2.));
last = intersection_ex(not_top_polygons, last);
if (has_gap_fill)
last = union_ex(last, top_gap);
}
}
if (i == loop_number && (! has_gap_fill || params.config.fill_density.value == 0)) {
// The last run of this loop is executed to collect gaps for gap fill.
// As the gap fill is either disabled or not
@ -1581,9 +1719,11 @@ void PerimeterGenerator::process_classic(
ext_perimeter_spacing / 2 :
// two or more loops?
perimeter_spacing / 2;
// only apply infill overlap if we actually have one perimeter
if (inset > 0)
inset -= coord_t(scale_(params.config.get_abs_value("infill_overlap", unscale<double>(inset + solid_infill_spacing / 2))));
// Only apply infill overlap if we actually have one perimeter.
const coord_t infill_perimeter_overlap = (inset > 0) ? coord_t(params.config.get_abs_value("infill_overlap", coordf_t(inset + solid_infill_spacing / 2.))) : 0;
inset -= infill_perimeter_overlap;
// simplify infill contours according to resolution
Polygons pp;
for (ExPolygon &ex : last)
@ -1597,6 +1737,12 @@ void PerimeterGenerator::process_classic(
float(- inset - min_perimeter_infill_spacing / 2.),
float(min_perimeter_infill_spacing / 2.));
// Apply single perimeter feature.
if (!top_fills.empty()) {
const ExPolygons top_infill_areas = intersection_ex(fill_clip, offset_ex(top_fills, float(ext_perimeter_spacing) / 2.f));
infill_areas = union_ex(infill_areas, offset_ex(top_infill_areas, float(infill_perimeter_overlap)));
}
if (lower_slices != nullptr && params.config.overhangs && params.config.extra_perimeters_on_overhangs &&
params.config.perimeters > 0 && params.layer_id > params.object_config.raft_layers) {
// Generate extra perimeters on overhang areas, and cut them to these parts only, to save print time and material

View File

@ -76,6 +76,7 @@ void process_classic(
const Parameters &params,
const Surface &surface,
const ExPolygons *lower_slices,
const ExPolygons *upper_slices,
// Cache:
Polygons &lower_slices_polygons_cache,
// Output:
@ -91,6 +92,7 @@ void process_arachne(
const Parameters &params,
const Surface &surface,
const ExPolygons *lower_slices,
const ExPolygons *upper_slices,
// Cache:
Polygons &lower_slices_polygons_cache,
// Output:

View File

@ -471,7 +471,8 @@ static std::vector<std::string> s_Preset_print_options {
"wipe_tower_width", "wipe_tower_cone_angle", "wipe_tower_rotation_angle", "wipe_tower_brim_width", "wipe_tower_bridging", "single_extruder_multi_material_priming", "mmu_segmented_region_max_width",
"mmu_segmented_region_interlocking_depth", "wipe_tower_extruder", "wipe_tower_no_sparse_layers", "wipe_tower_extra_flow", "wipe_tower_extra_spacing", "compatible_printers", "compatible_printers_condition", "inherits",
"perimeter_generator", "wall_transition_length", "wall_transition_filter_deviation", "wall_transition_angle",
"wall_distribution_count", "min_feature_size", "min_bead_width"
"wall_distribution_count", "min_feature_size", "min_bead_width",
"top_one_perimeter_type", "only_one_perimeter_first_layer",
};
static std::vector<std::string> s_Preset_filament_options {

View File

@ -257,6 +257,13 @@ static t_config_enum_values s_keys_map_PerimeterGeneratorType {
};
CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(PerimeterGeneratorType)
static t_config_enum_values s_keys_map_TopOnePerimeterType {
{ "none", int(TopOnePerimeterType::None) },
{ "top", int(TopOnePerimeterType::TopSurfaces) },
{ "topmost", int(TopOnePerimeterType::TopmostOnly) }
};
CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(TopOnePerimeterType)
static void assign_printer_technology_to_unknown(t_optiondef_map &options, PrinterTechnology printer_technology)
{
for (std::pair<const t_config_option_key, ConfigOptionDef> &kvp : options)
@ -569,6 +576,25 @@ void PrintConfigDef::init_fff_params()
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionFloat(1));
def = this->add("top_one_perimeter_type", coEnum);
def->label = L("Only one perimeter type");
def->category = L("Layers and Perimeters");
def->tooltip = L("Use only one perimeter on flat top surface, to give more space to the top infill pattern. Could be applied on topmost surface or all top surface.");
def->mode = comExpert;
def->set_enum<TopOnePerimeterType>({
{ "none", L("None surfaces") },
{ "top", L("All top surfaces") },
{ "topmost", L("Topmost surface only") }
});
def->set_default_value(new ConfigOptionEnum<TopOnePerimeterType>(TopOnePerimeterType::None));
def = this->add("only_one_perimeter_first_layer", coBool);
def->label = L("Only one perimeter on first layer");
def->category = L("Layers and Perimeters");
def->tooltip = L("Use only one perimeter on the first layer of model.");
def->mode = comExpert;
def->set_default_value(new ConfigOptionBool(false));
def = this->add("bridge_speed", coFloat);
def->label = L("Bridges");
def->category = L("Speed");

View File

@ -159,6 +159,14 @@ enum class PerimeterGeneratorType
Arachne
};
enum class TopOnePerimeterType
{
None,
TopSurfaces,
TopmostOnly,
Count
};
enum class GCodeThumbnailsFormat {
PNG, JPG, QOI
};
@ -190,7 +198,7 @@ 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)
CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(TopOnePerimeterType)
#undef CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS
@ -662,6 +670,9 @@ PRINT_CONFIG_CLASS_DEFINE(
((ConfigOptionFloat, top_solid_min_thickness))
((ConfigOptionFloatOrPercent, top_solid_infill_speed))
((ConfigOptionBool, wipe_into_infill))
// Single perimeter.
((ConfigOptionEnum<TopOnePerimeterType>, top_one_perimeter_type))
((ConfigOptionBool, only_one_perimeter_first_layer))
)
PRINT_CONFIG_CLASS_DEFINE(

View File

@ -715,7 +715,9 @@ bool PrintObject::invalidate_state_by_config_options(
|| opt_key == "perimeter_extrusion_width"
|| opt_key == "infill_overlap"
|| opt_key == "external_perimeters_first"
|| opt_key == "arc_fitting") {
|| opt_key == "arc_fitting"
|| opt_key == "top_one_perimeter_type"
|| opt_key == "only_one_perimeter_first_layer") {
steps.emplace_back(posPerimeters);
} else if (
opt_key == "gap_fill_enabled"

View File

@ -1467,6 +1467,10 @@ void TabPrint::build()
optgroup->append_single_option_line("fuzzy_skin_thickness", category_path + "fuzzy-skin-thickness");
optgroup->append_single_option_line("fuzzy_skin_point_dist", category_path + "fuzzy-skin-point-distance");
optgroup = page->new_optgroup(L("Only one perimeter"));
optgroup->append_single_option_line("top_one_perimeter_type", category_path + "top-one-perimeter-type");
optgroup->append_single_option_line("only_one_perimeter_first_layer", category_path + "only-one-perimeter-first-layer");
page = add_options_page(L("Infill"), "infill");
category_path = "infill_42#";
optgroup = page->new_optgroup(L("Infill"));

View File

@ -65,6 +65,7 @@ SCENARIO("Perimeter nesting", "[Perimeters]")
perimeter_generator_params,
surface,
nullptr,
nullptr,
// cache:
lower_layer_polygons_cache,
// output: