diff --git a/src/libslic3r/PerimeterGenerator.cpp b/src/libslic3r/PerimeterGenerator.cpp index 44d30aa789..e2b5090213 100644 --- a/src/libslic3r/PerimeterGenerator.cpp +++ b/src/libslic3r/PerimeterGenerator.cpp @@ -759,6 +759,59 @@ static void clip_inner_walls_over_top(std::vector & } } +// ORCA: only_one_wall_top - widest bead of the given walls. +static coord_t widest_bead(const std::vector &walls) +{ + coord_t widest = 0; + for (const Arachne::VariableWidthLines &group : walls) + for (const Arachne::ExtrusionLine &el : group) + for (const Arachne::ExtrusionJunction &j : el.junctions) + widest = std::max(widest, j.w); + return widest; +} + +// ORCA: only_one_wall_top - length of the walls running further than tolerance from the reference walls, outside the +// excluded area. +static double length_off_reference(const std::vector &walls, const Arachne::VariableWidthLines &reference, + const ExPolygons &excluded, coord_t tolerance) +{ + auto append_centerlines = [](const Arachne::VariableWidthLines &lines, Polylines &out) { + for (const Arachne::ExtrusionLine &el : lines) { + if (el.junctions.size() < 2) + continue; + Polyline ¢erline = out.emplace_back(); + centerline.points.reserve(el.junctions.size()); + for (const Arachne::ExtrusionJunction &j : el.junctions) + centerline.points.emplace_back(j.p); + } + }; + Polylines wall_centerlines; + Polylines reference_centerlines; + for (const Arachne::VariableWidthLines &group : walls) + append_centerlines(group, wall_centerlines); + append_centerlines(reference, reference_centerlines); + + Polylines off_reference = diff_pl(wall_centerlines, offset(reference_centerlines, float(tolerance))); + if (! excluded.empty()) + off_reference = diff_pl(off_reference, excluded); + return total_length(off_reference); +} + +// ORCA: only_one_wall_top - area covered by the given walls at their local widths. +static Polygons walls_footprint(const Arachne::VariableWidthLines &walls) +{ + Polygons footprint; + for (const Arachne::ExtrusionLine &el : walls) + for (size_t i = 1; i < el.junctions.size(); ++ i) { + const Arachne::ExtrusionJunction &a = el.junctions[i - 1]; + const Arachne::ExtrusionJunction &b = el.junctions[i]; + const coord_t width = std::max(a.w, b.w); + if (width > 0) + append(footprint, offset(Polyline(a.p, b.p), float(width) / 2.f)); + } + return union_(footprint); +} + void PerimeterGenerator::split_top_surfaces(const ExPolygons &orig_polygons, ExPolygons &top_fills, ExPolygons &non_top_polygons, ExPolygons &fill_clip) const { // other perimeters @@ -2546,44 +2599,90 @@ void PerimeterGenerator::process_arachne() 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); - coord_t perimeter_width = this->perimeter_flow.scaled_width(); - // Get top ExPolygons from current infill contour. - Polygons upper_slices_clipped; - if (object_config->interface_shells) { - auto upper_slicer_same_region = to_expolygons(this->upper_slices_same_region->surfaces); - upper_slices_clipped = ClipperUtils::clip_clipper_polygons_with_subject_bbox(upper_slicer_same_region, infill_contour_bbox); - } else - upper_slices_clipped = ClipperUtils::clip_clipper_polygons_with_subject_bbox(*upper_slices, infill_contour_bbox); + // Filter out areas that are too thin and expand top surface polygons a bit to hide the wall line. + // ORCA: skip if the top surface area is smaller than "min_width_top_surface" + const float top_surface_min_width = std::max(float(ext_perimeter_spacing) / 4.f + scaled(0.00001), float(scale_(config->min_width_top_surface.get_abs_value(unscale_(perimeter_width)))) / 4.f); - top_expolygons = diff_ex(infill_contour, upper_slices_clipped); + // Get top ExPolygons from the given contour. uncovered reports whether the upper layer leaves any of the + // contour uncovered, before bridges and too thin areas are filtered out. + auto get_top_expolygons = [&](const ExPolygons &contour, bool &uncovered) { + // Contour bounding box. + BoundingBox contour_bbox = get_extents(contour); + contour_bbox.offset(SCALED_EPSILON); + + Polygons upper_slices_clipped; + if (object_config->interface_shells) { + auto upper_slicer_same_region = to_expolygons(this->upper_slices_same_region->surfaces); + upper_slices_clipped = ClipperUtils::clip_clipper_polygons_with_subject_bbox(upper_slicer_same_region, contour_bbox); + } else + upper_slices_clipped = ClipperUtils::clip_clipper_polygons_with_subject_bbox(*upper_slices, contour_bbox); + + ExPolygons top = diff_ex(contour, upper_slices_clipped); + uncovered = !top.empty(); + if (top.empty()) + return top; - if (!top_expolygons.empty()) { if (lower_slices != nullptr) { const float bridge_offset = float(std::max(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); + const Polygons lower_slices_clipped = ClipperUtils::clip_clipper_polygons_with_subject_bbox(*lower_slices, contour_bbox); + const ExPolygons current_slices_bridges = offset_ex(diff_ex(top, lower_slices_clipped), bridge_offset); // Remove bridges from top surface polygons. - top_expolygons = diff_ex(top_expolygons, current_slices_bridges); + top = diff_ex(top, current_slices_bridges); } - // Filter out areas that are too thin and expand top surface polygons a bit to hide the wall line. - // ORCA: skip if the top surface area is smaller than "min_width_top_surface" - const float top_surface_min_width = std::max(float(ext_perimeter_spacing) / 4.f + scaled(0.00001), float(scale_(config->min_width_top_surface.get_abs_value(unscale_(perimeter_width)))) / 4.f); // Shrink the polygon to remove the small areas, then expand it back out plus a maragin to hide the wall line a little. // ORCA: Expand the polygon with half the perimeter width in addition to the contracted amount, // not the full perimeter width as PS does, to enable thin lettering to print on the top surface without nozzle collisions // due to thin lines being generated - top_expolygons = offset2_ex(top_expolygons, -top_surface_min_width, top_surface_min_width + float(perimeter_width * 0.85)); + top = offset2_ex(top, -top_surface_min_width, top_surface_min_width + float(perimeter_width * 0.85)); // Get final top ExPolygons (bridges were excluded above, so they stay walled). - top_expolygons = intersection_ex(top_expolygons, infill_contour); + return intersection_ex(top, contour); + }; + // Walls with the full count, as generated when the single perimeter feature is disabled. Generated on first use. + std::vector full_perimeters; + Polygons full_inner_contour; + bool full_perimeters_generated = false; + auto generate_full_perimeters = [&]() { + if (full_perimeters_generated) + return; + Arachne::WallToolPaths full_tool_paths(last_p, bead_width_0, perimeter_spacing, coord_t(inner_loop_number + 2), wall_0_inset, layer_height, input_params_tmp); + full_perimeters = full_tool_paths.getToolPaths(); + full_inner_contour = full_tool_paths.getInnerContour(); + full_perimeters_generated = true; + }; + + // ORCA: the single wall pass allows Arachne 2 beads across a wall, so it fills a wall narrower than 3 outer wall + // widths by widening both, where the full pass adds a middle bead. Over the top surface that is the intent; + // anywhere else it leaves no room for the inner walls. When the single wall pass's outer walls run away from + // the full pass's outside the top surface, take the full pass's outer walls and the area inside them instead. + // Walls closer than outer_wall_tolerance count as the same wall: a widened bead's centerline moves by half + // the width added, and only beads widened by more than twice the tolerance are looked for. + const coord_t outer_wall_tolerance = bead_width_0 / 10; + if (widest_bead(perimeters) > bead_width_0 + 2 * outer_wall_tolerance) { + // The single wall pass's inner contour where it widens no bead: inside nominal width outer walls. + const ExPolygons nominal_infill_contour = offset_ex(last, -float(bead_width_0 + wall_0_inset)); + bool nominal_uncovered = false; + // Grown by an outer wall width to take in the outer walls bordering the top surface. + const ExPolygons top_zone = offset_ex(get_top_expolygons(nominal_infill_contour, nominal_uncovered), float(bead_width_0)); + if (nominal_uncovered) { + generate_full_perimeters(); + if (! full_perimeters.empty() && ! full_perimeters.front().empty() && + length_off_reference(perimeters, full_perimeters.front(), top_zone, outer_wall_tolerance) > double(perimeter_width)) { + perimeters = { full_perimeters.front() }; + infill_contour = diff_ex(nominal_infill_contour, walls_footprint(full_perimeters.front()), ApplySafetyOffset::Yes); + } + } + } + + bool uncovered = false; + top_expolygons = get_top_expolygons(infill_contour, uncovered); + + if (uncovered) { // ORCA: onion the real region (inside the outer wall) so the remaining walls follow the actual // geometry, then cut away the parts over the top surface. Re-onioning the non-top complement // instead - the fallback when there is no top fill - walls the top/non-top interface and rings @@ -2612,11 +2711,11 @@ void PerimeterGenerator::process_arachne() 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, bead_width_0, perimeter_spacing, coord_t(inner_loop_number + 2), wall_0_inset, layer_height, input_params_tmp); - perimeters = no_single_perimeter_tool_paths.getToolPaths(); - infill_contour = union_ex(no_single_perimeter_tool_paths.getInnerContour()); + // There is no top surface ExPolygon, so use the walls generated like when the single perimeter + // feature is disabled. + generate_full_perimeters(); + perimeters = std::move(full_perimeters); + infill_contour = union_ex(full_inner_contour); } } //PS diff --git a/tests/fff_print/test_perimeters.cpp b/tests/fff_print/test_perimeters.cpp index 6ab3473e3c..1cbd9e9351 100644 --- a/tests/fff_print/test_perimeters.cpp +++ b/tests/fff_print/test_perimeters.cpp @@ -19,8 +19,10 @@ #include #include "libslic3r/libslic3r.h" #include "libslic3r/BoundingBox.hpp" +#include "libslic3r/ClipperUtils.hpp" #include "libslic3r/ExPolygon.hpp" #include "libslic3r/Point.hpp" +#include "libslic3r/Polyline.hpp" #include #include #include @@ -275,6 +277,124 @@ TEST_CASE("Only one wall on the first layer needs a bottom shell", "[Perimeters] namespace { +// The last layer of the tab, whose top surface shares an island with the tube walls rising past it. +const double tab_top_z = 5.0; + +// With the widths below the tube walls are 1.10mm wide once the precise outer wall offset (0.043mm a side) is +// taken off. That is narrower than 3 outer wall spacings (3 x 0.377 = 1.131mm), so an Arachne pass limited to a +// single wall fills it by widening its 2 beads, yet wide enough for the full 2 wall pass to add a middle wall +// (from 1.062mm). +const double narrow_wall = 1.186; + +// A 20x30x10 tube with narrow_wall thick walls, and a 20x8x5 tab against its -Y side. +Print &tube_with_tab(Print &print, Model &model, const DynamicPrintConfig &config) +{ + ModelObject *object = model.add_object(); + object->name = "tube_with_tab.stl"; + object->add_volume(make_cube(20., 30., 10.), ModelVolumeType::MODEL_PART, false); + // Overlaps the tube wall by 0.5mm so the two parts slice as one island. + TriangleMesh tab = make_cube(20., 8.5, 5.); + tab.translate(0.f, -8.f, 0.f); + object->add_volume(std::move(tab), ModelVolumeType::MODEL_PART, false); + TriangleMesh bore = make_cube(20. - 2. * narrow_wall, 30. - 2. * narrow_wall, 12.); + bore.translate(float(narrow_wall), float(narrow_wall), -1.f); + object->add_volume(std::move(bore), 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(); + return print; +} + +// Every width the narrow_wall arithmetic depends on, so none of them rests on a default. +DynamicPrintConfig narrow_wall_config(bool only_one_wall_top, double top_surface_expansion) +{ + DynamicPrintConfig config = base_config("arachne"); + config.set_deserialize_strict({ + { "wall_loops", 2 }, + { "nozzle_diameter", "0.4" }, + { "line_width", 0.42 }, + { "outer_wall_line_width", 0.42 }, + { "inner_wall_line_width", 0.45 }, + { "min_bead_width", "85%" }, + { "precise_outer_wall", true }, + { "wall_sequence", "inner wall/outer wall" }, + { "only_one_wall_top", only_one_wall_top }, + { "top_surface_expansion", top_surface_expansion }, + }); + return config; +} + +// Inner wall length the layer at print_z extrudes within 3mm of its +Y edge: the tube wall facing away from the tab. +double far_wall_inner_wall_length(const Print &print, double print_z) +{ + for (const Layer *layer : print.objects().front()->layers()) { + if (std::abs(layer->print_z - print_z) > EPSILON) + continue; + BoundingBox band = get_extents(layer->lslices); + band.min.y() = band.max.y() - scaled(3.); + + Polylines inner_walls; + auto collect = [&inner_walls](const ExtrusionPaths &paths) { + for (const ExtrusionPath &path : paths) + if (path.role() == erPerimeter) + inner_walls.emplace_back(path.as_polyline()); + }; + for (const LayerRegion *region : layer->regions()) { + const ExtrusionEntityCollection walls = region->perimeters.flatten(); + for (const ExtrusionEntity *entity : walls.entities) { + if (const auto *loop = dynamic_cast(entity)) + collect(loop->paths); + else if (const auto *multi_path = dynamic_cast(entity)) + collect(multi_path->paths); + else if (const auto *path = dynamic_cast(entity)) + collect({ *path }); + } + } + return unscaled(total_length(intersection_pl(inner_walls, band.polygon()))); + } + return 0.; +} + +} // namespace + +// only_one_wall_top first lays out an island with a single Arachne wall and generates the inner walls inside it. +// On a wall narrower than 3 outer wall spacings that single wall pass widens its 2 beads to fill the wall and leaves +// no room for the middle wall, which is only intended over the top surface. The tube walls away from the tab are not +// under the tab's top surface, so on the tab's last layer they keep the inner wall they get with the option off. +TEST_CASE("Only one wall on top surfaces keeps the inner walls of narrow walls away from the top surface", "[Perimeters]") +{ + // 0 re-onions the region beside the top surface, 2 clips the inner walls over it. + const double top_surface_expansion = GENERATE(0.0, 2.0); + CAPTURE(top_surface_expansion); + + struct TabTopLayer { + double perimeters; + double far_wall_inner_walls; + }; + auto tab_top_layer_for = [top_surface_expansion](bool only_one_wall_top) { + Print print; + Model model; + tube_with_tab(print, model, narrow_wall_config(only_one_wall_top, top_surface_expansion)); + print.process(); + REQUIRE_FALSE(print.objects().empty()); + return TabTopLayer{ perimeter_length_at(print, tab_top_z), far_wall_inner_wall_length(print, tab_top_z) }; + }; + + const TabTopLayer plain = tab_top_layer_for(false); + const TabTopLayer one_wall = tab_top_layer_for(true); + + // The option acts on this layer: the inner walls under the tab's top surface are gone. + REQUIRE(plain.far_wall_inner_walls > 10.); + CHECK(one_wall.perimeters < plain.perimeters); + CHECK_THAT(one_wall.far_wall_inner_walls, Catch::Matchers::WithinAbs(plain.far_wall_inner_walls, 1.0)); +} + +namespace { + // The layer that closes the cavity of box_over_cavity(), the first one printed over air. const double cavity_ceiling_z = 6.2;