From 7b0e2f3ce5f007be695ed3df82d2be142f719a79 Mon Sep 17 00:00:00 2001 From: Ian Bassi Date: Fri, 2 Oct 2026 20:19:05 -0300 Subject: [PATCH] Keep the G-code identical to main on Clipper2 Since main moved to Clipper2, parts of this branch no longer gave the same G-code as main: - bridge_over_infill dropped expand(limiting_area, 0.3 * flow.spacing()) as a no-op. The offset is below one unit, but Clipper2 still unites its result, which splits and merges touching polygons and so changes the anchor lines. Running it on the polygons next to the bridge gives main's anchors without the whole-layer pass. - tsp_remove_crossings stopped at the first repeated ordering, where main runs on to its pn * pn cap. The loop is periodic from that point, so it now takes only the steps to the ordering main stops on. - With a single tile, the tiled booleans now make the plain call instead of cutting the clip to the tile first. The tiled boolean test compared rings exactly. With the safety offset a tile unites only the clip polygons near it, and Clipper2 can then round a crossing 1 unit differently, so that case allows 1 unit. Comments that named ClipperLib now say Clipper, and docs/HLSD/polygon-clipping.md describes the tiled booleans. G-code of the five handy models in four configurations and of a baked texture relief is byte-identical to main. Colour-painted models still differ: segmenting each island on its own splits a colour's region into different pieces than one diagram over the layer, which on one model also changes the first layer's tool order. --- docs/HLSD/polygon-clipping.md | 17 ++++++++++++++ src/libslic3r/ClipperUtils.cpp | 5 ++++- src/libslic3r/ClipperUtils.hpp | 4 ++-- src/libslic3r/GCode/OrderingStrategies.cpp | 25 +++++++++++++-------- src/libslic3r/MultiMaterialSegmentation.cpp | 4 ++-- src/libslic3r/PrintObject.cpp | 11 +++++---- tests/libslic3r/test_clipper_utils.cpp | 16 +++++++++++-- 7 files changed, 60 insertions(+), 22 deletions(-) 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)); }