diff --git a/docs/HLSD/polygon-clipping.md b/docs/HLSD/polygon-clipping.md index 60bf97d515..00a1e2863b 100644 --- a/docs/HLSD/polygon-clipping.md +++ b/docs/HLSD/polygon-clipping.md @@ -62,6 +62,23 @@ Clipper2 behaves differently by default, the wrapper adjusts it. clip do not leave slivers. - Open polylines are clipped with the non-zero rule and keep their direction. +### Tiled booleans + +The sweep slows down with the number of edges crossing a scan line, so a layer +cut into thousands of pieces makes every whole-layer boolean expensive. +`diff_ex_by_piece()` and `intersection_ex_by_piece()` take a subject of +non-overlapping `ExPolygons`, group them into tiles with +`ClipperUtils::tile_expolygons()`, and run each tile in parallel against only +the clip polygons near it, cut to the tile's box. Below 128 pieces there is a +single tile, and they are the plain `diff_ex()` / `intersection_ex()`. + +The result covers the same area as the plain call. Without the safety offset +the rings are the same. With it, each tile unites only the clip polygons near +it, so a clip edge that the whole-layer union splits where it crosses a distant +clip polygon stays whole, and a crossing with the subject can round 1 unit +differently. The tiles' results are concatenated in tile order, so the order of +the output `ExPolygons` differs from the plain call. + ### Offsets - Before offsetting, input vertices closer than diff --git a/src/libslic3r/ClipperUtils.cpp b/src/libslic3r/ClipperUtils.cpp index 1893e192c2..8770603784 100644 --- a/src/libslic3r/ClipperUtils.cpp +++ b/src/libslic3r/ClipperUtils.cpp @@ -819,8 +819,11 @@ namespace ClipperUtils { static Slic3r::ExPolygons clipper_ex_by_piece(ClipType clipType, const Slic3r::ExPolygons &subject, const Slic3r::Polygons &clip, ApplySafetyOffset do_safety_offset) { - // A few dozen subject ExPolygons to a tile, each tile one ClipperLib call with the clip cut to the tile's box. + // A few dozen subject ExPolygons to a tile, each tile one Clipper call with the clip cut to the tile's box. const std::vector tiles = ClipperUtils::tile_expolygons(subject, 32); + // One tile is the plain call: cutting the clip would only cost time. + if (tiles.size() <= 1) + return _clipper_ex(clipType, ClipperUtils::ExPolygonsProvider(subject), ClipperUtils::PolygonsProvider(clip), do_safety_offset); std::vector clip_bboxes; clip_bboxes.reserve(clip.size()); for (const Polygon &polygon : clip) diff --git a/src/libslic3r/ClipperUtils.hpp b/src/libslic3r/ClipperUtils.hpp index 44497eb2db..52e0abab9c 100644 --- a/src/libslic3r/ClipperUtils.hpp +++ b/src/libslic3r/ClipperUtils.hpp @@ -331,7 +331,7 @@ namespace ClipperUtils { [[nodiscard]] Polygons clip_clipper_polygons_with_subject_bbox(const ExPolygon &src, const BoundingBox &bbox, const bool get_entire_polygons = false); [[nodiscard]] Polygons clip_clipper_polygons_with_subject_bbox(const ExPolygons &src, const BoundingBox &bbox, const bool get_entire_polygons = false); - // Splits ExPolygons into tiles by the centres of their boxes, about `per_tile` of them to a tile, to run ClipperLib on a + // Splits ExPolygons into tiles by the centres of their boxes, about `per_tile` of them to a tile, to run Clipper on a // layer of many pieces tile by tile. Returns the non-empty tiles, each with the indices of its ExPolygons and their box. struct ExPolygonsTile { @@ -537,7 +537,7 @@ Slic3r::ExPolygons intersection_ex(const Slic3r::Surfaces &subject, const Slic3r Slic3r::ExPolygons intersection_ex(const Slic3r::SurfacesPtr &subject, const Slic3r::ExPolygons &clip, ApplySafetyOffset do_safety_offset = ApplySafetyOffset::No); // diff_ex() / intersection_ex() of the subject split into tiles, each against only the part of the clip near it, the tiles in // parallel. The same area as the operation on the whole subject when its ExPolygons do not overlap, and much faster for a -// subject of thousands of pieces spread over a layer: ClipperLib slows down with the number of edges crossing a scan line. +// subject of thousands of pieces spread over a layer: Clipper slows down with the number of edges crossing a scan line. Slic3r::ExPolygons diff_ex_by_piece(const Slic3r::ExPolygons &subject, const Slic3r::Polygons &clip, ApplySafetyOffset do_safety_offset = ApplySafetyOffset::No); Slic3r::ExPolygons intersection_ex_by_piece(const Slic3r::ExPolygons &subject, const Slic3r::Polygons &clip, ApplySafetyOffset do_safety_offset = ApplySafetyOffset::No); Slic3r::Polylines intersection_pl(const Slic3r::Polylines &subject, const Slic3r::Polygon &clip); diff --git a/src/libslic3r/GCode/OrderingStrategies.cpp b/src/libslic3r/GCode/OrderingStrategies.cpp index 69a19954c3..ba299dd76b 100644 --- a/src/libslic3r/GCode/OrderingStrategies.cpp +++ b/src/libslic3r/GCode/OrderingStrategies.cpp @@ -10,7 +10,6 @@ #include #include #include -#include #include #include @@ -188,26 +187,34 @@ bool tsp_remove_crossings(std::vector& path, const Points& centers) // Process crossings one at a time: find first, reverse it, restart scan. // Cap iterations to prevent infinite loops on collinear/overlapping segments. - int max_iters = static_cast(pn * pn); + const int max_iters = static_cast(pn * pn); bool improved = false; // Reversing between two segments that only touch or overlap along a line need not remove the intersection, so on // islands laid out on a regular grid (a tiled texture, an array of parts) the loop can cycle through the same - // orderings until the pn * pn cap. Stop as soon as an ordering repeats; up to that point this is the same loop. - std::unordered_set seen_paths; + // orderings until the pn * pn cap. Once an ordering repeats the rest of the loop is periodic, so only the steps + // to the ordering the capped loop would have stopped on are taken. + std::unordered_map seen_paths; // path hash -> reversals done when it was reached const auto path_hash = [&path]() { uint64_t h = 1469598103934665603ull; // FNV-1a for (size_t idx : path) h = (h ^ uint64_t(idx)) * 1099511628211ull; return h; }; - seen_paths.insert(path_hash()); - while (max_iters-- > 0) { + const auto reverse_first_crossing = [&]() { auto [ci, cj] = pn >= grid_min_size ? find_crossing_grid() : find_crossing(); - if (ci == std::numeric_limits::max()) break; - improved = true; + if (ci == std::numeric_limits::max()) + return false; std::reverse(path.begin() + ci + 1, path.begin() + cj + 1); - if (!seen_paths.insert(path_hash()).second) + return true; + }; + seen_paths.emplace(path_hash(), 0); + for (int iter = 1; iter <= max_iters && reverse_first_crossing(); ++iter) { + improved = true; + if (auto [it, inserted] = seen_paths.emplace(path_hash(), iter); !inserted) { + for (int steps = (max_iters - iter) % (iter - it->second); steps > 0; --steps) + reverse_first_crossing(); break; + } } return improved; } diff --git a/src/libslic3r/MultiMaterialSegmentation.cpp b/src/libslic3r/MultiMaterialSegmentation.cpp index 9bce9a6145..7eba251df6 100644 --- a/src/libslic3r/MultiMaterialSegmentation.cpp +++ b/src/libslic3r/MultiMaterialSegmentation.cpp @@ -1403,7 +1403,7 @@ static inline std::vector> segmentation_top_and_bottom_l // `shell_layers`, nearest first), one more perimeter in on each, stopping at the first layer where nothing is left. // Only the slices within the deepest offset of `ex` (three times that with the miter joins) decide the result, so the // work is done per tile of `ex`'s ExPolygons on the slices cut to the tile's box grown by that much: the same result, but - // each ClipperLib call stays the size of a tile rather than of a layer cut through a fine relief, and the tiles run in + // each Clipper call stays the size of a tile rather than of a layer cut through a fine relief, and the tiles run in // parallel. const auto project_to_shells = [&input_expolygons](const ExPolygons &ex, size_t layer_idx, const std::vector &shell_layers, const LayerColorStat &stat, ShellProjections &dst) { @@ -1965,7 +1965,7 @@ static std::vector> merge_segmented_layers(const std::ve BOOST_LOG_TRIVIAL(debug) << "Print object segmentation - Merging segmented layers in parallel - Begin"; // Every region of a layer is merged together with the regions of the islands it overlaps, and the islands are further // apart than the dimple removal below reaches, so this gives the same result as merging the layer at once. On a layer - // cut through a fine relief every region shares thousands of hole contours with every other, and ClipperLib, splitting + // cut through a fine relief every region shares thousands of hole contours with every other, and Clipper, splitting // and re-linking one huge polygon over and over, took anything up to half an hour for a layer; per island each operation // stays the size of the island, and the islands run in parallel. tbb::parallel_for(tbb::blocked_range(0, num_layers), [&](const tbb::blocked_range &range) { diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index f530bb13c0..1b1b44afcd 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -3531,7 +3531,7 @@ void PrintObject::bridge_over_infill() std::map boundary_by_spacing; // expansion_area is a clean, non-overlapping set, so uniting it with a bridge or cutting a bridge // out of it only changes the polygons near that bridge. The rest are passed through untouched - // instead of being fed to ClipperLib with the whole layer again for every candidate. + // instead of being fed to Clipper with the whole layer again for every candidate. // Not `near`/`far`: the Windows headers still define those as macros, and they expand to // nothing, which turns the declaration below into an empty one. const auto split_near = [](const Polygons &polys, const BoundingBox &bbox, Polygons &rest) { @@ -3568,6 +3568,7 @@ void PrintObject::bridge_over_infill() Polygons limiting_area; const Polygons near_expansion = split_near(expansion_area, get_extents(area_to_be_bridge).inflated(SCALED_EPSILON), limiting_area); + const size_t num_far = limiting_area.size(); append(limiting_area, union_(area_to_be_bridge, near_expansion)); auto boundary_it = boundary_by_spacing.find(flow.scaled_spacing()); @@ -3577,11 +3578,9 @@ void PrintObject::bridge_over_infill() .first; Polylines boundary_plines = boundary_it->second; { - // No offset here: flow.spacing() is in mm, so the expand(limiting_area, 0.3 * flow.spacing()) - // this used to be moved the outline by 0.135 scaled units - nothing beyond rounding - while - // costing a whole-layer ClipperLib pass for every candidate. limiting_area is already a clean - // union, so its own outline is the same boundary. - Polylines limiting_plines = to_polylines(limiting_area); + // The sub-unit offset (spacing is in mm) still re-unites touching polygons by the bridge, which the anchors depend on. + Polylines limiting_plines = to_polylines(Polygons(limiting_area.begin(), limiting_area.begin() + num_far)); + append(limiting_plines, to_polylines(expand(Polygons(limiting_area.begin() + num_far, limiting_area.end()), 0.3 * flow.spacing()))); boundary_plines.insert(boundary_plines.end(), limiting_plines.begin(), limiting_plines.end()); } diff --git a/tests/libslic3r/test_clipper_utils.cpp b/tests/libslic3r/test_clipper_utils.cpp index 7090628057..889258f7e4 100644 --- a/tests/libslic3r/test_clipper_utils.cpp +++ b/tests/libslic3r/test_clipper_utils.cpp @@ -325,6 +325,15 @@ static std::vector> canonical_rings(const ExPolygons &expol return rings; } +// The same rings, every coordinate within `tolerance`. +static bool same_rings(const ExPolygons &a, const ExPolygons &b, coord_t tolerance) +{ + const std::vector> ra = canonical_rings(a), rb = canonical_rings(b); + return std::equal(ra.begin(), ra.end(), rb.begin(), rb.end(), [tolerance](const std::vector &x, const std::vector &y) { + return std::equal(x.begin(), x.end(), y.begin(), y.end(), [tolerance](coord_t u, coord_t v) { return std::abs(u - v) <= tolerance; }); + }); +} + TEST_CASE("Tiled diff and intersection return the same polygons as the plain calls", "[ClipperUtils]") { // A grid of disjoint framed squares, enough of them to be split into several tiles. const int n = 40; @@ -358,13 +367,16 @@ TEST_CASE("Tiled diff and intersection return the same polygons as the plain cal // path under test is never taken. REQUIRE(ClipperUtils::tile_expolygons(subject, 32).size() > 1); + // With the safety offset a tile unites fewer clip polygons, so Clipper2 can round a crossing 1 unit differently. + const coord_t tolerance = safety == ApplySafetyOffset::Yes ? 1 : 0; + const ExPolygons diff_plain = diff_ex(subject, clip, safety); const ExPolygons diff_tiled = diff_ex_by_piece(subject, clip, safety); REQUIRE(area(diff_plain) > 0.); - CHECK(canonical_rings(diff_tiled) == canonical_rings(diff_plain)); + CHECK(same_rings(diff_tiled, diff_plain, tolerance)); const ExPolygons intersection_plain = intersection_ex(subject, clip, safety); const ExPolygons intersection_tiled = intersection_ex_by_piece(subject, clip, safety); REQUIRE(area(intersection_plain) > 0.); - CHECK(canonical_rings(intersection_tiled) == canonical_rings(intersection_plain)); + CHECK(same_rings(intersection_tiled, intersection_plain, tolerance)); }